diff --git a/lib/actions/integrity-actions.js b/lib/actions/integrity-actions.js new file mode 100644 --- /dev/null +++ b/lib/actions/integrity-actions.js @@ -0,0 +1,5 @@ +// @flow + +const updateIntegrityStoreActionType = 'UPDATE_INTEGRITY_STORE'; + +export { updateIntegrityStoreActionType }; diff --git a/lib/components/integrity-handler.react.js b/lib/components/integrity-handler.react.js new file mode 100644 --- /dev/null +++ b/lib/components/integrity-handler.react.js @@ -0,0 +1,68 @@ +// @flow + +import * as React from 'react'; +import { useDispatch } from 'react-redux'; + +import { updateIntegrityStoreActionType } from '../actions/integrity-actions.js'; +import type { RawThreadInfo } from '../types/thread-types.js'; +import { useSelector } from '../utils/redux-utils.js'; + +const BATCH_SIZE = 50; +function calculateThreadBatches(threadInfos: { + +[id: string]: RawThreadInfo, +}): string[][] { + const threadIDs = Object.keys(threadInfos); + const batches = []; + + let i = 0; + while (i < threadIDs.length) { + batches.push(threadIDs.slice(i, i + BATCH_SIZE)); + i += BATCH_SIZE; + } + + return batches; +} + +function IntegrityHandler(): null { + const dispatch = useDispatch(); + + const threadInfos = useSelector(state => state.threadStore.threadInfos); + const integrityStore = useSelector(state => state.integrityStore); + + const [batches, setBatches] = React.useState(null); + const timeout = React.useRef(null); + + React.useEffect(() => { + if (!integrityStore.threadHashingComplete) { + clearTimeout(timeout.current); + setBatches(calculateThreadBatches(threadInfos)); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [integrityStore.threadHashingComplete]); + + React.useEffect(() => { + if (!batches) { + return; + } + const [batch, ...rest] = batches; + if (!batch) { + dispatch({ + type: updateIntegrityStoreActionType, + payload: { threadHashingComplete: true }, + }); + return; + } + + dispatch({ + type: updateIntegrityStoreActionType, + payload: { threadIDsToHash: batch }, + }); + + timeout.current = setTimeout(() => setBatches(rest), 500); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [batches]); + + return null; +} + +export default IntegrityHandler; diff --git a/lib/reducers/integrity-reducer.js b/lib/reducers/integrity-reducer.js new file mode 100644 --- /dev/null +++ b/lib/reducers/integrity-reducer.js @@ -0,0 +1,76 @@ +// @flow + +import { updateIntegrityStoreActionType } from '../actions/integrity-actions.js'; +import { siweAuthActionTypes } from '../actions/siwe-actions.js'; +import { + logInActionTypes, + registerActionTypes, +} from '../actions/user-actions.js'; +import type { ThreadStoreOperation } from '../ops/thread-store-ops'; +import type { IntegrityStore } from '../types/integrity-types'; +import type { BaseAction } from '../types/redux-types.js'; +import { fullStateSyncActionType } from '../types/socket-types.js'; +import type { RawThreadInfo } from '../types/thread-types.js'; +import { hash } from '../utils/objects.js'; + +function reduceIntegrityStore( + state: IntegrityStore, + action: BaseAction, + threadInfos: { +[string]: RawThreadInfo }, + threadStoreOperations: $ReadOnlyArray, +): IntegrityStore { + if ( + action.type === logInActionTypes.success || + action.type === siweAuthActionTypes.success || + action.type === registerActionTypes.success || + action.type === fullStateSyncActionType + ) { + return { threadHashes: {}, threadHashingComplete: false }; + } + if (action.type === updateIntegrityStoreActionType) { + let newState = state; + if (action.payload.threadIDsToHash) { + const newThreadHashes = Object.fromEntries( + action.payload.threadIDsToHash + .map(id => [id, threadInfos[id]]) + .filter(([, info]) => !!info) + .map(([id, info]) => [id, hash(info)]), + ); + + newState = { + ...newState, + threadHashes: { + ...newState.threadHashes, + ...newThreadHashes, + }, + }; + } + if (action.payload.threadHashingComplete) { + newState = { + ...newState, + threadHashingComplete: action.payload.threadHashingComplete, + }; + } + return newState; + } + if (threadStoreOperations.length === 0) { + return state; + } + let processedThreadHashes = { ...state.threadHashes }; + for (const operation of threadStoreOperations) { + if (operation.type === 'replace') { + processedThreadHashes[operation.payload.id] = hash( + operation.payload.threadInfo, + ); + } else if (operation.type === 'remove') { + for (const id of operation.payload.ids) { + delete processedThreadHashes[id]; + } + } else if (operation.type === 'remove_all') { + processedThreadHashes = {}; + } + } + return { ...state, threadHashes: processedThreadHashes }; +} + +export { reduceIntegrityStore }; diff --git a/lib/reducers/master-reducer.js b/lib/reducers/master-reducer.js --- a/lib/reducers/master-reducer.js +++ b/lib/reducers/master-reducer.js @@ -7,6 +7,7 @@ import { reduceDraftStore } from './draft-reducer.js'; import reduceEnabledApps from './enabled-apps-reducer.js'; import { reduceEntryInfos } from './entry-reducer.js'; +import { reduceIntegrityStore } from './integrity-reducer.js'; import reduceInviteLinks from './invite-links-reducer.js'; import reduceKeyserverStore from './keyserver-reducer.js'; import reduceLifecycleState from './lifecycle-state-reducer.js'; @@ -150,6 +151,12 @@ state.threadActivityStore, action, ), + integrityStore: reduceIntegrityStore( + state.integrityStore, + action, + threadStore.threadInfos, + threadStoreOperations, + ), }, storeOperations: { draftStoreOperations, diff --git a/lib/types/integrity-types.js b/lib/types/integrity-types.js new file mode 100644 --- /dev/null +++ b/lib/types/integrity-types.js @@ -0,0 +1,6 @@ +// @flow + +export type IntegrityStore = { + +threadHashes: { +[string]: number }, + +threadHashingComplete: boolean, +}; diff --git a/lib/types/redux-types.js b/lib/types/redux-types.js --- a/lib/types/redux-types.js +++ b/lib/types/redux-types.js @@ -38,6 +38,7 @@ CalendarThreadFilter, SetCalendarDeletedFilterPayload, } from './filter-types.js'; +import type { IntegrityStore } from './integrity-types.js'; import type { KeyserverStore } from './keyserver-types.js'; import type { LifecycleState } from './lifecycle-state-types.js'; import type { @@ -139,6 +140,7 @@ +inviteLinksStore: InviteLinksStore, +keyserverStore: KeyserverStore, +threadActivityStore: ThreadActivityStore, + +integrityStore: IntegrityStore, ... }; @@ -1217,6 +1219,13 @@ | { +type: 'UPDATE_THREAD_LAST_NAVIGATED', +payload: { +threadID: string, +time: number }, + } + | { + +type: 'UPDATE_INTEGRITY_STORE', + payload: { + threadIDsToHash?: $ReadOnlyArray, + threadHashingComplete?: boolean, + }, }; export type ActionPayload = ?(Object | Array<*> | $ReadOnlyArray<*> | string); 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 @@ -86,6 +86,7 @@ isBackupEnabled: false, }, threadActivityStore: {}, + integrityStore: { threadHashes: {}, threadHashingComplete: false }, }: AppState); export { defaultState }; diff --git a/native/redux/state-types.js b/native/redux/state-types.js --- a/native/redux/state-types.js +++ b/native/redux/state-types.js @@ -7,6 +7,7 @@ import type { EnabledApps } from 'lib/types/enabled-apps.js'; import type { EntryStore, CalendarQuery } 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'; @@ -60,4 +61,5 @@ +keyserverStore: KeyserverStore, +threadActivityStore: ThreadActivityStore, +localSettings: LocalSettings, + +integrityStore: IntegrityStore, }; diff --git a/native/root.react.js b/native/root.react.js --- a/native/root.react.js +++ b/native/root.react.js @@ -21,6 +21,7 @@ import { EditUserAvatarProvider } from 'lib/components/edit-user-avatar-provider.react.js'; import { ENSCacheProvider } from 'lib/components/ens-cache-provider.react.js'; +import IntegrityHandler from 'lib/components/integrity-handler.react.js'; import { MediaCacheProvider } from 'lib/components/media-cache-provider.react.js'; import { actionLogger } from 'lib/utils/action-logger.js'; @@ -242,6 +243,7 @@ + ); let navigation; 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 @@ -89,6 +89,7 @@ }, threadActivityStore: {}, initialStateLoaded: false, + integrityStore: { threadHashes: {}, threadHashingComplete: false }, }); export { defaultWebState }; diff --git a/web/redux/redux-setup.js b/web/redux/redux-setup.js --- a/web/redux/redux-setup.js +++ b/web/redux/redux-setup.js @@ -22,6 +22,7 @@ import type { EnabledApps } from 'lib/types/enabled-apps.js'; import type { EntryStore, CalendarQuery } 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'; @@ -100,6 +101,7 @@ +keyserverStore: KeyserverStore, +threadActivityStore: ThreadActivityStore, +initialStateLoaded: boolean, + +integrityStore: IntegrityStore, }; export type Action = diff --git a/web/root.js b/web/root.js --- a/web/root.js +++ b/web/root.js @@ -9,6 +9,7 @@ import { persistReducer, persistStore } from 'redux-persist'; import thunk from 'redux-thunk'; +import IntegrityHandler from 'lib/components/integrity-handler.react.js'; import { reduxLoggerMiddleware } from 'lib/utils/action-logger.js'; import App from './app.react.js'; @@ -41,6 +42,7 @@ +