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 {TextProps as RNTextProps} from 'react-native'
import {StyleProp, TextStyle} from 'react-native' import {StyleProp, TextStyle} from 'react-native'
import {UITextView} from 'react-native-uitextview' import {UITextView} from 'react-native-uitextview'
import createEmojiRegex from 'emoji-regex' import createEmojiRegex from 'emoji-regex'
import {isNative} from '#/platform/detection' import {isNative} from '#/platform/detection'
import {isIOS} from '#/platform/detection'
import {Alf, applyFonts, atoms, flatten} from '#/alf' import {Alf, applyFonts, atoms, flatten} from '#/alf'
/** /**
@@ -57,7 +58,7 @@ export function normalizeTextStyles(
} }
export type StringChild = string | (string | null)[] 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. * 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. * Appears as a small tooltip on web hover.
*/ */
title?: string title?: string
} & ( /**
| { * Whether the children could possibly contain emoji.
emoji?: true */
children: StringChild emoji?: boolean
} }
| {
emoji?: false
children: RNTextProps['children']
}
)
const EMOJI = createEmojiRegex() const EMOJI = createEmojiRegex()
export function childHasEmoji(children: React.ReactNode) { export function childHasEmoji(children: React.ReactNode) {
return (Array.isArray(children) ? children : [children]).some( let hasEmoji = false
child => typeof child === 'string' && createEmojiRegex().test(child), Children.forEach(children, child => {
) if (typeof child === 'string' && createEmojiRegex().test(child)) {
} hasEmoji = true
}
export function childIsString( })
children: React.ReactNode, return hasEmoji
): children is StringChild {
return (
typeof children === 'string' ||
(Array.isArray(children) &&
children.every(child => typeof child === 'string' || child === null))
)
} }
export function renderChildrenWithEmoji( export function renderChildrenWithEmoji(
children: StringChild, children: React.ReactNode,
props: Omit<TextProps, 'children'> = {}, 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 ( const emojis = child.match(EMOJI)
<UITextView {...props}>
{normalized.map(child => {
if (typeof child !== 'string') return child
const emojis = child.match(EMOJI) if (emojis === null) {
return child
}
if (emojis === null) { return child.split(EMOJI).map((stringPart, index) => [
return child stringPart,
} emojis[index] ? (
<UITextView
return child.split(EMOJI).map((stringPart, index) => ( {...props}
<UITextView key={index} {...props}> style={[props?.style, {color: 'black', fontFamily: 'System'}]}>
{stringPart} {emojis[index]}
{emojis[index] ? ( </UITextView>
<UITextView ) : null,
{...props} ])
style={[props?.style, {color: 'black', fontFamily: 'System'}]}> })
{emojis[index]} }
</UITextView>
) : null} const SINGLE_EMOJI_RE = /^[\p{Emoji_Presentation}\p{Extended_Pictographic}]+$/u
</UITextView> export function isOnlyEmoji(text: string) {
)) return text.length <= 15 && SINGLE_EMOJI_RE.test(text)
})}
</UITextView>
)
} }
+3 -12
View File
@@ -9,6 +9,7 @@ import {NavigationProp} from '#/lib/routes/types'
import {toShortUrl} from '#/lib/strings/url-helpers' import {toShortUrl} from '#/lib/strings/url-helpers'
import {isNative} from '#/platform/detection' import {isNative} from '#/platform/detection'
import {atoms as a, flatten, native, TextStyleProp, useTheme, web} from '#/alf' import {atoms as a, flatten, native, TextStyleProp, useTheme, web} from '#/alf'
import {isOnlyEmoji} from '#/alf/typography'
import {useInteractionState} from '#/components/hooks/useInteractionState' import {useInteractionState} from '#/components/hooks/useInteractionState'
import {InlineLinkText, LinkProps} from '#/components/Link' import {InlineLinkText, LinkProps} from '#/components/Link'
import {ProfileHoverCard} from '#/components/ProfileHoverCard' import {ProfileHoverCard} from '#/components/ProfileHoverCard'
@@ -150,17 +151,14 @@ export function RichText({
/>, />,
) )
} else { } else {
els.push( els.push(segment.text)
<Text key={key} emoji style={plainStyles}>
{segment.text}
</Text>,
)
} }
key++ key++
} }
return ( return (
<Text <Text
emoji
selectable={selectable} selectable={selectable}
testID={testID} testID={testID}
style={plainStyles} style={plainStyles}
@@ -250,10 +248,3 @@ function RichTextTag({
</React.Fragment> </React.Fragment>
) )
} }
export function isOnlyEmoji(text: string) {
return (
text.length <= 15 &&
/^[\p{Emoji_Presentation}\p{Extended_Pictographic}]+$/u.test(text)
)
}
+1 -7
View File
@@ -1,11 +1,9 @@
import {UITextView} from 'react-native-uitextview' import {UITextView} from 'react-native-uitextview'
import {logger} from '#/logger' import {logger} from '#/logger'
import {isIOS} from '#/platform/detection'
import {atoms, flatten, useAlf, useTheme, web} from '#/alf' import {atoms, flatten, useAlf, useTheme, web} from '#/alf'
import { import {
childHasEmoji, childHasEmoji,
childIsString,
normalizeTextStyles, normalizeTextStyles,
renderChildrenWithEmoji, renderChildrenWithEmoji,
TextProps, TextProps,
@@ -39,10 +37,6 @@ export function Text({
`Text: emoji detected but emoji not enabled: "${children}"\n\nPlease add <Text emoji />'`, `Text: emoji detected but emoji not enabled: "${children}"\n\nPlease add <Text emoji />'`,
) )
} }
if (emoji && !childIsString(children)) {
logger.error('Text: when <Text emoji />, children can only be strings.')
}
} }
const shared = { const shared = {
@@ -55,7 +49,7 @@ export function Text({
return ( return (
<UITextView {...shared}> <UITextView {...shared}>
{isIOS && emoji ? renderChildrenWithEmoji(children, shared) : children} {renderChildrenWithEmoji(children, shared, emoji ?? false)}
</UITextView> </UITextView>
) )
} }
+2 -1
View File
@@ -19,10 +19,11 @@ import {ConvoItem} from '#/state/messages/convo/types'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {TimeElapsed} from '#/view/com/util/TimeElapsed' import {TimeElapsed} from '#/view/com/util/TimeElapsed'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {isOnlyEmoji} from '#/alf/typography'
import {ActionsWrapper} from '#/components/dms/ActionsWrapper' import {ActionsWrapper} from '#/components/dms/ActionsWrapper'
import {InlineLinkText} from '#/components/Link' import {InlineLinkText} from '#/components/Link'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {isOnlyEmoji, RichText} from '../RichText' import {RichText} from '../RichText'
import {DateDivider} from './DateDivider' import {DateDivider} from './DateDivider'
import {MessageItemEmbed} from './MessageItemEmbed' import {MessageItemEmbed} from './MessageItemEmbed'
import {localDateString} from './util' import {localDateString} from './util'
+4 -19
View File
@@ -1,5 +1,5 @@
import React from 'react' import React from 'react'
import {StyleSheet, Text as RNText, TextProps} from 'react-native' import {StyleSheet, TextProps} from 'react-native'
import {UITextView} from 'react-native-uitextview' import {UITextView} from 'react-native-uitextview'
import {lh, s} from '#/lib/styles' import {lh, s} from '#/lib/styles'
@@ -9,7 +9,6 @@ import {isIOS, isWeb} from '#/platform/detection'
import {applyFonts, useAlf} from '#/alf' import {applyFonts, useAlf} from '#/alf'
import { import {
childHasEmoji, childHasEmoji,
childIsString,
renderChildrenWithEmoji, renderChildrenWithEmoji,
StringChild, StringChild,
} from '#/alf/typography' } from '#/alf/typography'
@@ -56,10 +55,6 @@ function Text_DEPRECATED({
`Text: emoji detected but emoji not enabled: "${children}"\n\nPlease add <Text emoji />'`, `Text: emoji detected but emoji not enabled: "${children}"\n\nPlease add <Text emoji />'`,
) )
} }
if (emoji && !childIsString(children)) {
logger.error('Text: when <Text emoji />, children can only be strings.')
}
} }
const textProps = React.useMemo(() => { const textProps = React.useMemo(() => {
@@ -107,19 +102,9 @@ function Text_DEPRECATED({
type, type,
]) ])
if (selectable && isIOS) {
return (
<UITextView {...textProps}>
{isIOS && emoji
? renderChildrenWithEmoji(children, textProps)
: children}
</UITextView>
)
}
return ( return (
<RNText {...textProps}> <UITextView {...textProps}>
{isIOS && emoji ? renderChildrenWithEmoji(children, textProps) : children} {renderChildrenWithEmoji(children, textProps, emoji ?? false)}
</RNText> </UITextView>
) )
} }