diff --git a/src/account/accountsSelectors.js b/src/account/accountsSelectors.js index a8dfcfb3451..353027d96a6 100644 --- a/src/account/accountsSelectors.js +++ b/src/account/accountsSelectors.js @@ -1,6 +1,6 @@ /* @flow strict-local */ import { createSelector } from 'reselect'; -import type { Account, Auth, GlobalState, Identity, Selector } from '../types'; +import type { Account, Auth, GlobalState, Identity, GlobalSelector } from '../types'; import { getAccounts } from '../directSelectors'; import { identityOfAccount, keyOfIdentity, identityOfAuth, authOfAccount } from './accountMisc'; import { ZulipVersion } from '../utils/zulipVersion'; @@ -14,14 +14,14 @@ export type AccountStatus = {| ...Identity, isLoggedIn: boolean |}; * This should be used in preference to `getAccounts` where we don't * actually need the API keys, but just need to know whether we have them. */ -export const getAccountStatuses: Selector< +export const getAccountStatuses: GlobalSelector< $ReadOnlyArray, > = createSelector(getAccounts, accounts => accounts.map(({ realm, email, apiKey }) => ({ realm, email, isLoggedIn: apiKey !== '' })), ); /** The list of known accounts, reduced to `Identity`. */ -export const getIdentities: Selector<$ReadOnlyArray> = createSelector( +export const getIdentities: GlobalSelector<$ReadOnlyArray> = createSelector( getAccounts, accounts => accounts.map(identityOfAccount), ); @@ -29,7 +29,7 @@ export const getIdentities: Selector<$ReadOnlyArray> = createSelector( /** * All known accounts, indexed by identity. */ -export const getAccountsByIdentity: Selector<(Identity) => Account | void> = createSelector( +export const getAccountsByIdentity: GlobalSelector<(Identity) => Account | void> = createSelector( getAccounts, accounts => { const map = new Map( @@ -86,12 +86,15 @@ export const getCurrentRealm = (state: GlobalState): URL => getActiveAccount(sta * * `getAuth` for use in the bulk of the app, operating on a logged-in * active account. */ -export const tryGetAuth: Selector = createSelector(tryGetActiveAccount, account => { - if (!account || account.apiKey === '') { - return undefined; - } - return authOfAccount(account); -}); +export const tryGetAuth: GlobalSelector = createSelector( + tryGetActiveAccount, + account => { + if (!account || account.apiKey === '') { + return undefined; + } + return authOfAccount(account); + }, +); /** * True just if there is an active, logged-in account. @@ -123,7 +126,7 @@ export const getAuth = (state: GlobalState): Auth => { * * See `getAuth` and `tryGetAuth` for discussion. */ -export const getIdentity: Selector = createSelector(getAuth, auth => +export const getIdentity: GlobalSelector = createSelector(getAuth, auth => identityOfAuth(auth), ); diff --git a/src/caughtup/caughtUpSelectors.js b/src/caughtup/caughtUpSelectors.js index 49964b78d3f..6372bb0af62 100644 --- a/src/caughtup/caughtUpSelectors.js +++ b/src/caughtup/caughtUpSelectors.js @@ -1,5 +1,5 @@ /* @flow strict-local */ -import type { CaughtUp, CaughtUpState, GlobalState, Narrow } from '../types'; +import type { CaughtUp, CaughtUpState, PerAccountState, Narrow } from '../types'; import { NULL_OBJECT } from '../nullObjects'; import { keyFromNarrow } from '../utils/narrow'; @@ -9,7 +9,7 @@ export const DEFAULT_CAUGHTUP: CaughtUp = { newer: false, }; -export const getCaughtUp = (state: GlobalState): CaughtUpState => state.caughtUp || NULL_OBJECT; +export const getCaughtUp = (state: PerAccountState): CaughtUpState => state.caughtUp || NULL_OBJECT; -export const getCaughtUpForNarrow = (state: GlobalState, narrow: Narrow): CaughtUp => +export const getCaughtUpForNarrow = (state: PerAccountState, narrow: Narrow): CaughtUp => getCaughtUp(state)[keyFromNarrow(narrow)] || DEFAULT_CAUGHTUP; diff --git a/src/chat/fetchingSelectors.js b/src/chat/fetchingSelectors.js index 173af5498aa..7a97ea68430 100644 --- a/src/chat/fetchingSelectors.js +++ b/src/chat/fetchingSelectors.js @@ -1,10 +1,10 @@ /* @flow strict-local */ -import type { Fetching, GlobalState, Narrow } from '../types'; +import type { Fetching, PerAccountState, Narrow } from '../types'; import { getFetching } from '../directSelectors'; import { keyFromNarrow } from '../utils/narrow'; /** The value implicitly represented by a missing entry in FetchingState. */ export const DEFAULT_FETCHING = { older: false, newer: false }; -export const getFetchingForNarrow = (state: GlobalState, narrow: Narrow): Fetching => +export const getFetchingForNarrow = (state: PerAccountState, narrow: Narrow): Fetching => getFetching(state)[keyFromNarrow(narrow)] || DEFAULT_FETCHING; diff --git a/src/chat/narrowsSelectors.js b/src/chat/narrowsSelectors.js index 8242045d16c..70de15c7d9b 100644 --- a/src/chat/narrowsSelectors.js +++ b/src/chat/narrowsSelectors.js @@ -4,7 +4,7 @@ import isEqual from 'lodash.isequal'; import { createSelector } from 'reselect'; import type { - GlobalState, + PerAccountState, Message, Narrow, Outbox, @@ -61,7 +61,7 @@ export const outboxMessagesForNarrow: Selector<$ReadOnlyArray, Narrow> = ); export const getFetchedMessageIdsForNarrow = ( - state: GlobalState, + state: PerAccountState, narrow: Narrow, ): $ReadOnlyArray => getAllNarrows(state).get(keyFromNarrow(narrow)) || NULL_ARRAY; @@ -181,12 +181,12 @@ export const getShownMessagesForNarrow: Selector<$ReadOnlyArray { +export const getFirstMessageId = (state: PerAccountState, narrow: Narrow): number | void => { const ids = getFetchedMessageIdsForNarrow(state, narrow); return ids.length > 0 ? ids[0] : undefined; }; -export const getLastMessageId = (state: GlobalState, narrow: Narrow): number | void => { +export const getLastMessageId = (state: PerAccountState, narrow: Narrow): number | void => { const ids = getFetchedMessageIdsForNarrow(state, narrow); return ids.length > 0 ? ids[ids.length - 1] : undefined; }; diff --git a/src/directSelectors.js b/src/directSelectors.js index 33bb190f3cf..84698c1dd69 100644 --- a/src/directSelectors.js +++ b/src/directSelectors.js @@ -1,5 +1,6 @@ /* @flow strict-local */ import type { + PerAccountState, GlobalState, AccountsState, SubscriptionsState, @@ -36,42 +37,43 @@ export const getIsOnline = (state: GlobalState): boolean | null => state.session export const getDebug = (state: GlobalState): Debug => state.session.debug; export const getIsHydrated = (state: GlobalState): boolean => state.session.isHydrated; -export const getCanCreateStreams = (state: GlobalState): boolean => state.realm.canCreateStreams; +export const getCanCreateStreams = (state: PerAccountState): boolean => + state.realm.canCreateStreams; -export const getDrafts = (state: GlobalState): DraftsState => state.drafts; +export const getDrafts = (state: PerAccountState): DraftsState => state.drafts; -export const getLoading = (state: GlobalState): boolean => state.session.loading; +export const getLoading = (state: PerAccountState): boolean => state.session.loading; -export const getMessages = (state: GlobalState): MessagesState => state.messages; +export const getMessages = (state: PerAccountState): MessagesState => state.messages; -export const getMute = (state: GlobalState): MuteState => state.mute; +export const getMute = (state: PerAccountState): MuteState => state.mute; -export const getMutedUsers = (state: GlobalState): MutedUsersState => state.mutedUsers; +export const getMutedUsers = (state: PerAccountState): MutedUsersState => state.mutedUsers; -export const getTyping = (state: GlobalState): TypingState => state.typing; +export const getTyping = (state: PerAccountState): TypingState => state.typing; -export const getTopics = (state: GlobalState): TopicsState => state.topics; +export const getTopics = (state: PerAccountState): TopicsState => state.topics; -export const getUserGroups = (state: GlobalState): UserGroupsState => state.userGroups; +export const getUserGroups = (state: PerAccountState): UserGroupsState => state.userGroups; -export const getUserStatus = (state: GlobalState): UserStatusState => state.userStatus; +export const getUserStatus = (state: PerAccountState): UserStatusState => state.userStatus; /** * WARNING: despite the name, only (a) `is_active` users (b) excluding cross-realm bots. * * See `getAllUsers`. */ -export const getUsers = (state: GlobalState): UsersState => state.users; +export const getUsers = (state: PerAccountState): UsersState => state.users; -export const getFetching = (state: GlobalState): FetchingState => state.fetching; +export const getFetching = (state: PerAccountState): FetchingState => state.fetching; -export const getFlags = (state: GlobalState): FlagsState => state.flags; +export const getFlags = (state: PerAccountState): FlagsState => state.flags; -export const getAllNarrows = (state: GlobalState): NarrowsState => state.narrows; +export const getAllNarrows = (state: PerAccountState): NarrowsState => state.narrows; export const getSettings = (state: GlobalState): SettingsState => state.settings; -export const getSubscriptions = (state: GlobalState): SubscriptionsState => state.subscriptions; +export const getSubscriptions = (state: PerAccountState): SubscriptionsState => state.subscriptions; /** * All streams in the current realm. @@ -79,23 +81,23 @@ export const getSubscriptions = (state: GlobalState): SubscriptionsState => stat * This is rarely the right selector to use: consider `getStreamForId` * or `getStreamsById` instead. */ -export const getStreams = (state: GlobalState): StreamsState => state.streams; +export const getStreams = (state: PerAccountState): StreamsState => state.streams; -export const getPresence = (state: GlobalState): PresenceState => state.presence; +export const getPresence = (state: PerAccountState): PresenceState => state.presence; -export const getOutbox = (state: GlobalState): OutboxState => state.outbox; +export const getOutbox = (state: PerAccountState): OutboxState => state.outbox; -export const getRealm = (state: GlobalState): RealmState => state.realm; +export const getRealm = (state: PerAccountState): RealmState => state.realm; -export const getCrossRealmBots = (state: GlobalState): $ReadOnlyArray => +export const getCrossRealmBots = (state: PerAccountState): $ReadOnlyArray => state.realm.crossRealmBots; -export const getRawRealmEmoji = (state: GlobalState): RealmEmojiById => state.realm.emoji; +export const getRawRealmEmoji = (state: PerAccountState): RealmEmojiById => state.realm.emoji; -export const getNonActiveUsers = (state: GlobalState): $ReadOnlyArray => +export const getNonActiveUsers = (state: PerAccountState): $ReadOnlyArray => state.realm.nonActiveUsers; -export const getIsAdmin = (state: GlobalState): boolean => state.realm.isAdmin; +export const getIsAdmin = (state: PerAccountState): boolean => state.realm.isAdmin; -export const getVideoChatProvider = (state: GlobalState): VideoChatProvider | null => +export const getVideoChatProvider = (state: PerAccountState): VideoChatProvider | null => state.realm.videoChatProvider; diff --git a/src/drafts/draftsSelectors.js b/src/drafts/draftsSelectors.js index 2b4015b0b03..6d55442a5d6 100644 --- a/src/drafts/draftsSelectors.js +++ b/src/drafts/draftsSelectors.js @@ -1,6 +1,6 @@ /* @flow strict-local */ -import type { Narrow, GlobalState } from '../types'; +import type { Narrow, PerAccountState } from '../types'; import { keyFromNarrow } from '../utils/narrow'; -export const getDraftForNarrow = (state: GlobalState, narrow: Narrow): string => +export const getDraftForNarrow = (state: PerAccountState, narrow: Narrow): string => state.drafts[keyFromNarrow(narrow)] || ''; diff --git a/src/emoji/emojiSelectors.js b/src/emoji/emojiSelectors.js index 78df38f1e1c..84b257e060e 100644 --- a/src/emoji/emojiSelectors.js +++ b/src/emoji/emojiSelectors.js @@ -1,13 +1,14 @@ /* @flow strict-local */ import { createSelector } from 'reselect'; import type { Selector, RealmEmojiById, ImageEmojiType } from '../types'; +import { assumeSecretlyGlobalState } from '../reduxTypes'; import { getRawRealmEmoji } from '../directSelectors'; import { getIdentity } from '../account/accountsSelectors'; import zulipExtraEmojiMap from './zulipExtraEmojiMap'; import { objectFromEntries } from '../jsBackport'; export const getAllImageEmojiById: Selector = createSelector( - getIdentity, + state => getIdentity(assumeSecretlyGlobalState(state)), getRawRealmEmoji, (identity, realmEmoji) => { const result: {| [string]: ImageEmojiType |} = {}; diff --git a/src/pm-conversations/pmConversationsSelectors.js b/src/pm-conversations/pmConversationsSelectors.js index b7a9709c466..6bf5e5ef6ed 100644 --- a/src/pm-conversations/pmConversationsSelectors.js +++ b/src/pm-conversations/pmConversationsSelectors.js @@ -2,7 +2,8 @@ import invariant from 'invariant'; import { createSelector } from 'reselect'; -import type { GlobalState, Message, PmConversationData, Selector } from '../types'; +import type { PerAccountState, Message, PmConversationData, Selector } from '../types'; +import { assumeSecretlyGlobalState } from '../reduxTypes'; import { getPrivateMessages } from '../message/messageSelectors'; import { getAllUsersById, getOwnUserId } from '../users/userSelectors'; import { getUnreadByPms, getUnreadByHuddles } from '../unread/unreadSelectors'; @@ -121,14 +122,14 @@ function getRecentConversationsModernImpl( } const getServerIsOld: Selector = createSelector( - getServerVersion, + state => getServerVersion(assumeSecretlyGlobalState(state)), version => !(version && version.isAtLeast(model.MIN_RECENTPMS_SERVER_VERSION)), ); /** * The most recent PM conversations, with unread count and latest message ID. */ -export const getRecentConversations = (state: GlobalState): PmConversationData[] => +export const getRecentConversations = (state: PerAccountState): PmConversationData[] => getServerIsOld(state) ? getRecentConversationsLegacy(state) : getRecentConversationsModern(state); export const getUnreadConversations: Selector< diff --git a/src/redux.js b/src/redux.js index 8e48f96eaa2..5e0238fe543 100644 --- a/src/redux.js +++ b/src/redux.js @@ -1,7 +1,7 @@ /* @flow strict-local */ import type { Store } from 'redux'; -import type { Selector, GlobalState, Action } from './reduxTypes'; +import type { GlobalSelector, GlobalState, Action } from './reduxTypes'; /** * Call a function whenever a selected piece of state changes. @@ -15,7 +15,7 @@ import type { Selector, GlobalState, Action } from './reduxTypes'; */ export function observeStore( store: Store, - select: Selector, + select: GlobalSelector, onChange: (state: T) => void, ): () => void { // Start as a nonce object, so the initial `prevState !== state` diff --git a/src/reduxTypes.js b/src/reduxTypes.js index 654d4ca5a0a..cd470b2116e 100644 --- a/src/reduxTypes.js +++ b/src/reduxTypes.js @@ -27,7 +27,7 @@ import type { UserPresence, UserStatusMapObject, } from './api/apiTypes'; -import type { SessionState } from './session/sessionReducer'; +import type { PerAccountSessionState, SessionState } from './session/sessionReducer'; import type { PmConversationsState } from './pm-conversations/pmConversationsModel'; import type { UnreadState } from './unread/unreadModelTypes'; @@ -287,19 +287,50 @@ export type ThemeName = 'default' | 'night'; */ export type BrowserPreference = 'embedded' | 'external' | 'default'; +/** + * The user's chosen settings specific to this account. + */ +export type PerAccountSettingsState = $ReadOnly<{ + offlineNotification: boolean, + onlineNotification: boolean, + streamNotification: boolean, + ... +}>; + +/** + * The user's chosen settings. + * + * This is a mix of server data representing the active account (see + * {@link PerAccountSettingsState}), and client-only data that applies + * across all the user's accounts on this client (i.e. on this install of + * the app on this device.) + */ export type SettingsState = $ReadOnly<{| + ...$Exact, + + // The properties below apply independent of account. That also means + // they can't come from the server. For per-account settings, see + // PerAccountSettingsState. + // The user's chosen language, as an IETF BCP 47 language tag. language: string, theme: ThemeName, - offlineNotification: boolean, - onlineNotification: boolean, - experimentalFeaturesEnabled: boolean, - streamNotification: boolean, browser: BrowserPreference, + + // TODO cut this? what was it? + experimentalFeaturesEnabled: boolean, + + // Possibly this should be per-account. If so it should probably be put + // on the server, so it can also be cross-device for the account. doNotMarkMessagesAsRead: boolean, |}>; +// As part of letting GlobalState freely convert to PerAccountState, +// we'll want the same for SettingsState. (This is also why +// PerAccountSettingsState is inexact.) +(s: SettingsState): PerAccountSettingsState => s; // eslint-disable-line no-unused-expressions + export type StreamsState = $ReadOnlyArray; export type SubscriptionsState = $ReadOnlyArray; @@ -327,38 +358,47 @@ export type UserStatusState = UserStatusMapObject; */ export type UsersState = $ReadOnlyArray; +/* eslint-disable no-use-before-define */ + /** - * Our complete Redux state tree. - * - * Each property is a subtree maintained by its own reducer function. These - * are named in a regular pattern; see also `src/boot/reducers.js`. - * - * We use `redux-persist` to write parts of this state tree to persistent - * device storage that survives when the app is closed, and to read it back - * ("rehydrate" it) from that storage at launch time. See `src/boot/store.js`. - * Other parts of the state tree are not persisted. - * - * See in particular `discardKeys`, `storeKeys`, and `cacheKeys`, which - * identify which subtrees are persisted and which are not. + * The portion of our Redux state with a single account's data. + * + * In a multi-account world (#5005), the full Redux state will contain one + * of these per account. Before that, in a multi-account-ready schema + * (#5006), the full Redux state may contain just one of these but as a + * subtree somewhere inside it. + * + * Initially, though, the full Redux state tree actually qualifies as a + * value of this type, and the values of this type we pass around are + * secretly just the full Redux state. The purpose of this type is to + * expose only the data that in a multi-account future will live on a single + * account's state subtree, and to recruit Flow's help in tracking which + * parts of our code will in that future operate on a particular account and + * which parts will operate on all accounts' data or none. */ -export type GlobalState = $ReadOnly<{| - accounts: AccountsState, +export type PerAccountState = $ReadOnly<{ + // TODO accounts + + // Jumbles of per-account state and client state. + session: PerAccountSessionState, + settings: PerAccountSettingsState, + + // Per-account state that's *not* from the server. + drafts: DraftsState, + outbox: OutboxState, + + // Per-account state: server data for the active account. alertWords: AlertWordsState, caughtUp: CaughtUpState, - drafts: DraftsState, fetching: FetchingState, flags: FlagsState, messages: MessagesState, - migrations: MigrationsState, mute: MuteState, mutedUsers: MutedUsersState, narrows: NarrowsState, - outbox: OutboxState, pmConversations: PmConversationsState, presence: PresenceState, realm: RealmState, - session: SessionState, - settings: SettingsState, streams: StreamsState, subscriptions: SubscriptionsState, topics: TopicsState, @@ -367,8 +407,58 @@ export type GlobalState = $ReadOnly<{| userGroups: UserGroupsState, userStatus: UserStatusState, users: UsersState, + + ... +}>; + +/** + * Our complete Redux state tree. + * + * Each property is a subtree maintained by its own reducer function. These + * are named in a regular pattern; see also `src/boot/reducers.js`. + * + * We use `redux-persist` to write parts of this state tree to persistent + * device storage that survives when the app is closed, and to read it back + * ("rehydrate" it) from that storage at launch time. See `src/boot/store.js`. + * Other parts of the state tree are not persisted. + * + * See in particular `discardKeys`, `storeKeys`, and `cacheKeys`, which + * identify which subtrees are persisted and which are not. + */ +export type GlobalState = $ReadOnly<{| + ...$Exact, + + // Metadata for the global state, as persisted on disk. + migrations: MigrationsState, + + // Jumbles of per-account state and client state. + session: SessionState, + settings: SettingsState, + + // Per-account state but for all accounts together. + // Mix of server data and otherwise. + accounts: AccountsState, |}>; +// For now, under our single-active-account model, we want a GlobalState +// to be seamlessly usable as a PerAccountState. +(s: GlobalState): PerAccountState => s; // eslint-disable-line no-unused-expressions + +/** + * Assume the given per-account state object is secretly a GlobalState. + * + * At present, this assumption is always true. But in the future it won't + * be. Calling this function has much the same effect as a fixme, but just + * makes it quite explicit that this particular assumption is being used. + * + * TODO(#5006): We'll have to fix and eliminate each call to this. + */ +export function assumeSecretlyGlobalState(state: PerAccountState): GlobalState { + // $FlowFixMe[incompatible-exact] + // $FlowFixMe[prop-missing] + return state; +} + // No substate should allow `undefined`; our use of AsyncStorage // depends on it. (This check will also complain on `null`, which I // don't think we'd have a problem with. We could try to write this @@ -378,15 +468,19 @@ type NonMaybeGlobalState = NonMaybeProperties; // This function definition will fail typechecking if GlobalState is wrong. (s: GlobalState): NonMaybeGlobalState => s; // eslint-disable-line no-unused-expressions -/** A selector returning TResult, with extra parameter TParam. */ +/** A per-account selector returning TResult, with extra parameter TParam. */ // Seems like this should be OutputSelector... but for whatever reason, // putting that on a selector doesn't cause the result type to propagate to // the corresponding parameter when used in `createSelector`, and this does. -export type Selector = InputSelector; +export type Selector = InputSelector; + +/** A GlobalState selector returning TResult, with extra parameter TParam. */ +// Seems like this should be OutputSelector; see comment on Selector above. +export type GlobalSelector = InputSelector; export interface Dispatch { (action: A): A; - (ThunkAction): T; // eslint-disable-line no-use-before-define + (ThunkAction): T; } export type ThunkAction = (Dispatch, () => GlobalState) => T; diff --git a/src/session/sessionReducer.js b/src/session/sessionReducer.js index c44a088be1f..61881499daf 100644 --- a/src/session/sessionReducer.js +++ b/src/session/sessionReducer.js @@ -19,6 +19,41 @@ import { } from '../actionConstants'; import { getHasAuth } from '../account/accountsSelectors'; +/** + * Miscellaneous non-persistent state specific to a particular account. + * + * See {@link SessionState} for discussion of what "non-persistent" means. + */ +export type PerAccountSessionState = $ReadOnly<{ + eventQueueId: number, + + /** + * Whether the /register request is in progress. + * + * This happens on startup, or on re-init following a dead event + * queue after 10 minutes of inactivity. + */ + loading: boolean, + + needsInitialFetch: boolean, + + outboxSending: boolean, + + /** + * Whether `ServerCompatNotice` (which we'll add soon) has been + * dismissed this session. + * + * We put this in the per-session state deliberately, so that users + * see the notice on every startup until the server is upgraded. + * That's a better experience than not being able to load the realm + * on mobile at all, which is what will happen soon if the user + * doesn't act on the notice. + */ + hasDismissedServerCompatNotice: boolean, + + ... +}>; + /** * Miscellaneous non-persistent state about this run of the app. * @@ -28,7 +63,11 @@ import { getHasAuth } from '../account/accountsSelectors'; * initialized to their default values. */ export type SessionState = $ReadOnly<{| - eventQueueId: number, + ...$Exact, + + // The properties below are data about the device and the app as a whole, + // independent of any particular Zulip server or account. + // For per-account data, see PerAccountSessionState. // `null` if we don't know. See the place where we set this, for what that // means. @@ -36,17 +75,7 @@ export type SessionState = $ReadOnly<{| isHydrated: boolean, - /** - * Whether the /register request is in progress. - * - * This happens on startup, or on re-init following a dead event - * queue after 10 minutes of inactivity. - */ - loading: boolean, - - needsInitialFetch: boolean, orientation: Orientation, - outboxSending: boolean, /** * Our actual device token, as most recently learned from the system. @@ -67,20 +96,13 @@ export type SessionState = $ReadOnly<{| pushToken: string | null, debug: Debug, - - /** - * Whether `ServerCompatNotice` (which we'll add soon) has been - * dismissed this session. - * - * We put this in the per-session state deliberately, so that users - * see the notice on every startup until the server is upgraded. - * That's a better experience than not being able to load the realm - * on mobile at all, which is what will happen soon if the user - * doesn't act on the notice. - */ - hasDismissedServerCompatNotice: boolean, |}>; +// As part of letting GlobalState freely convert to PerAccountState, +// we'll want the same for SessionState. (This is also why +// PerAccountSessionState is inexact.) +(s: SessionState): PerAccountSessionState => s; // eslint-disable-line no-unused-expressions + const initialState: SessionState = { eventQueueId: -1, diff --git a/src/subscriptions/subscriptionSelectors.js b/src/subscriptions/subscriptionSelectors.js index de75d562e7d..9583f7f7b51 100644 --- a/src/subscriptions/subscriptionSelectors.js +++ b/src/subscriptions/subscriptionSelectors.js @@ -1,7 +1,7 @@ /* @flow strict-local */ import { createSelector } from 'reselect'; -import type { GlobalState, Narrow, Selector, Stream, Subscription } from '../types'; +import type { PerAccountState, Narrow, Selector, Stream, Subscription } from '../types'; import { isStreamOrTopicNarrow, streamNameOfNarrow } from '../utils/narrow'; import { getSubscriptions, getStreams } from '../directSelectors'; @@ -70,7 +70,7 @@ export const getSubscribedStreams: Selector = createSelector( * See `getStreamsById` for use when a stream might not exist, or when * multiple streams are relevant. */ -export const getStreamForId = (state: GlobalState, streamId: number): Stream => { +export const getStreamForId = (state: PerAccountState, streamId: number): Stream => { const stream = getStreamsById(state).get(streamId); if (!stream) { throw new Error(`getStreamForId: missing stream: id ${streamId}`); @@ -97,7 +97,7 @@ export const getIsActiveStreamAnnouncementOnly: Selector = crea * * Gives undefined for narrows that are not stream or topic narrows. */ -export const getStreamColorForNarrow = (state: GlobalState, narrow: Narrow): string | void => { +export const getStreamColorForNarrow = (state: PerAccountState, narrow: Narrow): string | void => { if (!isStreamOrTopicNarrow(narrow)) { return undefined; } diff --git a/src/unread/unreadModel.js b/src/unread/unreadModel.js index 54e8aa65ced..2c9671c9e0c 100644 --- a/src/unread/unreadModel.js +++ b/src/unread/unreadModel.js @@ -10,7 +10,7 @@ import type { UnreadHuddlesState, UnreadMentionsState, } from './unreadModelTypes'; -import type { GlobalState } from '../reduxTypes'; +import type { PerAccountState } from '../reduxTypes'; import unreadPmsReducer from './unreadPmsReducer'; import unreadHuddlesReducer from './unreadHuddlesReducer'; import unreadMentionsReducer from './unreadMentionsReducer'; @@ -32,15 +32,18 @@ import { // /** The unread-messages state as a whole. */ -export const getUnread = (state: GlobalState): UnreadState => state.unread; +export const getUnread = (state: PerAccountState): UnreadState => state.unread; -export const getUnreadStreams = (state: GlobalState): UnreadStreamsState => state.unread.streams; +export const getUnreadStreams = (state: PerAccountState): UnreadStreamsState => + state.unread.streams; -export const getUnreadPms = (state: GlobalState): UnreadPmsState => state.unread.pms; +export const getUnreadPms = (state: PerAccountState): UnreadPmsState => state.unread.pms; -export const getUnreadHuddles = (state: GlobalState): UnreadHuddlesState => state.unread.huddles; +export const getUnreadHuddles = (state: PerAccountState): UnreadHuddlesState => + state.unread.huddles; -export const getUnreadMentions = (state: GlobalState): UnreadMentionsState => state.unread.mentions; +export const getUnreadMentions = (state: PerAccountState): UnreadMentionsState => + state.unread.mentions; // // @@ -116,7 +119,7 @@ function deleteMessages( function streamsReducer( state: UnreadStreamsState = initialStreamsState, action: Action, - globalState: GlobalState, + globalState: PerAccountState, ): UnreadStreamsState { switch (action.type) { case LOGOUT: @@ -208,7 +211,7 @@ function streamsReducer( export const reducer = ( state: void | UnreadState, action: Action, - globalState: GlobalState, + globalState: PerAccountState, ): UnreadState => { const nextState = { streams: streamsReducer(state?.streams, action, globalState), diff --git a/src/user-status/userStatusSelectors.js b/src/user-status/userStatusSelectors.js index 0bb661b3304..81090ec407d 100644 --- a/src/user-status/userStatusSelectors.js +++ b/src/user-status/userStatusSelectors.js @@ -1,5 +1,5 @@ /* @flow strict-local */ -import type { GlobalState, Selector, UserId, UserStatus } from '../types'; +import type { PerAccountState, Selector, UserId, UserStatus } from '../types'; import { getUserStatus } from '../directSelectors'; import { getOwnUserId } from '../users/userSelectors'; @@ -7,7 +7,7 @@ import { getOwnUserId } from '../users/userSelectors'; * Extract the user status object for the logged in user. * If no away status and status text have been set we do not have any data thus `undefined`. */ -export const getSelfUserStatus: Selector = (state: GlobalState) => { +export const getSelfUserStatus: Selector = (state: PerAccountState) => { const userStatus = getUserStatus(state); return userStatus[getOwnUserId(state)]; }; @@ -17,7 +17,7 @@ export const getSelfUserStatus: Selector = (state: GlobalState) => * It is `true` if explicitly set to that value. * If no value is set we consider it `false`. */ -export const getSelfUserAwayStatus = (state: GlobalState): boolean => { +export const getSelfUserAwayStatus = (state: PerAccountState): boolean => { const selfUserStatus = getSelfUserStatus(state); return !!(selfUserStatus && selfUserStatus.away); }; @@ -27,7 +27,7 @@ export const getSelfUserAwayStatus = (state: GlobalState): boolean => { * If it is set we get as result that value. * If no value is set we get a valid but empty string. */ -export const getSelfUserStatusText = (state: GlobalState): string => { +export const getSelfUserStatusText = (state: PerAccountState): string => { const selfUserStatus = getSelfUserStatus(state); return (selfUserStatus && selfUserStatus.status_text) || ''; }; @@ -36,7 +36,7 @@ export const getSelfUserStatusText = (state: GlobalState): string => { * Returns the `status text` value of the user with the given userId. * We return `undefined` if no value is set. */ -export const getUserStatusTextForUser = (state: GlobalState, userId: UserId): string | void => { +export const getUserStatusTextForUser = (state: PerAccountState, userId: UserId): string | void => { const userStatus = getUserStatus(state); return userStatus[userId] && userStatus[userId].status_text; }; diff --git a/src/users/userSelectors.js b/src/users/userSelectors.js index 8070092de07..d91c57789e3 100644 --- a/src/users/userSelectors.js +++ b/src/users/userSelectors.js @@ -1,7 +1,7 @@ /* @flow strict-local */ import { createSelector } from 'reselect'; -import type { GlobalState, UserOrBot, Selector, User, UserId } from '../types'; +import type { GlobalState, PerAccountState, UserOrBot, Selector, User, UserId } from '../types'; import { getUsers, getCrossRealmBots, getNonActiveUsers } from '../directSelectors'; import { getHasAuth, tryGetActiveAccount } from '../account/accountsSelectors'; @@ -82,7 +82,7 @@ export const getSortedUsers: Selector = createSelector(getUsers, users = */ // Not currently used, but should replace uses of `getOwnEmail` (e.g. inside // `getOwnUser`). See #3764. -export const getOwnUserId = (state: GlobalState): UserId => { +export const getOwnUserId = (state: PerAccountState): UserId => { const { user_id } = state.realm; if (user_id === undefined) { throw new Error('No server data found'); @@ -97,7 +97,7 @@ export const getOwnUserId = (state: GlobalState): UserId => { * * Prefer using `getOwnUserId` or `getOwnUser`; see #3764. */ -export const getOwnEmail = (state: GlobalState): string => { +export const getOwnEmail = (state: PerAccountState): string => { const { email } = state.realm; if (email === undefined) { throw new Error('No server data found'); @@ -116,7 +116,7 @@ export const getOwnEmail = (state: GlobalState): string => { * * See also `getOwnUserId` and `getOwnEmail`. */ -export const getOwnUser = (state: GlobalState): User => { +export const getOwnUser = (state: PerAccountState): User => { const ownUser = getUsersById(state).get(getOwnUserId(state)); if (ownUser === undefined) { throw new Error('Have ownUserId, but not found in user data'); @@ -134,7 +134,7 @@ export const getOwnUser = (state: GlobalState): User => { * throwing if none. That makes it a bit simpler to use in contexts where * we assume the relevant user must exist, which is true of most of the app. */ -export const tryGetUserForId = (state: GlobalState, userId: UserId): UserOrBot | null => +export const tryGetUserForId = (state: PerAccountState, userId: UserId): UserOrBot | null => getAllUsersById(state).get(userId) ?? null; /** @@ -147,7 +147,7 @@ export const tryGetUserForId = (state: GlobalState, userId: UserId): UserOrBot | * * See `tryGetUserForId` for a non-throwing version. */ -export const getUserForId = (state: GlobalState, userId: UserId): UserOrBot => { +export const getUserForId = (state: PerAccountState, userId: UserId): UserOrBot => { const user = tryGetUserForId(state, userId); if (!user) { throw new Error(`getUserForId: missing user: id ${userId}`); @@ -185,7 +185,7 @@ const getActiveUsersById: Selector> = createSelector( */ // To understand this implementation, see the comment about `is_active` in // the `User` type definition. -export const getUserIsActive = (state: GlobalState, userId: UserId): boolean => +export const getUserIsActive = (state: PerAccountState, userId: UserId): boolean => !!getActiveUsersById(state).get(userId); /**