diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/Navigation.tsx | 104 | ||||
-rw-r--r-- | src/components/Link.tsx | 36 | ||||
-rw-r--r-- | src/lib/hooks/useNavigationDeduped.ts | 47 | ||||
-rw-r--r-- | src/lib/routes/types.ts | 2 | ||||
-rw-r--r-- | src/view/com/util/Link.tsx | 83 | ||||
-rw-r--r-- | src/view/shell/Drawer.tsx | 22 | ||||
-rw-r--r-- | src/view/shell/bottom-bar/BottomBar.tsx | 52 | ||||
-rw-r--r-- | src/view/shell/createNativeStackNavigatorWithAuth.tsx | 48 | ||||
-rw-r--r-- | src/view/shell/desktop/LeftNav.tsx | 16 |
9 files changed, 215 insertions, 195 deletions
diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 8b981df7c..2f26c0971 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -103,7 +103,7 @@ import { import {Wizard} from '#/screens/StarterPack/Wizard' import TopicScreen from '#/screens/Topic' import {VideoFeed} from '#/screens/VideoFeed' -import {useTheme} from '#/alf' +import {type Theme, useTheme} from '#/alf' import { EmailDialogScreenID, useEmailDialogControl, @@ -127,7 +127,7 @@ const Tab = createBottomTabNavigator<BottomTabNavigatorParams>() /** * These "common screens" are reused across stacks. */ -function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) { +function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { const title = (page: MessageDescriptor) => bskyTitle(i18n._(page), unreadCountLabel) @@ -500,6 +500,10 @@ function TabsNavigator() { <Tab.Screen name="HomeTab" getComponent={() => HomeTabNavigator} /> <Tab.Screen name="SearchTab" getComponent={() => SearchTabNavigator} /> <Tab.Screen + name="MessagesTab" + getComponent={() => MessagesTabNavigator} + /> + <Tab.Screen name="NotificationsTab" getComponent={() => NotificationsTabNavigator} /> @@ -507,29 +511,26 @@ function TabsNavigator() { name="MyProfileTab" getComponent={() => MyProfileTabNavigator} /> - <Tab.Screen - name="MessagesTab" - getComponent={() => MessagesTabNavigator} - /> </Tab.Navigator> ) } +function screenOptions(t: Theme) { + return { + fullScreenGestureEnabled: true, + headerShown: false, + contentStyle: t.atoms.bg, + } as const +} + function HomeTabNavigator() { const t = useTheme() return ( - <HomeTab.Navigator - screenOptions={{ - animationDuration: 285, - gestureEnabled: true, - fullScreenGestureEnabled: true, - headerShown: false, - contentStyle: t.atoms.bg, - }}> + <HomeTab.Navigator screenOptions={screenOptions(t)} initialRouteName="Home"> <HomeTab.Screen name="Home" getComponent={() => HomeScreen} /> <HomeTab.Screen name="Start" getComponent={() => HomeScreen} /> - {commonScreens(HomeTab)} + {commonScreens(HomeTab as typeof Flat)} </HomeTab.Navigator> ) } @@ -538,15 +539,10 @@ function SearchTabNavigator() { const t = useTheme() return ( <SearchTab.Navigator - screenOptions={{ - animationDuration: 285, - gestureEnabled: true, - fullScreenGestureEnabled: true, - headerShown: false, - contentStyle: t.atoms.bg, - }}> + screenOptions={screenOptions(t)} + initialRouteName="Search"> <SearchTab.Screen name="Search" getComponent={() => SearchScreen} /> - {commonScreens(SearchTab as typeof HomeTab)} + {commonScreens(SearchTab as typeof Flat)} </SearchTab.Navigator> ) } @@ -555,19 +551,14 @@ function NotificationsTabNavigator() { const t = useTheme() return ( <NotificationsTab.Navigator - screenOptions={{ - animationDuration: 285, - gestureEnabled: true, - fullScreenGestureEnabled: true, - headerShown: false, - contentStyle: t.atoms.bg, - }}> + screenOptions={screenOptions(t)} + initialRouteName="Notifications"> <NotificationsTab.Screen name="Notifications" getComponent={() => NotificationsScreen} options={{requireAuth: true}} /> - {commonScreens(NotificationsTab as typeof HomeTab)} + {commonScreens(NotificationsTab as typeof Flat)} </NotificationsTab.Navigator> ) } @@ -576,23 +567,16 @@ function MyProfileTabNavigator() { const t = useTheme() return ( <MyProfileTab.Navigator - screenOptions={{ - animationDuration: 285, - gestureEnabled: true, - fullScreenGestureEnabled: true, - headerShown: false, - contentStyle: t.atoms.bg, - }}> + screenOptions={screenOptions(t)} + initialRouteName="MyProfile"> <MyProfileTab.Screen - // @ts-ignore // TODO: fix this broken type in ProfileScreen - name="MyProfile" + // MyProfile is not in AllNavigationParams - asserting as Profile at least + // gives us typechecking for initialParams -sfn + name={'MyProfile' as 'Profile'} getComponent={() => ProfileScreen} - initialParams={{ - name: 'me', - hideBackButton: true, - }} + initialParams={{name: 'me', hideBackButton: true}} /> - {commonScreens(MyProfileTab as typeof HomeTab)} + {commonScreens(MyProfileTab as unknown as typeof Flat)} </MyProfileTab.Navigator> ) } @@ -601,13 +585,8 @@ function MessagesTabNavigator() { const t = useTheme() return ( <MessagesTab.Navigator - screenOptions={{ - animationDuration: 285, - gestureEnabled: true, - fullScreenGestureEnabled: true, - headerShown: false, - contentStyle: t.atoms.bg, - }}> + screenOptions={screenOptions(t)} + initialRouteName="Messages"> <MessagesTab.Screen name="Messages" getComponent={() => MessagesScreen} @@ -616,7 +595,7 @@ function MessagesTabNavigator() { animationTypeForReplace: route.params?.animation ?? 'push', })} /> - {commonScreens(MessagesTab as typeof HomeTab)} + {commonScreens(MessagesTab as typeof Flat)} </MessagesTab.Navigator> ) } @@ -634,13 +613,7 @@ const FlatNavigator = () => { return ( <Flat.Navigator screenListeners={screenListeners} - screenOptions={{ - animationDuration: 285, - gestureEnabled: true, - fullScreenGestureEnabled: true, - headerShown: false, - contentStyle: t.atoms.bg, - }}> + screenOptions={screenOptions(t)}> <Flat.Screen name="Home" getComponent={() => HomeScreen} @@ -666,7 +639,7 @@ const FlatNavigator = () => { getComponent={() => HomeScreen} options={{title: title(msg`Home`)}} /> - {commonScreens(Flat as typeof HomeTab, numUnread)} + {commonScreens(Flat, numUnread)} </Flat.Navigator> ) } @@ -773,7 +746,14 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) { logModuleInitTime() onReady() logger.metric('router:navigate', {}, {statsig: false}) - }}> + }} + // WARNING: Implicit navigation to nested navigators is depreciated in React Navigation 7.x + // However, there's a fair amount of places we do that, especially in when popping to the top of stacks. + // See BottomBar.tsx for an example of how to handle nested navigators in the tabs correctly. + // I'm scared of missing a spot (esp. with push notifications etc) so let's enable this legacy behaviour for now. + // We will need to confirm we handle nested navigators correctly by the time we migrate to React Navigation 8.x + // -sfn + navigationInChildEnabled> {children} </NavigationContainer> </> diff --git a/src/components/Link.tsx b/src/components/Link.tsx index cca93c0c8..d73a3db4a 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -1,7 +1,11 @@ -import React from 'react' +import React, {useMemo} from 'react' import {type GestureResponderEvent} from 'react-native' import {sanitizeUrl} from '@braintree/sanitize-url' -import {StackActions, useLinkProps} from '@react-navigation/native' +import { + type LinkProps as RNLinkProps, + StackActions, + useLinkBuilder, +} from '@react-navigation/native' import {BSKY_DOWNLOAD_URL} from '#/lib/constants' import {useNavigationDeduped} from '#/lib/hooks/useNavigationDeduped' @@ -28,12 +32,11 @@ import {router} from '#/routes' */ export {useButtonContext as useLinkContext} from '#/components/Button' -type BaseLinkProps = Pick< - Parameters<typeof useLinkProps<AllNavigatorParams>>[0], - 'to' -> & { +type BaseLinkProps = { testID?: string + to: RNLinkProps<AllNavigatorParams> | string + /** * The React Navigation `StackAction` to perform when the link is pressed. */ @@ -92,10 +95,23 @@ export function useLink({ shouldProxy?: boolean }) { const navigation = useNavigationDeduped() - const {href} = useLinkProps<AllNavigatorParams>({ - to: - typeof to === 'string' ? convertBskyAppUrlIfNeeded(sanitizeUrl(to)) : to, - }) + const {buildHref} = useLinkBuilder() + const href = useMemo(() => { + return typeof to === 'string' + ? convertBskyAppUrlIfNeeded(sanitizeUrl(to)) + : to.screen + ? buildHref(to.screen, to.params) + : to.href + ? convertBskyAppUrlIfNeeded(sanitizeUrl(to.href)) + : undefined + }, [to, buildHref]) + + if (!href) { + throw new Error( + 'Link `to` prop must be a string or an object with `screen` and `params` properties', + ) + } + const isExternal = isExternalUrl(href) const {openModal, closeModal} = useModalControls() const openLink = useOpenLink() diff --git a/src/lib/hooks/useNavigationDeduped.ts b/src/lib/hooks/useNavigationDeduped.ts index 56ae5e8a2..dc18742c0 100644 --- a/src/lib/hooks/useNavigationDeduped.ts +++ b/src/lib/hooks/useNavigationDeduped.ts @@ -1,10 +1,8 @@ -import React from 'react' +import {useMemo} from 'react' import {useNavigation} from '@react-navigation/core' -import {NavigationState} from '@react-navigation/native' -import type {NavigationAction} from '@react-navigation/routers' import {useDedupe} from '#/lib/hooks/useDedupe' -import {AllNavigatorParams, NavigationProp} from '#/lib/routes/types' +import {type NavigationProp} from '#/lib/routes/types' export type DebouncedNavigationProp = Pick< NavigationProp, @@ -22,46 +20,19 @@ export function useNavigationDeduped() { const navigation = useNavigation<NavigationProp>() const dedupe = useDedupe() - return React.useMemo( - (): DebouncedNavigationProp => ({ - // Types from @react-navigation/routers/lib/typescript/src/StackRouter.ts - push: <RouteName extends keyof AllNavigatorParams>( - ...args: undefined extends AllNavigatorParams[RouteName] - ? - | [screen: RouteName] - | [screen: RouteName, params: AllNavigatorParams[RouteName]] - : [screen: RouteName, params: AllNavigatorParams[RouteName]] - ) => { + return useMemo<DebouncedNavigationProp>( + () => ({ + push: (...args: Parameters<typeof navigation.push>) => { dedupe(() => navigation.push(...args)) }, - // Types from @react-navigation/core/src/types.tsx - navigate: <RouteName extends keyof AllNavigatorParams>( - ...args: RouteName extends unknown - ? undefined extends AllNavigatorParams[RouteName] - ? - | [screen: RouteName] - | [screen: RouteName, params: AllNavigatorParams[RouteName]] - : [screen: RouteName, params: AllNavigatorParams[RouteName]] - : never - ) => { + navigate: (...args: Parameters<typeof navigation.navigate>) => { dedupe(() => navigation.navigate(...args)) }, - // Types from @react-navigation/routers/lib/typescript/src/StackRouter.ts - replace: <RouteName extends keyof AllNavigatorParams>( - ...args: undefined extends AllNavigatorParams[RouteName] - ? - | [screen: RouteName] - | [screen: RouteName, params: AllNavigatorParams[RouteName]] - : [screen: RouteName, params: AllNavigatorParams[RouteName]] - ) => { + replace: (...args: Parameters<typeof navigation.replace>) => { dedupe(() => navigation.replace(...args)) }, - dispatch: ( - action: - | NavigationAction - | ((state: NavigationState) => NavigationAction), - ) => { - dedupe(() => navigation.dispatch(action)) + dispatch: (...args: Parameters<typeof navigation.dispatch>) => { + dedupe(() => navigation.dispatch(...args)) }, popToTop: () => { dedupe(() => navigation.popToTop()) diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index 0bc85b630..6f102d438 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -92,7 +92,7 @@ export type NotificationsTabNavigatorParams = CommonNavigatorParams & { } export type MyProfileTabNavigatorParams = CommonNavigatorParams & { - MyProfile: undefined + MyProfile: {name: 'me'; hideBackButton: true} } export type MessagesTabNavigatorParams = CommonNavigatorParams & { diff --git a/src/view/com/util/Link.tsx b/src/view/com/util/Link.tsx index 489fbc59c..3a0bf6f6d 100644 --- a/src/view/com/util/Link.tsx +++ b/src/view/com/util/Link.tsx @@ -1,20 +1,20 @@ -import React, {ComponentProps, memo, useMemo} from 'react' +import {memo, useCallback, useMemo} from 'react' import { - GestureResponderEvent, + type GestureResponderEvent, Platform, Pressable, - StyleProp, - TextProps, - TextStyle, - TouchableOpacity, + type StyleProp, + type TextProps, + type TextStyle, + type TouchableOpacity, View, - ViewStyle, + type ViewStyle, } from 'react-native' import {sanitizeUrl} from '@braintree/sanitize-url' -import {StackActions, useLinkProps} from '@react-navigation/native' +import {StackActions} from '@react-navigation/native' import { - DebouncedNavigationProp, + type DebouncedNavigationProp, useNavigationDeduped, } from '#/lib/hooks/useNavigationDeduped' import {useOpenLink} from '#/lib/hooks/useOpenLink' @@ -24,7 +24,7 @@ import { isExternalUrl, linkRequiresWarning, } from '#/lib/strings/url-helpers' -import {TypographyVariant} from '#/lib/ThemeContext' +import {type TypographyVariant} from '#/lib/ThemeContext' import {isAndroid, isWeb} from '#/platform/detection' import {emitSoftReset} from '#/state/events' import {useModalControls} from '#/state/modals' @@ -38,7 +38,7 @@ type Event = | React.MouseEvent<HTMLAnchorElement, MouseEvent> | GestureResponderEvent -interface Props extends ComponentProps<typeof TouchableOpacity> { +interface Props extends React.ComponentProps<typeof TouchableOpacity> { testID?: string style?: StyleProp<ViewStyle> href?: string @@ -47,7 +47,7 @@ interface Props extends ComponentProps<typeof TouchableOpacity> { hoverStyle?: StyleProp<ViewStyle> noFeedback?: boolean asAnchor?: boolean - dataSet?: Object | undefined + dataSet?: any anchorNoUnderline?: boolean navigationAction?: 'push' | 'replace' | 'navigate' onPointerEnter?: () => void @@ -69,6 +69,7 @@ export const Link = memo(function Link({ onBeforePress, accessibilityActions, onAccessibilityAction, + dataSet: dataSetProp, ...props }: Props) { const t = useTheme() @@ -77,7 +78,7 @@ export const Link = memo(function Link({ const anchorHref = asAnchor ? sanitizeUrl(href) : undefined const openLink = useOpenLink() - const onPress = React.useCallback( + const onPress = useCallback( (e?: Event) => { onBeforePress?.() if (typeof href === 'string') { @@ -99,6 +100,14 @@ export const Link = memo(function Link({ {name: 'activate', label: title}, ] + const dataSet = useMemo(() => { + const ds = {...dataSetProp} + if (anchorNoUnderline) { + ds.noUnderline = 1 + } + return ds + }, [dataSetProp, anchorNoUnderline]) + if (noFeedback) { return ( <WebAuxClickWrapper> @@ -129,17 +138,6 @@ export const Link = memo(function Link({ ) } - if (anchorNoUnderline) { - // @ts-ignore web only -prf - props.dataSet = props.dataSet || {} - // @ts-ignore web only -prf - props.dataSet.noUnderline = 1 - } - - if (title && !props.accessibilityLabel) { - props.accessibilityLabel = title - } - const Com = props.hoverStyle ? PressableWithHover : Pressable return ( <Com @@ -148,8 +146,11 @@ export const Link = memo(function Link({ onPress={onPress} accessible={accessible} accessibilityRole="link" + accessibilityLabel={props.accessibilityLabel ?? title} + accessibilityHint={props.accessibilityHint} // @ts-ignore web only -prf href={anchorHref} + dataSet={dataSet} {...props}> {children ? children : <Text>{title || 'link'}</Text>} </Com> @@ -164,14 +165,14 @@ export const TextLink = memo(function TextLink({ text, numberOfLines, lineHeight, - dataSet, + dataSet: dataSetProp, title, - onPress, + onPress: onPressProp, onBeforePress, disableMismatchWarning, navigationAction, anchorNoUnderline, - ...orgProps + ...props }: { testID?: string type?: TypographyVariant @@ -187,7 +188,6 @@ export const TextLink = memo(function TextLink({ anchorNoUnderline?: boolean onBeforePress?: () => void } & TextProps) { - const {...props} = useLinkProps({to: sanitizeUrl(href)}) const navigation = useNavigationDeduped() const {openModal, closeModal} = useModalControls() const openLink = useOpenLink() @@ -196,12 +196,15 @@ export const TextLink = memo(function TextLink({ console.error('Unable to detect mismatching label') } - if (anchorNoUnderline) { - dataSet = dataSet ?? {} - dataSet.noUnderline = 1 - } + const dataSet = useMemo(() => { + const ds = {...dataSetProp} + if (anchorNoUnderline) { + ds.noUnderline = 1 + } + return ds + }, [dataSetProp, anchorNoUnderline]) - props.onPress = React.useCallback( + const onPress = useCallback( (e?: Event) => { const requiresWarning = !disableMismatchWarning && @@ -224,10 +227,10 @@ export const TextLink = memo(function TextLink({ return } onBeforePress?.() - if (onPress) { + if (onPressProp) { e?.preventDefault?.() - // @ts-ignore function signature differs by platform -prf - return onPress() + // @ts-expect-error function signature differs by platform -prf + return onPressProp() } return onPressInner( closeModal, @@ -240,7 +243,7 @@ export const TextLink = memo(function TextLink({ }, [ onBeforePress, - onPress, + onPressProp, closeModal, openModal, navigation, @@ -273,8 +276,10 @@ export const TextLink = memo(function TextLink({ title={title} // @ts-ignore web only -prf hrefAttrs={hrefAttrs} // hack to get open in new tab to work on safari. without this, safari will open in a new window - {...props} - {...orgProps}> + onPress={onPress} + accessibilityRole="link" + href={convertBskyAppUrlIfNeeded(sanitizeUrl(href))} + {...props}> {text} </Text> ) diff --git a/src/view/shell/Drawer.tsx b/src/view/shell/Drawer.tsx index c4624e8e1..79d8a21ae 100644 --- a/src/view/shell/Drawer.tsx +++ b/src/view/shell/Drawer.tsx @@ -160,7 +160,7 @@ let DrawerContent = ({}: React.PropsWithoutRef<{}>): React.ReactNode => { // = const onPressTab = React.useCallback( - (tab: string) => { + (tab: 'Home' | 'Search' | 'Messages' | 'Notifications' | 'MyProfile') => { const state = navigation.getState() setDrawerOpen(false) if (isWeb) { @@ -168,7 +168,7 @@ let DrawerContent = ({}: React.PropsWithoutRef<{}>): React.ReactNode => { if (tab === 'MyProfile') { navigation.navigate('Profile', {name: currentAccount!.handle}) } else { - // @ts-ignore must be Home, Search, Notifications, or MyProfile + // @ts-expect-error struggles with string unions, apparently navigation.navigate(tab) } } else { @@ -176,9 +176,23 @@ let DrawerContent = ({}: React.PropsWithoutRef<{}>): React.ReactNode => { if (tabState === TabState.InsideAtRoot) { emitSoftReset() } else if (tabState === TabState.Inside) { - navigation.dispatch(StackActions.popToTop()) + // find the correct navigator in which to pop-to-top + const target = state.routes.find(route => route.name === `${tab}Tab`) + ?.state?.key + if (target) { + // if we found it, trigger pop-to-top + navigation.dispatch({ + ...StackActions.popToTop(), + target, + }) + } else { + // fallback: reset navigation + navigation.reset({ + index: 0, + routes: [{name: `${tab}Tab`}], + }) + } } else { - // @ts-ignore must be Home, Search, Notifications, or MyProfile navigation.navigate(`${tab}Tab`) } } diff --git a/src/view/shell/bottom-bar/BottomBar.tsx b/src/view/shell/bottom-bar/BottomBar.tsx index 92be6c67e..5e9168ecd 100644 --- a/src/view/shell/bottom-bar/BottomBar.tsx +++ b/src/view/shell/bottom-bar/BottomBar.tsx @@ -1,4 +1,4 @@ -import React, {type ComponentProps} from 'react' +import {useCallback} from 'react' import {type GestureResponderEvent, View} from 'react-native' import Animated from 'react-native-reanimated' import {useSafeAreaInsets} from 'react-native-safe-area-context' @@ -52,13 +52,7 @@ import { import {useDemoMode} from '#/storage/hooks/demo-mode' import {styles} from './BottomBarStyles' -type TabOptions = - | 'Home' - | 'Search' - | 'Notifications' - | 'MyProfile' - | 'Feeds' - | 'Messages' +type TabOptions = 'Home' | 'Search' | 'Messages' | 'Notifications' | 'MyProfile' export function BottomBar({navigation}: BottomTabBarProps) { const {hasSession, currentAccount} = useSession() @@ -81,48 +75,62 @@ export function BottomBar({navigation}: BottomTabBarProps) { const gate = useGate() const iconWidth = 28 - const showSignIn = React.useCallback(() => { + const showSignIn = useCallback(() => { closeAllActiveElements() requestSwitchToAccount({requestedAccount: 'none'}) }, [requestSwitchToAccount, closeAllActiveElements]) - const showCreateAccount = React.useCallback(() => { + const showCreateAccount = useCallback(() => { closeAllActiveElements() requestSwitchToAccount({requestedAccount: 'new'}) // setShowLoggedOut(true) }, [requestSwitchToAccount, closeAllActiveElements]) - const onPressTab = React.useCallback( + const onPressTab = useCallback( (tab: TabOptions) => { const state = navigation.getState() const tabState = getTabState(state, tab) if (tabState === TabState.InsideAtRoot) { emitSoftReset() } else if (tabState === TabState.Inside) { - dedupe(() => navigation.dispatch(StackActions.popToTop())) + // find the correct navigator in which to pop-to-top + const target = state.routes.find(route => route.name === `${tab}Tab`) + ?.state?.key + dedupe(() => { + if (target) { + // if we found it, trigger pop-to-top + navigation.dispatch({ + ...StackActions.popToTop(), + target, + }) + } else { + // fallback: reset navigation + navigation.reset({ + index: 0, + routes: [{name: `${tab}Tab`}], + }) + } + }) } else { dedupe(() => navigation.navigate(`${tab}Tab`)) } }, [navigation, dedupe], ) - const onPressHome = React.useCallback(() => onPressTab('Home'), [onPressTab]) - const onPressSearch = React.useCallback( - () => onPressTab('Search'), - [onPressTab], - ) - const onPressNotifications = React.useCallback( + const onPressHome = useCallback(() => onPressTab('Home'), [onPressTab]) + const onPressSearch = useCallback(() => onPressTab('Search'), [onPressTab]) + const onPressNotifications = useCallback( () => onPressTab('Notifications'), [onPressTab], ) - const onPressProfile = React.useCallback(() => { + const onPressProfile = useCallback(() => { onPressTab('MyProfile') }, [onPressTab]) - const onPressMessages = React.useCallback(() => { + const onPressMessages = useCallback(() => { onPressTab('Messages') }, [onPressTab]) - const onLongPressProfile = React.useCallback(() => { + const onLongPressProfile = useCallback(() => { playHaptic() accountSwitchControl.open() }, [accountSwitchControl, playHaptic]) @@ -361,7 +369,7 @@ export function BottomBar({navigation}: BottomTabBarProps) { interface BtnProps extends Pick< - ComponentProps<typeof PressableScale>, + React.ComponentProps<typeof PressableScale>, | 'accessible' | 'accessibilityRole' | 'accessibilityHint' diff --git a/src/view/shell/createNativeStackNavigatorWithAuth.tsx b/src/view/shell/createNativeStackNavigatorWithAuth.tsx index 868bba5b0..1c32971d4 100644 --- a/src/view/shell/createNativeStackNavigatorWithAuth.tsx +++ b/src/view/shell/createNativeStackNavigatorWithAuth.tsx @@ -1,25 +1,29 @@ import * as React from 'react' import {View} from 'react-native' -// Based on @react-navigation/native-stack/src/createNativeStackNavigator.ts +// Based on @react-navigation/native-stack/src/navigators/createNativeStackNavigator.ts // MIT License // Copyright (c) 2017 React Navigation Contributors import { createNavigatorFactory, type EventArg, + type NavigatorTypeBagBase, type ParamListBase, type StackActionHelpers, StackActions, type StackNavigationState, StackRouter, type StackRouterOptions, + type StaticConfig, + type TypedNavigator, useNavigationBuilder, } from '@react-navigation/native' +import {NativeStackView} from '@react-navigation/native-stack' import { type NativeStackNavigationEventMap, type NativeStackNavigationOptions, + type NativeStackNavigationProp, + type NativeStackNavigatorProps, } from '@react-navigation/native-stack' -import {NativeStackView} from '@react-navigation/native-stack' -import {type NativeStackNavigatorProps} from '@react-navigation/native-stack/src/types' import {PWI_ENABLED} from '#/lib/build-flags' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' @@ -48,12 +52,14 @@ function NativeStackNavigator({ id, initialRouteName, children, + layout, screenListeners, screenOptions, + screenLayout, ...rest }: NativeStackNavigatorProps) { // --- this is copy and pasted from the original native stack navigator --- - const {state, descriptors, navigation, NavigationContent} = + const {state, describe, descriptors, navigation, NavigationContent} = useNavigationBuilder< StackNavigationState<ParamListBase>, StackRouterOptions, @@ -64,9 +70,12 @@ function NativeStackNavigator({ id, initialRouteName, children, + layout, screenListeners, screenOptions, + screenLayout, }) + React.useEffect( () => // @ts-expect-error: there may not be a tab navigator in parent @@ -148,7 +157,8 @@ function NativeStackNavigator({ {...rest} state={state} navigation={navigation} - descriptors={newDescriptors} + descriptors={descriptors} + describe={describe} /> </View> {isWeb && ( @@ -161,9 +171,25 @@ function NativeStackNavigator({ ) } -export const createNativeStackNavigatorWithAuth = createNavigatorFactory< - StackNavigationState<ParamListBase>, - NativeStackNavigationOptionsWithAuth, - NativeStackNavigationEventMap, - typeof NativeStackNavigator ->(NativeStackNavigator) +export function createNativeStackNavigatorWithAuth< + const ParamList extends ParamListBase, + const NavigatorID extends string | undefined = undefined, + const TypeBag extends NavigatorTypeBagBase = { + ParamList: ParamList + NavigatorID: NavigatorID + State: StackNavigationState<ParamList> + ScreenOptions: NativeStackNavigationOptionsWithAuth + EventMap: NativeStackNavigationEventMap + NavigationList: { + [RouteName in keyof ParamList]: NativeStackNavigationProp< + ParamList, + RouteName, + NavigatorID + > + } + Navigator: typeof NativeStackNavigator + }, + const Config extends StaticConfig<TypeBag> = StaticConfig<TypeBag>, +>(config?: Config): TypedNavigator<TypeBag, Config> { + return createNavigatorFactory(NativeStackNavigator)(config) +} diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx index f6c852ca1..52df66d70 100644 --- a/src/view/shell/desktop/LeftNav.tsx +++ b/src/view/shell/desktop/LeftNav.tsx @@ -1,10 +1,10 @@ -import React from 'react' +import {useCallback, useMemo, useState} from 'react' import {StyleSheet, View} from 'react-native' import {type AppBskyActorDefs} from '@atproto/api' import {msg, plural, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import { - useLinkProps, + useLinkTo, useNavigation, useNavigationState, } from '@react-navigation/native' @@ -326,7 +326,7 @@ function NavItem({count, hasNew, href, icon, iconFilled, label}: NavItemProps) { const {_} = useLingui() const {currentAccount} = useSession() const {leftNavMinimal} = useLayoutBreakpoints() - const [pathName] = React.useMemo(() => router.matchPath(href), [href]) + const [pathName] = useMemo(() => router.matchPath(href), [href]) const currentRouteInfo = useNavigationState(state => { if (!state) { return {name: 'Home'} @@ -339,8 +339,8 @@ function NavItem({count, hasNew, href, icon, iconFilled, label}: NavItemProps) { (currentRouteInfo.params as CommonNavigatorParams['Profile']).name === currentAccount?.handle : isTab(currentRouteInfo.name, pathName) - const {onPress} = useLinkProps({to: href}) - const onPressWrapped = React.useCallback( + const linkTo = useLinkTo() + const onPressWrapped = useCallback( (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => { if (e.ctrlKey || e.metaKey || e.altKey) { return @@ -349,10 +349,10 @@ function NavItem({count, hasNew, href, icon, iconFilled, label}: NavItemProps) { if (isCurrent) { emitSoftReset() } else { - onPress() + linkTo(href) } }, - [onPress, isCurrent], + [linkTo, href, isCurrent], ) return ( @@ -468,7 +468,7 @@ function ComposeBtn() { const {openComposer} = useOpenComposer() const {_} = useLingui() const {leftNavMinimal} = useLayoutBreakpoints() - const [isFetchingHandle, setIsFetchingHandle] = React.useState(false) + const [isFetchingHandle, setIsFetchingHandle] = useState(false) const fetchHandle = useFetchHandle() const getProfileHandle = async () => { |