about summary refs log tree commit diff
path: root/src/view/com/util/Link.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/com/util/Link.tsx')
-rw-r--r--src/view/com/util/Link.tsx169
1 files changed, 137 insertions, 32 deletions
diff --git a/src/view/com/util/Link.tsx b/src/view/com/util/Link.tsx
index bdc447937..cee4d4136 100644
--- a/src/view/com/util/Link.tsx
+++ b/src/view/com/util/Link.tsx
@@ -2,6 +2,8 @@ import React from 'react'
 import {observer} from 'mobx-react-lite'
 import {
   Linking,
+  GestureResponderEvent,
+  Platform,
   StyleProp,
   TouchableWithoutFeedback,
   TouchableOpacity,
@@ -9,10 +11,22 @@ import {
   View,
   ViewStyle,
 } from 'react-native'
+import {
+  useLinkProps,
+  useNavigation,
+  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 {useStores, RootStoreModel} from 'state/index'
 import {convertBskyAppUrlIfNeeded} from 'lib/strings/url-helpers'
+import {isDesktopWeb} from 'platform/detection'
+
+type Event =
+  | React.MouseEvent<HTMLAnchorElement, MouseEvent>
+  | GestureResponderEvent
 
 export const Link = observer(function Link({
   style,
@@ -20,30 +34,33 @@ export const Link = observer(function Link({
   title,
   children,
   noFeedback,
+  asAnchor,
 }: {
   style?: StyleProp<ViewStyle>
   href?: string
   title?: string
   children?: React.ReactNode
   noFeedback?: boolean
+  asAnchor?: boolean
 }) {
   const store = useStores()
-  const onPress = () => {
-    if (href) {
-      handleLink(store, href, false)
-    }
-  }
-  const onLongPress = () => {
-    if (href) {
-      handleLink(store, href, true)
-    }
-  }
+  const navigation = useNavigation<NavigationProp>()
+
+  const onPress = React.useCallback(
+    (e?: Event) => {
+      if (typeof href === 'string') {
+        return onPressInner(store, navigation, href, e)
+      }
+    },
+    [store, navigation, href],
+  )
+
   if (noFeedback) {
     return (
       <TouchableWithoutFeedback
         onPress={onPress}
-        onLongPress={onLongPress}
-        delayPressIn={50}>
+        // @ts-ignore web only -prf
+        href={asAnchor ? href : undefined}>
         <View style={style}>
           {children ? children : <Text>{title || 'link'}</Text>}
         </View>
@@ -52,10 +69,10 @@ export const Link = observer(function Link({
   }
   return (
     <TouchableOpacity
+      style={style}
       onPress={onPress}
-      onLongPress={onLongPress}
-      delayPressIn={50}
-      style={style}>
+      // @ts-ignore web only -prf
+      href={asAnchor ? href : undefined}>
       {children ? children : <Text>{title || 'link'}</Text>}
     </TouchableOpacity>
   )
@@ -66,35 +83,123 @@ export const TextLink = observer(function TextLink({
   style,
   href,
   text,
+  numberOfLines,
+  lineHeight,
 }: {
   type?: TypographyVariant
   style?: StyleProp<TextStyle>
   href: string
-  text: string
+  text: string | JSX.Element
+  numberOfLines?: number
+  lineHeight?: number
 }) {
+  const {...props} = useLinkProps({to: href})
   const store = useStores()
-  const onPress = () => {
-    handleLink(store, href, false)
-  }
-  const onLongPress = () => {
-    handleLink(store, href, true)
+  const navigation = useNavigation<NavigationProp>()
+
+  props.onPress = React.useCallback(
+    (e?: Event) => {
+      return onPressInner(store, navigation, href, e)
+    },
+    [store, navigation, href],
+  )
+
+  return (
+    <Text
+      type={type}
+      style={style}
+      numberOfLines={numberOfLines}
+      lineHeight={lineHeight}
+      {...props}>
+      {text}
+    </Text>
+  )
+})
+
+/**
+ * Only acts as a link on desktop web
+ */
+export const DesktopWebTextLink = observer(function DesktopWebTextLink({
+  type = 'md',
+  style,
+  href,
+  text,
+  numberOfLines,
+  lineHeight,
+}: {
+  type?: TypographyVariant
+  style?: StyleProp<TextStyle>
+  href: string
+  text: string | JSX.Element
+  numberOfLines?: number
+  lineHeight?: number
+}) {
+  if (isDesktopWeb) {
+    return (
+      <TextLink
+        type={type}
+        style={style}
+        href={href}
+        text={text}
+        numberOfLines={numberOfLines}
+        lineHeight={lineHeight}
+      />
+    )
   }
   return (
-    <Text type={type} style={style} onPress={onPress} onLongPress={onLongPress}>
+    <Text
+      type={type}
+      style={style}
+      numberOfLines={numberOfLines}
+      lineHeight={lineHeight}>
       {text}
     </Text>
   )
 })
 
-function handleLink(store: RootStoreModel, href: string, longPress: boolean) {
-  href = convertBskyAppUrlIfNeeded(href)
-  if (href.startsWith('http')) {
-    Linking.openURL(href)
-  } else if (longPress) {
-    store.shell.closeModal() // close any active modals
-    store.nav.newTab(href)
-  } else {
-    store.shell.closeModal() // close any active modals
-    store.nav.navigate(href)
+// NOTE
+// we can't use the onPress given by useLinkProps because it will
+// match most paths to the HomeTab routes while we actually want to
+// preserve the tab the app is currently in
+//
+// we also have some additional behaviors - closing the current modal,
+// converting bsky urls, and opening http/s links in the system browser
+//
+// this method copies from the onPress implementation but adds our
+// needed customizations
+// -prf
+function onPressInner(
+  store: RootStoreModel,
+  navigation: NavigationProp,
+  href: string,
+  e?: Event,
+) {
+  let shouldHandle = false
+
+  if (Platform.OS !== 'web' || !e) {
+    shouldHandle = e ? !e.defaultPrevented : true
+  } else if (
+    !e.defaultPrevented && // onPress prevented default
+    // @ts-ignore Web only -prf
+    !(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) && // ignore clicks with modifier keys
+    // @ts-ignore Web only -prf
+    (e.button == null || e.button === 0) && // ignore everything but left clicks
+    // @ts-ignore Web only -prf
+    [undefined, null, '', 'self'].includes(e.currentTarget?.target) // let browser handle "target=_blank" etc.
+  ) {
+    e.preventDefault()
+    shouldHandle = true
+  }
+
+  if (shouldHandle) {
+    href = convertBskyAppUrlIfNeeded(href)
+    if (href.startsWith('http')) {
+      Linking.openURL(href)
+    } else {
+      store.shell.closeModal() // close any active modals
+
+      // @ts-ignore we're not able to type check on this one -prf
+      navigation.dispatch(StackActions.push(...router.matchPath(href)))
+    }
   }
 }