diff --git a/src/state/queries/notifications/feed.ts b/src/state/queries/notifications/feed.ts index 55e048308d..19a92fc3c0 100644 --- a/src/state/queries/notifications/feed.ts +++ b/src/state/queries/notifications/feed.ts @@ -17,7 +17,12 @@ */ import {useCallback, useEffect, useMemo, useRef} from 'react' -import {AppBskyActorDefs, AppBskyFeedDefs, AtUri} from '@atproto/api' +import { + AppBskyActorDefs, + AppBskyFeedDefs, + AppBskyFeedPost, + AtUri, +} from '@atproto/api' import { InfiniteData, QueryClient, @@ -26,6 +31,7 @@ import { useQueryClient, } from '@tanstack/react-query' +import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped' import {useAgent} from '#/state/session' import {useThreadgateHiddenReplyUris} from '#/state/threadgate-hidden-replies' import {useModerationOpts} from '../../preferences/moderation-opts' @@ -67,9 +73,15 @@ export function useNotificationFeedQuery(opts?: { const selectArgs = useMemo(() => { return { + moderationOpts, hiddenReplyUris, } - }, [hiddenReplyUris]) + }, [moderationOpts, hiddenReplyUris]) + const lastRun = useRef<{ + data: InfiniteData + args: typeof selectArgs + result: InfiniteData + } | null>(null) const query = useInfiniteQuery< FeedPage, @@ -111,7 +123,38 @@ export function useNotificationFeedQuery(opts?: { enabled, select: useCallback( (data: InfiniteData) => { - const {hiddenReplyUris} = selectArgs + const {moderationOpts, hiddenReplyUris} = selectArgs + + // Keep track of the last run and whether we can reuse + // some already selected pages from there. + let reusedPages = [] + if (lastRun.current) { + const { + data: lastData, + args: lastArgs, + result: lastResult, + } = lastRun.current + let canReuse = true + for (let key in selectArgs) { + if (selectArgs.hasOwnProperty(key)) { + if ((selectArgs as any)[key] !== (lastArgs as any)[key]) { + // Can't do reuse anything if any input has changed. + canReuse = false + break + } + } + } + if (canReuse) { + for (let i = 0; i < data.pages.length; i++) { + if (data.pages[i] && lastData.pages[i] === data.pages[i]) { + reusedPages.push(lastResult.pages[i]) + continue + } + // Stop as soon as pages stop matching up. + break + } + } + } // override 'isRead' using the first page's returned seenAt // we do this because the `markAllRead()` call above will @@ -124,23 +167,49 @@ export function useNotificationFeedQuery(opts?: { } } - data = { + const result = { ...data, - pages: data.pages.map(page => { - return { - ...page, - items: page.items.filter(item => { - const isHiddenReply = - item.type === 'reply' && - item.subjectUri && - hiddenReplyUris.has(item.subjectUri) - return !isHiddenReply - }), - } - }), + pages: [ + ...reusedPages, + ...data.pages.slice(reusedPages.length).map(page => { + return { + ...page, + items: page.items + .filter(item => { + const isHiddenReply = + item.type === 'reply' && + item.subjectUri && + hiddenReplyUris.has(item.subjectUri) + return !isHiddenReply + }) + .filter(item => { + if ( + item.type === 'reply' || + item.type === 'mention' || + item.type === 'quote' + ) { + /* + * The `isPostView` check will fail here bc we don't have + * a `$type` field on the `subject`. But if the nested + * `record` is a post, we know it's a post view. + */ + if (AppBskyFeedPost.isRecord(item.subject?.record)) { + const mod = moderatePost(item.subject, moderationOpts!) + if (mod.ui('contentList').filter) { + return false + } + } + } + return true + }), + } + }), + ], } - return data + lastRun.current = {data, result, args: selectArgs} + + return result }, [selectArgs], ), diff --git a/src/state/queries/search-posts.ts b/src/state/queries/search-posts.ts index 5c50ad2671..8a8a3fa52f 100644 --- a/src/state/queries/search-posts.ts +++ b/src/state/queries/search-posts.ts @@ -1,3 +1,4 @@ +import React from 'react' import { AppBskyActorDefs, AppBskyFeedDefs, @@ -11,6 +12,8 @@ import { useInfiniteQuery, } from '@tanstack/react-query' +import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped' +import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useAgent} from '#/state/session' import { didOrHandleUriMatches, @@ -35,6 +38,20 @@ export function useSearchPostsQuery({ enabled?: boolean }) { const agent = useAgent() + const moderationOpts = useModerationOpts() + const selectArgs = React.useMemo( + () => ({ + isSearchingSpecificUser: /from:(\w+)/.test(query), + moderationOpts, + }), + [query, moderationOpts], + ) + const lastRun = React.useRef<{ + data: InfiniteData + args: typeof selectArgs + result: InfiniteData + } | null>(null) + return useInfiniteQuery< AppBskyFeedSearchPosts.OutputSchema, Error, @@ -54,7 +71,73 @@ export function useSearchPostsQuery({ }, initialPageParam: undefined, getNextPageParam: lastPage => lastPage.cursor, - enabled, + enabled: enabled ?? !!moderationOpts, + select: React.useCallback( + (data: InfiniteData) => { + const {moderationOpts, isSearchingSpecificUser} = selectArgs + + /* + * If a user applies the `from:` filter, don't apply any + * moderation. Note that if we add any more filtering logic below, we + * may need to adjust this. + */ + if (isSearchingSpecificUser) { + return data + } + + // Keep track of the last run and whether we can reuse + // some already selected pages from there. + let reusedPages = [] + if (lastRun.current) { + const { + data: lastData, + args: lastArgs, + result: lastResult, + } = lastRun.current + let canReuse = true + for (let key in selectArgs) { + if (selectArgs.hasOwnProperty(key)) { + if ((selectArgs as any)[key] !== (lastArgs as any)[key]) { + // Can't do reuse anything if any input has changed. + canReuse = false + break + } + } + } + if (canReuse) { + for (let i = 0; i < data.pages.length; i++) { + if (data.pages[i] && lastData.pages[i] === data.pages[i]) { + reusedPages.push(lastResult.pages[i]) + continue + } + // Stop as soon as pages stop matching up. + break + } + } + } + + const result = { + ...data, + pages: [ + ...reusedPages, + ...data.pages.slice(reusedPages.length).map(page => { + return { + ...page, + posts: page.posts.filter(post => { + const mod = moderatePost(post, moderationOpts!) + return !mod.ui('contentList').filter + }), + } + }), + ], + } + + lastRun.current = {data, result, args: selectArgs} + + return result + }, + [selectArgs], + ), }) }