diff --git a/lib/reducers/keyserver-reducer.js b/lib/reducers/keyserver-reducer.js index 7ad8b360c..aa837fab4 100644 --- a/lib/reducers/keyserver-reducer.js +++ b/lib/reducers/keyserver-reducer.js @@ -1,391 +1,400 @@ // @flow import { unsupervisedBackgroundActionType } from './lifecycle-state-reducer.js'; import { updateActivityActionTypes } from '../actions/activity-actions.js'; import { updateLastCommunicatedPlatformDetailsActionType, setDeviceTokenActionTypes, } from '../actions/device-actions.js'; import { addKeyserverActionType, removeKeyserverActionType, } from '../actions/keyserver-actions.js'; import { siweAuthActionTypes } from '../actions/siwe-actions.js'; import { logOutActionTypes, deleteKeyserverAccountActionTypes, registerActionTypes, logInActionTypes, resetUserStateActionType, } from '../actions/user-actions.js'; import { queueActivityUpdatesActionType } from '../types/activity-types.js'; import type { KeyserverInfos, KeyserverStore } from '../types/keyserver-types'; import type { BaseAction } from '../types/redux-types.js'; import { fullStateSyncActionType, incrementalStateSyncActionType, updateConnectionStatusActionType, setLateResponseActionType, updateDisconnectedBarActionType, setConnectionIssueActionType, } from '../types/socket-types.js'; import { updateTypes } from '../types/update-types-enum.js'; import { processUpdatesActionType } from '../types/update-types.js'; import { setNewSessionActionType } from '../utils/action-utils.js'; import { getConfig } from '../utils/config.js'; import { setURLPrefix } from '../utils/url-utils.js'; import { ashoatKeyserverID } from '../utils/validation-utils.js'; export default function reduceKeyserverStore( state: KeyserverStore, action: BaseAction, ): KeyserverStore { if (action.type === addKeyserverActionType) { return { ...state, keyserverInfos: { ...state.keyserverInfos, [action.payload.keyserverAdminUserID]: { ...action.payload.newKeyserverInfo, }, }, }; } else if (action.type === removeKeyserverActionType) { const { [action.payload.keyserverAdminUserID]: _, ...rest } = state.keyserverInfos; return { ...state, keyserverInfos: rest, }; } else if (action.type === resetUserStateActionType) { // this action is only dispatched on native const keyserverInfos = { ...state.keyserverInfos }; for (const keyserverID in keyserverInfos) { const stateCookie = state.keyserverInfos[keyserverID]?.cookie; const cookie = stateCookie && stateCookie.startsWith('anonymous=') ? stateCookie : null; keyserverInfos[keyserverID] = { ...keyserverInfos[keyserverID], cookie }; } return { ...state, keyserverInfos, }; } else if (action.type === setNewSessionActionType) { - const { keyserverID } = action.payload; - if (action.payload.sessionChange.cookie !== undefined) { + const { keyserverID, sessionChange } = action.payload; + if (!state.keyserverInfos[keyserverID]) { + if (sessionChange.cookie?.startsWith('user=')) { + console.log( + 'received sessionChange with user cookie, ' + + `but keyserver ${keyserverID} is not in KeyserverStore!`, + ); + } + return state; + } + if (sessionChange.cookie !== undefined) { state = { ...state, keyserverInfos: { ...state.keyserverInfos, [keyserverID]: { ...state.keyserverInfos[keyserverID], - cookie: action.payload.sessionChange.cookie, + cookie: sessionChange.cookie, }, }, }; } - if (action.payload.sessionChange.cookieInvalidated) { + if (sessionChange.cookieInvalidated) { state = { ...state, keyserverInfos: { ...state.keyserverInfos, [keyserverID]: { ...state.keyserverInfos[keyserverID], connection: { ...state.keyserverInfos[keyserverID].connection, queuedActivityUpdates: [], }, }, }, }; } return state; } else if ( action.type === logInActionTypes.success || action.type === siweAuthActionTypes.success ) { const { updatesCurrentAsOf } = action.payload; let keyserverInfos = { ...state.keyserverInfos }; for (const keyserverID in updatesCurrentAsOf) { keyserverInfos = { ...keyserverInfos, [keyserverID]: { ...keyserverInfos[keyserverID], updatesCurrentAsOf: updatesCurrentAsOf[keyserverID], lastCommunicatedPlatformDetails: getConfig().platformDetails, }, }; } return { ...state, keyserverInfos, }; } else if (action.type === fullStateSyncActionType) { const { keyserverID } = action.payload; return { ...state, keyserverInfos: { ...state.keyserverInfos, [keyserverID]: { ...state.keyserverInfos[keyserverID], updatesCurrentAsOf: action.payload.updatesCurrentAsOf, }, }, }; } else if (action.type === incrementalStateSyncActionType) { const { keyserverID } = action.payload; let { deviceToken } = state.keyserverInfos[keyserverID]; for (const update of action.payload.updatesResult.newUpdates) { if ( update.type === updateTypes.BAD_DEVICE_TOKEN && update.deviceToken === state.keyserverInfos[keyserverID].deviceToken ) { deviceToken = null; break; } } return { ...state, keyserverInfos: { ...state.keyserverInfos, [keyserverID]: { ...state.keyserverInfos[keyserverID], updatesCurrentAsOf: action.payload.updatesResult.currentAsOf, deviceToken, }, }, }; } else if (action.type === processUpdatesActionType) { const { keyserverID } = action.payload; const updatesCurrentAsOf = Math.max( action.payload.updatesResult.currentAsOf, state.keyserverInfos[keyserverID].updatesCurrentAsOf, ); return { ...state, keyserverInfos: { ...state.keyserverInfos, [keyserverID]: { ...state.keyserverInfos[keyserverID], updatesCurrentAsOf, }, }, }; } else if (action.type === setURLPrefix) { return { ...state, keyserverInfos: { ...state.keyserverInfos, [ashoatKeyserverID]: { ...state.keyserverInfos[ashoatKeyserverID], urlPrefix: action.payload, }, }, }; } else if (action.type === updateLastCommunicatedPlatformDetailsActionType) { const { keyserverID, platformDetails } = action.payload; return { ...state, keyserverInfos: { ...state.keyserverInfos, [keyserverID]: { ...state.keyserverInfos[keyserverID], lastCommunicatedPlatformDetails: platformDetails, }, }, }; } else if (action.type === registerActionTypes.success) { state = { ...state, keyserverInfos: { ...state.keyserverInfos, [ashoatKeyserverID]: { ...state.keyserverInfos[ashoatKeyserverID], lastCommunicatedPlatformDetails: getConfig().platformDetails, }, }, }; } else if (action.type === updateConnectionStatusActionType) { const { keyserverID, status } = action.payload; return { ...state, keyserverInfos: { ...state.keyserverInfos, [keyserverID]: { ...state.keyserverInfos[keyserverID], connection: { ...state.keyserverInfos[keyserverID].connection, status, lateResponses: [], }, }, }, }; } else if (action.type === unsupervisedBackgroundActionType) { const { keyserverID } = action.payload; return { ...state, keyserverInfos: { ...state.keyserverInfos, [keyserverID]: { ...state.keyserverInfos[keyserverID], connection: { ...state.keyserverInfos[keyserverID].connection, status: 'disconnected', lateResponses: [], }, }, }, }; } else if (action.type === queueActivityUpdatesActionType) { const { activityUpdates, keyserverID } = action.payload; const oldConnection = state.keyserverInfos[keyserverID].connection; const connection = { ...oldConnection, queuedActivityUpdates: [ ...oldConnection.queuedActivityUpdates.filter(existingUpdate => { for (const activityUpdate of activityUpdates) { if ( ((existingUpdate.focus && activityUpdate.focus) || (existingUpdate.focus === false && activityUpdate.focus !== undefined)) && existingUpdate.threadID === activityUpdate.threadID ) { return false; } } return true; }), ...activityUpdates, ], }; return { ...state, keyserverInfos: { ...state.keyserverInfos, [keyserverID]: { ...state.keyserverInfos[keyserverID], connection, }, }, }; } else if (action.type === updateActivityActionTypes.success) { const { activityUpdates } = action.payload; let keyserverInfos = { ...state.keyserverInfos }; for (const keyserverID in activityUpdates) { const oldConnection = keyserverInfos[keyserverID].connection; const queuedActivityUpdates = oldConnection.queuedActivityUpdates.filter( activityUpdate => !activityUpdates[keyserverID].includes(activityUpdate), ); keyserverInfos = { ...keyserverInfos, [keyserverID]: { ...keyserverInfos[keyserverID], connection: { ...oldConnection, queuedActivityUpdates }, }, }; } return { ...state, keyserverInfos, }; } else if ( action.type === logOutActionTypes.success || action.type === deleteKeyserverAccountActionTypes.success ) { // We want to remove all keyservers but Ashoat's keyserver const oldConnection = state.keyserverInfos[ashoatKeyserverID].connection; const keyserverInfos = { [ashoatKeyserverID]: { ...state.keyserverInfos[ashoatKeyserverID], connection: { ...oldConnection, connectionIssue: null, queuedActivityUpdates: [], }, cookie: null, }, }; return { ...state, keyserverInfos, }; } else if (action.type === setLateResponseActionType) { const { messageID, isLate, keyserverID } = action.payload; const lateResponsesSet = new Set( state.keyserverInfos[keyserverID].connection.lateResponses, ); if (isLate) { lateResponsesSet.add(messageID); } else { lateResponsesSet.delete(messageID); } return { ...state, keyserverInfos: { ...state.keyserverInfos, [keyserverID]: { ...state.keyserverInfos[keyserverID], connection: { ...state.keyserverInfos[keyserverID].connection, lateResponses: [...lateResponsesSet], }, }, }, }; } else if (action.type === updateDisconnectedBarActionType) { const { keyserverID } = action.payload; return { ...state, keyserverInfos: { ...state.keyserverInfos, [keyserverID]: { ...state.keyserverInfos[keyserverID], connection: { ...state.keyserverInfos[keyserverID].connection, showDisconnectedBar: action.payload.visible, }, }, }, }; } else if (action.type === setDeviceTokenActionTypes.success) { const { deviceTokens } = action.payload; const keyserverInfos: { ...KeyserverInfos } = { ...state.keyserverInfos }; for (const keyserverID in deviceTokens) { keyserverInfos[keyserverID] = { ...keyserverInfos[keyserverID], deviceToken: deviceTokens[keyserverID], }; } return { ...state, keyserverInfos, }; } else if (action.type === setConnectionIssueActionType) { const { connectionIssue, keyserverID } = action.payload; return { ...state, keyserverInfos: { ...state.keyserverInfos, [keyserverID]: { ...state.keyserverInfos[keyserverID], connection: { ...state.keyserverInfos[keyserverID].connection, connectionIssue, }, }, }, }; } return state; } diff --git a/web/redux/redux-setup.js b/web/redux/redux-setup.js index 49978fbbd..20898e303 100644 --- a/web/redux/redux-setup.js +++ b/web/redux/redux-setup.js @@ -1,380 +1,389 @@ // @flow import invariant from 'invariant'; import type { PersistState } from 'redux-persist/es/types.js'; import { logOutActionTypes, deleteKeyserverAccountActionTypes, } from 'lib/actions/user-actions.js'; import { type ThreadStoreOperation, threadStoreOpsHandlers, } from 'lib/ops/thread-store-ops.js'; import { reduceLoadingStatuses } from 'lib/reducers/loading-reducer.js'; import baseReducer from 'lib/reducers/master-reducer.js'; import { mostRecentlyReadThreadSelector } from 'lib/selectors/thread-selectors.js'; import { isLoggedIn } from 'lib/selectors/user-selectors.js'; import { invalidSessionDowngrade } from 'lib/shared/session-utils.js'; import type { CryptoStore } from 'lib/types/crypto-types.js'; import type { DraftStore } from 'lib/types/draft-types.js'; 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'; import type { LoadingStatus } from 'lib/types/loading-types.js'; import type { MessageStore } from 'lib/types/message-types.js'; import type { UserPolicies } from 'lib/types/policy-types.js'; import type { BaseAction } from 'lib/types/redux-types.js'; import type { ReportStore } from 'lib/types/report-types.js'; import type { StoreOperations } from 'lib/types/store-ops-types.js'; import type { GlobalThemeInfo } from 'lib/types/theme-types.js'; import type { ThreadActivityStore } from 'lib/types/thread-activity-types'; import type { ThreadStore } from 'lib/types/thread-types.js'; import type { CurrentUserInfo, UserStore } from 'lib/types/user-types.js'; import { setNewSessionActionType } from 'lib/utils/action-utils.js'; import type { NotifPermissionAlertInfo } from 'lib/utils/push-alerts.js'; import { ashoatKeyserverID } from 'lib/utils/validation-utils.js'; import { updateWindowActiveActionType, updateNavInfoActionType, updateWindowDimensionsActionType, setInitialReduxState, } from './action-types.js'; import { reduceCommunityPickerStore } from './community-picker-reducer.js'; import { reduceCryptoStore, setCryptoStore } from './crypto-store-reducer.js'; import reduceNavInfo from './nav-reducer.js'; import { onStateDifference } from './redux-debug-utils.js'; import { getVisibility } from './visibility.js'; import { processDBStoreOperations } from '../database/utils/store.js'; import { activeThreadSelector } from '../selectors/nav-selectors.js'; import { type NavInfo } from '../types/nav-types.js'; import type { InitialReduxState } from '../types/redux-types.js'; export type WindowDimensions = { width: number, height: number }; export type CommunityPickerStore = { +chat: ?string, +calendar: ?string, }; export type AppState = { +navInfo: NavInfo, +currentUserInfo: ?CurrentUserInfo, +draftStore: DraftStore, +entryStore: EntryStore, +threadStore: ThreadStore, +userStore: UserStore, +messageStore: MessageStore, +loadingStatuses: { [key: string]: { [idx: number]: LoadingStatus } }, +calendarFilters: $ReadOnlyArray, +communityPickerStore: CommunityPickerStore, +windowDimensions: WindowDimensions, +notifPermissionAlertInfo: NotifPermissionAlertInfo, +actualizedCalendarQuery: CalendarQuery, +watchedThreadIDs: $ReadOnlyArray, +lifecycleState: LifecycleState, +enabledApps: EnabledApps, +reportStore: ReportStore, +nextLocalID: number, +dataLoaded: boolean, +windowActive: boolean, +userPolicies: UserPolicies, +cryptoStore: ?CryptoStore, +pushApiPublicKey: ?string, +_persist: ?PersistState, +commServicesAccessToken: ?string, +inviteLinksStore: InviteLinksStore, +keyserverStore: KeyserverStore, +threadActivityStore: ThreadActivityStore, +initialStateLoaded: boolean, +integrityStore: IntegrityStore, +globalThemeInfo: GlobalThemeInfo, +customServer: ?string, }; export type Action = | BaseAction | { type: 'UPDATE_NAV_INFO', payload: Partial } | { type: 'UPDATE_WINDOW_DIMENSIONS', payload: WindowDimensions, } | { type: 'UPDATE_WINDOW_ACTIVE', payload: boolean, } | { +type: 'SET_CRYPTO_STORE', payload: CryptoStore } | { +type: 'SET_INITIAL_REDUX_STATE', payload: InitialReduxState }; export function reducer(oldState: AppState | void, action: Action): AppState { invariant(oldState, 'should be set'); let state = oldState; let storeOperations: StoreOperations = { draftStoreOperations: [], threadStoreOperations: [], messageStoreOperations: [], reportStoreOperations: [], userStoreOperations: [], }; if (action.type === setInitialReduxState) { const { userInfos, keyserverInfos, ...rest } = action.payload; const newKeyserverInfos = { ...state.keyserverStore.keyserverInfos }; for (const keyserverID in keyserverInfos) { newKeyserverInfos[keyserverID] = { ...newKeyserverInfos[keyserverID], ...keyserverInfos[keyserverID], }; } return validateStateAndProcessDBOperations( oldState, { ...state, ...rest, userStore: { userInfos }, keyserverStore: { ...state.keyserverStore, keyserverInfos: newKeyserverInfos, }, initialStateLoaded: true, }, storeOperations, ); } else if (action.type === updateWindowDimensionsActionType) { return validateStateAndProcessDBOperations( oldState, { ...state, windowDimensions: action.payload, }, storeOperations, ); } else if (action.type === updateWindowActiveActionType) { return validateStateAndProcessDBOperations( oldState, { ...state, windowActive: action.payload, }, storeOperations, ); } else if (action.type === setNewSessionActionType) { - const { keyserverID } = action.payload; + const { keyserverID, sessionChange } = action.payload; + if (!state.keyserverStore.keyserverInfos[keyserverID]) { + if (sessionChange.cookie?.startsWith('user=')) { + console.log( + 'received sessionChange with user cookie, ' + + `but keyserver ${keyserverID} is not in KeyserverStore!`, + ); + } + return state; + } if ( invalidSessionDowngrade( oldState, - action.payload.sessionChange.currentUserInfo, + sessionChange.currentUserInfo, action.payload.preRequestUserState, keyserverID, ) ) { return { ...oldState, loadingStatuses: reduceLoadingStatuses(state.loadingStatuses, action), }; } state = { ...state, keyserverStore: { ...state.keyserverStore, keyserverInfos: { ...state.keyserverStore.keyserverInfos, [keyserverID]: { ...state.keyserverStore.keyserverInfos[keyserverID], - sessionID: action.payload.sessionChange.sessionID, + sessionID: sessionChange.sessionID, }, }, }, }; } else if ( (action.type === logOutActionTypes.success && invalidSessionDowngrade( oldState, action.payload.currentUserInfo, action.payload.preRequestUserState, ashoatKeyserverID, )) || (action.type === deleteKeyserverAccountActionTypes.success && invalidSessionDowngrade( oldState, action.payload.currentUserInfo, action.payload.preRequestUserState, ashoatKeyserverID, )) ) { return { ...oldState, loadingStatuses: reduceLoadingStatuses(state.loadingStatuses, action), }; } if ( action.type !== updateNavInfoActionType && action.type !== setCryptoStore ) { const baseReducerResult = baseReducer(state, action, onStateDifference); state = baseReducerResult.state; storeOperations = baseReducerResult.storeOperations; } const communityPickerStore = reduceCommunityPickerStore( state.communityPickerStore, action, ); state = { ...state, navInfo: reduceNavInfo( state.navInfo, action, state.threadStore.threadInfos, ), cryptoStore: reduceCryptoStore(state.cryptoStore, action), communityPickerStore, }; return validateStateAndProcessDBOperations(oldState, state, storeOperations); } function validateStateAndProcessDBOperations( oldState: AppState, state: AppState, storeOperations: StoreOperations, ): AppState { const updateActiveThreadOps: ThreadStoreOperation[] = []; if ( (state.navInfo.activeChatThreadID && !state.navInfo.pendingThread && !state.threadStore.threadInfos[state.navInfo.activeChatThreadID]) || (!state.navInfo.activeChatThreadID && isLoggedIn(state)) ) { // Makes sure the active thread always exists state = { ...state, navInfo: { ...state.navInfo, activeChatThreadID: mostRecentlyReadThreadSelector(state), }, }; } const activeThread = activeThreadSelector(state); if ( activeThread && !state.navInfo.pendingThread && state.threadStore.threadInfos[activeThread].currentUser.unread && getVisibility().hidden() ) { console.warn( `thread ${activeThread} is active and unread, ` + 'but visibilityjs reports the window is not visible', ); } if ( activeThread && !state.navInfo.pendingThread && state.threadStore.threadInfos[activeThread].currentUser.unread && typeof document !== 'undefined' && document && 'hasFocus' in document && !document.hasFocus() ) { console.warn( `thread ${activeThread} is active and unread, ` + 'but document.hasFocus() is false', ); } if ( activeThread && !getVisibility().hidden() && typeof document !== 'undefined' && document && 'hasFocus' in document && document.hasFocus() && !state.navInfo.pendingThread && state.threadStore.threadInfos[activeThread].currentUser.unread ) { // Makes sure a currently focused thread is never unread const activeThreadInfo = state.threadStore.threadInfos[activeThread]; // TODO (atul): Try to get rid of this ridiculous branching. if (activeThreadInfo.minimallyEncoded) { updateActiveThreadOps.push({ type: 'replace', payload: { id: activeThread, threadInfo: { ...activeThreadInfo, currentUser: { ...activeThreadInfo.currentUser, unread: false, }, }, }, }); } else { updateActiveThreadOps.push({ type: 'replace', payload: { id: activeThread, threadInfo: { ...activeThreadInfo, currentUser: { ...activeThreadInfo.currentUser, unread: false, }, }, }, }); } } const oldActiveThread = activeThreadSelector(oldState); if ( activeThread && oldActiveThread !== activeThread && state.messageStore.threads[activeThread] ) { const now = Date.now(); state = { ...state, threadActivityStore: { ...state.threadActivityStore, [(activeThread: string)]: { ...state.threadActivityStore[activeThread], lastNavigatedTo: now, }, }, }; } if (updateActiveThreadOps.length > 0) { state = { ...state, threadStore: threadStoreOpsHandlers.processStoreOperations( state.threadStore, updateActiveThreadOps, ), }; storeOperations = { ...storeOperations, threadStoreOperations: [ ...storeOperations.threadStoreOperations, ...updateActiveThreadOps, ], }; } void processDBStoreOperations( storeOperations, state.currentUserInfo?.id ?? null, ); return state; }