diff --git a/native/account/registration/avatar-selection.react.js b/native/account/registration/avatar-selection.react.js index 147081ded..f0bde1c3c 100644 --- a/native/account/registration/avatar-selection.react.js +++ b/native/account/registration/avatar-selection.react.js @@ -1,199 +1,202 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { Text, View } from 'react-native'; import RegistrationButtonContainer from './registration-button-container.react.js'; import RegistrationButton from './registration-button.react.js'; import RegistrationContainer from './registration-container.react.js'; import RegistrationContentContainer from './registration-content-container.react.js'; import { RegistrationContext } from './registration-context.js'; import type { RegistrationNavigationProp } from './registration-navigator.react.js'; import { type CoolOrNerdMode, type AccountSelection, type AvatarData, ensAvatarSelection, } from './registration-types.js'; import { EditUserAvatarContext, type UserAvatarSelection, } from '../../avatars/edit-user-avatar-provider.react.js'; import EditUserAvatar from '../../avatars/edit-user-avatar.react.js'; -import type { NavigationRoute } from '../../navigation/route-names.js'; +import { useCurrentLeafRouteName } from '../../navigation/nav-selectors.js'; +import { + type NavigationRoute, + RegistrationTermsRouteName, + AvatarSelectionRouteName, + EmojiAvatarSelectionRouteName, + RegistrationUserAvatarCameraModalRouteName, +} from '../../navigation/route-names.js'; import { useStyles } from '../../themes/colors.js'; export type AvatarSelectionParams = { +userSelections: { +coolOrNerdMode: CoolOrNerdMode, +keyserverUsername: string, +accountSelection: AccountSelection, }, }; type Props = { +navigation: RegistrationNavigationProp<'AvatarSelection'>, +route: NavigationRoute<'AvatarSelection'>, }; function AvatarSelection(props: Props): React.Node { const { userSelections } = props.route.params; const { accountSelection } = userSelections; const usernameOrETHAddress = accountSelection.accountType === 'username' ? accountSelection.username : accountSelection.address; const registrationContext = React.useContext(RegistrationContext); invariant(registrationContext, 'registrationContext should be set'); - const { cachedSelections, setCachedSelections, register } = - registrationContext; + const { cachedSelections, setCachedSelections } = registrationContext; const editUserAvatarContext = React.useContext(EditUserAvatarContext); invariant(editUserAvatarContext, 'editUserAvatarContext should be set'); const { setRegistrationMode } = editUserAvatarContext; const prefetchedAvatarURI = accountSelection.accountType === 'ethereum' ? accountSelection.avatarURI : undefined; let initialAvatarData = cachedSelections.avatarData; if (!initialAvatarData && prefetchedAvatarURI) { initialAvatarData = ensAvatarSelection; } const [avatarData, setAvatarData] = React.useState(initialAvatarData); const setClientAvatarFromSelection = React.useCallback( (selection: UserAvatarSelection) => { if (selection.needsUpload) { const newAvatarData = { ...selection, clientAvatar: { type: 'image', uri: selection.mediaSelection.uri, }, }; setAvatarData(newAvatarData); setCachedSelections(oldUserSelections => ({ ...oldUserSelections, avatarData: newAvatarData, })); } else if (selection.updateUserAvatarRequest.type !== 'remove') { const clientRequest = selection.updateUserAvatarRequest; invariant( clientRequest.type !== 'image', 'image avatars need to be uploaded', ); const newAvatarData = { ...selection, clientAvatar: clientRequest, }; setAvatarData(newAvatarData); setCachedSelections(oldUserSelections => ({ ...oldUserSelections, avatarData: newAvatarData, })); } else { setAvatarData(undefined); setCachedSelections(oldUserSelections => ({ ...oldUserSelections, avatarData: undefined, })); } }, [setCachedSelections], ); - const [registrationInProgress, setRegistrationInProgress] = - React.useState(false); - + const currentRouteName = useCurrentLeafRouteName(); + const avatarSelectionHappening = + currentRouteName === AvatarSelectionRouteName || + currentRouteName === EmojiAvatarSelectionRouteName || + currentRouteName === RegistrationUserAvatarCameraModalRouteName; React.useEffect(() => { - if (registrationInProgress) { + if (!avatarSelectionHappening) { return undefined; } setRegistrationMode({ registrationMode: 'on', successCallback: setClientAvatarFromSelection, }); return () => { setRegistrationMode({ registrationMode: 'off' }); }; }, [ - registrationInProgress, + avatarSelectionHappening, setRegistrationMode, setClientAvatarFromSelection, ]); + const { navigate } = props.navigation; const onProceed = React.useCallback(async () => { - setRegistrationInProgress(true); - try { - await register({ - ...userSelections, - avatarData, - }); - } finally { - setRegistrationInProgress(false); - } - }, [register, userSelections, avatarData]); + const newUserSelections = { + ...userSelections, + avatarData, + }; + navigate<'RegistrationTerms'>({ + name: RegistrationTermsRouteName, + params: { userSelections: newUserSelections }, + }); + }, [userSelections, avatarData, navigate]); const clientAvatar = avatarData?.clientAvatar; const userInfoOverride = React.useMemo( () => ({ username: usernameOrETHAddress, avatar: clientAvatar, }), [usernameOrETHAddress, clientAvatar], ); const styles = useStyles(unboundStyles); return ( Pick an avatar - + ); } const unboundStyles = { scrollViewContentContainer: { paddingHorizontal: 0, }, header: { fontSize: 24, color: 'panelForegroundLabel', paddingBottom: 16, paddingHorizontal: 16, }, stagedAvatarSection: { marginTop: 16, backgroundColor: 'panelForeground', paddingVertical: 24, alignItems: 'center', }, editUserAvatar: { alignItems: 'center', justifyContent: 'center', }, }; export default AvatarSelection; diff --git a/native/account/registration/registration-terms.react.js b/native/account/registration/registration-terms.react.js index 6889b9716..ed1dedb06 100644 --- a/native/account/registration/registration-terms.react.js +++ b/native/account/registration/registration-terms.react.js @@ -1,61 +1,77 @@ // @flow +import invariant from 'invariant'; import * as React from 'react'; import { Text } from 'react-native'; import RegistrationButtonContainer from './registration-button-container.react.js'; import RegistrationButton from './registration-button.react.js'; import RegistrationContainer from './registration-container.react.js'; import RegistrationContentContainer from './registration-content-container.react.js'; +import { RegistrationContext } from './registration-context.js'; import type { RegistrationNavigationProp } from './registration-navigator.react.js'; import type { CoolOrNerdMode, AccountSelection, AvatarData, } from './registration-types.js'; import type { NavigationRoute } from '../../navigation/route-names.js'; import { useStyles } from '../../themes/colors.js'; export type RegistrationTermsParams = { +userSelections: { +coolOrNerdMode: CoolOrNerdMode, +keyserverUsername: string, +accountSelection: AccountSelection, +avatarData: ?AvatarData, }, }; type Props = { +navigation: RegistrationNavigationProp<'RegistrationTerms'>, +route: NavigationRoute<'RegistrationTerms'>, }; -// eslint-disable-next-line no-unused-vars function RegistrationTerms(props: Props): React.Node { - const onProceed = React.useCallback(() => {}, []); + const registrationContext = React.useContext(RegistrationContext); + invariant(registrationContext, 'registrationContext should be set'); + const { register } = registrationContext; + + const [registrationInProgress, setRegistrationInProgress] = + React.useState(false); + + const { userSelections } = props.route.params; + const onProceed = React.useCallback(async () => { + setRegistrationInProgress(true); + try { + await register(userSelections); + } finally { + setRegistrationInProgress(false); + } + }, [register, userSelections]); const styles = useStyles(unboundStyles); return ( Finish registration ); } const unboundStyles = { header: { fontSize: 24, color: 'panelForegroundLabel', paddingBottom: 16, }, }; export default RegistrationTerms; diff --git a/native/navigation/nav-selectors.js b/native/navigation/nav-selectors.js index 530c7a2f7..271f57da6 100644 --- a/native/navigation/nav-selectors.js +++ b/native/navigation/nav-selectors.js @@ -1,333 +1,345 @@ // @flow import type { PossiblyStaleNavigationState } from '@react-navigation/native'; import _memoize from 'lodash/memoize.js'; import * as React from 'react'; import { createSelector } from 'reselect'; import { nonThreadCalendarFiltersSelector } from 'lib/selectors/calendar-filter-selectors.js'; import { currentCalendarQuery } from 'lib/selectors/nav-selectors.js'; import type { CalendarQuery } from 'lib/types/entry-types.js'; import type { CalendarFilter } from 'lib/types/filter-types.js'; import type { NavContextType } from './navigation-context.js'; import { NavContext } from './navigation-context.js'; import { getStateFromNavigatorRoute, getThreadIDFromRoute, + currentLeafRoute, } from './navigation-utils.js'; import { AppRouteName, TabNavigatorRouteName, MessageListRouteName, ChatRouteName, CalendarRouteName, ThreadPickerModalRouteName, ActionResultModalRouteName, accountModals, scrollBlockingModals, chatRootModals, threadRoutes, CommunityDrawerNavigatorRouteName, } from './route-names.js'; import { useSelector } from '../redux/redux-utils.js'; import type { NavPlusRedux } from '../types/selector-types.js'; import type { GlobalTheme } from '../types/themes.js'; const baseCreateIsForegroundSelector = (routeName: string) => createSelector( (context: ?NavContextType) => context && context.state, (navigationState: ?PossiblyStaleNavigationState) => { if (!navigationState) { return false; } return navigationState.routes[navigationState.index].name === routeName; }, ); const createIsForegroundSelector: ( routeName: string, ) => (context: ?NavContextType) => boolean = _memoize( baseCreateIsForegroundSelector, ); function useIsAppLoggedIn(): boolean { const navContext = React.useContext(NavContext); return React.useMemo(() => { if (!navContext) { return false; } const { state } = navContext; return !accountModals.includes(state.routes[state.index].name); }, [navContext]); } const baseCreateActiveTabSelector = (routeName: string) => createSelector( (context: ?NavContextType) => context && context.state, (navigationState: ?PossiblyStaleNavigationState) => { if (!navigationState) { return false; } const currentRootSubroute = navigationState.routes[navigationState.index]; if (currentRootSubroute.name !== AppRouteName) { return false; } const appState = getStateFromNavigatorRoute(currentRootSubroute); const [firstAppSubroute] = appState.routes; if (firstAppSubroute.name !== CommunityDrawerNavigatorRouteName) { return false; } const communityDrawerState = getStateFromNavigatorRoute(firstAppSubroute); const [firstCommunityDrawerSubroute] = communityDrawerState.routes; if (firstCommunityDrawerSubroute.name !== TabNavigatorRouteName) { return false; } const tabState = getStateFromNavigatorRoute(firstCommunityDrawerSubroute); return tabState.routes[tabState.index].name === routeName; }, ); const createActiveTabSelector: ( routeName: string, ) => (context: ?NavContextType) => boolean = _memoize( baseCreateActiveTabSelector, ); const scrollBlockingModalsClosedSelector: ( context: ?NavContextType, ) => boolean = createSelector( (context: ?NavContextType) => context && context.state, (navigationState: ?PossiblyStaleNavigationState) => { if (!navigationState) { return false; } const currentRootSubroute = navigationState.routes[navigationState.index]; if (currentRootSubroute.name !== AppRouteName) { return true; } const appState = getStateFromNavigatorRoute(currentRootSubroute); for (let i = appState.index; i >= 0; i--) { const route = appState.routes[i]; if (scrollBlockingModals.includes(route.name)) { return false; } } return true; }, ); function selectBackgroundIsDark( navigationState: ?PossiblyStaleNavigationState, theme: ?GlobalTheme, ): boolean { if (!navigationState) { return false; } const currentRootSubroute = navigationState.routes[navigationState.index]; if (currentRootSubroute.name !== AppRouteName) { // Very bright... we'll call it non-dark. Doesn't matter right now since // we only use this selector for determining ActionResultModal appearance return false; } const appState = getStateFromNavigatorRoute(currentRootSubroute); let appIndex = appState.index; let currentAppSubroute = appState.routes[appIndex]; while (currentAppSubroute.name === ActionResultModalRouteName) { currentAppSubroute = appState.routes[--appIndex]; } if (scrollBlockingModals.includes(currentAppSubroute.name)) { // All the scroll-blocking chat modals have a dark background return true; } return theme === 'dark'; } function activeThread( navigationState: ?PossiblyStaleNavigationState, validRouteNames: $ReadOnlyArray, ): ?string { if (!navigationState) { return null; } let rootIndex = navigationState.index; let currentRootSubroute = navigationState.routes[rootIndex]; while (currentRootSubroute.name !== AppRouteName) { if (!chatRootModals.includes(currentRootSubroute.name)) { return null; } if (rootIndex === 0) { return null; } currentRootSubroute = navigationState.routes[--rootIndex]; } const appState = getStateFromNavigatorRoute(currentRootSubroute); const [firstAppSubroute] = appState.routes; if (firstAppSubroute.name !== CommunityDrawerNavigatorRouteName) { return null; } const communityDrawerState = getStateFromNavigatorRoute(firstAppSubroute); const [firstCommunityDrawerSubroute] = communityDrawerState.routes; if (firstCommunityDrawerSubroute.name !== TabNavigatorRouteName) { return null; } const tabState = getStateFromNavigatorRoute(firstCommunityDrawerSubroute); const currentTabSubroute = tabState.routes[tabState.index]; if (currentTabSubroute.name !== ChatRouteName) { return null; } const chatState = getStateFromNavigatorRoute(currentTabSubroute); const currentChatSubroute = chatState.routes[chatState.index]; return getThreadIDFromRoute(currentChatSubroute, validRouteNames); } const activeThreadSelector: (context: ?NavContextType) => ?string = createSelector( (context: ?NavContextType) => context && context.state, (navigationState: ?PossiblyStaleNavigationState): ?string => activeThread(navigationState, threadRoutes), ); const messageListRouteNames = [MessageListRouteName]; const activeMessageListSelector: (context: ?NavContextType) => ?string = createSelector( (context: ?NavContextType) => context && context.state, (navigationState: ?PossiblyStaleNavigationState): ?string => activeThread(navigationState, messageListRouteNames), ); function useActiveThread(): ?string { const navContext = React.useContext(NavContext); return React.useMemo(() => { if (!navContext) { return null; } const { state } = navContext; return activeThread(state, threadRoutes); }, [navContext]); } function useActiveMessageList(): ?string { const navContext = React.useContext(NavContext); return React.useMemo(() => { if (!navContext) { return null; } const { state } = navContext; return activeThread(state, messageListRouteNames); }, [navContext]); } const calendarTabActiveSelector = createActiveTabSelector(CalendarRouteName); const threadPickerActiveSelector = createIsForegroundSelector( ThreadPickerModalRouteName, ); const calendarActiveSelector: (context: ?NavContextType) => boolean = createSelector( calendarTabActiveSelector, threadPickerActiveSelector, (calendarTabActive: boolean, threadPickerActive: boolean) => calendarTabActive || threadPickerActive, ); const nativeCalendarQuery: (input: NavPlusRedux) => () => CalendarQuery = createSelector( (input: NavPlusRedux) => currentCalendarQuery(input.redux), (input: NavPlusRedux) => calendarActiveSelector(input.navContext), ( calendarQuery: (calendarActive: boolean) => CalendarQuery, calendarActive: boolean, ) => () => calendarQuery(calendarActive), ); const nonThreadCalendarQuery: (input: NavPlusRedux) => () => CalendarQuery = createSelector( nativeCalendarQuery, (input: NavPlusRedux) => nonThreadCalendarFiltersSelector(input.redux), ( calendarQuery: () => CalendarQuery, filters: $ReadOnlyArray, ) => { return (): CalendarQuery => { const query = calendarQuery(); return { startDate: query.startDate, endDate: query.endDate, filters, }; }; }, ); function useCalendarQuery(): () => CalendarQuery { const navContext = React.useContext(NavContext); return useSelector(state => nonThreadCalendarQuery({ redux: state, navContext, }), ); } const drawerSwipeEnabledSelector: (context: ?NavContextType) => boolean = createSelector( (context: ?NavContextType) => context && context.state, (navigationState: ?PossiblyStaleNavigationState) => { if (!navigationState) { return true; } // First, we recurse into the navigation state until we find the tab route // The tab route should always be accessible by recursing through the // first routes of each subsequent nested navigation state const [firstRootSubroute] = navigationState.routes; if (firstRootSubroute.name !== AppRouteName) { return true; } const appState = getStateFromNavigatorRoute(firstRootSubroute); const [firstAppSubroute] = appState.routes; if (firstAppSubroute.name !== CommunityDrawerNavigatorRouteName) { return true; } const communityDrawerState = getStateFromNavigatorRoute(firstAppSubroute); const [firstCommunityDrawerSubroute] = communityDrawerState.routes; if (firstCommunityDrawerSubroute.name !== TabNavigatorRouteName) { return true; } const tabState = getStateFromNavigatorRoute(firstCommunityDrawerSubroute); // Once we have the tab state, we want to figure out if we currently have // an active StackNavigator const currentTabSubroute = tabState.routes[tabState.index]; if (!currentTabSubroute.state) { return true; } const currentTabSubrouteState = getStateFromNavigatorRoute(currentTabSubroute); if (currentTabSubrouteState.type !== 'stack') { return true; } // Finally, we want to disable the swipe gesture if there is a stack with // more than one subroute, since then the stack will have its own swipe // gesture that will conflict with the drawer's return currentTabSubrouteState.routes.length < 2; }, ); +function useCurrentLeafRouteName(): ?string { + const navContext = React.useContext(NavContext); + return React.useMemo(() => { + if (!navContext) { + return undefined; + } + return currentLeafRoute(navContext.state).name; + }, [navContext]); +} + export { createIsForegroundSelector, useIsAppLoggedIn, createActiveTabSelector, scrollBlockingModalsClosedSelector, selectBackgroundIsDark, activeThreadSelector, activeMessageListSelector, useActiveThread, useActiveMessageList, calendarActiveSelector, nativeCalendarQuery, nonThreadCalendarQuery, useCalendarQuery, drawerSwipeEnabledSelector, + useCurrentLeafRouteName, };