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 {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>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user