diff --git a/lib/reducers/integrity-reducer.js b/lib/reducers/integrity-reducer.js index e9af26f66..615fb0401 100644 --- a/lib/reducers/integrity-reducer.js +++ b/lib/reducers/integrity-reducer.js @@ -1,90 +1,96 @@ // @flow import { setClientDBStoreActionType } from '../actions/client-db-store-actions.js'; import { updateIntegrityStoreActionType } from '../actions/integrity-actions.js'; import { siweAuthActionTypes } from '../actions/siwe-actions.js'; import { keyserverAuthActionTypes, logInActionTypes, keyserverRegisterActionTypes, } from '../actions/user-actions.js'; +import { extractKeyserverIDFromID } from '../keyserver-conn/keyserver-call-utils.js'; import type { ThreadStoreOperation } from '../ops/thread-store-ops'; import type { IntegrityStore } from '../types/integrity-types'; import type { RawThreadInfo } from '../types/minimally-encoded-thread-permissions-types.js'; import type { BaseAction } from '../types/redux-types.js'; import { fullStateSyncActionType } from '../types/socket-types.js'; import { hash } from '../utils/objects.js'; function reduceIntegrityStore( state: IntegrityStore, action: BaseAction, threadInfos: { +[string]: RawThreadInfo, }, threadStoreOperations: $ReadOnlyArray, ): IntegrityStore { - if ( + if (action.type === fullStateSyncActionType) { + const threadHashesArray = Object.entries(state.threadHashes).filter( + ([key]) => extractKeyserverIDFromID(key) !== action.payload.keyserverID, + ); + const threadHashes = Object.fromEntries(threadHashesArray); + return { threadHashes, threadHashingStatus: 'starting' }; + } else if ( action.type === logInActionTypes.success || action.type === siweAuthActionTypes.success || action.type === keyserverRegisterActionTypes.success || - action.type === fullStateSyncActionType || (action.type === setClientDBStoreActionType && !!action.payload.threadStore && state.threadHashingStatus !== 'completed') ) { return { threadHashes: {}, threadHashingStatus: 'starting' }; } else if (action.type === keyserverAuthActionTypes.success) { return { ...state, threadHashingStatus: 'starting' }; } let newState = state; if (action.type === updateIntegrityStoreActionType) { 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.threadHashingStatus) { newState = { ...newState, threadHashingStatus: action.payload.threadHashingStatus, }; } } if (threadStoreOperations.length === 0) { return newState; } let processedThreadHashes = { ...newState.threadHashes }; let threadHashingStatus = newState.threadHashingStatus; 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 = {}; threadHashingStatus = 'completed'; } } return { ...newState, threadHashes: processedThreadHashes, threadHashingStatus, }; } export { reduceIntegrityStore }; diff --git a/lib/reducers/thread-reducer.js b/lib/reducers/thread-reducer.js index 0fc72081a..888f491bb 100644 --- a/lib/reducers/thread-reducer.js +++ b/lib/reducers/thread-reducer.js @@ -1,473 +1,499 @@ // @flow import { setThreadUnreadStatusActionTypes, updateActivityActionTypes, } from '../actions/activity-actions.js'; import { setClientDBStoreActionType } from '../actions/client-db-store-actions.js'; import { saveMessagesActionType } from '../actions/message-actions.js'; import { siweAuthActionTypes } from '../actions/siwe-actions.js'; import { changeThreadSettingsActionTypes, deleteThreadActionTypes, newThreadActionTypes, removeUsersFromThreadActionTypes, changeThreadMemberRolesActionTypes, joinThreadActionTypes, leaveThreadActionTypes, modifyCommunityRoleActionTypes, deleteCommunityRoleActionTypes, } from '../actions/thread-actions.js'; import { keyserverAuthActionTypes, deleteKeyserverAccountActionTypes, logInActionTypes, keyserverRegisterActionTypes, updateSubscriptionActionTypes, } from '../actions/user-actions.js'; -import { getThreadIDsForKeyservers } from '../keyserver-conn/keyserver-call-utils.js'; +import { + getThreadIDsForKeyservers, + extractKeyserverIDFromID, +} from '../keyserver-conn/keyserver-call-utils.js'; import { setNewSessionActionType } from '../keyserver-conn/keyserver-conn-types.js'; import { type ThreadStoreOperation, threadStoreOpsHandlers, } from '../ops/thread-store-ops.js'; import { stateSyncSpecs } from '../shared/state-sync/state-sync-specs.js'; import { updateSpecs } from '../shared/updates/update-specs.js'; import type { RawThreadInfo } from '../types/minimally-encoded-thread-permissions-types.js'; import type { BaseAction } from '../types/redux-types.js'; import { type ClientThreadInconsistencyReportCreationRequest } from '../types/report-types.js'; import { serverRequestTypes, processServerRequestsActionType, } from '../types/request-types.js'; import { fullStateSyncActionType, incrementalStateSyncActionType, } from '../types/socket-types.js'; import type { RawThreadInfos, ThreadStore } from '../types/thread-types.js'; import { type ClientUpdateInfo, processUpdatesActionType, } from '../types/update-types.js'; const { processStoreOperations: processThreadStoreOperations } = threadStoreOpsHandlers; function generateOpsForThreadUpdates( threadInfos: RawThreadInfos, payload: { +updatesResult: { +newUpdates: $ReadOnlyArray, ... }, ... }, ): $ReadOnlyArray { return payload.updatesResult.newUpdates .map(update => updateSpecs[update.type].generateOpsForThreadUpdates?.( threadInfos, update, ), ) .filter(Boolean) .flat(); } function reduceThreadInfos( state: ThreadStore, action: BaseAction, ): { threadStore: ThreadStore, newThreadInconsistencies: $ReadOnlyArray, threadStoreOperations: $ReadOnlyArray, } { - if ( + if (action.type === fullStateSyncActionType) { + const threadsToRemove = Object.keys(state.threadInfos).filter( + key => extractKeyserverIDFromID(key) === action.payload.keyserverID, + ); + const newThreadInfos = action.payload.threadInfos; + const threadStoreOperations = [ + { + type: 'remove', + payload: { ids: threadsToRemove }, + }, + ...Object.keys(newThreadInfos).map((id: string) => ({ + type: 'replace', + payload: { id, threadInfo: newThreadInfos[id] }, + })), + ]; + const updatedThreadStore = processThreadStoreOperations( + state, + threadStoreOperations, + ); + return { + threadStore: updatedThreadStore, + newThreadInconsistencies: [], + threadStoreOperations, + }; + } else if ( action.type === logInActionTypes.success || action.type === siweAuthActionTypes.success || - action.type === keyserverRegisterActionTypes.success || - action.type === fullStateSyncActionType + action.type === keyserverRegisterActionTypes.success ) { const newThreadInfos = action.payload.threadInfos; const threadStoreOperations = [ { type: 'remove_all', }, ...Object.keys(newThreadInfos).map((id: string) => ({ type: 'replace', payload: { id, threadInfo: newThreadInfos[id] }, })), ]; const updatedThreadStore = processThreadStoreOperations( state, threadStoreOperations, ); return { threadStore: updatedThreadStore, newThreadInconsistencies: [], threadStoreOperations, }; } else if (action.type === keyserverAuthActionTypes.success) { const keyserverIDs = Object.keys(action.payload.updatesCurrentAsOf); const threadIDsToRemove = getThreadIDsForKeyservers( Object.keys(state.threadInfos), keyserverIDs, ); const newThreadInfos = action.payload.threadInfos; const threadStoreOperations = [ { type: 'remove', payload: { ids: threadIDsToRemove }, }, ...Object.keys(newThreadInfos).map((id: string) => ({ type: 'replace', payload: { id, threadInfo: newThreadInfos[id] }, })), ]; const updatedThreadStore = processThreadStoreOperations( state, threadStoreOperations, ); return { threadStore: updatedThreadStore, newThreadInconsistencies: [], threadStoreOperations, }; } else if (action.type === deleteKeyserverAccountActionTypes.success) { const threadIDsToRemove = getThreadIDsForKeyservers( Object.keys(state.threadInfos), action.payload.keyserverIDs, ); if (threadIDsToRemove.length === 0) { return { threadStore: state, newThreadInconsistencies: [], threadStoreOperations: [], }; } const threadStoreOperations = [ { type: 'remove', payload: { ids: threadIDsToRemove }, }, ]; const updatedThreadStore = processThreadStoreOperations( state, threadStoreOperations, ); return { threadStore: updatedThreadStore, newThreadInconsistencies: [], threadStoreOperations, }; } else if ( action.type === setNewSessionActionType && action.payload.sessionChange.cookieInvalidated ) { const threadIDsToRemove = getThreadIDsForKeyservers( Object.keys(state.threadInfos), [action.payload.keyserverID], ); if (threadIDsToRemove.length === 0) { return { threadStore: state, newThreadInconsistencies: [], threadStoreOperations: [], }; } const threadStoreOperations = [ { type: 'remove', payload: { ids: threadIDsToRemove }, }, ]; const updatedThreadStore = processThreadStoreOperations( state, threadStoreOperations, ); return { threadStore: updatedThreadStore, newThreadInconsistencies: [], threadStoreOperations, }; } 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 { threadStore: state, newThreadInconsistencies: [], threadStoreOperations: [], }; } const threadStoreOperations = generateOpsForThreadUpdates( state.threadInfos, action.payload, ); const updatedThreadStore = processThreadStoreOperations( state, threadStoreOperations, ); return { threadStore: updatedThreadStore, newThreadInconsistencies: [], threadStoreOperations, }; } else if (action.type === updateSubscriptionActionTypes.success) { const { threadID, subscription } = action.payload; const threadInfo = state.threadInfos[threadID]; const newThreadInfo = { ...threadInfo, currentUser: { ...threadInfo.currentUser, subscription, }, }; const threadStoreOperations = [ { type: 'replace', payload: { id: threadID, threadInfo: newThreadInfo, }, }, ]; const updatedThreadStore = processThreadStoreOperations( state, threadStoreOperations, ); return { threadStore: updatedThreadStore, newThreadInconsistencies: [], threadStoreOperations, }; } else if (action.type === saveMessagesActionType) { const threadIDToMostRecentTime = new Map(); for (const messageInfo of action.payload.rawMessageInfos) { const current = threadIDToMostRecentTime.get(messageInfo.threadID); if (!current || current < messageInfo.time) { threadIDToMostRecentTime.set(messageInfo.threadID, messageInfo.time); } } const changedThreadInfos: { [string]: RawThreadInfo } = {}; for (const [threadID, mostRecentTime] of threadIDToMostRecentTime) { const threadInfo = state.threadInfos[threadID]; if ( !threadInfo || threadInfo.currentUser.unread || action.payload.updatesCurrentAsOf > mostRecentTime ) { continue; } changedThreadInfos[threadID] = { ...threadInfo, currentUser: { ...threadInfo.currentUser, unread: true, }, }; } if (Object.keys(changedThreadInfos).length !== 0) { const threadStoreOperations = Object.keys(changedThreadInfos).map(id => ({ type: 'replace', payload: { id, threadInfo: changedThreadInfos[id], }, })); const updatedThreadStore = processThreadStoreOperations( state, threadStoreOperations, ); return { threadStore: updatedThreadStore, newThreadInconsistencies: [], threadStoreOperations, }; } } else if (action.type === processServerRequestsActionType) { const checkStateRequest = action.payload.serverRequests.find( candidate => candidate.type === serverRequestTypes.CHECK_STATE, ); if (!checkStateRequest || !checkStateRequest.stateChanges) { return { threadStore: state, newThreadInconsistencies: [], threadStoreOperations: [], }; } const { rawThreadInfos, deleteThreadIDs } = checkStateRequest.stateChanges; if (!rawThreadInfos && !deleteThreadIDs) { return { threadStore: state, newThreadInconsistencies: [], threadStoreOperations: [], }; } const threadStoreOperations: ThreadStoreOperation[] = []; if (rawThreadInfos) { for (const rawThreadInfo of rawThreadInfos) { threadStoreOperations.push({ type: 'replace', payload: { id: rawThreadInfo.id, threadInfo: rawThreadInfo, }, }); } } if (deleteThreadIDs) { threadStoreOperations.push({ type: 'remove', payload: { ids: deleteThreadIDs, }, }); } const updatedThreadStore = processThreadStoreOperations( state, threadStoreOperations, ); const newThreadInconsistencies = stateSyncSpecs.threads.findStoreInconsistencies( action, state.threadInfos, updatedThreadStore.threadInfos, ); return { threadStore: updatedThreadStore, newThreadInconsistencies, threadStoreOperations, }; } else if (action.type === updateActivityActionTypes.success) { const updatedThreadInfos: { [string]: RawThreadInfo } = {}; for (const setToUnread of action.payload.result.unfocusedToUnread) { const threadInfo = state.threadInfos[setToUnread]; if (threadInfo && !threadInfo.currentUser.unread) { updatedThreadInfos[setToUnread] = { ...threadInfo, currentUser: { ...threadInfo.currentUser, unread: true, }, }; } } if (Object.keys(updatedThreadInfos).length === 0) { return { threadStore: state, newThreadInconsistencies: [], threadStoreOperations: [], }; } const threadStoreOperations = Object.keys(updatedThreadInfos).map(id => ({ type: 'replace', payload: { id, threadInfo: updatedThreadInfos[id], }, })); const updatedThreadStore = processThreadStoreOperations( state, threadStoreOperations, ); return { threadStore: updatedThreadStore, newThreadInconsistencies: [], threadStoreOperations, }; } else if (action.type === setThreadUnreadStatusActionTypes.started) { const { threadID, unread } = action.payload; const threadInfo = state.threadInfos[threadID]; const updatedThreadInfo = { ...threadInfo, currentUser: { ...threadInfo.currentUser, unread, }, }; const threadStoreOperations = [ { type: 'replace', payload: { id: threadID, threadInfo: updatedThreadInfo, }, }, ]; const updatedThreadStore = processThreadStoreOperations( state, threadStoreOperations, ); return { threadStore: updatedThreadStore, newThreadInconsistencies: [], threadStoreOperations, }; } else if (action.type === setThreadUnreadStatusActionTypes.success) { const { threadID, resetToUnread } = action.payload; const threadInfo = state.threadInfos[threadID]; const { currentUser } = threadInfo; if (!resetToUnread || currentUser.unread) { return { threadStore: state, newThreadInconsistencies: [], threadStoreOperations: [], }; } const updatedThread = { ...threadInfo, currentUser: { ...currentUser, unread: true }, }; const threadStoreOperations = [ { type: 'replace', payload: { id: threadID, threadInfo: updatedThread, }, }, ]; const updatedThreadStore = processThreadStoreOperations( state, threadStoreOperations, ); return { threadStore: updatedThreadStore, newThreadInconsistencies: [], threadStoreOperations, }; } else if (action.type === setClientDBStoreActionType) { return { threadStore: action.payload.threadStore ?? state, newThreadInconsistencies: [], threadStoreOperations: [], }; } return { threadStore: state, newThreadInconsistencies: [], threadStoreOperations: [], }; } export { reduceThreadInfos }; diff --git a/lib/reducers/user-reducer.js b/lib/reducers/user-reducer.js index ac1c7e144..8ddeb524f 100644 --- a/lib/reducers/user-reducer.js +++ b/lib/reducers/user-reducer.js @@ -1,478 +1,510 @@ // @flow import _isEqual from 'lodash/fp/isEqual.js'; import _keyBy from 'lodash/fp/keyBy.js'; import { setClientDBStoreActionType } from '../actions/client-db-store-actions.js'; import { siweAuthActionTypes } from '../actions/siwe-actions.js'; import { joinThreadActionTypes, newThreadActionTypes, } from '../actions/thread-actions.js'; import { identityLogInActionTypes, identityRegisterActionTypes, deleteAccountActionTypes, keyserverAuthActionTypes, logOutActionTypes, logInActionTypes, keyserverRegisterActionTypes, setUserSettingsActionTypes, updateUserAvatarActionTypes, } from '../actions/user-actions.js'; import { setNewSessionActionType } from '../keyserver-conn/keyserver-conn-types.js'; import { convertUserInfosToReplaceUserOps, type UserStoreOperation, userStoreOpsHandlers, } from '../ops/user-store-ops.js'; import { stateSyncSpecs } from '../shared/state-sync/state-sync-specs.js'; import { updateSpecs } from '../shared/updates/update-specs.js'; import type { BaseAction } from '../types/redux-types.js'; import type { ClientUserInconsistencyReportCreationRequest } from '../types/report-types.js'; import { serverRequestTypes, processServerRequestsActionType, } from '../types/request-types.js'; import { fullStateSyncActionType, incrementalStateSyncActionType, } from '../types/socket-types.js'; import { updateTypes } from '../types/update-types-enum.js'; import { processUpdatesActionType } from '../types/update-types.js'; import type { ClientUpdateInfo } from '../types/update-types.js'; import type { CurrentUserInfo, UserInfos, UserStore, } from '../types/user-types.js'; import { authoritativeKeyserverID } from '../utils/authoritative-keyserver.js'; import { getMessageForException } from '../utils/errors.js'; import { assertObjectsAreEqual } from '../utils/objects.js'; import { relyingOnAuthoritativeKeyserver } from '../utils/services-utils.js'; function reduceCurrentUserInfo( state: ?CurrentUserInfo, action: BaseAction, ): ?CurrentUserInfo { if ( action.type === identityLogInActionTypes.success || action.type === identityRegisterActionTypes.success ) { const newUserInfo = { id: action.payload.userID, username: action.payload.username, }; if (!_isEqual(newUserInfo)(state)) { return newUserInfo; } } else if (action.type === keyserverAuthActionTypes.success) { const { currentUserInfo: actionUserInfo } = action.payload; if (actionUserInfo) { if (!state?.id || actionUserInfo.id !== state.id) { console.log( 'keyserver auth returned a different user info than identity login', ); } else { const newUserInfo = { ...state, avatar: actionUserInfo.avatar, settings: actionUserInfo.settings, }; if (!_isEqual(newUserInfo)(state)) { return newUserInfo; } } } } else if ( action.type === logInActionTypes.success || action.type === siweAuthActionTypes.success || action.type === keyserverRegisterActionTypes.success || action.type === logOutActionTypes.success || action.type === deleteAccountActionTypes.success ) { if (!_isEqual(action.payload.currentUserInfo)(state)) { return action.payload.currentUserInfo; } } else if ( action.type === setNewSessionActionType && action.payload.sessionChange.currentUserInfo && action.payload.keyserverID === authoritativeKeyserverID() && relyingOnAuthoritativeKeyserver ) { const { sessionChange } = action.payload; if (!_isEqual(sessionChange.currentUserInfo)(state)) { return sessionChange.currentUserInfo; } - } else if (action.type === fullStateSyncActionType) { + } else if ( + action.type === fullStateSyncActionType && + relyingOnAuthoritativeKeyserver + ) { + if (action.payload.keyserverID !== authoritativeKeyserverID()) { + return state; + } const { currentUserInfo } = action.payload; if (!_isEqual(currentUserInfo)(state)) { return currentUserInfo; } } else if ( action.type === incrementalStateSyncActionType || action.type === processUpdatesActionType ) { return action.payload.updatesResult.newUpdates.reduce( (reducedState, update) => { const { reduceCurrentUser } = updateSpecs[update.type]; return reduceCurrentUser ? reduceCurrentUser(reducedState, update) : reducedState; }, state, ); } else if (action.type === processServerRequestsActionType) { const checkStateRequest = action.payload.serverRequests.find( candidate => candidate.type === serverRequestTypes.CHECK_STATE, ); const newCurrentUserInfo = checkStateRequest?.stateChanges?.currentUserInfo; if (newCurrentUserInfo && !_isEqual(newCurrentUserInfo)(state)) { return newCurrentUserInfo; } } else if ( action.type === updateUserAvatarActionTypes.success && state && !state.anonymous ) { const { viewerUpdates } = action.payload.updates; for (const update of viewerUpdates) { if ( update.type === updateTypes.UPDATE_CURRENT_USER && !_isEqual(update.currentUserInfo.avatar)(state.avatar) ) { return { ...state, avatar: update.currentUserInfo.avatar, }; } } return state; } else if (action.type === setUserSettingsActionTypes.success) { if (state?.settings) { return { ...state, settings: { ...state.settings, ...action.payload, }, }; } } return state; } function assertUserStoresAreEqual( processedUserStore: UserInfos, expectedUserStore: UserInfos, location: string, onStateDifference: (message: string) => mixed, ) { try { assertObjectsAreEqual( processedUserStore, expectedUserStore, `UserInfos - ${location}`, ); } catch (e) { console.log( 'Error processing UserStore ops', processedUserStore, expectedUserStore, ); const message = `Error processing UserStore ops ${ getMessageForException(e) ?? '{no exception message}' }`; onStateDifference(message); } } const { processStoreOperations: processUserStoreOps } = userStoreOpsHandlers; function generateOpsForUserUpdates(payload: { +updatesResult: { +newUpdates: $ReadOnlyArray, ... }, ... }): $ReadOnlyArray { return payload.updatesResult.newUpdates .map(update => updateSpecs[update.type].generateOpsForUserInfoUpdates?.(update), ) .filter(Boolean) .flat(); } function reduceUserInfos( state: UserStore, action: BaseAction, onStateDifference: (message: string) => mixed, ): [ UserStore, $ReadOnlyArray, $ReadOnlyArray, ] { if ( action.type === joinThreadActionTypes.success || action.type === newThreadActionTypes.success ) { const newUserInfos: UserInfos = _keyBy(userInfo => userInfo.id)( action.payload.userInfos, ); const userStoreOps: $ReadOnlyArray = convertUserInfosToReplaceUserOps(newUserInfos); const processedUserInfos: UserInfos = processUserStoreOps( state.userInfos, userStoreOps, ); const updated: UserInfos = { ...state.userInfos, ...newUserInfos }; assertUserStoresAreEqual( processedUserInfos, updated, action.type, onStateDifference, ); if (!_isEqual(state.userInfos)(updated)) { return [ { ...state, userInfos: updated, }, [], userStoreOps, ]; } } else if ( action.type === setNewSessionActionType && action.payload.sessionChange.cookieInvalidated && action.payload.keyserverID === authoritativeKeyserverID() && relyingOnAuthoritativeKeyserver ) { const userStoreOps: $ReadOnlyArray = [ { type: 'remove_all_users' }, ]; const processedUserInfos: UserInfos = processUserStoreOps( state.userInfos, userStoreOps, ); assertUserStoresAreEqual( processedUserInfos, {}, action.type, onStateDifference, ); if (Object.keys(state.userInfos).length === 0) { return [state, [], []]; } return [ { userInfos: {}, }, [], userStoreOps, ]; } else if ( - action.type === logInActionTypes.success || - action.type === siweAuthActionTypes.success || - action.type === keyserverRegisterActionTypes.success || - action.type === fullStateSyncActionType + action.type === fullStateSyncActionType && + relyingOnAuthoritativeKeyserver ) { + if (action.payload.keyserverID !== authoritativeKeyserverID()) { + return [state, [], []]; + } const newUserInfos = _keyBy(userInfo => userInfo.id)( action.payload.userInfos, ); const userStoreOps: $ReadOnlyArray = [ { type: 'remove_all_users' }, ...convertUserInfosToReplaceUserOps(newUserInfos), ]; const processedUserInfos: UserInfos = processUserStoreOps( state.userInfos, userStoreOps, ); assertUserStoresAreEqual( processedUserInfos, newUserInfos, action.type, onStateDifference, ); - if ( - action.type === logInActionTypes.success || - action.type === siweAuthActionTypes.success || - action.type === keyserverRegisterActionTypes.success || - !_isEqual(state.userInfos)(newUserInfos) - ) { + if (!_isEqual(state.userInfos)(newUserInfos)) { return [ { userInfos: newUserInfos, }, [], userStoreOps, ]; } + } else if ( + action.type === logInActionTypes.success || + action.type === siweAuthActionTypes.success || + action.type === keyserverRegisterActionTypes.success + ) { + const newUserInfos = _keyBy(userInfo => userInfo.id)( + action.payload.userInfos, + ); + const userStoreOps: $ReadOnlyArray = [ + { type: 'remove_all_users' }, + ...convertUserInfosToReplaceUserOps(newUserInfos), + ]; + const processedUserInfos: UserInfos = processUserStoreOps( + state.userInfos, + userStoreOps, + ); + assertUserStoresAreEqual( + processedUserInfos, + newUserInfos, + action.type, + onStateDifference, + ); + + return [ + { + userInfos: newUserInfos, + }, + [], + userStoreOps, + ]; } else if (action.type === keyserverAuthActionTypes.success) { const newUserInfos = _keyBy(userInfo => userInfo.id)( action.payload.userInfos, ); const mergedUserInfos: UserInfos = { ...state.userInfos, ...newUserInfos }; const userStoreOps: $ReadOnlyArray = convertUserInfosToReplaceUserOps(newUserInfos); const processedUserInfos: UserInfos = processUserStoreOps( state.userInfos, userStoreOps, ); assertUserStoresAreEqual( processedUserInfos, mergedUserInfos, action.type, onStateDifference, ); if (!_isEqual(state.userInfos)(mergedUserInfos)) { return [ { ...state, userInfos: mergedUserInfos, }, [], userStoreOps, ]; } } else if ( action.type === incrementalStateSyncActionType || action.type === processUpdatesActionType ) { const newUserInfos = _keyBy(userInfo => userInfo.id)( action.payload.userInfos, ); const userStoreOps: $ReadOnlyArray = [ ...convertUserInfosToReplaceUserOps(newUserInfos), ...generateOpsForUserUpdates(action.payload), ]; const updated = action.payload.updatesResult.newUpdates.reduce( (reducedState, update) => { const { reduceUserInfos: reduceUserInfosUpdate } = updateSpecs[update.type]; return reduceUserInfosUpdate ? reduceUserInfosUpdate(reducedState, update) : reducedState; }, { ...state.userInfos, ...newUserInfos }, ); const processedUserInfos: UserInfos = processUserStoreOps( state.userInfos, userStoreOps, ); assertUserStoresAreEqual( processedUserInfos, updated, action.type, onStateDifference, ); if (!_isEqual(state.userInfos)(updated)) { return [ { ...state, userInfos: updated, }, [], userStoreOps, ]; } } else if (action.type === processServerRequestsActionType) { const checkStateRequest = action.payload.serverRequests.find( candidate => candidate.type === serverRequestTypes.CHECK_STATE, ); if (!checkStateRequest || !checkStateRequest.stateChanges) { return [state, [], []]; } const { userInfos, deleteUserInfoIDs } = checkStateRequest.stateChanges; if (!userInfos && !deleteUserInfoIDs) { return [state, [], []]; } const userStoreOps: UserStoreOperation[] = []; const newUserInfos = { ...state.userInfos }; if (userInfos) { for (const userInfo of userInfos) { newUserInfos[userInfo.id] = userInfo; userStoreOps.push({ type: 'replace_user', payload: { ...userInfo } }); } } if (deleteUserInfoIDs) { for (const deleteUserInfoID of deleteUserInfoIDs) { delete newUserInfos[deleteUserInfoID]; } userStoreOps.push({ type: 'remove_users', payload: { ids: deleteUserInfoIDs }, }); } const processedUserInfos: UserInfos = processUserStoreOps( state.userInfos, userStoreOps, ); assertUserStoresAreEqual( processedUserInfos, newUserInfos, action.type, onStateDifference, ); const newInconsistencies = stateSyncSpecs.users.findStoreInconsistencies( action, state.userInfos, newUserInfos, ); return [ { userInfos: newUserInfos, }, newInconsistencies, userStoreOps, ]; } else if (action.type === updateUserAvatarActionTypes.success) { const newUserInfos = _keyBy(userInfo => userInfo.id)( action.payload.updates.userInfos, ); const userStoreOps: $ReadOnlyArray = convertUserInfosToReplaceUserOps(newUserInfos); const processedUserInfos: UserInfos = processUserStoreOps( state.userInfos, userStoreOps, ); const updated: UserInfos = { ...state.userInfos, ...newUserInfos }; assertUserStoresAreEqual( processedUserInfos, updated, action.type, onStateDifference, ); const newState = !_isEqual(state.userInfos)(updated) ? { ...state, userInfos: updated, } : state; return [newState, [], userStoreOps]; } else if (action.type === setClientDBStoreActionType) { if (!action.payload.users) { return [state, [], []]; } // Once the functionality is confirmed to work correctly, // we will proceed with returning the users from the payload. assertUserStoresAreEqual( action.payload.users ?? {}, state.userInfos, action.type, onStateDifference, ); return [state, [], []]; } return [state, [], []]; } export { reduceCurrentUserInfo, reduceUserInfos };