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