From a03f57c8c380097abeadfade91235f6f96c1e8ca Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Wed, 29 Nov 2023 20:11:01 -0800 Subject: [PATCH] Apply feed preferences (react-query refactor) (#2040) * Actually implement the feed tuners hook * Move feed-tuner pass into select() to have it apply immediately on change --- src/state/preferences/feed-tuners.tsx | 22 ++-- src/state/queries/post-feed.ts | 148 ++++++++++++++------------ src/state/queries/post-thread.ts | 17 ++- 3 files changed, 102 insertions(+), 85 deletions(-) diff --git a/src/state/preferences/feed-tuners.tsx b/src/state/preferences/feed-tuners.tsx index 96770055..c4954d20 100644 --- a/src/state/preferences/feed-tuners.tsx +++ b/src/state/preferences/feed-tuners.tsx @@ -2,9 +2,13 @@ import {useMemo} from 'react' import {FeedTuner} from '#/lib/api/feed-manip' import {FeedDescriptor} from '../queries/post-feed' import {useLanguagePrefs} from './languages' +import {usePreferencesQuery} from '../queries/preferences' +import {useSession} from '../session' export function useFeedTuners(feedDesc: FeedDescriptor) { const langPrefs = useLanguagePrefs() + const {data: preferences} = usePreferencesQuery() + const {currentAccount} = useSession() return useMemo(() => { if (feedDesc.startsWith('feedgen')) { @@ -19,30 +23,30 @@ export function useFeedTuners(feedDesc: FeedDescriptor) { if (feedDesc === 'home' || feedDesc === 'following') { const feedTuners = [] - if (false /*TODOthis.homeFeed.hideReposts*/) { + if (preferences?.feedViewPrefs.hideReposts) { feedTuners.push(FeedTuner.removeReposts) } else { feedTuners.push(FeedTuner.dedupReposts) } - if (true /*TODOthis.homeFeed.hideReplies*/) { + if (preferences?.feedViewPrefs.hideReplies) { feedTuners.push(FeedTuner.removeReplies) - } /* TODO else { + } else { feedTuners.push( FeedTuner.thresholdRepliesOnly({ - userDid: this.rootStore.session.data?.did || '', - minLikes: this.homeFeed.hideRepliesByLikeCount, - followedOnly: !!this.homeFeed.hideRepliesByUnfollowed, + userDid: currentAccount?.did || '', + minLikes: preferences?.feedViewPrefs.hideRepliesByLikeCount || 0, + followedOnly: !!preferences?.feedViewPrefs.hideRepliesByUnfollowed, }), ) - }*/ + } - if (false /*TODOthis.homeFeed.hideQuotePosts*/) { + if (preferences?.feedViewPrefs.hideQuotePosts) { feedTuners.push(FeedTuner.removeQuotePosts) } return feedTuners } return [] - }, [feedDesc, langPrefs]) + }, [feedDesc, currentAccount, preferences, langPrefs]) } diff --git a/src/state/queries/post-feed.ts b/src/state/queries/post-feed.ts index 2595e762..7cf315ef 100644 --- a/src/state/queries/post-feed.ts +++ b/src/state/queries/post-feed.ts @@ -43,9 +43,7 @@ export interface FeedParams { mergeFeedSources?: string[] } -type RQPageParam = - | {cursor: string | undefined; api: FeedAPI; tuner: FeedTuner | NoopFeedTuner} - | undefined +type RQPageParam = {cursor: string | undefined; api: FeedAPI} | undefined export function RQKEY(feedDesc: FeedDescriptor, params?: FeedParams) { return ['post-feed', feedDesc, params || {}] @@ -66,6 +64,12 @@ export interface FeedPostSlice { items: FeedPostSliceItem[] } +export interface FeedPageUnselected { + api: FeedAPI + cursor: string | undefined + feed: AppBskyFeedDefs.FeedViewPost[] +} + export interface FeedPage { api: FeedAPI tuner: FeedTuner | NoopFeedTuner @@ -83,30 +87,27 @@ export function usePostFeedQuery( const enabled = opts?.enabled !== false return useInfiniteQuery< - FeedPage, + FeedPageUnselected, Error, InfiniteData, QueryKey, RQPageParam >({ + enabled, staleTime: STALE.INFINITY, queryKey: RQKEY(feedDesc, params), async queryFn({pageParam}: {pageParam: RQPageParam}) { logger.debug('usePostFeedQuery', {feedDesc, pageParam}) - const {api, tuner, cursor} = pageParam + const {api, cursor} = pageParam ? pageParam : { api: createApi(feedDesc, params || {}, feedTuners), - tuner: params?.disableTuner - ? new NoopFeedTuner() - : new FeedTuner(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) /* * If this is a public view, we need to check if posts fail moderation. @@ -115,69 +116,60 @@ export function usePostFeedQuery( * some not. */ if (!getAgent().session) { - // assume false - let somePostsPassModeration = false - - for (const slice of slices) { - for (let i = 0; i < slice.items.length; i++) { - const item = slice.items[i] - 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) - } + assertSomePostsPassModeration(res.feed) } return { api, - tuner, 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, - } - } - return undefined - }) - .filter(Boolean) as FeedPostSliceItem[], - })), + feed: res.feed, } }, initialPageParam: undefined, getNextPageParam: lastPage => ({ api: lastPage.api, - tuner: lastPage.tuner, cursor: lastPage.cursor, }), - enabled, + 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[], + })), + })), + } + }, }) } @@ -235,8 +227,10 @@ function createApi( export function findPostInQueryData( queryClient: QueryClient, uri: string, -): FeedPostSliceItem | undefined { - const queryDatas = queryClient.getQueriesData>({ +): AppBskyFeedDefs.FeedViewPost | undefined { + const queryDatas = queryClient.getQueriesData< + InfiniteData + >({ queryKey: ['post-feed'], }) for (const [_queryKey, queryData] of queryDatas) { @@ -244,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) + } +} diff --git a/src/state/queries/post-thread.ts b/src/state/queries/post-thread.ts index c616b05c..4b586c86 100644 --- a/src/state/queries/post-thread.ts +++ b/src/state/queries/post-thread.ts @@ -8,10 +8,7 @@ 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 findPostInFeedQueryData} from './post-feed' import {findPostInQueryData as findPostInNotifsQueryData} from './notifications/feed' import {precacheThreadPosts as precacheResolvedUris} from './resolve-uri' @@ -93,7 +90,7 @@ export function usePostThreadQuery(uri: string | undefined) { { const item = findPostInFeedQueryData(queryClient, uri) if (item) { - return feedItemToPlaceholderThread(item) + return feedViewPostToPlaceholderThread(item) } } { @@ -275,13 +272,15 @@ function threadNodeToPlaceholderThread( } } -function feedItemToPlaceholderThread(item: FeedPostSliceItem): ThreadNode { +function feedViewPostToPlaceholderThread( + item: AppBskyFeedDefs.FeedViewPost, +): ThreadNode { return { type: 'post', _reactKey: item.post.uri, uri: item.post.uri, post: item.post, - record: item.record, + record: item.post.record as AppBskyFeedPost.Record, // validated in post-feed parent: undefined, replies: undefined, viewer: item.post.viewer, @@ -291,7 +290,7 @@ function feedItemToPlaceholderThread(item: FeedPostSliceItem): ThreadNode { hasMore: false, showChildReplyLine: false, showParentReplyLine: false, - isParentLoading: !!item.record.reply, + isParentLoading: !!(item.post.record as AppBskyFeedPost.Record).reply, isChildLoading: !!item.post.replyCount, }, } @@ -305,7 +304,7 @@ function postViewToPlaceholderThread( _reactKey: post.uri, uri: post.uri, post: post, - record: post.record as AppBskyFeedPost.Record, // validate in notifs + record: post.record as AppBskyFeedPost.Record, // validated in notifs parent: undefined, replies: undefined, viewer: post.viewer,