about summary refs log tree commit diff
path: root/src/view/com/util/RichText.tsx
blob: 8b4e6a50a64a912632ba616857172ef397d6681d (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
import React from 'react'
import {Text, TextStyle, StyleProp} from 'react-native'
import {TextLink} from './Link'
import {s} from '../../lib/styles'
import {toShortUrl} from '../../../lib/strings'

type TextSlice = {start: number; end: number}
type Entity = {
  index: TextSlice
  type: string
  value: string
}

export function RichText({
  text,
  entities,
  style,
  numberOfLines,
}: {
  text: string
  entities?: Entity[]
  style?: StyleProp<TextStyle>
  numberOfLines?: number
}) {
  if (!entities?.length) {
    return <Text style={style}>{text}</Text>
  }
  if (!style) style = []
  else if (!Array.isArray(style)) style = [style]
  entities.sort(sortByIndex)
  const segments = Array.from(toSegments(text, entities))
  const els = []
  let key = 0
  for (const segment of segments) {
    if (typeof segment === 'string') {
      els.push(segment)
    } else {
      if (segment.entity.type === 'mention') {
        els.push(
          <TextLink
            key={key}
            text={segment.text}
            href={`/profile/${segment.entity.value}`}
            style={[style, s.blue3]}
          />,
        )
      } else if (segment.entity.type === 'link') {
        els.push(
          <TextLink
            key={key}
            text={toShortUrl(segment.text)}
            href={segment.entity.value}
            style={[style, s.blue3]}
          />,
        )
      }
    }
    key++
  }
  return (
    <Text style={style} numberOfLines={numberOfLines}>
      {els}
    </Text>
  )
}

function sortByIndex(a: Entity, b: Entity) {
  return a.index.start - b.index.start
}

function* toSegments(text: string, entities: Entity[]) {
  let cursor = 0
  let i = 0
  do {
    let currEnt = entities[i]
    if (cursor < currEnt.index.start) {
      yield text.slice(cursor, currEnt.index.start)
    } else if (cursor > currEnt.index.start) {
      i++
      continue
    }
    if (currEnt.index.start < currEnt.index.end) {
      let subtext = text.slice(currEnt.index.start, currEnt.index.end)
      if (
        !subtext.trim() ||
        (currEnt.type === 'mention' &&
          stripUsername(subtext) !== stripUsername(currEnt.value)) ||
        (currEnt.type === 'link' && !isSameLink(subtext, currEnt.value))
      ) {
        // dont yield links to empty strings or strings that don't match the entity value
        yield subtext
      } else {
        yield {
          entity: currEnt,
          text: subtext,
        }
      }
    }
    cursor = currEnt.index.end
    i++
  } while (i < entities.length)
  if (cursor < text.length) {
    yield text.slice(cursor, text.length)
  }
}

function stripUsername(v: string): string {
  return v.trim().replace('@', '')
}

function isSameLink(a: string, b: string) {
  a = a.startsWith('http') ? a : `https://${a}`
  b = b.startsWith('http') ? b : `https://${b}`
  return a === b
}