Update react-navigation (#5967)
This commit is contained in:
+44
-39
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user