diff options
Diffstat (limited to 'src/state/queries/post-feed.ts')
-rw-r--r-- | src/state/queries/post-feed.ts | 261 |
1 files changed, 157 insertions, 104 deletions
diff --git a/src/state/queries/post-feed.ts b/src/state/queries/post-feed.ts index 113e6f2fb..7cf315ef6 100644 --- a/src/state/queries/post-feed.ts +++ b/src/state/queries/post-feed.ts @@ -1,4 +1,3 @@ -import {useCallback, useMemo} from 'react' import {AppBskyFeedDefs, AppBskyFeedPost, moderatePost} from '@atproto/api' import { useInfiniteQuery, @@ -7,9 +6,8 @@ import { QueryClient, useQueryClient, } from '@tanstack/react-query' -import {getAgent} from '../session' import {useFeedTuners} from '../preferences/feed-tuners' -import {FeedTuner, NoopFeedTuner} from 'lib/api/feed-manip' +import {FeedTuner, FeedTunerFn, 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' @@ -17,10 +15,13 @@ 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 {useModerationOpts} from '#/state/queries/preferences' import {logger} from '#/logger' import {STALE} from '#/state/queries' import {precacheFeedPosts as precacheResolvedUris} from './resolve-uri' +import {getAgent} from '#/state/session' +import {DEFAULT_LOGGED_OUT_PREFERENCES} from '#/state/queries/preferences/const' +import {getModerationOpts} from '#/state/queries/preferences/moderation' +import {KnownError} from '#/view/com/posts/FeedErrorMessage' type ActorDid = string type AuthorFilter = @@ -42,7 +43,7 @@ export interface FeedParams { mergeFeedSources?: string[] } -type RQPageParam = string | undefined +type RQPageParam = {cursor: string | undefined; api: FeedAPI} | undefined export function RQKEY(feedDesc: FeedDescriptor, params?: FeedParams) { return ['post-feed', feedDesc, params || {}] @@ -63,7 +64,15 @@ export interface FeedPostSlice { items: FeedPostSliceItem[] } +export interface FeedPageUnselected { + api: FeedAPI + cursor: string | undefined + feed: AppBskyFeedDefs.FeedViewPost[] +} + export interface FeedPage { + api: FeedAPI + tuner: FeedTuner | NoopFeedTuner cursor: string | undefined slices: FeedPostSlice[] } @@ -76,117 +85,139 @@ export function usePostFeedQuery( const queryClient = useQueryClient() const feedTuners = useFeedTuners(feedDesc) const enabled = opts?.enabled !== false - const moderationOpts = useModerationOpts() - const agent = getAgent() - - 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 disableTuner = !!params?.disableTuner - const tuner = useMemo( - () => (disableTuner ? new NoopFeedTuner() : new FeedTuner()), - [disableTuner], - ) - - const pollLatest = useCallback(async () => { - if (!enabled) { - return false - } - - logger.debug('usePostFeedQuery: pollLatest') - - const post = await api.peekLatest() - - if (post && moderationOpts) { - const slices = tuner.tune([post], feedTuners, { - dryRun: true, - maintainOrder: true, - }) - if (slices[0]) { - if ( - !moderatePost(slices[0].items[0].post, moderationOpts).content.filter - ) { - return true - } - } - } - return false - }, [api, tuner, feedTuners, moderationOpts, enabled]) - - const out = useInfiniteQuery< - FeedPage, + return useInfiniteQuery< + FeedPageUnselected, Error, InfiniteData<FeedPage>, QueryKey, RQPageParam >({ + enabled, staleTime: STALE.INFINITY, queryKey: RQKEY(feedDesc, params), async queryFn({pageParam}: {pageParam: RQPageParam}) { logger.debug('usePostFeedQuery', {feedDesc, pageParam}) - if (!pageParam) { - tuner.reset() - } - const res = await api.fetch({cursor: pageParam, limit: 30}) + + const {api, cursor} = pageParam + ? pageParam + : { + api: createApi(feedDesc, params || {}, feedTuners), + cursor: undefined, + } + + const res = await api.fetch({cursor, limit: 30}) precacheResolvedUris(queryClient, res.feed) // precache the handle->did resolution - const slices = tuner.tune(res.feed, feedTuners) + + /* + * If this is a public view, we need to check if posts fail moderation. + * If all fail, we throw an error. If only some fail, we continue and let + * moderations happen later, which results in some posts being shown and + * some not. + */ + if (!getAgent().session) { + assertSomePostsPassModeration(res.feed) + } + return { + api, 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, - ), - 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, + feed: res.feed, + } + }, + initialPageParam: undefined, + getNextPageParam: lastPage => ({ + api: lastPage.api, + cursor: lastPage.cursor, + }), + select(data) { + const tuner = params?.disableTuner + ? new NoopFeedTuner() + : new FeedTuner(feedTuners) + return { + pageParams: data.pageParams, + pages: data.pages.map(page => ({ + api: page.api, + tuner, + cursor: page.cursor, + slices: tuner.tune(page.feed).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, + ), + 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[], + return undefined + }) + .filter(Boolean) as FeedPostSliceItem[], + })), })), } }, - initialPageParam: undefined, - getNextPageParam: lastPage => lastPage.cursor, - enabled, }) +} + +export async function pollLatest(page: FeedPage | undefined) { + if (!page) { + return false + } - return {...out, pollLatest} + logger.debug('usePostFeedQuery: pollLatest') + const post = await page.api.peekLatest() + if (post) { + const slices = page.tuner.tune([post], { + dryRun: true, + maintainOrder: true, + }) + if (slices[0]) { + return true + } + } + + return false +} + +function createApi( + feedDesc: FeedDescriptor, + params: FeedParams, + feedTuners: FeedTunerFn[], +) { + if (feedDesc === 'home') { + return new MergeFeedAPI(params, feedTuners) + } else if (feedDesc === 'following') { + return new FollowingFeedAPI() + } else if (feedDesc.startsWith('author')) { + const [_, actor, filter] = feedDesc.split('|') + return new AuthorFeedAPI({actor, filter}) + } else if (feedDesc.startsWith('likes')) { + const [_, actor] = feedDesc.split('|') + return new LikesFeedAPI({actor}) + } else if (feedDesc.startsWith('feedgen')) { + const [_, feed] = feedDesc.split('|') + return new CustomFeedAPI({feed}) + } else if (feedDesc.startsWith('list')) { + const [_, list] = feedDesc.split('|') + return new ListFeedAPI({list}) + } else { + // shouldnt happen + return new FollowingFeedAPI() + } } /** @@ -196,8 +227,10 @@ export function usePostFeedQuery( export function findPostInQueryData( queryClient: QueryClient, uri: string, -): FeedPostSliceItem | undefined { - const queryDatas = queryClient.getQueriesData<InfiniteData<FeedPage>>({ +): AppBskyFeedDefs.FeedViewPost | undefined { + const queryDatas = queryClient.getQueriesData< + InfiniteData<FeedPageUnselected> + >({ queryKey: ['post-feed'], }) for (const [_queryKey, queryData] of queryDatas) { @@ -205,14 +238,34 @@ export function findPostInQueryData( 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 - } + for (const item of page.feed) { + if (item.post.uri === uri) { + return item } } } } return undefined } + +function assertSomePostsPassModeration(feed: AppBskyFeedDefs.FeedViewPost[]) { + // assume false + let somePostsPassModeration = false + + for (const item of feed) { + const moderationOpts = getModerationOpts({ + userDid: '', + preferences: DEFAULT_LOGGED_OUT_PREFERENCES, + }) + const moderation = moderatePost(item.post, moderationOpts) + + if (!moderation.content.filter) { + // we have a sfw post + somePostsPassModeration = true + } + } + + if (!somePostsPassModeration) { + throw new Error(KnownError.FeedNSFPublic) + } +} |