my fork of the bluesky client

Fix Fast Refresh in <Text> files (#6609)

* Separate non-components from components

* Mark old Text as deprecated

* Move emoji utilities to non-React file

* Fix type

* Fix import

authored by danabra.mov and committed by

GitHub cc60566c ff23ddb5

+156 -139
+135
src/alf/typography.tsx
··· 1 + import React from 'react' 2 + import {TextProps as RNTextProps} from 'react-native' 3 + import {StyleProp, TextStyle} from 'react-native' 4 + import {UITextView} from 'react-native-uitextview' 5 + import createEmojiRegex from 'emoji-regex' 6 + 7 + import {isNative} from '#/platform/detection' 8 + import {Alf, applyFonts, atoms, flatten} from '#/alf' 9 + 10 + /** 11 + * Util to calculate lineHeight from a text size atom and a leading atom 12 + * 13 + * Example: 14 + * `leading(atoms.text_md, atoms.leading_normal)` // => 24 15 + */ 16 + export function leading< 17 + Size extends {fontSize?: number}, 18 + Leading extends {lineHeight?: number}, 19 + >(textSize: Size, leading: Leading) { 20 + const size = textSize?.fontSize || atoms.text_md.fontSize 21 + const lineHeight = leading?.lineHeight || atoms.leading_normal.lineHeight 22 + return Math.round(size * lineHeight) 23 + } 24 + 25 + /** 26 + * Ensures that `lineHeight` defaults to a relative value of `1`, or applies 27 + * other relative leading atoms. 28 + * 29 + * If the `lineHeight` value is > 2, we assume it's an absolute value and 30 + * returns it as-is. 31 + */ 32 + export function normalizeTextStyles( 33 + styles: StyleProp<TextStyle>, 34 + { 35 + fontScale, 36 + fontFamily, 37 + }: { 38 + fontScale: number 39 + fontFamily: Alf['fonts']['family'] 40 + } & Pick<Alf, 'flags'>, 41 + ) { 42 + const s = flatten(styles) 43 + // should always be defined on these components 44 + s.fontSize = (s.fontSize || atoms.text_md.fontSize) * fontScale 45 + 46 + if (s?.lineHeight) { 47 + if (s.lineHeight !== 0 && s.lineHeight <= 2) { 48 + s.lineHeight = Math.round(s.fontSize * s.lineHeight) 49 + } 50 + } else if (!isNative) { 51 + s.lineHeight = s.fontSize 52 + } 53 + 54 + applyFonts(s, fontFamily) 55 + 56 + return s 57 + } 58 + 59 + export type StringChild = string | (string | null)[] 60 + export type TextProps = Omit<RNTextProps, 'children'> & { 61 + /** 62 + * Lets the user select text, to use the native copy and paste functionality. 63 + */ 64 + selectable?: boolean 65 + /** 66 + * Provides `data-*` attributes to the underlying `UITextView` component on 67 + * web only. 68 + */ 69 + dataSet?: Record<string, string | number | undefined> 70 + /** 71 + * Appears as a small tooltip on web hover. 72 + */ 73 + title?: string 74 + } & ( 75 + | { 76 + emoji?: true 77 + children: StringChild 78 + } 79 + | { 80 + emoji?: false 81 + children: RNTextProps['children'] 82 + } 83 + ) 84 + 85 + const EMOJI = createEmojiRegex() 86 + 87 + export function childHasEmoji(children: React.ReactNode) { 88 + return (Array.isArray(children) ? children : [children]).some( 89 + child => typeof child === 'string' && createEmojiRegex().test(child), 90 + ) 91 + } 92 + 93 + export function childIsString( 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 + props: Omit<TextProps, 'children'> = {}, 106 + ) { 107 + const normalized = Array.isArray(children) ? children : [children] 108 + 109 + return ( 110 + <UITextView {...props}> 111 + {normalized.map(child => { 112 + if (typeof child !== 'string') return child 113 + 114 + const emojis = child.match(EMOJI) 115 + 116 + if (emojis === null) { 117 + return child 118 + } 119 + 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 + ) 135 + }
+11 -134
src/components/Typography.tsx
··· 1 - import React from 'react' 2 - import {StyleProp, TextProps as RNTextProps, TextStyle} from 'react-native' 3 1 import {UITextView} from 'react-native-uitextview' 4 - import createEmojiRegex from 'emoji-regex' 5 2 6 3 import {logger} from '#/logger' 7 - import {isIOS, isNative} from '#/platform/detection' 8 - import {Alf, applyFonts, atoms, flatten, useAlf, useTheme, web} from '#/alf' 4 + import {isIOS} from '#/platform/detection' 5 + import {atoms, flatten, useAlf, useTheme, web} from '#/alf' 6 + import { 7 + childHasEmoji, 8 + childIsString, 9 + normalizeTextStyles, 10 + renderChildrenWithEmoji, 11 + TextProps, 12 + } from '#/alf/typography' 9 13 import {IS_DEV} from '#/env' 10 - 11 - export type StringChild = string | (string | null)[] 12 - 13 - export type TextProps = Omit<RNTextProps, 'children'> & { 14 - /** 15 - * Lets the user select text, to use the native copy and paste functionality. 16 - */ 17 - selectable?: boolean 18 - /** 19 - * Provides `data-*` attributes to the underlying `UITextView` component on 20 - * web only. 21 - */ 22 - dataSet?: Record<string, string | number | undefined> 23 - /** 24 - * Appears as a small tooltip on web hover. 25 - */ 26 - title?: string 27 - } & ( 28 - | { 29 - emoji?: true 30 - children: StringChild 31 - } 32 - | { 33 - emoji?: false 34 - children: RNTextProps['children'] 35 - } 36 - ) 37 - 38 - const EMOJI = createEmojiRegex() 39 - 40 - export function childHasEmoji(children: React.ReactNode) { 41 - return (Array.isArray(children) ? children : [children]).some( 42 - child => typeof child === 'string' && createEmojiRegex().test(child), 43 - ) 44 - } 45 - 46 - export function childIsString( 47 - children: React.ReactNode, 48 - ): children is StringChild { 49 - return ( 50 - typeof children === 'string' || 51 - (Array.isArray(children) && 52 - children.every(child => typeof child === 'string' || child === null)) 53 - ) 54 - } 55 - 56 - export function renderChildrenWithEmoji( 57 - children: StringChild, 58 - props: Omit<TextProps, 'children'> = {}, 59 - ) { 60 - const normalized = Array.isArray(children) ? children : [children] 61 - 62 - return ( 63 - <UITextView {...props}> 64 - {normalized.map(child => { 65 - if (typeof child !== 'string') return child 66 - 67 - const emojis = child.match(EMOJI) 68 - 69 - if (emojis === null) { 70 - return child 71 - } 72 - 73 - return child.split(EMOJI).map((stringPart, index) => ( 74 - <UITextView key={index} {...props}> 75 - {stringPart} 76 - {emojis[index] ? ( 77 - <UITextView 78 - {...props} 79 - style={[props?.style, {color: 'black', fontFamily: 'System'}]}> 80 - {emojis[index]} 81 - </UITextView> 82 - ) : null} 83 - </UITextView> 84 - )) 85 - })} 86 - </UITextView> 87 - ) 88 - } 89 - 90 - /** 91 - * Util to calculate lineHeight from a text size atom and a leading atom 92 - * 93 - * Example: 94 - * `leading(atoms.text_md, atoms.leading_normal)` // => 24 95 - */ 96 - export function leading< 97 - Size extends {fontSize?: number}, 98 - Leading extends {lineHeight?: number}, 99 - >(textSize: Size, leading: Leading) { 100 - const size = textSize?.fontSize || atoms.text_md.fontSize 101 - const lineHeight = leading?.lineHeight || atoms.leading_normal.lineHeight 102 - return Math.round(size * lineHeight) 103 - } 104 - 105 - /** 106 - * Ensures that `lineHeight` defaults to a relative value of `1`, or applies 107 - * other relative leading atoms. 108 - * 109 - * If the `lineHeight` value is > 2, we assume it's an absolute value and 110 - * returns it as-is. 111 - */ 112 - export function normalizeTextStyles( 113 - styles: StyleProp<TextStyle>, 114 - { 115 - fontScale, 116 - fontFamily, 117 - }: { 118 - fontScale: number 119 - fontFamily: Alf['fonts']['family'] 120 - } & Pick<Alf, 'flags'>, 121 - ) { 122 - const s = flatten(styles) 123 - // should always be defined on these components 124 - s.fontSize = (s.fontSize || atoms.text_md.fontSize) * fontScale 125 - 126 - if (s?.lineHeight) { 127 - if (s.lineHeight !== 0 && s.lineHeight <= 2) { 128 - s.lineHeight = Math.round(s.fontSize * s.lineHeight) 129 - } 130 - } else if (!isNative) { 131 - s.lineHeight = s.fontSize 132 - } 133 - 134 - applyFonts(s, fontFamily) 135 - 136 - return s 137 - } 14 + export type {TextProps} 138 15 139 16 /** 140 17 * Our main text component. Use this most of the time. ··· 183 60 ) 184 61 } 185 62 186 - export function createHeadingElement({level}: {level: number}) { 63 + function createHeadingElement({level}: {level: number}) { 187 64 return function HeadingElement({style, ...rest}: TextProps) { 188 65 const attr = 189 66 web({
+2 -1
src/screens/Onboarding/Layout.tsx
··· 17 17 useTheme, 18 18 web, 19 19 } from '#/alf' 20 + import {leading} from '#/alf/typography' 20 21 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 21 22 import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron' 22 23 import {createPortalGroup} from '#/components/Portal' 23 - import {leading, P, Text} from '#/components/Typography' 24 + import {P, Text} from '#/components/Typography' 24 25 import {IS_DEV} from '#/env' 25 26 26 27 const COL_WIDTH = 420
+1 -1
src/view/com/composer/text-input/TextInput.tsx
··· 31 31 suggestLinkCardUri, 32 32 } from '#/view/com/composer/text-input/text-input-util' 33 33 import {atoms as a, useAlf} from '#/alf' 34 - import {normalizeTextStyles} from '#/components/Typography' 34 + import {normalizeTextStyles} from '#/alf/typography' 35 35 import {Autocomplete} from './mobile/Autocomplete' 36 36 37 37 export interface TextInputRef {
+1 -1
src/view/com/composer/text-input/TextInput.web.tsx
··· 23 23 } from '#/view/com/composer/text-input/text-input-util' 24 24 import {textInputWebEmitter} from '#/view/com/composer/text-input/textInputWebEmitter' 25 25 import {atoms as a, useAlf} from '#/alf' 26 + import {normalizeTextStyles} from '#/alf/typography' 26 27 import {Portal} from '#/components/Portal' 27 - import {normalizeTextStyles} from '#/components/Typography' 28 28 import {Text} from '../../util/text/Text' 29 29 import {createSuggestion} from './web/Autocomplete' 30 30 import {Emoji} from './web/EmojiPicker.web'
+6 -2
src/view/com/util/text/Text.tsx
··· 12 12 childIsString, 13 13 renderChildrenWithEmoji, 14 14 StringChild, 15 - } from '#/components/Typography' 15 + } from '#/alf/typography' 16 16 import {IS_DEV} from '#/env' 17 17 18 18 export type CustomTextProps = Omit<TextProps, 'children'> & { ··· 32 32 } 33 33 ) 34 34 35 - export function Text({ 35 + export {Text_DEPRECATED as Text} 36 + /** 37 + * @deprecated use Text from Typography instead. 38 + */ 39 + function Text_DEPRECATED({ 36 40 type = 'md', 37 41 children, 38 42 emoji,