diff options
Diffstat (limited to 'src/state')
-rw-r--r-- | src/state/queries/notifications/feed.ts | 39 | ||||
-rw-r--r-- | src/state/queries/post-feed.ts | 40 | ||||
-rw-r--r-- | src/state/queries/post-thread.ts | 146 | ||||
-rw-r--r-- | src/state/queries/resolve-uri.ts | 77 |
4 files changed, 283 insertions, 19 deletions
diff --git a/src/state/queries/notifications/feed.ts b/src/state/queries/notifications/feed.ts index d78370e07..54bd87540 100644 --- a/src/state/queries/notifications/feed.ts +++ b/src/state/queries/notifications/feed.ts @@ -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[] { diff --git a/src/state/queries/post-feed.ts b/src/state/queries/post-feed.ts index 5f81cb44d..1334461cf 100644 --- a/src/state/queries/post-feed.ts +++ b/src/state/queries/post-feed.ts @@ -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 +} diff --git a/src/state/queries/post-thread.ts b/src/state/queries/post-thread.ts index b4a474eab..c616b05cc 100644 --- a/src/state/queries/post-thread.ts +++ b/src/state/queries/post-thread.ts @@ -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, + }, + } +} diff --git a/src/state/queries/resolve-uri.ts b/src/state/queries/resolve-uri.ts index dc8e7fbe1..05a9f4b1c 100644 --- a/src/state/queries/resolve-uri.ts +++ b/src/state/queries/resolve-uri.ts @@ -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>({ +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 useQuery<string, Error>({ staleTime: STALE.INFINITY, - queryKey: RQKEY(uri || ''), + queryKey: RQKEY(didOrHandle || ''), 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 + if (!didOrHandle) { + return '' } - return {did: urip.host, uri: urip.toString()} + if (!didOrHandle.startsWith('did:')) { + const res = await getAgent().resolveHandle({handle: didOrHandle}) + didOrHandle = res.data.did + } + return didOrHandle }, - enabled: !!uri, + enabled: !!didOrHandle, }) } -export function useResolveDidQuery(didOrHandle: string | undefined) { - return useResolveUriQuery(didOrHandle ? `at://${didOrHandle}/` : undefined) +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) + } + } + } } |