remove precacheThreadPostProfiles (#3729)

* remove `precacheThreadPostProfiles`

* add `displayName` to `PreviewableUserAvatar`

* memo

* use `precacheProfile`

* pass `profile` directly to `PreviewableUserAvatar`

* update the `UserAvatar`'s props

* remove feed cache

* one more spot

* rm unused queryClient

* Don't call fn unnecessarily

* Preload for display name too

* try notification item

* add to feeditem

* and finally, precache for post threads

* timestamp

* Fix

* onBeforePress

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
zio/stable
Hailey 2024-04-26 22:31:07 -07:00 committed by GitHub
parent ce85375c85
commit 7eb1444f2c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 119 additions and 168 deletions

View File

@ -1,25 +1,27 @@
import React, {ComponentProps} from 'react' import React, {ComponentProps} from 'react'
import {StyleSheet, Pressable, View, ViewStyle, StyleProp} from 'react-native' import {Pressable, StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
import {ModerationUI} from '@atproto/api' import {AppBskyActorDefs, ModerationUI} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {Trans, msg} from '@lingui/macro' import {useQueryClient} from '@tanstack/react-query'
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription' import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
import {addStyle} from 'lib/styles' import {addStyle} from 'lib/styles'
import {precacheProfile} from 'state/queries/profile'
import {useTheme, atoms as a} from '#/alf' // 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 { import {
ModerationDetailsDialog, ModerationDetailsDialog,
useModerationDetailsDialogControl, useModerationDetailsDialogControl,
} from '#/components/moderation/ModerationDetailsDialog' } from '#/components/moderation/ModerationDetailsDialog'
import {Text} from '#/components/Typography' 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> { interface Props extends ComponentProps<typeof Link> {
iconSize: number iconSize: number
iconStyles: StyleProp<ViewStyle> iconStyles: StyleProp<ViewStyle>
modui: ModerationUI modui: ModerationUI
profile: AppBskyActorDefs.ProfileViewBasic
} }
export function PostHider({ export function PostHider({
@ -30,8 +32,10 @@ export function PostHider({
children, children,
iconSize, iconSize,
iconStyles, iconStyles,
profile,
...props ...props
}: Props) { }: Props) {
const queryClient = useQueryClient()
const t = useTheme() const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
const [override, setOverride] = React.useState(false) const [override, setOverride] = React.useState(false)
@ -39,6 +43,10 @@ export function PostHider({
const blur = modui.blurs[0] const blur = modui.blurs[0]
const desc = useModerationCauseDescription(blur) const desc = useModerationCauseDescription(blur)
const onBeforePress = React.useCallback(() => {
precacheProfile(queryClient, profile)
}, [queryClient, profile])
if (!blur) { if (!blur) {
return ( return (
<Link <Link
@ -46,6 +54,7 @@ export function PostHider({
style={style} style={style}
href={href} href={href}
accessible={false} accessible={false}
onBeforePress={onBeforePress}
{...props}> {...props}>
{children} {children}
</Link> </Link>

View File

@ -113,12 +113,7 @@ export function MessagesListScreen({}: Props) {
<Link <Link
to={`/messages/${item.profile.handle}`} to={`/messages/${item.profile.handle}`}
style={[a.flex_1, a.pl_md, a.py_sm, a.gap_md, a.pr_2xl]}> style={[a.flex_1, a.pl_md, a.py_sm, a.gap_md, a.pr_2xl]}>
<PreviewableUserAvatar <PreviewableUserAvatar profile={item.profile} size={44} />
did={item.profile.did}
handle={item.profile.handle}
size={44}
avatar={item.profile.avatar}
/>
<View style={[a.flex_1]}> <View style={[a.flex_1]}>
<View <View
style={[ style={[

View File

@ -12,7 +12,6 @@ import {
QueryClient, QueryClient,
QueryKey, QueryKey,
useInfiniteQuery, useInfiniteQuery,
useQueryClient,
} from '@tanstack/react-query' } from '@tanstack/react-query'
import {HomeFeedAPI} from '#/lib/api/feed/home' 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 {KnownError} from '#/view/com/posts/FeedErrorMessage'
import {useFeedTuners} from '../preferences/feed-tuners' import {useFeedTuners} from '../preferences/feed-tuners'
import {useModerationOpts} from './preferences' import {useModerationOpts} from './preferences'
import {precacheFeedPostProfiles} from './profile'
import {embedViewRecordToPostView, getEmbeddedPost} from './util' import {embedViewRecordToPostView, getEmbeddedPost} from './util'
type ActorDid = string type ActorDid = string
@ -102,7 +100,6 @@ export function usePostFeedQuery(
params?: FeedParams, params?: FeedParams,
opts?: {enabled?: boolean; ignoreFilterFor?: string}, opts?: {enabled?: boolean; ignoreFilterFor?: string},
) { ) {
const queryClient = useQueryClient()
const feedTuners = useFeedTuners(feedDesc) const feedTuners = useFeedTuners(feedDesc)
const moderationOpts = useModerationOpts() const moderationOpts = useModerationOpts()
const {getAgent} = useAgent() const {getAgent} = useAgent()
@ -151,7 +148,6 @@ export function usePostFeedQuery(
try { try {
const res = await api.fetch({cursor, limit: PAGE_SIZE}) 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. * If this is a public view, we need to check if posts fail moderation.

View File

@ -11,7 +11,6 @@ import {useAgent} from '#/state/session'
import {findAllPostsInQueryData as findAllPostsInSearchQueryData} from 'state/queries/search-posts' import {findAllPostsInQueryData as findAllPostsInSearchQueryData} from 'state/queries/search-posts'
import {findAllPostsInQueryData as findAllPostsInNotifsQueryData} from './notifications/feed' import {findAllPostsInQueryData as findAllPostsInNotifsQueryData} from './notifications/feed'
import {findAllPostsInQueryData as findAllPostsInFeedQueryData} from './post-feed' import {findAllPostsInQueryData as findAllPostsInFeedQueryData} from './post-feed'
import {precacheThreadPostProfiles} from './profile'
import {embedViewRecordToPostView, getEmbeddedPost} from './util' import {embedViewRecordToPostView, getEmbeddedPost} from './util'
const RQKEY_ROOT = 'post-thread' const RQKEY_ROOT = 'post-thread'
@ -73,9 +72,7 @@ export function usePostThreadQuery(uri: string | undefined) {
async queryFn() { async queryFn() {
const res = await getAgent().getPostThread({uri: uri!}) const res = await getAgent().getPostThread({uri: uri!})
if (res.success) { if (res.success) {
const nodes = responseToThreadNodes(res.data.thread) return responseToThreadNodes(res.data.thread)
precacheThreadPostProfiles(queryClient, nodes)
return nodes
} }
return {type: 'unknown', uri: uri!} return {type: 'unknown', uri: uri!}
}, },

View File

@ -4,9 +4,6 @@ import {
AppBskyActorDefs, AppBskyActorDefs,
AppBskyActorGetProfile, AppBskyActorGetProfile,
AppBskyActorProfile, AppBskyActorProfile,
AppBskyEmbedRecord,
AppBskyEmbedRecordWithMedia,
AppBskyFeedDefs,
AtUri, AtUri,
BskyAgent, BskyAgent,
} from '@atproto/api' } from '@atproto/api'
@ -29,7 +26,6 @@ import {updateProfileShadow} from '../cache/profile-shadow'
import {useAgent, useSession} from '../session' import {useAgent, useSession} from '../session'
import {RQKEY as RQKEY_MY_BLOCKED} from './my-blocked-accounts' import {RQKEY as RQKEY_MY_BLOCKED} from './my-blocked-accounts'
import {RQKEY as RQKEY_MY_MUTED} from './my-muted-accounts' import {RQKEY as RQKEY_MY_MUTED} from './my-muted-accounts'
import {ThreadNode} from './post-thread'
const RQKEY_ROOT = 'profile' const RQKEY_ROOT = 'profile'
export const RQKEY = (did: string) => [RQKEY_ROOT, did] export const RQKEY = (did: string) => [RQKEY_ROOT, did]
@ -477,56 +473,6 @@ export function precacheProfile(
queryClient.setQueryData(profileBasicQueryKey(profile.did), profile) 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( async function whenAppViewReady(
getAgent: () => BskyAgent, getAgent: () => BskyAgent,
actor: string, actor: string,

View File

@ -86,9 +86,7 @@ export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
)}> )}>
<PreviewableUserAvatar <PreviewableUserAvatar
size={50} size={50}
did={replyTo.author.did} profile={replyTo.author}
handle={replyTo.author.handle}
avatar={replyTo.author.avatar}
moderation={replyTo.moderation?.ui('avatar')} moderation={replyTo.moderation?.ui('avatar')}
type={replyTo.author.associated?.labeler ? 'labeler' : 'user'} type={replyTo.author.associated?.labeler ? 'labeler' : 'user'}
/> />

View File

@ -24,6 +24,7 @@ import {
} from '@fortawesome/react-native-fontawesome' } from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
import {FeedNotification} from '#/state/queries/notifications/feed' import {FeedNotification} from '#/state/queries/notifications/feed'
import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
@ -36,6 +37,7 @@ import {pluralize} from 'lib/strings/helpers'
import {niceDate} from 'lib/strings/time' import {niceDate} from 'lib/strings/time'
import {colors, s} from 'lib/styles' import {colors, s} from 'lib/styles'
import {isWeb} from 'platform/detection' import {isWeb} from 'platform/detection'
import {precacheProfile} from 'state/queries/profile'
import {Link as NewLink} from '#/components/Link' import {Link as NewLink} from '#/components/Link'
import {ProfileHoverCard} from '#/components/ProfileHoverCard' import {ProfileHoverCard} from '#/components/ProfileHoverCard'
import {FeedSourceCard} from '../feeds/FeedSourceCard' import {FeedSourceCard} from '../feeds/FeedSourceCard'
@ -52,13 +54,9 @@ const MAX_AUTHORS = 5
const EXPANDED_AUTHOR_EL_HEIGHT = 35 const EXPANDED_AUTHOR_EL_HEIGHT = 35
interface Author { interface Author {
profile: AppBskyActorDefs.ProfileViewBasic
href: string href: string
did: string
handle: string
displayName?: string
avatar?: string
moderation: ModerationDecision moderation: ModerationDecision
associated?: AppBskyActorDefs.ProfileAssociated
} }
let FeedItem = ({ let FeedItem = ({
@ -68,6 +66,7 @@ let FeedItem = ({
item: FeedNotification item: FeedNotification
moderationOpts: ModerationOpts moderationOpts: ModerationOpts
}): React.ReactNode => { }): React.ReactNode => {
const queryClient = useQueryClient()
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui() const {_} = useLingui()
const [isAuthorsExpanded, setAuthorsExpanded] = useState<boolean>(false) const [isAuthorsExpanded, setAuthorsExpanded] = useState<boolean>(false)
@ -95,28 +94,22 @@ let FeedItem = ({
setAuthorsExpanded(currentlyExpanded => !currentlyExpanded) setAuthorsExpanded(currentlyExpanded => !currentlyExpanded)
} }
const onBeforePress = React.useCallback(() => {
precacheProfile(queryClient, item.notification.author)
}, [queryClient, item.notification.author])
const authors: Author[] = useMemo(() => { const authors: Author[] = useMemo(() => {
return [ return [
{ {
profile: item.notification.author,
href: makeProfileLink(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), moderation: moderateProfile(item.notification.author, moderationOpts),
associated: item.notification.author.associated,
}, },
...(item.additional?.map(({author}) => { ...(item.additional?.map(({author}) => ({
return { profile: author,
href: makeProfileLink(author), href: makeProfileLink(author),
did: author.did,
handle: author.handle,
displayName: author.displayName,
avatar: author.avatar,
moderation: moderateProfile(author, moderationOpts), moderation: moderateProfile(author, moderationOpts),
associated: author.associated, })) || []),
}
}) || []),
] ]
}, [item, moderationOpts]) }, [item, moderationOpts])
@ -201,7 +194,8 @@ let FeedItem = ({
accessible={ accessible={
(item.type === 'post-like' && authors.length === 1) || (item.type === 'post-like' && authors.length === 1) ||
item.type === 'repost' item.type === 'repost'
}> }
onBeforePress={onBeforePress}>
<View style={styles.layoutIcon}> <View style={styles.layoutIcon}>
{/* TODO: Prevent conditional rendering and move toward composable {/* TODO: Prevent conditional rendering and move toward composable
notifications for clearer accessibility labeling */} notifications for clearer accessibility labeling */}
@ -231,7 +225,7 @@ let FeedItem = ({
style={[pal.text, s.bold]} style={[pal.text, s.bold]}
href={authors[0].href} href={authors[0].href}
text={sanitizeDisplayName( text={sanitizeDisplayName(
authors[0].displayName || authors[0].handle, authors[0].profile.displayName || authors[0].profile.handle,
)} )}
disableMismatchWarning disableMismatchWarning
/> />
@ -339,11 +333,9 @@ function CondensedAuthorsList({
<View style={styles.avis}> <View style={styles.avis}>
<PreviewableUserAvatar <PreviewableUserAvatar
size={35} size={35}
did={authors[0].did} profile={authors[0].profile}
handle={authors[0].handle}
avatar={authors[0].avatar}
moderation={authors[0].moderation.ui('avatar')} moderation={authors[0].moderation.ui('avatar')}
type={authors[0].associated?.labeler ? 'labeler' : 'user'} type={authors[0].profile.associated?.labeler ? 'labeler' : 'user'}
/> />
</View> </View>
) )
@ -360,11 +352,9 @@ function CondensedAuthorsList({
<View key={author.href} style={s.mr5}> <View key={author.href} style={s.mr5}>
<PreviewableUserAvatar <PreviewableUserAvatar
size={35} size={35}
did={author.did} profile={author.profile}
handle={author.handle}
avatar={author.avatar}
moderation={author.moderation.ui('avatar')} moderation={author.moderation.ui('avatar')}
type={author.associated?.labeler ? 'labeler' : 'user'} type={author.profile.associated?.labeler ? 'labeler' : 'user'}
/> />
</View> </View>
))} ))}
@ -415,20 +405,20 @@ function ExpandedAuthorsList({
]}> ]}>
{authors.map(author => ( {authors.map(author => (
<NewLink <NewLink
key={author.did} key={author.profile.did}
label={_(msg`See profile`)} label={_(msg`See profile`)}
to={makeProfileLink({ to={makeProfileLink({
did: author.did, did: author.profile.did,
handle: author.handle, handle: author.profile.handle,
})} })}
style={styles.expandedAuthor}> style={styles.expandedAuthor}>
<View style={styles.expandedAuthorAvi}> <View style={styles.expandedAuthorAvi}>
<ProfileHoverCard did={author.did}> <ProfileHoverCard did={author.profile.did}>
<UserAvatar <UserAvatar
size={35} size={35}
avatar={author.avatar} avatar={author.profile.avatar}
moderation={author.moderation.ui('avatar')} moderation={author.moderation.ui('avatar')}
type={author.associated?.labeler ? 'labeler' : 'user'} type={author.profile.associated?.labeler ? 'labeler' : 'user'}
/> />
</ProfileHoverCard> </ProfileHoverCard>
</View> </View>
@ -438,10 +428,12 @@ function ExpandedAuthorsList({
numberOfLines={1} numberOfLines={1}
style={pal.text} style={pal.text}
lineHeight={1.2}> lineHeight={1.2}>
{sanitizeDisplayName(author.displayName || author.handle)} {sanitizeDisplayName(
author.profile.displayName || author.profile.handle,
)}
&nbsp; &nbsp;
<Text style={[pal.textLight]} lineHeight={1.2}> <Text style={[pal.textLight]} lineHeight={1.2}>
{sanitizeHandle(author.handle)} {sanitizeHandle(author.profile.handle)}
</Text> </Text>
</Text> </Text>
</View> </View>

View File

@ -249,9 +249,7 @@ let PostThreadItemLoaded = ({
<View style={[styles.layoutAvi, {paddingBottom: 8}]}> <View style={[styles.layoutAvi, {paddingBottom: 8}]}>
<PreviewableUserAvatar <PreviewableUserAvatar
size={42} size={42}
did={post.author.did} profile={post.author}
handle={post.author.handle}
avatar={post.author.avatar}
moderation={moderation.ui('avatar')} moderation={moderation.ui('avatar')}
type={post.author.associated?.labeler ? 'labeler' : 'user'} type={post.author.associated?.labeler ? 'labeler' : 'user'}
/> />
@ -399,7 +397,8 @@ let PostThreadItemLoaded = ({
isThreadedChild isThreadedChild
? {marginRight: 4} ? {marginRight: 4}
: {marginLeft: 2, marginRight: 2} : {marginLeft: 2, marginRight: 2}
}> }
profile={post.author}>
<View <View
style={{ style={{
flexDirection: 'row', flexDirection: 'row',
@ -440,9 +439,7 @@ let PostThreadItemLoaded = ({
<View style={styles.layoutAvi}> <View style={styles.layoutAvi}>
<PreviewableUserAvatar <PreviewableUserAvatar
size={38} size={38}
did={post.author.did} profile={post.author}
handle={post.author.handle}
avatar={post.author.avatar}
moderation={moderation.ui('avatar')} moderation={moderation.ui('avatar')}
type={post.author.associated?.labeler ? 'labeler' : 'user'} type={post.author.associated?.labeler ? 'labeler' : 'user'}
/> />

View File

@ -21,7 +21,7 @@ import {usePalette} from 'lib/hooks/usePalette'
import {makeProfileLink} from 'lib/routes/links' import {makeProfileLink} from 'lib/routes/links'
import {countLines} from 'lib/strings/helpers' import {countLines} from 'lib/strings/helpers'
import {colors, s} from 'lib/styles' 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 {atoms as a} from '#/alf'
import {ProfileHoverCard} from '#/components/ProfileHoverCard' import {ProfileHoverCard} from '#/components/ProfileHoverCard'
import {RichText} from '#/components/RichText' import {RichText} from '#/components/RichText'
@ -135,8 +135,8 @@ function PostInner({
}, [setLimitLines]) }, [setLimitLines])
const onBeforePress = React.useCallback(() => { const onBeforePress = React.useCallback(() => {
queryClient.setQueryData(RQKEY_URI(post.author.handle), post.author.did) precacheProfile(queryClient, post.author)
}, [queryClient, post.author.handle, post.author.did]) }, [queryClient, post.author])
return ( return (
<Link <Link
@ -148,9 +148,7 @@ function PostInner({
<View style={styles.layoutAvi}> <View style={styles.layoutAvi}>
<PreviewableUserAvatar <PreviewableUserAvatar
size={52} size={52}
did={post.author.did} profile={post.author}
handle={post.author.handle}
avatar={post.author.avatar}
moderation={moderation.ui('avatar')} moderation={moderation.ui('avatar')}
type={post.author.associated?.labeler ? 'labeler' : 'user'} type={post.author.associated?.labeler ? 'labeler' : 'user'}
/> />

View File

@ -13,6 +13,7 @@ import {
} from '@fortawesome/react-native-fontawesome' } from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow' import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow'
import {useComposerControls} from '#/state/shell/composer' import {useComposerControls} from '#/state/shell/composer'
@ -24,6 +25,7 @@ import {sanitizeDisplayName} from 'lib/strings/display-names'
import {sanitizeHandle} from 'lib/strings/handles' import {sanitizeHandle} from 'lib/strings/handles'
import {countLines} from 'lib/strings/helpers' import {countLines} from 'lib/strings/helpers'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import {precacheProfile} from 'state/queries/profile'
import {atoms as a} from '#/alf' import {atoms as a} from '#/alf'
import {ContentHider} from '#/components/moderation/ContentHider' import {ContentHider} from '#/components/moderation/ContentHider'
import {ProfileHoverCard} from '#/components/ProfileHoverCard' import {ProfileHoverCard} from '#/components/ProfileHoverCard'
@ -106,6 +108,7 @@ let FeedItemInner = ({
isThreadLastChild?: boolean isThreadLastChild?: boolean
isThreadParent?: boolean isThreadParent?: boolean
}): React.ReactNode => { }): React.ReactNode => {
const queryClient = useQueryClient()
const {openComposer} = useComposerControls() const {openComposer} = useComposerControls()
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui() const {_} = useLingui()
@ -135,6 +138,10 @@ let FeedItemInner = ({
}) })
}, [post, record, openComposer, moderation]) }, [post, record, openComposer, moderation])
const onBeforePress = React.useCallback(() => {
precacheProfile(queryClient, post.author)
}, [queryClient, post.author])
const outerStyles = [ const outerStyles = [
styles.outer, styles.outer,
{ {
@ -153,7 +160,8 @@ let FeedItemInner = ({
style={outerStyles} style={outerStyles}
href={href} href={href}
noFeedback noFeedback
accessible={false}> accessible={false}
onBeforePress={onBeforePress}>
<View style={{flexDirection: 'row', gap: 10, paddingLeft: 8}}> <View style={{flexDirection: 'row', gap: 10, paddingLeft: 8}}>
<View style={{width: 52}}> <View style={{width: 52}}>
{isThreadChild && ( {isThreadChild && (
@ -240,9 +248,7 @@ let FeedItemInner = ({
<View style={styles.layoutAvi}> <View style={styles.layoutAvi}>
<PreviewableUserAvatar <PreviewableUserAvatar
size={52} size={52}
did={post.author.did} profile={post.author}
handle={post.author.handle}
avatar={post.author.avatar}
moderation={moderation.ui('avatar')} moderation={moderation.ui('avatar')}
type={post.author.associated?.labeler ? 'labeler' : 'user'} type={post.author.associated?.labeler ? 'labeler' : 'user'}
/> />

View File

@ -20,8 +20,7 @@ import {makeProfileLink} from 'lib/routes/links'
import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeDisplayName} from 'lib/strings/display-names'
import {sanitizeHandle} from 'lib/strings/handles' import {sanitizeHandle} from 'lib/strings/handles'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import {profileBasicQueryKey as RQKEY_PROFILE_BASIC} from 'state/queries/profile' import {precacheProfile} from 'state/queries/profile'
import {RQKEY as RQKEY_URI} from 'state/queries/resolve-uri'
import {Link} from '../util/Link' import {Link} from '../util/Link'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import {PreviewableUserAvatar} from '../util/UserAvatar' import {PreviewableUserAvatar} from '../util/UserAvatar'
@ -58,9 +57,7 @@ export function ProfileCard({
const onBeforePress = React.useCallback(() => { const onBeforePress = React.useCallback(() => {
onPress?.() onPress?.()
precacheProfile(queryClient, profile)
queryClient.setQueryData(RQKEY_URI(profile.handle), profile.did)
queryClient.setQueryData(RQKEY_PROFILE_BASIC(profile.did), profile)
}, [onPress, profile, queryClient]) }, [onPress, profile, queryClient])
if (!moderationOpts) { if (!moderationOpts) {
@ -91,9 +88,7 @@ export function ProfileCard({
<View style={styles.layoutAvi}> <View style={styles.layoutAvi}>
<PreviewableUserAvatar <PreviewableUserAvatar
size={40} size={40}
did={profile.did} profile={profile}
handle={profile.handle}
avatar={profile.avatar}
moderation={moderation.ui('avatar')} moderation={moderation.ui('avatar')}
type={isLabeler ? 'labeler' : 'user'} type={isLabeler ? 'labeler' : 'user'}
/> />
@ -238,9 +233,7 @@ function FollowersList({
<View style={[styles.followedByAvi, pal.view]}> <View style={[styles.followedByAvi, pal.view]}>
<PreviewableUserAvatar <PreviewableUserAvatar
size={32} size={32}
did={f.did} profile={f}
handle={f.handle}
avatar={f.avatar}
moderation={mod.ui('avatar')} moderation={mod.ui('avatar')}
type={f.associated?.labeler ? 'labeler' : 'user'} type={f.associated?.labeler ? 'labeler' : 'user'}
/> />

View File

@ -220,8 +220,7 @@ function SuggestedFollow({
]}> ]}>
<PreviewableUserAvatar <PreviewableUserAvatar
size={60} size={60}
did={profile.did} profile={profile}
handle={profile.handle}
avatar={profile.avatar} avatar={profile.avatar}
moderation={moderation.ui('avatar')} moderation={moderation.ui('avatar')}
/> />

View File

@ -148,6 +148,7 @@ export const TextLink = memo(function TextLink({
dataSet, dataSet,
title, title,
onPress, onPress,
onBeforePress,
disableMismatchWarning, disableMismatchWarning,
navigationAction, navigationAction,
anchorNoUnderline, anchorNoUnderline,
@ -165,6 +166,7 @@ export const TextLink = memo(function TextLink({
disableMismatchWarning?: boolean disableMismatchWarning?: boolean
navigationAction?: 'push' | 'replace' | 'navigate' navigationAction?: 'push' | 'replace' | 'navigate'
anchorNoUnderline?: boolean anchorNoUnderline?: boolean
onBeforePress?: () => void
} & TextProps) { } & TextProps) {
const {...props} = useLinkProps({to: sanitizeUrl(href)}) const {...props} = useLinkProps({to: sanitizeUrl(href)})
const navigation = useNavigationDeduped() const navigation = useNavigationDeduped()
@ -202,6 +204,7 @@ export const TextLink = memo(function TextLink({
// Let the browser handle opening in new tab etc. // Let the browser handle opening in new tab etc.
return return
} }
onBeforePress?.()
if (onPress) { if (onPress) {
e?.preventDefault?.() e?.preventDefault?.()
// @ts-ignore function signature differs by platform -prf // @ts-ignore function signature differs by platform -prf
@ -226,6 +229,7 @@ export const TextLink = memo(function TextLink({
disableMismatchWarning, disableMismatchWarning,
navigationAction, navigationAction,
openLink, openLink,
onBeforePress,
], ],
) )
const hrefAttrs = useMemo(() => { const hrefAttrs = useMemo(() => {
@ -274,6 +278,7 @@ interface TextLinkOnWebOnlyProps extends TextProps {
title?: string title?: string
navigationAction?: 'push' | 'replace' | 'navigate' navigationAction?: 'push' | 'replace' | 'navigate'
disableMismatchWarning?: boolean disableMismatchWarning?: boolean
onBeforePress?: () => void
onPointerEnter?: () => void onPointerEnter?: () => void
anchorNoUnderline?: boolean anchorNoUnderline?: boolean
} }
@ -287,6 +292,7 @@ export const TextLinkOnWebOnly = memo(function DesktopWebTextLink({
lineHeight, lineHeight,
navigationAction, navigationAction,
disableMismatchWarning, disableMismatchWarning,
onBeforePress,
...props ...props
}: TextLinkOnWebOnlyProps) { }: TextLinkOnWebOnlyProps) {
if (isWeb) { if (isWeb) {
@ -302,6 +308,7 @@ export const TextLinkOnWebOnly = memo(function DesktopWebTextLink({
title={props.title} title={props.title}
navigationAction={navigationAction} navigationAction={navigationAction}
disableMismatchWarning={disableMismatchWarning} disableMismatchWarning={disableMismatchWarning}
onBeforePress={onBeforePress}
{...props} {...props}
/> />
) )

View File

@ -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 {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native'
import {AppBskyActorDefs, ModerationDecision, ModerationUI} from '@atproto/api' 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 {usePalette} from 'lib/hooks/usePalette'
import {makeProfileLink} from 'lib/routes/links' import {makeProfileLink} from 'lib/routes/links'
import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeDisplayName} from 'lib/strings/display-names'
@ -40,15 +41,18 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
? () => prefetchProfileQuery(opts.author.did) ? () => prefetchProfileQuery(opts.author.did)
: undefined : undefined
const queryClient = useQueryClient()
const onBeforePress = useCallback(() => {
precacheProfile(queryClient, opts.author)
}, [queryClient, opts.author])
return ( return (
<View style={[styles.container, opts.style]}> <View style={[styles.container, opts.style]}>
{opts.showAvatar && ( {opts.showAvatar && (
<View style={styles.avatar}> <View style={styles.avatar}>
<PreviewableUserAvatar <PreviewableUserAvatar
size={opts.avatarSize || 16} size={opts.avatarSize || 16}
did={opts.author.did} profile={opts.author}
handle={opts.author.handle}
avatar={opts.author.avatar}
moderation={opts.avatarModeration} moderation={opts.avatarModeration}
type={opts.author.associated?.labeler ? 'labeler' : 'user'} type={opts.author.associated?.labeler ? 'labeler' : 'user'}
/> />
@ -71,6 +75,7 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
</> </>
} }
href={profileLink} href={profileLink}
onBeforePress={onBeforePress}
onPointerEnter={onPointerEnter} onPointerEnter={onPointerEnter}
/> />
<TextLinkOnWebOnly <TextLinkOnWebOnly
@ -79,6 +84,7 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
style={[pal.textLight, {flexShrink: 4}]} style={[pal.textLight, {flexShrink: 4}]}
text={'\xa0' + sanitizeHandle(handle, '@')} text={'\xa0' + sanitizeHandle(handle, '@')}
href={profileLink} href={profileLink}
onBeforePress={onBeforePress}
onPointerEnter={onPointerEnter} onPointerEnter={onPointerEnter}
anchorNoUnderline anchorNoUnderline
/> />
@ -103,6 +109,7 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
title={niceDate(opts.timestamp)} title={niceDate(opts.timestamp)}
accessibilityHint="" accessibilityHint=""
href={opts.postHref} href={opts.postHref}
onBeforePress={onBeforePress}
/> />
)} )}
</TimeElapsed> </TimeElapsed>

View File

@ -1,6 +1,7 @@
import React from 'react' import React from 'react'
import {ago} from 'lib/strings/time'
import {useTickEveryMinute} from '#/state/shell' import {useTickEveryMinute} from '#/state/shell'
import {ago} from 'lib/strings/time'
// FIXME(dan): Figure out why the false positives // FIXME(dan): Figure out why the false positives
@ -12,7 +13,7 @@ export function TimeElapsed({
children: ({timeElapsed}: {timeElapsed: string}) => JSX.Element children: ({timeElapsed}: {timeElapsed: string}) => JSX.Element
}) { }) {
const tick = useTickEveryMinute() const tick = useTickEveryMinute()
const [timeElapsed, setTimeAgo] = React.useState(ago(timestamp)) const [timeElapsed, setTimeAgo] = React.useState(() => ago(timestamp))
React.useEffect(() => { React.useEffect(() => {
setTimeAgo(ago(timestamp)) setTimeAgo(ago(timestamp))

View File

@ -2,10 +2,11 @@ import React, {memo, useMemo} from 'react'
import {Image, StyleSheet, TouchableOpacity, View} from 'react-native' import {Image, StyleSheet, TouchableOpacity, View} from 'react-native'
import {Image as RNImage} from 'react-native-image-crop-picker' import {Image as RNImage} from 'react-native-image-crop-picker'
import Svg, {Circle, Path, Rect} from 'react-native-svg' 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 {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import { import {
@ -15,6 +16,7 @@ import {
import {makeProfileLink} from 'lib/routes/links' import {makeProfileLink} from 'lib/routes/links'
import {colors} from 'lib/styles' import {colors} from 'lib/styles'
import {isAndroid, isNative, isWeb} from 'platform/detection' import {isAndroid, isNative, isWeb} from 'platform/detection'
import {precacheProfile} from 'state/queries/profile'
import {HighPriorityImage} from 'view/com/util/images/Image' import {HighPriorityImage} from 'view/com/util/images/Image'
import {tokens, useTheme} from '#/alf' import {tokens, useTheme} from '#/alf'
import { import {
@ -47,8 +49,7 @@ interface EditableUserAvatarProps extends BaseUserAvatarProps {
interface PreviewableUserAvatarProps extends BaseUserAvatarProps { interface PreviewableUserAvatarProps extends BaseUserAvatarProps {
moderation?: ModerationUI moderation?: ModerationUI
did: string profile: AppBskyActorDefs.ProfileViewBasic
handle: string
} }
const BLUR_AMOUNT = isWeb ? 5 : 100 const BLUR_AMOUNT = isWeb ? 5 : 100
@ -371,19 +372,28 @@ let EditableUserAvatar = ({
EditableUserAvatar = memo(EditableUserAvatar) EditableUserAvatar = memo(EditableUserAvatar)
export {EditableUserAvatar} export {EditableUserAvatar}
let PreviewableUserAvatar = ( let PreviewableUserAvatar = ({
props: PreviewableUserAvatarProps, moderation,
): React.ReactNode => { profile,
...rest
}: PreviewableUserAvatarProps): React.ReactNode => {
const {_} = useLingui() const {_} = useLingui()
const queryClient = useQueryClient()
const onPress = React.useCallback(() => {
precacheProfile(queryClient, profile)
}, [profile, queryClient])
return ( return (
<ProfileHoverCard did={props.did}> <ProfileHoverCard did={profile.did}>
<Link <Link
label={_(msg`See profile`)} label={_(msg`See profile`)}
to={makeProfileLink({ to={makeProfileLink({
did: props.did, did: profile.did,
handle: props.handle, handle: profile.handle,
})}> })}
<UserAvatar {...props} /> onPress={onPress}>
<UserAvatar avatar={profile.avatar} moderation={moderation} {...rest} />
</Link> </Link>
</ProfileHoverCard> </ProfileHoverCard>
) )

View File

@ -26,10 +26,10 @@ import {useQueryClient} from '@tanstack/react-query'
import {HITSLOP_20} from '#/lib/constants' import {HITSLOP_20} from '#/lib/constants'
import {s} from '#/lib/styles' import {s} from '#/lib/styles'
import {useModerationOpts} from '#/state/queries/preferences' import {useModerationOpts} from '#/state/queries/preferences'
import {RQKEY as RQKEY_URI} from '#/state/queries/resolve-uri'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {InfoCircleIcon} from 'lib/icons' import {InfoCircleIcon} from 'lib/icons'
import {makeProfileLink} from 'lib/routes/links' import {makeProfileLink} from 'lib/routes/links'
import {precacheProfile} from 'state/queries/profile'
import {ComposerOptsQuote} from 'state/shell/composer' import {ComposerOptsQuote} from 'state/shell/composer'
import {atoms as a} from '#/alf' import {atoms as a} from '#/alf'
import {RichText} from '#/components/RichText' import {RichText} from '#/components/RichText'
@ -149,8 +149,8 @@ export function QuoteEmbed({
}, [quote.embeds]) }, [quote.embeds])
const onBeforePress = React.useCallback(() => { const onBeforePress = React.useCallback(() => {
queryClient.setQueryData(RQKEY_URI(quote.author.handle), quote.author.did) precacheProfile(queryClient, quote.author)
}, [queryClient, quote.author.did, quote.author.handle]) }, [queryClient, quote.author])
return ( return (
<ContentHider modui={moderation?.ui('contentList')}> <ContentHider modui={moderation?.ui('contentList')}>