diff --git a/keyserver/src/responders/website-responders.js b/keyserver/src/responders/website-responders.js index bc9ff147b..d0b139206 100644 --- a/keyserver/src/responders/website-responders.js +++ b/keyserver/src/responders/website-responders.js @@ -1,839 +1,829 @@ // @flow import html from 'common-tags/lib/html/index.js'; import { detect as detectBrowser } from 'detect-browser'; import type { $Response, $Request } from 'express'; import fs from 'fs'; import _isEqual from 'lodash/fp/isEqual.js'; import _keyBy from 'lodash/fp/keyBy.js'; import * as React from 'react'; // eslint-disable-next-line import/extensions import ReactDOMServer from 'react-dom/server'; import t from 'tcomb'; import { promisify } from 'util'; import { inviteLinkUrl } from 'lib/facts/links.js'; import { baseLegalPolicies } from 'lib/facts/policies.js'; import stores from 'lib/facts/stores.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 { defaultWebEnabledApps } from 'lib/types/enabled-apps.js'; import { entryStoreValidator } from 'lib/types/entry-types.js'; import { defaultCalendarFilters } from 'lib/types/filter-types.js'; import { keyserverStoreValidator } from 'lib/types/keyserver-types.js'; import { inviteLinksStoreValidator } from 'lib/types/link-types.js'; import { defaultNumberPerThread, messageStoreValidator, } from 'lib/types/message-types.js'; import { defaultEnabledReports } from 'lib/types/report-types.js'; import { defaultConnectionInfo } from 'lib/types/socket-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 { defaultNotifPermissionAlertInfo } from 'lib/utils/push-alerts.js'; import { infoFromURL, urlInfoValidator } from 'lib/utils/url-utils.js'; import { tBool, tNumber, tShape, tString, ashoatKeyserverID, } from 'lib/utils/validation-utils.js'; import getTitle from 'web/title/getTitle.js'; import { navInfoValidator } from 'web/types/nav-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'; import { streamJSON, waitForStream } from '../utils/json-stream.js'; import { getAppURLFactsFromRequestURL, getCommAppURLFacts, } from '../utils/urls.js'; import { validateOutput, validateInput } from '../utils/validation-utils.js'; const { renderToNodeStream } = ReactDOMServer; const access = promisify(fs.access); const readFile = promisify(fs.readFile); const googleFontsURL = 'https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&family=Inter:wght@400;500;600&display=swap'; const localFontsURL = 'fonts/local-fonts.css'; async function getFontsURL() { try { await access(localFontsURL); return localFontsURL; } catch { return googleFontsURL; } } type AssetInfo = { +jsURL: string, +fontsURL: string, +cssInclude: string, +olmFilename: string, +commQueryExecutorFilename: string, +opaqueURL: string, }; let assetInfo: ?AssetInfo = null; async function getAssetInfo() { if (assetInfo) { return assetInfo; } if (process.env.NODE_ENV === 'development') { const fontsURL = await getFontsURL(); assetInfo = { jsURL: 'http://localhost:8080/dev.build.js', fontsURL, cssInclude: '', olmFilename: '', commQueryExecutorFilename: '', opaqueURL: 'http://localhost:8080/opaque-ke.wasm', }; return assetInfo; } try { const manifestString = await readFile('../web/dist/manifest.json', 'utf8'); const manifest = JSON.parse(manifestString); const webworkersManifestString = await readFile( '../web/dist/webworkers/manifest.json', 'utf8', ); const webworkersManifest = JSON.parse(webworkersManifestString); assetInfo = { jsURL: `compiled/${manifest['browser.js']}`, fontsURL: googleFontsURL, cssInclude: html` `, olmFilename: manifest['olm.wasm'], commQueryExecutorFilename: webworkersManifest['comm_query_executor.wasm'], opaqueURL: `compiled/${manifest['comm_opaque2_wasm_bg.wasm']}`, }; return assetInfo; } catch { throw new Error( 'Could not load manifest.json for web build. ' + 'Did you forget to run `yarn dev` in the web folder?', ); } } let webpackCompiledRootComponent: ?React.ComponentType<{}> = null; async function getWebpackCompiledRootComponentForSSR() { if (webpackCompiledRootComponent) { return webpackCompiledRootComponent; } try { // $FlowFixMe web/dist doesn't always exist const webpackBuild = await import('web/dist/app.build.cjs'); webpackCompiledRootComponent = webpackBuild.app.default; return webpackCompiledRootComponent; } catch { throw new Error( 'Could not load app.build.cjs. ' + 'Did you forget to run `yarn dev` in the web folder?', ); } } const initialReduxStateValidator = tShape({ navInfo: navInfoValidator, deviceID: t.Nil, currentUserInfo: currentUserInfoValidator, draftStore: t.irreducible('default draftStore', _isEqual({ drafts: {} })), entryStore: entryStoreValidator, threadStore: threadStoreValidator, userStore: tShape({ userInfos: userInfosValidator, inconsistencyReports: t.irreducible( 'default inconsistencyReports', _isEqual([]), ), }), messageStore: messageStoreValidator, loadingStatuses: t.irreducible('default loadingStatuses', _isEqual({})), calendarFilters: t.irreducible( 'defaultCalendarFilters', _isEqual(defaultCalendarFilters), ), communityPickerStore: t.irreducible( 'default communityPickerStore', _isEqual({ chat: null, calendar: null }), ), windowDimensions: t.irreducible( 'default windowDimensions', _isEqual({ width: 0, height: 0 }), ), notifPermissionAlertInfo: t.irreducible( 'default notifPermissionAlertInfo', _isEqual(defaultNotifPermissionAlertInfo), ), - connection: tShape({ - status: tString('connecting'), - queuedActivityUpdates: t.irreducible( - 'default queuedActivityUpdates', - _isEqual([]), - ), - lateResponses: t.irreducible('default lateResponses', _isEqual([])), - showDisconnectedBar: tBool(false), - }), actualizedCalendarQuery: tShape({ startDate: t.String, endDate: t.String, filters: t.irreducible('default filters', _isEqual(defaultCalendarFilters)), }), watchedThreadIDs: t.irreducible('default watchedThreadIDs', _isEqual([])), lifecycleState: tString('active'), enabledApps: t.irreducible( 'defaultWebEnabledApps', _isEqual(defaultWebEnabledApps), ), reportStore: t.irreducible( 'default reportStore', _isEqual({ enabledReports: defaultEnabledReports, queuedReports: [], }), ), nextLocalID: tNumber(0), deviceToken: t.Nil, dataLoaded: t.Boolean, windowActive: tBool(true), userPolicies: t.irreducible('default userPolicies', _isEqual({})), cryptoStore: t.irreducible( 'default cryptoStore', _isEqual({ primaryIdentityKeys: null, notificationIdentityKeys: null, }), ), pushApiPublicKey: t.maybe(t.String), _persist: t.Nil, commServicesAccessToken: t.Nil, inviteLinksStore: inviteLinksStoreValidator, lastCommunicatedPlatformDetails: t.irreducible( 'default lastCommunicatedPlatformDetails', _isEqual({}), ), keyserverStore: keyserverStoreValidator, }); async function websiteResponder( viewer: Viewer, req: $Request, res: $Response, ): Promise { const appURLFacts = getAppURLFactsFromRequestURL(req.originalUrl); const { basePath, baseDomain } = appURLFacts; const baseURL = basePath.replace(/\/$/, ''); const urlPrefix = baseDomain + baseURL; const loadingPromise = getWebpackCompiledRootComponentForSSR(); const hasNotAcknowledgedPoliciesPromise = hasAnyNotAcknowledgedPolicies( viewer.id, baseLegalPolicies, ); const initialNavInfoPromise = (async () => { try { let urlInfo = infoFromURL(decodeURI(req.url)); try { urlInfo = await validateInput(viewer, urlInfoValidator, urlInfo, true); } catch (exc) { // We should still be able to handle older links if (exc.message !== 'invalid_client_id_prefix') { throw exc; } } let backupInfo = { now: currentDateInTimeZone(viewer.timeZone), }; // Some user ids in selectedUserList might not exist in the userStore // (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 assetInfoPromise = getAssetInfo(); 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 userStorePromise = (async () => { const [userInfos, hasNotAcknowledgedPolicies] = await Promise.all([ userInfoPromise, hasNotAcknowledgedPoliciesPromise, ]); return { userInfos: hasNotAcknowledgedPolicies ? {} : userInfos, inconsistencyReports: [], }; })(); const navInfoPromise = (async () => { const [ { threadInfos }, messageStore, currentUserInfo, userStore, finalNavInfo, ] = await Promise.all([ threadInfoPromise, messageStorePromise, currentUserInfoPromise, userStorePromise, 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 { userInfos } = userStore; 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 keyserverStorePromise = (async () => { const { sessionID, updatesCurrentAsOf } = await promiseAll({ sessionID: sessionIDPromise, updatesCurrentAsOf: currentAsOfPromise, }); return { keyserverInfos: { [ashoatKeyserverID]: { cookie: undefined, sessionID, updatesCurrentAsOf, urlPrefix, connection: defaultConnectionInfo, }, }, }; })(); const { jsURL, fontsURL, cssInclude, olmFilename, opaqueURL, commQueryExecutorFilename, } = await assetInfoPromise; // prettier-ignore res.write(html` ${getTitle(0)} ${cssInclude} `); const Loading = await loadingPromise; const reactStream = renderToNodeStream(); reactStream.pipe(res, { end: false }); await waitForStream(reactStream); res.write(html` `); } const inviteSecretRegex = /^[a-z0-9]+$/i; // On native, if this responder is called, it means that the app isn't // installed. async function inviteResponder(req: $Request, res: $Response): Promise { const { secret } = req.params; const userAgent = req.get('User-Agent'); const detectionResult = detectBrowser(userAgent); if (detectionResult.os === 'Android OS') { const isSecretValid = inviteSecretRegex.test(secret); const referrer = isSecretValid ? `&referrer=${encodeURIComponent(`utm_source=invite/${secret}`)}` : ''; const redirectUrl = `${stores.googlePlayUrl}${referrer}`; res.writeHead(301, { Location: redirectUrl, }); res.end(); return; } else if (detectionResult.os !== 'iOS') { const urlFacts = getCommAppURLFacts(); const baseDomain = urlFacts?.baseDomain ?? ''; const basePath = urlFacts?.basePath ?? '/'; const redirectUrl = `${baseDomain}${basePath}handle/invite/${secret}`; res.writeHead(301, { Location: redirectUrl, }); res.end(); return; } const fontsURL = await getFontsURL(); res.end(html` Comm Comm To join this community, download the Comm app and reopen this invite link Download Comm Invite Link Visit Comm’s website `); } export { websiteResponder, inviteResponder }; diff --git a/lib/reducers/master-reducer.js b/lib/reducers/master-reducer.js index 1b8515b92..64788dcb1 100644 --- a/lib/reducers/master-reducer.js +++ b/lib/reducers/master-reducer.js @@ -1,166 +1,162 @@ // @flow import reduceCalendarFilters from './calendar-filters-reducer.js'; import { reduceCalendarQuery } from './calendar-query-reducer.js'; -import reduceConnectionInfo from './connection-reducer.js'; import reduceDataLoaded from './data-loaded-reducer.js'; import { reduceDeviceToken } from './device-token-reducer.js'; import { reduceDraftStore } from './draft-reducer.js'; import reduceEnabledApps from './enabled-apps-reducer.js'; import { reduceEntryInfos } from './entry-reducer.js'; import reduceInviteLinks from './invite-links-reducer.js'; import reduceKeyserverStore from './keyserver-reducer.js'; import reduceLastCommunicatedPlatformDetails from './last-communicated-platform-details-reducer.js'; import reduceLifecycleState from './lifecycle-state-reducer.js'; import { reduceLoadingStatuses } from './loading-reducer.js'; import reduceNextLocalID from './local-id-reducer.js'; import { reduceMessageStore } from './message-reducer.js'; import reduceBaseNavInfo from './nav-reducer.js'; import { reduceNotifPermissionAlertInfo } from './notif-permission-alert-info-reducer.js'; import policiesReducer from './policies-reducer.js'; import reduceReportStore from './report-store-reducer.js'; import reduceServicesAccessToken from './services-access-token-reducer.js'; import { reduceThreadInfos } from './thread-reducer.js'; import { reduceCurrentUserInfo, reduceUserInfos } from './user-reducer.js'; import { siweAuthActionTypes } from '../actions/siwe-actions.js'; import { registerActionTypes, logInActionTypes, } from '../actions/user-actions.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 { ashoatKeyserverID } from '../utils/validation-utils.js'; export default function baseReducer>( state: T, action: BaseAction, ): { state: T, storeOperations: StoreOperations } { const { threadStore, newThreadInconsistencies, threadStoreOperations } = reduceThreadInfos(state.threadStore, action); const { threadInfos } = threadStore; const [entryStore, newEntryInconsistencies] = reduceEntryInfos( state.entryStore, action, threadInfos, ); const newInconsistencies = [ ...newEntryInconsistencies, ...newThreadInconsistencies, ]; // 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; - const connection = reduceConnectionInfo(state.connection, action); - let keyserverStore = reduceKeyserverStore(state.keyserverStore, action); if ( keyserverStore.keyserverInfos[ashoatKeyserverID].connection.status !== 'connected' && action.type !== incrementalStateSyncActionType && action.type !== fullStateSyncActionType && action.type !== registerActionTypes.success && action.type !== logInActionTypes.success && action.type !== siweAuthActionTypes.success ) { if ( messageStore.currentAsOf[ashoatKeyserverID] !== state.messageStore.currentAsOf[ashoatKeyserverID] ) { messageStore = { ...messageStore, currentAsOf: { ...messageStore.currentAsOf, [ashoatKeyserverID]: state.messageStore.currentAsOf[ashoatKeyserverID], }, }; } if ( keyserverStore.keyserverInfos[ashoatKeyserverID].updatesCurrentAsOf !== state.keyserverStore.keyserverInfos[ashoatKeyserverID].updatesCurrentAsOf ) { const keyserverInfos = { ...keyserverStore.keyserverInfos }; keyserverInfos[ashoatKeyserverID] = { ...keyserverInfos[ashoatKeyserverID], updatesCurrentAsOf: state.keyserverStore.keyserverInfos[ashoatKeyserverID] .updatesCurrentAsOf, }; keyserverStore = { ...keyserverStore, keyserverInfos }; } } const { draftStore, draftStoreOperations } = reduceDraftStore( state.draftStore, action, ); const { reportStore, reportStoreOperations } = reduceReportStore( state.reportStore, action, newInconsistencies, ); return { state: { ...state, navInfo: reduceBaseNavInfo(state.navInfo, action), draftStore, entryStore, loadingStatuses: reduceLoadingStatuses(state.loadingStatuses, action), currentUserInfo: reduceCurrentUserInfo(state.currentUserInfo, action), threadStore, userStore: reduceUserInfos(state.userStore, action), messageStore, calendarFilters: reduceCalendarFilters( state.calendarFilters, action, threadStore, ), notifPermissionAlertInfo: reduceNotifPermissionAlertInfo( state.notifPermissionAlertInfo, action, ), - connection, actualizedCalendarQuery: reduceCalendarQuery( state.actualizedCalendarQuery, action, ), lifecycleState: reduceLifecycleState(state.lifecycleState, action), enabledApps: reduceEnabledApps(state.enabledApps, action), reportStore, nextLocalID: reduceNextLocalID(state.nextLocalID, action), dataLoaded: reduceDataLoaded(state.dataLoaded, action), userPolicies: policiesReducer(state.userPolicies, action), deviceToken: reduceDeviceToken(state.deviceToken, action), commServicesAccessToken: reduceServicesAccessToken( state.commServicesAccessToken, action, ), inviteLinksStore: reduceInviteLinks(state.inviteLinksStore, action), lastCommunicatedPlatformDetails: reduceLastCommunicatedPlatformDetails( state.lastCommunicatedPlatformDetails, action, state.keyserverStore.keyserverInfos[ashoatKeyserverID].urlPrefix, ), keyserverStore, }, storeOperations: { draftStoreOperations, threadStoreOperations, messageStoreOperations, reportStoreOperations, }, }; } diff --git a/lib/types/redux-types.js b/lib/types/redux-types.js index 094ef6c55..afef7c14a 100644 --- a/lib/types/redux-types.js +++ b/lib/types/redux-types.js @@ -1,1217 +1,1215 @@ // @flow import type { LogOutResult, LogInStartingPayload, LogInResult, RegisterResult, DefaultNotificationPayload, } from './account-types.js'; import type { ActivityUpdateSuccessPayload, QueueActivityUpdatesPayload, SetThreadUnreadStatusPayload, } from './activity-types.js'; import type { UpdateUserAvatarRequest, UpdateUserAvatarResponse, } from './avatar-types.js'; import type { CryptoStore } from './crypto-types.js'; import type { VersionResponse, LastCommunicatedPlatformDetails, } from './device-types.js'; import type { ClientDBDraftInfo, DraftStore } from './draft-types.js'; import type { EnabledApps, SupportedApps } from './enabled-apps.js'; import type { RawEntryInfo, EntryStore, SaveEntryPayload, CreateEntryPayload, DeleteEntryResult, RestoreEntryPayload, FetchEntryInfosResult, CalendarQueryUpdateResult, CalendarQueryUpdateStartingPayload, CalendarQuery, } from './entry-types.js'; import type { CalendarFilter, CalendarThreadFilter, SetCalendarDeletedFilterPayload, } from './filter-types.js'; import type { KeyserverStore } from './keyserver-types.js'; import type { LifecycleState } from './lifecycle-state-types.js'; import type { FetchInviteLinksResponse, InviteLink, InviteLinksStore, InviteLinkVerificationResponse, DisableInviteLinkPayload, } from './link-types.js'; import type { LoadingStatus, LoadingInfo } from './loading-types.js'; import type { UpdateMultimediaMessageMediaPayload } from './media-types.js'; import type { MessageReportCreationResult } from './message-report-types.js'; import type { MessageStore, RawMultimediaMessageInfo, FetchMessageInfosPayload, SendMessagePayload, EditMessagePayload, SaveMessagesPayload, NewMessagesPayload, MessageStorePrunePayload, LocallyComposedMessageInfo, ClientDBMessageInfo, SimpleMessagesPayload, ClientDBThreadMessageInfo, FetchPinnedMessagesResult, SearchMessagesResponse, } from './message-types.js'; import type { RawReactionMessageInfo } from './messages/reaction.js'; import type { RawTextMessageInfo } from './messages/text.js'; import type { BaseNavInfo } from './nav-types.js'; import { type ForcePolicyAcknowledgmentPayload, type PolicyAcknowledgmentPayload, type UserPolicies, } from './policy-types.js'; import type { RelationshipErrors } from './relationship-types.js'; import type { EnabledReports, ClearDeliveredReportsPayload, QueueReportsPayload, ReportStore, ClientReportCreationRequest, } from './report-types.js'; import type { ProcessServerRequestsPayload, GetOlmSessionInitializationDataResponse, } from './request-types.js'; import type { UserSearchResult, ExactUserSearchResult, } from './search-types.js'; import type { SetSessionPayload } from './session-types.js'; import type { - ConnectionInfo, StateSyncFullActionPayload, StateSyncIncrementalActionPayload, UpdateConnectionStatusPayload, SetLateResponsePayload, UpdateDisconnectedBarPayload, } from './socket-types.js'; import type { SubscriptionUpdateResult } from './subscription-types.js'; import type { ThreadStore, ChangeThreadSettingsPayload, LeaveThreadPayload, NewThreadResult, ThreadJoinPayload, ToggleMessagePinResult, RoleModificationPayload, RoleDeletionPayload, } from './thread-types.js'; import type { ClientUpdatesResultWithUserInfos } from './update-types.js'; import type { CurrentUserInfo, UserStore } from './user-types.js'; import type { Shape } from '../types/core.js'; import type { NotifPermissionAlertInfo } from '../utils/push-alerts.js'; export type BaseAppState = { +navInfo: NavInfo, +currentUserInfo: ?CurrentUserInfo, +draftStore: DraftStore, +entryStore: EntryStore, +threadStore: ThreadStore, +userStore: UserStore, +messageStore: MessageStore, +loadingStatuses: { [key: string]: { [idx: number]: LoadingStatus } }, +calendarFilters: $ReadOnlyArray, +notifPermissionAlertInfo: NotifPermissionAlertInfo, - +connection: ConnectionInfo, +actualizedCalendarQuery: CalendarQuery, +watchedThreadIDs: $ReadOnlyArray, +lifecycleState: LifecycleState, +enabledApps: EnabledApps, +reportStore: ReportStore, +nextLocalID: number, +dataLoaded: boolean, +userPolicies: UserPolicies, +deviceToken: ?string, +commServicesAccessToken: ?string, +inviteLinksStore: InviteLinksStore, +lastCommunicatedPlatformDetails: LastCommunicatedPlatformDetails, +keyserverStore: KeyserverStore, ... }; export type NativeAppState = BaseAppState<>; export type WebAppState = BaseAppState<> & { +cryptoStore: CryptoStore, +pushApiPublicKey: ?string, ... }; export type AppState = NativeAppState | WebAppState; export type BaseAction = | { +type: '@@redux/INIT', +payload?: void, } | { +type: 'FETCH_ENTRIES_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_ENTRIES_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_ENTRIES_SUCCESS', +payload: FetchEntryInfosResult, +loadingInfo: LoadingInfo, } | { +type: 'LOG_OUT_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'LOG_OUT_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'LOG_OUT_SUCCESS', +payload: LogOutResult, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_ACCOUNT_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_ACCOUNT_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_ACCOUNT_SUCCESS', +payload: LogOutResult, +loadingInfo: LoadingInfo, } | { +type: 'CREATE_LOCAL_ENTRY', +payload: RawEntryInfo, } | { +type: 'CREATE_ENTRY_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'CREATE_ENTRY_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'CREATE_ENTRY_SUCCESS', +payload: CreateEntryPayload, +loadingInfo: LoadingInfo, } | { +type: 'SAVE_ENTRY_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'SAVE_ENTRY_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SAVE_ENTRY_SUCCESS', +payload: SaveEntryPayload, +loadingInfo: LoadingInfo, } | { +type: 'CONCURRENT_MODIFICATION_RESET', +payload: { +id: string, +dbText: string, }, } | { +type: 'DELETE_ENTRY_STARTED', +loadingInfo: LoadingInfo, +payload: { +localID: ?string, +serverID: ?string, }, } | { +type: 'DELETE_ENTRY_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_ENTRY_SUCCESS', +payload: ?DeleteEntryResult, +loadingInfo: LoadingInfo, } | { +type: 'LOG_IN_STARTED', +loadingInfo: LoadingInfo, +payload: LogInStartingPayload, } | { +type: 'LOG_IN_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'LOG_IN_SUCCESS', +payload: LogInResult, +loadingInfo: LoadingInfo, } | { +type: 'REGISTER_STARTED', +loadingInfo: LoadingInfo, +payload: LogInStartingPayload, } | { +type: 'REGISTER_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'REGISTER_SUCCESS', +payload: RegisterResult, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_USER_PASSWORD_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_USER_PASSWORD_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_USER_PASSWORD_SUCCESS', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_THREAD_SETTINGS_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_THREAD_SETTINGS_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_THREAD_SETTINGS_SUCCESS', +payload: ChangeThreadSettingsPayload, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_THREAD_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_THREAD_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_THREAD_SUCCESS', +payload: LeaveThreadPayload, +loadingInfo: LoadingInfo, } | { +type: 'NEW_THREAD_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'NEW_THREAD_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'NEW_THREAD_SUCCESS', +payload: NewThreadResult, +loadingInfo: LoadingInfo, } | { +type: 'REMOVE_USERS_FROM_THREAD_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'REMOVE_USERS_FROM_THREAD_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'REMOVE_USERS_FROM_THREAD_SUCCESS', +payload: ChangeThreadSettingsPayload, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_THREAD_MEMBER_ROLES_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_THREAD_MEMBER_ROLES_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_THREAD_MEMBER_ROLES_SUCCESS', +payload: ChangeThreadSettingsPayload, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_REVISIONS_FOR_ENTRY_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_REVISIONS_FOR_ENTRY_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_REVISIONS_FOR_ENTRY_SUCCESS', +payload: { +entryID: string, +text: string, +deleted: boolean, }, +loadingInfo: LoadingInfo, } | { +type: 'RESTORE_ENTRY_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'RESTORE_ENTRY_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'RESTORE_ENTRY_SUCCESS', +payload: RestoreEntryPayload, +loadingInfo: LoadingInfo, } | { +type: 'JOIN_THREAD_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'JOIN_THREAD_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'JOIN_THREAD_SUCCESS', +payload: ThreadJoinPayload, +loadingInfo: LoadingInfo, } | { +type: 'LEAVE_THREAD_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'LEAVE_THREAD_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'LEAVE_THREAD_SUCCESS', +payload: LeaveThreadPayload, +loadingInfo: LoadingInfo, } | { +type: 'SET_NEW_SESSION', +payload: SetSessionPayload, } | { +type: 'persist/REHYDRATE', +payload: ?BaseAppState<>, } | { +type: 'FETCH_MESSAGES_BEFORE_CURSOR_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_MESSAGES_BEFORE_CURSOR_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_MESSAGES_BEFORE_CURSOR_SUCCESS', +payload: FetchMessageInfosPayload, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_MOST_RECENT_MESSAGES_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_MOST_RECENT_MESSAGES_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_MOST_RECENT_MESSAGES_SUCCESS', +payload: FetchMessageInfosPayload, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_SINGLE_MOST_RECENT_MESSAGES_FROM_THREADS_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_SINGLE_MOST_RECENT_MESSAGES_FROM_THREADS_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_SINGLE_MOST_RECENT_MESSAGES_FROM_THREADS_SUCCESS', +payload: SimpleMessagesPayload, +loadingInfo: LoadingInfo, } | { +type: 'SEND_TEXT_MESSAGE_STARTED', +loadingInfo?: LoadingInfo, +payload: RawTextMessageInfo, } | { +type: 'SEND_TEXT_MESSAGE_FAILED', +error: true, +payload: Error & { +localID: string, +threadID: string, }, +loadingInfo?: LoadingInfo, } | { +type: 'SEND_TEXT_MESSAGE_SUCCESS', +payload: SendMessagePayload, +loadingInfo: LoadingInfo, } | { +type: 'SEND_MULTIMEDIA_MESSAGE_STARTED', +loadingInfo?: LoadingInfo, +payload: RawMultimediaMessageInfo, } | { +type: 'SEND_MULTIMEDIA_MESSAGE_FAILED', +error: true, +payload: Error & { +localID: string, +threadID: string, }, +loadingInfo?: LoadingInfo, } | { +type: 'SEND_MULTIMEDIA_MESSAGE_SUCCESS', +payload: SendMessagePayload, +loadingInfo: LoadingInfo, } | { +type: 'SEND_REACTION_MESSAGE_STARTED', +loadingInfo?: LoadingInfo, +payload: RawReactionMessageInfo, } | { +type: 'SEND_REACTION_MESSAGE_FAILED', +error: true, +payload: Error & { +localID: string, +threadID: string, +targetMessageID: string, +reaction: string, +action: string, }, +loadingInfo: LoadingInfo, } | { +type: 'SEND_REACTION_MESSAGE_SUCCESS', +payload: SendMessagePayload, +loadingInfo: LoadingInfo, } | { +type: 'SEARCH_USERS_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'SEARCH_USERS_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SEARCH_USERS_SUCCESS', +payload: UserSearchResult, +loadingInfo: LoadingInfo, } | { +type: 'EXACT_SEARCH_USER_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'EXACT_SEARCH_USER_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'EXACT_SEARCH_USER_SUCCESS', +payload: ExactUserSearchResult, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_DRAFT', +payload: { +key: string, +text: string, }, } | { +type: 'MOVE_DRAFT', +payload: { +oldKey: string, +newKey: string, }, } | { +type: 'SET_CLIENT_DB_STORE', +payload: { +currentUserID: ?string, +drafts: $ReadOnlyArray, +messages: ?$ReadOnlyArray, +threadStore: ?ThreadStore, +messageStoreThreads: ?$ReadOnlyArray, +reports: ?$ReadOnlyArray, }, } | { +type: 'UPDATE_ACTIVITY_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_ACTIVITY_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_ACTIVITY_SUCCESS', +payload: ActivityUpdateSuccessPayload, +loadingInfo: LoadingInfo, } | { +type: 'SET_DEVICE_TOKEN_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'SET_DEVICE_TOKEN_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SET_DEVICE_TOKEN_SUCCESS', +payload: ?string, +loadingInfo: LoadingInfo, } | { +type: 'SEND_REPORT_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'SEND_REPORT_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SEND_REPORT_SUCCESS', +payload?: ClearDeliveredReportsPayload, +loadingInfo: LoadingInfo, } | { +type: 'SEND_REPORTS_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'SEND_REPORTS_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SEND_REPORTS_SUCCESS', +payload?: ClearDeliveredReportsPayload, +loadingInfo: LoadingInfo, } | { +type: 'QUEUE_REPORTS', +payload: QueueReportsPayload, } | { +type: 'SET_URL_PREFIX', +payload: string, } | { +type: 'SAVE_MESSAGES', +payload: SaveMessagesPayload, } | { +type: 'UPDATE_CALENDAR_THREAD_FILTER', +payload: CalendarThreadFilter, } | { +type: 'CLEAR_CALENDAR_THREAD_FILTER', +payload?: void, } | { +type: 'SET_CALENDAR_DELETED_FILTER', +payload: SetCalendarDeletedFilterPayload, } | { +type: 'UPDATE_SUBSCRIPTION_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_SUBSCRIPTION_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_SUBSCRIPTION_SUCCESS', +payload: SubscriptionUpdateResult, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_CALENDAR_QUERY_STARTED', +loadingInfo: LoadingInfo, +payload?: CalendarQueryUpdateStartingPayload, } | { +type: 'UPDATE_CALENDAR_QUERY_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_CALENDAR_QUERY_SUCCESS', +payload: CalendarQueryUpdateResult, +loadingInfo: LoadingInfo, } | { +type: 'FULL_STATE_SYNC', +payload: StateSyncFullActionPayload, } | { +type: 'INCREMENTAL_STATE_SYNC', +payload: StateSyncIncrementalActionPayload, } | { +type: 'PROCESS_SERVER_REQUESTS', +payload: ProcessServerRequestsPayload, } | { +type: 'UPDATE_CONNECTION_STATUS', +payload: UpdateConnectionStatusPayload, } | { +type: 'QUEUE_ACTIVITY_UPDATES', +payload: QueueActivityUpdatesPayload, } | { +type: 'UNSUPERVISED_BACKGROUND', +payload?: void, } | { +type: 'UPDATE_LIFECYCLE_STATE', +payload: LifecycleState, } | { +type: 'ENABLE_APP', +payload: SupportedApps, } | { +type: 'DISABLE_APP', +payload: SupportedApps, } | { +type: 'UPDATE_REPORTS_ENABLED', +payload: Shape, } | { +type: 'PROCESS_UPDATES', +payload: ClientUpdatesResultWithUserInfos, } | { +type: 'PROCESS_MESSAGES', +payload: NewMessagesPayload, } | { +type: 'MESSAGE_STORE_PRUNE', +payload: MessageStorePrunePayload, } | { +type: 'SET_LATE_RESPONSE', +payload: SetLateResponsePayload, } | { +type: 'UPDATE_DISCONNECTED_BAR', +payload: UpdateDisconnectedBarPayload, } | { +type: 'REQUEST_ACCESS_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'REQUEST_ACCESS_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'REQUEST_ACCESS_SUCCESS', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_MULTIMEDIA_MESSAGE_MEDIA', +payload: UpdateMultimediaMessageMediaPayload, } | { +type: 'CREATE_LOCAL_MESSAGE', +payload: LocallyComposedMessageInfo, } | { +type: 'UPDATE_RELATIONSHIPS_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_RELATIONSHIPS_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_RELATIONSHIPS_SUCCESS', +payload: RelationshipErrors, +loadingInfo: LoadingInfo, } | { +type: 'SET_THREAD_UNREAD_STATUS_STARTED', +payload: { +threadID: string, +unread: boolean, }, +loadingInfo: LoadingInfo, } | { +type: 'SET_THREAD_UNREAD_STATUS_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SET_THREAD_UNREAD_STATUS_SUCCESS', +payload: SetThreadUnreadStatusPayload, } | { +type: 'SET_USER_SETTINGS_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'SET_USER_SETTINGS_SUCCESS', +payload: DefaultNotificationPayload, } | { +type: 'SET_USER_SETTINGS_FAILED', +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SEND_MESSAGE_REPORT_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'SEND_MESSAGE_REPORT_SUCCESS', +payload: MessageReportCreationResult, +loadingInfo: LoadingInfo, } | { +type: 'SEND_MESSAGE_REPORT_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'FORCE_POLICY_ACKNOWLEDGMENT', +payload: ForcePolicyAcknowledgmentPayload, +loadingInfo: LoadingInfo, } | { +type: 'POLICY_ACKNOWLEDGMENT_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'POLICY_ACKNOWLEDGMENT_SUCCESS', +payload: PolicyAcknowledgmentPayload, +loadingInfo: LoadingInfo, } | { +type: 'POLICY_ACKNOWLEDGMENT_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'GET_SIWE_NONCE_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'GET_SIWE_NONCE_SUCCESS', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'GET_SIWE_NONCE_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SIWE_AUTH_STARTED', +payload: LogInStartingPayload, +loadingInfo: LoadingInfo, } | { +type: 'SIWE_AUTH_SUCCESS', +payload: LogInResult, +loadingInfo: LoadingInfo, } | { +type: 'SIWE_AUTH_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'RECORD_NOTIF_PERMISSION_ALERT', +payload: { +time: number }, } | { +type: 'UPDATE_USER_AVATAR_STARTED', +payload: UpdateUserAvatarRequest, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_USER_AVATAR_SUCCESS', +payload: UpdateUserAvatarResponse, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_USER_AVATAR_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SEND_EDIT_MESSAGE_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'SEND_EDIT_MESSAGE_SUCCESS', +payload: EditMessagePayload, +loadingInfo: LoadingInfo, } | { +type: 'SEND_EDIT_MESSAGE_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'TOGGLE_MESSAGE_PIN_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'TOGGLE_MESSAGE_PIN_SUCCESS', +payload: ToggleMessagePinResult, +loadingInfo: LoadingInfo, } | { +type: 'TOGGLE_MESSAGE_PIN_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_PINNED_MESSAGES_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'FETCH_PINNED_MESSAGES_SUCCESS', +payload: FetchPinnedMessagesResult, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_PINNED_MESSAGES_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'VERIFY_INVITE_LINK_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'VERIFY_INVITE_LINK_SUCCESS', +payload: InviteLinkVerificationResponse, +loadingInfo: LoadingInfo, } | { +type: 'VERIFY_INVITE_LINK_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_PRIMARY_INVITE_LINKS_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'FETCH_PRIMARY_INVITE_LINKS_SUCCESS', +payload: FetchInviteLinksResponse, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_PRIMARY_INVITE_LINKS_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_CALENDAR_COMMUNITY_FILTER', +payload: string, } | { +type: 'CLEAR_CALENDAR_COMMUNITY_FILTER', +payload: void, } | { +type: 'UPDATE_CHAT_COMMUNITY_FILTER', +payload: string, } | { +type: 'CLEAR_CHAT_COMMUNITY_FILTER', +payload: void, } | { +type: 'SEARCH_MESSAGES_STARTED', +payload: void, +loadingInfo?: LoadingInfo, } | { +type: 'SEARCH_MESSAGES_SUCCESS', +payload: SearchMessagesResponse, +loadingInfo: LoadingInfo, } | { +type: 'SEARCH_MESSAGES_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'CREATE_OR_UPDATE_PUBLIC_LINK_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'CREATE_OR_UPDATE_PUBLIC_LINK_SUCCESS', +payload: InviteLink, +loadingInfo: LoadingInfo, } | { +type: 'CREATE_OR_UPDATE_PUBLIC_LINK_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'DISABLE_INVITE_LINK_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'DISABLE_INVITE_LINK_SUCCESS', +payload: DisableInviteLinkPayload, +loadingInfo: LoadingInfo, } | { +type: 'DISABLE_INVITE_LINK_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'GET_OLM_SESSION_INITIALIZATION_DATA_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'GET_OLM_SESSION_INITIALIZATION_DATA_SUCCESS', +payload: GetOlmSessionInitializationDataResponse, +loadingInfo: LoadingInfo, } | { +type: 'GET_OLM_SESSION_INITIALIZATION_DATA_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SET_DATA_LOADED', +payload: { +dataLoaded: boolean, }, } | { +type: 'GET_VERSION_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'GET_VERSION_SUCCESS', +payload: VersionResponse, +loadingInfo: LoadingInfo, } | { +type: 'GET_VERSION_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_LAST_COMMUNICATED_PLATFORM_DETAILS', +payload: LastCommunicatedPlatformDetails, } | { +type: 'RESET_USER_STATE', +payload?: void } | { +type: 'MODIFY_COMMUNITY_ROLE_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'MODIFY_COMMUNITY_ROLE_SUCCESS', +payload: RoleModificationPayload, +loadingInfo: LoadingInfo, } | { +type: 'MODIFY_COMMUNITY_ROLE_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_COMMUNITY_ROLE_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'DELETE_COMMUNITY_ROLE_SUCCESS', +payload: RoleDeletionPayload, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_COMMUNITY_ROLE_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, }; export type ActionPayload = ?(Object | Array<*> | $ReadOnlyArray<*> | string); export type SuperAction = { type: string, payload?: ActionPayload, loadingInfo?: LoadingInfo, error?: boolean, }; type ThunkedAction = (dispatch: Dispatch) => void; export type PromisedAction = (dispatch: Dispatch) => Promise; export type Dispatch = ((promisedAction: PromisedAction) => Promise) & ((thunkedAction: ThunkedAction) => void) & ((action: SuperAction) => boolean); // This is lifted from redux-persist/lib/constants.js // I don't want to add redux-persist to the web/server bundles... // import { REHYDRATE } from 'redux-persist'; export const rehydrateActionType = 'persist/REHYDRATE'; diff --git a/native/redux/redux-setup.js b/native/redux/redux-setup.js index b5da687d1..c748e655f 100644 --- a/native/redux/redux-setup.js +++ b/native/redux/redux-setup.js @@ -1,492 +1,491 @@ // @flow import { AppState as NativeAppState, Platform, Alert } from 'react-native'; import Orientation from 'react-native-orientation-locker'; import { createStore, applyMiddleware, type Store, compose } from 'redux'; import { persistStore, persistReducer } from 'redux-persist'; import thunk from 'redux-thunk'; import { setClientDBStoreActionType } from 'lib/actions/client-db-store-actions.js'; import { siweAuthActionTypes } from 'lib/actions/siwe-actions.js'; import { logOutActionTypes, deleteAccountActionTypes, logInActionTypes, } from 'lib/actions/user-actions.js'; import type { ThreadStoreOperation } from 'lib/ops/thread-store-ops.js'; import { threadStoreOpsHandlers } from 'lib/ops/thread-store-ops.js'; import baseReducer from 'lib/reducers/master-reducer.js'; import { invalidSessionDowngrade, invalidSessionRecovery, } from 'lib/shared/session-utils.js'; import { isStaff } from 'lib/shared/staff-utils.js'; import { defaultEnabledApps } from 'lib/types/enabled-apps.js'; import { defaultCalendarQuery } from 'lib/types/entry-types.js'; import { defaultCalendarFilters } from 'lib/types/filter-types.js'; import type { Dispatch, BaseAction } from 'lib/types/redux-types.js'; import { rehydrateActionType } from 'lib/types/redux-types.js'; import type { SetSessionPayload } from 'lib/types/session-types.js'; import { defaultConnectionInfo } from 'lib/types/socket-types.js'; import { reduxLoggerMiddleware } from 'lib/utils/action-logger.js'; import { setNewSessionActionType } from 'lib/utils/action-utils.js'; import { defaultNotifPermissionAlertInfo } from 'lib/utils/push-alerts.js'; import { ashoatKeyserverID } from 'lib/utils/validation-utils.js'; import { updateDimensionsActiveType, updateConnectivityActiveType, updateThemeInfoActionType, updateDeviceCameraInfoActionType, updateDeviceOrientationActionType, updateThreadLastNavigatedActionType, backgroundActionTypes, setReduxStateActionType, setStoreLoadedActionType, type Action, } from './action-types.js'; import { remoteReduxDevServerConfig } from './dev-tools.js'; import { defaultDimensionsInfo } from './dimensions-updater.react.js'; import { persistConfig, setPersistor } from './persist.js'; import { processDBStoreOperations } from './redux-utils.js'; import type { AppState } from './state-types.js'; import reduceGlobalThemeInfo from './theme-reducer.js'; import { defaultNavInfo } from '../navigation/default-state.js'; import { getGlobalNavContext } from '../navigation/icky-global.js'; import { activeMessageListSelector } from '../navigation/nav-selectors.js'; import reactotron from '../reactotron.js'; import { defaultDeviceCameraInfo } from '../types/camera.js'; import { defaultConnectivityInfo } from '../types/connectivity.js'; import { defaultGlobalThemeInfo } from '../types/themes.js'; import { isStaffRelease } from '../utils/staff-utils.js'; import { defaultURLPrefix, natNodeServer, setCustomServer, getDevServerHostname, } from '../utils/url-utils.js'; const defaultState = ({ navInfo: defaultNavInfo, currentUserInfo: null, draftStore: { drafts: {} }, entryStore: { entryInfos: {}, daysToEntries: {}, lastUserInteractionCalendar: 0, }, threadStore: { threadInfos: {}, }, userStore: { userInfos: {}, inconsistencyReports: [], }, messageStore: { messages: {}, threads: {}, local: {}, currentAsOf: { [ashoatKeyserverID]: 0 }, }, storeLoaded: false, loadingStatuses: {}, calendarFilters: defaultCalendarFilters, deviceToken: null, dataLoaded: false, customServer: natNodeServer, notifPermissionAlertInfo: defaultNotifPermissionAlertInfo, - connection: defaultConnectionInfo, actualizedCalendarQuery: defaultCalendarQuery(Platform.OS), watchedThreadIDs: [], lifecycleState: 'active', enabledApps: defaultEnabledApps, reportStore: { enabledReports: { crashReports: __DEV__, inconsistencyReports: __DEV__, mediaReports: __DEV__, }, queuedReports: [], }, nextLocalID: 0, _persist: null, dimensions: defaultDimensionsInfo, connectivity: defaultConnectivityInfo, globalThemeInfo: defaultGlobalThemeInfo, deviceCameraInfo: defaultDeviceCameraInfo, deviceOrientation: Orientation.getInitialOrientation(), frozen: false, userPolicies: {}, commServicesAccessToken: null, inviteLinksStore: { links: {}, }, lastCommunicatedPlatformDetails: {}, keyserverStore: { keyserverInfos: { [ashoatKeyserverID]: { updatesCurrentAsOf: 0, urlPrefix: defaultURLPrefix, connection: defaultConnectionInfo, }, }, }, }: AppState); function reducer(state: AppState = defaultState, action: Action) { if (action.type === setReduxStateActionType) { return action.payload.state; } // We want to alert staff/developers if there's a difference between the keys // we expect to see REHYDRATED and the keys that are actually REHYDRATED. // Context: https://linear.app/comm/issue/ENG-2127/ if ( action.type === rehydrateActionType && (__DEV__ || isStaffRelease || (state.currentUserInfo && state.currentUserInfo.id && isStaff(state.currentUserInfo.id))) ) { // 1. Construct set of keys expected to be REHYDRATED const defaultKeys = Object.keys(defaultState); const expectedKeys = defaultKeys.filter( each => !persistConfig.blacklist.includes(each), ); const expectedKeysSet = new Set(expectedKeys); // 2. Construct set of keys actually REHYDRATED const rehydratedKeys = Object.keys(action.payload ?? {}); const rehydratedKeysSet = new Set(rehydratedKeys); // 3. Determine the difference between the two sets const expectedKeysNotRehydrated = expectedKeys.filter( each => !rehydratedKeysSet.has(each), ); const rehydratedKeysNotExpected = rehydratedKeys.filter( each => !expectedKeysSet.has(each), ); // 4. Display alerts with the differences between the two sets if (expectedKeysNotRehydrated.length > 0) { Alert.alert( `EXPECTED KEYS NOT REHYDRATED: ${JSON.stringify( expectedKeysNotRehydrated, )}`, ); } if (rehydratedKeysNotExpected.length > 0) { Alert.alert( `REHYDRATED KEYS NOT EXPECTED: ${JSON.stringify( rehydratedKeysNotExpected, )}`, ); } } if ( (action.type === setNewSessionActionType && invalidSessionDowngrade( state, action.payload.sessionChange.currentUserInfo, action.payload.preRequestUserState, )) || (action.type === logOutActionTypes.success && invalidSessionDowngrade( state, action.payload.currentUserInfo, action.payload.preRequestUserState, )) || (action.type === deleteAccountActionTypes.success && invalidSessionDowngrade( state, action.payload.currentUserInfo, action.payload.preRequestUserState, )) ) { return state; } if ( (action.type === setNewSessionActionType && action.payload.sessionChange.currentUserInfo && invalidSessionRecovery( state, action.payload.sessionChange.currentUserInfo, action.payload.logInActionSource, )) || ((action.type === logInActionTypes.success || action.type === siweAuthActionTypes.success) && invalidSessionRecovery( state, action.payload.currentUserInfo, action.payload.logInActionSource, )) ) { return state; } state = { ...state, globalThemeInfo: reduceGlobalThemeInfo(state.globalThemeInfo, action), }; if (action.type === setCustomServer) { return { ...state, customServer: action.payload, }; } else if (action.type === updateDimensionsActiveType) { return { ...state, dimensions: { ...state.dimensions, ...action.payload, }, }; } else if (action.type === updateConnectivityActiveType) { return { ...state, connectivity: action.payload, }; } else if (action.type === updateThemeInfoActionType) { // Handled above by reduceGlobalThemeInfo return state; } else if (action.type === updateDeviceCameraInfoActionType) { return { ...state, deviceCameraInfo: { ...state.deviceCameraInfo, ...action.payload, }, }; } else if (action.type === updateDeviceOrientationActionType) { return { ...state, deviceOrientation: action.payload, }; } else if (action.type === updateThreadLastNavigatedActionType) { const { threadID, time } = action.payload; if (state.messageStore.threads[threadID]) { const updatedThreads = { [threadID]: { ...state.messageStore.threads[threadID], lastNavigatedTo: time, }, }; state = { ...state, messageStore: { ...state.messageStore, threads: { ...state.messageStore.threads, ...updatedThreads, }, }, }; processDBStoreOperations({ draftStoreOperations: [], messageStoreOperations: [ { type: 'replace_threads', payload: { threads: updatedThreads, }, }, ], threadStoreOperations: [], reportStoreOperations: [], }); } return state; } if (action.type === setNewSessionActionType) { sessionInvalidationAlert(action.payload); } if (action.type === setStoreLoadedActionType) { return { ...state, storeLoaded: true, }; } if (action.type === setClientDBStoreActionType) { state = { ...state, storeLoaded: true, }; const currentLoggedInUserID = state.currentUserInfo?.anonymous ? undefined : state.currentUserInfo?.id; const actionCurrentLoggedInUserID = action.payload.currentUserID; if ( !currentLoggedInUserID || !actionCurrentLoggedInUserID || actionCurrentLoggedInUserID !== currentLoggedInUserID ) { // If user is logged out now, was logged out at the time action was // dispatched or their ID changed between action dispatch and a // call to reducer we ignore the SQLite data since it is not valid return state; } } const baseReducerResult = baseReducer(state, (action: BaseAction)); state = baseReducerResult.state; const { storeOperations } = baseReducerResult; const { draftStoreOperations, threadStoreOperations, messageStoreOperations, reportStoreOperations, } = storeOperations; const fixUnreadActiveThreadResult = fixUnreadActiveThread(state, action); state = fixUnreadActiveThreadResult.state; const threadStoreOperationsWithUnreadFix = [ ...threadStoreOperations, ...fixUnreadActiveThreadResult.threadStoreOperations, ]; processDBStoreOperations({ draftStoreOperations, messageStoreOperations, threadStoreOperations: threadStoreOperationsWithUnreadFix, reportStoreOperations, }); return state; } function sessionInvalidationAlert(payload: SetSessionPayload) { if ( !payload.sessionChange.cookieInvalidated || !payload.preRequestUserState || !payload.preRequestUserState.currentUserInfo || payload.preRequestUserState.currentUserInfo.anonymous ) { return; } if (payload.error === 'client_version_unsupported') { const app = Platform.select({ ios: 'App Store', android: 'Play Store', }); Alert.alert( 'App out of date', 'Your app version is pretty old, and the server doesn’t know how to ' + `speak to it anymore. Please use the ${app} app to update!`, [{ text: 'OK' }], { cancelable: true }, ); } else { Alert.alert( 'Session invalidated', 'We’re sorry, but your session was invalidated by the server. ' + 'Please log in again.', [{ text: 'OK' }], { cancelable: true }, ); } } // Makes sure a currently focused thread is never unread. Note that we consider // a backgrounded NativeAppState to actually be active if it last changed to // inactive more than 10 seconds ago. This is because there is a delay when // NativeAppState is updating in response to a foreground, and actions don't get // processed more than 10 seconds after a backgrounding anyways. However we // don't consider this for action types that can be expected to happen while the // app is backgrounded. type FixUnreadActiveThreadResult = { +state: AppState, +threadStoreOperations: $ReadOnlyArray, }; function fixUnreadActiveThread( state: AppState, action: *, ): FixUnreadActiveThreadResult { const navContext = getGlobalNavContext(); const activeThread = activeMessageListSelector(navContext); if ( !activeThread || !state.threadStore.threadInfos[activeThread]?.currentUser.unread || (NativeAppState.currentState !== 'active' && (appLastBecameInactive + 10000 >= Date.now() || backgroundActionTypes.has(action.type))) ) { return { state, threadStoreOperations: [] }; } const updatedActiveThreadInfo = { ...state.threadStore.threadInfos[activeThread], currentUser: { ...state.threadStore.threadInfos[activeThread].currentUser, unread: false, }, }; const threadStoreOperations = [ { type: 'replace', payload: { id: activeThread, threadInfo: updatedActiveThreadInfo, }, }, ]; const updatedThreadStore = threadStoreOpsHandlers.processStoreOperations( state.threadStore, threadStoreOperations, ); return { state: { ...state, threadStore: updatedThreadStore }, threadStoreOperations, }; } let appLastBecameInactive = 0; function appBecameInactive() { appLastBecameInactive = Date.now(); } const middleware = applyMiddleware(thunk, reduxLoggerMiddleware); let composeFunc = compose; if (__DEV__ && global.HermesInternal) { const { composeWithDevTools } = require('remote-redux-devtools/src/index.js'); composeFunc = composeWithDevTools({ name: 'Redux', hostname: getDevServerHostname(), ...remoteReduxDevServerConfig, }); } else if (global.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) { composeFunc = global.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ name: 'Redux', }); } let enhancers; if (reactotron) { enhancers = composeFunc(middleware, reactotron.createEnhancer()); } else { enhancers = composeFunc(middleware); } const store: Store = createStore( persistReducer(persistConfig, reducer), defaultState, enhancers, ); const persistor = persistStore(store); setPersistor(persistor); const unsafeDispatch: any = store.dispatch; const dispatch: Dispatch = unsafeDispatch; export { store, dispatch, appBecameInactive }; diff --git a/native/redux/state-types.js b/native/redux/state-types.js index b741d8e9c..5e0ad025b 100644 --- a/native/redux/state-types.js +++ b/native/redux/state-types.js @@ -1,63 +1,61 @@ // @flow import type { Orientations } from 'react-native-orientation-locker'; import type { PersistState } from 'redux-persist/es/types.js'; import type { LastCommunicatedPlatformDetails } from 'lib/types/device-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 { 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 { ReportStore } from 'lib/types/report-types.js'; -import type { ConnectionInfo } from 'lib/types/socket-types.js'; import type { ThreadStore } from 'lib/types/thread-types.js'; import type { CurrentUserInfo, UserStore } from 'lib/types/user-types.js'; import type { NotifPermissionAlertInfo } from 'lib/utils/push-alerts.js'; import type { DimensionsInfo } from './dimensions-updater.react.js'; import type { NavInfo } from '../navigation/default-state.js'; import type { DeviceCameraInfo } from '../types/camera.js'; import type { ConnectivityInfo } from '../types/connectivity.js'; import type { GlobalThemeInfo } from '../types/themes.js'; export type AppState = { +navInfo: NavInfo, +currentUserInfo: ?CurrentUserInfo, +draftStore: DraftStore, +entryStore: EntryStore, +threadStore: ThreadStore, +userStore: UserStore, +messageStore: MessageStore, +storeLoaded: boolean, +loadingStatuses: { [key: string]: { [idx: number]: LoadingStatus } }, +calendarFilters: $ReadOnlyArray, +deviceToken: ?string, +dataLoaded: boolean, +customServer: ?string, +notifPermissionAlertInfo: NotifPermissionAlertInfo, - +connection: ConnectionInfo, +actualizedCalendarQuery: CalendarQuery, +watchedThreadIDs: $ReadOnlyArray, +lifecycleState: LifecycleState, +enabledApps: EnabledApps, +reportStore: ReportStore, +nextLocalID: number, +_persist: ?PersistState, +dimensions: DimensionsInfo, +connectivity: ConnectivityInfo, +globalThemeInfo: GlobalThemeInfo, +deviceCameraInfo: DeviceCameraInfo, +deviceOrientation: Orientations, +frozen: boolean, +userPolicies: UserPolicies, +commServicesAccessToken: ?string, +inviteLinksStore: InviteLinksStore, +lastCommunicatedPlatformDetails: LastCommunicatedPlatformDetails, +keyserverStore: KeyserverStore, }; diff --git a/web/redux/redux-setup.js b/web/redux/redux-setup.js index 9f565b92b..372647a92 100644 --- a/web/redux/redux-setup.js +++ b/web/redux/redux-setup.js @@ -1,327 +1,325 @@ // @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 { LastCommunicatedPlatformDetails } from 'lib/types/device-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 { 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 { ConnectionInfo } from 'lib/types/socket-types.js'; 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, setDeviceIDActionType, updateNavInfoActionType, updateWindowDimensionsActionType, } from './action-types.js'; import { reduceCommunityPickerStore } from './community-picker-reducer.js'; import { reduceCryptoStore, setPrimaryIdentityKeys, setNotificationIdentityKeys, setPickledNotificationAccount, setPickledPrimaryAccount, } from './crypto-store-reducer.js'; import { reduceDeviceID } from './device-id-reducer.js'; import reduceNavInfo from './nav-reducer.js'; import { getVisibility } from './visibility.js'; import { databaseModule } from '../database/database-module-provider.js'; import { activeThreadSelector } from '../selectors/nav-selectors.js'; import { type NavInfo } from '../types/nav-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, +deviceID: ?string, +currentUserInfo: ?CurrentUserInfo, +draftStore: DraftStore, +entryStore: EntryStore, +threadStore: ThreadStore, +userStore: UserStore, +messageStore: MessageStore, +loadingStatuses: { [key: string]: { [idx: number]: LoadingStatus } }, +calendarFilters: $ReadOnlyArray, +communityPickerStore: CommunityPickerStore, +windowDimensions: WindowDimensions, +deviceToken: ?string, +notifPermissionAlertInfo: NotifPermissionAlertInfo, - +connection: ConnectionInfo, +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, +lastCommunicatedPlatformDetails: LastCommunicatedPlatformDetails, +keyserverStore: KeyserverStore, }; export type Action = | BaseAction | { type: 'UPDATE_NAV_INFO', payload: Shape } | { type: 'UPDATE_WINDOW_DIMENSIONS', payload: WindowDimensions, } | { type: 'UPDATE_WINDOW_ACTIVE', payload: boolean, } | { type: 'SET_DEVICE_ID', payload: string, } | { +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 }; export function reducer(oldState: AppState | void, action: Action): AppState { invariant(oldState, 'should be set'); let state = oldState; 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 !== setDeviceIDActionType && 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 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, ), deviceID: reduceDeviceID(state.deviceID, action), 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] ) { // Update messageStore.threads[activeThread].lastNavigatedTo state = { ...state, messageStore: { ...state.messageStore, threads: { ...state.messageStore.threads, [activeThread]: { ...state.messageStore.threads[activeThread], lastNavigatedTo: Date.now(), }, }, }, }; } return state; }
To join this community, download the Comm app and reopen this invite link