about summary refs log tree commit diff
path: root/src/components/RichText.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/RichText.tsx')
-rw-r--r--src/components/RichText.tsx157
1 files changed, 125 insertions, 32 deletions
diff --git a/src/components/RichText.tsx b/src/components/RichText.tsx
index 068ee99e0..1a14415cf 100644
--- a/src/components/RichText.tsx
+++ b/src/components/RichText.tsx
@@ -1,11 +1,15 @@
 import React from 'react'
 import {RichText as RichTextAPI, AppBskyRichtextFacet} from '@atproto/api'
+import {useLingui} from '@lingui/react'
+import {msg} from '@lingui/macro'
 
-import {atoms as a, TextStyleProp} from '#/alf'
+import {atoms as a, TextStyleProp, flatten, useTheme, web, native} from '#/alf'
 import {InlineLink} from '#/components/Link'
-import {Text} from '#/components/Typography'
+import {Text, TextProps} from '#/components/Typography'
 import {toShortUrl} from 'lib/strings/url-helpers'
-import {getAgent} from '#/state/session'
+import {TagMenu, useTagMenuControl} from '#/components/TagMenu'
+import {isNative} from '#/platform/detection'
+import {useInteractionState} from '#/components/hooks/useInteractionState'
 
 const WORD_WRAP = {wordWrap: 1}
 
@@ -15,34 +19,24 @@ export function RichText({
   style,
   numberOfLines,
   disableLinks,
-  resolveFacets = false,
-}: TextStyleProp & {
-  value: RichTextAPI | string
-  testID?: string
-  numberOfLines?: number
-  disableLinks?: boolean
-  resolveFacets?: boolean
-}) {
-  const detected = React.useRef(false)
-  const [richText, setRichText] = React.useState<RichTextAPI>(() =>
-    value instanceof RichTextAPI ? value : new RichTextAPI({text: value}),
+  selectable,
+  enableTags = false,
+  authorHandle,
+}: TextStyleProp &
+  Pick<TextProps, 'selectable'> & {
+    value: RichTextAPI | string
+    testID?: string
+    numberOfLines?: number
+    disableLinks?: boolean
+    enableTags?: boolean
+    authorHandle?: string
+  }) {
+  const richText = React.useMemo(
+    () =>
+      value instanceof RichTextAPI ? value : new RichTextAPI({text: value}),
+    [value],
   )
-  const styles = [a.leading_normal, style]
-
-  React.useEffect(() => {
-    if (!resolveFacets) return
-
-    async function detectFacets() {
-      const rt = new RichTextAPI({text: richText.text})
-      await rt.detectFacets(getAgent())
-      setRichText(rt)
-    }
-
-    if (!detected.current) {
-      detected.current = true
-      detectFacets()
-    }
-  }, [richText, setRichText, resolveFacets])
+  const styles = [a.leading_snug, flatten(style)]
 
   const {text, facets} = richText
 
@@ -50,6 +44,7 @@ export function RichText({
     if (text.length <= 5 && /^\p{Extended_Pictographic}+$/u.test(text)) {
       return (
         <Text
+          selectable={selectable}
           testID={testID}
           style={[
             {
@@ -65,6 +60,7 @@ export function RichText({
     }
     return (
       <Text
+        selectable={selectable}
         testID={testID}
         style={styles}
         numberOfLines={numberOfLines}
@@ -81,6 +77,7 @@ export function RichText({
   for (const segment of richText.segments()) {
     const link = segment.link
     const mention = segment.mention
+    const tag = segment.tag
     if (
       mention &&
       AppBskyRichtextFacet.validateMention(mention).success &&
@@ -88,6 +85,7 @@ export function RichText({
     ) {
       els.push(
         <InlineLink
+          selectable={selectable}
           key={key}
           to={`/profile/${mention.did}`}
           style={[...styles, {pointerEvents: 'auto'}]}
@@ -102,16 +100,32 @@ export function RichText({
       } else {
         els.push(
           <InlineLink
+            selectable={selectable}
             key={key}
             to={link.uri}
             style={[...styles, {pointerEvents: 'auto'}]}
             // @ts-ignore TODO
-            dataSet={WORD_WRAP}
-            warnOnMismatchingLabel>
+            dataSet={WORD_WRAP}>
             {toShortUrl(segment.text)}
           </InlineLink>,
         )
       }
+    } else if (
+      !disableLinks &&
+      enableTags &&
+      tag &&
+      AppBskyRichtextFacet.validateTag(tag).success
+    ) {
+      els.push(
+        <RichTextTag
+          key={key}
+          text={segment.text}
+          tag={tag.tag}
+          style={styles}
+          selectable={selectable}
+          authorHandle={authorHandle}
+        />,
+      )
     } else {
       els.push(segment.text)
     }
@@ -120,6 +134,7 @@ export function RichText({
 
   return (
     <Text
+      selectable={selectable}
       testID={testID}
       style={styles}
       numberOfLines={numberOfLines}
@@ -129,3 +144,81 @@ export function RichText({
     </Text>
   )
 }
+
+function RichTextTag({
+  text,
+  tag,
+  style,
+  selectable,
+  authorHandle,
+}: {
+  text: string
+  tag: string
+  selectable?: boolean
+  authorHandle?: string
+} & TextStyleProp) {
+  const t = useTheme()
+  const {_} = useLingui()
+  const control = useTagMenuControl()
+  const {
+    state: hovered,
+    onIn: onHoverIn,
+    onOut: onHoverOut,
+  } = useInteractionState()
+  const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
+  const {
+    state: pressed,
+    onIn: onPressIn,
+    onOut: onPressOut,
+  } = useInteractionState()
+
+  const open = React.useCallback(() => {
+    control.open()
+  }, [control])
+
+  /*
+   * N.B. On web, this is wrapped in another pressable comopnent with a11y
+   * labels, etc. That's why only some of these props are applied here.
+   */
+
+  return (
+    <React.Fragment>
+      <TagMenu control={control} tag={tag} authorHandle={authorHandle}>
+        <Text
+          selectable={selectable}
+          {...native({
+            accessibilityLabel: _(msg`Hashtag: #${tag}`),
+            accessibilityHint: _(msg`Click here to open tag menu for #${tag}`),
+            accessibilityRole: isNative ? 'button' : undefined,
+            onPress: open,
+            onPressIn: onPressIn,
+            onPressOut: onPressOut,
+          })}
+          {...web({
+            onMouseEnter: onHoverIn,
+            onMouseLeave: onHoverOut,
+          })}
+          // @ts-ignore
+          onFocus={onFocus}
+          onBlur={onBlur}
+          style={[
+            style,
+            {
+              pointerEvents: 'auto',
+              color: t.palette.primary_500,
+            },
+            web({
+              cursor: 'pointer',
+            }),
+            (hovered || focused || pressed) && {
+              ...web({outline: 0}),
+              textDecorationLine: 'underline',
+              textDecorationColor: t.palette.primary_500,
+            },
+          ]}>
+          {text}
+        </Text>
+      </TagMenu>
+    </React.Fragment>
+  )
+}