about summary refs log tree commit diff
diff options
context:
space:
mode:
authorEric Bailey <git@esb.lol>2024-09-23 10:40:37 -0500
committerGitHub <noreply@github.com>2024-09-24 00:40:37 +0900
commit5eb294488f08534abac3335acfa366cffea9259e (patch)
tree94453e05d751b5b2ef91467460c258ed5e00b80d
parent443f3a64069f081764c2f49578108a9570e8e834 (diff)
downloadvoidsky-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
-rw-r--r--package.json1
-rw-r--r--src/components/FeedCard.tsx13
-rw-r--r--src/components/KnownFollowers.tsx14
-rw-r--r--src/components/LabelingServiceCard/index.tsx11
-rw-r--r--src/components/ListCard.tsx24
-rw-r--r--src/components/Pills.tsx1
-rw-r--r--src/components/ProfileCard.tsx12
-rw-r--r--src/components/ReportDialog/SubmitView.tsx1
-rw-r--r--src/components/RichText.tsx9
-rw-r--r--src/components/StarterPack/StarterPackCard.tsx27
-rw-r--r--src/components/StarterPack/Wizard/WizardListCard.tsx11
-rw-r--r--src/components/Typography.tsx108
-rw-r--r--src/components/dms/MessagesListHeader.tsx17
-rw-r--r--src/components/moderation/LabelsOnMeDialog.tsx6
-rw-r--r--src/components/moderation/ModerationDetailsDialog.tsx8
-rw-r--r--src/screens/Messages/List/ChatListItem.tsx10
-rw-r--r--src/screens/Profile/Header/DisplayName.tsx1
-rw-r--r--src/screens/Profile/Header/Handle.tsx11
-rw-r--r--src/view/com/composer/text-input/TextInput.tsx19
-rw-r--r--src/view/com/composer/text-input/web/Autocomplete.tsx15
-rw-r--r--src/view/com/feeds/FeedSourceCard.tsx12
-rw-r--r--src/view/com/modals/UserAddRemoveLists.tsx32
-rw-r--r--src/view/com/notifications/FeedItem.tsx23
-rw-r--r--src/view/com/pager/TabBar.tsx1
-rw-r--r--src/view/com/post-thread/PostThreadItem.tsx31
-rw-r--r--src/view/com/posts/FeedItem.tsx26
-rw-r--r--src/view/com/profile/ProfileCard.tsx19
-rw-r--r--src/view/com/util/PostMeta.tsx44
-rw-r--r--src/view/com/util/UserInfoText.tsx31
-rw-r--r--src/view/com/util/post-embeds/ExternalLinkEmbed.tsx27
-rw-r--r--src/view/com/util/text/Text.tsx50
-rw-r--r--src/view/com/util/text/ThemedText.tsx80
-rw-r--r--src/view/screens/Search/Search.tsx1
-rw-r--r--src/view/shell/desktop/Search.tsx11
-rw-r--r--yarn.lock5
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,
-                )}
-                &nbsp;
+                <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"