my fork of the bluesky client

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

authored by danabra.mov and committed by

GitHub dc3a42ed 84724bb9

+51 -89
+41 -50
src/alf/typography.tsx
··· 1 - import React from 'react' 1 + import React, {Children} from 'react' 2 2 import {TextProps as RNTextProps} from 'react-native' 3 3 import {StyleProp, TextStyle} from 'react-native' 4 4 import {UITextView} from 'react-native-uitextview' 5 5 import createEmojiRegex from 'emoji-regex' 6 6 7 7 import {isNative} from '#/platform/detection' 8 + import {isIOS} from '#/platform/detection' 8 9 import {Alf, applyFonts, atoms, flatten} from '#/alf' 9 10 10 11 /** ··· 57 58 } 58 59 59 60 export type StringChild = string | (string | null)[] 60 - export type TextProps = Omit<RNTextProps, 'children'> & { 61 + export type TextProps = RNTextProps & { 61 62 /** 62 63 * Lets the user select text, to use the native copy and paste functionality. 63 64 */ ··· 71 72 * Appears as a small tooltip on web hover. 72 73 */ 73 74 title?: string 74 - } & ( 75 - | { 76 - emoji?: true 77 - children: StringChild 78 - } 79 - | { 80 - emoji?: false 81 - children: RNTextProps['children'] 82 - } 83 - ) 75 + /** 76 + * Whether the children could possibly contain emoji. 77 + */ 78 + emoji?: boolean 79 + } 84 80 85 81 const EMOJI = createEmojiRegex() 86 82 87 83 export function childHasEmoji(children: React.ReactNode) { 88 - return (Array.isArray(children) ? children : [children]).some( 89 - child => typeof child === 'string' && createEmojiRegex().test(child), 90 - ) 84 + let hasEmoji = false 85 + Children.forEach(children, child => { 86 + if (typeof child === 'string' && createEmojiRegex().test(child)) { 87 + hasEmoji = true 88 + } 89 + }) 90 + return hasEmoji 91 91 } 92 92 93 - export function childIsString( 93 + export function renderChildrenWithEmoji( 94 94 children: React.ReactNode, 95 - ): children is StringChild { 96 - return ( 97 - typeof children === 'string' || 98 - (Array.isArray(children) && 99 - children.every(child => typeof child === 'string' || child === null)) 100 - ) 101 - } 102 - 103 - export function renderChildrenWithEmoji( 104 - children: StringChild, 105 95 props: Omit<TextProps, 'children'> = {}, 96 + emoji: boolean, 106 97 ) { 107 - const normalized = Array.isArray(children) ? children : [children] 98 + if (!isIOS || !emoji) { 99 + return children 100 + } 101 + return Children.map(children, child => { 102 + if (typeof child !== 'string') return child 108 103 109 - return ( 110 - <UITextView {...props}> 111 - {normalized.map(child => { 112 - if (typeof child !== 'string') return child 104 + const emojis = child.match(EMOJI) 113 105 114 - const emojis = child.match(EMOJI) 106 + if (emojis === null) { 107 + return child 108 + } 115 109 116 - if (emojis === null) { 117 - return child 118 - } 110 + return child.split(EMOJI).map((stringPart, index) => [ 111 + stringPart, 112 + emojis[index] ? ( 113 + <UITextView 114 + {...props} 115 + style={[props?.style, {color: 'black', fontFamily: 'System'}]}> 116 + {emojis[index]} 117 + </UITextView> 118 + ) : null, 119 + ]) 120 + }) 121 + } 119 122 120 - return child.split(EMOJI).map((stringPart, index) => ( 121 - <UITextView key={index} {...props}> 122 - {stringPart} 123 - {emojis[index] ? ( 124 - <UITextView 125 - {...props} 126 - style={[props?.style, {color: 'black', fontFamily: 'System'}]}> 127 - {emojis[index]} 128 - </UITextView> 129 - ) : null} 130 - </UITextView> 131 - )) 132 - })} 133 - </UITextView> 134 - ) 123 + const SINGLE_EMOJI_RE = /^[\p{Emoji_Presentation}\p{Extended_Pictographic}]+$/u 124 + export function isOnlyEmoji(text: string) { 125 + return text.length <= 15 && SINGLE_EMOJI_RE.test(text) 135 126 }
+3 -12
src/components/RichText.tsx
··· 9 9 import {toShortUrl} from '#/lib/strings/url-helpers' 10 10 import {isNative} from '#/platform/detection' 11 11 import {atoms as a, flatten, native, TextStyleProp, useTheme, web} from '#/alf' 12 + import {isOnlyEmoji} from '#/alf/typography' 12 13 import {useInteractionState} from '#/components/hooks/useInteractionState' 13 14 import {InlineLinkText, LinkProps} from '#/components/Link' 14 15 import {ProfileHoverCard} from '#/components/ProfileHoverCard' ··· 150 151 />, 151 152 ) 152 153 } else { 153 - els.push( 154 - <Text key={key} emoji style={plainStyles}> 155 - {segment.text} 156 - </Text>, 157 - ) 154 + els.push(segment.text) 158 155 } 159 156 key++ 160 157 } 161 158 162 159 return ( 163 160 <Text 161 + emoji 164 162 selectable={selectable} 165 163 testID={testID} 166 164 style={plainStyles} ··· 250 248 </React.Fragment> 251 249 ) 252 250 } 253 - 254 - export function isOnlyEmoji(text: string) { 255 - return ( 256 - text.length <= 15 && 257 - /^[\p{Emoji_Presentation}\p{Extended_Pictographic}]+$/u.test(text) 258 - ) 259 - }
+1 -7
src/components/Typography.tsx
··· 1 1 import {UITextView} from 'react-native-uitextview' 2 2 3 3 import {logger} from '#/logger' 4 - import {isIOS} from '#/platform/detection' 5 4 import {atoms, flatten, useAlf, useTheme, web} from '#/alf' 6 5 import { 7 6 childHasEmoji, 8 - childIsString, 9 7 normalizeTextStyles, 10 8 renderChildrenWithEmoji, 11 9 TextProps, ··· 39 37 `Text: emoji detected but emoji not enabled: "${children}"\n\nPlease add <Text emoji />'`, 40 38 ) 41 39 } 42 - 43 - if (emoji && !childIsString(children)) { 44 - logger.error('Text: when <Text emoji />, children can only be strings.') 45 - } 46 40 } 47 41 48 42 const shared = { ··· 55 49 56 50 return ( 57 51 <UITextView {...shared}> 58 - {isIOS && emoji ? renderChildrenWithEmoji(children, shared) : children} 52 + {renderChildrenWithEmoji(children, shared, emoji ?? false)} 59 53 </UITextView> 60 54 ) 61 55 }
+2 -1
src/components/dms/MessageItem.tsx
··· 19 19 import {useSession} from '#/state/session' 20 20 import {TimeElapsed} from '#/view/com/util/TimeElapsed' 21 21 import {atoms as a, useTheme} from '#/alf' 22 + import {isOnlyEmoji} from '#/alf/typography' 22 23 import {ActionsWrapper} from '#/components/dms/ActionsWrapper' 23 24 import {InlineLinkText} from '#/components/Link' 24 25 import {Text} from '#/components/Typography' 25 - import {isOnlyEmoji, RichText} from '../RichText' 26 + import {RichText} from '../RichText' 26 27 import {DateDivider} from './DateDivider' 27 28 import {MessageItemEmbed} from './MessageItemEmbed' 28 29 import {localDateString} from './util'
+4 -19
src/view/com/util/text/Text.tsx
··· 1 1 import React from 'react' 2 - import {StyleSheet, Text as RNText, TextProps} from 'react-native' 2 + import {StyleSheet, TextProps} from 'react-native' 3 3 import {UITextView} from 'react-native-uitextview' 4 4 5 5 import {lh, s} from '#/lib/styles' ··· 9 9 import {applyFonts, useAlf} from '#/alf' 10 10 import { 11 11 childHasEmoji, 12 - childIsString, 13 12 renderChildrenWithEmoji, 14 13 StringChild, 15 14 } from '#/alf/typography' ··· 56 55 `Text: emoji detected but emoji not enabled: "${children}"\n\nPlease add <Text emoji />'`, 57 56 ) 58 57 } 59 - 60 - if (emoji && !childIsString(children)) { 61 - logger.error('Text: when <Text emoji />, children can only be strings.') 62 - } 63 58 } 64 59 65 60 const textProps = React.useMemo(() => { ··· 107 102 type, 108 103 ]) 109 104 110 - if (selectable && isIOS) { 111 - return ( 112 - <UITextView {...textProps}> 113 - {isIOS && emoji 114 - ? renderChildrenWithEmoji(children, textProps) 115 - : children} 116 - </UITextView> 117 - ) 118 - } 119 - 120 105 return ( 121 - <RNText {...textProps}> 122 - {isIOS && emoji ? renderChildrenWithEmoji(children, textProps) : children} 123 - </RNText> 106 + <UITextView {...textProps}> 107 + {renderChildrenWithEmoji(children, textProps, emoji ?? false)} 108 + </UITextView> 124 109 ) 125 110 }