diff options
-rw-r--r-- | package.json | 10 | ||||
-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 | ||||
-rw-r--r-- | yarn.lock | 127 |
11 files changed, 292 insertions, 255 deletions
diff --git a/package.json b/package.json index 280312ce5..ac2171a22 100644 --- a/package.json +++ b/package.json @@ -94,10 +94,10 @@ "@react-native-async-storage/async-storage": "2.1.2", "@react-native-menu/menu": "^1.2.3", "@react-native-picker/picker": "2.11.0", - "@react-navigation/bottom-tabs": "^6.5.20", - "@react-navigation/drawer": "^6.6.15", - "@react-navigation/native": "^6.1.17", - "@react-navigation/native-stack": "^6.9.26", + "@react-navigation/bottom-tabs": "^7.3.13", + "@react-navigation/drawer": "^7.3.12", + "@react-navigation/native": "^7.1.9", + "@react-navigation/native-stack": "^7.3.13", "@sentry/react-native": "~6.10.0", "@tanstack/query-async-storage-persister": "^5.25.0", "@tanstack/react-query": "^5.8.1", @@ -198,7 +198,7 @@ "react-native-reanimated": "~3.17.5", "react-native-root-siblings": "^4.1.1", "react-native-safe-area-context": "5.4.0", - "react-native-screens": "~4.10.0", + "react-native-screens": "^4.11.1", "react-native-svg": "15.11.2", "react-native-uitextview": "^1.4.0", "react-native-url-polyfill": "^1.3.0", 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 () => { diff --git a/yarn.lock b/yarn.lock index a9901c42c..d299bede5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6289,65 +6289,69 @@ invariant "^2.2.4" nullthrows "^1.1.1" -"@react-navigation/bottom-tabs@^6.5.20": - version "6.5.20" - resolved "https://registry.yarnpkg.com/@react-navigation/bottom-tabs/-/bottom-tabs-6.5.20.tgz#5335e75b02c527ef0569bd97d4f9185d65616e49" - integrity sha512-ow6Z06iS4VqBO8d7FP+HsGjJLWt2xTWIvuWjpoCvsM/uQXzCRDIjBv9HaKcXbF0yTW7IMir0oDAbU5PFzEDdgA== +"@react-navigation/bottom-tabs@^7.3.13": + version "7.3.14" + resolved "https://registry.yarnpkg.com/@react-navigation/bottom-tabs/-/bottom-tabs-7.3.14.tgz#9ee02baea86ab24abe267726665bc69c6df0bf4c" + integrity sha512-s2qinJggS2HYZdCOey9A+fN+bNpWeEKwiL/FjAVOTcv+uofxPWN6CtEZUZGPEjfRjis/srURBmCmpNZSI6sQ9Q== dependencies: - "@react-navigation/elements" "^1.3.30" + "@react-navigation/elements" "^2.4.3" color "^4.2.3" - warn-once "^0.1.0" -"@react-navigation/core@^6.4.16": - version "6.4.16" - resolved "https://registry.yarnpkg.com/@react-navigation/core/-/core-6.4.16.tgz#f9369a134805174536b9aa0f0f483b930511caf9" - integrity sha512-UDTJBsHxnzgFETR3ZxhctP+RWr4SkyeZpbhpkQoIGOuwSCkt1SE0qjU48/u6r6w6XlX8OqVudn1Ab0QFXTHxuQ== +"@react-navigation/core@^7.10.0": + version "7.10.0" + resolved "https://registry.yarnpkg.com/@react-navigation/core/-/core-7.10.0.tgz#8205ea6b84ce34b2fc2c196701b4cd9b434211b9" + integrity sha512-qZBA5gGm+9liT4+EHk+kl9apwvqh7HqhLF1XeX6SQRmC/n2QI0u1B8OevKc+EPUDEM9Od15IuwT/GRbSs7/Umw== dependencies: - "@react-navigation/routers" "^6.1.9" + "@react-navigation/routers" "^7.4.0" escape-string-regexp "^4.0.0" - nanoid "^3.1.23" + nanoid "^3.3.11" query-string "^7.1.3" - react-is "^16.13.0" - use-latest-callback "^0.1.9" + react-is "^19.1.0" + use-latest-callback "^0.2.3" + use-sync-external-store "^1.5.0" -"@react-navigation/drawer@^6.6.15": - version "6.6.15" - resolved "https://registry.yarnpkg.com/@react-navigation/drawer/-/drawer-6.6.15.tgz#fcedba68f735103dbc035911f5959ce926081d62" - integrity sha512-GLkFQNxjtmxB/qXSHmu1DfoB89jCzW64tmX68iPndth+9U+0IP27GcCCaMZxQfwj+nI8Kn2zlTlXAZDIIHE+DQ== +"@react-navigation/drawer@^7.3.12": + version "7.4.1" + resolved "https://registry.yarnpkg.com/@react-navigation/drawer/-/drawer-7.4.1.tgz#50517d8c57f09cdbfc20a485c47016066b918e76" + integrity sha512-kj5wL31smDLw/6l+0KPR5cjaOZg6oHJCl3RPQonFPuYolUPZBVnuS++uvlifWcD/mqdGmhl3rgLTircRH4vQ7Q== dependencies: - "@react-navigation/elements" "^1.3.30" + "@react-navigation/elements" "^2.4.3" color "^4.2.3" - warn-once "^0.1.0" + react-native-drawer-layout "^4.1.10" + use-latest-callback "^0.2.3" -"@react-navigation/elements@^1.3.30": - version "1.3.30" - resolved "https://registry.yarnpkg.com/@react-navigation/elements/-/elements-1.3.30.tgz#a81371f599af1070b12014f05d6c09b1a611fd9a" - integrity sha512-plhc8UvCZs0UkV+sI+3bisIyn78wz9O/BiWZXpounu72k/R/Sj5PuZYFJ1fi6psvriUveMCGh4LeZckAZu2qiQ== +"@react-navigation/elements@^2.4.3": + version "2.4.3" + resolved "https://registry.yarnpkg.com/@react-navigation/elements/-/elements-2.4.3.tgz#cc1dde4c98739d35a0c9c23872316063962cfaee" + integrity sha512-psoNmnZ0DQIt9nxxPITVLtYW04PGCAfnmd/Pcd3yhiBs93aj+HYKH+SDZDpUnXMf3BN7Wvo4+jPI+/Xjqb+m9w== + dependencies: + color "^4.2.3" -"@react-navigation/native-stack@^6.9.26": - version "6.9.26" - resolved "https://registry.yarnpkg.com/@react-navigation/native-stack/-/native-stack-6.9.26.tgz#90facf7783c9927f094bc9f01c613af75b6c241e" - integrity sha512-++dueQ+FDj2XkZ902DVrK79ub1vp19nSdAZWxKRgd6+Bc0Niiesua6rMCqymYOVaYh+dagwkA9r00bpt/U5WLw== +"@react-navigation/native-stack@^7.3.13": + version "7.3.14" + resolved "https://registry.yarnpkg.com/@react-navigation/native-stack/-/native-stack-7.3.14.tgz#d1c90f2e50cd13bbced923991cf2faee8083f725" + integrity sha512-45Sf7ReqSCIySXS5nrKtLGmNlFXm5x+u32YQMwKDONCqVGOBCfo4ryKqeQq1EMJ7Py6IDyOwHMhA+jhNOxnfPw== dependencies: - "@react-navigation/elements" "^1.3.30" - warn-once "^0.1.0" + "@react-navigation/elements" "^2.4.3" + warn-once "^0.1.1" -"@react-navigation/native@^6.1.17": - version "6.1.17" - resolved "https://registry.yarnpkg.com/@react-navigation/native/-/native-6.1.17.tgz#439f15a99809d26ea4682d2a3766081cf2ca31cf" - integrity sha512-mer3OvfwWOHoUSMJyLa4vnBH3zpFmCwuzrBPlw7feXklurr/ZDiLjLxUScOot6jLRMz/67GyilEYMmP99LL0RQ== +"@react-navigation/native@^7.1.9": + version "7.1.10" + resolved "https://registry.yarnpkg.com/@react-navigation/native/-/native-7.1.10.tgz#768f674f7c09b6a57215762052aa62a7dc107402" + integrity sha512-Ug4IML0DkAxZTMF/E7lyyLXSclkGAYElY2cxZWITwfBjtlVeda0NjsdnTWY5EGjnd7bwvhTIUC+CO6qSlrDn5A== dependencies: - "@react-navigation/core" "^6.4.16" + "@react-navigation/core" "^7.10.0" escape-string-regexp "^4.0.0" fast-deep-equal "^3.1.3" - nanoid "^3.1.23" + nanoid "^3.3.11" + use-latest-callback "^0.2.3" -"@react-navigation/routers@^6.1.9": - version "6.1.9" - resolved "https://registry.yarnpkg.com/@react-navigation/routers/-/routers-6.1.9.tgz#73f5481a15a38e36592a0afa13c3c064b9f90bed" - integrity sha512-lTM8gSFHSfkJvQkxacGM6VJtBt61ip2XO54aNfswD+KMw6eeZ4oehl7m0me3CR9hnDE4+60iAZR8sAhvCiI3NA== +"@react-navigation/routers@^7.4.0": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@react-navigation/routers/-/routers-7.4.0.tgz#5bace799713ac163310c18711b98dfbe418c6b36" + integrity sha512-th5THnuWKJlmr7GGHiicy979di11ycDWub9iIXbEDvQwmwmsRzppmVbfs2nD8bC/MgyMgqWu/gxfys+HqN+kcw== dependencies: - nanoid "^3.1.23" + nanoid "^3.3.11" "@remirror/core-constants@3.0.0": version "3.0.0" @@ -14962,11 +14966,16 @@ mz@^2.7.0: object-assign "^4.0.1" thenify-all "^1.0.0" -nanoid@^3.1.23, nanoid@^3.3.1, nanoid@^3.3.6: +nanoid@^3.3.1, nanoid@^3.3.6: version "3.3.6" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== +nanoid@^3.3.11: + version "3.3.11" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" + integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== + nanoid@^3.3.7: version "3.3.7" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" @@ -16727,12 +16736,12 @@ react-image-crop@^11.0.7: resolved "https://registry.yarnpkg.com/react-image-crop/-/react-image-crop-11.0.7.tgz#25f3d37ccbb65a05d19d23b4740a5912835c741e" integrity sha512-ZciKWHDYzmm366JDL18CbrVyjnjH0ojufGDmScfS4ZUqLHg4nm6ATY+K62C75W4ZRNt4Ii+tX0bSjNk9LQ2xzQ== -react-is@19, react-is@^19.0.0: +react-is@19, react-is@^19.0.0, react-is@^19.1.0: version "19.1.0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-19.1.0.tgz#805bce321546b7e14c084989c77022351bbdd11b" integrity sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg== -react-is@^16.13.0, react-is@^16.13.1, react-is@^16.7.0: +react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -16764,6 +16773,13 @@ react-native-dotenv@^3.4.11: dependencies: dotenv "^16.4.5" +react-native-drawer-layout@^4.1.10: + version "4.1.10" + resolved "https://registry.yarnpkg.com/react-native-drawer-layout/-/react-native-drawer-layout-4.1.10.tgz#9007cb747767ca8e1c9c3337671ad35ed95ad4d9" + integrity sha512-wejQo0F+EffCkOkRh+DP6ENWMB+aWEHkXV8Pd564PmtoySZLUsV/ksYrh/mrufh7T7EuvGT8+fNHz7mMRYftWg== + dependencies: + use-latest-callback "^0.2.3" + react-native-drawer-layout@^4.1.6: version "4.1.7" resolved "https://registry.yarnpkg.com/react-native-drawer-layout/-/react-native-drawer-layout-4.1.7.tgz#1c741c9bf9c739d6672201692e4ba4839ca0c8ff" @@ -16806,7 +16822,7 @@ react-native-ios-context-menu@^1.15.3: dependencies: "@dominicstop/ts-event-emitter" "^1.1.0" -react-native-is-edge-to-edge@1.1.7: +react-native-is-edge-to-edge@1.1.7, react-native-is-edge-to-edge@^1.1.7: version "1.1.7" resolved "https://registry.yarnpkg.com/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.1.7.tgz#28947688f9fafd584e73a4f935ea9603bd9b1939" integrity sha512-EH6i7E8epJGIcu7KpfXYXiV2JFIYITtq+rVS8uEb+92naMRBdxhTuS8Wn2Q7j9sqyO0B+Xbaaf9VdipIAmGW4w== @@ -16875,12 +16891,13 @@ react-native-safe-area-context@5.4.0: resolved "https://registry.yarnpkg.com/react-native-safe-area-context/-/react-native-safe-area-context-5.4.0.tgz#04b51940408c114f75628a12a93569d30c525454" integrity sha512-JaEThVyJcLhA+vU0NU8bZ0a1ih6GiF4faZ+ArZLqpYbL6j7R3caRqj+mE3lEtKCuHgwjLg3bCxLL1GPUJZVqUA== -react-native-screens@~4.10.0: - version "4.10.0" - resolved "https://registry.yarnpkg.com/react-native-screens/-/react-native-screens-4.10.0.tgz#40634aead590c6b7034ded6a9f92465d1d611906" - integrity sha512-Tw21NGuXm3PbiUGtZd0AnXirUixaAbPXDjNR0baBH7/WJDaDTTELLcQ7QRXuqAWbmr/EVCrKj1348ei1KFIr8A== +react-native-screens@^4.11.1: + version "4.11.1" + resolved "https://registry.yarnpkg.com/react-native-screens/-/react-native-screens-4.11.1.tgz#7d0f3d313d8ddc1e55437c5e038f15f8805dc991" + integrity sha512-F0zOzRVa3ptZfLpD0J8ROdo+y1fEPw+VBFq1MTY/iyDu08al7qFUO5hLMd+EYMda5VXGaTFCa8q7bOppUszhJw== dependencies: react-freeze "^1.0.0" + react-native-is-edge-to-edge "^1.1.7" warn-once "^0.1.0" react-native-svg@15.11.2: @@ -19388,11 +19405,6 @@ use-isomorphic-layout-effect@^1.1.1: resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz#497cefb13d863d687b08477d9e5a164ad8c1a6fb" integrity sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA== -use-latest-callback@^0.1.9: - version "0.1.9" - resolved "https://registry.yarnpkg.com/use-latest-callback/-/use-latest-callback-0.1.9.tgz#10191dc54257e65a8e52322127643a8940271e2a" - integrity sha512-CL/29uS74AwreI/f2oz2hLTW7ZqVeV5+gxFeGudzQrgkCytrHw33G4KbnQOrRlAEzzAFXi7dDLMC9zhWcVpzmw== - use-latest-callback@^0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/use-latest-callback/-/use-latest-callback-0.2.3.tgz#2d644d3063040b9bc2d4c55bb525a13ae3de9e16" @@ -19426,6 +19438,11 @@ use-sync-external-store@^1.2.2: resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz#c3b6390f3a30eba13200d2302dcdf1e7b57b2ef9" integrity sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw== +use-sync-external-store@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz#55122e2a3edd2a6c106174c27485e0fd59bcfca0" + integrity sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A== + util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" @@ -19527,7 +19544,7 @@ walker@^1.0.7, walker@^1.0.8: dependencies: makeerror "1.0.12" -warn-once@0.1.1, warn-once@^0.1.0: +warn-once@0.1.1, warn-once@^0.1.0, warn-once@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/warn-once/-/warn-once-0.1.1.tgz#952088f4fb56896e73fd4e6a3767272a3fccce43" integrity sha512-VkQZJbO8zVImzYFteBXvBOZEl1qL175WH8VmZcxF2fZAoudNhNDvHi+doCaAEdU2l2vtcIwa2zn0QK5+I1HQ3Q== |