From 01486d27e057a49f5937a17fa4e6a2e14aab25b8 Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Mon, 17 Aug 2026 09:55:45 -0700 Subject: [PATCH] Add pagination to feed search (#11475) --- oxlint-suppressions.json | 5 --- src/screens/Search/SearchResults.tsx | 33 ++++++++++++++---- src/screens/StarterPack/Wizard/StepFeeds.tsx | 35 +++++++++++++++++--- src/state/queries/feed.ts | 23 +++++++++---- 4 files changed, 74 insertions(+), 22 deletions(-) diff --git a/oxlint-suppressions.json b/oxlint-suppressions.json index 1cb148b151..b259a81297 100644 --- a/oxlint-suppressions.json +++ b/oxlint-suppressions.json @@ -1175,11 +1175,6 @@ "count": 1 } }, - "src/screens/StarterPack/Wizard/StepFeeds.tsx": { - "typescript/no-misused-promises": { - "count": 1 - } - }, "src/screens/StarterPack/Wizard/index.tsx": { "typescript/no-floating-promises": { "count": 2 diff --git a/src/screens/Search/SearchResults.tsx b/src/screens/Search/SearchResults.tsx index 300c36fd65..b4099db3cd 100644 --- a/src/screens/Search/SearchResults.tsx +++ b/src/screens/Search/SearchResults.tsx @@ -648,26 +648,41 @@ let SearchScreenFeedsResults = ({ const ax = useAnalytics() const t = useTheme() - const {data: results, isFetched} = usePopularFeedsSearch({ + const { + data: results, + isFetched, + isFetching, + error, + fetchNextPage, + isFetchingNextPage, + hasNextPage, + } = usePopularFeedsSearch({ query, enabled: active, }) + const feeds = useMemo(() => { + return results?.pages.flatMap(page => page.feeds) || [] + }, [results]) + const onEndReached = useCallback(() => { + if (isFetching || !hasNextPage || error) return + void fetchNextPage() + }, [isFetching, error, hasNextPage, fetchNextPage]) const fireTracking = useCallOnce(() => { ax.metric('search:results:loaded', { tab: 'feeds', - initialCount: results?.length ?? 0, + initialCount: feeds.length, }) }) if (isFetched) { fireTracking() } - return isFetched && results ? ( + return isFetched ? ( <> - {results.length ? ( + {feeds.length || hasNextPage ? ( )} keyExtractor={(item: app.bsky.feed.defs.GeneratorView) => item.uri} + onEndReached={onEndReached} desktopFixedHeight - ListFooterComponent={} + ListFooterComponent={ + + } /> ) : ( } /> diff --git a/src/screens/StarterPack/Wizard/StepFeeds.tsx b/src/screens/StarterPack/Wizard/StepFeeds.tsx index 741c8622c6..c3650694fd 100644 --- a/src/screens/StarterPack/Wizard/StepFeeds.tsx +++ b/src/screens/StarterPack/Wizard/StepFeeds.tsx @@ -1,4 +1,4 @@ -import {useState} from 'react' +import {useMemo, useState} from 'react' import {type ListRenderItemInfo, View} from 'react-native' import {KeyboardAwareScrollView} from 'react-native-keyboard-controller' import {type ModerationOpts} from '@bsky/sdk/moderation' @@ -16,6 +16,7 @@ import {useWizardState} from '#/screens/StarterPack/Wizard/State' import {atoms as a, useTheme} from '#/alf' import {SearchInput} from '#/components/forms/SearchInput' import {useThrottledValue} from '#/components/hooks/useThrottledValue' +import {ListFooter} from '#/components/Lists' import {Loader} from '#/components/Loader' import {ScreenTransition} from '#/components/ScreenTransition' import {WizardFeedCard} from '#/components/StarterPack/Wizard/WizardListCard' @@ -59,8 +60,16 @@ export function StepFeeds({moderationOpts}: {moderationOpts: ModerationOpts}) { : savedFeeds : undefined - const {data: searchedFeeds, isFetching: isFetchingSearchedFeeds} = - usePopularFeedsSearch({query: throttledQuery}) + const { + data: searchedFeedsPages, + fetchNextPage: fetchNextSearchedFeedsPage, + hasNextPage: hasNextSearchedFeedsPage, + isFetching: isFetchingSearchedFeeds, + isFetchingNextPage: isFetchingNextSearchedFeedsPage, + } = usePopularFeedsSearch({query: throttledQuery}) + const searchedFeeds = useMemo(() => { + return searchedFeedsPages?.pages.flatMap(page => page.feeds) ?? [] + }, [searchedFeedsPages]) const isLoading = !isFetchedSavedFeeds || isLoadingPopularFeeds || isFetchingSearchedFeeds @@ -98,7 +107,17 @@ export function StepFeeds({moderationOpts}: {moderationOpts: ModerationOpts}) { renderItem={renderItem} keyExtractor={keyExtractor} onEndReached={ - !query && !screenReaderEnabled ? () => fetchNextPage() : undefined + !screenReaderEnabled + ? () => { + if (query) { + if (hasNextSearchedFeedsPage) { + void fetchNextSearchedFeedsPage() + } + } else { + void fetchNextPage() + } + } + : undefined } onEndReachedThreshold={2} keyboardDismissMode="on-drag" @@ -107,6 +126,14 @@ export function StepFeeds({moderationOpts}: {moderationOpts: ModerationOpts}) { disableFullWindowScroll={true} sideBorders={false} style={{flex: 1}} + ListFooterComponent={ + query ? ( + + ) : undefined + } ListEmptyComponent={ {isLoading ? ( diff --git a/src/state/queries/feed.ts b/src/state/queries/feed.ts index 850f9dbaa6..e3b99846fc 100644 --- a/src/state/queries/feed.ts +++ b/src/state/queries/feed.ts @@ -380,26 +380,35 @@ export function usePopularFeedsSearch({ const moderationOpts = useModerationOpts() const enabledInner = enabled ?? Boolean(moderationOpts) - return useQuery({ + return useInfiniteQuery({ enabled: enabledInner, queryKey: createPopularFeedsSearchQueryKey(query), - queryFn: async () => { + queryFn: async ({pageParam}) => { const data = await client.call( app.bsky.unspecced.getPopularFeedGenerators, { limit: 15, query: query, + cursor: pageParam, }, ) - return data.feeds + return data }, + initialPageParam: undefined as string | undefined, + getNextPageParam: lastPage => lastPage.cursor, placeholderData: keepPreviousData, select(data) { - return data.filter(feed => { - const decision = moderateFeedGenerator(feed, moderationOpts!) - return !decision.ui('contentMedia').blur - }) + return { + ...data, + pages: data.pages.map(page => ({ + ...page, + feeds: page.feeds.filter(feed => { + const decision = moderateFeedGenerator(feed, moderationOpts!) + return !decision.ui('contentMedia').blur + }), + })), + } }, }) }