diff options
35 files changed, 423 insertions, 289 deletions
diff --git a/package.json b/package.json index bb9706c39..245c095f1 100644 --- a/package.json +++ b/package.json @@ -116,6 +116,7 @@ "deprecated-react-native-prop-types": "^5.0.0", "email-validator": "^2.0.4", "emoji-mart": "^5.5.2", + "emoji-regex": "^10.4.0", "eventemitter3": "^5.0.1", "expo": "^51.0.8", "expo-application": "^5.9.1", 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]}> diff --git a/src/screens/Messages/List/ChatListItem.tsx b/src/screens/Messages/List/ChatListItem.tsx index c45cc28d7..e9668b4e1 100644 --- a/src/screens/Messages/List/ChatListItem.tsx +++ b/src/screens/Messages/List/ChatListItem.tsx @@ -10,6 +10,10 @@ import { import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {useHaptics} from '#/lib/haptics' +import {decrementBadgeCount} from '#/lib/notifications/notifications' +import {logEvent} from '#/lib/statsig/statsig' +import {sanitizeDisplayName} from '#/lib/strings/display-names' import { postUriToRelativePath, toBskyAppUrl, @@ -19,10 +23,6 @@ import {isNative} from '#/platform/detection' import {useProfileShadow} from '#/state/cache/profile-shadow' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useSession} from '#/state/session' -import {useHaptics} from 'lib/haptics' -import {decrementBadgeCount} from 'lib/notifications/notifications' -import {logEvent} from 'lib/statsig/statsig' -import {sanitizeDisplayName} from 'lib/strings/display-names' import {TimeElapsed} from '#/view/com/util/TimeElapsed' import {UserAvatar} from '#/view/com/util/UserAvatar' import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' @@ -248,6 +248,7 @@ function ChatListItemReady({ numberOfLines={1} style={[{maxWidth: '85%'}, web([a.leading_normal])]}> <Text + emoji style={[ a.text_md, t.atoms.text, @@ -301,6 +302,7 @@ function ChatListItemReady({ )} <Text + emoji numberOfLines={2} style={[ a.text_sm, diff --git a/src/screens/Profile/Header/DisplayName.tsx b/src/screens/Profile/Header/DisplayName.tsx index ca966a01f..e30162c3a 100644 --- a/src/screens/Profile/Header/DisplayName.tsx +++ b/src/screens/Profile/Header/DisplayName.tsx @@ -19,6 +19,7 @@ export function ProfileHeaderDisplayName({ return ( <View pointerEvents="none"> <Text + emoji testID="profileHeaderDisplayName" style={[t.atoms.text, a.text_4xl, a.self_start, {fontWeight: '600'}]}> {sanitizeDisplayName( diff --git a/src/screens/Profile/Header/Handle.tsx b/src/screens/Profile/Header/Handle.tsx index 0344f1a23..ba869b662 100644 --- a/src/screens/Profile/Header/Handle.tsx +++ b/src/screens/Profile/Header/Handle.tsx @@ -1,11 +1,12 @@ import React from 'react' import {View} from 'react-native' import {AppBskyActorDefs} from '@atproto/api' -import {Trans} from '@lingui/macro' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {isInvalidHandle} from '#/lib/strings/handles' +import {isIOS} from '#/platform/detection' import {Shadow} from '#/state/cache/types' -import {isInvalidHandle} from 'lib/strings/handles' -import {isIOS} from 'platform/detection' import {atoms as a, useTheme, web} from '#/alf' import {NewskieDialog} from '#/components/NewskieDialog' import {Text} from '#/components/Typography' @@ -18,6 +19,7 @@ export function ProfileHeaderHandle({ disableTaps?: boolean }) { const t = useTheme() + const {_} = useLingui() const invalidHandle = isInvalidHandle(profile.handle) const blockHide = profile.viewer?.blocking || profile.viewer?.blockedBy return ( @@ -33,6 +35,7 @@ export function ProfileHeaderHandle({ </View> ) : undefined} <Text + emoji numberOfLines={1} style={[ invalidHandle @@ -47,7 +50,7 @@ export function ProfileHeaderHandle({ : [a.text_md, a.leading_tight, t.atoms.text_contrast_medium], web({wordBreak: 'break-all'}), ]}> - {invalidHandle ? <Trans>⚠Invalid Handle</Trans> : `@${profile.handle}`} + {invalidHandle ? _(msg`⚠Invalid Handle`) : `@${profile.handle}`} </Text> </View> ) diff --git a/src/view/com/composer/text-input/TextInput.tsx b/src/view/com/composer/text-input/TextInput.tsx index 778439259..95c57ad89 100644 --- a/src/view/com/composer/text-input/TextInput.tsx +++ b/src/view/com/composer/text-input/TextInput.tsx @@ -19,19 +19,19 @@ import PasteInput, { PasteInputRef, } from '@mattermost/react-native-paste-input' +import {POST_IMG_MAX} from '#/lib/constants' +import {usePalette} from '#/lib/hooks/usePalette' +import {downloadAndResize} from '#/lib/media/manip' +import {isUriImage} from '#/lib/media/util' +import {cleanError} from '#/lib/strings/errors' +import {getMentionAt, insertMentionAt} from '#/lib/strings/mention-manip' +import {useTheme} from '#/lib/ThemeContext' import {isAndroid} from '#/platform/detection' -import {POST_IMG_MAX} from 'lib/constants' -import {usePalette} from 'lib/hooks/usePalette' -import {downloadAndResize} from 'lib/media/manip' -import {isUriImage} from 'lib/media/util' -import {cleanError} from 'lib/strings/errors' -import {getMentionAt, insertMentionAt} from 'lib/strings/mention-manip' -import {useTheme} from 'lib/ThemeContext' import { LinkFacetMatch, suggestLinkCardUri, -} from 'view/com/composer/text-input/text-input-util' -import {Text} from 'view/com/util/text/Text' +} from '#/view/com/composer/text-input/text-input-util' +import {Text} from '#/view/com/util/text/Text' import {atoms as a, useAlf} from '#/alf' import {normalizeTextStyles} from '#/components/Typography' import {Autocomplete} from './mobile/Autocomplete' @@ -216,6 +216,7 @@ export const TextInput = forwardRef(function TextInputImpl( return Array.from(richtext.segments()).map(segment => { return ( <Text + emoji key={i++} style={[inputTextStyle, segment.facet ? pal.link : pal.text]}> {segment.text} diff --git a/src/view/com/composer/text-input/web/Autocomplete.tsx b/src/view/com/composer/text-input/web/Autocomplete.tsx index 29b8f0bc6..a43e67c04 100644 --- a/src/view/com/composer/text-input/web/Autocomplete.tsx +++ b/src/view/com/composer/text-input/web/Autocomplete.tsx @@ -5,19 +5,20 @@ import React, { useState, } from 'react' import {Pressable, StyleSheet, View} from 'react-native' +import {Trans} from '@lingui/macro' import {ReactRenderer} from '@tiptap/react' -import tippy, {Instance as TippyInstance} from 'tippy.js' import { + SuggestionKeyDownProps, SuggestionOptions, SuggestionProps, - SuggestionKeyDownProps, } from '@tiptap/suggestion' +import tippy, {Instance as TippyInstance} from 'tippy.js' + +import {usePalette} from '#/lib/hooks/usePalette' import {ActorAutocompleteFn} from '#/state/queries/actor-autocomplete' -import {usePalette} from 'lib/hooks/usePalette' -import {Text} from 'view/com/util/text/Text' -import {UserAvatar} from 'view/com/util/UserAvatar' +import {Text} from '#/view/com/util/text/Text' +import {UserAvatar} from '#/view/com/util/UserAvatar' import {useGrapheme} from '../hooks/useGrapheme' -import {Trans} from '@lingui/macro' interface MentionListRef { onKeyDown: (props: SuggestionKeyDownProps) => boolean @@ -180,7 +181,7 @@ const MentionList = forwardRef<MentionListRef, SuggestionProps>( size={26} type={item.associated?.labeler ? 'labeler' : 'user'} /> - <Text style={pal.text} numberOfLines={1}> + <Text emoji style={pal.text} numberOfLines={1}> {displayName} </Text> </View> diff --git a/src/view/com/feeds/FeedSourceCard.tsx b/src/view/com/feeds/FeedSourceCard.tsx index 68437c37a..3276cf882 100644 --- a/src/view/com/feeds/FeedSourceCard.tsx +++ b/src/view/com/feeds/FeedSourceCard.tsx @@ -12,6 +12,10 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {msg, Plural, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {useNavigationDeduped} from '#/lib/hooks/useNavigationDeduped' +import {usePalette} from '#/lib/hooks/usePalette' +import {sanitizeHandle} from '#/lib/strings/handles' +import {s} from '#/lib/styles' import {logger} from '#/logger' import {shouldClickOpenNewTab} from '#/platform/urls' import {FeedSourceInfo, useFeedSourceInfoQuery} from '#/state/queries/feed' @@ -21,12 +25,8 @@ import { UsePreferencesQueryResponse, useRemoveFeedMutation, } from '#/state/queries/preferences' -import {useNavigationDeduped} from 'lib/hooks/useNavigationDeduped' -import {usePalette} from 'lib/hooks/usePalette' -import {sanitizeHandle} from 'lib/strings/handles' -import {s} from 'lib/styles' import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' -import * as Toast from 'view/com/util/Toast' +import * as Toast from '#/view/com/util/Toast' import {useTheme} from '#/alf' import {atoms as a} from '#/alf' import * as Prompt from '#/components/Prompt' @@ -242,7 +242,7 @@ export function FeedSourceCardLoaded({ <UserAvatar type="algo" size={36} avatar={feed.avatar} /> </View> <View style={[styles.headerTextContainer]}> - <Text style={[pal.text, s.bold]} numberOfLines={1}> + <Text emoji style={[pal.text, s.bold]} numberOfLines={1}> {feed.displayName} </Text> <Text style={[pal.textLight]} numberOfLines={1}> diff --git a/src/view/com/modals/UserAddRemoveLists.tsx b/src/view/com/modals/UserAddRemoveLists.tsx index 29caf4660..b0b76644f 100644 --- a/src/view/com/modals/UserAddRemoveLists.tsx +++ b/src/view/com/modals/UserAddRemoveLists.tsx @@ -65,21 +65,27 @@ export function Component({ return [pal.border, {flex: 1, borderTopWidth: StyleSheet.hairlineWidth}] }, [pal.border, screenHeight]) + const headerStyles = [ + { + textAlign: 'center', + fontWeight: '600', + fontSize: 20, + marginBottom: 12, + paddingHorizontal: 12, + } as const, + pal.text, + ] + return ( <View testID="userAddRemoveListsModal" style={s.hContentRegion}> - <Text - style={[ - { - textAlign: 'center', - fontWeight: '600', - fontSize: 20, - marginBottom: 12, - paddingHorizontal: 12, - }, - pal.text, - ]} - numberOfLines={1}> - <Trans>Update {displayName} in Lists</Trans> + <Text style={headerStyles} numberOfLines={1}> + <Trans> + Update{' '} + <Text style={headerStyles} numberOfLines={1}> + {displayName} + </Text>{' '} + in Lists + </Trans> </Text> <MyLists filter="all" diff --git a/src/view/com/notifications/FeedItem.tsx b/src/view/com/notifications/FeedItem.tsx index 5dd328062..5fbaaa155 100644 --- a/src/view/com/notifications/FeedItem.tsx +++ b/src/view/com/notifications/FeedItem.tsx @@ -310,7 +310,11 @@ let FeedItem = ({ key={authors[0].href} style={[pal.text, s.bold]} href={authors[0].href} - text={forceLTR(firstAuthorName)} + text={ + <Text emoji style={[pal.text, s.bold]}> + {forceLTR(firstAuthorName)} + </Text> + } disableMismatchWarning /> {authors.length > 1 ? ( @@ -570,12 +574,13 @@ function ExpandedAuthorsList({ numberOfLines={1} style={pal.text} lineHeight={1.2}> - {sanitizeDisplayName( - author.profile.displayName || author.profile.handle, - )} - + <Text emoji type="lg-bold" style={pal.text} lineHeight={1.2}> + {sanitizeDisplayName( + author.profile.displayName || author.profile.handle, + )} + </Text>{' '} <Text style={[pal.textLight]} lineHeight={1.2}> - {sanitizeHandle(author.profile.handle)} + {sanitizeHandle(author.profile.handle, '@')} </Text> </Text> </View> @@ -592,7 +597,11 @@ function AdditionalPostText({post}: {post?: AppBskyFeedDefs.PostView}) { return ( <> - {text?.length > 0 && <Text style={pal.textLight}>{text}</Text>} + {text?.length > 0 && ( + <Text emoji style={pal.textLight}> + {text} + </Text> + )} <MediaPreview.Embed embed={post.embed} style={styles.additionalPostImages} diff --git a/src/view/com/pager/TabBar.tsx b/src/view/com/pager/TabBar.tsx index d36d794b7..83de3775c 100644 --- a/src/view/com/pager/TabBar.tsx +++ b/src/view/com/pager/TabBar.tsx @@ -138,6 +138,7 @@ export function TabBar({ onPress={() => onPressItem(i)}> <View style={[styles.itemInner, selected && indicatorStyle]}> <Text + emoji type={isDesktop || isTablet ? 'xl-bold' : 'lg-bold'} testID={testID ? `${testID}-${item}` : undefined} style={[ diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index 8cd6e70be..3fb2309b9 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -12,24 +12,24 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {msg, Plural, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {MAX_POST_LINES} from '#/lib/constants' +import {usePalette} from '#/lib/hooks/usePalette' +import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' +import {makeProfileLink} from '#/lib/routes/links' +import {sanitizeDisplayName} from '#/lib/strings/display-names' +import {sanitizeHandle} from '#/lib/strings/handles' +import {countLines} from '#/lib/strings/helpers' +import {niceDate} from '#/lib/strings/time' +import {s} from '#/lib/styles' +import {isWeb} from '#/platform/detection' import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow' import {useLanguagePrefs} from '#/state/preferences' import {useOpenLink} from '#/state/preferences/in-app-browser' import {ThreadPost} from '#/state/queries/post-thread' +import {useSession} from '#/state/session' import {useComposerControls} from '#/state/shell/composer' import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' -import {MAX_POST_LINES} from 'lib/constants' -import {usePalette} from 'lib/hooks/usePalette' -import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import {makeProfileLink} from 'lib/routes/links' -import {sanitizeDisplayName} from 'lib/strings/display-names' -import {sanitizeHandle} from 'lib/strings/handles' -import {countLines} from 'lib/strings/helpers' -import {niceDate} from 'lib/strings/time' -import {s} from 'lib/styles' -import {isWeb} from 'platform/detection' -import {useSession} from 'state/session' -import {PostThreadFollowBtn} from 'view/com/post-thread/PostThreadFollowBtn' +import {PostThreadFollowBtn} from '#/view/com/post-thread/PostThreadFollowBtn' import {atoms as a} from '#/alf' import {AppModerationCause} from '#/components/Pills' import {RichText} from '#/components/RichText' @@ -308,6 +308,7 @@ let PostThreadItemLoaded = ({ style={[styles.meta, styles.metaExpandedLine1, {zIndex: 1}]}> <Link style={s.flex1} href={authorHref} title={authorTitle}> <Text + emoji type="xl-bold" style={[pal.text, a.self_start]} numberOfLines={1} @@ -322,7 +323,11 @@ let PostThreadItemLoaded = ({ </View> <View style={styles.meta}> <Link style={s.flex1} href={authorHref} title={authorTitle}> - <Text type="md" style={[pal.textLight]} numberOfLines={1}> + <Text + emoji + type="md" + style={[pal.textLight]} + numberOfLines={1}> {sanitizeHandle(post.author.handle, '@')} </Text> </Link> diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx index 7537a4644..b1509b271 100644 --- a/src/view/com/posts/FeedItem.tsx +++ b/src/view/com/posts/FeedItem.tsx @@ -316,11 +316,19 @@ let FeedItemInner = ({ style={pal.textLight} lineHeight={1.2} numberOfLines={1} - text={sanitizeDisplayName( - reason.by.displayName || - sanitizeHandle(reason.by.handle), - moderation.ui('displayName'), - )} + text={ + <Text + emoji + type="sm-bold" + style={pal.textLight} + lineHeight={1.2}> + {sanitizeDisplayName( + reason.by.displayName || + sanitizeHandle(reason.by.handle), + moderation.ui('displayName'), + )} + </Text> + } href={makeProfileLink(reason.by)} onBeforePress={onOpenReposter} /> @@ -527,9 +535,11 @@ function ReplyToLabel({ numberOfLines={1} href={makeProfileLink(profile)} text={ - profile.displayName - ? sanitizeDisplayName(profile.displayName) - : sanitizeHandle(profile.handle) + <Text emoji type="md" style={pal.textLight} lineHeight={1.2}> + {profile.displayName + ? sanitizeDisplayName(profile.displayName) + : sanitizeHandle(profile.handle)} + </Text> } /> </ProfileHoverCard> diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx index fd32e37a4..eab8611dd 100644 --- a/src/view/com/profile/ProfileCard.tsx +++ b/src/view/com/profile/ProfileCard.tsx @@ -7,17 +7,17 @@ import { } from '@atproto/api' import {useQueryClient} from '@tanstack/react-query' +import {usePalette} from '#/lib/hooks/usePalette' +import {getModerationCauseKey, isJustAMute} from '#/lib/moderation' +import {makeProfileLink} from '#/lib/routes/links' +import {sanitizeDisplayName} from '#/lib/strings/display-names' +import {sanitizeHandle} from '#/lib/strings/handles' +import {s} from '#/lib/styles' import {useProfileShadow} from '#/state/cache/profile-shadow' import {Shadow} from '#/state/cache/types' import {useModerationOpts} from '#/state/preferences/moderation-opts' +import {precacheProfile} from '#/state/queries/profile' import {useSession} from '#/state/session' -import {usePalette} from 'lib/hooks/usePalette' -import {getModerationCauseKey, isJustAMute} from 'lib/moderation' -import {makeProfileLink} from 'lib/routes/links' -import {sanitizeDisplayName} from 'lib/strings/display-names' -import {sanitizeHandle} from 'lib/strings/handles' -import {s} from 'lib/styles' -import {precacheProfile} from 'state/queries/profile' import {atoms as a} from '#/alf' import { KnownFollowers, @@ -103,6 +103,7 @@ export function ProfileCard({ </View> <View style={styles.layoutContent}> <Text + emoji type="lg" style={[s.bold, pal.text, a.self_start]} numberOfLines={1} @@ -112,7 +113,7 @@ export function ProfileCard({ moderation.ui('displayName'), )} </Text> - <Text type="md" style={[pal.textLight]} numberOfLines={1}> + <Text emoji type="md" style={[pal.textLight]} numberOfLines={1}> {sanitizeHandle(profile.handle, '@')} </Text> <ProfileCardPills @@ -128,7 +129,7 @@ export function ProfileCard({ {profile.description || knownFollowersVisible ? ( <View style={styles.details}> {profile.description ? ( - <Text style={pal.text} numberOfLines={4}> + <Text emoji style={pal.text} numberOfLines={4}> {profile.description as string} </Text> ) : null} diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx index 3bd350bf3..f2d717e96 100644 --- a/src/view/com/util/PostMeta.tsx +++ b/src/view/com/util/PostMeta.tsx @@ -4,16 +4,16 @@ import {AppBskyActorDefs, ModerationDecision, ModerationUI} from '@atproto/api' import {useLingui} from '@lingui/react' import {useQueryClient} from '@tanstack/react-query' +import {usePalette} from '#/lib/hooks/usePalette' +import {makeProfileLink} from '#/lib/routes/links' +import {forceLTR} from '#/lib/strings/bidi' +import {NON_BREAKING_SPACE} from '#/lib/strings/constants' +import {sanitizeDisplayName} from '#/lib/strings/display-names' +import {sanitizeHandle} from '#/lib/strings/handles' +import {niceDate} from '#/lib/strings/time' +import {TypographyVariant} from '#/lib/ThemeContext' +import {isAndroid} from '#/platform/detection' import {precacheProfile} from '#/state/queries/profile' -import {usePalette} from 'lib/hooks/usePalette' -import {makeProfileLink} from 'lib/routes/links' -import {forceLTR} from 'lib/strings/bidi' -import {NON_BREAKING_SPACE} from 'lib/strings/constants' -import {sanitizeDisplayName} from 'lib/strings/display-names' -import {sanitizeHandle} from 'lib/strings/handles' -import {niceDate} from 'lib/strings/time' -import {TypographyVariant} from 'lib/ThemeContext' -import {isAndroid} from 'platform/detection' import {ProfileHoverCard} from '#/components/ProfileHoverCard' import {TextLinkOnWebOnly} from './Link' import {Text} from './text/Text' @@ -73,12 +73,20 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => { style={[pal.text]} lineHeight={1.2} disableMismatchWarning - text={forceLTR( - sanitizeDisplayName( - displayName, - opts.moderation?.ui('displayName'), - ), - )} + text={ + <Text + type={opts.displayNameType || 'lg-bold'} + emoji + style={[pal.text]} + lineHeight={1.2}> + {forceLTR( + sanitizeDisplayName( + displayName, + opts.moderation?.ui('displayName'), + ), + )} + </Text> + } href={profileLink} onBeforePress={onBeforePressAuthor} /> @@ -86,7 +94,11 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => { type="md" disableMismatchWarning style={[pal.textLight, {flexShrink: 4}]} - text={NON_BREAKING_SPACE + sanitizeHandle(handle, '@')} + text={ + <Text emoji style={[pal.textLight, {flexShrink: 4}]}> + {NON_BREAKING_SPACE + sanitizeHandle(handle, '@')} + </Text> + } href={profileLink} onBeforePress={onBeforePressAuthor} anchorNoUnderline diff --git a/src/view/com/util/UserInfoText.tsx b/src/view/com/util/UserInfoText.tsx index 9cb9997f6..8a444d590 100644 --- a/src/view/com/util/UserInfoText.tsx +++ b/src/view/com/util/UserInfoText.tsx @@ -1,15 +1,16 @@ import React from 'react' -import {AppBskyActorGetProfile as GetProfile} from '@atproto/api' import {StyleProp, StyleSheet, TextStyle} from 'react-native' +import {AppBskyActorGetProfile as GetProfile} from '@atproto/api' + +import {makeProfileLink} from '#/lib/routes/links' +import {sanitizeDisplayName} from '#/lib/strings/display-names' +import {sanitizeHandle} from '#/lib/strings/handles' +import {TypographyVariant} from '#/lib/ThemeContext' +import {STALE} from '#/state/queries' +import {useProfileQuery} from '#/state/queries/profile' import {TextLinkOnWebOnly} from './Link' -import {Text} from './text/Text' import {LoadingPlaceholder} from './LoadingPlaceholder' -import {TypographyVariant} from 'lib/ThemeContext' -import {sanitizeDisplayName} from 'lib/strings/display-names' -import {sanitizeHandle} from 'lib/strings/handles' -import {makeProfileLink} from 'lib/routes/links' -import {useProfileQuery} from '#/state/queries/profile' -import {STALE} from '#/state/queries' +import {Text} from './text/Text' export function UserInfoText({ type = 'md', @@ -50,11 +51,15 @@ export function UserInfoText({ lineHeight={1.2} numberOfLines={1} href={makeProfileLink(profile)} - text={`${prefix || ''}${sanitizeDisplayName( - typeof profile[attr] === 'string' && profile[attr] - ? (profile[attr] as string) - : sanitizeHandle(profile.handle), - )}`} + text={ + <Text emoji type={type} style={style} lineHeight={1.2}> + {`${prefix || ''}${sanitizeDisplayName( + typeof profile[attr] === 'string' && profile[attr] + ? (profile[attr] as string) + : sanitizeHandle(profile.handle), + )}`} + </Text> + } /> ) } else { diff --git a/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx b/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx index 54e1eb4d5..98332c33b 100644 --- a/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx +++ b/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx @@ -5,21 +5,21 @@ import {AppBskyEmbedExternal} from '@atproto/api' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {usePalette} from 'lib/hooks/usePalette' -import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import {shareUrl} from 'lib/sharing' -import {parseEmbedPlayerFromUrl} from 'lib/strings/embed-player' +import {usePalette} from '#/lib/hooks/usePalette' +import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' +import {shareUrl} from '#/lib/sharing' +import {parseEmbedPlayerFromUrl} from '#/lib/strings/embed-player' import { getStarterPackOgCard, parseStarterPackUri, -} from 'lib/strings/starter-pack' -import {toNiceDomain} from 'lib/strings/url-helpers' -import {isNative} from 'platform/detection' -import {useExternalEmbedsPrefs} from 'state/preferences' -import {Link} from 'view/com/util/Link' -import {ExternalGifEmbed} from 'view/com/util/post-embeds/ExternalGifEmbed' -import {ExternalPlayer} from 'view/com/util/post-embeds/ExternalPlayerEmbed' -import {GifEmbed} from 'view/com/util/post-embeds/GifEmbed' +} from '#/lib/strings/starter-pack' +import {toNiceDomain} from '#/lib/strings/url-helpers' +import {isNative} from '#/platform/detection' +import {useExternalEmbedsPrefs} from '#/state/preferences' +import {Link} from '#/view/com/util/Link' +import {ExternalGifEmbed} from '#/view/com/util/post-embeds/ExternalGifEmbed' +import {ExternalPlayer} from '#/view/com/util/post-embeds/ExternalPlayerEmbed' +import {GifEmbed} from '#/view/com/util/post-embeds/GifEmbed' import {atoms as a, useTheme} from '#/alf' import {MediaInsetBorder} from '#/components/MediaInsetBorder' import {Text} from '../text/Text' @@ -115,12 +115,13 @@ export const ExternalLinkEmbed = ({ </Text> {!embedPlayerParams?.isGif && !embedPlayerParams?.dimensions && ( - <Text type="lg-bold" numberOfLines={3} style={[pal.text]}> + <Text emoji type="lg-bold" numberOfLines={3} style={[pal.text]}> {link.title || link.uri} </Text> )} {link.description ? ( <Text + emoji type="md" numberOfLines={link.thumb ? 2 : 4} style={[pal.text, a.mt_xs]}> diff --git a/src/view/com/util/text/Text.tsx b/src/view/com/util/text/Text.tsx index 52a45b0e2..3d885480c 100644 --- a/src/view/com/util/text/Text.tsx +++ b/src/view/com/util/text/Text.tsx @@ -2,27 +2,40 @@ import React from 'react' import {StyleSheet, Text as RNText, TextProps} from 'react-native' import {UITextView} from 'react-native-uitextview' -import {lh, s} from 'lib/styles' -import {TypographyVariant, useTheme} from 'lib/ThemeContext' -import {isIOS, isWeb} from 'platform/detection' +import {lh, s} from '#/lib/styles' +import {TypographyVariant, useTheme} from '#/lib/ThemeContext' +import {logger} from '#/logger' +import {isIOS} from '#/platform/detection' import {applyFonts, useAlf} from '#/alf' +import { + childHasEmoji, + childIsString, + renderChildrenWithEmoji, + StringChild, +} from '#/components/Typography' +import {IS_DEV} from '#/env' -export type CustomTextProps = TextProps & { +export type CustomTextProps = Omit<TextProps, 'children'> & { type?: TypographyVariant lineHeight?: number title?: string dataSet?: Record<string, string | number> selectable?: boolean -} - -const fontFamilyStyle = { - fontFamily: - '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Liberation Sans", Helvetica, Arial, sans-serif', -} +} & ( + | { + emoji: true + children: StringChild + } + | { + emoji?: false + children: TextProps['children'] + } + ) export function Text({ type = 'md', children, + emoji, lineHeight, style, title, @@ -35,6 +48,18 @@ export function Text({ const lineHeightStyle = lineHeight ? lh(theme, type, lineHeight) : undefined const {fonts} = useAlf() + 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.') + } + } + if (selectable && isIOS) { const flattened = StyleSheet.flatten([ s.black, @@ -58,7 +83,7 @@ export function Text({ selectable={selectable} uiTextView {...props}> - {children} + {isIOS && emoji ? renderChildrenWithEmoji(children) : children} </UITextView> ) } @@ -66,7 +91,6 @@ export function Text({ const flattened = StyleSheet.flatten([ s.black, typography, - isWeb && fontFamilyStyle, lineHeightStyle, style, ]) @@ -87,7 +111,7 @@ export function Text({ dataSet={Object.assign({tooltip: title}, dataSet || {})} selectable={selectable} {...props}> - {children} + {isIOS && emoji ? renderChildrenWithEmoji(children) : children} </RNText> ) } diff --git a/src/view/com/util/text/ThemedText.tsx b/src/view/com/util/text/ThemedText.tsx deleted file mode 100644 index 2844d273c..000000000 --- a/src/view/com/util/text/ThemedText.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import React from 'react' -import {CustomTextProps, Text} from './Text' -import {usePalette} from 'lib/hooks/usePalette' -import {addStyle} from 'lib/styles' - -export type ThemedTextProps = CustomTextProps & { - fg?: 'default' | 'light' | 'error' | 'inverted' | 'inverted-light' - bg?: 'default' | 'light' | 'error' | 'inverted' | 'inverted-light' - border?: 'default' | 'dark' | 'error' | 'inverted' | 'inverted-dark' - lineHeight?: number -} - -export function ThemedText({ - fg, - bg, - border, - style, - children, - ...props -}: React.PropsWithChildren<ThemedTextProps>) { - const pal = usePalette('default') - const palInverted = usePalette('inverted') - const palError = usePalette('error') - switch (fg) { - case 'default': - style = addStyle(style, pal.text) - break - case 'light': - style = addStyle(style, pal.textLight) - break - case 'error': - style = addStyle(style, {color: palError.colors.background}) - break - case 'inverted': - style = addStyle(style, palInverted.text) - break - case 'inverted-light': - style = addStyle(style, palInverted.textLight) - break - } - switch (bg) { - case 'default': - style = addStyle(style, pal.view) - break - case 'light': - style = addStyle(style, pal.viewLight) - break - case 'error': - style = addStyle(style, palError.view) - break - case 'inverted': - style = addStyle(style, palInverted.view) - break - case 'inverted-light': - style = addStyle(style, palInverted.viewLight) - break - } - switch (border) { - case 'default': - style = addStyle(style, pal.border) - break - case 'dark': - style = addStyle(style, pal.borderDark) - break - case 'error': - style = addStyle(style, palError.border) - break - case 'inverted': - style = addStyle(style, palInverted.border) - break - case 'inverted-dark': - style = addStyle(style, palInverted.borderDark) - break - } - return ( - <Text style={style} {...props}> - {children} - </Text> - ) -} diff --git a/src/view/screens/Search/Search.tsx b/src/view/screens/Search/Search.tsx index e1e412648..07d762c0f 100644 --- a/src/view/screens/Search/Search.tsx +++ b/src/view/screens/Search/Search.tsx @@ -959,6 +959,7 @@ function SearchHistory({ accessibilityIgnoresInvertColors /> <Text + emoji style={[pal.text, styles.profileName]} numberOfLines={1}> {profile.displayName || profile.handle} diff --git a/src/view/shell/desktop/Search.tsx b/src/view/shell/desktop/Search.tsx index 1ba2d3f3d..b43dbcce3 100644 --- a/src/view/shell/desktop/Search.tsx +++ b/src/view/shell/desktop/Search.tsx @@ -16,19 +16,19 @@ import {useLingui} from '@lingui/react' import {StackActions, useNavigation} from '@react-navigation/native' import {useQueryClient} from '@tanstack/react-query' +import {usePalette} from '#/lib/hooks/usePalette' import {makeProfileLink} from '#/lib/routes/links' +import {NavigationProp} from '#/lib/routes/types' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' import {s} from '#/lib/styles' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete' -import {usePalette} from 'lib/hooks/usePalette' -import {NavigationProp} from 'lib/routes/types' -import {precacheProfile} from 'state/queries/profile' +import {precacheProfile} from '#/state/queries/profile' +import {SearchInput} from '#/view/com/util/forms/SearchInput' import {Link} from '#/view/com/util/Link' +import {Text} from '#/view/com/util/text/Text' import {UserAvatar} from '#/view/com/util/UserAvatar' -import {SearchInput} from 'view/com/util/forms/SearchInput' -import {Text} from 'view/com/util/text/Text' import {atoms as a} from '#/alf' let SearchLinkCard = ({ @@ -126,6 +126,7 @@ let SearchProfileCard = ({ /> <View style={{flex: 1}}> <Text + emoji type="lg" style={[s.bold, pal.text, a.self_start]} numberOfLines={1} diff --git a/yarn.lock b/yarn.lock index d199d5869..7077aeda3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11360,6 +11360,11 @@ emoji-mart@^5.5.2: resolved "https://registry.yarnpkg.com/emoji-mart/-/emoji-mart-5.5.2.tgz#3ddbaf053139cf4aa217650078bc1c50ca8381af" integrity sha512-Sqc/nso4cjxhOwWJsp9xkVm8OF5c+mJLZJFoFfzRuKO+yWiN7K8c96xmtughYb0d/fZ8UC6cLIQ/p4BR6Pv3/A== +emoji-regex@^10.4.0: + version "10.4.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.4.0.tgz#03553afea80b3975749cfcb36f776ca268e413d4" + integrity sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw== + emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" |