Precache basic profile from posts for instant future navigations (#2795)
* skeleton for caching * modify some existing logic * refactor uri resolution query * add precache feed posts * adjustments * remove prefetch on hover (maybe revert, just example) * fix * change arg name to match what we want * optional infinite stale time * use `ProfileViewDetailed` * Revert "remove prefetch on hover (maybe revert, just example)" This reverts commit 08609deb0defa7cea040438bc37dd3488ddc56f4. * add warning comment back for stale time * remove comment * store profile with both the handle and did for query key * remove extra block from revert * clarify argument name * remove QT cache * structure queries the same (put `enabled` at bottom) * use both `ProfileViewDetailed` and `ProfileView` for the query return type * placeholder profile header * remove logs * remove a few other things we don't need * add placeholder * refactor * refactor * we don't need this height adjustment now * use gray banner while loading * set background color of image to the loading placeholder color * reorg imports * add border to header on loading * Fix style * Rm radius * oops * Undo edit * Back out type changes * Tighten some types and moderate shadow * Move precaching fns to profile where the cache is * Rename functions to match what they do now * Remove anys --------- Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
This commit is contained in:
parent
d9b62955b5
commit
de28626001
8 changed files with 170 additions and 85 deletions
|
@ -12,7 +12,7 @@ import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
|
|||
import chunk from 'lodash.chunk'
|
||||
import {QueryClient} from '@tanstack/react-query'
|
||||
import {getAgent} from '../../session'
|
||||
import {precacheProfile as precacheResolvedUri} from '../resolve-uri'
|
||||
import {precacheProfile} from '../profile'
|
||||
import {NotificationType, FeedNotification, FeedPage} from './types'
|
||||
|
||||
const GROUPABLE_REASONS = ['like', 'repost', 'follow']
|
||||
|
@ -59,7 +59,7 @@ export async function fetchPage({
|
|||
if (notif.subjectUri) {
|
||||
notif.subject = subjects.get(notif.subjectUri)
|
||||
if (notif.subject) {
|
||||
precacheResolvedUri(queryClient, notif.subject.author) // precache the handle->did resolution
|
||||
precacheProfile(queryClient, notif.subject.author)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ import {MergeFeedAPI} from 'lib/api/feed/merge'
|
|||
import {HomeFeedAPI} from '#/lib/api/feed/home'
|
||||
import {logger} from '#/logger'
|
||||
import {STALE} from '#/state/queries'
|
||||
import {precacheFeedPosts as precacheResolvedUris} from './resolve-uri'
|
||||
import {precacheFeedPostProfiles} from './profile'
|
||||
import {getAgent} from '#/state/session'
|
||||
import {DEFAULT_LOGGED_OUT_PREFERENCES} from '#/state/queries/preferences/const'
|
||||
import {getModerationOpts} from '#/state/queries/preferences/moderation'
|
||||
|
@ -138,7 +138,7 @@ export function usePostFeedQuery(
|
|||
}
|
||||
|
||||
const res = await api.fetch({cursor, limit: PAGE_SIZE})
|
||||
precacheResolvedUris(queryClient, res.feed) // precache the handle->did resolution
|
||||
precacheFeedPostProfiles(queryClient, res.feed)
|
||||
|
||||
/*
|
||||
* If this is a public view, we need to check if posts fail moderation.
|
||||
|
|
|
@ -10,7 +10,7 @@ import {getAgent} from '#/state/session'
|
|||
import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
|
||||
import {findPostInQueryData as findPostInFeedQueryData} from './post-feed'
|
||||
import {findPostInQueryData as findPostInNotifsQueryData} from './notifications/feed'
|
||||
import {precacheThreadPosts as precacheResolvedUris} from './resolve-uri'
|
||||
import {precacheThreadPostProfiles} from './profile'
|
||||
import {getEmbeddedPost} from './util'
|
||||
|
||||
export const RQKEY = (uri: string) => ['post-thread', uri]
|
||||
|
@ -71,7 +71,7 @@ export function usePostThreadQuery(uri: string | undefined) {
|
|||
const res = await getAgent().getPostThread({uri: uri!})
|
||||
if (res.success) {
|
||||
const nodes = responseToThreadNodes(res.data.thread)
|
||||
precacheResolvedUris(queryClient, nodes) // precache the handle->did resolution
|
||||
precacheThreadPostProfiles(queryClient, nodes)
|
||||
return nodes
|
||||
}
|
||||
return {type: 'unknown', uri: uri!}
|
||||
|
|
|
@ -4,6 +4,9 @@ import {
|
|||
AppBskyActorDefs,
|
||||
AppBskyActorProfile,
|
||||
AppBskyActorGetProfile,
|
||||
AppBskyFeedDefs,
|
||||
AppBskyEmbedRecord,
|
||||
AppBskyEmbedRecordWithMedia,
|
||||
} from '@atproto/api'
|
||||
import {
|
||||
useQuery,
|
||||
|
@ -23,9 +26,14 @@ import {RQKEY as RQKEY_MY_MUTED} from './my-muted-accounts'
|
|||
import {RQKEY as RQKEY_MY_BLOCKED} from './my-blocked-accounts'
|
||||
import {STALE} from '#/state/queries'
|
||||
import {track} from '#/lib/analytics/analytics'
|
||||
import {ThreadNode} from './post-thread'
|
||||
|
||||
export const RQKEY = (did: string) => ['profile', did]
|
||||
export const profilesQueryKey = (handles: string[]) => ['profiles', handles]
|
||||
export const profileBasicQueryKey = (didOrHandle: string) => [
|
||||
'profileBasic',
|
||||
didOrHandle,
|
||||
]
|
||||
|
||||
export function useProfileQuery({
|
||||
did,
|
||||
|
@ -34,18 +42,26 @@ export function useProfileQuery({
|
|||
did: string | undefined
|
||||
staleTime?: number
|
||||
}) {
|
||||
return useQuery({
|
||||
const queryClient = useQueryClient()
|
||||
return useQuery<AppBskyActorDefs.ProfileViewDetailed>({
|
||||
// WARNING
|
||||
// this staleTime is load-bearing
|
||||
// if you remove it, the UI infinite-loops
|
||||
// -prf
|
||||
staleTime,
|
||||
refetchOnWindowFocus: true,
|
||||
queryKey: RQKEY(did || ''),
|
||||
queryKey: RQKEY(did ?? ''),
|
||||
queryFn: async () => {
|
||||
const res = await getAgent().getProfile({actor: did || ''})
|
||||
const res = await getAgent().getProfile({actor: did ?? ''})
|
||||
return res.data
|
||||
},
|
||||
placeholderData: () => {
|
||||
if (!did) return
|
||||
|
||||
return queryClient.getQueryData<AppBskyActorDefs.ProfileViewBasic>(
|
||||
profileBasicQueryKey(did),
|
||||
)
|
||||
},
|
||||
enabled: !!did,
|
||||
})
|
||||
}
|
||||
|
@ -405,6 +421,64 @@ function useProfileUnblockMutation() {
|
|||
})
|
||||
}
|
||||
|
||||
export function precacheProfile(
|
||||
queryClient: QueryClient,
|
||||
profile: AppBskyActorDefs.ProfileViewBasic,
|
||||
) {
|
||||
queryClient.setQueryData(profileBasicQueryKey(profile.handle), 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(
|
||||
actor: string,
|
||||
fn: (res: AppBskyActorGetProfile.Response) => boolean,
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import {QueryClient, useQuery, UseQueryResult} from '@tanstack/react-query'
|
||||
import {AtUri, AppBskyActorDefs, AppBskyFeedDefs} from '@atproto/api'
|
||||
import {useQuery, useQueryClient, UseQueryResult} from '@tanstack/react-query'
|
||||
import {AtUri, AppBskyActorDefs} from '@atproto/api'
|
||||
|
||||
import {profileBasicQueryKey as RQKEY_PROFILE_BASIC} from './profile'
|
||||
import {getAgent} from '#/state/session'
|
||||
import {STALE} from '#/state/queries'
|
||||
import {ThreadNode} from './post-thread'
|
||||
|
||||
export const RQKEY = (didOrHandle: string) => ['resolved-did', didOrHandle]
|
||||
|
||||
|
@ -22,55 +22,29 @@ export function useResolveUriQuery(uri: string | undefined): UriUseQueryResult {
|
|||
}
|
||||
|
||||
export function useResolveDidQuery(didOrHandle: string | undefined) {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useQuery<string, Error>({
|
||||
staleTime: STALE.HOURS.ONE,
|
||||
queryKey: RQKEY(didOrHandle || ''),
|
||||
async queryFn() {
|
||||
if (!didOrHandle) {
|
||||
return ''
|
||||
}
|
||||
if (!didOrHandle.startsWith('did:')) {
|
||||
const res = await getAgent().resolveHandle({handle: didOrHandle})
|
||||
didOrHandle = res.data.did
|
||||
}
|
||||
return didOrHandle
|
||||
queryKey: RQKEY(didOrHandle ?? ''),
|
||||
queryFn: async () => {
|
||||
if (!didOrHandle) return ''
|
||||
// Just return the did if it's already one
|
||||
if (didOrHandle.startsWith('did:')) return didOrHandle
|
||||
|
||||
const res = await getAgent().resolveHandle({handle: didOrHandle})
|
||||
return res.data.did
|
||||
},
|
||||
initialData: () => {
|
||||
// Return undefined if no did or handle
|
||||
if (!didOrHandle) return
|
||||
|
||||
const profile =
|
||||
queryClient.getQueryData<AppBskyActorDefs.ProfileViewBasic>(
|
||||
RQKEY_PROFILE_BASIC(didOrHandle),
|
||||
)
|
||||
return profile?.did
|
||||
},
|
||||
enabled: !!didOrHandle,
|
||||
})
|
||||
}
|
||||
|
||||
export function precacheProfile(
|
||||
queryClient: QueryClient,
|
||||
profile:
|
||||
| AppBskyActorDefs.ProfileView
|
||||
| AppBskyActorDefs.ProfileViewBasic
|
||||
| AppBskyActorDefs.ProfileViewDetailed,
|
||||
) {
|
||||
queryClient.setQueryData(RQKEY(profile.handle), profile.did)
|
||||
}
|
||||
|
||||
export function precacheFeedPosts(
|
||||
queryClient: QueryClient,
|
||||
posts: AppBskyFeedDefs.FeedViewPost[],
|
||||
) {
|
||||
for (const post of posts) {
|
||||
precacheProfile(queryClient, post.post.author)
|
||||
}
|
||||
}
|
||||
|
||||
export function precacheThreadPosts(
|
||||
queryClient: QueryClient,
|
||||
node: ThreadNode,
|
||||
) {
|
||||
if (node.type === 'post') {
|
||||
precacheProfile(queryClient, node.post.author)
|
||||
if (node.parent) {
|
||||
precacheThreadPosts(queryClient, node.parent)
|
||||
}
|
||||
if (node.replies?.length) {
|
||||
for (const reply of node.replies) {
|
||||
precacheThreadPosts(queryClient, reply)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue