about summary refs log tree commit diff
path: root/src/view/com
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/com')
-rw-r--r--src/view/com/composer/Composer.tsx4
-rw-r--r--src/view/com/feeds/CustomFeed.tsx3
-rw-r--r--src/view/com/lists/ListCard.tsx6
-rw-r--r--src/view/com/lists/ListItems.tsx6
-rw-r--r--src/view/com/modals/ListAddRemoveUser.tsx3
-rw-r--r--src/view/com/notifications/FeedItem.tsx12
-rw-r--r--src/view/com/notifications/InvitedUsers.tsx5
-rw-r--r--src/view/com/post-thread/PostThreadItem.tsx36
-rw-r--r--src/view/com/post/Post.tsx12
-rw-r--r--src/view/com/posts/FeedItem.tsx22
-rw-r--r--src/view/com/posts/FeedSlice.tsx5
-rw-r--r--src/view/com/profile/ProfileCard.tsx12
-rw-r--r--src/view/com/profile/ProfileHeader.tsx43
-rw-r--r--src/view/com/search/Suggestions.tsx3
-rw-r--r--src/view/com/util/PostMeta.tsx31
-rw-r--r--src/view/com/util/UserInfoText.tsx6
-rw-r--r--src/view/com/util/UserPreviewLink.tsx3
-rw-r--r--src/view/com/util/post-ctrls/PostCtrls.tsx7
-rw-r--r--src/view/com/util/post-embeds/QuoteEmbed.tsx8
-rw-r--r--src/view/com/util/text/ThemedText.tsx80
20 files changed, 215 insertions, 92 deletions
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx
index 66722ab2f..0fae996ff 100644
--- a/src/view/com/composer/Composer.tsx
+++ b/src/view/com/composer/Composer.tsx
@@ -30,6 +30,7 @@ import * as apilib from 'lib/api/index'
 import {ComposerOpts} from 'state/models/ui/shell'
 import {s, colors, gradients} from 'lib/styles'
 import {sanitizeDisplayName} from 'lib/strings/display-names'
+import {sanitizeHandle} from 'lib/strings/handles'
 import {cleanError} from 'lib/strings/errors'
 import {SelectPhotoBtn} from './photos/SelectPhotoBtn'
 import {OpenCameraBtn} from './photos/OpenCameraBtn'
@@ -319,7 +320,8 @@ export const ComposePost = observer(function ComposePost({
               <View style={styles.replyToPost}>
                 <Text type="xl-medium" style={[pal.text]}>
                   {sanitizeDisplayName(
-                    replyTo.author.displayName || replyTo.author.handle,
+                    replyTo.author.displayName ||
+                      sanitizeHandle(replyTo.author.handle),
                   )}
                 </Text>
                 <Text type="post-text" style={pal.text} numberOfLines={6}>
diff --git a/src/view/com/feeds/CustomFeed.tsx b/src/view/com/feeds/CustomFeed.tsx
index ef8de8b85..79f1dd74d 100644
--- a/src/view/com/feeds/CustomFeed.tsx
+++ b/src/view/com/feeds/CustomFeed.tsx
@@ -20,6 +20,7 @@ import {useStores} from 'state/index'
 import {pluralize} from 'lib/strings/helpers'
 import {AtUri} from '@atproto/api'
 import * as Toast from 'view/com/util/Toast'
+import {sanitizeHandle} from 'lib/strings/handles'
 
 export const CustomFeed = observer(
   ({
@@ -86,7 +87,7 @@ export const CustomFeed = observer(
               {item.displayName}
             </Text>
             <Text style={[pal.textLight]} numberOfLines={3}>
-              by @{item.data.creator.handle}
+              by {sanitizeHandle(item.data.creator.handle, '@')}
             </Text>
           </View>
           {showSaveBtn && (
diff --git a/src/view/com/lists/ListCard.tsx b/src/view/com/lists/ListCard.tsx
index b70fa3773..159d966eb 100644
--- a/src/view/com/lists/ListCard.tsx
+++ b/src/view/com/lists/ListCard.tsx
@@ -9,6 +9,8 @@ import {s} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useStores} from 'state/index'
 import {sanitizeDisplayName} from 'lib/strings/display-names'
+import {sanitizeHandle} from 'lib/strings/handles'
+import {makeProfileLink} from 'lib/routes/links'
 
 export const ListCard = ({
   testID,
@@ -57,7 +59,7 @@ export const ListCard = ({
         !noBg && pal.view,
         style,
       ]}
-      href={`/profile/${list.creator.did}/lists/${rkey}`}
+      href={makeProfileLink(list.creator, 'lists', rkey)}
       title={list.name}
       asAnchor
       anchorNoUnderline>
@@ -77,7 +79,7 @@ export const ListCard = ({
             {list.purpose === 'app.bsky.graph.defs#modlist' && 'Mute list'} by{' '}
             {list.creator.did === store.me.did
               ? 'you'
-              : `@${list.creator.handle}`}
+              : sanitizeHandle(list.creator.handle, '@')}
           </Text>
           {!!list.viewer?.muted && (
             <View style={s.flexRow}>
diff --git a/src/view/com/lists/ListItems.tsx b/src/view/com/lists/ListItems.tsx
index 289ba000b..188518ea5 100644
--- a/src/view/com/lists/ListItems.tsx
+++ b/src/view/com/lists/ListItems.tsx
@@ -26,6 +26,8 @@ import {useStores} from 'state/index'
 import {s} from 'lib/styles'
 import {isDesktopWeb} from 'platform/detection'
 import {ListActions} from './ListActions'
+import {makeProfileLink} from 'lib/routes/links'
+import {sanitizeHandle} from 'lib/strings/handles'
 
 const LOADING_ITEM = {_reactKey: '__loading__'}
 const HEADER_ITEM = {_reactKey: '__header__'}
@@ -296,8 +298,8 @@ const ListHeader = observer(
                   'you'
                 ) : (
                   <TextLink
-                    text={`@${list.creator.handle}`}
-                    href={`/profile/${list.creator.did}`}
+                    text={sanitizeHandle(list.creator.handle, '@')}
+                    href={makeProfileLink(list.creator)}
                   />
                 )}
               </Text>
diff --git a/src/view/com/modals/ListAddRemoveUser.tsx b/src/view/com/modals/ListAddRemoveUser.tsx
index c2d63ef6e..49f46e741 100644
--- a/src/view/com/modals/ListAddRemoveUser.tsx
+++ b/src/view/com/modals/ListAddRemoveUser.tsx
@@ -16,6 +16,7 @@ import {Button} from '../util/forms/Button'
 import * as Toast from '../util/Toast'
 import {useStores} from 'state/index'
 import {sanitizeDisplayName} from 'lib/strings/display-names'
+import {sanitizeHandle} from 'lib/strings/handles'
 import {s} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
 import {isDesktopWeb, isAndroid} from 'platform/detection'
@@ -122,7 +123,7 @@ export const Component = observer(
                 by{' '}
                 {list.creator.did === store.me.did
                   ? 'you'
-                  : `@${list.creator.handle}`}
+                  : sanitizeHandle(list.creator.handle, '@')}
               </Text>
             </View>
             <View
diff --git a/src/view/com/notifications/FeedItem.tsx b/src/view/com/notifications/FeedItem.tsx
index d5acf2305..7b9f0715b 100644
--- a/src/view/com/notifications/FeedItem.tsx
+++ b/src/view/com/notifications/FeedItem.tsx
@@ -19,6 +19,7 @@ import {PostThreadModel} from 'state/models/content/post-thread'
 import {s, colors} from 'lib/styles'
 import {ago} from 'lib/strings/time'
 import {sanitizeDisplayName} from 'lib/strings/display-names'
+import {sanitizeHandle} from 'lib/strings/handles'
 import {pluralize} from 'lib/strings/helpers'
 import {HeartIconSolid} from 'lib/icons'
 import {Text} from '../util/text/Text'
@@ -36,6 +37,7 @@ import {
 } from 'lib/labeling/helpers'
 import {ProfileModeration} from 'lib/labeling/types'
 import {formatCount} from '../util/numeric/format'
+import {makeProfileLink} from 'lib/routes/links'
 
 const MAX_AUTHORS = 5
 
@@ -63,7 +65,7 @@ export const FeedItem = observer(function ({
       const urip = new AtUri(item.subjectUri)
       return `/profile/${urip.host}/post/${urip.rkey}`
     } else if (item.isFollow) {
-      return `/profile/${item.author.handle}`
+      return makeProfileLink(item.author)
     } else if (item.isReply) {
       const urip = new AtUri(item.uri)
       return `/profile/${urip.host}/post/${urip.rkey}`
@@ -92,7 +94,7 @@ export const FeedItem = observer(function ({
   const authors: Author[] = useMemo(() => {
     return [
       {
-        href: `/profile/${item.author.handle}`,
+        href: makeProfileLink(item.author),
         did: item.author.did,
         handle: item.author.handle,
         displayName: item.author.displayName,
@@ -104,7 +106,7 @@ export const FeedItem = observer(function ({
       },
       ...(item.additional?.map(({author}) => {
         return {
-          href: `/profile/${author.handle}`,
+          href: makeProfileLink(author),
           did: author.did,
           handle: author.handle,
           displayName: author.displayName,
@@ -158,7 +160,7 @@ export const FeedItem = observer(function ({
     action = 'liked your post'
     icon = 'HeartIconSolid'
     iconStyle = [
-      s.red3 as FontAwesomeIconStyle,
+      s.likeColor as FontAwesomeIconStyle,
       {position: 'relative', top: -4},
     ]
   } else if (item.isRepost) {
@@ -377,7 +379,7 @@ function ExpandedAuthorsList({
               {sanitizeDisplayName(author.displayName || author.handle)}
               &nbsp;
               <Text style={[pal.textLight]} lineHeight={1.2}>
-                {author.handle}
+                {sanitizeHandle(author.handle)}
               </Text>
             </Text>
           </View>
diff --git a/src/view/com/notifications/InvitedUsers.tsx b/src/view/com/notifications/InvitedUsers.tsx
index 73469d2ad..1bdb42a9c 100644
--- a/src/view/com/notifications/InvitedUsers.tsx
+++ b/src/view/com/notifications/InvitedUsers.tsx
@@ -16,6 +16,7 @@ import {useStores} from 'state/index'
 import {usePalette} from 'lib/hooks/usePalette'
 import {s} from 'lib/styles'
 import {sanitizeDisplayName} from 'lib/strings/display-names'
+import {makeProfileLink} from 'lib/routes/links'
 
 export const InvitedUsers = observer(() => {
   const store = useStores()
@@ -58,14 +59,14 @@ function InvitedUser({
         />
       </View>
       <View style={s.flex1}>
-        <Link href={`/profile/${profile.handle}`}>
+        <Link href={makeProfileLink(profile)}>
           <UserAvatar avatar={profile.avatar} size={35} />
         </Link>
         <Text style={[styles.desc, pal.text]}>
           <TextLink
             type="md-bold"
             style={pal.text}
-            href={`/profile/${profile.handle}`}
+            href={makeProfileLink(profile)}
             text={sanitizeDisplayName(profile.displayName || profile.handle)}
           />{' '}
           joined using your invite code!
diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx
index efc9fe694..0680bbc06 100644
--- a/src/view/com/post-thread/PostThreadItem.tsx
+++ b/src/view/com/post-thread/PostThreadItem.tsx
@@ -17,6 +17,7 @@ import {PreviewableUserAvatar} from '../util/UserAvatar'
 import {s} from 'lib/styles'
 import {niceDate} from 'lib/strings/time'
 import {sanitizeDisplayName} from 'lib/strings/display-names'
+import {sanitizeHandle} from 'lib/strings/handles'
 import {pluralize} from 'lib/strings/helpers'
 import {getTranslatorLink, isPostInLanguage} from '../../../locale/helpers'
 import {useStores} from 'state/index'
@@ -31,6 +32,7 @@ import {ErrorMessage} from '../util/error/ErrorMessage'
 import {usePalette} from 'lib/hooks/usePalette'
 import {formatCount} from '../util/numeric/format'
 import {TimeElapsed} from 'view/com/util/TimeElapsed'
+import {makeProfileLink} from 'lib/routes/links'
 
 const PARENT_REPLY_LINE_LENGTH = 8
 
@@ -51,20 +53,20 @@ export const PostThreadItem = observer(function PostThreadItem({
   const itemCid = item.post.cid
   const itemHref = React.useMemo(() => {
     const urip = new AtUri(item.post.uri)
-    return `/profile/${item.post.author.handle}/post/${urip.rkey}`
-  }, [item.post.uri, item.post.author.handle])
+    return makeProfileLink(item.post.author, 'post', urip.rkey)
+  }, [item.post.uri, item.post.author])
   const itemTitle = `Post by ${item.post.author.handle}`
-  const authorHref = `/profile/${item.post.author.handle}`
+  const authorHref = makeProfileLink(item.post.author)
   const authorTitle = item.post.author.handle
   const likesHref = React.useMemo(() => {
     const urip = new AtUri(item.post.uri)
-    return `/profile/${item.post.author.handle}/post/${urip.rkey}/liked-by`
-  }, [item.post.uri, item.post.author.handle])
+    return makeProfileLink(item.post.author, 'post', urip.rkey, 'liked-by')
+  }, [item.post.uri, item.post.author])
   const likesTitle = 'Likes on this post'
   const repostsHref = React.useMemo(() => {
     const urip = new AtUri(item.post.uri)
-    return `/profile/${item.post.author.handle}/post/${urip.rkey}/reposted-by`
-  }, [item.post.uri, item.post.author.handle])
+    return makeProfileLink(item.post.author, 'post', urip.rkey, 'reposted-by')
+  }, [item.post.uri, item.post.author])
   const repostsTitle = 'Reposts of this post'
 
   const primaryLanguage = store.preferences.contentLanguages[0] || 'en'
@@ -185,7 +187,8 @@ export const PostThreadItem = observer(function PostThreadItem({
                     numberOfLines={1}
                     lineHeight={1.2}>
                     {sanitizeDisplayName(
-                      item.post.author.displayName || item.post.author.handle,
+                      item.post.author.displayName ||
+                        sanitizeHandle(item.post.author.handle),
                     )}
                   </Text>
                 </Link>
@@ -223,7 +226,7 @@ export const PostThreadItem = observer(function PostThreadItem({
                 href={authorHref}
                 title={authorTitle}>
                 <Text type="md" style={[pal.textLight]} numberOfLines={1}>
-                  @{item.post.author.handle}
+                  {sanitizeHandle(item.post.author.handle, '@')}
                 </Text>
               </Link>
             </View>
@@ -297,11 +300,7 @@ export const PostThreadItem = observer(function PostThreadItem({
               itemCid={itemCid}
               itemHref={itemHref}
               itemTitle={itemTitle}
-              author={{
-                avatar: item.post.author.avatar!,
-                handle: item.post.author.handle,
-                displayName: item.post.author.displayName!,
-              }}
+              author={item.post.author}
               text={item.richText?.text || record.text}
               indexedAt={item.post.indexedAt}
               isAuthor={item.post.author.did === store.me.did}
@@ -362,8 +361,7 @@ export const PostThreadItem = observer(function PostThreadItem({
             </View>
             <View style={styles.layoutContent}>
               <PostMeta
-                authorHandle={item.post.author.handle}
-                authorDisplayName={item.post.author.displayName}
+                author={item.post.author}
                 authorHasWarning={!!item.post.author.labels?.length}
                 timestamp={item.post.indexedAt}
                 postHref={itemHref}
@@ -399,11 +397,7 @@ export const PostThreadItem = observer(function PostThreadItem({
                 itemCid={itemCid}
                 itemHref={itemHref}
                 itemTitle={itemTitle}
-                author={{
-                  avatar: item.post.author.avatar!,
-                  handle: item.post.author.handle,
-                  displayName: item.post.author.displayName!,
-                }}
+                author={item.post.author}
                 text={item.richText?.text || record.text}
                 indexedAt={item.post.indexedAt}
                 isAuthor={item.post.author.did === store.me.did}
diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx
index bdc84b62f..ac5e7d20b 100644
--- a/src/view/com/post/Post.tsx
+++ b/src/view/com/post/Post.tsx
@@ -30,6 +30,7 @@ import {useStores} from 'state/index'
 import {s, colors} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
 import {getTranslatorLink, isPostInLanguage} from '../../../locale/helpers'
+import {makeProfileLink} from 'lib/routes/links'
 
 export const Post = observer(function Post({
   uri,
@@ -125,7 +126,7 @@ const PostLoaded = observer(
     const itemUri = item.post.uri
     const itemCid = item.post.cid
     const itemUrip = new AtUri(item.post.uri)
-    const itemHref = `/profile/${item.post.author.handle}/post/${itemUrip.rkey}`
+    const itemHref = makeProfileLink(item.post.author, 'post', itemUrip.rkey)
     const itemTitle = `Post by ${item.post.author.handle}`
     let replyAuthorDid = ''
     if (record.reply) {
@@ -222,8 +223,7 @@ const PostLoaded = observer(
           </View>
           <View style={styles.layoutContent}>
             <PostMeta
-              authorHandle={item.post.author.handle}
-              authorDisplayName={item.post.author.displayName}
+              author={item.post.author}
               authorHasWarning={!!item.post.author.labels?.length}
               timestamp={item.post.indexedAt}
               postHref={itemHref}
@@ -282,11 +282,7 @@ const PostLoaded = observer(
               itemCid={itemCid}
               itemHref={itemHref}
               itemTitle={itemTitle}
-              author={{
-                avatar: item.post.author.avatar!,
-                handle: item.post.author.handle,
-                displayName: item.post.author.displayName!,
-              }}
+              author={item.post.author}
               indexedAt={item.post.indexedAt}
               text={item.richText?.text || record.text}
               isAuthor={item.post.author.did === store.me.did}
diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx
index e1b160dcb..75c321145 100644
--- a/src/view/com/posts/FeedItem.tsx
+++ b/src/view/com/posts/FeedItem.tsx
@@ -27,7 +27,9 @@ import {useStores} from 'state/index'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useAnalytics} from 'lib/analytics/analytics'
 import {sanitizeDisplayName} from 'lib/strings/display-names'
+import {sanitizeHandle} from 'lib/strings/handles'
 import {getTranslatorLink, isPostInLanguage} from '../../../locale/helpers'
+import {makeProfileLink} from 'lib/routes/links'
 
 export const FeedItem = observer(function ({
   item,
@@ -50,8 +52,8 @@ export const FeedItem = observer(function ({
   const itemCid = item.post.cid
   const itemHref = useMemo(() => {
     const urip = new AtUri(item.post.uri)
-    return `/profile/${item.post.author.handle}/post/${urip.rkey}`
-  }, [item.post.uri, item.post.author.handle])
+    return makeProfileLink(item.post.author, 'post', urip.rkey)
+  }, [item.post.uri, item.post.author])
   const itemTitle = `Post by ${item.post.author.handle}`
   const replyAuthorDid = useMemo(() => {
     if (!record?.reply) {
@@ -178,7 +180,7 @@ export const FeedItem = observer(function ({
       {item.reasonRepost && (
         <Link
           style={styles.includeReason}
-          href={`/profile/${item.reasonRepost.by.handle}`}
+          href={makeProfileLink(item.reasonRepost.by)}
           title={sanitizeDisplayName(
             item.reasonRepost.by.displayName || item.reasonRepost.by.handle,
           )}>
@@ -201,9 +203,10 @@ export const FeedItem = observer(function ({
               lineHeight={1.2}
               numberOfLines={1}
               text={sanitizeDisplayName(
-                item.reasonRepost.by.displayName || item.reasonRepost.by.handle,
+                item.reasonRepost.by.displayName ||
+                  sanitizeHandle(item.reasonRepost.by.handle),
               )}
-              href={`/profile/${item.reasonRepost.by.handle}`}
+              href={makeProfileLink(item.reasonRepost.by)}
             />
           </Text>
         </Link>
@@ -221,8 +224,7 @@ export const FeedItem = observer(function ({
         </View>
         <View style={styles.layoutContent}>
           <PostMeta
-            authorHandle={item.post.author.handle}
-            authorDisplayName={item.post.author.displayName}
+            author={item.post.author}
             authorHasWarning={!!item.post.author.labels?.length}
             timestamp={item.post.indexedAt}
             postHref={itemHref}
@@ -284,11 +286,7 @@ export const FeedItem = observer(function ({
             itemCid={itemCid}
             itemHref={itemHref}
             itemTitle={itemTitle}
-            author={{
-              avatar: item.post.author.avatar!,
-              handle: item.post.author.handle,
-              displayName: item.post.author.displayName!,
-            }}
+            author={item.post.author}
             text={item.richText?.text || record.text}
             indexedAt={item.post.indexedAt}
             isAuthor={item.post.author.did === store.me.did}
diff --git a/src/view/com/posts/FeedSlice.tsx b/src/view/com/posts/FeedSlice.tsx
index 8ac813b92..b73d4a99d 100644
--- a/src/view/com/posts/FeedSlice.tsx
+++ b/src/view/com/posts/FeedSlice.tsx
@@ -8,6 +8,7 @@ import Svg, {Circle, Line} from 'react-native-svg'
 import {FeedItem} from './FeedItem'
 import {usePalette} from 'lib/hooks/usePalette'
 import {ModerationBehaviorCode} from 'lib/labeling/types'
+import {makeProfileLink} from 'lib/routes/links'
 
 export function FeedSlice({
   slice,
@@ -70,8 +71,8 @@ function ViewFullThread({slice}: {slice: PostsFeedSliceModel}) {
   const pal = usePalette('default')
   const itemHref = React.useMemo(() => {
     const urip = new AtUri(slice.rootItem.post.uri)
-    return `/profile/${slice.rootItem.post.author.handle}/post/${urip.rkey}`
-  }, [slice.rootItem.post.uri, slice.rootItem.post.author.handle])
+    return makeProfileLink(slice.rootItem.post.author, 'post', urip.rkey)
+  }, [slice.rootItem.post.uri, slice.rootItem.post.author])
 
   return (
     <Link style={[pal.view, styles.viewFullThread]} href={itemHref} noFeedback>
diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx
index 2dfc7ad30..946e0f2ab 100644
--- a/src/view/com/profile/ProfileCard.tsx
+++ b/src/view/com/profile/ProfileCard.tsx
@@ -10,11 +10,13 @@ import {usePalette} from 'lib/hooks/usePalette'
 import {useStores} from 'state/index'
 import {FollowButton} from './FollowButton'
 import {sanitizeDisplayName} from 'lib/strings/display-names'
+import {sanitizeHandle} from 'lib/strings/handles'
 import {
   getProfileViewBasicLabelInfo,
   getProfileModeration,
 } from 'lib/labeling/helpers'
 import {ModerationBehaviorCode} from 'lib/labeling/types'
+import {makeProfileLink} from 'lib/routes/links'
 
 export const ProfileCard = observer(
   ({
@@ -60,7 +62,7 @@ export const ProfileCard = observer(
           noBorder && styles.outerNoBorder,
           !noBg && pal.view,
         ]}
-        href={`/profile/${profile.handle}`}
+        href={makeProfileLink(profile)}
         title={profile.handle}
         asAnchor
         anchorNoUnderline>
@@ -78,10 +80,12 @@ export const ProfileCard = observer(
               style={[s.bold, pal.text]}
               numberOfLines={1}
               lineHeight={1.2}>
-              {sanitizeDisplayName(profile.displayName || profile.handle)}
+              {sanitizeDisplayName(
+                profile.displayName || sanitizeHandle(profile.handle),
+              )}
             </Text>
             <Text type="md" style={[pal.textLight]} numberOfLines={1}>
-              @{profile.handle}
+              {sanitizeHandle(profile.handle, '@')}
             </Text>
             {!!profile.viewer?.followedBy && (
               <View style={s.flexRow}>
@@ -160,7 +164,7 @@ export const ProfileCardWithFollowBtn = observer(
     followers?: AppBskyActorDefs.ProfileView[] | undefined
   }) => {
     const store = useStores()
-    const isMe = store.me.handle === profile.handle
+    const isMe = store.me.did === profile.did
 
     return (
       <ProfileCard
diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx
index 320d8a778..11e9e74c3 100644
--- a/src/view/com/profile/ProfileHeader.tsx
+++ b/src/view/com/profile/ProfileHeader.tsx
@@ -15,11 +15,13 @@ import {ProfileImageLightbox} from 'state/models/ui/shell'
 import {pluralize} from 'lib/strings/helpers'
 import {toShareUrl} from 'lib/strings/url-helpers'
 import {sanitizeDisplayName} from 'lib/strings/display-names'
+import {sanitizeHandle} from 'lib/strings/handles'
 import {s, colors} from 'lib/styles'
 import {DropdownButton, DropdownItem} from '../util/forms/DropdownButton'
 import * as Toast from '../util/Toast'
 import {LoadingPlaceholder} from '../util/LoadingPlaceholder'
 import {Text} from '../util/text/Text'
+import {ThemedText} from '../util/text/ThemedText'
 import {TextLink} from '../util/Link'
 import {RichText} from '../util/text/RichText'
 import {UserAvatar} from '../util/UserAvatar'
@@ -34,6 +36,8 @@ import {FollowState} from 'state/models/cache/my-follows'
 import {shareUrl} from 'lib/sharing'
 import {formatCount} from '../util/numeric/format'
 import {navigate} from '../../../Navigation'
+import {isInvalidHandle} from 'lib/strings/handles'
+import {makeProfileLink} from 'lib/routes/links'
 
 const BACK_HITSLOP = {left: 30, top: 30, right: 30, bottom: 30}
 
@@ -67,7 +71,9 @@ export const ProfileHeader = observer(
             </View>
             <View>
               <Text type="title-2xl" style={[pal.text, styles.title]}>
-                {sanitizeDisplayName(view.displayName || view.handle)}
+                {sanitizeDisplayName(
+                  view.displayName || sanitizeHandle(view.handle),
+                )}
               </Text>
             </View>
           </View>
@@ -104,6 +110,7 @@ const ProfileHeaderLoaded = observer(
     const store = useStores()
     const navigation = useNavigation<NavigationProp>()
     const {track} = useAnalytics()
+    const invalidHandle = isInvalidHandle(view.handle)
 
     const onPressBack = React.useCallback(() => {
       navigation.goBack()
@@ -144,19 +151,23 @@ const ProfileHeaderLoaded = observer(
 
     const onPressFollowers = React.useCallback(() => {
       track('ProfileHeader:FollowersButtonClicked')
-      navigate('ProfileFollowers', {name: view.handle})
+      navigate('ProfileFollowers', {
+        name: isInvalidHandle(view.handle) ? view.did : view.handle,
+      })
       store.shell.closeAllActiveElements() // for when used in the profile preview modal
     }, [track, view, store.shell])
 
     const onPressFollows = React.useCallback(() => {
       track('ProfileHeader:FollowsButtonClicked')
-      navigate('ProfileFollows', {name: view.handle})
+      navigate('ProfileFollows', {
+        name: isInvalidHandle(view.handle) ? view.did : view.handle,
+      })
       store.shell.closeAllActiveElements() // for when used in the profile preview modal
     }, [track, view, store.shell])
 
     const onPressShare = React.useCallback(() => {
       track('ProfileHeader:ShareButtonClicked')
-      const url = toShareUrl(`/profile/${view.handle}`)
+      const url = toShareUrl(makeProfileLink(view))
       shareUrl(url)
     }, [track, view])
 
@@ -338,7 +349,7 @@ const ProfileHeaderLoaded = observer(
                     style={[styles.btn, styles.mainBtn, pal.btn]}
                     accessibilityRole="button"
                     accessibilityLabel={`Unfollow ${view.handle}`}
-                    accessibilityHint={`Hides direct posts from ${view.handle} in your feed`}>
+                    accessibilityHint={`Hides posts from ${view.handle} in your feed`}>
                     <FontAwesomeIcon
                       icon="check"
                       style={[pal.text, s.mr5]}
@@ -355,7 +366,7 @@ const ProfileHeaderLoaded = observer(
                     style={[styles.btn, styles.mainBtn, palInverted.view]}
                     accessibilityRole="button"
                     accessibilityLabel={`Follow ${view.handle}`}
-                    accessibilityHint={`Shows direct posts from ${view.handle} in your feed`}>
+                    accessibilityHint={`Shows posts from ${view.handle} in your feed`}>
                     <FontAwesomeIcon
                       icon="plus"
                       style={[palInverted.text, s.mr5]}
@@ -382,7 +393,9 @@ const ProfileHeaderLoaded = observer(
               testID="profileHeaderDisplayName"
               type="title-2xl"
               style={[pal.text, styles.title]}>
-              {sanitizeDisplayName(view.displayName || view.handle)}
+              {sanitizeDisplayName(
+                view.displayName || sanitizeHandle(view.handle),
+              )}
             </Text>
           </View>
           <View style={styles.handleLine}>
@@ -393,7 +406,16 @@ const ProfileHeaderLoaded = observer(
                 </Text>
               </View>
             ) : undefined}
-            <Text style={[pal.textLight, styles.handle]}>@{view.handle}</Text>
+            <ThemedText
+              type={invalidHandle ? 'xs' : 'md'}
+              fg={invalidHandle ? 'error' : 'light'}
+              border={invalidHandle ? 'error' : undefined}
+              style={[
+                invalidHandle ? styles.invalidHandle : undefined,
+                styles.handle,
+              ]}>
+              {invalidHandle ? '⚠Invalid Handle' : `@${view.handle}`}
+            </ThemedText>
           </View>
           {!blockHide && (
             <>
@@ -600,6 +622,11 @@ const styles = StyleSheet.create({
         // @ts-ignore web only -prf
         wordBreak: 'break-all',
       },
+  invalidHandle: {
+    borderWidth: 1,
+    borderRadius: 4,
+    paddingHorizontal: 4,
+  },
 
   handleLine: {
     flexDirection: 'row',
diff --git a/src/view/com/search/Suggestions.tsx b/src/view/com/search/Suggestions.tsx
index c8941e24d..440d912af 100644
--- a/src/view/com/search/Suggestions.tsx
+++ b/src/view/com/search/Suggestions.tsx
@@ -12,6 +12,7 @@ import {Text} from '../util/text/Text'
 import {ProfileCardWithFollowBtn} from '../profile/ProfileCard'
 import {ProfileCardFeedLoadingPlaceholder} from 'view/com/util/LoadingPlaceholder'
 import {sanitizeDisplayName} from 'lib/strings/display-names'
+import {sanitizeHandle} from 'lib/strings/handles'
 import {RefWithInfoAndFollowers} from 'state/models/discovery/foafs'
 import {usePalette} from 'lib/hooks/usePalette'
 
@@ -99,7 +100,7 @@ export const Suggestions = observer(
                 _reactKey: `__${item.did}_heading__`,
                 type: 'heading',
                 title: `Followed by ${sanitizeDisplayName(
-                  item.displayName || item.handle,
+                  item.displayName || sanitizeHandle(item.handle),
                 )}`,
               },
             ])
diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx
index 5df6b3983..2ce499765 100644
--- a/src/view/com/util/PostMeta.tsx
+++ b/src/view/com/util/PostMeta.tsx
@@ -7,13 +7,19 @@ import {usePalette} from 'lib/hooks/usePalette'
 import {UserAvatar} from './UserAvatar'
 import {observer} from 'mobx-react-lite'
 import {sanitizeDisplayName} from 'lib/strings/display-names'
+import {sanitizeHandle} from 'lib/strings/handles'
 import {isAndroid} from 'platform/detection'
 import {TimeElapsed} from './TimeElapsed'
+import {makeProfileLink} from 'lib/routes/links'
 
 interface PostMetaOpts {
-  authorAvatar?: string
-  authorHandle: string
-  authorDisplayName: string | undefined
+  author: {
+    avatar?: string
+    did: string
+    handle: string
+    displayName?: string | undefined
+  }
+  showAvatar?: boolean
   authorHasWarning: boolean
   postHref: string
   timestamp: string
@@ -21,15 +27,15 @@ interface PostMetaOpts {
 
 export const PostMeta = observer(function (opts: PostMetaOpts) {
   const pal = usePalette('default')
-  const displayName = opts.authorDisplayName || opts.authorHandle
-  const handle = opts.authorHandle
+  const displayName = opts.author.displayName || opts.author.handle
+  const handle = opts.author.handle
 
   return (
     <View style={styles.metaOneLine}>
-      {typeof opts.authorAvatar !== 'undefined' && (
+      {opts.showAvatar && typeof opts.author.avatar !== 'undefined' && (
         <View style={styles.avatar}>
           <UserAvatar
-            avatar={opts.authorAvatar}
+            avatar={opts.author.avatar}
             size={16}
             // TODO moderation
           />
@@ -43,17 +49,17 @@ export const PostMeta = observer(function (opts: PostMetaOpts) {
           lineHeight={1.2}
           text={
             <>
-              {sanitizeDisplayName(displayName)}
+              {sanitizeDisplayName(displayName)}&nbsp;
               <Text
                 type="md"
-                style={[pal.textLight]}
                 numberOfLines={1}
-                lineHeight={1.2}>
-                &nbsp;@{handle}
+                lineHeight={1.2}
+                style={pal.textLight}>
+                {sanitizeHandle(handle, '@')}
               </Text>
             </>
           }
-          href={`/profile/${opts.authorHandle}`}
+          href={makeProfileLink(opts.author)}
         />
       </View>
       {!isAndroid && (
@@ -85,6 +91,7 @@ export const PostMeta = observer(function (opts: PostMetaOpts) {
 const styles = StyleSheet.create({
   metaOneLine: {
     flexDirection: 'row',
+    alignItems: 'baseline',
     paddingBottom: 2,
     gap: 4,
   },
diff --git a/src/view/com/util/UserInfoText.tsx b/src/view/com/util/UserInfoText.tsx
index b737b2b1e..695711b2a 100644
--- a/src/view/com/util/UserInfoText.tsx
+++ b/src/view/com/util/UserInfoText.tsx
@@ -7,6 +7,8 @@ import {LoadingPlaceholder} from './LoadingPlaceholder'
 import {useStores} from 'state/index'
 import {TypographyVariant} from 'lib/ThemeContext'
 import {sanitizeDisplayName} from 'lib/strings/display-names'
+import {sanitizeHandle} from 'lib/strings/handles'
+import {makeProfileLink} from 'lib/routes/links'
 
 export function UserInfoText({
   type = 'md',
@@ -68,11 +70,11 @@ export function UserInfoText({
         style={style}
         lineHeight={1.2}
         numberOfLines={1}
-        href={`/profile/${profile.handle}`}
+        href={makeProfileLink(profile)}
         text={`${prefix || ''}${sanitizeDisplayName(
           typeof profile[attr] === 'string' && profile[attr]
             ? (profile[attr] as string)
-            : profile.handle,
+            : sanitizeHandle(profile.handle),
         )}`}
       />
     )
diff --git a/src/view/com/util/UserPreviewLink.tsx b/src/view/com/util/UserPreviewLink.tsx
index ae49301fd..7eedbc2d4 100644
--- a/src/view/com/util/UserPreviewLink.tsx
+++ b/src/view/com/util/UserPreviewLink.tsx
@@ -3,6 +3,7 @@ import {Pressable, StyleProp, ViewStyle} from 'react-native'
 import {useStores} from 'state/index'
 import {Link} from './Link'
 import {isDesktopWeb} from 'platform/detection'
+import {makeProfileLink} from 'lib/routes/links'
 
 interface UserPreviewLinkProps {
   did: string
@@ -17,7 +18,7 @@ export function UserPreviewLink(
   if (isDesktopWeb) {
     return (
       <Link
-        href={`/profile/${props.handle}`}
+        href={makeProfileLink(props)}
         title={props.handle}
         asAnchor
         style={props.style}>
diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx
index cd6db408c..c544f6409 100644
--- a/src/view/com/util/post-ctrls/PostCtrls.tsx
+++ b/src/view/com/util/post-ctrls/PostCtrls.tsx
@@ -32,9 +32,10 @@ interface PostCtrlsOpts {
   itemTitle: string
   isAuthor: boolean
   author: {
+    did: string
     handle: string
-    displayName: string
-    avatar: string
+    displayName?: string | undefined
+    avatar?: string | undefined
   }
   text: string
   indexedAt: string
@@ -269,7 +270,7 @@ const styles = StyleSheet.create({
     margin: -5,
   },
   ctrlIconLiked: {
-    color: colors.red3,
+    color: colors.like,
   },
   mt1: {
     marginTop: 1,
diff --git a/src/view/com/util/post-embeds/QuoteEmbed.tsx b/src/view/com/util/post-embeds/QuoteEmbed.tsx
index 3836132d5..4995562ac 100644
--- a/src/view/com/util/post-embeds/QuoteEmbed.tsx
+++ b/src/view/com/util/post-embeds/QuoteEmbed.tsx
@@ -8,6 +8,7 @@ import {Text} from '../text/Text'
 import {usePalette} from 'lib/hooks/usePalette'
 import {ComposerOptsQuote} from 'state/models/ui/shell'
 import {PostEmbeds} from '.'
+import {makeProfileLink} from 'lib/routes/links'
 
 export function QuoteEmbed({
   quote,
@@ -18,7 +19,7 @@ export function QuoteEmbed({
 }) {
   const pal = usePalette('default')
   const itemUrip = new AtUri(quote.uri)
-  const itemHref = `/profile/${quote.author.handle}/post/${itemUrip.rkey}`
+  const itemHref = makeProfileLink(quote.author, 'post', itemUrip.rkey)
   const itemTitle = `Post by ${quote.author.handle}`
   const isEmpty = React.useMemo(
     () => quote.text.trim().length === 0,
@@ -39,9 +40,8 @@ export function QuoteEmbed({
       href={itemHref}
       title={itemTitle}>
       <PostMeta
-        authorAvatar={quote.author.avatar}
-        authorHandle={quote.author.handle}
-        authorDisplayName={quote.author.displayName}
+        author={quote.author}
+        showAvatar
         authorHasWarning={false}
         postHref={itemHref}
         timestamp={quote.indexedAt}
diff --git a/src/view/com/util/text/ThemedText.tsx b/src/view/com/util/text/ThemedText.tsx
new file mode 100644
index 000000000..2844d273c
--- /dev/null
+++ b/src/view/com/util/text/ThemedText.tsx
@@ -0,0 +1,80 @@
+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>
+  )
+}