about summary refs log tree commit diff
path: root/src/components/RichText.tsx
diff options
context:
space:
mode:
authorJan-Olof Eriksson <jan-olof.eriksson@iki.fi>2024-02-29 11:55:03 +0200
committerGitHub <noreply@github.com>2024-02-29 11:55:03 +0200
commit963a44ab872a1044d6997a8fcf7b2fc754ac618a (patch)
treebbd64f464a8f14e55cbb06e28811cdc43f059d29 /src/components/RichText.tsx
parent1f9562847512bb41cd8bb381b735a388be4db59b (diff)
parenta35976cdc9b6467ad8b6e0c4ff46ba684fee9064 (diff)
downloadvoidsky-963a44ab872a1044d6997a8fcf7b2fc754ac618a.tar.zst
Merge branch 'bluesky-social:main' into main
Diffstat (limited to 'src/components/RichText.tsx')
-rw-r--r--src/components/RichText.tsx128
1 files changed, 106 insertions, 22 deletions
diff --git a/src/components/RichText.tsx b/src/components/RichText.tsx
index c72fcabdd..3d5f08026 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, flatten} from '#/alf'
+import {atoms as a, TextStyleProp, flatten, useTheme, web, native} from '#/alf'
 import {InlineLink} from '#/components/Link'
 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,37 +19,25 @@ export function RichText({
   style,
   numberOfLines,
   disableLinks,
-  resolveFacets = false,
   selectable,
+  enableTags = false,
+  authorHandle,
 }: TextStyleProp &
   Pick<TextProps, 'selectable'> & {
     value: RichTextAPI | string
     testID?: string
     numberOfLines?: number
     disableLinks?: boolean
-    resolveFacets?: boolean
+    enableTags?: boolean
+    authorHandle?: string
   }) {
-  const detected = React.useRef(false)
-  const [richText, setRichText] = React.useState<RichTextAPI>(() =>
-    value instanceof RichTextAPI ? value : new RichTextAPI({text: value}),
+  const richText = React.useMemo(
+    () =>
+      value instanceof RichTextAPI ? value : new RichTextAPI({text: value}),
+    [value],
   )
   const styles = [a.leading_snug, flatten(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 {text, facets} = richText
 
   if (!facets?.length) {
@@ -85,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 &&
@@ -118,6 +111,21 @@ export function RichText({
           </InlineLink>,
         )
       }
+    } else if (
+      !disableLinks &&
+      enableTags &&
+      tag &&
+      AppBskyRichtextFacet.validateTag(tag).success
+    ) {
+      els.push(
+        <RichTextTag
+          key={key}
+          text={segment.text}
+          style={styles}
+          selectable={selectable}
+          authorHandle={authorHandle}
+        />,
+      )
     } else {
       els.push(segment.text)
     }
@@ -136,3 +144,79 @@ export function RichText({
     </Text>
   )
 }
+
+function RichTextTag({
+  text: tag,
+  style,
+  selectable,
+  authorHandle,
+}: {
+  text: 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,
+            },
+          ]}>
+          {tag}
+        </Text>
+      </TagMenu>
+    </React.Fragment>
+  )
+}