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
zio/stable
Paul Frazee 2023-11-29 20:11:01 -08:00 committed by GitHub
parent 3e1b2346ee
commit a03f57c8c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 102 additions and 85 deletions

View File

@ -2,9 +2,13 @@ import {useMemo} from 'react'
import {FeedTuner} from '#/lib/api/feed-manip' import {FeedTuner} from '#/lib/api/feed-manip'
import {FeedDescriptor} from '../queries/post-feed' import {FeedDescriptor} from '../queries/post-feed'
import {useLanguagePrefs} from './languages' import {useLanguagePrefs} from './languages'
import {usePreferencesQuery} from '../queries/preferences'
import {useSession} from '../session'
export function useFeedTuners(feedDesc: FeedDescriptor) { export function useFeedTuners(feedDesc: FeedDescriptor) {
const langPrefs = useLanguagePrefs() const langPrefs = useLanguagePrefs()
const {data: preferences} = usePreferencesQuery()
const {currentAccount} = useSession()
return useMemo(() => { return useMemo(() => {
if (feedDesc.startsWith('feedgen')) { if (feedDesc.startsWith('feedgen')) {
@ -19,30 +23,30 @@ export function useFeedTuners(feedDesc: FeedDescriptor) {
if (feedDesc === 'home' || feedDesc === 'following') { if (feedDesc === 'home' || feedDesc === 'following') {
const feedTuners = [] const feedTuners = []
if (false /*TODOthis.homeFeed.hideReposts*/) { if (preferences?.feedViewPrefs.hideReposts) {
feedTuners.push(FeedTuner.removeReposts) feedTuners.push(FeedTuner.removeReposts)
} else { } else {
feedTuners.push(FeedTuner.dedupReposts) feedTuners.push(FeedTuner.dedupReposts)
} }
if (true /*TODOthis.homeFeed.hideReplies*/) { if (preferences?.feedViewPrefs.hideReplies) {
feedTuners.push(FeedTuner.removeReplies) feedTuners.push(FeedTuner.removeReplies)
} /* TODO else { } else {
feedTuners.push( feedTuners.push(
FeedTuner.thresholdRepliesOnly({ FeedTuner.thresholdRepliesOnly({
userDid: this.rootStore.session.data?.did || '', userDid: currentAccount?.did || '',
minLikes: this.homeFeed.hideRepliesByLikeCount, minLikes: preferences?.feedViewPrefs.hideRepliesByLikeCount || 0,
followedOnly: !!this.homeFeed.hideRepliesByUnfollowed, followedOnly: !!preferences?.feedViewPrefs.hideRepliesByUnfollowed,
}), }),
) )
}*/ }
if (false /*TODOthis.homeFeed.hideQuotePosts*/) { if (preferences?.feedViewPrefs.hideQuotePosts) {
feedTuners.push(FeedTuner.removeQuotePosts) feedTuners.push(FeedTuner.removeQuotePosts)
} }
return feedTuners return feedTuners
} }
return [] return []
}, [feedDesc, langPrefs]) }, [feedDesc, currentAccount, preferences, langPrefs])
} }

View File

@ -43,9 +43,7 @@ export interface FeedParams {
mergeFeedSources?: string[] mergeFeedSources?: string[]
} }
type RQPageParam = type RQPageParam = {cursor: string | undefined; api: FeedAPI} | undefined
| {cursor: string | undefined; api: FeedAPI; tuner: FeedTuner | NoopFeedTuner}
| undefined
export function RQKEY(feedDesc: FeedDescriptor, params?: FeedParams) { export function RQKEY(feedDesc: FeedDescriptor, params?: FeedParams) {
return ['post-feed', feedDesc, params || {}] return ['post-feed', feedDesc, params || {}]
@ -66,6 +64,12 @@ export interface FeedPostSlice {
items: FeedPostSliceItem[] items: FeedPostSliceItem[]
} }
export interface FeedPageUnselected {
api: FeedAPI
cursor: string | undefined
feed: AppBskyFeedDefs.FeedViewPost[]
}
export interface FeedPage { export interface FeedPage {
api: FeedAPI api: FeedAPI
tuner: FeedTuner | NoopFeedTuner tuner: FeedTuner | NoopFeedTuner
@ -83,30 +87,27 @@ export function usePostFeedQuery(
const enabled = opts?.enabled !== false const enabled = opts?.enabled !== false
return useInfiniteQuery< return useInfiniteQuery<
FeedPage, FeedPageUnselected,
Error, Error,
InfiniteData<FeedPage>, InfiniteData<FeedPage>,
QueryKey, QueryKey,
RQPageParam RQPageParam
>({ >({
enabled,
staleTime: STALE.INFINITY, staleTime: STALE.INFINITY,
queryKey: RQKEY(feedDesc, params), queryKey: RQKEY(feedDesc, params),
async queryFn({pageParam}: {pageParam: RQPageParam}) { async queryFn({pageParam}: {pageParam: RQPageParam}) {
logger.debug('usePostFeedQuery', {feedDesc, pageParam}) logger.debug('usePostFeedQuery', {feedDesc, pageParam})
const {api, tuner, cursor} = pageParam const {api, cursor} = pageParam
? pageParam ? pageParam
: { : {
api: createApi(feedDesc, params || {}, feedTuners), api: createApi(feedDesc, params || {}, feedTuners),
tuner: params?.disableTuner
? new NoopFeedTuner()
: new FeedTuner(feedTuners),
cursor: undefined, cursor: undefined,
} }
const res = await api.fetch({cursor, limit: 30}) const res = await api.fetch({cursor, limit: 30})
precacheResolvedUris(queryClient, res.feed) // precache the handle->did resolution 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. * If this is a public view, we need to check if posts fail moderation.
@ -115,69 +116,60 @@ export function usePostFeedQuery(
* some not. * some not.
*/ */
if (!getAgent().session) { if (!getAgent().session) {
// assume false assertSomePostsPassModeration(res.feed)
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)
}
} }
return { return {
api, api,
tuner,
cursor: res.cursor, cursor: res.cursor,
slices: slices.map(slice => ({ feed: res.feed,
_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[],
})),
} }
}, },
initialPageParam: undefined, initialPageParam: undefined,
getNextPageParam: lastPage => ({ getNextPageParam: lastPage => ({
api: lastPage.api, api: lastPage.api,
tuner: lastPage.tuner,
cursor: lastPage.cursor, 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( export function findPostInQueryData(
queryClient: QueryClient, queryClient: QueryClient,
uri: string, uri: string,
): FeedPostSliceItem | undefined { ): AppBskyFeedDefs.FeedViewPost | undefined {
const queryDatas = queryClient.getQueriesData<InfiniteData<FeedPage>>({ const queryDatas = queryClient.getQueriesData<
InfiniteData<FeedPageUnselected>
>({
queryKey: ['post-feed'], queryKey: ['post-feed'],
}) })
for (const [_queryKey, queryData] of queryDatas) { for (const [_queryKey, queryData] of queryDatas) {
@ -244,14 +238,34 @@ export function findPostInQueryData(
continue continue
} }
for (const page of queryData?.pages) { for (const page of queryData?.pages) {
for (const slice of page.slices) { for (const item of page.feed) {
for (const item of slice.items) { if (item.post.uri === uri) {
if (item.uri === uri) { return item
return item
}
} }
} }
} }
} }
return undefined 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)
}
}

View File

@ -8,10 +8,7 @@ import {useQuery, useQueryClient, QueryClient} from '@tanstack/react-query'
import {getAgent} from '#/state/session' import {getAgent} from '#/state/session'
import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types' import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
import {STALE} from '#/state/queries' import {STALE} from '#/state/queries'
import { import {findPostInQueryData as findPostInFeedQueryData} from './post-feed'
findPostInQueryData as findPostInFeedQueryData,
FeedPostSliceItem,
} from './post-feed'
import {findPostInQueryData as findPostInNotifsQueryData} from './notifications/feed' import {findPostInQueryData as findPostInNotifsQueryData} from './notifications/feed'
import {precacheThreadPosts as precacheResolvedUris} from './resolve-uri' import {precacheThreadPosts as precacheResolvedUris} from './resolve-uri'
@ -93,7 +90,7 @@ export function usePostThreadQuery(uri: string | undefined) {
{ {
const item = findPostInFeedQueryData(queryClient, uri) const item = findPostInFeedQueryData(queryClient, uri)
if (item) { 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 { return {
type: 'post', type: 'post',
_reactKey: item.post.uri, _reactKey: item.post.uri,
uri: item.post.uri, uri: item.post.uri,
post: item.post, post: item.post,
record: item.record, record: item.post.record as AppBskyFeedPost.Record, // validated in post-feed
parent: undefined, parent: undefined,
replies: undefined, replies: undefined,
viewer: item.post.viewer, viewer: item.post.viewer,
@ -291,7 +290,7 @@ function feedItemToPlaceholderThread(item: FeedPostSliceItem): ThreadNode {
hasMore: false, hasMore: false,
showChildReplyLine: false, showChildReplyLine: false,
showParentReplyLine: false, showParentReplyLine: false,
isParentLoading: !!item.record.reply, isParentLoading: !!(item.post.record as AppBskyFeedPost.Record).reply,
isChildLoading: !!item.post.replyCount, isChildLoading: !!item.post.replyCount,
}, },
} }
@ -305,7 +304,7 @@ function postViewToPlaceholderThread(
_reactKey: post.uri, _reactKey: post.uri,
uri: post.uri, uri: post.uri,
post: post, post: post,
record: post.record as AppBskyFeedPost.Record, // validate in notifs record: post.record as AppBskyFeedPost.Record, // validated in notifs
parent: undefined, parent: undefined,
replies: undefined, replies: undefined,
viewer: post.viewer, viewer: post.viewer,