about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/components/moderation/PostHider.tsx23
-rw-r--r--src/screens/Messages/List/index.tsx7
-rw-r--r--src/state/queries/post-feed.ts4
-rw-r--r--src/state/queries/post-thread.ts5
-rw-r--r--src/state/queries/profile.ts54
-rw-r--r--src/view/com/composer/ComposerReplyTo.tsx4
-rw-r--r--src/view/com/notifications/FeedItem.tsx70
-rw-r--r--src/view/com/post-thread/PostThreadItem.tsx11
-rw-r--r--src/view/com/post/Post.tsx10
-rw-r--r--src/view/com/posts/FeedItem.tsx14
-rw-r--r--src/view/com/profile/ProfileCard.tsx15
-rw-r--r--src/view/com/profile/ProfileHeaderSuggestedFollows.tsx3
-rw-r--r--src/view/com/util/Link.tsx7
-rw-r--r--src/view/com/util/PostMeta.tsx17
-rw-r--r--src/view/com/util/TimeElapsed.tsx5
-rw-r--r--src/view/com/util/UserAvatar.tsx32
-rw-r--r--src/view/com/util/post-embeds/QuoteEmbed.tsx6
17 files changed, 119 insertions, 168 deletions
diff --git a/src/components/moderation/PostHider.tsx b/src/components/moderation/PostHider.tsx
index 464ee2077..05cb8464e 100644
--- a/src/components/moderation/PostHider.tsx
+++ b/src/components/moderation/PostHider.tsx
@@ -1,25 +1,27 @@
 import React, {ComponentProps} from 'react'
-import {StyleSheet, Pressable, View, ViewStyle, StyleProp} from 'react-native'
-import {ModerationUI} from '@atproto/api'
+import {Pressable, StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
+import {AppBskyActorDefs, ModerationUI} from '@atproto/api'
+import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
-import {Trans, msg} from '@lingui/macro'
+import {useQueryClient} from '@tanstack/react-query'
 
 import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
 import {addStyle} from 'lib/styles'
-
-import {useTheme, atoms as a} from '#/alf'
+import {precacheProfile} from 'state/queries/profile'
+// import {Link} from '#/components/Link' TODO this imposes some styles that screw things up
+import {Link} from '#/view/com/util/Link'
+import {atoms as a, useTheme} from '#/alf'
 import {
   ModerationDetailsDialog,
   useModerationDetailsDialogControl,
 } from '#/components/moderation/ModerationDetailsDialog'
 import {Text} from '#/components/Typography'
-// import {Link} from '#/components/Link' TODO this imposes some styles that screw things up
-import {Link} from '#/view/com/util/Link'
 
 interface Props extends ComponentProps<typeof Link> {
   iconSize: number
   iconStyles: StyleProp<ViewStyle>
   modui: ModerationUI
+  profile: AppBskyActorDefs.ProfileViewBasic
 }
 
 export function PostHider({
@@ -30,8 +32,10 @@ export function PostHider({
   children,
   iconSize,
   iconStyles,
+  profile,
   ...props
 }: Props) {
+  const queryClient = useQueryClient()
   const t = useTheme()
   const {_} = useLingui()
   const [override, setOverride] = React.useState(false)
@@ -39,6 +43,10 @@ export function PostHider({
   const blur = modui.blurs[0]
   const desc = useModerationCauseDescription(blur)
 
+  const onBeforePress = React.useCallback(() => {
+    precacheProfile(queryClient, profile)
+  }, [queryClient, profile])
+
   if (!blur) {
     return (
       <Link
@@ -46,6 +54,7 @@ export function PostHider({
         style={style}
         href={href}
         accessible={false}
+        onBeforePress={onBeforePress}
         {...props}>
         {children}
       </Link>
diff --git a/src/screens/Messages/List/index.tsx b/src/screens/Messages/List/index.tsx
index 56dfb76c2..c4490aa5c 100644
--- a/src/screens/Messages/List/index.tsx
+++ b/src/screens/Messages/List/index.tsx
@@ -113,12 +113,7 @@ export function MessagesListScreen({}: Props) {
             <Link
               to={`/messages/${item.profile.handle}`}
               style={[a.flex_1, a.pl_md, a.py_sm, a.gap_md, a.pr_2xl]}>
-              <PreviewableUserAvatar
-                did={item.profile.did}
-                handle={item.profile.handle}
-                size={44}
-                avatar={item.profile.avatar}
-              />
+              <PreviewableUserAvatar profile={item.profile} size={44} />
               <View style={[a.flex_1]}>
                 <View
                   style={[
diff --git a/src/state/queries/post-feed.ts b/src/state/queries/post-feed.ts
index 42d7dbb9e..747dba02e 100644
--- a/src/state/queries/post-feed.ts
+++ b/src/state/queries/post-feed.ts
@@ -12,7 +12,6 @@ import {
   QueryClient,
   QueryKey,
   useInfiniteQuery,
-  useQueryClient,
 } from '@tanstack/react-query'
 
 import {HomeFeedAPI} from '#/lib/api/feed/home'
@@ -33,7 +32,6 @@ import {BSKY_FEED_OWNER_DIDS} from 'lib/constants'
 import {KnownError} from '#/view/com/posts/FeedErrorMessage'
 import {useFeedTuners} from '../preferences/feed-tuners'
 import {useModerationOpts} from './preferences'
-import {precacheFeedPostProfiles} from './profile'
 import {embedViewRecordToPostView, getEmbeddedPost} from './util'
 
 type ActorDid = string
@@ -102,7 +100,6 @@ export function usePostFeedQuery(
   params?: FeedParams,
   opts?: {enabled?: boolean; ignoreFilterFor?: string},
 ) {
-  const queryClient = useQueryClient()
   const feedTuners = useFeedTuners(feedDesc)
   const moderationOpts = useModerationOpts()
   const {getAgent} = useAgent()
@@ -151,7 +148,6 @@ export function usePostFeedQuery(
 
       try {
         const res = await api.fetch({cursor, limit: PAGE_SIZE})
-        precacheFeedPostProfiles(queryClient, res.feed)
 
         /*
          * If this is a public view, we need to check if posts fail moderation.
diff --git a/src/state/queries/post-thread.ts b/src/state/queries/post-thread.ts
index 46c7445ba..133304d2e 100644
--- a/src/state/queries/post-thread.ts
+++ b/src/state/queries/post-thread.ts
@@ -11,7 +11,6 @@ import {useAgent} from '#/state/session'
 import {findAllPostsInQueryData as findAllPostsInSearchQueryData} from 'state/queries/search-posts'
 import {findAllPostsInQueryData as findAllPostsInNotifsQueryData} from './notifications/feed'
 import {findAllPostsInQueryData as findAllPostsInFeedQueryData} from './post-feed'
-import {precacheThreadPostProfiles} from './profile'
 import {embedViewRecordToPostView, getEmbeddedPost} from './util'
 
 const RQKEY_ROOT = 'post-thread'
@@ -73,9 +72,7 @@ export function usePostThreadQuery(uri: string | undefined) {
     async queryFn() {
       const res = await getAgent().getPostThread({uri: uri!})
       if (res.success) {
-        const nodes = responseToThreadNodes(res.data.thread)
-        precacheThreadPostProfiles(queryClient, nodes)
-        return nodes
+        return responseToThreadNodes(res.data.thread)
       }
       return {type: 'unknown', uri: uri!}
     },
diff --git a/src/state/queries/profile.ts b/src/state/queries/profile.ts
index 6c801426e..103d34733 100644
--- a/src/state/queries/profile.ts
+++ b/src/state/queries/profile.ts
@@ -4,9 +4,6 @@ import {
   AppBskyActorDefs,
   AppBskyActorGetProfile,
   AppBskyActorProfile,
-  AppBskyEmbedRecord,
-  AppBskyEmbedRecordWithMedia,
-  AppBskyFeedDefs,
   AtUri,
   BskyAgent,
 } from '@atproto/api'
@@ -29,7 +26,6 @@ import {updateProfileShadow} from '../cache/profile-shadow'
 import {useAgent, useSession} from '../session'
 import {RQKEY as RQKEY_MY_BLOCKED} from './my-blocked-accounts'
 import {RQKEY as RQKEY_MY_MUTED} from './my-muted-accounts'
-import {ThreadNode} from './post-thread'
 
 const RQKEY_ROOT = 'profile'
 export const RQKEY = (did: string) => [RQKEY_ROOT, did]
@@ -477,56 +473,6 @@ export function precacheProfile(
   queryClient.setQueryData(profileBasicQueryKey(profile.did), profile)
 }
 
-export function precacheFeedPostProfiles(
-  queryClient: QueryClient,
-  posts: AppBskyFeedDefs.FeedViewPost[],
-) {
-  for (const post of posts) {
-    // Save the author of the post every time
-    precacheProfile(queryClient, post.post.author)
-    precachePostEmbedProfile(queryClient, post.post.embed)
-
-    // Cache parent author and embeds
-    const parent = post.reply?.parent
-    if (AppBskyFeedDefs.isPostView(parent)) {
-      precacheProfile(queryClient, parent.author)
-      precachePostEmbedProfile(queryClient, parent.embed)
-    }
-  }
-}
-
-function precachePostEmbedProfile(
-  queryClient: QueryClient,
-  embed: AppBskyFeedDefs.PostView['embed'],
-) {
-  if (AppBskyEmbedRecord.isView(embed)) {
-    if (AppBskyEmbedRecord.isViewRecord(embed.record)) {
-      precacheProfile(queryClient, embed.record.author)
-    }
-  } else if (AppBskyEmbedRecordWithMedia.isView(embed)) {
-    if (AppBskyEmbedRecord.isViewRecord(embed.record.record)) {
-      precacheProfile(queryClient, embed.record.record.author)
-    }
-  }
-}
-
-export function precacheThreadPostProfiles(
-  queryClient: QueryClient,
-  node: ThreadNode,
-) {
-  if (node.type === 'post') {
-    precacheProfile(queryClient, node.post.author)
-    if (node.parent) {
-      precacheThreadPostProfiles(queryClient, node.parent)
-    }
-    if (node.replies?.length) {
-      for (const reply of node.replies) {
-        precacheThreadPostProfiles(queryClient, reply)
-      }
-    }
-  }
-}
-
 async function whenAppViewReady(
   getAgent: () => BskyAgent,
   actor: string,
diff --git a/src/view/com/composer/ComposerReplyTo.tsx b/src/view/com/composer/ComposerReplyTo.tsx
index 24a2373f5..7dc17fd4a 100644
--- a/src/view/com/composer/ComposerReplyTo.tsx
+++ b/src/view/com/composer/ComposerReplyTo.tsx
@@ -86,9 +86,7 @@ export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
       )}>
       <PreviewableUserAvatar
         size={50}
-        did={replyTo.author.did}
-        handle={replyTo.author.handle}
-        avatar={replyTo.author.avatar}
+        profile={replyTo.author}
         moderation={replyTo.moderation?.ui('avatar')}
         type={replyTo.author.associated?.labeler ? 'labeler' : 'user'}
       />
diff --git a/src/view/com/notifications/FeedItem.tsx b/src/view/com/notifications/FeedItem.tsx
index 3c9c64061..94844cb1a 100644
--- a/src/view/com/notifications/FeedItem.tsx
+++ b/src/view/com/notifications/FeedItem.tsx
@@ -24,6 +24,7 @@ import {
 } from '@fortawesome/react-native-fontawesome'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
+import {useQueryClient} from '@tanstack/react-query'
 
 import {FeedNotification} from '#/state/queries/notifications/feed'
 import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
@@ -36,6 +37,7 @@ import {pluralize} from 'lib/strings/helpers'
 import {niceDate} from 'lib/strings/time'
 import {colors, s} from 'lib/styles'
 import {isWeb} from 'platform/detection'
+import {precacheProfile} from 'state/queries/profile'
 import {Link as NewLink} from '#/components/Link'
 import {ProfileHoverCard} from '#/components/ProfileHoverCard'
 import {FeedSourceCard} from '../feeds/FeedSourceCard'
@@ -52,13 +54,9 @@ const MAX_AUTHORS = 5
 const EXPANDED_AUTHOR_EL_HEIGHT = 35
 
 interface Author {
+  profile: AppBskyActorDefs.ProfileViewBasic
   href: string
-  did: string
-  handle: string
-  displayName?: string
-  avatar?: string
   moderation: ModerationDecision
-  associated?: AppBskyActorDefs.ProfileAssociated
 }
 
 let FeedItem = ({
@@ -68,6 +66,7 @@ let FeedItem = ({
   item: FeedNotification
   moderationOpts: ModerationOpts
 }): React.ReactNode => {
+  const queryClient = useQueryClient()
   const pal = usePalette('default')
   const {_} = useLingui()
   const [isAuthorsExpanded, setAuthorsExpanded] = useState<boolean>(false)
@@ -95,28 +94,22 @@ let FeedItem = ({
     setAuthorsExpanded(currentlyExpanded => !currentlyExpanded)
   }
 
+  const onBeforePress = React.useCallback(() => {
+    precacheProfile(queryClient, item.notification.author)
+  }, [queryClient, item.notification.author])
+
   const authors: Author[] = useMemo(() => {
     return [
       {
+        profile: item.notification.author,
         href: makeProfileLink(item.notification.author),
-        did: item.notification.author.did,
-        handle: item.notification.author.handle,
-        displayName: item.notification.author.displayName,
-        avatar: item.notification.author.avatar,
         moderation: moderateProfile(item.notification.author, moderationOpts),
-        associated: item.notification.author.associated,
       },
-      ...(item.additional?.map(({author}) => {
-        return {
-          href: makeProfileLink(author),
-          did: author.did,
-          handle: author.handle,
-          displayName: author.displayName,
-          avatar: author.avatar,
-          moderation: moderateProfile(author, moderationOpts),
-          associated: author.associated,
-        }
-      }) || []),
+      ...(item.additional?.map(({author}) => ({
+        profile: author,
+        href: makeProfileLink(author),
+        moderation: moderateProfile(author, moderationOpts),
+      })) || []),
     ]
   }, [item, moderationOpts])
 
@@ -201,7 +194,8 @@ let FeedItem = ({
       accessible={
         (item.type === 'post-like' && authors.length === 1) ||
         item.type === 'repost'
-      }>
+      }
+      onBeforePress={onBeforePress}>
       <View style={styles.layoutIcon}>
         {/* TODO: Prevent conditional rendering and move toward composable
         notifications for clearer accessibility labeling */}
@@ -231,7 +225,7 @@ let FeedItem = ({
               style={[pal.text, s.bold]}
               href={authors[0].href}
               text={sanitizeDisplayName(
-                authors[0].displayName || authors[0].handle,
+                authors[0].profile.displayName || authors[0].profile.handle,
               )}
               disableMismatchWarning
             />
@@ -339,11 +333,9 @@ function CondensedAuthorsList({
       <View style={styles.avis}>
         <PreviewableUserAvatar
           size={35}
-          did={authors[0].did}
-          handle={authors[0].handle}
-          avatar={authors[0].avatar}
+          profile={authors[0].profile}
           moderation={authors[0].moderation.ui('avatar')}
-          type={authors[0].associated?.labeler ? 'labeler' : 'user'}
+          type={authors[0].profile.associated?.labeler ? 'labeler' : 'user'}
         />
       </View>
     )
@@ -360,11 +352,9 @@ function CondensedAuthorsList({
           <View key={author.href} style={s.mr5}>
             <PreviewableUserAvatar
               size={35}
-              did={author.did}
-              handle={author.handle}
-              avatar={author.avatar}
+              profile={author.profile}
               moderation={author.moderation.ui('avatar')}
-              type={author.associated?.labeler ? 'labeler' : 'user'}
+              type={author.profile.associated?.labeler ? 'labeler' : 'user'}
             />
           </View>
         ))}
@@ -415,20 +405,20 @@ function ExpandedAuthorsList({
       ]}>
       {authors.map(author => (
         <NewLink
-          key={author.did}
+          key={author.profile.did}
           label={_(msg`See profile`)}
           to={makeProfileLink({
-            did: author.did,
-            handle: author.handle,
+            did: author.profile.did,
+            handle: author.profile.handle,
           })}
           style={styles.expandedAuthor}>
           <View style={styles.expandedAuthorAvi}>
-            <ProfileHoverCard did={author.did}>
+            <ProfileHoverCard did={author.profile.did}>
               <UserAvatar
                 size={35}
-                avatar={author.avatar}
+                avatar={author.profile.avatar}
                 moderation={author.moderation.ui('avatar')}
-                type={author.associated?.labeler ? 'labeler' : 'user'}
+                type={author.profile.associated?.labeler ? 'labeler' : 'user'}
               />
             </ProfileHoverCard>
           </View>
@@ -438,10 +428,12 @@ function ExpandedAuthorsList({
               numberOfLines={1}
               style={pal.text}
               lineHeight={1.2}>
-              {sanitizeDisplayName(author.displayName || author.handle)}
+              {sanitizeDisplayName(
+                author.profile.displayName || author.profile.handle,
+              )}
               &nbsp;
               <Text style={[pal.textLight]} lineHeight={1.2}>
-                {sanitizeHandle(author.handle)}
+                {sanitizeHandle(author.profile.handle)}
               </Text>
             </Text>
           </View>
diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx
index 4c11fdff3..564e37e7a 100644
--- a/src/view/com/post-thread/PostThreadItem.tsx
+++ b/src/view/com/post-thread/PostThreadItem.tsx
@@ -249,9 +249,7 @@ let PostThreadItemLoaded = ({
             <View style={[styles.layoutAvi, {paddingBottom: 8}]}>
               <PreviewableUserAvatar
                 size={42}
-                did={post.author.did}
-                handle={post.author.handle}
-                avatar={post.author.avatar}
+                profile={post.author}
                 moderation={moderation.ui('avatar')}
                 type={post.author.associated?.labeler ? 'labeler' : 'user'}
               />
@@ -399,7 +397,8 @@ let PostThreadItemLoaded = ({
               isThreadedChild
                 ? {marginRight: 4}
                 : {marginLeft: 2, marginRight: 2}
-            }>
+            }
+            profile={post.author}>
             <View
               style={{
                 flexDirection: 'row',
@@ -440,9 +439,7 @@ let PostThreadItemLoaded = ({
                 <View style={styles.layoutAvi}>
                   <PreviewableUserAvatar
                     size={38}
-                    did={post.author.did}
-                    handle={post.author.handle}
-                    avatar={post.author.avatar}
+                    profile={post.author}
                     moderation={moderation.ui('avatar')}
                     type={post.author.associated?.labeler ? 'labeler' : 'user'}
                   />
diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx
index b46586941..546eb2821 100644
--- a/src/view/com/post/Post.tsx
+++ b/src/view/com/post/Post.tsx
@@ -21,7 +21,7 @@ import {usePalette} from 'lib/hooks/usePalette'
 import {makeProfileLink} from 'lib/routes/links'
 import {countLines} from 'lib/strings/helpers'
 import {colors, s} from 'lib/styles'
-import {RQKEY as RQKEY_URI} from 'state/queries/resolve-uri'
+import {precacheProfile} from 'state/queries/profile'
 import {atoms as a} from '#/alf'
 import {ProfileHoverCard} from '#/components/ProfileHoverCard'
 import {RichText} from '#/components/RichText'
@@ -135,8 +135,8 @@ function PostInner({
   }, [setLimitLines])
 
   const onBeforePress = React.useCallback(() => {
-    queryClient.setQueryData(RQKEY_URI(post.author.handle), post.author.did)
-  }, [queryClient, post.author.handle, post.author.did])
+    precacheProfile(queryClient, post.author)
+  }, [queryClient, post.author])
 
   return (
     <Link
@@ -148,9 +148,7 @@ function PostInner({
         <View style={styles.layoutAvi}>
           <PreviewableUserAvatar
             size={52}
-            did={post.author.did}
-            handle={post.author.handle}
-            avatar={post.author.avatar}
+            profile={post.author}
             moderation={moderation.ui('avatar')}
             type={post.author.associated?.labeler ? 'labeler' : 'user'}
           />
diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx
index 7694b5024..605dffde9 100644
--- a/src/view/com/posts/FeedItem.tsx
+++ b/src/view/com/posts/FeedItem.tsx
@@ -13,6 +13,7 @@ import {
 } from '@fortawesome/react-native-fontawesome'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
+import {useQueryClient} from '@tanstack/react-query'
 
 import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow'
 import {useComposerControls} from '#/state/shell/composer'
@@ -24,6 +25,7 @@ import {sanitizeDisplayName} from 'lib/strings/display-names'
 import {sanitizeHandle} from 'lib/strings/handles'
 import {countLines} from 'lib/strings/helpers'
 import {s} from 'lib/styles'
+import {precacheProfile} from 'state/queries/profile'
 import {atoms as a} from '#/alf'
 import {ContentHider} from '#/components/moderation/ContentHider'
 import {ProfileHoverCard} from '#/components/ProfileHoverCard'
@@ -106,6 +108,7 @@ let FeedItemInner = ({
   isThreadLastChild?: boolean
   isThreadParent?: boolean
 }): React.ReactNode => {
+  const queryClient = useQueryClient()
   const {openComposer} = useComposerControls()
   const pal = usePalette('default')
   const {_} = useLingui()
@@ -135,6 +138,10 @@ let FeedItemInner = ({
     })
   }, [post, record, openComposer, moderation])
 
+  const onBeforePress = React.useCallback(() => {
+    precacheProfile(queryClient, post.author)
+  }, [queryClient, post.author])
+
   const outerStyles = [
     styles.outer,
     {
@@ -153,7 +160,8 @@ let FeedItemInner = ({
       style={outerStyles}
       href={href}
       noFeedback
-      accessible={false}>
+      accessible={false}
+      onBeforePress={onBeforePress}>
       <View style={{flexDirection: 'row', gap: 10, paddingLeft: 8}}>
         <View style={{width: 52}}>
           {isThreadChild && (
@@ -240,9 +248,7 @@ let FeedItemInner = ({
         <View style={styles.layoutAvi}>
           <PreviewableUserAvatar
             size={52}
-            did={post.author.did}
-            handle={post.author.handle}
-            avatar={post.author.avatar}
+            profile={post.author}
             moderation={moderation.ui('avatar')}
             type={post.author.associated?.labeler ? 'labeler' : 'user'}
           />
diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx
index b52573a01..90ab9b738 100644
--- a/src/view/com/profile/ProfileCard.tsx
+++ b/src/view/com/profile/ProfileCard.tsx
@@ -20,8 +20,7 @@ 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 {profileBasicQueryKey as RQKEY_PROFILE_BASIC} from 'state/queries/profile'
-import {RQKEY as RQKEY_URI} from 'state/queries/resolve-uri'
+import {precacheProfile} from 'state/queries/profile'
 import {Link} from '../util/Link'
 import {Text} from '../util/text/Text'
 import {PreviewableUserAvatar} from '../util/UserAvatar'
@@ -58,9 +57,7 @@ export function ProfileCard({
 
   const onBeforePress = React.useCallback(() => {
     onPress?.()
-
-    queryClient.setQueryData(RQKEY_URI(profile.handle), profile.did)
-    queryClient.setQueryData(RQKEY_PROFILE_BASIC(profile.did), profile)
+    precacheProfile(queryClient, profile)
   }, [onPress, profile, queryClient])
 
   if (!moderationOpts) {
@@ -91,9 +88,7 @@ export function ProfileCard({
         <View style={styles.layoutAvi}>
           <PreviewableUserAvatar
             size={40}
-            did={profile.did}
-            handle={profile.handle}
-            avatar={profile.avatar}
+            profile={profile}
             moderation={moderation.ui('avatar')}
             type={isLabeler ? 'labeler' : 'user'}
           />
@@ -238,9 +233,7 @@ function FollowersList({
           <View style={[styles.followedByAvi, pal.view]}>
             <PreviewableUserAvatar
               size={32}
-              did={f.did}
-              handle={f.handle}
-              avatar={f.avatar}
+              profile={f}
               moderation={mod.ui('avatar')}
               type={f.associated?.labeler ? 'labeler' : 'user'}
             />
diff --git a/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx b/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx
index cf35885cd..4c9d164f7 100644
--- a/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx
+++ b/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx
@@ -220,8 +220,7 @@ function SuggestedFollow({
         ]}>
         <PreviewableUserAvatar
           size={60}
-          did={profile.did}
-          handle={profile.handle}
+          profile={profile}
           avatar={profile.avatar}
           moderation={moderation.ui('avatar')}
         />
diff --git a/src/view/com/util/Link.tsx b/src/view/com/util/Link.tsx
index d35d0fcc6..78d995ee8 100644
--- a/src/view/com/util/Link.tsx
+++ b/src/view/com/util/Link.tsx
@@ -148,6 +148,7 @@ export const TextLink = memo(function TextLink({
   dataSet,
   title,
   onPress,
+  onBeforePress,
   disableMismatchWarning,
   navigationAction,
   anchorNoUnderline,
@@ -165,6 +166,7 @@ export const TextLink = memo(function TextLink({
   disableMismatchWarning?: boolean
   navigationAction?: 'push' | 'replace' | 'navigate'
   anchorNoUnderline?: boolean
+  onBeforePress?: () => void
 } & TextProps) {
   const {...props} = useLinkProps({to: sanitizeUrl(href)})
   const navigation = useNavigationDeduped()
@@ -202,6 +204,7 @@ export const TextLink = memo(function TextLink({
         // Let the browser handle opening in new tab etc.
         return
       }
+      onBeforePress?.()
       if (onPress) {
         e?.preventDefault?.()
         // @ts-ignore function signature differs by platform -prf
@@ -226,6 +229,7 @@ export const TextLink = memo(function TextLink({
       disableMismatchWarning,
       navigationAction,
       openLink,
+      onBeforePress,
     ],
   )
   const hrefAttrs = useMemo(() => {
@@ -274,6 +278,7 @@ interface TextLinkOnWebOnlyProps extends TextProps {
   title?: string
   navigationAction?: 'push' | 'replace' | 'navigate'
   disableMismatchWarning?: boolean
+  onBeforePress?: () => void
   onPointerEnter?: () => void
   anchorNoUnderline?: boolean
 }
@@ -287,6 +292,7 @@ export const TextLinkOnWebOnly = memo(function DesktopWebTextLink({
   lineHeight,
   navigationAction,
   disableMismatchWarning,
+  onBeforePress,
   ...props
 }: TextLinkOnWebOnlyProps) {
   if (isWeb) {
@@ -302,6 +308,7 @@ export const TextLinkOnWebOnly = memo(function DesktopWebTextLink({
         title={props.title}
         navigationAction={navigationAction}
         disableMismatchWarning={disableMismatchWarning}
+        onBeforePress={onBeforePress}
         {...props}
       />
     )
diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx
index ed3d3e5b0..db16ff066 100644
--- a/src/view/com/util/PostMeta.tsx
+++ b/src/view/com/util/PostMeta.tsx
@@ -1,8 +1,9 @@
-import React, {memo} from 'react'
+import React, {memo, useCallback} from 'react'
 import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native'
 import {AppBskyActorDefs, ModerationDecision, ModerationUI} from '@atproto/api'
+import {useQueryClient} from '@tanstack/react-query'
 
-import {usePrefetchProfileQuery} from '#/state/queries/profile'
+import {precacheProfile, usePrefetchProfileQuery} from '#/state/queries/profile'
 import {usePalette} from 'lib/hooks/usePalette'
 import {makeProfileLink} from 'lib/routes/links'
 import {sanitizeDisplayName} from 'lib/strings/display-names'
@@ -40,15 +41,18 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
     ? () => prefetchProfileQuery(opts.author.did)
     : undefined
 
+  const queryClient = useQueryClient()
+  const onBeforePress = useCallback(() => {
+    precacheProfile(queryClient, opts.author)
+  }, [queryClient, opts.author])
+
   return (
     <View style={[styles.container, opts.style]}>
       {opts.showAvatar && (
         <View style={styles.avatar}>
           <PreviewableUserAvatar
             size={opts.avatarSize || 16}
-            did={opts.author.did}
-            handle={opts.author.handle}
-            avatar={opts.author.avatar}
+            profile={opts.author}
             moderation={opts.avatarModeration}
             type={opts.author.associated?.labeler ? 'labeler' : 'user'}
           />
@@ -71,6 +75,7 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
             </>
           }
           href={profileLink}
+          onBeforePress={onBeforePress}
           onPointerEnter={onPointerEnter}
         />
         <TextLinkOnWebOnly
@@ -79,6 +84,7 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
           style={[pal.textLight, {flexShrink: 4}]}
           text={'\xa0' + sanitizeHandle(handle, '@')}
           href={profileLink}
+          onBeforePress={onBeforePress}
           onPointerEnter={onPointerEnter}
           anchorNoUnderline
         />
@@ -103,6 +109,7 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
             title={niceDate(opts.timestamp)}
             accessibilityHint=""
             href={opts.postHref}
+            onBeforePress={onBeforePress}
           />
         )}
       </TimeElapsed>
diff --git a/src/view/com/util/TimeElapsed.tsx b/src/view/com/util/TimeElapsed.tsx
index aa3a09223..6ea41b82b 100644
--- a/src/view/com/util/TimeElapsed.tsx
+++ b/src/view/com/util/TimeElapsed.tsx
@@ -1,6 +1,7 @@
 import React from 'react'
-import {ago} from 'lib/strings/time'
+
 import {useTickEveryMinute} from '#/state/shell'
+import {ago} from 'lib/strings/time'
 
 // FIXME(dan): Figure out why the false positives
 
@@ -12,7 +13,7 @@ export function TimeElapsed({
   children: ({timeElapsed}: {timeElapsed: string}) => JSX.Element
 }) {
   const tick = useTickEveryMinute()
-  const [timeElapsed, setTimeAgo] = React.useState(ago(timestamp))
+  const [timeElapsed, setTimeAgo] = React.useState(() => ago(timestamp))
 
   React.useEffect(() => {
     setTimeAgo(ago(timestamp))
diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx
index 89aa56b73..118e2ce2b 100644
--- a/src/view/com/util/UserAvatar.tsx
+++ b/src/view/com/util/UserAvatar.tsx
@@ -2,10 +2,11 @@ import React, {memo, useMemo} from 'react'
 import {Image, StyleSheet, TouchableOpacity, View} from 'react-native'
 import {Image as RNImage} from 'react-native-image-crop-picker'
 import Svg, {Circle, Path, Rect} from 'react-native-svg'
-import {ModerationUI} from '@atproto/api'
+import {AppBskyActorDefs, ModerationUI} from '@atproto/api'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
+import {useQueryClient} from '@tanstack/react-query'
 
 import {usePalette} from 'lib/hooks/usePalette'
 import {
@@ -15,6 +16,7 @@ import {
 import {makeProfileLink} from 'lib/routes/links'
 import {colors} from 'lib/styles'
 import {isAndroid, isNative, isWeb} from 'platform/detection'
+import {precacheProfile} from 'state/queries/profile'
 import {HighPriorityImage} from 'view/com/util/images/Image'
 import {tokens, useTheme} from '#/alf'
 import {
@@ -47,8 +49,7 @@ interface EditableUserAvatarProps extends BaseUserAvatarProps {
 
 interface PreviewableUserAvatarProps extends BaseUserAvatarProps {
   moderation?: ModerationUI
-  did: string
-  handle: string
+  profile: AppBskyActorDefs.ProfileViewBasic
 }
 
 const BLUR_AMOUNT = isWeb ? 5 : 100
@@ -371,19 +372,28 @@ let EditableUserAvatar = ({
 EditableUserAvatar = memo(EditableUserAvatar)
 export {EditableUserAvatar}
 
-let PreviewableUserAvatar = (
-  props: PreviewableUserAvatarProps,
-): React.ReactNode => {
+let PreviewableUserAvatar = ({
+  moderation,
+  profile,
+  ...rest
+}: PreviewableUserAvatarProps): React.ReactNode => {
   const {_} = useLingui()
+  const queryClient = useQueryClient()
+
+  const onPress = React.useCallback(() => {
+    precacheProfile(queryClient, profile)
+  }, [profile, queryClient])
+
   return (
-    <ProfileHoverCard did={props.did}>
+    <ProfileHoverCard did={profile.did}>
       <Link
         label={_(msg`See profile`)}
         to={makeProfileLink({
-          did: props.did,
-          handle: props.handle,
-        })}>
-        <UserAvatar {...props} />
+          did: profile.did,
+          handle: profile.handle,
+        })}
+        onPress={onPress}>
+        <UserAvatar avatar={profile.avatar} moderation={moderation} {...rest} />
       </Link>
     </ProfileHoverCard>
   )
diff --git a/src/view/com/util/post-embeds/QuoteEmbed.tsx b/src/view/com/util/post-embeds/QuoteEmbed.tsx
index 935696ab7..e0178f34b 100644
--- a/src/view/com/util/post-embeds/QuoteEmbed.tsx
+++ b/src/view/com/util/post-embeds/QuoteEmbed.tsx
@@ -26,10 +26,10 @@ import {useQueryClient} from '@tanstack/react-query'
 import {HITSLOP_20} from '#/lib/constants'
 import {s} from '#/lib/styles'
 import {useModerationOpts} from '#/state/queries/preferences'
-import {RQKEY as RQKEY_URI} from '#/state/queries/resolve-uri'
 import {usePalette} from 'lib/hooks/usePalette'
 import {InfoCircleIcon} from 'lib/icons'
 import {makeProfileLink} from 'lib/routes/links'
+import {precacheProfile} from 'state/queries/profile'
 import {ComposerOptsQuote} from 'state/shell/composer'
 import {atoms as a} from '#/alf'
 import {RichText} from '#/components/RichText'
@@ -149,8 +149,8 @@ export function QuoteEmbed({
   }, [quote.embeds])
 
   const onBeforePress = React.useCallback(() => {
-    queryClient.setQueryData(RQKEY_URI(quote.author.handle), quote.author.did)
-  }, [queryClient, quote.author.did, quote.author.handle])
+    precacheProfile(queryClient, quote.author)
+  }, [queryClient, quote.author])
 
   return (
     <ContentHider modui={moderation?.ui('contentList')}>