diff options
author | Paul Frazee <pfrazee@gmail.com> | 2023-11-10 15:34:25 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-11-10 15:34:25 -0800 |
commit | c8c308e31e63607280648e3e9f1f56a371adcd05 (patch) | |
tree | 09cea4c603968a1a0b4cab299af9a417880c8115 /src/state/queries | |
parent | 51f04b96200e38d95e486628d3cbc43398c47980 (diff) | |
download | voidsky-c8c308e31e63607280648e3e9f1f56a371adcd05.tar.zst |
Refactor feeds to use react-query (#1862)
* Update to react-query v5 * Introduce post-feed react query * Add feed refresh behaviors * Only fetch feeds of visible pages * Implement polling for latest on feeds * Add moderation filtering to slices * Handle block errors * Update feed error messages * Remove old models * Replace simple-feed option with disable-tuner option * Add missing useMemo * Implement the mergefeed and fixes to polling * Correctly handle failed load more state * Improve error and empty state behaviors * Clearer naming
Diffstat (limited to 'src/state/queries')
-rw-r--r-- | src/state/queries/post-feed.ts | 176 | ||||
-rw-r--r-- | src/state/queries/post-thread.ts | 10 | ||||
-rw-r--r-- | src/state/queries/post.ts | 98 | ||||
-rw-r--r-- | src/state/queries/resolve-uri.ts | 17 |
4 files changed, 237 insertions, 64 deletions
diff --git a/src/state/queries/post-feed.ts b/src/state/queries/post-feed.ts new file mode 100644 index 000000000..1a391d5c3 --- /dev/null +++ b/src/state/queries/post-feed.ts @@ -0,0 +1,176 @@ +import {useCallback, useMemo} from 'react' +import {AppBskyFeedDefs, AppBskyFeedPost, moderatePost} from '@atproto/api' +import {useInfiniteQuery, InfiniteData, QueryKey} from '@tanstack/react-query' +import {useSession} from '../session' +import {useFeedTuners} from '../preferences/feed-tuners' +import {FeedTuner, NoopFeedTuner} from 'lib/api/feed-manip' +import {FeedAPI, ReasonFeedSource} from 'lib/api/feed/types' +import {FollowingFeedAPI} from 'lib/api/feed/following' +import {AuthorFeedAPI} from 'lib/api/feed/author' +import {LikesFeedAPI} from 'lib/api/feed/likes' +import {CustomFeedAPI} from 'lib/api/feed/custom' +import {ListFeedAPI} from 'lib/api/feed/list' +import {MergeFeedAPI} from 'lib/api/feed/merge' +import {useStores} from '../models/root-store' + +type ActorDid = string +type AuthorFilter = + | 'posts_with_replies' + | 'posts_no_replies' + | 'posts_with_media' +type FeedUri = string +type ListUri = string +export type FeedDescriptor = + | 'home' + | 'following' + | `author|${ActorDid}|${AuthorFilter}` + | `feedgen|${FeedUri}` + | `likes|${ActorDid}` + | `list|${ListUri}` +export interface FeedParams { + disableTuner?: boolean + mergeFeedEnabled?: boolean + mergeFeedSources?: string[] +} + +type RQPageParam = string | undefined + +export function RQKEY(feedDesc: FeedDescriptor, params?: FeedParams) { + return ['post-feed', feedDesc, params || {}] +} + +export interface FeedPostSliceItem { + _reactKey: string + uri: string + post: AppBskyFeedDefs.PostView + record: AppBskyFeedPost.Record + reason?: AppBskyFeedDefs.ReasonRepost | ReasonFeedSource +} + +export interface FeedPostSlice { + _reactKey: string + rootUri: string + isThread: boolean + items: FeedPostSliceItem[] +} + +export interface FeedPage { + cursor: string | undefined + slices: FeedPostSlice[] +} + +export function usePostFeedQuery( + feedDesc: FeedDescriptor, + params?: FeedParams, + opts?: {enabled?: boolean}, +) { + const {agent} = useSession() + const feedTuners = useFeedTuners(feedDesc) + const store = useStores() + const enabled = opts?.enabled !== false + + const api: FeedAPI = useMemo(() => { + if (feedDesc === 'home') { + return new MergeFeedAPI(agent, params || {}, feedTuners) + } else if (feedDesc === 'following') { + return new FollowingFeedAPI(agent) + } else if (feedDesc.startsWith('author')) { + const [_, actor, filter] = feedDesc.split('|') + return new AuthorFeedAPI(agent, {actor, filter}) + } else if (feedDesc.startsWith('likes')) { + const [_, actor] = feedDesc.split('|') + return new LikesFeedAPI(agent, {actor}) + } else if (feedDesc.startsWith('feedgen')) { + const [_, feed] = feedDesc.split('|') + return new CustomFeedAPI(agent, {feed}) + } else if (feedDesc.startsWith('list')) { + const [_, list] = feedDesc.split('|') + return new ListFeedAPI(agent, {list}) + } else { + // shouldnt happen + return new FollowingFeedAPI(agent) + } + }, [feedDesc, params, feedTuners, agent]) + const tuner = useMemo( + () => (params?.disableTuner ? new NoopFeedTuner() : new FeedTuner()), + [params], + ) + + const pollLatest = useCallback(async () => { + if (!enabled) { + return false + } + console.log('poll') + const post = await api.peekLatest() + if (post) { + const slices = tuner.tune([post], feedTuners, { + dryRun: true, + maintainOrder: true, + }) + if (slices[0]) { + if ( + !moderatePost( + slices[0].items[0].post, + store.preferences.moderationOpts, + ).content.filter + ) { + return true + } + } + } + return false + }, [api, tuner, feedTuners, store.preferences.moderationOpts, enabled]) + + const out = useInfiniteQuery< + FeedPage, + Error, + InfiniteData<FeedPage>, + QueryKey, + RQPageParam + >({ + queryKey: RQKEY(feedDesc, params), + async queryFn({pageParam}: {pageParam: RQPageParam}) { + console.log('fetch', feedDesc, pageParam) + if (!pageParam) { + tuner.reset() + } + const res = await api.fetch({cursor: pageParam, limit: 30}) + const slices = tuner.tune(res.feed, feedTuners) + return { + cursor: res.cursor, + slices: slices.map(slice => ({ + _reactKey: slice._reactKey, + rootUri: slice.rootItem.post.uri, + isThread: + slice.items.length > 1 && + slice.items.every( + item => item.post.author.did === slice.items[0].post.author.did, + ), + source: undefined, // TODO + items: slice.items + .map((item, i) => { + if ( + AppBskyFeedPost.isRecord(item.post.record) && + AppBskyFeedPost.validateRecord(item.post.record).success + ) { + return { + _reactKey: `${slice._reactKey}-${i}`, + uri: item.post.uri, + post: item.post, + record: item.post.record, + reason: i === 0 && slice.source ? slice.source : item.reason, + } + } + return undefined + }) + .filter(Boolean) as FeedPostSliceItem[], + })), + } + }, + initialPageParam: undefined, + getNextPageParam: lastPage => lastPage.cursor, + enabled, + }) + + return {...out, pollLatest} +} diff --git a/src/state/queries/post-thread.ts b/src/state/queries/post-thread.ts index 4dea8aaf1..386c70483 100644 --- a/src/state/queries/post-thread.ts +++ b/src/state/queries/post-thread.ts @@ -57,17 +57,17 @@ export type ThreadNode = export function usePostThreadQuery(uri: string | undefined) { const {agent} = useSession() - return useQuery<ThreadNode, Error>( - RQKEY(uri || ''), - async () => { + return useQuery<ThreadNode, Error>({ + queryKey: RQKEY(uri || ''), + async queryFn() { const res = await agent.getPostThread({uri: uri!}) if (res.success) { return responseToThreadNodes(res.data.thread) } return {type: 'unknown', uri: uri!} }, - {enabled: !!uri}, - ) + enabled: !!uri, + }) } export function sortThread( diff --git a/src/state/queries/post.ts b/src/state/queries/post.ts index f62190c67..ffff7f967 100644 --- a/src/state/queries/post.ts +++ b/src/state/queries/post.ts @@ -7,9 +7,9 @@ export const RQKEY = (postUri: string) => ['post', postUri] export function usePostQuery(uri: string | undefined) { const {agent} = useSession() - return useQuery<AppBskyFeedDefs.PostView>( - RQKEY(uri || ''), - async () => { + return useQuery<AppBskyFeedDefs.PostView>({ + queryKey: RQKEY(uri || ''), + async queryFn() { const res = await agent.getPosts({uris: [uri!]}) if (res.success && res.data.posts[0]) { return res.data.posts[0] @@ -17,10 +17,8 @@ export function usePostQuery(uri: string | undefined) { throw new Error('No data') }, - { - enabled: !!uri, - }, - ) + enabled: !!uri, + }) } export function usePostLikeMutation() { @@ -29,7 +27,8 @@ export function usePostLikeMutation() { {uri: string}, // responds with the uri of the like Error, {uri: string; cid: string; likeCount: number} // the post's uri, cid, and likes - >(post => agent.like(post.uri, post.cid), { + >({ + mutationFn: post => agent.like(post.uri, post.cid), onMutate(variables) { // optimistically update the post-shadow updatePostShadow(variables.uri, { @@ -59,27 +58,25 @@ export function usePostUnlikeMutation() { void, Error, {postUri: string; likeUri: string; likeCount: number} - >( - async ({likeUri}) => { + >({ + mutationFn: async ({likeUri}) => { await agent.deleteLike(likeUri) }, - { - onMutate(variables) { - // optimistically update the post-shadow - updatePostShadow(variables.postUri, { - likeCount: variables.likeCount - 1, - likeUri: undefined, - }) - }, - onError(error, variables) { - // revert the optimistic update - updatePostShadow(variables.postUri, { - likeCount: variables.likeCount, - likeUri: variables.likeUri, - }) - }, + onMutate(variables) { + // optimistically update the post-shadow + updatePostShadow(variables.postUri, { + likeCount: variables.likeCount - 1, + likeUri: undefined, + }) + }, + onError(error, variables) { + // revert the optimistic update + updatePostShadow(variables.postUri, { + likeCount: variables.likeCount, + likeUri: variables.likeUri, + }) }, - ) + }) } export function usePostRepostMutation() { @@ -88,7 +85,8 @@ export function usePostRepostMutation() { {uri: string}, // responds with the uri of the repost Error, {uri: string; cid: string; repostCount: number} // the post's uri, cid, and reposts - >(post => agent.repost(post.uri, post.cid), { + >({ + mutationFn: post => agent.repost(post.uri, post.cid), onMutate(variables) { // optimistically update the post-shadow updatePostShadow(variables.uri, { @@ -118,39 +116,35 @@ export function usePostUnrepostMutation() { void, Error, {postUri: string; repostUri: string; repostCount: number} - >( - async ({repostUri}) => { + >({ + mutationFn: async ({repostUri}) => { await agent.deleteRepost(repostUri) }, - { - onMutate(variables) { - // optimistically update the post-shadow - updatePostShadow(variables.postUri, { - repostCount: variables.repostCount - 1, - repostUri: undefined, - }) - }, - onError(error, variables) { - // revert the optimistic update - updatePostShadow(variables.postUri, { - repostCount: variables.repostCount, - repostUri: variables.repostUri, - }) - }, + onMutate(variables) { + // optimistically update the post-shadow + updatePostShadow(variables.postUri, { + repostCount: variables.repostCount - 1, + repostUri: undefined, + }) + }, + onError(error, variables) { + // revert the optimistic update + updatePostShadow(variables.postUri, { + repostCount: variables.repostCount, + repostUri: variables.repostUri, + }) }, - ) + }) } export function usePostDeleteMutation() { const {agent} = useSession() - return useMutation<void, Error, {uri: string}>( - async ({uri}) => { + return useMutation<void, Error, {uri: string}>({ + mutationFn: async ({uri}) => { await agent.deletePost(uri) }, - { - onSuccess(data, variables) { - updatePostShadow(variables.uri, {isDeleted: true}) - }, + onSuccess(data, variables) { + updatePostShadow(variables.uri, {isDeleted: true}) }, - ) + }) } diff --git a/src/state/queries/resolve-uri.ts b/src/state/queries/resolve-uri.ts index 770be5cf8..26e0a475b 100644 --- a/src/state/queries/resolve-uri.ts +++ b/src/state/queries/resolve-uri.ts @@ -6,12 +6,15 @@ export const RQKEY = (uri: string) => ['resolved-uri', uri] export function useResolveUriQuery(uri: string) { const {agent} = useSession() - return useQuery<string | undefined, Error>(RQKEY(uri), async () => { - const urip = new AtUri(uri) - if (!urip.host.startsWith('did:')) { - const res = await agent.resolveHandle({handle: urip.host}) - urip.host = res.data.did - } - return urip.toString() + return useQuery<string | undefined, Error>({ + queryKey: RQKEY(uri), + async queryFn() { + const urip = new AtUri(uri) + if (!urip.host.startsWith('did:')) { + const res = await agent.resolveHandle({handle: urip.host}) + urip.host = res.data.did + } + return urip.toString() + }, }) } |