Restore post-thread caching behaviors (react-query refactor) (#2010)
* Rework resolve-did and resolve-uri queries to be smarter about cache reuse * Precache handle resolutions * Remove old unused code * Load placeholder threads from the post-feed and notifications-feed queries * Remove logs * Fix bad ref * Add loading spinners to the cache-loading thread view * Scroll replies into view when loading threads * Add caching within a thread * Fix: dont show bottom border when the child spinner is active
This commit is contained in:
parent
d4714baf13
commit
f580d4daf0
8 changed files with 369 additions and 111 deletions
|
@ -7,11 +7,18 @@ import {
|
|||
BskyAgent,
|
||||
} from '@atproto/api'
|
||||
import chunk from 'lodash.chunk'
|
||||
import {useInfiniteQuery, InfiniteData, QueryKey} from '@tanstack/react-query'
|
||||
import {
|
||||
useInfiniteQuery,
|
||||
InfiniteData,
|
||||
QueryKey,
|
||||
useQueryClient,
|
||||
QueryClient,
|
||||
} from '@tanstack/react-query'
|
||||
import {getAgent} from '../../session'
|
||||
import {useModerationOpts} from '../preferences'
|
||||
import {shouldFilterNotif} from './util'
|
||||
import {useMutedThreads} from '#/state/muted-threads'
|
||||
import {precacheProfile as precacheResolvedUri} from '../resolve-uri'
|
||||
|
||||
const GROUPABLE_REASONS = ['like', 'repost', 'follow']
|
||||
const PAGE_SIZE = 30
|
||||
|
@ -48,6 +55,7 @@ export interface FeedPage {
|
|||
}
|
||||
|
||||
export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
|
||||
const queryClient = useQueryClient()
|
||||
const moderationOpts = useModerationOpts()
|
||||
const threadMutes = useMutedThreads()
|
||||
const enabled = opts?.enabled !== false
|
||||
|
@ -80,6 +88,9 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
|
|||
for (const notif of notifsGrouped) {
|
||||
if (notif.subjectUri) {
|
||||
notif.subject = subjects.get(notif.subjectUri)
|
||||
if (notif.subject) {
|
||||
precacheResolvedUri(queryClient, notif.subject.author) // precache the handle->did resolution
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -99,6 +110,32 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
|
|||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* This helper is used by the post-thread placeholder function to
|
||||
* find a post in the query-data cache
|
||||
*/
|
||||
export function findPostInQueryData(
|
||||
queryClient: QueryClient,
|
||||
uri: string,
|
||||
): AppBskyFeedDefs.PostView | undefined {
|
||||
const queryDatas = queryClient.getQueriesData<InfiniteData<FeedPage>>({
|
||||
queryKey: ['notification-feed'],
|
||||
})
|
||||
for (const [_queryKey, queryData] of queryDatas) {
|
||||
if (!queryData?.pages) {
|
||||
continue
|
||||
}
|
||||
for (const page of queryData?.pages) {
|
||||
for (const item of page.items) {
|
||||
if (item.subject?.uri === uri) {
|
||||
return item.subject
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function groupNotifications(
|
||||
notifs: AppBskyNotificationListNotifications.Notification[],
|
||||
): FeedNotification[] {
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
import {useCallback, useMemo} from 'react'
|
||||
import {AppBskyFeedDefs, AppBskyFeedPost, moderatePost} from '@atproto/api'
|
||||
import {useInfiniteQuery, InfiniteData, QueryKey} from '@tanstack/react-query'
|
||||
import {
|
||||
useInfiniteQuery,
|
||||
InfiniteData,
|
||||
QueryKey,
|
||||
QueryClient,
|
||||
useQueryClient,
|
||||
} from '@tanstack/react-query'
|
||||
import {getAgent} from '../session'
|
||||
import {useFeedTuners} from '../preferences/feed-tuners'
|
||||
import {FeedTuner, NoopFeedTuner} from 'lib/api/feed-manip'
|
||||
|
@ -14,6 +20,7 @@ import {MergeFeedAPI} from 'lib/api/feed/merge'
|
|||
import {useModerationOpts} from '#/state/queries/preferences'
|
||||
import {logger} from '#/logger'
|
||||
import {STALE} from '#/state/queries'
|
||||
import {precacheFeedPosts as precacheResolvedUris} from './resolve-uri'
|
||||
|
||||
type ActorDid = string
|
||||
type AuthorFilter =
|
||||
|
@ -66,6 +73,7 @@ export function usePostFeedQuery(
|
|||
params?: FeedParams,
|
||||
opts?: {enabled?: boolean},
|
||||
) {
|
||||
const queryClient = useQueryClient()
|
||||
const feedTuners = useFeedTuners(feedDesc)
|
||||
const enabled = opts?.enabled !== false
|
||||
const moderationOpts = useModerationOpts()
|
||||
|
@ -141,6 +149,7 @@ export function usePostFeedQuery(
|
|||
tuner.reset()
|
||||
}
|
||||
const res = await api.fetch({cursor: pageParam, limit: 30})
|
||||
precacheResolvedUris(queryClient, res.feed) // precache the handle->did resolution
|
||||
const slices = tuner.tune(res.feed, feedTuners)
|
||||
return {
|
||||
cursor: res.cursor,
|
||||
|
@ -152,7 +161,6 @@ export function usePostFeedQuery(
|
|||
slice.items.every(
|
||||
item => item.post.author.did === slice.items[0].post.author.did,
|
||||
),
|
||||
source: undefined, // TODO
|
||||
items: slice.items
|
||||
.map((item, i) => {
|
||||
if (
|
||||
|
@ -180,3 +188,31 @@ export function usePostFeedQuery(
|
|||
|
||||
return {...out, pollLatest}
|
||||
}
|
||||
|
||||
/**
|
||||
* This helper is used by the post-thread placeholder function to
|
||||
* find a post in the query-data cache
|
||||
*/
|
||||
export function findPostInQueryData(
|
||||
queryClient: QueryClient,
|
||||
uri: string,
|
||||
): FeedPostSliceItem | undefined {
|
||||
const queryDatas = queryClient.getQueriesData<InfiniteData<FeedPage>>({
|
||||
queryKey: ['post-feed'],
|
||||
})
|
||||
for (const [_queryKey, queryData] of queryDatas) {
|
||||
if (!queryData?.pages) {
|
||||
continue
|
||||
}
|
||||
for (const page of queryData?.pages) {
|
||||
for (const slice of page.slices) {
|
||||
for (const item of slice.items) {
|
||||
if (item.uri === uri) {
|
||||
return item
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
|
|
@ -3,11 +3,17 @@ import {
|
|||
AppBskyFeedPost,
|
||||
AppBskyFeedGetPostThread,
|
||||
} from '@atproto/api'
|
||||
import {useQuery} from '@tanstack/react-query'
|
||||
import {useQuery, useQueryClient, QueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {getAgent} from '#/state/session'
|
||||
import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
|
||||
import {STALE} from '#/state/queries'
|
||||
import {
|
||||
findPostInQueryData as findPostInFeedQueryData,
|
||||
FeedPostSliceItem,
|
||||
} from './post-feed'
|
||||
import {findPostInQueryData as findPostInNotifsQueryData} from './notifications/feed'
|
||||
import {precacheThreadPosts as precacheResolvedUris} from './resolve-uri'
|
||||
|
||||
export const RQKEY = (uri: string) => ['post-thread', uri]
|
||||
type ThreadViewNode = AppBskyFeedGetPostThread.OutputSchema['thread']
|
||||
|
@ -18,6 +24,8 @@ export interface ThreadCtx {
|
|||
hasMore?: boolean
|
||||
showChildReplyLine?: boolean
|
||||
showParentReplyLine?: boolean
|
||||
isParentLoading?: boolean
|
||||
isChildLoading?: boolean
|
||||
}
|
||||
|
||||
export type ThreadPost = {
|
||||
|
@ -58,17 +66,44 @@ export type ThreadNode =
|
|||
| ThreadUnknown
|
||||
|
||||
export function usePostThreadQuery(uri: string | undefined) {
|
||||
const queryClient = useQueryClient()
|
||||
return useQuery<ThreadNode, Error>({
|
||||
staleTime: STALE.MINUTES.ONE,
|
||||
queryKey: RQKEY(uri || ''),
|
||||
async queryFn() {
|
||||
const res = await getAgent().getPostThread({uri: uri!})
|
||||
if (res.success) {
|
||||
return responseToThreadNodes(res.data.thread)
|
||||
const nodes = responseToThreadNodes(res.data.thread)
|
||||
precacheResolvedUris(queryClient, nodes) // precache the handle->did resolution
|
||||
return nodes
|
||||
}
|
||||
return {type: 'unknown', uri: uri!}
|
||||
},
|
||||
enabled: !!uri,
|
||||
placeholderData: () => {
|
||||
if (!uri) {
|
||||
return undefined
|
||||
}
|
||||
{
|
||||
const item = findPostInQueryData(queryClient, uri)
|
||||
if (item) {
|
||||
return threadNodeToPlaceholderThread(item)
|
||||
}
|
||||
}
|
||||
{
|
||||
const item = findPostInFeedQueryData(queryClient, uri)
|
||||
if (item) {
|
||||
return feedItemToPlaceholderThread(item)
|
||||
}
|
||||
}
|
||||
{
|
||||
const item = findPostInNotifsQueryData(queryClient, uri)
|
||||
if (item) {
|
||||
return postViewToPlaceholderThread(item)
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -178,3 +213,110 @@ function responseToThreadNodes(
|
|||
return {type: 'unknown', uri: ''}
|
||||
}
|
||||
}
|
||||
|
||||
function findPostInQueryData(
|
||||
queryClient: QueryClient,
|
||||
uri: string,
|
||||
): ThreadNode | undefined {
|
||||
const queryDatas = queryClient.getQueriesData<ThreadNode>({
|
||||
queryKey: ['post-thread'],
|
||||
})
|
||||
for (const [_queryKey, queryData] of queryDatas) {
|
||||
if (!queryData) {
|
||||
continue
|
||||
}
|
||||
for (const item of traverseThread(queryData)) {
|
||||
if (item.uri === uri) {
|
||||
return item
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function* traverseThread(node: ThreadNode): Generator<ThreadNode, void> {
|
||||
if (node.type === 'post') {
|
||||
if (node.parent) {
|
||||
yield* traverseThread(node.parent)
|
||||
}
|
||||
yield node
|
||||
if (node.replies?.length) {
|
||||
for (const reply of node.replies) {
|
||||
yield* traverseThread(reply)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function threadNodeToPlaceholderThread(
|
||||
node: ThreadNode,
|
||||
): ThreadNode | undefined {
|
||||
if (node.type !== 'post') {
|
||||
return undefined
|
||||
}
|
||||
return {
|
||||
type: node.type,
|
||||
_reactKey: node._reactKey,
|
||||
uri: node.uri,
|
||||
post: node.post,
|
||||
record: node.record,
|
||||
parent: undefined,
|
||||
replies: undefined,
|
||||
viewer: node.viewer,
|
||||
ctx: {
|
||||
depth: 0,
|
||||
isHighlightedPost: true,
|
||||
hasMore: false,
|
||||
showChildReplyLine: false,
|
||||
showParentReplyLine: false,
|
||||
isParentLoading: !!node.record.reply,
|
||||
isChildLoading: !!node.post.replyCount,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function feedItemToPlaceholderThread(item: FeedPostSliceItem): ThreadNode {
|
||||
return {
|
||||
type: 'post',
|
||||
_reactKey: item.post.uri,
|
||||
uri: item.post.uri,
|
||||
post: item.post,
|
||||
record: item.record,
|
||||
parent: undefined,
|
||||
replies: undefined,
|
||||
viewer: item.post.viewer,
|
||||
ctx: {
|
||||
depth: 0,
|
||||
isHighlightedPost: true,
|
||||
hasMore: false,
|
||||
showChildReplyLine: false,
|
||||
showParentReplyLine: false,
|
||||
isParentLoading: !!item.record.reply,
|
||||
isChildLoading: !!item.post.replyCount,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function postViewToPlaceholderThread(
|
||||
post: AppBskyFeedDefs.PostView,
|
||||
): ThreadNode {
|
||||
return {
|
||||
type: 'post',
|
||||
_reactKey: post.uri,
|
||||
uri: post.uri,
|
||||
post: post,
|
||||
record: post.record as AppBskyFeedPost.Record, // validate in notifs
|
||||
parent: undefined,
|
||||
replies: undefined,
|
||||
viewer: post.viewer,
|
||||
ctx: {
|
||||
depth: 0,
|
||||
isHighlightedPost: true,
|
||||
hasMore: false,
|
||||
showChildReplyLine: false,
|
||||
showParentReplyLine: false,
|
||||
isParentLoading: !!(post.record as AppBskyFeedPost.Record).reply,
|
||||
isChildLoading: !!post.replyCount,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,27 +1,76 @@
|
|||
import {useQuery} from '@tanstack/react-query'
|
||||
import {AtUri} from '@atproto/api'
|
||||
import {QueryClient, useQuery, UseQueryResult} from '@tanstack/react-query'
|
||||
import {AtUri, AppBskyActorDefs, AppBskyFeedDefs} from '@atproto/api'
|
||||
|
||||
import {getAgent} from '#/state/session'
|
||||
import {STALE} from '#/state/queries'
|
||||
import {ThreadNode} from './post-thread'
|
||||
|
||||
export const RQKEY = (uri: string) => ['resolved-uri', uri]
|
||||
export const RQKEY = (didOrHandle: string) => ['resolved-did', didOrHandle]
|
||||
|
||||
export function useResolveUriQuery(uri: string | undefined) {
|
||||
return useQuery<{uri: string; did: string}, Error>({
|
||||
staleTime: STALE.INFINITY,
|
||||
queryKey: RQKEY(uri || ''),
|
||||
async queryFn() {
|
||||
const urip = new AtUri(uri || '')
|
||||
if (!urip.host.startsWith('did:')) {
|
||||
const res = await getAgent().resolveHandle({handle: urip.host})
|
||||
urip.host = res.data.did
|
||||
}
|
||||
return {did: urip.host, uri: urip.toString()}
|
||||
},
|
||||
enabled: !!uri,
|
||||
})
|
||||
type UriUseQueryResult = UseQueryResult<{did: string; uri: string}, Error>
|
||||
export function useResolveUriQuery(uri: string | undefined): UriUseQueryResult {
|
||||
const urip = new AtUri(uri || '')
|
||||
const res = useResolveDidQuery(urip.host)
|
||||
if (res.data) {
|
||||
urip.host = res.data
|
||||
return {
|
||||
...res,
|
||||
data: {did: urip.host, uri: urip.toString()},
|
||||
} as UriUseQueryResult
|
||||
}
|
||||
return res as UriUseQueryResult
|
||||
}
|
||||
|
||||
export function useResolveDidQuery(didOrHandle: string | undefined) {
|
||||
return useResolveUriQuery(didOrHandle ? `at://${didOrHandle}/` : undefined)
|
||||
return useQuery<string, Error>({
|
||||
staleTime: STALE.INFINITY,
|
||||
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
|
||||
},
|
||||
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