diff options
author | Eric Bailey <git@esb.lol> | 2024-09-23 10:40:37 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-09-24 00:40:37 +0900 |
commit | 5eb294488f08534abac3335acfa366cffea9259e (patch) | |
tree | 94453e05d751b5b2ef91467460c258ed5e00b80d /src/components | |
parent | 443f3a64069f081764c2f49578108a9570e8e834 (diff) | |
download | voidsky-5eb294488f08534abac3335acfa366cffea9259e.tar.zst |
[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
Diffstat (limited to 'src/components')
-rw-r--r-- | src/components/FeedCard.tsx | 13 | ||||
-rw-r--r-- | src/components/KnownFollowers.tsx | 14 | ||||
-rw-r--r-- | src/components/LabelingServiceCard/index.tsx | 11 | ||||
-rw-r--r-- | src/components/ListCard.tsx | 24 | ||||
-rw-r--r-- | src/components/Pills.tsx | 1 | ||||
-rw-r--r-- | src/components/ProfileCard.tsx | 12 | ||||
-rw-r--r-- | src/components/ReportDialog/SubmitView.tsx | 1 | ||||
-rw-r--r-- | src/components/RichText.tsx | 9 | ||||
-rw-r--r-- | src/components/StarterPack/StarterPackCard.tsx | 27 | ||||
-rw-r--r-- | src/components/StarterPack/Wizard/WizardListCard.tsx | 11 | ||||
-rw-r--r-- | src/components/Typography.tsx | 108 | ||||
-rw-r--r-- | src/components/dms/MessagesListHeader.tsx | 17 | ||||
-rw-r--r-- | src/components/moderation/LabelsOnMeDialog.tsx | 6 | ||||
-rw-r--r-- | src/components/moderation/ModerationDetailsDialog.tsx | 8 |
14 files changed, 193 insertions, 69 deletions
diff --git a/src/components/FeedCard.tsx b/src/components/FeedCard.tsx index e6d664cfd..b28f66f83 100644 --- a/src/components/FeedCard.tsx +++ b/src/components/FeedCard.tsx @@ -11,17 +11,17 @@ import {msg, plural, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useQueryClient} from '@tanstack/react-query' +import {sanitizeHandle} from '#/lib/strings/handles' import {logger} from '#/logger' +import {precacheFeedFromGeneratorView} from '#/state/queries/feed' import { useAddSavedFeedsMutation, usePreferencesQuery, useRemoveFeedMutation, } from '#/state/queries/preferences' -import {sanitizeHandle} from 'lib/strings/handles' -import {precacheFeedFromGeneratorView} from 'state/queries/feed' -import {useSession} from 'state/session' +import {useSession} from '#/state/session' +import * as Toast from '#/view/com/util/Toast' import {UserAvatar} from '#/view/com/util/UserAvatar' -import * as Toast from 'view/com/util/Toast' import {useTheme} from '#/alf' import {atoms as a} from '#/alf' import {Button, ButtonIcon} from '#/components/Button' @@ -121,7 +121,10 @@ export function TitleAndByline({ return ( <View style={[a.flex_1]}> - <Text style={[a.text_md, a.font_bold, a.leading_snug]} numberOfLines={1}> + <Text + emoji + style={[a.text_md, a.font_bold, a.leading_snug]} + numberOfLines={1}> {title} </Text> {creator && ( diff --git a/src/components/KnownFollowers.tsx b/src/components/KnownFollowers.tsx index 4017a7b0b..35a346c3a 100644 --- a/src/components/KnownFollowers.tsx +++ b/src/components/KnownFollowers.tsx @@ -5,7 +5,7 @@ import {msg, Plural, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {makeProfileLink} from '#/lib/routes/links' -import {sanitizeDisplayName} from 'lib/strings/display-names' +import {sanitizeDisplayName} from '#/lib/strings/display-names' import {UserAvatar} from '#/view/com/util/UserAvatar' import {atoms as a, useTheme} from '#/alf' import {Link, LinkProps} from '#/components/Link' @@ -185,11 +185,11 @@ function KnownFollowersInner({ serverCount > 2 ? ( <Trans> Followed by{' '} - <Text key={slice[0].profile.did} style={textStyle}> + <Text emoji key={slice[0].profile.did} style={textStyle}> {slice[0].profile.displayName} </Text> ,{' '} - <Text key={slice[1].profile.did} style={textStyle}> + <Text emoji key={slice[1].profile.did} style={textStyle}> {slice[1].profile.displayName} </Text> , and{' '} @@ -203,11 +203,11 @@ function KnownFollowersInner({ // only 2 <Trans> Followed by{' '} - <Text key={slice[0].profile.did} style={textStyle}> + <Text emoji key={slice[0].profile.did} style={textStyle}> {slice[0].profile.displayName} </Text>{' '} and{' '} - <Text key={slice[1].profile.did} style={textStyle}> + <Text emoji key={slice[1].profile.did} style={textStyle}> {slice[1].profile.displayName} </Text> </Trans> @@ -216,7 +216,7 @@ function KnownFollowersInner({ // 1-n followers, including blocks <Trans> Followed by{' '} - <Text key={slice[0].profile.did} style={textStyle}> + <Text emoji key={slice[0].profile.did} style={textStyle}> {slice[0].profile.displayName} </Text>{' '} and{' '} @@ -230,7 +230,7 @@ function KnownFollowersInner({ // only 1 <Trans> Followed by{' '} - <Text key={slice[0].profile.did} style={textStyle}> + <Text emoji key={slice[0].profile.did} style={textStyle}> {slice[0].profile.displayName} </Text> </Trans> diff --git a/src/components/LabelingServiceCard/index.tsx b/src/components/LabelingServiceCard/index.tsx index 851645a48..03b8ece6b 100644 --- a/src/components/LabelingServiceCard/index.tsx +++ b/src/components/LabelingServiceCard/index.tsx @@ -44,17 +44,22 @@ export function Avatar({avatar}: {avatar?: string}) { } export function Title({value}: {value: string}) { - return <Text style={[a.text_md, a.font_bold, a.leading_tight]}>{value}</Text> + return ( + <Text emoji style={[a.text_md, a.font_bold, a.leading_tight]}> + {value} + </Text> + ) } export function Description({value, handle}: {value?: string; handle: string}) { + const {_} = useLingui() return value ? ( <Text numberOfLines={2}> <RichText value={value} style={[a.leading_snug]} /> </Text> ) : ( - <Text style={[a.leading_snug]}> - <Trans>By {sanitizeHandle(handle, '@')}</Trans> + <Text emoji style={[a.leading_snug]}> + {_(msg`By ${sanitizeHandle(handle, '@')}`)} </Text> ) } diff --git a/src/components/ListCard.tsx b/src/components/ListCard.tsx index 829f36d47..ed5838fb0 100644 --- a/src/components/ListCard.tsx +++ b/src/components/ListCard.tsx @@ -7,13 +7,14 @@ import { moderateUserList, ModerationUI, } from '@atproto/api' -import {Trans} from '@lingui/macro' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' import {useQueryClient} from '@tanstack/react-query' -import {sanitizeHandle} from 'lib/strings/handles' -import {useModerationOpts} from 'state/preferences/moderation-opts' -import {precacheList} from 'state/queries/feed' -import {useSession} from 'state/session' +import {sanitizeHandle} from '#/lib/strings/handles' +import {useModerationOpts} from '#/state/preferences/moderation-opts' +import {precacheList} from '#/state/queries/feed' +import {useSession} from '#/state/session' import {atoms as a, useTheme} from '#/alf' import { Avatar, @@ -111,6 +112,7 @@ export function TitleAndByline({ modUi?: ModerationUI }) { const t = useTheme() + const {_} = useLingui() const {currentAccount} = useSession() return ( @@ -130,6 +132,7 @@ export function TitleAndByline({ </Hider.Mask> <Hider.Content> <Text + emoji style={[a.text_md, a.font_bold, a.leading_snug]} numberOfLines={1}> {title} @@ -139,15 +142,12 @@ export function TitleAndByline({ {creator && ( <Text + emoji style={[a.leading_snug, t.atoms.text_contrast_medium]} numberOfLines={1}> - {purpose === MODLIST ? ( - <Trans> - Moderation list by {sanitizeHandle(creator.handle, '@')} - </Trans> - ) : ( - <Trans>List by {sanitizeHandle(creator.handle, '@')}</Trans> - )} + {purpose === MODLIST + ? _(msg`Moderation list by ${sanitizeHandle(creator.handle, '@')}`) + : _(msg`List by ${sanitizeHandle(creator.handle, '@')}`)} </Text> )} </View> diff --git a/src/components/Pills.tsx b/src/components/Pills.tsx index 6c8084743..974d83593 100644 --- a/src/components/Pills.tsx +++ b/src/components/Pills.tsx @@ -130,6 +130,7 @@ export function Label({ )} <Text + emoji style={[ text, a.font_bold, diff --git a/src/components/ProfileCard.tsx b/src/components/ProfileCard.tsx index b208903b4..50b34ba99 100644 --- a/src/components/ProfileCard.tsx +++ b/src/components/ProfileCard.tsx @@ -11,13 +11,13 @@ import {useLingui} from '@lingui/react' import {LogEvents} from '#/lib/statsig/statsig' import {sanitizeDisplayName} from '#/lib/strings/display-names' +import {sanitizeHandle} from '#/lib/strings/handles' +import {useProfileShadow} from '#/state/cache/profile-shadow' import {useProfileFollowMutationQueue} from '#/state/queries/profile' -import {sanitizeHandle} from 'lib/strings/handles' -import {useProfileShadow} from 'state/cache/profile-shadow' -import {useSession} from 'state/session' +import {useSession} from '#/state/session' +import {ProfileCardPills} from '#/view/com/profile/ProfileCard' import * as Toast from '#/view/com/util/Toast' -import {ProfileCardPills} from 'view/com/profile/ProfileCard' -import {UserAvatar} from 'view/com/util/UserAvatar' +import {UserAvatar} from '#/view/com/util/UserAvatar' import {atoms as a, useTheme} from '#/alf' import {Button, ButtonIcon, ButtonProps, ButtonText} from '#/components/Button' import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' @@ -175,11 +175,13 @@ export function NameAndHandle({ return ( <View style={[a.flex_1]}> <Text + emoji style={[a.text_md, a.font_bold, a.leading_snug, a.self_start]} numberOfLines={1}> {name} </Text> <Text + emoji style={[a.leading_snug, t.atoms.text_contrast_medium]} numberOfLines={1}> {handle} diff --git a/src/components/ReportDialog/SubmitView.tsx b/src/components/ReportDialog/SubmitView.tsx index 2def0fa4b..e323d1504 100644 --- a/src/components/ReportDialog/SubmitView.tsx +++ b/src/components/ReportDialog/SubmitView.tsx @@ -256,6 +256,7 @@ function LabelerToggle({title}: {title: string}) { a.z_10, ]}> <Text + emoji style={[ native({marginTop: 2}), t.atoms.text_contrast_medium, diff --git a/src/components/RichText.tsx b/src/components/RichText.tsx index 751177597..1c65a87ac 100644 --- a/src/components/RichText.tsx +++ b/src/components/RichText.tsx @@ -66,6 +66,7 @@ export function RichText({ (flattenedStyle.fontSize ?? a.text_sm.fontSize) * emojiMultiplier return ( <Text + emoji selectable={selectable} testID={testID} style={[plainStyles, {fontSize}]} @@ -77,6 +78,7 @@ export function RichText({ } return ( <Text + emoji selectable={selectable} testID={testID} style={plainStyles} @@ -148,7 +150,11 @@ export function RichText({ />, ) } else { - els.push(segment.text) + els.push( + <Text key={key} emoji style={plainStyles}> + {segment.text} + </Text>, + ) } key++ } @@ -213,6 +219,7 @@ function RichTextTag({ <React.Fragment> <TagMenu control={control} tag={tag} authorHandle={authorHandle}> <Text + emoji selectable={selectable} {...native({ accessibilityLabel: _(msg`Hashtag: #${tag}`), diff --git a/src/components/StarterPack/StarterPackCard.tsx b/src/components/StarterPack/StarterPackCard.tsx index 4c4bf246e..ead9c9248 100644 --- a/src/components/StarterPack/StarterPackCard.tsx +++ b/src/components/StarterPack/StarterPackCard.tsx @@ -2,15 +2,15 @@ import React from 'react' import {View} from 'react-native' import {Image} from 'expo-image' import {AppBskyGraphDefs, AppBskyGraphStarterpack, AtUri} from '@atproto/api' -import {msg, Trans} from '@lingui/macro' +import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useQueryClient} from '@tanstack/react-query' -import {sanitizeHandle} from 'lib/strings/handles' -import {getStarterPackOgCard} from 'lib/strings/starter-pack' -import {precacheResolvedUri} from 'state/queries/resolve-uri' -import {precacheStarterPack} from 'state/queries/starter-packs' -import {useSession} from 'state/session' +import {sanitizeHandle} from '#/lib/strings/handles' +import {getStarterPackOgCard} from '#/lib/strings/starter-pack' +import {precacheResolvedUri} from '#/state/queries/resolve-uri' +import {precacheStarterPack} from '#/state/queries/starter-packs' +import {useSession} from '#/state/session' import {atoms as a, useTheme} from '#/alf' import {StarterPack} from '#/components/icons/StarterPack' import {BaseLink} from '#/components/Link' @@ -66,21 +66,18 @@ export function Card({ <View style={[a.flex_row, a.gap_sm]}> {!noIcon ? <StarterPack width={40} gradient="sky" /> : null} <View> - <Text style={[a.text_md, a.font_bold, a.leading_snug]}> + <Text emoji style={[a.text_md, a.font_bold, a.leading_snug]}> {record.name} </Text> - <Text style={[a.leading_snug, t.atoms.text_contrast_medium]}> - <Trans> - Starter pack by{' '} - {creator?.did === currentAccount?.did - ? _(msg`you`) - : `@${sanitizeHandle(creator.handle)}`} - </Trans> + <Text emoji style={[a.leading_snug, t.atoms.text_contrast_medium]}> + {creator?.did === currentAccount?.did + ? _(msg`Starter pack by you`) + : _(msg`Starter pack by ${sanitizeHandle(creator.handle, '@')}`)} </Text> </View> </View> {!noDescription && record.description ? ( - <Text numberOfLines={3} style={[a.leading_snug]}> + <Text emoji numberOfLines={3} style={[a.leading_snug]}> {record.description} </Text> ) : null} diff --git a/src/components/StarterPack/Wizard/WizardListCard.tsx b/src/components/StarterPack/Wizard/WizardListCard.tsx index ad02cdc30..44f01a154 100644 --- a/src/components/StarterPack/Wizard/WizardListCard.tsx +++ b/src/components/StarterPack/Wizard/WizardListCard.tsx @@ -12,11 +12,11 @@ import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {DISCOVER_FEED_URI, STARTER_PACK_MAX_SIZE} from 'lib/constants' -import {sanitizeDisplayName} from 'lib/strings/display-names' -import {sanitizeHandle} from 'lib/strings/handles' -import {useSession} from 'state/session' -import {UserAvatar} from 'view/com/util/UserAvatar' +import {DISCOVER_FEED_URI, STARTER_PACK_MAX_SIZE} from '#/lib/constants' +import {sanitizeDisplayName} from '#/lib/strings/display-names' +import {sanitizeHandle} from '#/lib/strings/handles' +import {useSession} from '#/state/session' +import {UserAvatar} from '#/view/com/util/UserAvatar' import {WizardAction, WizardState} from '#/screens/StarterPack/Wizard/State' import {atoms as a, useTheme} from '#/alf' import {Button, ButtonText} from '#/components/Button' @@ -78,6 +78,7 @@ function WizardListCard({ /> <View style={[a.flex_1, a.gap_2xs]}> <Text + emoji style={[ a.flex_1, a.font_bold, diff --git a/src/components/Typography.tsx b/src/components/Typography.tsx index 15f88468a..501e23872 100644 --- a/src/components/Typography.tsx +++ b/src/components/Typography.tsx @@ -1,15 +1,85 @@ import React from 'react' import {StyleProp, TextProps as RNTextProps, TextStyle} from 'react-native' import {UITextView} from 'react-native-uitextview' +import createEmojiRegex from 'emoji-regex' -import {isNative} from '#/platform/detection' +import {logger} from '#/logger' +import {isIOS, isNative} from '#/platform/detection' import {Alf, applyFonts, atoms, flatten, useAlf, useTheme, web} from '#/alf' +import {IS_DEV} from '#/env' -export type TextProps = RNTextProps & { +export type StringChild = string | (string | null)[] + +export type TextProps = Omit<RNTextProps, 'children'> & { /** * Lets the user select text, to use the native copy and paste functionality. */ selectable?: boolean + /** + * Provides `data-*` attributes to the underlying `UITextView` component on + * web only. + */ + dataSet?: Record<string, string | number | undefined> + /** + * Appears as a small tooltip on web hover. + */ + title?: string +} & ( + | { + emoji: true + children: StringChild + } + | { + emoji?: false + children: RNTextProps['children'] + } + ) + +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)) + ) +} + +export function renderChildrenWithEmoji(children: StringChild) { + const normalized = Array.isArray(children) ? children : [children] + + return ( + <UITextView> + {normalized.map(child => { + if (typeof child !== 'string') return child + + const emojis = child.match(EMOJI) + + if (emojis === null) { + return child + } + + return child.split(EMOJI).map((stringPart, index) => ( + <UITextView key={index}> + {stringPart} + {emojis[index] ? ( + <UITextView style={{color: 'black', fontFamily: 'System'}}> + {emojis[index]} + </UITextView> + ) : null} + </UITextView> + )) + })} + </UITextView> + ) } /** @@ -64,7 +134,15 @@ export function normalizeTextStyles( /** * Our main text component. Use this most of the time. */ -export function Text({style, selectable, ...rest}: TextProps) { +export function Text({ + children, + emoji, + style, + selectable, + title, + dataSet, + ...rest +}: TextProps) { const {fonts, flags} = useAlf() const t = useTheme() const s = normalizeTextStyles([atoms.text_sm, t.atoms.text, flatten(style)], { @@ -73,7 +151,29 @@ export function Text({style, selectable, ...rest}: TextProps) { flags, }) - return <UITextView selectable={selectable} uiTextView style={s} {...rest} /> + if (IS_DEV) { + if (!emoji && childHasEmoji(children)) { + logger.warn( + `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.') + } + } + + return ( + <UITextView + selectable={selectable} + uiTextView + style={s} + {...rest} + // @ts-ignore + dataSet={Object.assign({tooltip: title}, dataSet || {})}> + {isIOS && emoji ? renderChildrenWithEmoji(children) : children} + </UITextView> + ) } export function createHeadingElement({level}: {level: number}) { diff --git a/src/components/dms/MessagesListHeader.tsx b/src/components/dms/MessagesListHeader.tsx index 1a6bbbe60..ab9ec16e4 100644 --- a/src/components/dms/MessagesListHeader.tsx +++ b/src/components/dms/MessagesListHeader.tsx @@ -10,14 +10,14 @@ import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useNavigation} from '@react-navigation/native' -import {BACK_HITSLOP} from 'lib/constants' -import {makeProfileLink} from 'lib/routes/links' -import {NavigationProp} from 'lib/routes/types' -import {sanitizeDisplayName} from 'lib/strings/display-names' -import {isWeb} from 'platform/detection' -import {useProfileShadow} from 'state/cache/profile-shadow' -import {isConvoActive, useConvo} from 'state/messages/convo' -import {PreviewableUserAvatar} from 'view/com/util/UserAvatar' +import {BACK_HITSLOP} from '#/lib/constants' +import {makeProfileLink} from '#/lib/routes/links' +import {NavigationProp} from '#/lib/routes/types' +import {sanitizeDisplayName} from '#/lib/strings/display-names' +import {isWeb} from '#/platform/detection' +import {useProfileShadow} from '#/state/cache/profile-shadow' +import {isConvoActive, useConvo} from '#/state/messages/convo' +import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' import {ConvoMenu} from '#/components/dms/ConvoMenu' import {Bell2Off_Filled_Corner0_Rounded as BellStroke} from '#/components/icons/Bell2' @@ -170,6 +170,7 @@ function HeaderReady({ </View> <View style={a.flex_1}> <Text + emoji style={[ a.text_md, a.font_bold, diff --git a/src/components/moderation/LabelsOnMeDialog.tsx b/src/components/moderation/LabelsOnMeDialog.tsx index c54a04a78..e63cea93b 100644 --- a/src/components/moderation/LabelsOnMeDialog.tsx +++ b/src/components/moderation/LabelsOnMeDialog.tsx @@ -132,8 +132,10 @@ function Label({ ]}> <View style={[a.p_md, a.gap_sm, a.flex_row]}> <View style={[a.flex_1, a.gap_xs]}> - <Text style={[a.font_bold, a.text_md]}>{strings.name}</Text> - <Text style={[t.atoms.text_contrast_medium, a.leading_snug]}> + <Text emoji style={[a.font_bold, a.text_md]}> + {strings.name} + </Text> + <Text emoji style={[t.atoms.text_contrast_medium, a.leading_snug]}> {strings.description} </Text> </View> diff --git a/src/components/moderation/ModerationDetailsDialog.tsx b/src/components/moderation/ModerationDetailsDialog.tsx index d95717cf4..225917853 100644 --- a/src/components/moderation/ModerationDetailsDialog.tsx +++ b/src/components/moderation/ModerationDetailsDialog.tsx @@ -118,7 +118,11 @@ function ModerationDetailsDialogInner({ : _(msg`The author of this thread has hidden this reply.`) } else if (modcause.type === 'label') { name = desc.name - description = desc.description + description = ( + <Text emoji style={[t.atoms.text, a.text_md, a.leading_snug]}> + {desc.description} + </Text> + ) } else { // should never happen name = '' @@ -127,7 +131,7 @@ function ModerationDetailsDialogInner({ return ( <Dialog.ScrollableInner label={_(msg`Moderation details`)}> - <Text style={[t.atoms.text, a.text_2xl, a.font_bold, a.mb_sm]}> + <Text emoji style={[t.atoms.text, a.text_2xl, a.font_bold, a.mb_sm]}> {name} </Text> <Text style={[t.atoms.text, a.text_md, a.leading_snug]}> |