diff --git a/lib/reducers/user-reducer.js b/lib/reducers/user-reducer.js index 3a3b0b033..71c2ae370 100644 --- a/lib/reducers/user-reducer.js +++ b/lib/reducers/user-reducer.js @@ -1,269 +1,273 @@ // @flow import _isEqual from 'lodash/fp/isEqual.js'; import _keyBy from 'lodash/fp/keyBy.js'; import { siweAuthActionTypes } from '../actions/siwe-actions.js'; import { joinThreadActionTypes, newThreadActionTypes, } from '../actions/thread-actions.js'; import { logOutActionTypes, deleteAccountActionTypes, logInActionTypes, registerActionTypes, setUserSettingsActionTypes, updateUserAvatarActionTypes, resetUserStateActionType, } from '../actions/user-actions.js'; import { updateSpecs } from '../shared/updates/update-specs.js'; import type { BaseAction } from '../types/redux-types.js'; import { type UserInconsistencyReportCreationRequest, reportTypes, } 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 { CurrentUserInfo, UserStore, UserInfos, } from '../types/user-types.js'; import { actionLogger } from '../utils/action-logger.js'; import { setNewSessionActionType } from '../utils/action-utils.js'; import { getConfig } from '../utils/config.js'; import { generateReportID } from '../utils/report-utils.js'; import { sanitizeActionSecrets } from '../utils/sanitization.js'; function reduceCurrentUserInfo( state: ?CurrentUserInfo, action: BaseAction, ): ?CurrentUserInfo { if ( action.type === logInActionTypes.success || action.type === siweAuthActionTypes.success || action.type === registerActionTypes.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 ) { const { sessionChange } = action.payload; if (!_isEqual(sessionChange.currentUserInfo)(state)) { return sessionChange.currentUserInfo; } } else if (action.type === fullStateSyncActionType) { 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, ); if ( checkStateRequest && checkStateRequest.stateChanges && checkStateRequest.stateChanges.currentUserInfo && !_isEqual(checkStateRequest.stateChanges.currentUserInfo)(state) ) { return checkStateRequest.stateChanges.currentUserInfo; } } 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, }, }; } } else if (action.type === resetUserStateActionType) { state = state && state.anonymous ? state : null; } return state; } function findInconsistencies( action: BaseAction, beforeStateCheck: UserInfos, afterStateCheck: UserInfos, ): UserInconsistencyReportCreationRequest[] { if (_isEqual(beforeStateCheck)(afterStateCheck)) { return []; } return [ { type: reportTypes.USER_INCONSISTENCY, platformDetails: getConfig().platformDetails, action: sanitizeActionSecrets(action), beforeStateCheck, afterStateCheck, lastActions: actionLogger.interestingActionSummaries, time: Date.now(), id: generateReportID(), }, ]; } function reduceUserInfos(state: UserStore, action: BaseAction): UserStore { if ( action.type === joinThreadActionTypes.success || action.type === newThreadActionTypes.success ) { const newUserInfos = _keyBy(userInfo => userInfo.id)( action.payload.userInfos, ); const updated = { ...state.userInfos, ...newUserInfos }; if (!_isEqual(state.userInfos)(updated)) { return { ...state, userInfos: updated, }; } } else if ( action.type === logOutActionTypes.success || action.type === deleteAccountActionTypes.success || (action.type === setNewSessionActionType && action.payload.sessionChange.cookieInvalidated) ) { if (Object.keys(state.userInfos).length === 0) { return state; } return { userInfos: {}, inconsistencyReports: state.inconsistencyReports, }; } else if ( action.type === logInActionTypes.success || action.type === siweAuthActionTypes.success || action.type === registerActionTypes.success || action.type === fullStateSyncActionType ) { const newUserInfos = _keyBy(userInfo => userInfo.id)( action.payload.userInfos, ); if (!_isEqual(state.userInfos)(newUserInfos)) { return { userInfos: newUserInfos, inconsistencyReports: state.inconsistencyReports, }; } } else if ( action.type === incrementalStateSyncActionType || action.type === processUpdatesActionType ) { const newUserInfos = _keyBy(userInfo => userInfo.id)( action.payload.userInfos, ); - const updated = { ...state.userInfos, ...newUserInfos }; - for (const update of action.payload.updatesResult.newUpdates) { - if (update.type === updateTypes.DELETE_ACCOUNT) { - delete updated[update.deletedUserID]; - } - } + const updated = action.payload.updatesResult.newUpdates.reduce( + (reducedState, update) => { + const { reduceUserInfos: reduceUserInfosUpdate } = + updateSpecs[update.type]; + return reduceUserInfosUpdate + ? reduceUserInfosUpdate(reducedState, update) + : reducedState; + }, + { ...state.userInfos, ...newUserInfos }, + ); if (!_isEqual(state.userInfos)(updated)) { return { + ...state, userInfos: updated, - inconsistencyReports: state.inconsistencyReports, }; } } 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 newUserInfos = { ...state.userInfos }; if (userInfos) { for (const userInfo of userInfos) { newUserInfos[userInfo.id] = userInfo; } } if (deleteUserInfoIDs) { for (const deleteUserInfoID of deleteUserInfoIDs) { delete newUserInfos[deleteUserInfoID]; } } const newInconsistencies = findInconsistencies( action, state.userInfos, newUserInfos, ); return { userInfos: newUserInfos, inconsistencyReports: [ ...state.inconsistencyReports, ...newInconsistencies, ], }; } else if (action.type === updateUserAvatarActionTypes.success) { const newUserInfos = _keyBy(userInfo => userInfo.id)( action.payload.updates.userInfos, ); const updated = { ...state.userInfos, ...newUserInfos }; return !_isEqual(state.userInfos)(updated) ? { ...state, userInfos: updated, } : state; } return state; } export { reduceCurrentUserInfo, reduceUserInfos }; diff --git a/lib/shared/updates/delete-account-spec.js b/lib/shared/updates/delete-account-spec.js index ba891898d..e6a82c477 100644 --- a/lib/shared/updates/delete-account-spec.js +++ b/lib/shared/updates/delete-account-spec.js @@ -1,35 +1,44 @@ // @flow import type { UpdateSpec } from './update-spec.js'; import type { RawThreadInfos } from '../../types/thread-types.js'; import type { AccountDeletionUpdateInfo } from '../../types/update-types.js'; +import type { UserInfos } from '../../types/user-types.js'; export const deleteAccountSpec: UpdateSpec = Object.freeze({ generateOpsForThreadUpdates( storeThreadInfos: RawThreadInfos, update: AccountDeletionUpdateInfo, ) { const operations = []; for (const threadID in storeThreadInfos) { const threadInfo = storeThreadInfos[threadID]; const newMembers = threadInfo.members.filter( member => member.id !== update.deletedUserID, ); if (newMembers.length < threadInfo.members.length) { const updatedThread = { ...threadInfo, members: newMembers, }; operations.push({ type: 'replace', payload: { id: threadID, threadInfo: updatedThread, }, }); } } return operations; }, + reduceUserInfos(state: UserInfos, update: AccountDeletionUpdateInfo) { + const { deletedUserID } = update; + if (!state[deletedUserID]) { + return state; + } + const { [deletedUserID]: deleted, ...rest } = state; + return rest; + }, }); diff --git a/lib/shared/updates/update-spec.js b/lib/shared/updates/update-spec.js index aad0ccea3..f687d0db7 100644 --- a/lib/shared/updates/update-spec.js +++ b/lib/shared/updates/update-spec.js @@ -1,23 +1,24 @@ // @flow import type { ThreadStoreOperation } from '../../ops/thread-store-ops.js'; import type { RawEntryInfo } from '../../types/entry-types.js'; import type { RawThreadInfos } from '../../types/thread-types.js'; import type { ClientUpdateInfo } from '../../types/update-types.js'; -import type { CurrentUserInfo } from '../../types/user-types.js'; +import type { CurrentUserInfo, UserInfos } from '../../types/user-types.js'; export type UpdateSpec = { +generateOpsForThreadUpdates?: ( storeThreadInfos: RawThreadInfos, update: UpdateInfo, ) => ?$ReadOnlyArray, +mergeEntryInfos?: ( entryIDs: Set, mergedEntryInfos: Array, update: UpdateInfo, ) => void, +reduceCurrentUser?: ( state: ?CurrentUserInfo, update: UpdateInfo, ) => ?CurrentUserInfo, + +reduceUserInfos?: (state: UserInfos, update: UpdateInfo) => UserInfos, };