about summary refs log tree commit diff
diff options
context:
space:
mode:
authorHailey <me@haileyok.com>2024-03-12 09:46:25 -0700
committerGitHub <noreply@github.com>2024-03-12 09:46:25 -0700
commitee57d74765e644de49c02de1e817eee0c6ed81aa (patch)
tree06945ebe5e8f856981cbfb25c81bdfcee807e43b
parentb8afb935f4eafbe64e83512ec5a97bb7b38a6ecb (diff)
downloadvoidsky-ee57d74765e644de49c02de1e817eee0c6ed81aa.tar.zst
Dedupe navigation events (push, navigate, pop, etc) (#3179)
-rw-r--r--src/components/Link.tsx11
-rw-r--r--src/components/Lists.tsx5
-rw-r--r--src/lib/hooks/useDedupe.ts17
-rw-r--r--src/lib/hooks/useNavigationDeduped.ts80
-rw-r--r--src/view/com/feeds/FeedSourceCard.tsx5
-rw-r--r--src/view/com/util/Link.tsx17
-rw-r--r--src/view/shell/bottom-bar/BottomBar.tsx8
7 files changed, 118 insertions, 25 deletions
diff --git a/src/components/Link.tsx b/src/components/Link.tsx
index 8c963909b..ff72a08ce 100644
--- a/src/components/Link.tsx
+++ b/src/components/Link.tsx
@@ -1,17 +1,13 @@
 import React from 'react'
 import {GestureResponderEvent} from 'react-native'
-import {
-  useLinkProps,
-  useNavigation,
-  StackActions,
-} from '@react-navigation/native'
+import {useLinkProps, StackActions} from '@react-navigation/native'
 import {sanitizeUrl} from '@braintree/sanitize-url'
 
 import {useInteractionState} from '#/components/hooks/useInteractionState'
 import {isWeb} from '#/platform/detection'
 import {useTheme, web, flatten, TextStyleProp, atoms as a} from '#/alf'
 import {Button, ButtonProps} from '#/components/Button'
-import {AllNavigatorParams, NavigationProp} from '#/lib/routes/types'
+import {AllNavigatorParams} from '#/lib/routes/types'
 import {
   convertBskyAppUrlIfNeeded,
   isExternalUrl,
@@ -21,6 +17,7 @@ import {useModalControls} from '#/state/modals'
 import {router} from '#/routes'
 import {Text, TextProps} from '#/components/Typography'
 import {useOpenLink} from 'state/preferences/in-app-browser'
+import {useNavigationDeduped} from 'lib/hooks/useNavigationDeduped'
 
 /**
  * Only available within a `Link`, since that inherits from `Button`.
@@ -74,7 +71,7 @@ export function useLink({
 }: BaseLinkProps & {
   displayText: string
 }) {
-  const navigation = useNavigation<NavigationProp>()
+  const navigation = useNavigationDeduped()
   const {href} = useLinkProps<AllNavigatorParams>({
     to:
       typeof to === 'string' ? convertBskyAppUrlIfNeeded(sanitizeUrl(to)) : to,
diff --git a/src/components/Lists.tsx b/src/components/Lists.tsx
index 58aa74b38..bb0d24797 100644
--- a/src/components/Lists.tsx
+++ b/src/components/Lists.tsx
@@ -8,9 +8,8 @@ import {cleanError} from 'lib/strings/errors'
 import {Button} from '#/components/Button'
 import {Text} from '#/components/Typography'
 import {StackActions} from '@react-navigation/native'
-import {useNavigation} from '@react-navigation/core'
-import {NavigationProp} from 'lib/routes/types'
 import {router} from '#/routes'
+import {useNavigationDeduped} from 'lib/hooks/useNavigationDeduped'
 
 export function ListFooter({
   isFetching,
@@ -142,7 +141,7 @@ export function ListMaybePlaceholder({
   notFoundType?: 'page' | 'results'
   onRetry?: () => Promise<unknown>
 }) {
-  const navigation = useNavigation<NavigationProp>()
+  const navigation = useNavigationDeduped()
   const t = useTheme()
   const {gtMobile, gtTablet} = useBreakpoints()
 
diff --git a/src/lib/hooks/useDedupe.ts b/src/lib/hooks/useDedupe.ts
new file mode 100644
index 000000000..d9432cb2c
--- /dev/null
+++ b/src/lib/hooks/useDedupe.ts
@@ -0,0 +1,17 @@
+import React from 'react'
+
+export const useDedupe = () => {
+  const canDo = React.useRef(true)
+
+  return React.useRef((cb: () => unknown) => {
+    if (canDo.current) {
+      canDo.current = false
+      setTimeout(() => {
+        canDo.current = true
+      }, 250)
+      cb()
+      return true
+    }
+    return false
+  }).current
+}
diff --git a/src/lib/hooks/useNavigationDeduped.ts b/src/lib/hooks/useNavigationDeduped.ts
new file mode 100644
index 000000000..d913f7f3d
--- /dev/null
+++ b/src/lib/hooks/useNavigationDeduped.ts
@@ -0,0 +1,80 @@
+import React from 'react'
+import {useNavigation} from '@react-navigation/core'
+import {AllNavigatorParams, NavigationProp} from 'lib/routes/types'
+import type {NavigationAction} from '@react-navigation/routers'
+import {NavigationState} from '@react-navigation/native'
+import {useDedupe} from 'lib/hooks/useDedupe'
+
+export type DebouncedNavigationProp = Pick<
+  NavigationProp,
+  | 'popToTop'
+  | 'push'
+  | 'navigate'
+  | 'canGoBack'
+  | 'replace'
+  | 'dispatch'
+  | 'goBack'
+  | 'getState'
+>
+
+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]]
+      ) => {
+        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
+      ) => {
+        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]]
+      ) => {
+        dedupe(() => navigation.replace(...args))
+      },
+      dispatch: (
+        action:
+          | NavigationAction
+          | ((state: NavigationState) => NavigationAction),
+      ) => {
+        dedupe(() => navigation.dispatch(action))
+      },
+      popToTop: () => {
+        dedupe(() => navigation.popToTop())
+      },
+      goBack: () => {
+        dedupe(() => navigation.goBack())
+      },
+      canGoBack: () => {
+        return navigation.canGoBack()
+      },
+      getState: () => {
+        return navigation.getState()
+      },
+    }),
+    [dedupe, navigation],
+  )
+}
diff --git a/src/view/com/feeds/FeedSourceCard.tsx b/src/view/com/feeds/FeedSourceCard.tsx
index 9bd7238df..4af62f6f6 100644
--- a/src/view/com/feeds/FeedSourceCard.tsx
+++ b/src/view/com/feeds/FeedSourceCard.tsx
@@ -6,8 +6,6 @@ import {RichText} from '#/components/RichText'
 import {usePalette} from 'lib/hooks/usePalette'
 import {s} from 'lib/styles'
 import {UserAvatar} from '../util/UserAvatar'
-import {useNavigation} from '@react-navigation/native'
-import {NavigationProp} from 'lib/routes/types'
 import {pluralize} from 'lib/strings/helpers'
 import {AtUri} from '@atproto/api'
 import * as Toast from 'view/com/util/Toast'
@@ -26,6 +24,7 @@ import {
 import {useFeedSourceInfoQuery, FeedSourceInfo} from '#/state/queries/feed'
 import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
 import {useTheme} from '#/alf'
+import {useNavigationDeduped} from 'lib/hooks/useNavigationDeduped'
 
 export function FeedSourceCard({
   feedUri,
@@ -86,7 +85,7 @@ export function FeedSourceCardLoaded({
   const t = useTheme()
   const pal = usePalette('default')
   const {_} = useLingui()
-  const navigation = useNavigation<NavigationProp>()
+  const navigation = useNavigationDeduped()
   const {openModal} = useModalControls()
 
   const {isPending: isSavePending, mutateAsync: saveFeed} =
diff --git a/src/view/com/util/Link.tsx b/src/view/com/util/Link.tsx
index e50fb7f09..f45622488 100644
--- a/src/view/com/util/Link.tsx
+++ b/src/view/com/util/Link.tsx
@@ -11,14 +11,9 @@ import {
   TouchableWithoutFeedback,
   TouchableOpacity,
 } from 'react-native'
-import {
-  useLinkProps,
-  useNavigation,
-  StackActions,
-} from '@react-navigation/native'
+import {useLinkProps, StackActions} from '@react-navigation/native'
 import {Text} from './text/Text'
 import {TypographyVariant} from 'lib/ThemeContext'
-import {NavigationProp} from 'lib/routes/types'
 import {router} from '../../../routes'
 import {
   convertBskyAppUrlIfNeeded,
@@ -32,6 +27,10 @@ import FixedTouchableHighlight from '../pager/FixedTouchableHighlight'
 import {useModalControls} from '#/state/modals'
 import {useOpenLink} from '#/state/preferences/in-app-browser'
 import {WebAuxClickWrapper} from 'view/com/util/WebAuxClickWrapper'
+import {
+  DebouncedNavigationProp,
+  useNavigationDeduped,
+} from 'lib/hooks/useNavigationDeduped'
 
 type Event =
   | React.MouseEvent<HTMLAnchorElement, MouseEvent>
@@ -65,7 +64,7 @@ export const Link = memo(function Link({
   ...props
 }: Props) {
   const {closeModal} = useModalControls()
-  const navigation = useNavigation<NavigationProp>()
+  const navigation = useNavigationDeduped()
   const anchorHref = asAnchor ? sanitizeUrl(href) : undefined
   const openLink = useOpenLink()
 
@@ -176,7 +175,7 @@ export const TextLink = memo(function TextLink({
   navigationAction?: 'push' | 'replace' | 'navigate'
 } & TextProps) {
   const {...props} = useLinkProps({to: sanitizeUrl(href)})
-  const navigation = useNavigation<NavigationProp>()
+  const navigation = useNavigationDeduped()
   const {openModal, closeModal} = useModalControls()
   const openLink = useOpenLink()
 
@@ -335,7 +334,7 @@ const EXEMPT_PATHS = ['/robots.txt', '/security.txt', '/.well-known/']
 // -prf
 function onPressInner(
   closeModal = () => {},
-  navigation: NavigationProp,
+  navigation: DebouncedNavigationProp,
   href: string,
   navigationAction: 'push' | 'replace' | 'navigate' = 'push',
   openLink: (href: string) => void,
diff --git a/src/view/shell/bottom-bar/BottomBar.tsx b/src/view/shell/bottom-bar/BottomBar.tsx
index 1ab3334fa..115faa296 100644
--- a/src/view/shell/bottom-bar/BottomBar.tsx
+++ b/src/view/shell/bottom-bar/BottomBar.tsx
@@ -36,6 +36,7 @@ import {Button} from '#/view/com/util/forms/Button'
 import {s} from 'lib/styles'
 import {Logo} from '#/view/icons/Logo'
 import {Logotype} from '#/view/icons/Logotype'
+import {useDedupe} from 'lib/hooks/useDedupe'
 
 type TabOptions = 'Home' | 'Search' | 'Notifications' | 'MyProfile' | 'Feeds'
 
@@ -54,6 +55,7 @@ export function BottomBar({navigation}: BottomTabBarProps) {
   const {data: profile} = useProfileQuery({did: currentAccount?.did})
   const {requestSwitchToAccount} = useLoggedOutViewControls()
   const closeAllActiveElements = useCloseAllActiveElements()
+  const dedupe = useDedupe()
 
   const showSignIn = React.useCallback(() => {
     closeAllActiveElements()
@@ -74,12 +76,12 @@ export function BottomBar({navigation}: BottomTabBarProps) {
       if (tabState === TabState.InsideAtRoot) {
         emitSoftReset()
       } else if (tabState === TabState.Inside) {
-        navigation.dispatch(StackActions.popToTop())
+        dedupe(() => navigation.dispatch(StackActions.popToTop()))
       } else {
-        navigation.navigate(`${tab}Tab`)
+        dedupe(() => navigation.navigate(`${tab}Tab`))
       }
     },
-    [track, navigation],
+    [track, navigation, dedupe],
   )
   const onPressHome = React.useCallback(() => onPressTab('Home'), [onPressTab])
   const onPressSearch = React.useCallback(