diff --git a/lib/keyserver-conn/call-keyserver-endpoint-provider.react.js b/lib/keyserver-conn/call-keyserver-endpoint-provider.react.js index 946039823..0902fd607 100644 --- a/lib/keyserver-conn/call-keyserver-endpoint-provider.react.js +++ b/lib/keyserver-conn/call-keyserver-endpoint-provider.react.js @@ -1,227 +1,272 @@ // @flow +import invariant from 'invariant'; import _memoize from 'lodash/memoize.js'; +import * as React from 'react'; import { createSelector } from 'reselect'; import { setNewSession } from './keyserver-conn-types.js'; import { canResolveKeyserverSessionInvalidation, resolveKeyserverSessionInvalidation, } from './recovery-utils.js'; import { logInActionSources } from '../types/account-types.js'; import type { PlatformDetails } from '../types/device-types.js'; import type { Endpoint, SocketAPIHandler } from '../types/endpoints.js'; import type { Dispatch } from '../types/redux-types.js'; import type { ClientSessionChange } from '../types/session-types.js'; import type { CurrentUserInfo } from '../types/user-types.js'; import type { CallServerEndpoint, CallServerEndpointOptions, } from '../utils/call-server-endpoint.js'; import callServerEndpoint from '../utils/call-server-endpoint.js'; +export type ActionFunc = (callServerEndpoint: CallServerEndpoint) => F; +type CreateBoundServerCallsSelectorType = ( + ActionFunc, +) => BindServerCallsParams => F; +type CallKeyserverEndpointContextType = { + +bindCookieAndUtilsIntoCallServerEndpoint: ( + params: BindServerCallsParams, + ) => CallServerEndpoint, + +createBoundServerCallsSelector: CreateBoundServerCallsSelectorType, +}; + +const CallKeyserverEndpointContext: React.Context = + React.createContext(); + let currentlyWaitingForNewCookie = false; let serverEndpointCallsWaitingForNewCookie: (( callServerEndpoint: ?CallServerEndpoint, ) => void)[] = []; -// Third param is optional and gets called with newCookie if we get a new cookie -// Necessary to propagate cookie in cookieInvalidationRecovery below -function bindCookieAndUtilsIntoCallServerEndpoint( - params: BindServerCallsParams, -): CallServerEndpoint { - const { - dispatch, - cookie, - urlPrefix, - sessionID, - currentUserInfo, - isSocketConnected, - lastCommunicatedPlatformDetails, - keyserverID, - } = params; - const loggedIn = !!(currentUserInfo && !currentUserInfo.anonymous && true); - const boundSetNewSession = ( - sessionChange: ClientSessionChange, - error: ?string, - ) => - setNewSession( - dispatch, - sessionChange, - { - currentUserInfo, - cookiesAndSessions: { [keyserverID]: { cookie, sessionID } }, - }, - error, - undefined, - keyserverID, - ); - const canResolveInvalidation = canResolveKeyserverSessionInvalidation(); - // This function gets called before callServerEndpoint sends a request, - // to make sure that we're not in the middle of trying to recover - // an invalidated cookie - const waitIfCookieInvalidated = () => { - if (!canResolveInvalidation) { - // If there is no way to resolve the session invalidation, - // just let the caller callServerEndpoint instance continue - return Promise.resolve(null); - } - if (!currentlyWaitingForNewCookie) { - // Our cookie seems to be valid - return Promise.resolve(null); - } - // Wait to run until we get our new cookie - return new Promise(r => - serverEndpointCallsWaitingForNewCookie.push(r), - ); - }; - // This function is a helper for the next function defined below - const attemptToResolveInvalidation = async ( - sessionChange: ClientSessionChange, - ) => { - const newAnonymousCookie = sessionChange.cookie; - const newSessionChange = await resolveKeyserverSessionInvalidation( - dispatch, - newAnonymousCookie, - urlPrefix, - logInActionSources.cookieInvalidationResolutionAttempt, - keyserverID, - ); - - currentlyWaitingForNewCookie = false; - const currentWaitingCalls = serverEndpointCallsWaitingForNewCookie; - serverEndpointCallsWaitingForNewCookie = []; - - const newCallServerEndpoint = newSessionChange - ? bindCookieAndUtilsIntoCallServerEndpoint({ - ...params, - cookie: newSessionChange.cookie, - sessionID: newSessionChange.sessionID, - currentUserInfo: newSessionChange.currentUserInfo, - }) - : null; - for (const func of currentWaitingCalls) { - func(newCallServerEndpoint); - } - return newCallServerEndpoint; - }; - // If this function is called, callServerEndpoint got a response invalidating - // its cookie, and is wondering if it should just like... give up? - // Or if there's a chance at redemption - const cookieInvalidationRecovery = (sessionChange: ClientSessionChange) => { - if (!canResolveInvalidation) { - // If there is no way to resolve the session invalidation, - // just let the caller callServerEndpoint instance continue - return Promise.resolve(null); - } - if (!loggedIn) { - // We don't want to attempt any use native credentials of a logged out - // user to log-in after a cookieInvalidation while logged out - return Promise.resolve(null); - } - if (currentlyWaitingForNewCookie) { - return new Promise(r => - serverEndpointCallsWaitingForNewCookie.push(r), - ); - } - currentlyWaitingForNewCookie = true; - return attemptToResolveInvalidation(sessionChange); - }; - - return ( - endpoint: Endpoint, - data: Object, - options?: ?CallServerEndpointOptions, - ) => - callServerEndpoint( - cookie, - boundSetNewSession, - waitIfCookieInvalidated, - cookieInvalidationRecovery, - urlPrefix, - sessionID, - isSocketConnected, - lastCommunicatedPlatformDetails, - socketAPIHandler, - endpoint, - data, - dispatch, - options, - loggedIn, - keyserverID, - ); -} - -export type ActionFunc = (callServerEndpoint: CallServerEndpoint) => F; export type BindServerCallsParams = { +dispatch: Dispatch, +cookie: ?string, +urlPrefix: string, +sessionID: ?string, +currentUserInfo: ?CurrentUserInfo, +isSocketConnected: boolean, +lastCommunicatedPlatformDetails: ?PlatformDetails, +keyserverID: string, }; -// All server calls needs to include some information from the Redux state -// (namely, the cookie). This information is used deep in the server call, -// at the point where callServerEndpoint is called. We don't want to bother -// propagating the cookie (and any future config info that callServerEndpoint -// needs) through to the server calls so they can pass it to callServerEndpoint. -// Instead, we "curry" the cookie onto callServerEndpoint within react-redux's -// connect's mapStateToProps function, and then pass that "bound" -// callServerEndpoint that no longer needs the cookie as a parameter on to -// the server call. -const baseCreateBoundServerCallsSelector = ( - actionFunc: ActionFunc, -): (BindServerCallsParams => F) => - createSelector( - (state: BindServerCallsParams) => state.dispatch, - (state: BindServerCallsParams) => state.cookie, - (state: BindServerCallsParams) => state.urlPrefix, - (state: BindServerCallsParams) => state.sessionID, - (state: BindServerCallsParams) => state.currentUserInfo, - (state: BindServerCallsParams) => state.isSocketConnected, - (state: BindServerCallsParams) => state.lastCommunicatedPlatformDetails, - (state: BindServerCallsParams) => state.keyserverID, - ( - dispatch: Dispatch, - cookie: ?string, - urlPrefix: string, - sessionID: ?string, - currentUserInfo: ?CurrentUserInfo, - isSocketConnected: boolean, - lastCommunicatedPlatformDetails: ?PlatformDetails, - keyserverID: string, +type Props = { + +children: React.Node, +}; +function CallKeyserverEndpointProvider(props: Props): React.Node { + const bindCookieAndUtilsIntoCallServerEndpoint: ( + params: BindServerCallsParams, + ) => CallServerEndpoint = React.useCallback(params => { + const { + dispatch, + cookie, + urlPrefix, + sessionID, + currentUserInfo, + isSocketConnected, + lastCommunicatedPlatformDetails, + keyserverID, + } = params; + const loggedIn = !!(currentUserInfo && !currentUserInfo.anonymous && true); + const boundSetNewSession = ( + sessionChange: ClientSessionChange, + error: ?string, + ) => + setNewSession( + dispatch, + sessionChange, + { + currentUserInfo, + cookiesAndSessions: { [keyserverID]: { cookie, sessionID } }, + }, + error, + undefined, + keyserverID, + ); + const canResolveInvalidation = canResolveKeyserverSessionInvalidation(); + // This function gets called before callServerEndpoint sends a request, + // to make sure that we're not in the middle of trying to recover + // an invalidated cookie + const waitIfCookieInvalidated = () => { + if (!canResolveInvalidation) { + // If there is no way to resolve the session invalidation, + // just let the caller callServerEndpoint instance continue + return Promise.resolve(null); + } + if (!currentlyWaitingForNewCookie) { + // Our cookie seems to be valid + return Promise.resolve(null); + } + // Wait to run until we get our new cookie + return new Promise(r => + serverEndpointCallsWaitingForNewCookie.push(r), + ); + }; + // This function is a helper for the next function defined below + const attemptToResolveInvalidation = async ( + sessionChange: ClientSessionChange, ) => { - const boundCallServerEndpoint = bindCookieAndUtilsIntoCallServerEndpoint({ + const newAnonymousCookie = sessionChange.cookie; + const newSessionChange = await resolveKeyserverSessionInvalidation( dispatch, + newAnonymousCookie, + urlPrefix, + logInActionSources.cookieInvalidationResolutionAttempt, + keyserverID, + ); + + currentlyWaitingForNewCookie = false; + const currentWaitingCalls = serverEndpointCallsWaitingForNewCookie; + serverEndpointCallsWaitingForNewCookie = []; + + const newCallServerEndpoint = newSessionChange + ? bindCookieAndUtilsIntoCallServerEndpoint({ + ...params, + cookie: newSessionChange.cookie, + sessionID: newSessionChange.sessionID, + currentUserInfo: newSessionChange.currentUserInfo, + }) + : null; + for (const func of currentWaitingCalls) { + func(newCallServerEndpoint); + } + return newCallServerEndpoint; + }; + // If this function is called, callServerEndpoint got a response + // invalidating its cookie, and is wondering if it should just like... + // give up? Or if there's a chance at redemption + const cookieInvalidationRecovery = (sessionChange: ClientSessionChange) => { + if (!canResolveInvalidation) { + // If there is no way to resolve the session invalidation, + // just let the caller callServerEndpoint instance continue + return Promise.resolve(null); + } + if (!loggedIn) { + // We don't want to attempt any use native credentials of a logged out + // user to log-in after a cookieInvalidation while logged out + return Promise.resolve(null); + } + if (currentlyWaitingForNewCookie) { + return new Promise(r => + serverEndpointCallsWaitingForNewCookie.push(r), + ); + } + currentlyWaitingForNewCookie = true; + return attemptToResolveInvalidation(sessionChange); + }; + + return ( + endpoint: Endpoint, + data: Object, + options?: ?CallServerEndpointOptions, + ) => + callServerEndpoint( cookie, + boundSetNewSession, + waitIfCookieInvalidated, + cookieInvalidationRecovery, urlPrefix, sessionID, - currentUserInfo, isSocketConnected, lastCommunicatedPlatformDetails, + socketAPIHandler, + endpoint, + data, + dispatch, + options, + loggedIn, keyserverID, - }); - return actionFunc(boundCallServerEndpoint); - }, + ); + }, []); + + // All server calls needs to include some information from the Redux state + // (namely, the cookie). This information is used deep in the server call, + // at the point where callServerEndpoint is called. We don't want to bother + // propagating the cookie (and any future config info that callServerEndpoint + // needs) through to the server calls so they can pass it to + // callServerEndpoint. Instead, we "curry" the cookie onto callServerEndpoint + // within react-redux's connect's mapStateToProps function, and then pass that + // "bound" callServerEndpoint that no longer needs the cookie as a parameter + // on to the server call. + const baseCreateBoundServerCallsSelector = React.useCallback( + (actionFunc: ActionFunc): (BindServerCallsParams => F) => + createSelector( + (state: BindServerCallsParams) => state.dispatch, + (state: BindServerCallsParams) => state.cookie, + (state: BindServerCallsParams) => state.urlPrefix, + (state: BindServerCallsParams) => state.sessionID, + (state: BindServerCallsParams) => state.currentUserInfo, + (state: BindServerCallsParams) => state.isSocketConnected, + (state: BindServerCallsParams) => state.lastCommunicatedPlatformDetails, + (state: BindServerCallsParams) => state.keyserverID, + ( + dispatch: Dispatch, + cookie: ?string, + urlPrefix: string, + sessionID: ?string, + currentUserInfo: ?CurrentUserInfo, + isSocketConnected: boolean, + lastCommunicatedPlatformDetails: ?PlatformDetails, + keyserverID: string, + ) => { + const boundCallServerEndpoint = + bindCookieAndUtilsIntoCallServerEndpoint({ + dispatch, + cookie, + urlPrefix, + sessionID, + currentUserInfo, + isSocketConnected, + lastCommunicatedPlatformDetails, + keyserverID, + }); + return actionFunc(boundCallServerEndpoint); + }, + ), + [bindCookieAndUtilsIntoCallServerEndpoint], ); -type CreateBoundServerCallsSelectorType = ( - ActionFunc, -) => BindServerCallsParams => F; -const createBoundServerCallsSelector: CreateBoundServerCallsSelectorType = - (_memoize(baseCreateBoundServerCallsSelector): any); + const createBoundServerCallsSelector: CreateBoundServerCallsSelectorType = + React.useMemo( + () => _memoize(baseCreateBoundServerCallsSelector), + [baseCreateBoundServerCallsSelector], + ); + + const value = React.useMemo( + () => ({ + bindCookieAndUtilsIntoCallServerEndpoint, + createBoundServerCallsSelector, + }), + [bindCookieAndUtilsIntoCallServerEndpoint, createBoundServerCallsSelector], + ); + + return ( + + {props.children} + + ); +} + +function useCallKeyserverEndpointContext(): CallKeyserverEndpointContextType { + const callKeyserverEndpointContext = React.useContext( + CallKeyserverEndpointContext, + ); + invariant( + callKeyserverEndpointContext, + 'callKeyserverEndpointContext should be set', + ); + return callKeyserverEndpointContext; +} let socketAPIHandler: ?SocketAPIHandler = null; function registerActiveSocket(passedSocketAPIHandler: ?SocketAPIHandler) { socketAPIHandler = passedSocketAPIHandler; } export { - createBoundServerCallsSelector, + CallKeyserverEndpointProvider, + useCallKeyserverEndpointContext, registerActiveSocket, - bindCookieAndUtilsIntoCallServerEndpoint, }; diff --git a/lib/utils/action-utils.js b/lib/utils/action-utils.js index 1b7a370f0..8a985b3c9 100644 --- a/lib/utils/action-utils.js +++ b/lib/utils/action-utils.js @@ -1,43 +1,50 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { useSelector, useDispatch } from './redux-utils.js'; import { ashoatKeyserverID } from './validation-utils.js'; import { type ActionFunc, type BindServerCallsParams, - createBoundServerCallsSelector, + useCallKeyserverEndpointContext, } from '../keyserver-conn/call-keyserver-endpoint-provider.react.js'; import { serverCallStateSelector } from '../selectors/server-calls.js'; function useServerCall( serverCall: ActionFunc, paramOverride?: ?Partial, ): F { const dispatch = useDispatch(); const serverCallState = useSelector( serverCallStateSelector(ashoatKeyserverID), ); + const { createBoundServerCallsSelector } = useCallKeyserverEndpointContext(); return React.useMemo(() => { const { urlPrefix, isSocketConnected } = serverCallState; invariant( !!urlPrefix && isSocketConnected !== undefined && isSocketConnected !== null, 'keyserver missing from keyserverStore', ); return createBoundServerCallsSelector(serverCall)({ ...serverCallState, urlPrefix, isSocketConnected, dispatch, ...paramOverride, keyserverID: ashoatKeyserverID, }); - }, [serverCall, serverCallState, dispatch, paramOverride]); + }, [ + serverCall, + serverCallState, + dispatch, + paramOverride, + createBoundServerCallsSelector, + ]); } export { useServerCall }; diff --git a/lib/utils/keyserver-call.js b/lib/utils/keyserver-call.js index 044530888..6137a83db 100644 --- a/lib/utils/keyserver-call.js +++ b/lib/utils/keyserver-call.js @@ -1,230 +1,244 @@ // @flow import _memoize from 'lodash/memoize.js'; import * as React from 'react'; import { createSelector } from 'reselect'; import type { CallServerEndpoint, CallServerEndpointOptions, } from './call-server-endpoint.js'; import { promiseAll } from './promises.js'; import { useSelector, useDispatch } from './redux-utils.js'; import { useDerivedObject } from '../hooks/objects.js'; import { - bindCookieAndUtilsIntoCallServerEndpoint, + useCallKeyserverEndpointContext, type BindServerCallsParams, } from '../keyserver-conn/call-keyserver-endpoint-provider.react.js'; import type { PlatformDetails } from '../types/device-types.js'; import type { Endpoint } from '../types/endpoints.js'; import type { KeyserverInfo } from '../types/keyserver-types.js'; import type { Dispatch } from '../types/redux-types.js'; import type { CurrentUserInfo } from '../types/user-types.js'; export type CallKeyserverEndpoint = ( endpoint: Endpoint, requests: { +[keyserverID: string]: ?{ +[string]: mixed } }, options?: ?CallServerEndpointOptions, ) => Promise<{ +[keyserverID: string]: any }>; export type ActionFunc = ( callServerEndpoint: CallKeyserverEndpoint, // The second argument is only used in actions that call all keyservers, // and the request to all keyservers are exactly the same. // An example of such action is fetchEntries. allKeyserverIDs: $ReadOnlyArray, ) => Args => Promise; // _memoize memoizes the function by caching the result. // The first argument of the memoized function is used as the map cache key. const baseCreateBoundServerCallsSelector = ( keyserverID: string, + bindCookieAndUtilsIntoCallServerEndpoint: ( + params: BindServerCallsParams, + ) => CallServerEndpoint, ): (BindServerCallsParams => CallServerEndpoint) => createSelector( (state: BindServerCallsParams) => state.dispatch, (state: BindServerCallsParams) => state.cookie, (state: BindServerCallsParams) => state.urlPrefix, (state: BindServerCallsParams) => state.sessionID, (state: BindServerCallsParams) => state.currentUserInfo, (state: BindServerCallsParams) => state.isSocketConnected, (state: BindServerCallsParams) => state.lastCommunicatedPlatformDetails, ( dispatch: Dispatch, cookie: ?string, urlPrefix: string, sessionID: ?string, currentUserInfo: ?CurrentUserInfo, isSocketConnected: boolean, lastCommunicatedPlatformDetails: ?PlatformDetails, ) => bindCookieAndUtilsIntoCallServerEndpoint({ dispatch, cookie, urlPrefix, sessionID, currentUserInfo, isSocketConnected, lastCommunicatedPlatformDetails, keyserverID, }), ); type CreateBoundServerCallsSelectorType = ( keyserverID: string, + bindCookieAndUtilsIntoCallServerEndpoint: ( + params: BindServerCallsParams, + ) => CallServerEndpoint, ) => BindServerCallsParams => CallServerEndpoint; const createBoundServerCallsSelector: CreateBoundServerCallsSelectorType = _memoize(baseCreateBoundServerCallsSelector); type KeyserverInfoPartial = $ReadOnly<{ ...Partial, +urlPrefix: $PropertyType, }>; type KeyserverCallInfo = { +cookie: ?string, +urlPrefix: string, +sessionID: ?string, +isSocketConnected: boolean, +lastCommunicatedPlatformDetails: ?PlatformDetails, }; const createKeyserverCallSelector: () => KeyserverInfoPartial => KeyserverCallInfo = () => createSelector( (keyserverInfo: KeyserverInfoPartial) => keyserverInfo.cookie, (keyserverInfo: KeyserverInfoPartial) => keyserverInfo.urlPrefix, (keyserverInfo: KeyserverInfoPartial) => keyserverInfo.sessionID, (keyserverInfo: KeyserverInfoPartial) => keyserverInfo.connection?.status === 'connected', (keyserverInfo: KeyserverInfoPartial) => keyserverInfo.lastCommunicatedPlatformDetails, ( cookie: ?string, urlPrefix: string, sessionID: ?string, isSocketConnected: boolean, lastCommunicatedPlatformDetails: ?PlatformDetails, ) => ({ cookie, urlPrefix, sessionID, isSocketConnected, lastCommunicatedPlatformDetails, }), ); function useKeyserverCallInfos(keyserverInfos: { +[keyserverID: string]: KeyserverInfoPartial, }): { +[keyserverID: string]: KeyserverCallInfo } { return useDerivedObject( keyserverInfos, createKeyserverCallSelector, ); } type BindCallKeyserverSelector = ( keyserverCall: ActionFunc, ) => Args => Promise; function useBindCallKeyserverEndpointSelector( dispatch: Dispatch, currentUserInfo: ?CurrentUserInfo, keyserverCallInfos: { +[keyserverID: string]: KeyserverCallInfo }, ): BindCallKeyserverSelector { + const { bindCookieAndUtilsIntoCallServerEndpoint } = + useCallKeyserverEndpointContext(); return React.useMemo( () => _memoize( ( keyserverCall: ActionFunc, ): (Args => Promise) => { const callKeyserverEndpoint = ( endpoint: Endpoint, requests: { +[keyserverID: string]: ?{ +[string]: mixed } }, options?: ?CallServerEndpointOptions, ) => { const bindCallKeyserverEndpoint = (keyserverID: string) => { const { cookie, urlPrefix, sessionID, isSocketConnected, lastCommunicatedPlatformDetails, } = keyserverCallInfos[keyserverID]; const boundCallServerEndpoint = createBoundServerCallsSelector( keyserverID, + bindCookieAndUtilsIntoCallServerEndpoint, )({ dispatch, currentUserInfo, cookie, urlPrefix, sessionID, isSocketConnected, lastCommunicatedPlatformDetails, keyserverID, }); return boundCallServerEndpoint( endpoint, requests[keyserverID], options, ); }; const promises: { [string]: Promise } = {}; for (const keyserverID in requests) { promises[keyserverID] = bindCallKeyserverEndpoint(keyserverID); } return promiseAll(promises); }; const keyserverIDs = Object.keys(keyserverCallInfos); return keyserverCall(callKeyserverEndpoint, keyserverIDs); }, ), - [dispatch, currentUserInfo, keyserverCallInfos], + [ + dispatch, + currentUserInfo, + keyserverCallInfos, + bindCookieAndUtilsIntoCallServerEndpoint, + ], ); } export type KeyserverCallParamOverride = Partial<{ +dispatch: Dispatch, +currentUserInfo: ?CurrentUserInfo, +keyserverInfos: { +[keyserverID: string]: KeyserverInfoPartial }, }>; function useKeyserverCall( keyserverCall: ActionFunc, paramOverride?: ?KeyserverCallParamOverride, ): Args => Promise { const baseDispatch = useDispatch(); const baseCurrentUserInfo = useSelector(state => state.currentUserInfo); const keyserverInfos = useSelector( state => state.keyserverStore.keyserverInfos, ); const baseCombinedInfo = { dispatch: baseDispatch, currentUserInfo: baseCurrentUserInfo, keyserverInfos, ...paramOverride, }; const { dispatch, currentUserInfo, keyserverInfos: keyserverInfoPartials, } = baseCombinedInfo; const keyserverCallInfos = useKeyserverCallInfos(keyserverInfoPartials); const bindCallKeyserverEndpointToAction = useBindCallKeyserverEndpointSelector( dispatch, currentUserInfo, keyserverCallInfos, ); return React.useMemo( () => bindCallKeyserverEndpointToAction(keyserverCall), [bindCallKeyserverEndpointToAction, keyserverCall], ); } export { useKeyserverCall }; diff --git a/native/root.react.js b/native/root.react.js index 814acb958..c545d6ad9 100644 --- a/native/root.react.js +++ b/native/root.react.js @@ -1,377 +1,380 @@ // @flow import { ActionSheetProvider } from '@expo/react-native-action-sheet'; import { BottomSheetModalProvider } from '@gorhom/bottom-sheet'; import AsyncStorage from '@react-native-async-storage/async-storage'; import type { PossiblyStaleNavigationState, UnsafeContainerActionEvent, GenericNavigationAction, } from '@react-navigation/core'; import { useReduxDevToolsExtension } from '@react-navigation/devtools'; import { NavigationContainer } from '@react-navigation/native'; import * as SplashScreen from 'expo-splash-screen'; import invariant from 'invariant'; import * as React from 'react'; import { Platform, UIManager, StyleSheet } from 'react-native'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import Orientation from 'react-native-orientation-locker'; import { SafeAreaProvider, initialWindowMetrics, } from 'react-native-safe-area-context'; import { Provider } from 'react-redux'; import { PersistGate as ReduxPersistGate } from 'redux-persist/es/integration/react.js'; import { ChatMentionContextProvider } from 'lib/components/chat-mention-provider.react.js'; import { EditUserAvatarProvider } from 'lib/components/edit-user-avatar-provider.react.js'; import { ENSCacheProvider } from 'lib/components/ens-cache-provider.react.js'; import IntegrityHandler from 'lib/components/integrity-handler.react.js'; import KeyserverConnectionsHandler from 'lib/components/keyserver-connections-handler.js'; import { MediaCacheProvider } from 'lib/components/media-cache-provider.react.js'; +import { CallKeyserverEndpointProvider } from 'lib/keyserver-conn/call-keyserver-endpoint-provider.react.js'; import { TunnelbrokerProvider } from 'lib/tunnelbroker/tunnelbroker-context.js'; import { actionLogger } from 'lib/utils/action-logger.js'; import { RegistrationContextProvider } from './account/registration/registration-context-provider.react.js'; import NativeEditThreadAvatarProvider from './avatars/native-edit-thread-avatar-provider.react.js'; import BackupHandler from './backup/backup-handler.js'; import { BottomSheetProvider } from './bottom-sheet/bottom-sheet-provider.react.js'; import ChatContextProvider from './chat/chat-context-provider.react.js'; import MessageEditingContextProvider from './chat/message-editing-context-provider.react.js'; import { FeatureFlagsProvider } from './components/feature-flags-provider.react.js'; import PersistedStateGate from './components/persisted-state-gate.js'; import VersionSupportedChecker from './components/version-supported.react.js'; import ConnectedStatusBar from './connected-status-bar.react.js'; import { SQLiteDataHandler } from './data/sqlite-data-handler.js'; import ErrorBoundary from './error-boundary.react.js'; import { peerToPeerMessageHandler } from './handlers/peer-to-peer-message-handler.js'; import IdentityServiceContextProvider from './identity-service/identity-service-context-provider.react.js'; import InputStateContainer from './input/input-state-container.react.js'; import LifecycleHandler from './lifecycle/lifecycle-handler.react.js'; import MarkdownContextProvider from './markdown/markdown-context-provider.react.js'; import { filesystemMediaCache } from './media/media-cache.js'; import { DeepLinksContextProvider } from './navigation/deep-links-context-provider.react.js'; import { defaultNavigationState } from './navigation/default-state.js'; import DisconnectedBarVisibilityHandler from './navigation/disconnected-bar-visibility-handler.react.js'; import { setGlobalNavContext } from './navigation/icky-global.js'; import { NavContext, type NavContextType, } from './navigation/navigation-context.js'; import NavigationHandler from './navigation/navigation-handler.react.js'; import { validNavState } from './navigation/navigation-utils.js'; import OrientationHandler from './navigation/orientation-handler.react.js'; import { navStateAsyncStorageKey } from './navigation/persistance.js'; import RootNavigator from './navigation/root-navigator.react.js'; import ConnectivityUpdater from './redux/connectivity-updater.react.js'; import { DimensionsUpdater } from './redux/dimensions-updater.react.js'; import { getPersistor } from './redux/persist.js'; import { store } from './redux/redux-setup.js'; import { useSelector } from './redux/redux-utils.js'; import { RootContext } from './root-context.js'; import { MessageSearchProvider } from './search/search-provider.react.js'; import Socket from './socket.react.js'; import { StaffContextProvider } from './staff/staff-context.provider.react.js'; import { useLoadCommFonts } from './themes/fonts.js'; import { DarkTheme, LightTheme } from './themes/navigation.js'; import ThemeHandler from './themes/theme-handler.react.js'; import { provider } from './utils/ethers-utils.js'; import { useTunnelbrokerInitMessage } from './utils/tunnelbroker-utils.js'; // Add custom items to expo-dev-menu import './dev-menu.js'; import './types/message-types-validator.js'; if (Platform.OS === 'android') { UIManager.setLayoutAnimationEnabledExperimental && UIManager.setLayoutAnimationEnabledExperimental(true); } const navInitAction = Object.freeze({ type: 'NAV/@@INIT' }); const navUnknownAction = Object.freeze({ type: 'NAV/@@UNKNOWN' }); SplashScreen.preventAutoHideAsync().catch(console.log); function Root() { const navStateRef = React.useRef(); const navDispatchRef = React.useRef GenericNavigationAction), ) => void>(); const navStateInitializedRef = React.useRef(false); // We call this here to start the loading process // We gate the UI on the fonts loading in AppNavigator useLoadCommFonts(); const [navContext, setNavContext] = React.useState(null); const updateNavContext = React.useCallback(() => { if ( !navStateRef.current || !navDispatchRef.current || !navStateInitializedRef.current ) { return; } const updatedNavContext = { state: navStateRef.current, dispatch: navDispatchRef.current, }; setNavContext(updatedNavContext); setGlobalNavContext(updatedNavContext); }, []); const [initialState, setInitialState] = React.useState( __DEV__ ? undefined : defaultNavigationState, ); React.useEffect(() => { Orientation.lockToPortrait(); void (async () => { let loadedState = initialState; if (__DEV__) { try { const navStateString = await AsyncStorage.getItem( navStateAsyncStorageKey, ); if (navStateString) { const savedState = JSON.parse(navStateString); if (validNavState(savedState)) { loadedState = savedState; } } } catch {} } if (!loadedState) { loadedState = defaultNavigationState; } if (loadedState !== initialState) { setInitialState(loadedState); } navStateRef.current = loadedState; updateNavContext(); actionLogger.addOtherAction('navState', navInitAction, null, loadedState); })(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [updateNavContext]); const setNavStateInitialized = React.useCallback(() => { navStateInitializedRef.current = true; updateNavContext(); }, [updateNavContext]); const [rootContext, setRootContext] = React.useState(() => ({ setNavStateInitialized, })); const detectUnsupervisedBackgroundRef = React.useCallback( (detectUnsupervisedBackground: ?(alreadyClosed: boolean) => boolean) => { setRootContext(prevRootContext => ({ ...prevRootContext, detectUnsupervisedBackground, })); }, [], ); const frozen = useSelector(state => state.frozen); const queuedActionsRef = React.useRef>([]); const onNavigationStateChange = React.useCallback( (state: ?PossiblyStaleNavigationState) => { invariant(state, 'nav state should be non-null'); const prevState = navStateRef.current; navStateRef.current = state; updateNavContext(); const queuedActions = queuedActionsRef.current; queuedActionsRef.current = []; if (queuedActions.length === 0) { queuedActions.push(navUnknownAction); } for (const action of queuedActions) { actionLogger.addOtherAction('navState', action, prevState, state); } if (!__DEV__ || frozen) { return; } void (async () => { try { await AsyncStorage.setItem( navStateAsyncStorageKey, JSON.stringify(state), ); } catch (e) { console.log('AsyncStorage threw while trying to persist navState', e); } })(); }, [updateNavContext, frozen], ); const navContainerRef = React.useRef>(); const containerRef = React.useCallback( (navContainer: ?React.ElementRef) => { navContainerRef.current = navContainer; if (navContainer && !navDispatchRef.current) { navDispatchRef.current = navContainer.dispatch; updateNavContext(); } }, [updateNavContext], ); useReduxDevToolsExtension(navContainerRef); const navContainer = navContainerRef.current; React.useEffect(() => { if (!navContainer) { return undefined; } return navContainer.addListener( '__unsafe_action__', (event: { +data: UnsafeContainerActionEvent, ... }) => { const { action, noop } = event.data; const navState = navStateRef.current; if (noop) { actionLogger.addOtherAction('navState', action, navState, navState); return; } queuedActionsRef.current.push({ ...action, type: `NAV/${action.type}`, }); }, ); }, [navContainer]); const activeTheme = useSelector(state => state.globalThemeInfo.activeTheme); const theme = (() => { if (activeTheme === 'light') { return LightTheme; } else if (activeTheme === 'dark') { return DarkTheme; } return undefined; })(); const tunnelbrokerInitMessage = useTunnelbrokerInitMessage(); const gated: React.Node = ( <> ); let navigation; if (initialState) { navigation = ( ); } return ( - - - - - - - - - - - - - - - - - - - - - - {gated} - - - - - - {navigation} - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + {gated} + + + + + + {navigation} + + + + + + + + + + + + + + + + + + + ); } const styles = StyleSheet.create({ app: { flex: 1, }, }); function AppRoot(): React.Node { return ( ); } export default AppRoot; diff --git a/web/root.js b/web/root.js index a7b049105..ccec3fd44 100644 --- a/web/root.js +++ b/web/root.js @@ -1,63 +1,66 @@ // @flow import localforage from 'localforage'; import * as React from 'react'; import { Provider } from 'react-redux'; import { Router, Route } from 'react-router'; import { createStore, applyMiddleware, type Store } from 'redux'; import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProduction.js'; import { persistReducer, persistStore } from 'redux-persist'; import thunk from 'redux-thunk'; import IntegrityHandler from 'lib/components/integrity-handler.react.js'; import KeyserverConnectionsHandler from 'lib/components/keyserver-connections-handler.js'; +import { CallKeyserverEndpointProvider } from 'lib/keyserver-conn/call-keyserver-endpoint-provider.react.js'; import { reduxLoggerMiddleware } from 'lib/utils/action-logger.js'; import { GetOrCreateCryptoStoreProvider, WebNotificationsSessionCreatorProvider, } from './account/account-hooks.js'; import App from './app.react.js'; import { SQLiteDataHandler } from './database/sqlite-data-handler.js'; import { localforageConfig } from './database/utils/constants.js'; import ErrorBoundary from './error-boundary.react.js'; import IdentityServiceContextProvider from './grpc/identity-service-context-provider.react.js'; import { defaultWebState } from './redux/default-state.js'; import InitialReduxStateGate from './redux/initial-state-gate.js'; import { persistConfig } from './redux/persist.js'; import { type AppState, type Action, reducer } from './redux/redux-setup.js'; import history from './router-history.js'; import Socket from './socket.react.js'; localforage.config(localforageConfig); const persistedReducer = persistReducer(persistConfig, reducer); const store: Store = createStore( persistedReducer, defaultWebState, composeWithDevTools({})(applyMiddleware(thunk, reduxLoggerMiddleware)), ); const persistor = persistStore(store); const RootProvider = (): React.Node => ( - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + ); export default RootProvider;