diff options
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} +} |