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/post-feed.ts | |
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/post-feed.ts')
-rw-r--r-- | src/state/queries/post-feed.ts | 176 |
1 files changed, 176 insertions, 0 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} +} |