diff --git a/src/account/AccountPickScreen.js b/src/account/AccountPickScreen.js index 07101bfeec6..888476e345e 100644 --- a/src/account/AccountPickScreen.js +++ b/src/account/AccountPickScreen.js @@ -9,7 +9,7 @@ import { TranslationContext } from '../boot/TranslationProvider'; import type { RouteProp } from '../react-navigation'; import type { AppNavigationProp } from '../nav/AppNavigator'; import * as NavigationService from '../nav/NavigationService'; -import { useSelector, useGlobalDispatch } from '../react-redux'; +import { useGlobalSelector, useGlobalDispatch } from '../react-redux'; import { getAccountStatuses } from '../selectors'; import { Centerer, ZulipButton, Logo, Screen, ViewPlaceholder } from '../common'; import AccountList from './AccountList'; @@ -29,7 +29,7 @@ type Props = $ReadOnly<{| export default function AccountPickScreen(props: Props): Node { const { navigation } = props; - const accounts = useSelector(getAccountStatuses); + const accounts = useGlobalSelector(getAccountStatuses); const dispatch = useGlobalDispatch(); const _ = useContext(TranslationContext); diff --git a/src/boot/HideIfNotHydrated.js b/src/boot/HideIfNotHydrated.js index 3f81e926f08..fe1fe2882e5 100644 --- a/src/boot/HideIfNotHydrated.js +++ b/src/boot/HideIfNotHydrated.js @@ -1,8 +1,8 @@ /* @flow strict-local */ import React from 'react'; import type { Node } from 'react'; -import { useSelector } from 'react-redux'; +import { useGlobalSelector } from '../react-redux'; import { getIsHydrated } from '../selectors'; type Props = $ReadOnly<{| @@ -12,7 +12,7 @@ type Props = $ReadOnly<{| |}>; export default function HideIfNotHydrated(props: Props): Node { - const isHydrated = useSelector(getIsHydrated); + const isHydrated = useGlobalSelector(getIsHydrated); const { children, PlaceholderComponent } = props; diff --git a/src/boot/ThemeProvider.js b/src/boot/ThemeProvider.js index 2b167f1f3ae..0be8aae64d5 100644 --- a/src/boot/ThemeProvider.js +++ b/src/boot/ThemeProvider.js @@ -3,7 +3,7 @@ import React from 'react'; import type { Node } from 'react'; -import { useSelector } from '../react-redux'; +import { useGlobalSelector } from '../react-redux'; import { getGlobalSettings } from '../directSelectors'; import { themeData, ThemeContext } from '../styles/theme'; import { ZulipStatusBar } from '../common'; @@ -14,7 +14,7 @@ type Props = $ReadOnly<{| export default function ThemeProvider(props: Props): Node { const { children } = props; - const theme = useSelector(state => getGlobalSettings(state).theme); + const theme = useGlobalSelector(state => getGlobalSettings(state).theme); return ( diff --git a/src/boot/TranslationProvider.js b/src/boot/TranslationProvider.js index efb7a6ecb3b..01577984104 100644 --- a/src/boot/TranslationProvider.js +++ b/src/boot/TranslationProvider.js @@ -6,7 +6,7 @@ import { IntlProvider, IntlContext } from 'react-intl'; import type { IntlShape } from 'react-intl'; import type { GetText } from '../types'; -import { useSelector } from '../react-redux'; +import { useGlobalSelector } from '../react-redux'; import { getGlobalSettings } from '../selectors'; import messages from '../i18n/messages'; @@ -78,7 +78,7 @@ type Props = $ReadOnly<{| export default function TranslationProvider(props: Props): Node { const { children } = props; - const language = useSelector(state => getGlobalSettings(state).language); + const language = useGlobalSelector(state => getGlobalSettings(state).language); return ( diff --git a/src/common/OfflineNotice.js b/src/common/OfflineNotice.js index 3a41bebf2d6..8877b6cf839 100644 --- a/src/common/OfflineNotice.js +++ b/src/common/OfflineNotice.js @@ -8,7 +8,7 @@ import NetInfo from '@react-native-community/netinfo'; import * as logging from '../utils/logging'; import { createStyleSheet, HALF_COLOR } from '../styles'; import { useHasStayedTrueForMs } from '../reactUtils'; -import { useSelector } from '../react-redux'; +import { useGlobalSelector } from '../react-redux'; import { getGlobalSession } from '../selectors'; import Label from './Label'; @@ -37,7 +37,7 @@ type Props = $ReadOnly<{||}>; * Shows nothing if the Internet is reachable. */ export default function OfflineNotice(props: Props): Node { - const isOnline = useSelector(state => getGlobalSession(state).isOnline); + const isOnline = useGlobalSelector(state => getGlobalSession(state).isOnline); const shouldShowUncertaintyNotice = useHasStayedTrueForMs( // See note in `SessionState` for what this means. diff --git a/src/common/Popup.js b/src/common/Popup.js index 104c3401a0c..1b9c838a1c4 100644 --- a/src/common/Popup.js +++ b/src/common/Popup.js @@ -4,7 +4,7 @@ import type { Node } from 'react'; import { View } from 'react-native'; import { ThemeContext, createStyleSheet } from '../styles'; -import { useSelector } from '../react-redux'; +import { useGlobalSelector } from '../react-redux'; import { getGlobalSettings } from '../directSelectors'; const styles = createStyleSheet({ @@ -46,7 +46,7 @@ type Props = $ReadOnly<{| export default function Popup(props: Props): Node { const themeContext = useContext(ThemeContext); // TODO(color/theme): find a cleaner way to express this - const isDarkTheme = useSelector(state => getGlobalSettings(state).theme !== 'default'); + const isDarkTheme = useGlobalSelector(state => getGlobalSettings(state).theme !== 'default'); return ( {props.children} diff --git a/src/common/ServerCompatBanner.js b/src/common/ServerCompatBanner.js index ec5d5d19c0a..136a9726199 100644 --- a/src/common/ServerCompatBanner.js +++ b/src/common/ServerCompatBanner.js @@ -4,7 +4,7 @@ import React from 'react'; import type { Node } from 'react'; import ZulipBanner from './ZulipBanner'; -import { useSelector, useDispatch } from '../react-redux'; +import { useSelector, useGlobalSelector, useDispatch } from '../react-redux'; import { getIdentity, getServerVersion } from '../account/accountsSelectors'; import { getIsAdmin, getSession, getGlobalSettings } from '../directSelectors'; import { dismissCompatNotice } from '../session/sessionActions'; @@ -27,7 +27,7 @@ export default function ServerCompatBanner(props: Props): Node { const zulipVersion = useSelector(getServerVersion); const realm = useSelector(state => getIdentity(state).realm); const isAdmin = useSelector(getIsAdmin); - const settings = useSelector(getGlobalSettings); + const settings = useGlobalSelector(getGlobalSettings); let visible = false; let text = ''; diff --git a/src/common/ZulipStatusBar.js b/src/common/ZulipStatusBar.js index 3b3036321f8..681d7a20408 100644 --- a/src/common/ZulipStatusBar.js +++ b/src/common/ZulipStatusBar.js @@ -7,7 +7,7 @@ import { Platform, StatusBar } from 'react-native'; import Color from 'color'; import type { ThemeName } from '../types'; -import { useSelector } from '../react-redux'; +import { useGlobalSelector } from '../react-redux'; import { foregroundColorFromBackground } from '../utils/color'; import { getGlobalSession, getGlobalSettings } from '../selectors'; @@ -45,8 +45,8 @@ type Props = $ReadOnly<{| */ export default function ZulipStatusBar(props: Props): Node { const { hidden = false } = props; - const theme = useSelector(state => getGlobalSettings(state).theme); - const orientation = useSelector(state => getGlobalSession(state).orientation); + const theme = useGlobalSelector(state => getGlobalSettings(state).theme); + const orientation = useGlobalSelector(state => getGlobalSession(state).orientation); const backgroundColor = props.backgroundColor; const statusBarColor = getStatusBarColor(backgroundColor, theme); return ( diff --git a/src/compose/MentionWarnings.js b/src/compose/MentionWarnings.js index aa4c466dfd3..481bcd428f9 100644 --- a/src/compose/MentionWarnings.js +++ b/src/compose/MentionWarnings.js @@ -2,9 +2,9 @@ import React, { useState, useCallback, useContext, forwardRef, useImperativeHandle } from 'react'; import type { AbstractComponent, Node } from 'react'; -import { useSelector } from 'react-redux'; import type { Stream, Narrow, UserOrBot, Subscription, UserId } from '../types'; +import { useSelector } from '../react-redux'; import { TranslationContext } from '../boot/TranslationProvider'; import { getAllUsersById, getAuth } from '../selectors'; import { isPmNarrow } from '../utils/narrow'; diff --git a/src/lightbox/Lightbox.js b/src/lightbox/Lightbox.js index fed73315e97..0c55e3e187d 100644 --- a/src/lightbox/Lightbox.js +++ b/src/lightbox/Lightbox.js @@ -10,7 +10,7 @@ import { useActionSheet } from '@expo/react-native-action-sheet'; import * as NavigationService from '../nav/NavigationService'; import type { Message } from '../types'; -import { useSelector } from '../react-redux'; +import { useGlobalSelector, useSelector } from '../react-redux'; import type { ShowActionSheetWithOptions } from '../action-sheets'; import { getAuth, getGlobalSession } from '../selectors'; import { getResource } from '../utils/url'; @@ -69,7 +69,7 @@ export default function Lightbox(props: Props): Node { // Since we're using `Dimensions.get` (below), we'll want a rerender // when the orientation changes. No need to store the value. - useSelector(state => getGlobalSession(state).orientation); + useGlobalSelector(state => getGlobalSession(state).orientation); const { width: windowWidth, height: windowHeight } = Dimensions.get('window'); diff --git a/src/nav/AppNavigator.js b/src/nav/AppNavigator.js index 7732ef1768b..962ccd89291 100644 --- a/src/nav/AppNavigator.js +++ b/src/nav/AppNavigator.js @@ -9,7 +9,7 @@ import { } from '@react-navigation/stack'; import type { RouteParamsOf } from '../react-navigation'; -import { useSelector } from '../react-redux'; +import { useGlobalSelector } from '../react-redux'; import { getHasAuth, getAccounts } from '../selectors'; import getInitialRouteInfo from './getInitialRouteInfo'; import type { GlobalParamList } from './globalTypes'; @@ -89,8 +89,8 @@ const Stack = createStackNavigator; export default function AppNavigator(props: Props): Node { - const hasAuth = useSelector(getHasAuth); - const accounts = useSelector(getAccounts); + const hasAuth = useGlobalSelector(getHasAuth); + const accounts = useGlobalSelector(getAccounts); const { initialRouteName, initialRouteParams } = getInitialRouteInfo({ hasAuth, diff --git a/src/nav/ZulipNavigationContainer.js b/src/nav/ZulipNavigationContainer.js index e7491a55db3..b3bb214723f 100644 --- a/src/nav/ZulipNavigationContainer.js +++ b/src/nav/ZulipNavigationContainer.js @@ -3,7 +3,7 @@ import React, { useContext, useEffect } from 'react'; import type { Node } from 'react'; import { NavigationContainer, DefaultTheme, DarkTheme } from '@react-navigation/native'; -import { useSelector } from '../react-redux'; +import { useGlobalSelector } from '../react-redux'; import { ThemeContext } from '../styles'; import * as NavigationService from './NavigationService'; import { getGlobalSettings } from '../selectors'; @@ -22,7 +22,7 @@ type Props = $ReadOnly<{||}>; * and `initialRouteParams` which we get from data in Redux. */ export default function ZulipAppContainer(props: Props): Node { - const themeName = useSelector(state => getGlobalSettings(state).theme); + const themeName = useGlobalSelector(state => getGlobalSettings(state).theme); useEffect( () => diff --git a/src/notification/notificationActions.js b/src/notification/notificationActions.js index 5f2035ebacf..e6c9f9a608f 100644 --- a/src/notification/notificationActions.js +++ b/src/notification/notificationActions.js @@ -1,7 +1,15 @@ /* @flow strict-local */ import { Platform } from 'react-native'; -import type { Account, Dispatch, Identity, Action, ThunkAction, GlobalThunkAction } from '../types'; +import type { + Account, + Dispatch, + GlobalDispatch, + Identity, + Action, + ThunkAction, + GlobalThunkAction, +} from '../types'; import * as api from '../api'; import { getNotificationToken, @@ -58,12 +66,30 @@ export const narrowToNotification = (data: ?Notification): GlobalThunkAction { +const sendPushToken = async ( + // Why `Dispatch | GlobalDispatch`? Well, this function is per-account... + // but whereas virtually all our other per-account code is implicitly + // about the active account, this is about a specific account it's + // explicitly passed. That makes it equally legitimate to call from + // per-account or global code, and we do both. + // TODO(#5006): Once we have per-account states for all accounts, make + // this an ordinary per-account action. + dispatch: Dispatch | GlobalDispatch, + account: Account | void, + pushToken: string, +) => { if (!account || account.apiKey === '') { // We've logged out of the account and/or forgotten it. Shrug. return; diff --git a/src/presence/PresenceHeartbeat.js b/src/presence/PresenceHeartbeat.js index ea79acd32e9..1599acfa7d9 100644 --- a/src/presence/PresenceHeartbeat.js +++ b/src/presence/PresenceHeartbeat.js @@ -2,8 +2,8 @@ import { PureComponent } from 'react'; import type { ComponentType } from 'react'; import { AppState } from 'react-native'; -import type { Dispatch } from '../types'; +import { assumeSecretlyGlobalState, type Dispatch } from '../reduxTypes'; import { connect } from '../react-redux'; import { getHasAuth } from '../account/accountsSelectors'; import { reportPresence } from '../actions'; @@ -76,7 +76,7 @@ class PresenceHeartbeatInner extends PureComponent { /** (NB this is a per-account component.) */ // TODO(#5005): either make one of these per account, or make it act on all accounts const PresenceHeartbeat: ComponentType = connect(state => ({ - hasAuth: getHasAuth(state), + hasAuth: getHasAuth(assumeSecretlyGlobalState(state)), // a job for withHaveServerDataGate? }))(PresenceHeartbeatInner); export default PresenceHeartbeat; diff --git a/src/react-redux.js b/src/react-redux.js index 0028ae09f30..135dce0d1ec 100644 --- a/src/react-redux.js +++ b/src/react-redux.js @@ -6,7 +6,7 @@ import { useDispatch as useDispatchInner, } from 'react-redux'; -import type { GlobalState, Dispatch, GlobalDispatch } from './types'; +import type { PerAccountState, GlobalState, Dispatch, GlobalDispatch } from './types'; import type { BoundedDiff } from './generics'; /* eslint-disable flowtype/generic-spacing */ @@ -69,8 +69,7 @@ export type OwnProps = BoundedDiff< */ // prettier-ignore export function connect>( - // TODO(#5006): should be PerAccountState - mapStateToProps?: (GlobalState, OwnProps) => SP, ): C => ComponentType<$ReadOnly>> { @@ -101,6 +100,13 @@ export function connectGlobal>( * function effectively gets no type-checking of anything it does with it. */ export function useSelector( + selector: (state: PerAccountState) => SS, + equalityFn?: (a: SS, b: SS) => boolean, +): SS { + return useSelectorInner(selector, equalityFn); +} + +export function useGlobalSelector( selector: (state: GlobalState) => SS, equalityFn?: (a: SS, b: SS) => boolean, ): SS { diff --git a/src/reduxTypes.js b/src/reduxTypes.js index ed12d496d2c..48538d6eac6 100644 --- a/src/reduxTypes.js +++ b/src/reduxTypes.js @@ -530,23 +530,17 @@ export interface GlobalDispatch { } /** A global thunk action returning T. */ -export type GlobalThunkAction = ( - GlobalDispatch, - () => GlobalState, - // These extras are meant for a per-account thunk action; when writing a - // global thunk action, everything they provide is redundant with the - // GlobalState provided by the previous argument. But passing them here - // allows a ThunkAction to be used as a GlobalThunkAction. For #5006 - // we'll want to disallow that, but it's convenient for the present. - ThunkExtras, -) => T; +// This might take some extras later (e.g., to do something per-account on a +// specific account), but for now it needs none. +export type GlobalThunkAction = (GlobalDispatch, () => GlobalState) => T; /* eslint-disable no-unused-expressions */ -// For now, it'll smooth our migration to let a GlobalDispatch be seamlessly -// usable as a plain Dispatch, and a ThunkAction as a GlobalThunkAction. -(d: GlobalDispatch): Dispatch => d; // TODO(#5006) -(a: ThunkAction): GlobalThunkAction => a; // TODO(#5006) -// But we don't allow the reverse. +// The two pairs of dispatch/thunk-action types aren't interchangeable, +// in either direction. +// $FlowExpectedError[incompatible-return] +(d: GlobalDispatch): Dispatch => d; +// $FlowExpectedError[incompatible-return] +(a: ThunkAction): GlobalThunkAction => a; // $FlowExpectedError[incompatible-return] (d: Dispatch): GlobalDispatch => d; // $FlowExpectedError[incompatible-exact] diff --git a/src/settings/LanguageScreen.js b/src/settings/LanguageScreen.js index 81c530e33d1..79196162459 100644 --- a/src/settings/LanguageScreen.js +++ b/src/settings/LanguageScreen.js @@ -5,7 +5,7 @@ import type { Node } from 'react'; import type { RouteProp } from '../react-navigation'; import type { AppNavigationProp } from '../nav/AppNavigator'; -import { useSelector, useDispatch } from '../react-redux'; +import { useGlobalSelector, useDispatch } from '../react-redux'; import { Screen } from '../common'; import LanguagePicker from './LanguagePicker'; import { getGlobalSettings } from '../selectors'; @@ -18,7 +18,7 @@ type Props = $ReadOnly<{| export default function LanguageScreen(props: Props): Node { const dispatch = useDispatch(); - const language = useSelector(state => getGlobalSettings(state).language); + const language = useGlobalSelector(state => getGlobalSettings(state).language); const [filter, setFilter] = useState(''); diff --git a/src/settings/SettingsScreen.js b/src/settings/SettingsScreen.js index db3515250fd..266efff4550 100644 --- a/src/settings/SettingsScreen.js +++ b/src/settings/SettingsScreen.js @@ -6,7 +6,7 @@ import type { Node } from 'react'; import type { RouteProp } from '../react-navigation'; import type { MainTabsNavigationProp } from '../main/MainTabsScreen'; import * as NavigationService from '../nav/NavigationService'; -import { useSelector, useDispatch } from '../react-redux'; +import { useGlobalSelector, useDispatch } from '../react-redux'; import { getGlobalSettings } from '../selectors'; import { NestedNavRow, SwitchRow, Screen } from '../common'; import { @@ -30,9 +30,9 @@ type Props = $ReadOnly<{| |}>; export default function SettingsScreen(props: Props): Node { - const theme = useSelector(state => getGlobalSettings(state).theme); - const browser = useSelector(state => getGlobalSettings(state).browser); - const doNotMarkMessagesAsRead = useSelector( + const theme = useGlobalSelector(state => getGlobalSettings(state).theme); + const browser = useGlobalSelector(state => getGlobalSettings(state).browser); + const doNotMarkMessagesAsRead = useGlobalSelector( state => getGlobalSettings(state).doNotMarkMessagesAsRead, ); const dispatch = useDispatch(); diff --git a/src/sharing/SharingScreen.js b/src/sharing/SharingScreen.js index 67ac405cefe..045237412e6 100644 --- a/src/sharing/SharingScreen.js +++ b/src/sharing/SharingScreen.js @@ -14,7 +14,7 @@ import * as NavigationService from '../nav/NavigationService'; import type { SharedData } from './types'; import { createStyleSheet } from '../styles'; import { materialTopTabNavigatorConfig } from '../styles/tabs'; -import { useSelector } from '../react-redux'; +import { useGlobalSelector } from '../react-redux'; import { Label, Screen } from '../common'; import { getHasAuth } from '../selectors'; import { navigateToAccountPicker } from '../nav/navActions'; @@ -50,7 +50,7 @@ const styles = createStyleSheet({ export default function SharingScreen(props: Props): Node { const { params } = props.route; - const hasAuth = useSelector(getHasAuth); + const hasAuth = useGlobalSelector(getHasAuth); // If there is no active logged-in account, abandon the sharing attempt, // and present the account picker screen to the user. diff --git a/src/start/IosCompliantAppleAuthButton/index.js b/src/start/IosCompliantAppleAuthButton/index.js index 67a0849dda2..e713e576ab0 100644 --- a/src/start/IosCompliantAppleAuthButton/index.js +++ b/src/start/IosCompliantAppleAuthButton/index.js @@ -4,7 +4,7 @@ import type { Node } from 'react'; import { View } from 'react-native'; import type { ViewStyle } from 'react-native/Libraries/StyleSheet/StyleSheet'; import * as AppleAuthentication from 'expo-apple-authentication'; -import { useSelector } from '../../react-redux'; +import { useGlobalSelector } from '../../react-redux'; import type { SubsetProperties } from '../../generics'; import Custom from './Custom'; @@ -35,7 +35,7 @@ type Props = $ReadOnly<{| */ export default function IosCompliantAppleAuthButton(props: Props): Node { const { style, onPress } = props; - const theme = useSelector(state => getGlobalSettings(state).theme); + const theme = useGlobalSelector(state => getGlobalSettings(state).theme); const [isNativeButtonAvailable, setIsNativeButtonAvailable] = useState(undefined); useEffect(() => { diff --git a/src/webview/MessageList.js b/src/webview/MessageList.js index 2a6eca07185..32239df416f 100644 --- a/src/webview/MessageList.js +++ b/src/webview/MessageList.js @@ -27,6 +27,7 @@ import type { UserOrBot, EditMessage, } from '../types'; +import { assumeSecretlyGlobalState } from '../reduxTypes'; import type { ThemeData } from '../styles'; import { ThemeContext } from '../styles'; import { connect } from '../react-redux'; @@ -336,6 +337,13 @@ const marksMessagesAsRead = (narrow: Narrow): boolean => const MessageList: ComponentType = connect( (state, props: OuterProps) => { + // If this were a function component with Hooks, these would be + // useGlobalSelector calls and would coexist perfectly smoothly with + // useSelector calls for the per-account data. As long as it's not, + // they should probably turn into a `connectGlobal` call. + const globalSettings = getGlobalSettings(assumeSecretlyGlobalState(state)); + const debug = getDebug(assumeSecretlyGlobalState(state)); + // TODO Ideally this ought to be a caching selector that doesn't change // when the inputs don't. Doesn't matter in a practical way here, because // we have a `shouldComponentUpdate` that doesn't look at this prop... but @@ -344,9 +352,9 @@ const MessageList: ComponentType = connect( alertWords: state.alertWords, allImageEmojiById: getAllImageEmojiById(state), auth: getAuth(state), - debug: getDebug(state), + debug, doNotMarkMessagesAsRead: - !marksMessagesAsRead(props.narrow) || getGlobalSettings(state).doNotMarkMessagesAsRead, + !marksMessagesAsRead(props.narrow) || globalSettings.doNotMarkMessagesAsRead, flags: getFlags(state), mute: getMute(state), mutedUsers: getMutedUsers(state), @@ -355,7 +363,7 @@ const MessageList: ComponentType = connect( streamsByName: getStreamsByName(state), subscriptions: getSubscriptionsById(state), unread: getUnread(state), - theme: getGlobalSettings(state).theme, + theme: globalSettings.theme, twentyFourHourTime: getRealm(state).twentyFourHourTime, userSettingStreamNotification: getSettings(state).streamNotification, };