diff --git a/keyserver/src/responders/redux-state-responders.js b/keyserver/src/responders/redux-state-responders.js index cc8fba0e6..76498acc9 100644 --- a/keyserver/src/responders/redux-state-responders.js +++ b/keyserver/src/responders/redux-state-responders.js @@ -1,339 +1,332 @@ // @flow import _keyBy from 'lodash/fp/keyBy.js'; import t, { type TInterface } from 'tcomb'; import { baseLegalPolicies } from 'lib/facts/policies.js'; import { daysToEntriesFromEntryInfos } from 'lib/reducers/entry-reducer.js'; import { freshMessageStore } from 'lib/reducers/message-reducer.js'; import { mostRecentlyReadThread } from 'lib/selectors/thread-selectors.js'; import { mostRecentMessageTimestamp } from 'lib/shared/message-utils.js'; import { threadHasPermission, threadIsPending, parsePendingThreadID, createPendingThread, } from 'lib/shared/thread-utils.js'; -import { - entryStoreValidator, - calendarQueryValidator, -} from 'lib/types/entry-types.js'; +import { entryStoreValidator } from 'lib/types/entry-types.js'; import { defaultCalendarFilters } from 'lib/types/filter-types.js'; import { inviteLinksStoreValidator } from 'lib/types/link-types.js'; import { defaultNumberPerThread, messageStoreValidator, } from 'lib/types/message-types.js'; import { threadPermissions } from 'lib/types/thread-permission-types.js'; import { threadTypes } from 'lib/types/thread-types-enum.js'; import { threadStoreValidator } from 'lib/types/thread-types.js'; import { currentUserInfoValidator, userInfosValidator, } from 'lib/types/user-types.js'; import { currentDateInTimeZone } from 'lib/utils/date-utils.js'; import { ServerError } from 'lib/utils/errors.js'; import { promiseAll } from 'lib/utils/promises.js'; import type { URLInfo } from 'lib/utils/url-utils.js'; import { tShape, ashoatKeyserverID } from 'lib/utils/validation-utils.js'; import { navInfoValidator } from 'web/types/nav-types.js'; import type { - InitialReduxState, + InitialReduxStateResponse, InitialKeyserverInfo, } from 'web/types/redux-types.js'; import { navInfoFromURL } from 'web/url-utils.js'; import { fetchEntryInfos } from '../fetchers/entry-fetchers.js'; import { fetchPrimaryInviteLinks } from '../fetchers/link-fetchers.js'; import { fetchMessageInfos } from '../fetchers/message-fetchers.js'; import { hasAnyNotAcknowledgedPolicies } from '../fetchers/policy-acknowledgment-fetchers.js'; import { fetchThreadInfos } from '../fetchers/thread-fetchers.js'; import { fetchCurrentUserInfo, fetchKnownUserInfos, fetchUserInfos, } from '../fetchers/user-fetchers.js'; import { getWebPushConfig } from '../push/providers.js'; import { setNewSession } from '../session/cookies.js'; import { Viewer } from '../session/viewer.js'; const initialKeyserverInfoValidator = tShape({ sessionID: t.maybe(t.String), updatesCurrentAsOf: t.Number, }); -export const initialReduxStateValidator: TInterface = - tShape({ +export const initialReduxStateValidator: TInterface = + tShape({ navInfo: navInfoValidator, currentUserInfo: currentUserInfoValidator, entryStore: entryStoreValidator, threadStore: threadStoreValidator, userInfos: userInfosValidator, - actualizedCalendarQuery: calendarQueryValidator, messageStore: messageStoreValidator, pushApiPublicKey: t.maybe(t.String), - dataLoaded: t.Boolean, commServicesAccessToken: t.Nil, inviteLinksStore: inviteLinksStoreValidator, keyserverInfo: initialKeyserverInfoValidator, }); async function getInitialReduxStateResponder( viewer: Viewer, urlInfo: URLInfo, -): Promise { +): Promise { const hasNotAcknowledgedPoliciesPromise = hasAnyNotAcknowledgedPolicies( viewer.id, baseLegalPolicies, ); const initialNavInfoPromise = (async () => { try { let backupInfo = { now: currentDateInTimeZone(viewer.timeZone), }; // Some user ids in selectedUserList might not exist in the userInfos // (e.g. they were included in the results of the user search endpoint) // Because of that we keep their userInfos inside the navInfo. if (urlInfo.selectedUserList) { const fetchedUserInfos = await fetchUserInfos(urlInfo.selectedUserList); const userInfos = {}; for (const userID in fetchedUserInfos) { const userInfo = fetchedUserInfos[userID]; if (userInfo.username) { userInfos[userID] = userInfo; } } backupInfo = { userInfos, ...backupInfo }; } return navInfoFromURL(urlInfo, backupInfo); } catch (e) { throw new ServerError(e.message); } })(); const calendarQueryPromise = (async () => { const initialNavInfo = await initialNavInfoPromise; return { startDate: initialNavInfo.startDate, endDate: initialNavInfo.endDate, filters: defaultCalendarFilters, }; })(); const messageSelectionCriteria = { joinedThreads: true }; const initialTime = Date.now(); const threadInfoPromise = fetchThreadInfos(viewer); const messageInfoPromise = fetchMessageInfos( viewer, messageSelectionCriteria, defaultNumberPerThread, ); const entryInfoPromise = (async () => { const calendarQuery = await calendarQueryPromise; return await fetchEntryInfos(viewer, [calendarQuery]); })(); const currentUserInfoPromise = fetchCurrentUserInfo(viewer); const userInfoPromise = fetchKnownUserInfos(viewer); const sessionIDPromise = (async () => { const calendarQuery = await calendarQueryPromise; if (viewer.loggedIn) { await setNewSession(viewer, calendarQuery, initialTime); } return viewer.sessionID; })(); const threadStorePromise = (async () => { const [{ threadInfos }, hasNotAcknowledgedPolicies] = await Promise.all([ threadInfoPromise, hasNotAcknowledgedPoliciesPromise, ]); return { threadInfos: hasNotAcknowledgedPolicies ? {} : threadInfos }; })(); const messageStorePromise = (async () => { const [ { threadInfos }, { rawMessageInfos, truncationStatuses }, hasNotAcknowledgedPolicies, ] = await Promise.all([ threadInfoPromise, messageInfoPromise, hasNotAcknowledgedPoliciesPromise, ]); if (hasNotAcknowledgedPolicies) { return { messages: {}, threads: {}, local: {}, currentAsOf: { [ashoatKeyserverID]: 0 }, }; } const { messageStore: freshStore } = freshMessageStore( rawMessageInfos, truncationStatuses, mostRecentMessageTimestamp(rawMessageInfos, initialTime), threadInfos, ); return freshStore; })(); const entryStorePromise = (async () => { const [{ rawEntryInfos }, hasNotAcknowledgedPolicies] = await Promise.all([ entryInfoPromise, hasNotAcknowledgedPoliciesPromise, ]); if (hasNotAcknowledgedPolicies) { return { entryInfos: {}, daysToEntries: {}, lastUserInteractionCalendar: 0, }; } return { entryInfos: _keyBy('id')(rawEntryInfos), daysToEntries: daysToEntriesFromEntryInfos(rawEntryInfos), lastUserInteractionCalendar: initialTime, }; })(); const userInfosPromise = (async () => { const [userInfos, hasNotAcknowledgedPolicies] = await Promise.all([ userInfoPromise, hasNotAcknowledgedPoliciesPromise, ]); return hasNotAcknowledgedPolicies ? {} : userInfos; })(); const navInfoPromise = (async () => { const [ { threadInfos }, messageStore, currentUserInfo, userInfos, finalNavInfo, ] = await Promise.all([ threadInfoPromise, messageStorePromise, currentUserInfoPromise, userInfosPromise, initialNavInfoPromise, ]); const requestedActiveChatThreadID = finalNavInfo.activeChatThreadID; if ( requestedActiveChatThreadID && !threadIsPending(requestedActiveChatThreadID) && !threadHasPermission( threadInfos[requestedActiveChatThreadID], threadPermissions.VISIBLE, ) ) { finalNavInfo.activeChatThreadID = null; } if (!finalNavInfo.activeChatThreadID) { const mostRecentThread = mostRecentlyReadThread( messageStore, threadInfos, ); if (mostRecentThread) { finalNavInfo.activeChatThreadID = mostRecentThread; } } if ( finalNavInfo.activeChatThreadID && threadIsPending(finalNavInfo.activeChatThreadID) && finalNavInfo.pendingThread?.id !== finalNavInfo.activeChatThreadID ) { const pendingThreadData = parsePendingThreadID( finalNavInfo.activeChatThreadID, ); if ( pendingThreadData && pendingThreadData.threadType !== threadTypes.SIDEBAR && currentUserInfo.id ) { const members = [...pendingThreadData.memberIDs, currentUserInfo.id] .map(id => { const userInfo = userInfos[id]; if (!userInfo || !userInfo.username) { return undefined; } const { username } = userInfo; return { id, username }; }) .filter(Boolean); const newPendingThread = createPendingThread({ viewerID: currentUserInfo.id, threadType: pendingThreadData.threadType, members, }); finalNavInfo.activeChatThreadID = newPendingThread.id; finalNavInfo.pendingThread = newPendingThread; } } return finalNavInfo; })(); const currentAsOfPromise = (async () => { const hasNotAcknowledgedPolicies = await hasNotAcknowledgedPoliciesPromise; return hasNotAcknowledgedPolicies ? 0 : initialTime; })(); const pushApiPublicKeyPromise = (async () => { const pushConfig = await getWebPushConfig(); if (!pushConfig) { if (process.env.NODE_ENV !== 'development') { console.warn('keyserver/secrets/web_push_config.json should exist'); } return null; } return pushConfig.publicKey; })(); const inviteLinksStorePromise = (async () => { const primaryInviteLinks = await fetchPrimaryInviteLinks(viewer); const links = {}; for (const link of primaryInviteLinks) { if (link.primary) { links[link.communityID] = { primaryLink: link, }; } } return { links, }; })(); const keyserverInfoPromise = (async () => { const { sessionID, updatesCurrentAsOf } = await promiseAll({ sessionID: sessionIDPromise, updatesCurrentAsOf: currentAsOfPromise, }); return { sessionID, updatesCurrentAsOf, }; })(); - const initialReduxState: InitialReduxState = await promiseAll({ + const initialReduxState: InitialReduxStateResponse = await promiseAll({ navInfo: navInfoPromise, currentUserInfo: currentUserInfoPromise, entryStore: entryStorePromise, threadStore: threadStorePromise, userInfos: userInfosPromise, - actualizedCalendarQuery: calendarQueryPromise, messageStore: messageStorePromise, - dataLoaded: viewer.loggedIn, pushApiPublicKey: pushApiPublicKeyPromise, commServicesAccessToken: null, inviteLinksStore: inviteLinksStorePromise, keyserverInfo: keyserverInfoPromise, }); return initialReduxState; } export { getInitialReduxStateResponder }; diff --git a/web/redux/action-types.js b/web/redux/action-types.js index 5cf923fb7..d7e4336a5 100644 --- a/web/redux/action-types.js +++ b/web/redux/action-types.js @@ -1,40 +1,149 @@ // @flow -import type { CallServerEndpoint } from 'lib/utils/call-server-endpoint.js'; +import { defaultCalendarFilters } from 'lib/types/filter-types.js'; +import { extractKeyserverIDFromID } from 'lib/utils/action-utils.js'; +import { useKeyserverCall } from 'lib/utils/keyserver-call.js'; +import type { CallKeyserverEndpoint } from 'lib/utils/keyserver-call.js'; import type { URLInfo } from 'lib/utils/url-utils.js'; +import { ashoatKeyserverID } from 'lib/utils/validation-utils.js'; -import type { InitialReduxState } from '../types/redux-types.js'; +import type { + InitialReduxState, + InitialReduxStateResponse, + InitialKeyserverInfo, +} from '../types/redux-types.js'; export const updateNavInfoActionType = 'UPDATE_NAV_INFO'; export const updateWindowDimensionsActionType = 'UPDATE_WINDOW_DIMENSIONS'; export const updateWindowActiveActionType = 'UPDATE_WINDOW_ACTIVE'; export const setInitialReduxState = 'SET_INITIAL_REDUX_STATE'; const getInitialReduxStateCallServerEndpointOptions = { timeout: 300000 }; const getInitialReduxState = ( - callServerEndpoint: CallServerEndpoint, - ): (URLInfo => Promise) => + callKeyserverEndpoint: CallKeyserverEndpoint, + allKeyserverIDs: $ReadOnlyArray, + ): ((input: URLInfo) => Promise) => async urlInfo => { - const response = await callServerEndpoint( - 'get_initial_redux_state', - urlInfo, - getInitialReduxStateCallServerEndpointOptions, - ); + const requests = {}; + const { thread, inviteSecret, ...rest } = urlInfo; + const threadKeyserverID = thread ? extractKeyserverIDFromID(thread) : null; + + for (const keyserverID of allKeyserverIDs) { + if (keyserverID === threadKeyserverID) { + requests[keyserverID] = urlInfo; + } else { + requests[keyserverID] = rest; + } + } + + const responses: { +[string]: InitialReduxStateResponse } = + await callKeyserverEndpoint( + 'get_initial_redux_state', + requests, + getInitialReduxStateCallServerEndpointOptions, + ); + + const { + currentUserInfo, + userInfos, + pushApiPublicKey, + commServicesAccessToken, + navInfo, + } = responses[ashoatKeyserverID]; + + const dataLoaded = currentUserInfo && !currentUserInfo.anonymous; + const actualizedCalendarQuery = { + startDate: navInfo.startDate, + endDate: navInfo.endDate, + filters: defaultCalendarFilters, + }; + + const entryStore = { + daysToEntries: {}, + entryInfos: {}, + lastUserInteractionCalendar: 0, + }; + const threadStore = { + threadInfos: {}, + }; + const messageStore = { + currentAsOf: {}, + local: {}, + messages: {}, + threads: {}, + }; + const inviteLinksStore = { + links: {}, + }; + let keyserverInfos: { [keyserverID: string]: InitialKeyserverInfo } = {}; + + for (const keyserverID in responses) { + entryStore.daysToEntries = { + ...entryStore.daysToEntries, + ...responses[keyserverID].entryStore.daysToEntries, + }; + entryStore.entryInfos = { + ...entryStore.entryInfos, + ...responses[keyserverID].entryStore.entryInfos, + }; + entryStore.lastUserInteractionCalendar = Math.max( + entryStore.lastUserInteractionCalendar, + responses[keyserverID].entryStore.lastUserInteractionCalendar, + ); + + threadStore.threadInfos = { + ...threadStore.threadInfos, + ...responses[keyserverID].threadStore.threadInfos, + }; + + messageStore.currentAsOf = { + ...messageStore.currentAsOf, + ...responses[keyserverID].messageStore.currentAsOf, + }; + messageStore.messages = { + ...messageStore.messages, + ...responses[keyserverID].messageStore.messages, + }; + messageStore.threads = { + ...messageStore.threads, + ...responses[keyserverID].messageStore.threads, + }; + + inviteLinksStore.links = { + ...inviteLinksStore.links, + ...responses[keyserverID].inviteLinksStore.links, + }; + + keyserverInfos = { + ...keyserverInfos, + [keyserverID]: responses[keyserverID].keyserverInfo, + }; + } + return { - navInfo: response.navInfo, - currentUserInfo: response.currentUserInfo, - entryStore: response.entryStore, - threadStore: response.threadStore, - userInfos: response.userInfos, - actualizedCalendarQuery: response.actualizedCalendarQuery, - messageStore: response.messageStore, - dataLoaded: response.dataLoaded, - pushApiPublicKey: response.pushApiPublicKey, - commServicesAccessToken: response.commServicesAccessToken, - inviteLinksStore: response.inviteLinksStore, - keyserverInfo: response.keyserverInfo, + navInfo: { + ...navInfo, + inviteSecret, + }, + currentUserInfo, + entryStore, + threadStore, + userInfos, + actualizedCalendarQuery, + messageStore, + dataLoaded, + pushApiPublicKey, + commServicesAccessToken, + inviteLinksStore, + keyserverInfos, }; }; -export { getInitialReduxState }; +function useGetInitialReduxState(): ( + input: URLInfo, +) => Promise { + return useKeyserverCall(getInitialReduxState); +} + +export { useGetInitialReduxState }; diff --git a/web/redux/initial-state-gate.js b/web/redux/initial-state-gate.js index 832cdd880..79b858356 100644 --- a/web/redux/initial-state-gate.js +++ b/web/redux/initial-state-gate.js @@ -1,75 +1,77 @@ // @flow import * as React from 'react'; import { useDispatch } from 'react-redux'; import { PersistGate } from 'redux-persist/es/integration/react.js'; import type { Persistor } from 'redux-persist/es/types'; -import { useServerCall } from 'lib/utils/action-utils.js'; import { convertIDToNewSchema } from 'lib/utils/migration-utils.js'; import { infoFromURL } from 'lib/utils/url-utils.js'; import { ashoatKeyserverID } from 'lib/utils/validation-utils.js'; -import { getInitialReduxState, setInitialReduxState } from './action-types.js'; +import { + setInitialReduxState, + useGetInitialReduxState, +} from './action-types.js'; import { useSelector } from './redux-utils.js'; import Loading from '../loading.react.js'; type Props = { +persistor: Persistor, +children: React.Node, }; function InitialReduxStateGate(props: Props): React.Node { const { children, persistor } = props; - const callGetInitialReduxState = useServerCall(getInitialReduxState); + const callGetInitialReduxState = useGetInitialReduxState(); const dispatch = useDispatch(); const [initError, setInitError] = React.useState(null); React.useEffect(() => { if (initError) { throw initError; } }, [initError]); const isRehydrated = useSelector(state => !!state._persist?.rehydrated); const prevIsRehydrated = React.useRef(false); React.useEffect(() => { if (!prevIsRehydrated.current && isRehydrated) { prevIsRehydrated.current = isRehydrated; (async () => { try { let urlInfo = infoFromURL(decodeURI(window.location.href)); // Handle older links if (urlInfo.thread) { urlInfo = { ...urlInfo, thread: convertIDToNewSchema(urlInfo.thread, ashoatKeyserverID), }; } const payload = await callGetInitialReduxState(urlInfo); dispatch({ type: setInitialReduxState, payload }); } catch (err) { setInitError(err); } })(); } }, [callGetInitialReduxState, dispatch, isRehydrated]); const initialStateLoaded = useSelector(state => state.initialStateLoaded); const childFunction = React.useCallback( // This argument is passed from `PersistGate`. It means that the state is // rehydrated and we can start fetching the initial info. bootstrapped => { if (bootstrapped && initialStateLoaded) { return children; } else { return ; } }, [children, initialStateLoaded], ); return {childFunction}; } export default InitialReduxStateGate; diff --git a/web/redux/redux-setup.js b/web/redux/redux-setup.js index ae68784e0..08dd6d758 100644 --- a/web/redux/redux-setup.js +++ b/web/redux/redux-setup.js @@ -1,339 +1,340 @@ // @flow import invariant from 'invariant'; import type { PersistState } from 'redux-persist/es/types.js'; import { logOutActionTypes, deleteAccountActionTypes, } from 'lib/actions/user-actions.js'; import { reportStoreOpsHandlers } from 'lib/ops/report-store-ops.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 { Shape } from 'lib/types/core.js'; import type { CryptoStore, OLMIdentityKeys, PickledOLMAccount, } 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 { 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, setPrimaryIdentityKeys, setNotificationIdentityKeys, setPickledNotificationAccount, setPickledPrimaryAccount, } from './crypto-store-reducer.js'; import reduceNavInfo from './nav-reducer.js'; import { getVisibility } from './visibility.js'; import { getDatabaseModule } from '../database/database-module-provider.js'; import { activeThreadSelector } from '../selectors/nav-selectors.js'; import { type NavInfo } from '../types/nav-types.js'; import type { InitialReduxState } from '../types/redux-types.js'; import { workerRequestMessageTypes } from '../types/worker-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, }; export type Action = | BaseAction | { type: 'UPDATE_NAV_INFO', payload: Shape } | { type: 'UPDATE_WINDOW_DIMENSIONS', payload: WindowDimensions, } | { type: 'UPDATE_WINDOW_ACTIVE', payload: boolean, } | { +type: 'SET_PRIMARY_IDENTITY_KEYS', payload: ?OLMIdentityKeys } | { +type: 'SET_NOTIFICATION_IDENTITY_KEYS', payload: ?OLMIdentityKeys } | { +type: 'SET_PICKLED_PRIMARY_ACCOUNT', payload: ?PickledOLMAccount } | { +type: 'SET_PICKLED_NOTIFICATION_ACCOUNT', payload: ?PickledOLMAccount } | { +type: 'SET_INITIAL_REDUX_STATE', payload: InitialReduxState }; export function reducer(oldState: AppState | void, action: Action): AppState { invariant(oldState, 'should be set'); let state = oldState; if (action.type === setInitialReduxState) { - const { userInfos, keyserverInfo, ...rest } = action.payload; + const { userInfos, keyserverInfos, ...rest } = action.payload; + const newKeyserverInfos = { ...state.keyserverStore.keyserverInfos }; + for (const keyserverID in keyserverInfos) { + newKeyserverInfos[keyserverID] = { + ...newKeyserverInfos[keyserverID], + ...keyserverInfos[keyserverID], + }; + } return validateState(oldState, { ...state, ...rest, userStore: { userInfos }, keyserverStore: { ...state.keyserverStore, - keyserverInfos: { - ...state.keyserverStore.keyserverInfos, - [ashoatKeyserverID]: { - ...state.keyserverStore.keyserverInfos[ashoatKeyserverID], - ...keyserverInfo, - }, - }, + keyserverInfos: newKeyserverInfos, }, initialStateLoaded: true, }); } else if (action.type === updateWindowDimensionsActionType) { return validateState(oldState, { ...state, windowDimensions: action.payload, }); } else if (action.type === updateWindowActiveActionType) { return validateState(oldState, { ...state, windowActive: action.payload, }); } else if (action.type === setNewSessionActionType) { if ( invalidSessionDowngrade( oldState, action.payload.sessionChange.currentUserInfo, action.payload.preRequestUserState, ) ) { return oldState; } state = { ...state, keyserverStore: { ...state.keyserverStore, keyserverInfos: { ...state.keyserverStore.keyserverInfos, [ashoatKeyserverID]: { ...state.keyserverStore.keyserverInfos[ashoatKeyserverID], sessionID: action.payload.sessionChange.sessionID, }, }, }, }; } else if ( (action.type === logOutActionTypes.success && invalidSessionDowngrade( oldState, action.payload.currentUserInfo, action.payload.preRequestUserState, )) || (action.type === deleteAccountActionTypes.success && invalidSessionDowngrade( oldState, action.payload.currentUserInfo, action.payload.preRequestUserState, )) ) { return oldState; } if ( action.type !== updateNavInfoActionType && action.type !== setPrimaryIdentityKeys && action.type !== setNotificationIdentityKeys && action.type !== setPickledPrimaryAccount && action.type !== setPickledNotificationAccount ) { const baseReducerResult = baseReducer(state, action); state = baseReducerResult.state; const { storeOperations: { draftStoreOperations, reportStoreOperations }, } = baseReducerResult; if (draftStoreOperations.length > 0 || reportStoreOperations.length > 0) { (async () => { const databaseModule = await getDatabaseModule(); const isSupported = await databaseModule.isDatabaseSupported(); if (!isSupported) { return; } const convertedReportStoreOperations = reportStoreOpsHandlers.convertOpsToClientDBOps(reportStoreOperations); await databaseModule.schedule({ type: workerRequestMessageTypes.PROCESS_STORE_OPERATIONS, storeOperations: { draftStoreOperations, reportStoreOperations: convertedReportStoreOperations, }, }); })(); } } const communityPickerStore = reduceCommunityPickerStore( state.communityPickerStore, action, ); state = { ...state, navInfo: reduceNavInfo( state.navInfo, action, state.threadStore.threadInfos, ), cryptoStore: reduceCryptoStore(state.cryptoStore, action), communityPickerStore, }; return validateState(oldState, state); } function validateState(oldState: AppState, state: AppState): AppState { 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 state = { ...state, threadStore: { ...state.threadStore, threadInfos: { ...state.threadStore.threadInfos, [activeThread]: { ...state.threadStore.threadInfos[activeThread], currentUser: { ...state.threadStore.threadInfos[activeThread].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]: { ...state.threadActivityStore[activeThread], lastNavigatedTo: now, }, }, }; } return state; } diff --git a/web/types/redux-types.js b/web/types/redux-types.js index 3b65a443d..647a6520d 100644 --- a/web/types/redux-types.js +++ b/web/types/redux-types.js @@ -1,29 +1,42 @@ // @flow import type { EntryStore, CalendarQuery } from 'lib/types/entry-types.js'; import type { InviteLinksStore } from 'lib/types/link-types.js'; import type { MessageStore } from 'lib/types/message-types.js'; import type { ThreadStore } from 'lib/types/thread-types.js'; import type { CurrentUserInfo, UserInfos } from 'lib/types/user-types.js'; import type { NavInfo } from '../types/nav-types.js'; -export type InitialReduxState = { +export type InitialReduxStateResponse = { +navInfo: NavInfo, +currentUserInfo: CurrentUserInfo, +entryStore: EntryStore, +threadStore: ThreadStore, +userInfos: UserInfos, - +actualizedCalendarQuery: CalendarQuery, +messageStore: MessageStore, - +dataLoaded: boolean, +pushApiPublicKey: ?string, +commServicesAccessToken: null, +inviteLinksStore: InviteLinksStore, +keyserverInfo: InitialKeyserverInfo, }; +export type InitialReduxState = { + +navInfo: NavInfo, + +currentUserInfo: CurrentUserInfo, + +entryStore: EntryStore, + +threadStore: ThreadStore, + +userInfos: UserInfos, + +messageStore: MessageStore, + +pushApiPublicKey: ?string, + +commServicesAccessToken: null, + +inviteLinksStore: InviteLinksStore, + +dataLoaded: boolean, + +actualizedCalendarQuery: CalendarQuery, + +keyserverInfos: { +[keyserverID: string]: InitialKeyserverInfo }, +}; + export type InitialKeyserverInfo = { +sessionID: ?string, +updatesCurrentAsOf: number, };