about summary refs log tree commit diff
path: root/src/components/RichText.tsx
blob: c72fcabdd8add89857888660bfd18f13893135d7 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
import React from 'react'
import {RichText as RichTextAPI, AppBskyRichtextFacet} from '@atproto/api'

import {atoms as a, TextStyleProp, flatten} 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'

const WORD_WRAP = {wordWrap: 1}

export function RichText({
  testID,
  value,
  style,
  numberOfLines,
  disableLinks,
  resolveFacets = false,
  selectable,
}: TextStyleProp &
  Pick<TextProps, 'selectable'> & {
    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}),
  )
  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) {
    if (text.length <= 5 && /^\p{Extended_Pictographic}+$/u.test(text)) {
      return (
        <Text
          selectable={selectable}
          testID={testID}
          style={[
            {
              fontSize: 26,
              lineHeight: 30,
            },
          ]}
          // @ts-ignore web only -prf
          dataSet={WORD_WRAP}>
          {text}
        </Text>
      )
    }
    return (
      <Text
        selectable={selectable}
        testID={testID}
        style={styles}
        numberOfLines={numberOfLines}
        // @ts-ignore web only -prf
        dataSet={WORD_WRAP}>
        {text}
      </Text>
    )
  }

  const els = []
  let key = 0
  // N.B. must access segments via `richText.segments`, not via destructuring
  for (const segment of richText.segments()) {
    const link = segment.link
    const mention = segment.mention
    if (
      mention &&
      AppBskyRichtextFacet.validateMention(mention).success &&
      !disableLinks
    ) {
      els.push(
        <InlineLink
          selectable={selectable}
          key={key}
          to={`/profile/${mention.did}`}
          style={[...styles, {pointerEvents: 'auto'}]}
          // @ts-ignore TODO
          dataSet={WORD_WRAP}>
          {segment.text}
        </InlineLink>,
      )
    } else if (link && AppBskyRichtextFacet.validateLink(link).success) {
      if (disableLinks) {
        els.push(toShortUrl(segment.text))
      } else {
        els.push(
          <InlineLink
            selectable={selectable}
            key={key}
            to={link.uri}
            style={[...styles, {pointerEvents: 'auto'}]}
            // @ts-ignore TODO
            dataSet={WORD_WRAP}
            warnOnMismatchingLabel>
            {toShortUrl(segment.text)}
          </InlineLink>,
        )
      }
    } else {
      els.push(segment.text)
    }
    key++
  }

  return (
    <Text
      selectable={selectable}
      testID={testID}
      style={styles}
      numberOfLines={numberOfLines}
      // @ts-ignore web only -prf
      dataSet={WORD_WRAP}>
      {els}
    </Text>
  )
}