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 @@ -8,6 +8,7 @@ import { freshMessageStore } from 'lib/reducers/message-reducer.js'; import { mostRecentlyReadThread } from 'lib/selectors/thread-selectors.js'; import { mostRecentMessageTimestamp } from 'lib/shared/message-utils.js'; +import { getThreadHashesFromThreadInfos } from 'lib/shared/thread-store-utils.js'; import { threadHasPermission, threadIsPending, @@ -33,6 +34,7 @@ } from 'lib/types/user-types.js'; import { currentDateInTimeZone } from 'lib/utils/date-utils.js'; import { ServerError } from 'lib/utils/errors.js'; +import { values } from 'lib/utils/objects.js'; import { promiseAll } from 'lib/utils/promises.js'; import type { URLInfo } from 'lib/utils/url-utils.js'; import { tShape, ashoatKeyserverID } from 'lib/utils/validation-utils.js'; @@ -149,7 +151,13 @@ threadInfoPromise, hasNotAcknowledgedPoliciesPromise, ]); - return { threadInfos: hasNotAcknowledgedPolicies ? {} : threadInfos }; + if (hasNotAcknowledgedPolicies) { + return { threadInfos: {}, threadHashes: {} }; + } + return { + threadInfos, + threadHashes: getThreadHashesFromThreadInfos(values(threadInfos), false), + }; })(); const messageStorePromise = (async () => { const [ diff --git a/lib/ops/thread-store-ops.js b/lib/ops/thread-store-ops.js --- a/lib/ops/thread-store-ops.js +++ b/lib/ops/thread-store-ops.js @@ -1,6 +1,7 @@ // @flow import { type BaseStoreOpsHandlers } from './base-ops.js'; +import { getThreadHashFromRawThreadInfo } from '../shared/thread-store-utils.js'; import type { ClientDBThreadInfo, RawThreadInfo, @@ -55,18 +56,29 @@ return store; } let processedThreads = { ...store.threadInfos }; + let processedHashes = { ...store.threadHashes }; for (const operation of ops) { if (operation.type === 'replace') { processedThreads[operation.payload.id] = operation.payload.threadInfo; + processedHashes[operation.payload.id] = getThreadHashFromRawThreadInfo( + operation.payload.threadInfo, + true, + ); } else if (operation.type === 'remove') { for (const id of operation.payload.ids) { delete processedThreads[id]; + delete processedHashes[id]; } } else if (operation.type === 'remove_all') { processedThreads = {}; + processedHashes = {}; } } - return { ...store, threadInfos: processedThreads }; + return { + ...store, + threadInfos: processedThreads, + threadHashes: processedHashes, + }; }, convertOpsToClientDBOps( diff --git a/lib/shared/thread-store-utils.js b/lib/shared/thread-store-utils.js new file mode 100644 --- /dev/null +++ b/lib/shared/thread-store-utils.js @@ -0,0 +1,40 @@ +// @flow + +import type { RawThreadInfo } from '../types/thread-types.js'; +import { rawThreadInfoValidator } from '../types/thread-types.js'; +import { convertClientIDsToServerIDs } from '../utils/conversion-utils.js'; +import { hash } from '../utils/objects.js'; +import { ashoatKeyserverID } from '../utils/validation-utils.js'; + +function getThreadHashFromRawThreadInfo( + threadInfo: RawThreadInfo, + shouldConvert: boolean, +): number { + let threadInfoToHash = threadInfo; + if (shouldConvert) { + threadInfoToHash = convertClientIDsToServerIDs( + ashoatKeyserverID, + rawThreadInfoValidator, + threadInfoToHash, + ); + } + return hash(threadInfoToHash); +} + +function getThreadHashesFromThreadInfos( + threadInfos: $ReadOnlyArray, + shouldConvert: boolean, +): { +[string]: number } { + const threadHashes: { [string]: number } = {}; + + for (const threadInfo of threadInfos) { + threadHashes[threadInfo.id] = getThreadHashFromRawThreadInfo( + threadInfo, + shouldConvert, + ); + } + + return threadHashes; +} + +export { getThreadHashFromRawThreadInfo, getThreadHashesFromThreadInfos }; diff --git a/lib/types/thread-types.js b/lib/types/thread-types.js --- a/lib/types/thread-types.js +++ b/lib/types/thread-types.js @@ -215,10 +215,12 @@ export type ThreadStore = { +threadInfos: { +[id: string]: RawThreadInfo }, + +threadHashes: { +[id: string]: number }, }; export const threadStoreValidator: TInterface = tShape({ threadInfos: t.dict(tID, rawThreadInfoValidator), + threadHashes: t.dict(tID, t.Number), }); export type ClientDBThreadInfo = { diff --git a/native/redux/default-state.js b/native/redux/default-state.js --- a/native/redux/default-state.js +++ b/native/redux/default-state.js @@ -29,6 +29,7 @@ }, threadStore: { threadInfos: {}, + threadHashes: {}, }, userStore: { userInfos: {}, diff --git a/native/redux/persist.js b/native/redux/persist.js --- a/native/redux/persist.js +++ b/native/redux/persist.js @@ -30,6 +30,7 @@ import { highestLocalIDSelector } from 'lib/selectors/local-id-selectors.js'; import { createAsyncMigrate } from 'lib/shared/create-async-migrate.js'; import { inconsistencyResponsesToReports } from 'lib/shared/report-utils.js'; +import { getThreadHashesFromThreadInfos } from 'lib/shared/thread-store-utils.js'; import { getContainingThreadID, getCommunity, @@ -58,7 +59,11 @@ defaultConnectionInfo, type ConnectionInfo, } from 'lib/types/socket-types.js'; -import type { ClientDBThreadInfo } from 'lib/types/thread-types.js'; +import type { + ClientDBThreadInfo, + RawThreadInfos, + ThreadStore, +} from 'lib/types/thread-types.js'; import { translateClientDBMessageInfoToRawMessageInfo, translateRawMessageInfoToClientDBMessageInfo, @@ -776,6 +781,20 @@ }, }; }, + [52]: async state => { + const clientDBThreadInfos = commCoreModule.getAllThreadsSync(); + const rawThreadInfos = clientDBThreadInfos.map( + convertClientDBThreadInfoToRawThreadInfo, + ); + + return { + ...state, + threadStore: { + ...state.threadStore, + threadHashes: getThreadHashesFromThreadInfos(rawThreadInfos, true), + }, + }; + }, }; // After migration 31, we'll no longer want to persist `messageStore.messages` @@ -891,6 +910,33 @@ { whitelist: ['keyserverStore'] }, ); +function transformAfterVersion( + version: number, + inbound: State => PersistState, +) { + return (state, key, wholeState) => { + if (wholeState._persist?.version > version) { + return state; + } + return (inbound(state): any); + }; +} + +type PersistedThreadStore = $Diff< + ThreadStore, + { +threadInfos: RawThreadInfos }, +>; +const threadStoreTransform: Transform = createTransform( + (state: ThreadStore): PersistedThreadStore => { + const { threadInfos, ...rest } = state; + return rest; + }, + transformAfterVersion(51, (state: PersistedThreadStore): ThreadStore => { + return { ...state, threadInfos: {} }; + }), + { whitelist: ['threadStore'] }, +); + const persistConfig = { key: 'root', storage: AsyncStorage, @@ -902,16 +948,16 @@ 'connectivity', 'deviceOrientation', 'frozen', - 'threadStore', 'storeLoaded', 'connection', ], debug: __DEV__, - version: 51, + version: 52, transforms: [ messageStoreMessagesBlocklistTransform, reportStoreTransform, keyserverStoreTransform, + threadStoreTransform, ], migrate: (createAsyncMigrate(migrations, { debug: __DEV__ }): any), timeout: ((__DEV__ ? 0 : undefined): number | void), diff --git a/web/redux/default-state.js b/web/redux/default-state.js --- a/web/redux/default-state.js +++ b/web/redux/default-state.js @@ -25,6 +25,7 @@ }, threadStore: { threadInfos: {}, + threadHashes: {}, }, userStore: { userInfos: {},