Reduce <Text> nesting (#6615)

* Move isOnlyEmoji out of RichText

To fix Fast Refresh.

* Make renderChildrenWithEmoji work with any children

* Always go through UITextView for consistency

It already contains the `selectable` and `iOS` checks inside.

* Move `emoji` check into `renderChildrenWithEmoji`

* Remove unnecessary intermediate UITextView nodes

* Make childHasEmoji check recursive

It didn't handle nested arrays etc correctly before.

* Remove the "children must be string" limitation

Should not be necessary now that we correctly handle nested arrays etc.

* Fix unnecessary regex reallocation

This doesn't have a global flag so it's okay to reuse.

* Remove unnecessary <Text> wrapper in RichText
This commit is contained in:
dan
2024-11-21 22:52:16 +00:00
committed by GitHub
parent 84724bb940
commit dc3a42edb1
5 changed files with 52 additions and 90 deletions
+42 -51
View File
@@ -1,10 +1,11 @@
import React from 'react'
import React, {Children} from 'react'
import {TextProps as RNTextProps} from 'react-native'
import {StyleProp, TextStyle} from 'react-native'
import {UITextView} from 'react-native-uitextview'
import createEmojiRegex from 'emoji-regex'
import {isNative} from '#/platform/detection'
import {isIOS} from '#/platform/detection'
import {Alf, applyFonts, atoms, flatten} from '#/alf'
/**
@@ -57,7 +58,7 @@ export function normalizeTextStyles(
}
export type StringChild = string | (string | null)[]
export type TextProps = Omit<RNTextProps, 'children'> & {
export type TextProps = RNTextProps & {
/**
* Lets the user select text, to use the native copy and paste functionality.
*/
@@ -71,65 +72,55 @@ export type TextProps = Omit<RNTextProps, 'children'> & {
* Appears as a small tooltip on web hover.
*/
title?: string
} & (
| {
emoji?: true
children: StringChild
}
| {
emoji?: false
children: RNTextProps['children']
}
)
/**
* Whether the children could possibly contain emoji.
*/
emoji?: boolean
}
const EMOJI = createEmojiRegex()
export function childHasEmoji(children: React.ReactNode) {
return (Array.isArray(children) ? children : [children]).some(
child => typeof child === 'string' && createEmojiRegex().test(child),
)
}
export function childIsString(
children: React.ReactNode,
): children is StringChild {
return (
typeof children === 'string' ||
(Array.isArray(children) &&
children.every(child => typeof child === 'string' || child === null))
)
let hasEmoji = false
Children.forEach(children, child => {
if (typeof child === 'string' && createEmojiRegex().test(child)) {
hasEmoji = true
}
})
return hasEmoji
}
export function renderChildrenWithEmoji(
children: StringChild,
children: React.ReactNode,
props: Omit<TextProps, 'children'> = {},
emoji: boolean,
) {
const normalized = Array.isArray(children) ? children : [children]
if (!isIOS || !emoji) {
return children
}
return Children.map(children, child => {
if (typeof child !== 'string') return child
return (
<UITextView {...props}>
{normalized.map(child => {
if (typeof child !== 'string') return child
const emojis = child.match(EMOJI)
const emojis = child.match(EMOJI)
if (emojis === null) {
return child
}
if (emojis === null) {
return child
}
return child.split(EMOJI).map((stringPart, index) => (
<UITextView key={index} {...props}>
{stringPart}
{emojis[index] ? (
<UITextView
{...props}
style={[props?.style, {color: 'black', fontFamily: 'System'}]}>
{emojis[index]}
</UITextView>
) : null}
</UITextView>
))
})}
</UITextView>
)
return child.split(EMOJI).map((stringPart, index) => [
stringPart,
emojis[index] ? (
<UITextView
{...props}
style={[props?.style, {color: 'black', fontFamily: 'System'}]}>
{emojis[index]}
</UITextView>
) : null,
])
})
}
const SINGLE_EMOJI_RE = /^[\p{Emoji_Presentation}\p{Extended_Pictographic}]+$/u
export function isOnlyEmoji(text: string) {
return text.length <= 15 && SINGLE_EMOJI_RE.test(text)
}