Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

[Neue] Handle emoji within custom font (#5449)

* Support emoji in text with custom font

* Add emoji support to elements that need it

* Remove unused file causing lint failure

* Fix a few more emoji locations

* Couple more

* No throw

authored by

Eric Bailey and committed by
GitHub
5eb29448 443f3a64

+423 -289
+1
package.json
··· 116 116 "deprecated-react-native-prop-types": "^5.0.0", 117 117 "email-validator": "^2.0.4", 118 118 "emoji-mart": "^5.5.2", 119 + "emoji-regex": "^10.4.0", 119 120 "eventemitter3": "^5.0.1", 120 121 "expo": "^51.0.8", 121 122 "expo-application": "^5.9.1",
+8 -5
src/components/FeedCard.tsx
··· 11 11 import {useLingui} from '@lingui/react' 12 12 import {useQueryClient} from '@tanstack/react-query' 13 13 14 + import {sanitizeHandle} from '#/lib/strings/handles' 14 15 import {logger} from '#/logger' 16 + import {precacheFeedFromGeneratorView} from '#/state/queries/feed' 15 17 import { 16 18 useAddSavedFeedsMutation, 17 19 usePreferencesQuery, 18 20 useRemoveFeedMutation, 19 21 } from '#/state/queries/preferences' 20 - import {sanitizeHandle} from 'lib/strings/handles' 21 - import {precacheFeedFromGeneratorView} from 'state/queries/feed' 22 - import {useSession} from 'state/session' 22 + import {useSession} from '#/state/session' 23 + import * as Toast from '#/view/com/util/Toast' 23 24 import {UserAvatar} from '#/view/com/util/UserAvatar' 24 - import * as Toast from 'view/com/util/Toast' 25 25 import {useTheme} from '#/alf' 26 26 import {atoms as a} from '#/alf' 27 27 import {Button, ButtonIcon} from '#/components/Button' ··· 121 121 122 122 return ( 123 123 <View style={[a.flex_1]}> 124 - <Text style={[a.text_md, a.font_bold, a.leading_snug]} numberOfLines={1}> 124 + <Text 125 + emoji 126 + style={[a.text_md, a.font_bold, a.leading_snug]} 127 + numberOfLines={1}> 125 128 {title} 126 129 </Text> 127 130 {creator && (
+7 -7
src/components/KnownFollowers.tsx
··· 5 5 import {useLingui} from '@lingui/react' 6 6 7 7 import {makeProfileLink} from '#/lib/routes/links' 8 - import {sanitizeDisplayName} from 'lib/strings/display-names' 8 + import {sanitizeDisplayName} from '#/lib/strings/display-names' 9 9 import {UserAvatar} from '#/view/com/util/UserAvatar' 10 10 import {atoms as a, useTheme} from '#/alf' 11 11 import {Link, LinkProps} from '#/components/Link' ··· 185 185 serverCount > 2 ? ( 186 186 <Trans> 187 187 Followed by{' '} 188 - <Text key={slice[0].profile.did} style={textStyle}> 188 + <Text emoji key={slice[0].profile.did} style={textStyle}> 189 189 {slice[0].profile.displayName} 190 190 </Text> 191 191 ,{' '} 192 - <Text key={slice[1].profile.did} style={textStyle}> 192 + <Text emoji key={slice[1].profile.did} style={textStyle}> 193 193 {slice[1].profile.displayName} 194 194 </Text> 195 195 , and{' '} ··· 203 203 // only 2 204 204 <Trans> 205 205 Followed by{' '} 206 - <Text key={slice[0].profile.did} style={textStyle}> 206 + <Text emoji key={slice[0].profile.did} style={textStyle}> 207 207 {slice[0].profile.displayName} 208 208 </Text>{' '} 209 209 and{' '} 210 - <Text key={slice[1].profile.did} style={textStyle}> 210 + <Text emoji key={slice[1].profile.did} style={textStyle}> 211 211 {slice[1].profile.displayName} 212 212 </Text> 213 213 </Trans> ··· 216 216 // 1-n followers, including blocks 217 217 <Trans> 218 218 Followed by{' '} 219 - <Text key={slice[0].profile.did} style={textStyle}> 219 + <Text emoji key={slice[0].profile.did} style={textStyle}> 220 220 {slice[0].profile.displayName} 221 221 </Text>{' '} 222 222 and{' '} ··· 230 230 // only 1 231 231 <Trans> 232 232 Followed by{' '} 233 - <Text key={slice[0].profile.did} style={textStyle}> 233 + <Text emoji key={slice[0].profile.did} style={textStyle}> 234 234 {slice[0].profile.displayName} 235 235 </Text> 236 236 </Trans>
+8 -3
src/components/LabelingServiceCard/index.tsx
··· 44 44 } 45 45 46 46 export function Title({value}: {value: string}) { 47 - return <Text style={[a.text_md, a.font_bold, a.leading_tight]}>{value}</Text> 47 + return ( 48 + <Text emoji style={[a.text_md, a.font_bold, a.leading_tight]}> 49 + {value} 50 + </Text> 51 + ) 48 52 } 49 53 50 54 export function Description({value, handle}: {value?: string; handle: string}) { 55 + const {_} = useLingui() 51 56 return value ? ( 52 57 <Text numberOfLines={2}> 53 58 <RichText value={value} style={[a.leading_snug]} /> 54 59 </Text> 55 60 ) : ( 56 - <Text style={[a.leading_snug]}> 57 - <Trans>By {sanitizeHandle(handle, '@')}</Trans> 61 + <Text emoji style={[a.leading_snug]}> 62 + {_(msg`By ${sanitizeHandle(handle, '@')}`)} 58 63 </Text> 59 64 ) 60 65 }
+12 -12
src/components/ListCard.tsx
··· 7 7 moderateUserList, 8 8 ModerationUI, 9 9 } from '@atproto/api' 10 - import {Trans} from '@lingui/macro' 10 + import {msg, Trans} from '@lingui/macro' 11 + import {useLingui} from '@lingui/react' 11 12 import {useQueryClient} from '@tanstack/react-query' 12 13 13 - import {sanitizeHandle} from 'lib/strings/handles' 14 - import {useModerationOpts} from 'state/preferences/moderation-opts' 15 - import {precacheList} from 'state/queries/feed' 16 - import {useSession} from 'state/session' 14 + import {sanitizeHandle} from '#/lib/strings/handles' 15 + import {useModerationOpts} from '#/state/preferences/moderation-opts' 16 + import {precacheList} from '#/state/queries/feed' 17 + import {useSession} from '#/state/session' 17 18 import {atoms as a, useTheme} from '#/alf' 18 19 import { 19 20 Avatar, ··· 111 112 modUi?: ModerationUI 112 113 }) { 113 114 const t = useTheme() 115 + const {_} = useLingui() 114 116 const {currentAccount} = useSession() 115 117 116 118 return ( ··· 130 132 </Hider.Mask> 131 133 <Hider.Content> 132 134 <Text 135 + emoji 133 136 style={[a.text_md, a.font_bold, a.leading_snug]} 134 137 numberOfLines={1}> 135 138 {title} ··· 139 142 140 143 {creator && ( 141 144 <Text 145 + emoji 142 146 style={[a.leading_snug, t.atoms.text_contrast_medium]} 143 147 numberOfLines={1}> 144 - {purpose === MODLIST ? ( 145 - <Trans> 146 - Moderation list by {sanitizeHandle(creator.handle, '@')} 147 - </Trans> 148 - ) : ( 149 - <Trans>List by {sanitizeHandle(creator.handle, '@')}</Trans> 150 - )} 148 + {purpose === MODLIST 149 + ? _(msg`Moderation list by ${sanitizeHandle(creator.handle, '@')}`) 150 + : _(msg`List by ${sanitizeHandle(creator.handle, '@')}`)} 151 151 </Text> 152 152 )} 153 153 </View>
+1
src/components/Pills.tsx
··· 130 130 )} 131 131 132 132 <Text 133 + emoji 133 134 style={[ 134 135 text, 135 136 a.font_bold,
+7 -5
src/components/ProfileCard.tsx
··· 11 11 12 12 import {LogEvents} from '#/lib/statsig/statsig' 13 13 import {sanitizeDisplayName} from '#/lib/strings/display-names' 14 + import {sanitizeHandle} from '#/lib/strings/handles' 15 + import {useProfileShadow} from '#/state/cache/profile-shadow' 14 16 import {useProfileFollowMutationQueue} from '#/state/queries/profile' 15 - import {sanitizeHandle} from 'lib/strings/handles' 16 - import {useProfileShadow} from 'state/cache/profile-shadow' 17 - import {useSession} from 'state/session' 17 + import {useSession} from '#/state/session' 18 + import {ProfileCardPills} from '#/view/com/profile/ProfileCard' 18 19 import * as Toast from '#/view/com/util/Toast' 19 - import {ProfileCardPills} from 'view/com/profile/ProfileCard' 20 - import {UserAvatar} from 'view/com/util/UserAvatar' 20 + import {UserAvatar} from '#/view/com/util/UserAvatar' 21 21 import {atoms as a, useTheme} from '#/alf' 22 22 import {Button, ButtonIcon, ButtonProps, ButtonText} from '#/components/Button' 23 23 import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' ··· 175 175 return ( 176 176 <View style={[a.flex_1]}> 177 177 <Text 178 + emoji 178 179 style={[a.text_md, a.font_bold, a.leading_snug, a.self_start]} 179 180 numberOfLines={1}> 180 181 {name} 181 182 </Text> 182 183 <Text 184 + emoji 183 185 style={[a.leading_snug, t.atoms.text_contrast_medium]} 184 186 numberOfLines={1}> 185 187 {handle}
+1
src/components/ReportDialog/SubmitView.tsx
··· 256 256 a.z_10, 257 257 ]}> 258 258 <Text 259 + emoji 259 260 style={[ 260 261 native({marginTop: 2}), 261 262 t.atoms.text_contrast_medium,
+8 -1
src/components/RichText.tsx
··· 66 66 (flattenedStyle.fontSize ?? a.text_sm.fontSize) * emojiMultiplier 67 67 return ( 68 68 <Text 69 + emoji 69 70 selectable={selectable} 70 71 testID={testID} 71 72 style={[plainStyles, {fontSize}]} ··· 77 78 } 78 79 return ( 79 80 <Text 81 + emoji 80 82 selectable={selectable} 81 83 testID={testID} 82 84 style={plainStyles} ··· 148 150 />, 149 151 ) 150 152 } else { 151 - els.push(segment.text) 153 + els.push( 154 + <Text key={key} emoji style={plainStyles}> 155 + {segment.text} 156 + </Text>, 157 + ) 152 158 } 153 159 key++ 154 160 } ··· 213 219 <React.Fragment> 214 220 <TagMenu control={control} tag={tag} authorHandle={authorHandle}> 215 221 <Text 222 + emoji 216 223 selectable={selectable} 217 224 {...native({ 218 225 accessibilityLabel: _(msg`Hashtag: #${tag}`),
+12 -15
src/components/StarterPack/StarterPackCard.tsx
··· 2 2 import {View} from 'react-native' 3 3 import {Image} from 'expo-image' 4 4 import {AppBskyGraphDefs, AppBskyGraphStarterpack, AtUri} from '@atproto/api' 5 - import {msg, Trans} from '@lingui/macro' 5 + import {msg} from '@lingui/macro' 6 6 import {useLingui} from '@lingui/react' 7 7 import {useQueryClient} from '@tanstack/react-query' 8 8 9 - import {sanitizeHandle} from 'lib/strings/handles' 10 - import {getStarterPackOgCard} from 'lib/strings/starter-pack' 11 - import {precacheResolvedUri} from 'state/queries/resolve-uri' 12 - import {precacheStarterPack} from 'state/queries/starter-packs' 13 - import {useSession} from 'state/session' 9 + import {sanitizeHandle} from '#/lib/strings/handles' 10 + import {getStarterPackOgCard} from '#/lib/strings/starter-pack' 11 + import {precacheResolvedUri} from '#/state/queries/resolve-uri' 12 + import {precacheStarterPack} from '#/state/queries/starter-packs' 13 + import {useSession} from '#/state/session' 14 14 import {atoms as a, useTheme} from '#/alf' 15 15 import {StarterPack} from '#/components/icons/StarterPack' 16 16 import {BaseLink} from '#/components/Link' ··· 66 66 <View style={[a.flex_row, a.gap_sm]}> 67 67 {!noIcon ? <StarterPack width={40} gradient="sky" /> : null} 68 68 <View> 69 - <Text style={[a.text_md, a.font_bold, a.leading_snug]}> 69 + <Text emoji style={[a.text_md, a.font_bold, a.leading_snug]}> 70 70 {record.name} 71 71 </Text> 72 - <Text style={[a.leading_snug, t.atoms.text_contrast_medium]}> 73 - <Trans> 74 - Starter pack by{' '} 75 - {creator?.did === currentAccount?.did 76 - ? _(msg`you`) 77 - : `@${sanitizeHandle(creator.handle)}`} 78 - </Trans> 72 + <Text emoji style={[a.leading_snug, t.atoms.text_contrast_medium]}> 73 + {creator?.did === currentAccount?.did 74 + ? _(msg`Starter pack by you`) 75 + : _(msg`Starter pack by ${sanitizeHandle(creator.handle, '@')}`)} 79 76 </Text> 80 77 </View> 81 78 </View> 82 79 {!noDescription && record.description ? ( 83 - <Text numberOfLines={3} style={[a.leading_snug]}> 80 + <Text emoji numberOfLines={3} style={[a.leading_snug]}> 84 81 {record.description} 85 82 </Text> 86 83 ) : null}
+6 -5
src/components/StarterPack/Wizard/WizardListCard.tsx
··· 12 12 import {msg, Trans} from '@lingui/macro' 13 13 import {useLingui} from '@lingui/react' 14 14 15 - import {DISCOVER_FEED_URI, STARTER_PACK_MAX_SIZE} from 'lib/constants' 16 - import {sanitizeDisplayName} from 'lib/strings/display-names' 17 - import {sanitizeHandle} from 'lib/strings/handles' 18 - import {useSession} from 'state/session' 19 - import {UserAvatar} from 'view/com/util/UserAvatar' 15 + import {DISCOVER_FEED_URI, STARTER_PACK_MAX_SIZE} from '#/lib/constants' 16 + import {sanitizeDisplayName} from '#/lib/strings/display-names' 17 + import {sanitizeHandle} from '#/lib/strings/handles' 18 + import {useSession} from '#/state/session' 19 + import {UserAvatar} from '#/view/com/util/UserAvatar' 20 20 import {WizardAction, WizardState} from '#/screens/StarterPack/Wizard/State' 21 21 import {atoms as a, useTheme} from '#/alf' 22 22 import {Button, ButtonText} from '#/components/Button' ··· 78 78 /> 79 79 <View style={[a.flex_1, a.gap_2xs]}> 80 80 <Text 81 + emoji 81 82 style={[ 82 83 a.flex_1, 83 84 a.font_bold,
+104 -4
src/components/Typography.tsx
··· 1 1 import React from 'react' 2 2 import {StyleProp, TextProps as RNTextProps, TextStyle} from 'react-native' 3 3 import {UITextView} from 'react-native-uitextview' 4 + import createEmojiRegex from 'emoji-regex' 4 5 5 - import {isNative} from '#/platform/detection' 6 + import {logger} from '#/logger' 7 + import {isIOS, isNative} from '#/platform/detection' 6 8 import {Alf, applyFonts, atoms, flatten, useAlf, useTheme, web} from '#/alf' 9 + import {IS_DEV} from '#/env' 7 10 8 - export type TextProps = RNTextProps & { 11 + export type StringChild = string | (string | null)[] 12 + 13 + export type TextProps = Omit<RNTextProps, 'children'> & { 9 14 /** 10 15 * Lets the user select text, to use the native copy and paste functionality. 11 16 */ 12 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(children: StringChild) { 57 + const normalized = Array.isArray(children) ? children : [children] 58 + 59 + return ( 60 + <UITextView> 61 + {normalized.map(child => { 62 + if (typeof child !== 'string') return child 63 + 64 + const emojis = child.match(EMOJI) 65 + 66 + if (emojis === null) { 67 + return child 68 + } 69 + 70 + return child.split(EMOJI).map((stringPart, index) => ( 71 + <UITextView key={index}> 72 + {stringPart} 73 + {emojis[index] ? ( 74 + <UITextView style={{color: 'black', fontFamily: 'System'}}> 75 + {emojis[index]} 76 + </UITextView> 77 + ) : null} 78 + </UITextView> 79 + )) 80 + })} 81 + </UITextView> 82 + ) 13 83 } 14 84 15 85 /** ··· 64 134 /** 65 135 * Our main text component. Use this most of the time. 66 136 */ 67 - export function Text({style, selectable, ...rest}: TextProps) { 137 + export function Text({ 138 + children, 139 + emoji, 140 + style, 141 + selectable, 142 + title, 143 + dataSet, 144 + ...rest 145 + }: TextProps) { 68 146 const {fonts, flags} = useAlf() 69 147 const t = useTheme() 70 148 const s = normalizeTextStyles([atoms.text_sm, t.atoms.text, flatten(style)], { ··· 73 151 flags, 74 152 }) 75 153 76 - return <UITextView selectable={selectable} uiTextView style={s} {...rest} /> 154 + if (IS_DEV) { 155 + if (!emoji && childHasEmoji(children)) { 156 + logger.warn( 157 + `Text: emoji detected but emoji not enabled: "${children}"\n\nPlease add <Text emoji />'`, 158 + ) 159 + } 160 + 161 + if (emoji && !childIsString(children)) { 162 + logger.error('Text: when <Text emoji />, children can only be strings.') 163 + } 164 + } 165 + 166 + return ( 167 + <UITextView 168 + selectable={selectable} 169 + uiTextView 170 + style={s} 171 + {...rest} 172 + // @ts-ignore 173 + dataSet={Object.assign({tooltip: title}, dataSet || {})}> 174 + {isIOS && emoji ? renderChildrenWithEmoji(children) : children} 175 + </UITextView> 176 + ) 77 177 } 78 178 79 179 export function createHeadingElement({level}: {level: number}) {
+9 -8
src/components/dms/MessagesListHeader.tsx
··· 10 10 import {useLingui} from '@lingui/react' 11 11 import {useNavigation} from '@react-navigation/native' 12 12 13 - import {BACK_HITSLOP} from 'lib/constants' 14 - import {makeProfileLink} from 'lib/routes/links' 15 - import {NavigationProp} from 'lib/routes/types' 16 - import {sanitizeDisplayName} from 'lib/strings/display-names' 17 - import {isWeb} from 'platform/detection' 18 - import {useProfileShadow} from 'state/cache/profile-shadow' 19 - import {isConvoActive, useConvo} from 'state/messages/convo' 20 - import {PreviewableUserAvatar} from 'view/com/util/UserAvatar' 13 + import {BACK_HITSLOP} from '#/lib/constants' 14 + import {makeProfileLink} from '#/lib/routes/links' 15 + import {NavigationProp} from '#/lib/routes/types' 16 + import {sanitizeDisplayName} from '#/lib/strings/display-names' 17 + import {isWeb} from '#/platform/detection' 18 + import {useProfileShadow} from '#/state/cache/profile-shadow' 19 + import {isConvoActive, useConvo} from '#/state/messages/convo' 20 + import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' 21 21 import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' 22 22 import {ConvoMenu} from '#/components/dms/ConvoMenu' 23 23 import {Bell2Off_Filled_Corner0_Rounded as BellStroke} from '#/components/icons/Bell2' ··· 170 170 </View> 171 171 <View style={a.flex_1}> 172 172 <Text 173 + emoji 173 174 style={[ 174 175 a.text_md, 175 176 a.font_bold,
+4 -2
src/components/moderation/LabelsOnMeDialog.tsx
··· 132 132 ]}> 133 133 <View style={[a.p_md, a.gap_sm, a.flex_row]}> 134 134 <View style={[a.flex_1, a.gap_xs]}> 135 - <Text style={[a.font_bold, a.text_md]}>{strings.name}</Text> 136 - <Text style={[t.atoms.text_contrast_medium, a.leading_snug]}> 135 + <Text emoji style={[a.font_bold, a.text_md]}> 136 + {strings.name} 137 + </Text> 138 + <Text emoji style={[t.atoms.text_contrast_medium, a.leading_snug]}> 137 139 {strings.description} 138 140 </Text> 139 141 </View>
+6 -2
src/components/moderation/ModerationDetailsDialog.tsx
··· 118 118 : _(msg`The author of this thread has hidden this reply.`) 119 119 } else if (modcause.type === 'label') { 120 120 name = desc.name 121 - description = desc.description 121 + description = ( 122 + <Text emoji style={[t.atoms.text, a.text_md, a.leading_snug]}> 123 + {desc.description} 124 + </Text> 125 + ) 122 126 } else { 123 127 // should never happen 124 128 name = '' ··· 127 131 128 132 return ( 129 133 <Dialog.ScrollableInner label={_(msg`Moderation details`)}> 130 - <Text style={[t.atoms.text, a.text_2xl, a.font_bold, a.mb_sm]}> 134 + <Text emoji style={[t.atoms.text, a.text_2xl, a.font_bold, a.mb_sm]}> 131 135 {name} 132 136 </Text> 133 137 <Text style={[t.atoms.text, a.text_md, a.leading_snug]}>
+6 -4
src/screens/Messages/List/ChatListItem.tsx
··· 10 10 import {msg} from '@lingui/macro' 11 11 import {useLingui} from '@lingui/react' 12 12 13 + import {useHaptics} from '#/lib/haptics' 14 + import {decrementBadgeCount} from '#/lib/notifications/notifications' 15 + import {logEvent} from '#/lib/statsig/statsig' 16 + import {sanitizeDisplayName} from '#/lib/strings/display-names' 13 17 import { 14 18 postUriToRelativePath, 15 19 toBskyAppUrl, ··· 19 23 import {useProfileShadow} from '#/state/cache/profile-shadow' 20 24 import {useModerationOpts} from '#/state/preferences/moderation-opts' 21 25 import {useSession} from '#/state/session' 22 - import {useHaptics} from 'lib/haptics' 23 - import {decrementBadgeCount} from 'lib/notifications/notifications' 24 - import {logEvent} from 'lib/statsig/statsig' 25 - import {sanitizeDisplayName} from 'lib/strings/display-names' 26 26 import {TimeElapsed} from '#/view/com/util/TimeElapsed' 27 27 import {UserAvatar} from '#/view/com/util/UserAvatar' 28 28 import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' ··· 248 248 numberOfLines={1} 249 249 style={[{maxWidth: '85%'}, web([a.leading_normal])]}> 250 250 <Text 251 + emoji 251 252 style={[ 252 253 a.text_md, 253 254 t.atoms.text, ··· 301 302 )} 302 303 303 304 <Text 305 + emoji 304 306 numberOfLines={2} 305 307 style={[ 306 308 a.text_sm,
+1
src/screens/Profile/Header/DisplayName.tsx
··· 19 19 return ( 20 20 <View pointerEvents="none"> 21 21 <Text 22 + emoji 22 23 testID="profileHeaderDisplayName" 23 24 style={[t.atoms.text, a.text_4xl, a.self_start, {fontWeight: '600'}]}> 24 25 {sanitizeDisplayName(
+7 -4
src/screens/Profile/Header/Handle.tsx
··· 1 1 import React from 'react' 2 2 import {View} from 'react-native' 3 3 import {AppBskyActorDefs} from '@atproto/api' 4 - import {Trans} from '@lingui/macro' 4 + import {msg, Trans} from '@lingui/macro' 5 + import {useLingui} from '@lingui/react' 5 6 7 + import {isInvalidHandle} from '#/lib/strings/handles' 8 + import {isIOS} from '#/platform/detection' 6 9 import {Shadow} from '#/state/cache/types' 7 - import {isInvalidHandle} from 'lib/strings/handles' 8 - import {isIOS} from 'platform/detection' 9 10 import {atoms as a, useTheme, web} from '#/alf' 10 11 import {NewskieDialog} from '#/components/NewskieDialog' 11 12 import {Text} from '#/components/Typography' ··· 18 19 disableTaps?: boolean 19 20 }) { 20 21 const t = useTheme() 22 + const {_} = useLingui() 21 23 const invalidHandle = isInvalidHandle(profile.handle) 22 24 const blockHide = profile.viewer?.blocking || profile.viewer?.blockedBy 23 25 return ( ··· 33 35 </View> 34 36 ) : undefined} 35 37 <Text 38 + emoji 36 39 numberOfLines={1} 37 40 style={[ 38 41 invalidHandle ··· 47 50 : [a.text_md, a.leading_tight, t.atoms.text_contrast_medium], 48 51 web({wordBreak: 'break-all'}), 49 52 ]}> 50 - {invalidHandle ? <Trans>⚠Invalid Handle</Trans> : `@${profile.handle}`} 53 + {invalidHandle ? _(msg`⚠Invalid Handle`) : `@${profile.handle}`} 51 54 </Text> 52 55 </View> 53 56 )
+10 -9
src/view/com/composer/text-input/TextInput.tsx
··· 19 19 PasteInputRef, 20 20 } from '@mattermost/react-native-paste-input' 21 21 22 + import {POST_IMG_MAX} from '#/lib/constants' 23 + import {usePalette} from '#/lib/hooks/usePalette' 24 + import {downloadAndResize} from '#/lib/media/manip' 25 + import {isUriImage} from '#/lib/media/util' 26 + import {cleanError} from '#/lib/strings/errors' 27 + import {getMentionAt, insertMentionAt} from '#/lib/strings/mention-manip' 28 + import {useTheme} from '#/lib/ThemeContext' 22 29 import {isAndroid} from '#/platform/detection' 23 - import {POST_IMG_MAX} from 'lib/constants' 24 - import {usePalette} from 'lib/hooks/usePalette' 25 - import {downloadAndResize} from 'lib/media/manip' 26 - import {isUriImage} from 'lib/media/util' 27 - import {cleanError} from 'lib/strings/errors' 28 - import {getMentionAt, insertMentionAt} from 'lib/strings/mention-manip' 29 - import {useTheme} from 'lib/ThemeContext' 30 30 import { 31 31 LinkFacetMatch, 32 32 suggestLinkCardUri, 33 - } from 'view/com/composer/text-input/text-input-util' 34 - import {Text} from 'view/com/util/text/Text' 33 + } from '#/view/com/composer/text-input/text-input-util' 34 + import {Text} from '#/view/com/util/text/Text' 35 35 import {atoms as a, useAlf} from '#/alf' 36 36 import {normalizeTextStyles} from '#/components/Typography' 37 37 import {Autocomplete} from './mobile/Autocomplete' ··· 216 216 return Array.from(richtext.segments()).map(segment => { 217 217 return ( 218 218 <Text 219 + emoji 219 220 key={i++} 220 221 style={[inputTextStyle, segment.facet ? pal.link : pal.text]}> 221 222 {segment.text}
+8 -7
src/view/com/composer/text-input/web/Autocomplete.tsx
··· 5 5 useState, 6 6 } from 'react' 7 7 import {Pressable, StyleSheet, View} from 'react-native' 8 + import {Trans} from '@lingui/macro' 8 9 import {ReactRenderer} from '@tiptap/react' 9 - import tippy, {Instance as TippyInstance} from 'tippy.js' 10 10 import { 11 + SuggestionKeyDownProps, 11 12 SuggestionOptions, 12 13 SuggestionProps, 13 - SuggestionKeyDownProps, 14 14 } from '@tiptap/suggestion' 15 + import tippy, {Instance as TippyInstance} from 'tippy.js' 16 + 17 + import {usePalette} from '#/lib/hooks/usePalette' 15 18 import {ActorAutocompleteFn} from '#/state/queries/actor-autocomplete' 16 - import {usePalette} from 'lib/hooks/usePalette' 17 - import {Text} from 'view/com/util/text/Text' 18 - import {UserAvatar} from 'view/com/util/UserAvatar' 19 + import {Text} from '#/view/com/util/text/Text' 20 + import {UserAvatar} from '#/view/com/util/UserAvatar' 19 21 import {useGrapheme} from '../hooks/useGrapheme' 20 - import {Trans} from '@lingui/macro' 21 22 22 23 interface MentionListRef { 23 24 onKeyDown: (props: SuggestionKeyDownProps) => boolean ··· 180 181 size={26} 181 182 type={item.associated?.labeler ? 'labeler' : 'user'} 182 183 /> 183 - <Text style={pal.text} numberOfLines={1}> 184 + <Text emoji style={pal.text} numberOfLines={1}> 184 185 {displayName} 185 186 </Text> 186 187 </View>
+6 -6
src/view/com/feeds/FeedSourceCard.tsx
··· 12 12 import {msg, Plural, Trans} from '@lingui/macro' 13 13 import {useLingui} from '@lingui/react' 14 14 15 + import {useNavigationDeduped} from '#/lib/hooks/useNavigationDeduped' 16 + import {usePalette} from '#/lib/hooks/usePalette' 17 + import {sanitizeHandle} from '#/lib/strings/handles' 18 + import {s} from '#/lib/styles' 15 19 import {logger} from '#/logger' 16 20 import {shouldClickOpenNewTab} from '#/platform/urls' 17 21 import {FeedSourceInfo, useFeedSourceInfoQuery} from '#/state/queries/feed' ··· 21 25 UsePreferencesQueryResponse, 22 26 useRemoveFeedMutation, 23 27 } from '#/state/queries/preferences' 24 - import {useNavigationDeduped} from 'lib/hooks/useNavigationDeduped' 25 - import {usePalette} from 'lib/hooks/usePalette' 26 - import {sanitizeHandle} from 'lib/strings/handles' 27 - import {s} from 'lib/styles' 28 28 import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' 29 - import * as Toast from 'view/com/util/Toast' 29 + import * as Toast from '#/view/com/util/Toast' 30 30 import {useTheme} from '#/alf' 31 31 import {atoms as a} from '#/alf' 32 32 import * as Prompt from '#/components/Prompt' ··· 242 242 <UserAvatar type="algo" size={36} avatar={feed.avatar} /> 243 243 </View> 244 244 <View style={[styles.headerTextContainer]}> 245 - <Text style={[pal.text, s.bold]} numberOfLines={1}> 245 + <Text emoji style={[pal.text, s.bold]} numberOfLines={1}> 246 246 {feed.displayName} 247 247 </Text> 248 248 <Text style={[pal.textLight]} numberOfLines={1}>
+19 -13
src/view/com/modals/UserAddRemoveLists.tsx
··· 65 65 return [pal.border, {flex: 1, borderTopWidth: StyleSheet.hairlineWidth}] 66 66 }, [pal.border, screenHeight]) 67 67 68 + const headerStyles = [ 69 + { 70 + textAlign: 'center', 71 + fontWeight: '600', 72 + fontSize: 20, 73 + marginBottom: 12, 74 + paddingHorizontal: 12, 75 + } as const, 76 + pal.text, 77 + ] 78 + 68 79 return ( 69 80 <View testID="userAddRemoveListsModal" style={s.hContentRegion}> 70 - <Text 71 - style={[ 72 - { 73 - textAlign: 'center', 74 - fontWeight: '600', 75 - fontSize: 20, 76 - marginBottom: 12, 77 - paddingHorizontal: 12, 78 - }, 79 - pal.text, 80 - ]} 81 - numberOfLines={1}> 82 - <Trans>Update {displayName} in Lists</Trans> 81 + <Text style={headerStyles} numberOfLines={1}> 82 + <Trans> 83 + Update{' '} 84 + <Text style={headerStyles} numberOfLines={1}> 85 + {displayName} 86 + </Text>{' '} 87 + in Lists 88 + </Trans> 83 89 </Text> 84 90 <MyLists 85 91 filter="all"
+16 -7
src/view/com/notifications/FeedItem.tsx
··· 310 310 key={authors[0].href} 311 311 style={[pal.text, s.bold]} 312 312 href={authors[0].href} 313 - text={forceLTR(firstAuthorName)} 313 + text={ 314 + <Text emoji style={[pal.text, s.bold]}> 315 + {forceLTR(firstAuthorName)} 316 + </Text> 317 + } 314 318 disableMismatchWarning 315 319 /> 316 320 {authors.length > 1 ? ( ··· 570 574 numberOfLines={1} 571 575 style={pal.text} 572 576 lineHeight={1.2}> 573 - {sanitizeDisplayName( 574 - author.profile.displayName || author.profile.handle, 575 - )} 576 - &nbsp; 577 + <Text emoji type="lg-bold" style={pal.text} lineHeight={1.2}> 578 + {sanitizeDisplayName( 579 + author.profile.displayName || author.profile.handle, 580 + )} 581 + </Text>{' '} 577 582 <Text style={[pal.textLight]} lineHeight={1.2}> 578 - {sanitizeHandle(author.profile.handle)} 583 + {sanitizeHandle(author.profile.handle, '@')} 579 584 </Text> 580 585 </Text> 581 586 </View> ··· 592 597 593 598 return ( 594 599 <> 595 - {text?.length > 0 && <Text style={pal.textLight}>{text}</Text>} 600 + {text?.length > 0 && ( 601 + <Text emoji style={pal.textLight}> 602 + {text} 603 + </Text> 604 + )} 596 605 <MediaPreview.Embed 597 606 embed={post.embed} 598 607 style={styles.additionalPostImages}
+1
src/view/com/pager/TabBar.tsx
··· 138 138 onPress={() => onPressItem(i)}> 139 139 <View style={[styles.itemInner, selected && indicatorStyle]}> 140 140 <Text 141 + emoji 141 142 type={isDesktop || isTablet ? 'xl-bold' : 'lg-bold'} 142 143 testID={testID ? `${testID}-${item}` : undefined} 143 144 style={[
+18 -13
src/view/com/post-thread/PostThreadItem.tsx
··· 12 12 import {msg, Plural, Trans} from '@lingui/macro' 13 13 import {useLingui} from '@lingui/react' 14 14 15 + import {MAX_POST_LINES} from '#/lib/constants' 16 + import {usePalette} from '#/lib/hooks/usePalette' 17 + import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 18 + import {makeProfileLink} from '#/lib/routes/links' 19 + import {sanitizeDisplayName} from '#/lib/strings/display-names' 20 + import {sanitizeHandle} from '#/lib/strings/handles' 21 + import {countLines} from '#/lib/strings/helpers' 22 + import {niceDate} from '#/lib/strings/time' 23 + import {s} from '#/lib/styles' 24 + import {isWeb} from '#/platform/detection' 15 25 import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow' 16 26 import {useLanguagePrefs} from '#/state/preferences' 17 27 import {useOpenLink} from '#/state/preferences/in-app-browser' 18 28 import {ThreadPost} from '#/state/queries/post-thread' 29 + import {useSession} from '#/state/session' 19 30 import {useComposerControls} from '#/state/shell/composer' 20 31 import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' 21 - import {MAX_POST_LINES} from 'lib/constants' 22 - import {usePalette} from 'lib/hooks/usePalette' 23 - import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 24 - import {makeProfileLink} from 'lib/routes/links' 25 - import {sanitizeDisplayName} from 'lib/strings/display-names' 26 - import {sanitizeHandle} from 'lib/strings/handles' 27 - import {countLines} from 'lib/strings/helpers' 28 - import {niceDate} from 'lib/strings/time' 29 - import {s} from 'lib/styles' 30 - import {isWeb} from 'platform/detection' 31 - import {useSession} from 'state/session' 32 - import {PostThreadFollowBtn} from 'view/com/post-thread/PostThreadFollowBtn' 32 + import {PostThreadFollowBtn} from '#/view/com/post-thread/PostThreadFollowBtn' 33 33 import {atoms as a} from '#/alf' 34 34 import {AppModerationCause} from '#/components/Pills' 35 35 import {RichText} from '#/components/RichText' ··· 308 308 style={[styles.meta, styles.metaExpandedLine1, {zIndex: 1}]}> 309 309 <Link style={s.flex1} href={authorHref} title={authorTitle}> 310 310 <Text 311 + emoji 311 312 type="xl-bold" 312 313 style={[pal.text, a.self_start]} 313 314 numberOfLines={1} ··· 322 323 </View> 323 324 <View style={styles.meta}> 324 325 <Link style={s.flex1} href={authorHref} title={authorTitle}> 325 - <Text type="md" style={[pal.textLight]} numberOfLines={1}> 326 + <Text 327 + emoji 328 + type="md" 329 + style={[pal.textLight]} 330 + numberOfLines={1}> 326 331 {sanitizeHandle(post.author.handle, '@')} 327 332 </Text> 328 333 </Link>
+18 -8
src/view/com/posts/FeedItem.tsx
··· 316 316 style={pal.textLight} 317 317 lineHeight={1.2} 318 318 numberOfLines={1} 319 - text={sanitizeDisplayName( 320 - reason.by.displayName || 321 - sanitizeHandle(reason.by.handle), 322 - moderation.ui('displayName'), 323 - )} 319 + text={ 320 + <Text 321 + emoji 322 + type="sm-bold" 323 + style={pal.textLight} 324 + lineHeight={1.2}> 325 + {sanitizeDisplayName( 326 + reason.by.displayName || 327 + sanitizeHandle(reason.by.handle), 328 + moderation.ui('displayName'), 329 + )} 330 + </Text> 331 + } 324 332 href={makeProfileLink(reason.by)} 325 333 onBeforePress={onOpenReposter} 326 334 /> ··· 527 535 numberOfLines={1} 528 536 href={makeProfileLink(profile)} 529 537 text={ 530 - profile.displayName 531 - ? sanitizeDisplayName(profile.displayName) 532 - : sanitizeHandle(profile.handle) 538 + <Text emoji type="md" style={pal.textLight} lineHeight={1.2}> 539 + {profile.displayName 540 + ? sanitizeDisplayName(profile.displayName) 541 + : sanitizeHandle(profile.handle)} 542 + </Text> 533 543 } 534 544 /> 535 545 </ProfileHoverCard>
+10 -9
src/view/com/profile/ProfileCard.tsx
··· 7 7 } from '@atproto/api' 8 8 import {useQueryClient} from '@tanstack/react-query' 9 9 10 + import {usePalette} from '#/lib/hooks/usePalette' 11 + import {getModerationCauseKey, isJustAMute} from '#/lib/moderation' 12 + import {makeProfileLink} from '#/lib/routes/links' 13 + import {sanitizeDisplayName} from '#/lib/strings/display-names' 14 + import {sanitizeHandle} from '#/lib/strings/handles' 15 + import {s} from '#/lib/styles' 10 16 import {useProfileShadow} from '#/state/cache/profile-shadow' 11 17 import {Shadow} from '#/state/cache/types' 12 18 import {useModerationOpts} from '#/state/preferences/moderation-opts' 19 + import {precacheProfile} from '#/state/queries/profile' 13 20 import {useSession} from '#/state/session' 14 - import {usePalette} from 'lib/hooks/usePalette' 15 - import {getModerationCauseKey, isJustAMute} from 'lib/moderation' 16 - import {makeProfileLink} from 'lib/routes/links' 17 - import {sanitizeDisplayName} from 'lib/strings/display-names' 18 - import {sanitizeHandle} from 'lib/strings/handles' 19 - import {s} from 'lib/styles' 20 - import {precacheProfile} from 'state/queries/profile' 21 21 import {atoms as a} from '#/alf' 22 22 import { 23 23 KnownFollowers, ··· 103 103 </View> 104 104 <View style={styles.layoutContent}> 105 105 <Text 106 + emoji 106 107 type="lg" 107 108 style={[s.bold, pal.text, a.self_start]} 108 109 numberOfLines={1} ··· 112 113 moderation.ui('displayName'), 113 114 )} 114 115 </Text> 115 - <Text type="md" style={[pal.textLight]} numberOfLines={1}> 116 + <Text emoji type="md" style={[pal.textLight]} numberOfLines={1}> 116 117 {sanitizeHandle(profile.handle, '@')} 117 118 </Text> 118 119 <ProfileCardPills ··· 128 129 {profile.description || knownFollowersVisible ? ( 129 130 <View style={styles.details}> 130 131 {profile.description ? ( 131 - <Text style={pal.text} numberOfLines={4}> 132 + <Text emoji style={pal.text} numberOfLines={4}> 132 133 {profile.description as string} 133 134 </Text> 134 135 ) : null}
+28 -16
src/view/com/util/PostMeta.tsx
··· 4 4 import {useLingui} from '@lingui/react' 5 5 import {useQueryClient} from '@tanstack/react-query' 6 6 7 + import {usePalette} from '#/lib/hooks/usePalette' 8 + import {makeProfileLink} from '#/lib/routes/links' 9 + import {forceLTR} from '#/lib/strings/bidi' 10 + import {NON_BREAKING_SPACE} from '#/lib/strings/constants' 11 + import {sanitizeDisplayName} from '#/lib/strings/display-names' 12 + import {sanitizeHandle} from '#/lib/strings/handles' 13 + import {niceDate} from '#/lib/strings/time' 14 + import {TypographyVariant} from '#/lib/ThemeContext' 15 + import {isAndroid} from '#/platform/detection' 7 16 import {precacheProfile} from '#/state/queries/profile' 8 - import {usePalette} from 'lib/hooks/usePalette' 9 - import {makeProfileLink} from 'lib/routes/links' 10 - import {forceLTR} from 'lib/strings/bidi' 11 - import {NON_BREAKING_SPACE} from 'lib/strings/constants' 12 - import {sanitizeDisplayName} from 'lib/strings/display-names' 13 - import {sanitizeHandle} from 'lib/strings/handles' 14 - import {niceDate} from 'lib/strings/time' 15 - import {TypographyVariant} from 'lib/ThemeContext' 16 - import {isAndroid} from 'platform/detection' 17 17 import {ProfileHoverCard} from '#/components/ProfileHoverCard' 18 18 import {TextLinkOnWebOnly} from './Link' 19 19 import {Text} from './text/Text' ··· 73 73 style={[pal.text]} 74 74 lineHeight={1.2} 75 75 disableMismatchWarning 76 - text={forceLTR( 77 - sanitizeDisplayName( 78 - displayName, 79 - opts.moderation?.ui('displayName'), 80 - ), 81 - )} 76 + text={ 77 + <Text 78 + type={opts.displayNameType || 'lg-bold'} 79 + emoji 80 + style={[pal.text]} 81 + lineHeight={1.2}> 82 + {forceLTR( 83 + sanitizeDisplayName( 84 + displayName, 85 + opts.moderation?.ui('displayName'), 86 + ), 87 + )} 88 + </Text> 89 + } 82 90 href={profileLink} 83 91 onBeforePress={onBeforePressAuthor} 84 92 /> ··· 86 94 type="md" 87 95 disableMismatchWarning 88 96 style={[pal.textLight, {flexShrink: 4}]} 89 - text={NON_BREAKING_SPACE + sanitizeHandle(handle, '@')} 97 + text={ 98 + <Text emoji style={[pal.textLight, {flexShrink: 4}]}> 99 + {NON_BREAKING_SPACE + sanitizeHandle(handle, '@')} 100 + </Text> 101 + } 90 102 href={profileLink} 91 103 onBeforePress={onBeforePressAuthor} 92 104 anchorNoUnderline
+18 -13
src/view/com/util/UserInfoText.tsx
··· 1 1 import React from 'react' 2 + import {StyleProp, StyleSheet, TextStyle} from 'react-native' 2 3 import {AppBskyActorGetProfile as GetProfile} from '@atproto/api' 3 - import {StyleProp, StyleSheet, TextStyle} from 'react-native' 4 + 5 + import {makeProfileLink} from '#/lib/routes/links' 6 + import {sanitizeDisplayName} from '#/lib/strings/display-names' 7 + import {sanitizeHandle} from '#/lib/strings/handles' 8 + import {TypographyVariant} from '#/lib/ThemeContext' 9 + import {STALE} from '#/state/queries' 10 + import {useProfileQuery} from '#/state/queries/profile' 4 11 import {TextLinkOnWebOnly} from './Link' 5 - import {Text} from './text/Text' 6 12 import {LoadingPlaceholder} from './LoadingPlaceholder' 7 - import {TypographyVariant} from 'lib/ThemeContext' 8 - import {sanitizeDisplayName} from 'lib/strings/display-names' 9 - import {sanitizeHandle} from 'lib/strings/handles' 10 - import {makeProfileLink} from 'lib/routes/links' 11 - import {useProfileQuery} from '#/state/queries/profile' 12 - import {STALE} from '#/state/queries' 13 + import {Text} from './text/Text' 13 14 14 15 export function UserInfoText({ 15 16 type = 'md', ··· 50 51 lineHeight={1.2} 51 52 numberOfLines={1} 52 53 href={makeProfileLink(profile)} 53 - text={`${prefix || ''}${sanitizeDisplayName( 54 - typeof profile[attr] === 'string' && profile[attr] 55 - ? (profile[attr] as string) 56 - : sanitizeHandle(profile.handle), 57 - )}`} 54 + text={ 55 + <Text emoji type={type} style={style} lineHeight={1.2}> 56 + {`${prefix || ''}${sanitizeDisplayName( 57 + typeof profile[attr] === 'string' && profile[attr] 58 + ? (profile[attr] as string) 59 + : sanitizeHandle(profile.handle), 60 + )}`} 61 + </Text> 62 + } 58 63 /> 59 64 ) 60 65 } else {
+14 -13
src/view/com/util/post-embeds/ExternalLinkEmbed.tsx
··· 5 5 import {msg} from '@lingui/macro' 6 6 import {useLingui} from '@lingui/react' 7 7 8 - import {usePalette} from 'lib/hooks/usePalette' 9 - import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 10 - import {shareUrl} from 'lib/sharing' 11 - import {parseEmbedPlayerFromUrl} from 'lib/strings/embed-player' 8 + import {usePalette} from '#/lib/hooks/usePalette' 9 + import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 10 + import {shareUrl} from '#/lib/sharing' 11 + import {parseEmbedPlayerFromUrl} from '#/lib/strings/embed-player' 12 12 import { 13 13 getStarterPackOgCard, 14 14 parseStarterPackUri, 15 - } from 'lib/strings/starter-pack' 16 - import {toNiceDomain} from 'lib/strings/url-helpers' 17 - import {isNative} from 'platform/detection' 18 - import {useExternalEmbedsPrefs} from 'state/preferences' 19 - import {Link} from 'view/com/util/Link' 20 - import {ExternalGifEmbed} from 'view/com/util/post-embeds/ExternalGifEmbed' 21 - import {ExternalPlayer} from 'view/com/util/post-embeds/ExternalPlayerEmbed' 22 - import {GifEmbed} from 'view/com/util/post-embeds/GifEmbed' 15 + } from '#/lib/strings/starter-pack' 16 + import {toNiceDomain} from '#/lib/strings/url-helpers' 17 + import {isNative} from '#/platform/detection' 18 + import {useExternalEmbedsPrefs} from '#/state/preferences' 19 + import {Link} from '#/view/com/util/Link' 20 + import {ExternalGifEmbed} from '#/view/com/util/post-embeds/ExternalGifEmbed' 21 + import {ExternalPlayer} from '#/view/com/util/post-embeds/ExternalPlayerEmbed' 22 + import {GifEmbed} from '#/view/com/util/post-embeds/GifEmbed' 23 23 import {atoms as a, useTheme} from '#/alf' 24 24 import {MediaInsetBorder} from '#/components/MediaInsetBorder' 25 25 import {Text} from '../text/Text' ··· 115 115 </Text> 116 116 117 117 {!embedPlayerParams?.isGif && !embedPlayerParams?.dimensions && ( 118 - <Text type="lg-bold" numberOfLines={3} style={[pal.text]}> 118 + <Text emoji type="lg-bold" numberOfLines={3} style={[pal.text]}> 119 119 {link.title || link.uri} 120 120 </Text> 121 121 )} 122 122 {link.description ? ( 123 123 <Text 124 + emoji 124 125 type="md" 125 126 numberOfLines={link.thumb ? 2 : 4} 126 127 style={[pal.text, a.mt_xs]}>
+37 -13
src/view/com/util/text/Text.tsx
··· 2 2 import {StyleSheet, Text as RNText, TextProps} from 'react-native' 3 3 import {UITextView} from 'react-native-uitextview' 4 4 5 - import {lh, s} from 'lib/styles' 6 - import {TypographyVariant, useTheme} from 'lib/ThemeContext' 7 - import {isIOS, isWeb} from 'platform/detection' 5 + import {lh, s} from '#/lib/styles' 6 + import {TypographyVariant, useTheme} from '#/lib/ThemeContext' 7 + import {logger} from '#/logger' 8 + import {isIOS} from '#/platform/detection' 8 9 import {applyFonts, useAlf} from '#/alf' 10 + import { 11 + childHasEmoji, 12 + childIsString, 13 + renderChildrenWithEmoji, 14 + StringChild, 15 + } from '#/components/Typography' 16 + import {IS_DEV} from '#/env' 9 17 10 - export type CustomTextProps = TextProps & { 18 + export type CustomTextProps = Omit<TextProps, 'children'> & { 11 19 type?: TypographyVariant 12 20 lineHeight?: number 13 21 title?: string 14 22 dataSet?: Record<string, string | number> 15 23 selectable?: boolean 16 - } 17 - 18 - const fontFamilyStyle = { 19 - fontFamily: 20 - '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Liberation Sans", Helvetica, Arial, sans-serif', 21 - } 24 + } & ( 25 + | { 26 + emoji: true 27 + children: StringChild 28 + } 29 + | { 30 + emoji?: false 31 + children: TextProps['children'] 32 + } 33 + ) 22 34 23 35 export function Text({ 24 36 type = 'md', 25 37 children, 38 + emoji, 26 39 lineHeight, 27 40 style, 28 41 title, ··· 35 48 const lineHeightStyle = lineHeight ? lh(theme, type, lineHeight) : undefined 36 49 const {fonts} = useAlf() 37 50 51 + if (IS_DEV) { 52 + if (!emoji && childHasEmoji(children)) { 53 + logger.warn( 54 + `Text: emoji detected but emoji not enabled: "${children}"\n\nPlease add <Text emoji />'`, 55 + ) 56 + } 57 + 58 + if (emoji && !childIsString(children)) { 59 + logger.error('Text: when <Text emoji />, children can only be strings.') 60 + } 61 + } 62 + 38 63 if (selectable && isIOS) { 39 64 const flattened = StyleSheet.flatten([ 40 65 s.black, ··· 58 83 selectable={selectable} 59 84 uiTextView 60 85 {...props}> 61 - {children} 86 + {isIOS && emoji ? renderChildrenWithEmoji(children) : children} 62 87 </UITextView> 63 88 ) 64 89 } ··· 66 91 const flattened = StyleSheet.flatten([ 67 92 s.black, 68 93 typography, 69 - isWeb && fontFamilyStyle, 70 94 lineHeightStyle, 71 95 style, 72 96 ]) ··· 87 111 dataSet={Object.assign({tooltip: title}, dataSet || {})} 88 112 selectable={selectable} 89 113 {...props}> 90 - {children} 114 + {isIOS && emoji ? renderChildrenWithEmoji(children) : children} 91 115 </RNText> 92 116 ) 93 117 }
-80
src/view/com/util/text/ThemedText.tsx
··· 1 - import React from 'react' 2 - import {CustomTextProps, Text} from './Text' 3 - import {usePalette} from 'lib/hooks/usePalette' 4 - import {addStyle} from 'lib/styles' 5 - 6 - export type ThemedTextProps = CustomTextProps & { 7 - fg?: 'default' | 'light' | 'error' | 'inverted' | 'inverted-light' 8 - bg?: 'default' | 'light' | 'error' | 'inverted' | 'inverted-light' 9 - border?: 'default' | 'dark' | 'error' | 'inverted' | 'inverted-dark' 10 - lineHeight?: number 11 - } 12 - 13 - export function ThemedText({ 14 - fg, 15 - bg, 16 - border, 17 - style, 18 - children, 19 - ...props 20 - }: React.PropsWithChildren<ThemedTextProps>) { 21 - const pal = usePalette('default') 22 - const palInverted = usePalette('inverted') 23 - const palError = usePalette('error') 24 - switch (fg) { 25 - case 'default': 26 - style = addStyle(style, pal.text) 27 - break 28 - case 'light': 29 - style = addStyle(style, pal.textLight) 30 - break 31 - case 'error': 32 - style = addStyle(style, {color: palError.colors.background}) 33 - break 34 - case 'inverted': 35 - style = addStyle(style, palInverted.text) 36 - break 37 - case 'inverted-light': 38 - style = addStyle(style, palInverted.textLight) 39 - break 40 - } 41 - switch (bg) { 42 - case 'default': 43 - style = addStyle(style, pal.view) 44 - break 45 - case 'light': 46 - style = addStyle(style, pal.viewLight) 47 - break 48 - case 'error': 49 - style = addStyle(style, palError.view) 50 - break 51 - case 'inverted': 52 - style = addStyle(style, palInverted.view) 53 - break 54 - case 'inverted-light': 55 - style = addStyle(style, palInverted.viewLight) 56 - break 57 - } 58 - switch (border) { 59 - case 'default': 60 - style = addStyle(style, pal.border) 61 - break 62 - case 'dark': 63 - style = addStyle(style, pal.borderDark) 64 - break 65 - case 'error': 66 - style = addStyle(style, palError.border) 67 - break 68 - case 'inverted': 69 - style = addStyle(style, palInverted.border) 70 - break 71 - case 'inverted-dark': 72 - style = addStyle(style, palInverted.borderDark) 73 - break 74 - } 75 - return ( 76 - <Text style={style} {...props}> 77 - {children} 78 - </Text> 79 - ) 80 - }
+1
src/view/screens/Search/Search.tsx
··· 959 959 accessibilityIgnoresInvertColors 960 960 /> 961 961 <Text 962 + emoji 962 963 style={[pal.text, styles.profileName]} 963 964 numberOfLines={1}> 964 965 {profile.displayName || profile.handle}
+6 -5
src/view/shell/desktop/Search.tsx
··· 16 16 import {StackActions, useNavigation} from '@react-navigation/native' 17 17 import {useQueryClient} from '@tanstack/react-query' 18 18 19 + import {usePalette} from '#/lib/hooks/usePalette' 19 20 import {makeProfileLink} from '#/lib/routes/links' 21 + import {NavigationProp} from '#/lib/routes/types' 20 22 import {sanitizeDisplayName} from '#/lib/strings/display-names' 21 23 import {sanitizeHandle} from '#/lib/strings/handles' 22 24 import {s} from '#/lib/styles' 23 25 import {useModerationOpts} from '#/state/preferences/moderation-opts' 24 26 import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete' 25 - import {usePalette} from 'lib/hooks/usePalette' 26 - import {NavigationProp} from 'lib/routes/types' 27 - import {precacheProfile} from 'state/queries/profile' 27 + import {precacheProfile} from '#/state/queries/profile' 28 + import {SearchInput} from '#/view/com/util/forms/SearchInput' 28 29 import {Link} from '#/view/com/util/Link' 30 + import {Text} from '#/view/com/util/text/Text' 29 31 import {UserAvatar} from '#/view/com/util/UserAvatar' 30 - import {SearchInput} from 'view/com/util/forms/SearchInput' 31 - import {Text} from 'view/com/util/text/Text' 32 32 import {atoms as a} from '#/alf' 33 33 34 34 let SearchLinkCard = ({ ··· 126 126 /> 127 127 <View style={{flex: 1}}> 128 128 <Text 129 + emoji 129 130 type="lg" 130 131 style={[s.bold, pal.text, a.self_start]} 131 132 numberOfLines={1}
+5
yarn.lock
··· 11360 11360 resolved "https://registry.yarnpkg.com/emoji-mart/-/emoji-mart-5.5.2.tgz#3ddbaf053139cf4aa217650078bc1c50ca8381af" 11361 11361 integrity sha512-Sqc/nso4cjxhOwWJsp9xkVm8OF5c+mJLZJFoFfzRuKO+yWiN7K8c96xmtughYb0d/fZ8UC6cLIQ/p4BR6Pv3/A== 11362 11362 11363 + emoji-regex@^10.4.0: 11364 + version "10.4.0" 11365 + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.4.0.tgz#03553afea80b3975749cfcb36f776ca268e413d4" 11366 + integrity sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw== 11367 + 11363 11368 emoji-regex@^8.0.0: 11364 11369 version "8.0.0" 11365 11370 resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"