diff options
Diffstat (limited to 'src/view')
-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 |
5 files changed, 137 insertions, 84 deletions
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 () => { |