diff --git a/lib/reducers/master-reducer.js b/lib/reducers/master-reducer.js index 8d713f893..a8e41f02b 100644 --- a/lib/reducers/master-reducer.js +++ b/lib/reducers/master-reducer.js @@ -1,237 +1,239 @@ // @flow import { reduceAlertStore } from './alert-reducer.js'; import { reduceAuxUserStore } from './aux-user-reducer.js'; import reduceCalendarFilters from './calendar-filters-reducer.js'; import { reduceCommunityStore } from './community-reducer.js'; import reduceCustomerServer from './custom-server-reducer.js'; import reduceDataLoaded from './data-loaded-reducer.js'; import { reduceDBOpsStore } from './db-ops-reducer.js'; 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'; import { reduceLoadingStatuses } from './loading-reducer.js'; import { reduceMessageStore } from './message-reducer.js'; import reduceBaseNavInfo from './nav-reducer.js'; import policiesReducer from './policies-reducer.js'; import reduceReportStore from './report-store-reducer.js'; import reduceServicesAccessToken from './services-access-token-reducer.js'; import { reduceSyncedMetadataStore } from './synced-metadata-reducer.js'; import reduceGlobalThemeInfo from './theme-reducer.js'; import { reduceThreadActivity } from './thread-activity-reducer.js'; import { reduceThreadInfos } from './thread-reducer.js'; import { reduceCurrentUserInfo, reduceUserInfos } from './user-reducer.js'; import { addKeyserverActionType } from '../actions/keyserver-actions.js'; import { siweAuthActionTypes } from '../actions/siwe-actions.js'; import { keyserverRegisterActionTypes, logInActionTypes, keyserverAuthActionTypes, } from '../actions/user-actions.js'; import { keyserverStoreOpsHandlers, type ReplaceKeyserverOperation, } from '../ops/keyserver-store-ops.js'; import { isStaff } from '../shared/staff-utils.js'; import type { BaseNavInfo } from '../types/nav-types.js'; import type { BaseAppState, BaseAction } from '../types/redux-types.js'; import { fullStateSyncActionType, incrementalStateSyncActionType, } from '../types/socket-types.js'; import type { StoreOperations } from '../types/store-ops-types.js'; import { isDev } from '../utils/dev-utils.js'; export default function baseReducer>( state: T, action: BaseAction, onStateDifference: (message: string) => mixed, ): { state: T, storeOperations: StoreOperations } { const { threadStore, newThreadInconsistencies, threadStoreOperations } = reduceThreadInfos(state.threadStore, action); const { threadInfos } = threadStore; const [entryStore, newEntryInconsistencies] = reduceEntryInfos( state.entryStore, action, threadInfos, ); const onStateDifferenceForStaff = (message: string) => { const isCurrentUserStaff = state.currentUserInfo?.id ? isStaff(state.currentUserInfo.id) : false; if (isCurrentUserStaff || isDev) { onStateDifference(message); } }; const [userStore, newUserInconsistencies, userStoreOperations] = reduceUserInfos(state.userStore, action); const newInconsistencies = [ ...newEntryInconsistencies, ...newThreadInconsistencies, ...newUserInconsistencies, ]; // Only allow checkpoints to increase if we are connected // or if the action is a STATE_SYNC const { messageStoreOperations, messageStore: reducedMessageStore } = reduceMessageStore(state.messageStore, action, threadInfos); let messageStore = reducedMessageStore; let { keyserverStore, keyserverStoreOperations } = reduceKeyserverStore( state.keyserverStore, action, onStateDifferenceForStaff, ); if ( action.type !== incrementalStateSyncActionType && action.type !== fullStateSyncActionType && action.type !== keyserverRegisterActionTypes.success && action.type !== logInActionTypes.success && action.type !== siweAuthActionTypes.success && action.type !== keyserverAuthActionTypes.success && action.type !== addKeyserverActionType ) { const replaceOperations: ReplaceKeyserverOperation[] = []; for (const keyserverID in keyserverStore.keyserverInfos) { if ( keyserverStore.keyserverInfos[keyserverID].connection.status === 'connected' ) { continue; } if ( messageStore.currentAsOf[keyserverID] !== state.messageStore.currentAsOf[keyserverID] ) { messageStore = { ...messageStore, currentAsOf: { ...messageStore.currentAsOf, [keyserverID]: state.messageStore.currentAsOf[keyserverID], }, }; } if ( state.keyserverStore.keyserverInfos[keyserverID] && keyserverStore.keyserverInfos[keyserverID].updatesCurrentAsOf !== state.keyserverStore.keyserverInfos[keyserverID].updatesCurrentAsOf ) { replaceOperations.push({ type: 'replace_keyserver', payload: { id: keyserverID, keyserverInfo: { ...keyserverStore.keyserverInfos[keyserverID], updatesCurrentAsOf: state.keyserverStore.keyserverInfos[keyserverID] .updatesCurrentAsOf, }, }, }); } } keyserverStore = keyserverStoreOpsHandlers.processStoreOperations( keyserverStore, replaceOperations, ); keyserverStoreOperations = [ ...keyserverStoreOperations, ...replaceOperations, ]; } const { draftStore, draftStoreOperations } = reduceDraftStore( state.draftStore, action, ); const { reportStore, reportStoreOperations } = reduceReportStore( state.reportStore, action, newInconsistencies, ); const { communityStore, communityStoreOperations } = reduceCommunityStore( state.communityStore, action, ); const { integrityStore, integrityStoreOperations } = reduceIntegrityStore( state.integrityStore, action, onStateDifferenceForStaff, threadInfos, threadStoreOperations, ); const { syncedMetadataStore, syncedMetadataStoreOperations } = reduceSyncedMetadataStore(state.syncedMetadataStore, action); const { auxUserStore, auxUserStoreOperations } = reduceAuxUserStore( state.auxUserStore, action, ); + const { threadActivityStore } = reduceThreadActivity( + state.threadActivityStore, + action, + ); + return { state: { ...state, navInfo: reduceBaseNavInfo(state.navInfo, action), draftStore, entryStore, loadingStatuses: reduceLoadingStatuses(state.loadingStatuses, action), currentUserInfo: reduceCurrentUserInfo(state.currentUserInfo, action), threadStore, userStore, messageStore, calendarFilters: reduceCalendarFilters( state.calendarFilters, action, threadStore, ), alertStore: reduceAlertStore(state.alertStore, action), lifecycleState: reduceLifecycleState(state.lifecycleState, action), enabledApps: reduceEnabledApps(state.enabledApps, action), reportStore, dataLoaded: reduceDataLoaded(state.dataLoaded, action), userPolicies: policiesReducer(state.userPolicies, action), commServicesAccessToken: reduceServicesAccessToken( state.commServicesAccessToken, action, ), inviteLinksStore: reduceInviteLinks(state.inviteLinksStore, action), keyserverStore, - threadActivityStore: reduceThreadActivity( - state.threadActivityStore, - action, - ), integrityStore, globalThemeInfo: reduceGlobalThemeInfo(state.globalThemeInfo, action), customServer: reduceCustomerServer(state.customServer, action), communityStore, dbOpsStore: reduceDBOpsStore(state.dbOpsStore, action), syncedMetadataStore, auxUserStore, + threadActivityStore, }, storeOperations: { draftStoreOperations, threadStoreOperations, messageStoreOperations, reportStoreOperations, userStoreOperations, keyserverStoreOperations, communityStoreOperations, integrityStoreOperations, syncedMetadataStoreOperations, auxUserStoreOperations, }, }; } diff --git a/lib/reducers/thread-activity-reducer.js b/lib/reducers/thread-activity-reducer.js index 27b8e10b5..4fee018c6 100644 --- a/lib/reducers/thread-activity-reducer.js +++ b/lib/reducers/thread-activity-reducer.js @@ -1,151 +1,181 @@ // @flow import invariant from 'invariant'; import { messageStorePruneActionType } from '../actions/message-actions.js'; import { changeThreadMemberRolesActionTypes, changeThreadSettingsActionTypes, deleteCommunityRoleActionTypes, deleteThreadActionTypes, joinThreadActionTypes, leaveThreadActionTypes, modifyCommunityRoleActionTypes, newThreadActionTypes, removeUsersFromThreadActionTypes, } from '../actions/thread-actions.js'; import { deleteKeyserverAccountActionTypes } from '../actions/user-actions.js'; import { extractKeyserverIDFromID } from '../keyserver-conn/keyserver-call-utils.js'; import { setNewSessionActionType } from '../keyserver-conn/keyserver-conn-types.js'; -import { threadActivityStoreOpsHandlers } from '../ops/thread-activity-store-ops.js'; +import { + threadActivityStoreOpsHandlers, + type ThreadActivityStoreOperation, +} from '../ops/thread-activity-store-ops.js'; import type { BaseAction } from '../types/redux-types.js'; import { incrementalStateSyncActionType } from '../types/socket-types.js'; import type { ThreadActivityStore } from '../types/thread-activity-types.js'; import { updateThreadLastNavigatedActionType } from '../types/thread-activity-types.js'; import { updateTypes } from '../types/update-types-enum.js'; import type { ClientUpdateInfo } from '../types/update-types.js'; import { processUpdatesActionType } from '../types/update-types.js'; const { processStoreOperations: processStoreOps } = threadActivityStoreOpsHandlers; function reduceThreadActivity( state: ThreadActivityStore, action: BaseAction, -): ThreadActivityStore { +): { + +threadActivityStore: ThreadActivityStore, + +threadActivityStoreOperations: $ReadOnlyArray, +} { if (action.type === updateThreadLastNavigatedActionType) { const { threadID, time } = action.payload; const replaceOperation = { type: 'replace_thread_activity_entry', payload: { id: threadID, threadActivityStoreEntry: { ...state[threadID], lastNavigatedTo: time, }, }, }; - return processStoreOps(state, [replaceOperation]); + return { + threadActivityStore: processStoreOps(state, [replaceOperation]), + threadActivityStoreOperations: [replaceOperation], + }; } else if (action.type === messageStorePruneActionType) { const now = Date.now(); const replaceOperations = []; for (const threadID: string of action.payload.threadIDs) { const replaceOperation = { type: 'replace_thread_activity_entry', payload: { id: threadID, threadActivityStoreEntry: { ...state[threadID], lastPruned: now, }, }, }; replaceOperations.push(replaceOperation); } - return processStoreOps(state, replaceOperations); + return { + threadActivityStore: processStoreOps(state, replaceOperations), + threadActivityStoreOperations: replaceOperations, + }; } else if ( action.type === joinThreadActionTypes.success || action.type === leaveThreadActionTypes.success || action.type === deleteThreadActionTypes.success || action.type === changeThreadSettingsActionTypes.success || action.type === removeUsersFromThreadActionTypes.success || action.type === changeThreadMemberRolesActionTypes.success || action.type === incrementalStateSyncActionType || action.type === processUpdatesActionType || action.type === newThreadActionTypes.success || action.type === modifyCommunityRoleActionTypes.success || action.type === deleteCommunityRoleActionTypes.success ) { const { newUpdates } = action.payload.updatesResult; if (newUpdates.length === 0) { - return state; + return { + threadActivityStore: state, + threadActivityStoreOperations: [], + }; } const deleteThreadUpdates = newUpdates.filter( (update: ClientUpdateInfo) => update.type === updateTypes.DELETE_THREAD, ); if (deleteThreadUpdates.length === 0) { - return state; + return { + threadActivityStore: state, + threadActivityStoreOperations: [], + }; } const threadIDsToRemove = []; for (const update: ClientUpdateInfo of deleteThreadUpdates) { invariant( update.type === updateTypes.DELETE_THREAD, 'update must be of type DELETE_THREAD', ); threadIDsToRemove.push(update.threadID); } const removeOperation = { type: 'remove_thread_activity_entries', payload: { ids: threadIDsToRemove, }, }; - return processStoreOps(state, [removeOperation]); + return { + threadActivityStore: processStoreOps(state, [removeOperation]), + threadActivityStoreOperations: [removeOperation], + }; } else if (action.type === deleteKeyserverAccountActionTypes.success) { const threadIDsToRemove = []; const keyserverIDsSet = new Set(action.payload.keyserverIDs); for (const threadID in state) { if (!keyserverIDsSet.has(extractKeyserverIDFromID(threadID))) { continue; } threadIDsToRemove.push(threadID); } const removeOperation = { type: 'remove_thread_activity_entries', payload: { ids: threadIDsToRemove, }, }; - return processStoreOps(state, [removeOperation]); + return { + threadActivityStore: processStoreOps(state, [removeOperation]), + threadActivityStoreOperations: [removeOperation], + }; } else if ( action.type === setNewSessionActionType && action.payload.sessionChange.cookieInvalidated ) { const threadIDsToRemove = []; const { keyserverID } = action.payload; for (const threadID in state) { if (extractKeyserverIDFromID(threadID) !== keyserverID) { continue; } threadIDsToRemove.push(threadID); } const removeOperation = { type: 'remove_thread_activity_entries', payload: { ids: threadIDsToRemove, }, }; - return processStoreOps(state, [removeOperation]); + return { + threadActivityStore: processStoreOps(state, [removeOperation]), + threadActivityStoreOperations: [removeOperation], + }; } - return state; + return { + threadActivityStore: state, + threadActivityStoreOperations: [], + }; } export { reduceThreadActivity }; diff --git a/lib/reducers/thread-activity-reducer.test.js b/lib/reducers/thread-activity-reducer.test.js index 03dde0c6d..1bdb8e52c 100644 --- a/lib/reducers/thread-activity-reducer.test.js +++ b/lib/reducers/thread-activity-reducer.test.js @@ -1,147 +1,149 @@ // @flow import { reduceThreadActivity } from './thread-activity-reducer.js'; import { messageStorePruneActionType } from '../actions/message-actions.js'; import { deleteKeyserverAccountActionTypes } from '../actions/user-actions.js'; import { updateThreadLastNavigatedActionType } from '../types/thread-activity-types.js'; // NOTE: These unit tests were generated by GitHub Copilot. describe('reduceThreadActivity', () => { test('updates the lastNavigatedTo time for a thread', () => { const initialState = { thread1: { lastNavigatedTo: 1639522314170, lastPruned: 1639522314170, }, thread2: { lastNavigatedTo: 1639522314170, lastPruned: 1639522314170, }, }; const action = { type: updateThreadLastNavigatedActionType, payload: { threadID: 'thread1', time: 1639522317443, }, }; const expectedState = { thread1: { lastNavigatedTo: 1639522317443, lastPruned: 1639522314170, }, thread2: { lastNavigatedTo: 1639522314170, lastPruned: 1639522314170, }, }; const result = reduceThreadActivity(initialState, action); - expect(result).toEqual(expectedState); + expect(result.threadActivityStore).toEqual(expectedState); }); test('should create new thread activity entry with only lastNavigatedTo field', () => { const initialState = {}; const action = { type: updateThreadLastNavigatedActionType, payload: { threadID: 'thread1', time: 1639522317443, }, }; const expectedState = { thread1: { lastNavigatedTo: 1639522317443, }, }; const result = reduceThreadActivity(initialState, action); - expect(result).toEqual(expectedState); + expect(result.threadActivityStore).toEqual(expectedState); }); test('should create new thread activity entry with only lastPruned field', () => { const initialState = {}; const action = { type: messageStorePruneActionType, payload: { threadIDs: ['thread1'], }, }; const expectedState = { thread1: { lastPruned: expect.any(Number), }, }; const result = reduceThreadActivity(initialState, action); - expect(result).toEqual(expectedState); + expect(result.threadActivityStore).toEqual(expectedState); }); test('returns the initial state if the action type is not recognized', () => { const initialState = { thread1: { lastNavigatedTo: 1639522314170, lastPruned: 1639522314170, }, thread2: { lastNavigatedTo: 1639522314170, lastPruned: 1639522314170, }, }; const action = { type: 'UPDATE_REPORTS_ENABLED', payload: {}, }; const expectedState = { thread1: { lastNavigatedTo: 1639522314170, lastPruned: 1639522314170, }, thread2: { lastNavigatedTo: 1639522314170, lastPruned: 1639522314170, }, }; const result = reduceThreadActivity(initialState, action); - expect(result).toEqual(expectedState); + expect(result.threadActivityStore).toEqual(expectedState); }); test('removes threads of keyserver the user has disconnected from', () => { const keyserver1 = '100'; const keyserver2 = '200'; const keyserver3 = '300'; const threads1 = { [keyserver1 + '|1']: { lastNavigatedTo: 1, lastPruned: 1 }, [keyserver1 + '|2']: { lastNavigatedTo: 1, lastPruned: 1 }, }; const threads2 = { [keyserver2 + '|1']: { lastNavigatedTo: 1, lastPruned: 1 }, [keyserver2 + '|2']: { lastNavigatedTo: 1, lastPruned: 1 }, }; const threads3 = { [keyserver3 + '|1']: { lastNavigatedTo: 1, lastPruned: 1 }, [keyserver3 + '|2']: { lastNavigatedTo: 1, lastPruned: 1 }, }; const threads = { ...threads1, ...threads2, ...threads3 }; const action = { type: deleteKeyserverAccountActionTypes.success, payload: { currentUserInfo: { anonymous: true }, preRequestUserState: { cookiesAndSessions: {}, currentUserInfo: { id: '83810', username: 'user1', }, }, keyserverIDs: [keyserver1, keyserver2], }, loadingInfo: { fetchIndex: 1, trackMultipleRequests: false, customKeyName: undefined, }, }; - expect(reduceThreadActivity(threads, action)).toEqual(threads3); + expect(reduceThreadActivity(threads, action).threadActivityStore).toEqual( + threads3, + ); }); });