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:
+42
-51
@@ -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)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {NavigationProp} from '#/lib/routes/types'
|
||||
import {toShortUrl} from '#/lib/strings/url-helpers'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {atoms as a, flatten, native, TextStyleProp, useTheme, web} from '#/alf'
|
||||
import {isOnlyEmoji} from '#/alf/typography'
|
||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||
import {InlineLinkText, LinkProps} from '#/components/Link'
|
||||
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
|
||||
@@ -150,17 +151,14 @@ export function RichText({
|
||||
/>,
|
||||
)
|
||||
} else {
|
||||
els.push(
|
||||
<Text key={key} emoji style={plainStyles}>
|
||||
{segment.text}
|
||||
</Text>,
|
||||
)
|
||||
els.push(segment.text)
|
||||
}
|
||||
key++
|
||||
}
|
||||
|
||||
return (
|
||||
<Text
|
||||
emoji
|
||||
selectable={selectable}
|
||||
testID={testID}
|
||||
style={plainStyles}
|
||||
@@ -250,10 +248,3 @@ function RichTextTag({
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
export function isOnlyEmoji(text: string) {
|
||||
return (
|
||||
text.length <= 15 &&
|
||||
/^[\p{Emoji_Presentation}\p{Extended_Pictographic}]+$/u.test(text)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import {UITextView} from 'react-native-uitextview'
|
||||
|
||||
import {logger} from '#/logger'
|
||||
import {isIOS} from '#/platform/detection'
|
||||
import {atoms, flatten, useAlf, useTheme, web} from '#/alf'
|
||||
import {
|
||||
childHasEmoji,
|
||||
childIsString,
|
||||
normalizeTextStyles,
|
||||
renderChildrenWithEmoji,
|
||||
TextProps,
|
||||
@@ -39,10 +37,6 @@ export function Text({
|
||||
`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 = {
|
||||
@@ -55,7 +49,7 @@ export function Text({
|
||||
|
||||
return (
|
||||
<UITextView {...shared}>
|
||||
{isIOS && emoji ? renderChildrenWithEmoji(children, shared) : children}
|
||||
{renderChildrenWithEmoji(children, shared, emoji ?? false)}
|
||||
</UITextView>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -19,10 +19,11 @@ import {ConvoItem} from '#/state/messages/convo/types'
|
||||
import {useSession} from '#/state/session'
|
||||
import {TimeElapsed} from '#/view/com/util/TimeElapsed'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {isOnlyEmoji} from '#/alf/typography'
|
||||
import {ActionsWrapper} from '#/components/dms/ActionsWrapper'
|
||||
import {InlineLinkText} from '#/components/Link'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {isOnlyEmoji, RichText} from '../RichText'
|
||||
import {RichText} from '../RichText'
|
||||
import {DateDivider} from './DateDivider'
|
||||
import {MessageItemEmbed} from './MessageItemEmbed'
|
||||
import {localDateString} from './util'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 {lh, s} from '#/lib/styles'
|
||||
@@ -9,7 +9,6 @@ import {isIOS, isWeb} from '#/platform/detection'
|
||||
import {applyFonts, useAlf} from '#/alf'
|
||||
import {
|
||||
childHasEmoji,
|
||||
childIsString,
|
||||
renderChildrenWithEmoji,
|
||||
StringChild,
|
||||
} from '#/alf/typography'
|
||||
@@ -56,10 +55,6 @@ function Text_DEPRECATED({
|
||||
`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(() => {
|
||||
@@ -107,19 +102,9 @@ function Text_DEPRECATED({
|
||||
type,
|
||||
])
|
||||
|
||||
if (selectable && isIOS) {
|
||||
return (
|
||||
<UITextView {...textProps}>
|
||||
{isIOS && emoji
|
||||
? renderChildrenWithEmoji(children, textProps)
|
||||
: children}
|
||||
</UITextView>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<RNText {...textProps}>
|
||||
{isIOS && emoji ? renderChildrenWithEmoji(children, textProps) : children}
|
||||
</RNText>
|
||||
<UITextView {...textProps}>
|
||||
{renderChildrenWithEmoji(children, textProps, emoji ?? false)}
|
||||
</UITextView>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user