forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {Children} from 'react'
2import {
3 type StyleProp,
4 type TextProps as RNTextProps,
5 type TextStyle,
6} from 'react-native'
7import {UITextView} from 'react-native-uitextview'
8import createEmojiRegex from 'emoji-regex'
9
10import {type Alf, applyFonts, atoms, flatten} from '#/alf'
11import {IS_IOS, IS_NATIVE} from '#/env'
12
13/**
14 * Ensures that `lineHeight` defaults to a relative value of `1`, or applies
15 * other relative leading atoms.
16 *
17 * If the `lineHeight` value is > 2, we assume it's an absolute value and
18 * returns it as-is.
19 */
20export function normalizeTextStyles(
21 styles: StyleProp<TextStyle>,
22 {
23 fontScale,
24 fontFamily,
25 }: {
26 fontScale: number
27 fontFamily: Alf['fonts']['family']
28 } & Pick<Alf, 'flags'>,
29) {
30 const s = flatten(styles) ?? {}
31
32 // should always be defined on these components
33 s.fontSize = (s.fontSize || atoms.text_md.fontSize) * fontScale
34
35 if (s?.lineHeight) {
36 if (s.lineHeight !== 0 && s.lineHeight <= 2) {
37 s.lineHeight = Math.round(s.fontSize * s.lineHeight)
38 }
39 } else if (!IS_NATIVE) {
40 s.lineHeight = s.fontSize
41 }
42
43 applyFonts(s, fontFamily)
44
45 return s
46}
47
48export type StringChild = string | (string | null)[]
49export type TextProps = RNTextProps & {
50 /**
51 * Lets the user select text, to use the native copy and paste functionality.
52 */
53 selectable?: boolean
54 /**
55 * Provides `data-*` attributes to the underlying `UITextView` component on
56 * web only.
57 */
58 dataSet?: Record<string, string | number | undefined>
59 /**
60 * Appears as a small tooltip on web hover.
61 */
62 title?: string
63 /**
64 * Whether the children could possibly contain emoji.
65 */
66 emoji?: boolean
67}
68
69const EMOJI = createEmojiRegex()
70
71export function childHasEmoji(children: React.ReactNode) {
72 let hasEmoji = false
73 Children.forEach(children, child => {
74 if (typeof child === 'string' && createEmojiRegex().test(child)) {
75 hasEmoji = true
76 }
77 })
78 return hasEmoji
79}
80
81export function renderChildrenWithEmoji(
82 children: React.ReactNode,
83 props: Omit<TextProps, 'children'> = {},
84 emoji: boolean,
85) {
86 if (!IS_IOS || !emoji) {
87 return children
88 }
89 return Children.map(children, child => {
90 if (typeof child !== 'string') return child
91
92 const emojis = child.match(EMOJI)
93
94 if (emojis === null) {
95 return child
96 }
97
98 return child.split(EMOJI).map((stringPart, index) => [
99 stringPart,
100 emojis[index] ? (
101 <UITextView
102 {...props}
103 style={[props?.style, {fontFamily: 'System'}]}
104 key={index}>
105 {emojis[index]}
106 </UITextView>
107 ) : null,
108 ])
109 })
110}
111
112const SINGLE_EMOJI_RE =
113 /^[\p{Emoji_Presentation}\p{Extended_Pictographic}\uFE0F\u200D]+$/u
114export function isOnlyEmoji(text: string) {
115 return text.length <= 15 && SINGLE_EMOJI_RE.test(text)
116}