Search lists cleanup, pagninate actor search (#9009)

This commit is contained in:
Samuel Newman
2025-11-20 17:59:20 +02:00
committed by GitHub
parent 2792b4cef0
commit 2919a7da7f
7 changed files with 142 additions and 84 deletions
@@ -8,7 +8,7 @@ import {popularInterests, useInterestsDisplayNames} from '#/lib/interests'
import {logEvent} from '#/lib/statsig/statsig'
import {isWeb} from '#/platform/detection'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useActorSearchPaginated} from '#/state/queries/actor-search'
import {useActorSearch} from '#/state/queries/actor-search'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {useGetSuggestedUsersQuery} from '#/state/queries/trending/useGetSuggestedUsersQuery'
import {useSession} from '#/state/session'
@@ -128,7 +128,7 @@ function DialogInner({guide}: {guide: Follow10ProgressGuide}) {
isFetching: isFetchingSearchResults,
error: searchResultsError,
isError: isSearchResultsError,
} = useActorSearchPaginated({
} = useActorSearch({
enabled: !!searchText,
query: searchText,
})
+1
View File
@@ -204,6 +204,7 @@ export const urls = {
website: {
blog: {
initialVerificationAnnouncement: `https://bsky.social/about/blog/04-21-2025-verification`,
searchTipsAndTricks: 'https://bsky.social/about/blog/05-31-2024-search',
},
},
}
+2 -2
View File
@@ -17,7 +17,7 @@ import {logger} from '#/logger'
import {type MetricEvents} from '#/logger/metrics'
import {useLanguagePrefs} from '#/state/preferences/languages'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {RQKEY_ROOT_PAGINATED as useActorSearchPaginatedQueryKeyRoot} from '#/state/queries/actor-search'
import {RQKEY_ROOT as useActorSearchQueryKeyRoot} from '#/state/queries/actor-search'
import {
type FeedPreviewItem,
useFeedPreviews,
@@ -308,7 +308,7 @@ export function Explore({
queryKey: [getSuggestedUsersQueryKeyRoot],
}),
qc.resetQueries({
queryKey: [useActorSearchPaginatedQueryKeyRoot],
queryKey: [useActorSearchQueryKeyRoot],
}),
qc.resetQueries({
queryKey: createGetSuggestedFeedsQueryKey(),
+106 -24
View File
@@ -4,7 +4,8 @@ import {type AppBskyFeedDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {usePalette} from '#/lib/hooks/usePalette'
import {urls} from '#/lib/constants'
import {cleanError} from '#/lib/strings/errors'
import {augmentSearchQuery} from '#/lib/strings/helpers'
import {useActorSearch} from '#/state/queries/actor-search'
import {usePopularFeedsSearch} from '#/state/queries/feed'
@@ -21,6 +22,7 @@ import {atoms as a, useTheme, web} from '#/alf'
import * as FeedCard from '#/components/FeedCard'
import * as Layout from '#/components/Layout'
import {InlineLinkText} from '#/components/Link'
import {ListFooter} from '#/components/Lists'
import {SearchError} from '#/components/SearchError'
import {Text} from '#/components/Typography'
@@ -112,11 +114,11 @@ function Loader() {
}
function EmptyState({
message,
messageText,
error,
children,
}: {
message: string
messageText: React.ReactNode
error?: string
children?: React.ReactNode
}) {
@@ -126,7 +128,7 @@ function EmptyState({
<Layout.Content>
<View style={[a.p_xl]}>
<View style={[t.atoms.bg_contrast_25, a.rounded_sm, a.p_lg]}>
<Text style={[a.text_md]}>{message}</Text>
<Text style={[a.text_md]}>{messageText}</Text>
{error && (
<>
@@ -155,6 +157,41 @@ function EmptyState({
)
}
function NoResultsText({query}: {query: string}) {
const t = useTheme()
const {_} = useLingui()
return (
<>
<Text style={[a.text_lg, t.atoms.text_contrast_high]}>
<Trans>
No results found for "
<Text style={[a.text_lg, t.atoms.text, a.font_medium]}>{query}</Text>
".
</Trans>
</Text>
{'\n\n'}
<Text style={[a.text_md, a.leading_snug, t.atoms.text_contrast_high]}>
<Trans context="english-only-resource">
Try a different search term, or{' '}
<InlineLinkText
label={_(
msg({
message: 'read about how to use search filters',
context: 'english-only-resource',
}),
)}
to={urls.website.blog.searchTipsAndTricks}
style={[a.text_md, a.leading_snug]}>
read about how to use search filters
</InlineLinkText>
.
</Trans>
</Text>
</>
)
}
type SearchResultSlice =
| {
type: 'post'
@@ -176,9 +213,8 @@ let SearchScreenPostResults = ({
active: boolean
}): React.ReactNode => {
const {_} = useLingui()
const {currentAccount} = useSession()
const {currentAccount, hasSession} = useSession()
const [isPTR, setIsPTR] = useState(false)
const isLoggedin = Boolean(currentAccount?.did)
const augmentedQuery = useMemo(() => {
return augmentSearchQuery(query || '', {did: currentAccount?.did})
@@ -195,7 +231,6 @@ let SearchScreenPostResults = ({
hasNextPage,
} = useSearchPostsQuery({query: augmentedQuery, sort, enabled: active})
const pal = usePalette('default')
const t = useTheme()
const onPullToRefresh = useCallback(async () => {
setIsPTR(true)
@@ -249,14 +284,13 @@ let SearchScreenPostResults = ({
requestSwitchToAccount({requestedAccount: 'new'})
}
if (!isLoggedin) {
if (!hasSession) {
return (
<SearchError
title={_(msg`Search is currently unavailable when logged out`)}>
<Text style={[a.text_md, a.text_center, a.leading_snug]}>
<Trans>
<InlineLinkText
style={[pal.link]}
label={_(msg`Sign in`)}
to={'#'}
onPress={showSignIn}>
@@ -264,7 +298,6 @@ let SearchScreenPostResults = ({
</InlineLinkText>
<Text style={t.atoms.text_contrast_medium}> or </Text>
<InlineLinkText
style={[pal.link]}
label={_(msg`Create an account`)}
to={'#'}
onPress={showCreateAccount}>
@@ -283,10 +316,10 @@ let SearchScreenPostResults = ({
return error ? (
<EmptyState
message={_(
messageText={_(
msg`We're sorry, but your search could not be completed. Please try again in a few minutes.`,
)}
error={error.toString()}
error={cleanError(error)}
/>
) : (
<>
@@ -307,10 +340,15 @@ let SearchScreenPostResults = ({
onRefresh={onPullToRefresh}
onEndReached={onEndReached}
desktopFixedHeight
contentContainerStyle={{paddingBottom: 100}}
ListFooterComponent={
<ListFooter
isFetchingNextPage={isFetchingNextPage}
hasNextPage={hasNextPage}
/>
}
/>
) : (
<EmptyState message={_(msg`No results found for ${query}`)} />
<EmptyState messageText={<NoResultsText query={query} />} />
)}
</>
) : (
@@ -329,24 +367,69 @@ let SearchScreenUserResults = ({
active: boolean
}): React.ReactNode => {
const {_} = useLingui()
const {hasSession} = useSession()
const [isPTR, setIsPTR] = useState(false)
const {data: results, isFetched} = useActorSearch({
const {
isFetched,
data: results,
isFetching,
error,
refetch,
fetchNextPage,
isFetchingNextPage,
hasNextPage,
} = useActorSearch({
query,
enabled: active,
})
return isFetched && results ? (
const onPullToRefresh = useCallback(async () => {
setIsPTR(true)
await refetch()
setIsPTR(false)
}, [setIsPTR, refetch])
const onEndReached = useCallback(() => {
if (!hasSession) return
if (isFetching || !hasNextPage || error) return
fetchNextPage()
}, [isFetching, error, hasNextPage, fetchNextPage, hasSession])
const profiles = useMemo(() => {
return results?.pages.flatMap(page => page.actors) || []
}, [results])
if (error) {
return (
<EmptyState
messageText={_(
msg`We're sorry, but your search could not be completed. Please try again in a few minutes.`,
)}
error={error.toString()}
/>
)
}
return isFetched && profiles ? (
<>
{results.length ? (
{profiles.length ? (
<List
data={results}
data={profiles}
renderItem={({item}) => <ProfileCardWithFollowBtn profile={item} />}
keyExtractor={item => item.did}
refreshing={isPTR}
onRefresh={onPullToRefresh}
onEndReached={onEndReached}
desktopFixedHeight
contentContainerStyle={{paddingBottom: 100}}
ListFooterComponent={
<ListFooter
hasNextPage={hasNextPage && hasSession}
isFetchingNextPage={isFetchingNextPage}
/>
}
/>
) : (
<EmptyState message={_(msg`No results found for ${query}`)} />
<EmptyState messageText={<NoResultsText query={query} />} />
)}
</>
) : (
@@ -363,7 +446,6 @@ let SearchScreenFeedsResults = ({
active: boolean
}): React.ReactNode => {
const t = useTheme()
const {_} = useLingui()
const {data: results, isFetched} = usePopularFeedsSearch({
query,
@@ -378,7 +460,7 @@ let SearchScreenFeedsResults = ({
renderItem={({item}) => (
<View
style={[
a.border_b,
a.border_t,
t.atoms.border_contrast_low,
a.px_lg,
a.py_lg,
@@ -388,10 +470,10 @@ let SearchScreenFeedsResults = ({
)}
keyExtractor={item => item.uri}
desktopFixedHeight
contentContainerStyle={{paddingBottom: 100}}
ListFooterComponent={<ListFooter />}
/>
) : (
<EmptyState message={_(msg`No results found for ${query}`)} />
<EmptyState messageText={<NoResultsText query={query} />} />
)}
</>
) : (
+2 -2
View File
@@ -1,7 +1,7 @@
import {useMemo} from 'react'
import {useInterestsDisplayNames} from '#/lib/interests'
import {useActorSearchPaginated} from '#/state/queries/actor-search'
import {useActorSearch} from '#/state/queries/actor-search'
import {useGetSuggestedUsersQuery} from '#/state/queries/trending/useGetSuggestedUsersQuery'
/**
@@ -31,7 +31,7 @@ export function useSuggestedUsers({
category,
overrideInterests,
})
const searched = useActorSearchPaginated({
const searched = useActorSearch({
enabled: !!search,
// use user's app language translation for this value
query: category ? interestsDisplayNames[category] : '',
@@ -7,7 +7,7 @@ import {Trans} from '@lingui/macro'
import {isNative} from '#/platform/detection'
import {useA11y} from '#/state/a11y'
import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
import {useActorSearchPaginated} from '#/state/queries/actor-search'
import {useActorSearch} from '#/state/queries/actor-search'
import {List} from '#/view/com/util/List'
import {useWizardState} from '#/screens/StarterPack/Wizard/State'
import {atoms as a, useTheme} from '#/alf'
@@ -36,7 +36,7 @@ export function StepProfiles({
data: topPages,
fetchNextPage,
isLoading: isLoadingTopPages,
} = useActorSearchPaginated({
} = useActorSearch({
query: encodeURIComponent('*'),
})
const topFollowers = topPages?.pages
+27 -52
View File
@@ -1,25 +1,18 @@
import {
type AppBskyActorDefs,
type AppBskyActorSearchActors,
} from '@atproto/api'
import {type AppBskyActorSearchActors} from '@atproto/api'
import {
type InfiniteData,
keepPreviousData,
type QueryClient,
type QueryKey,
useInfiniteQuery,
useQuery,
} from '@tanstack/react-query'
import {STALE} from '#/state/queries'
import {useAgent} from '#/state/session'
const RQKEY_ROOT = 'actor-search'
export const RQKEY = (query: string) => [RQKEY_ROOT, query]
export const RQKEY_ROOT_PAGINATED = `${RQKEY_ROOT}_paginated`
export const RQKEY_PAGINATED = (query: string, limit?: number) => [
RQKEY_ROOT_PAGINATED,
export const RQKEY_ROOT = 'actor-search'
export const RQKEY = (query: string, limit?: number) => [
RQKEY_ROOT,
query,
limit,
]
@@ -27,27 +20,6 @@ export const RQKEY_PAGINATED = (query: string, limit?: number) => [
export function useActorSearch({
query,
enabled,
}: {
query: string
enabled?: boolean
}) {
const agent = useAgent()
return useQuery<AppBskyActorDefs.ProfileView[]>({
staleTime: STALE.MINUTES.ONE,
queryKey: RQKEY(query || ''),
async queryFn() {
const res = await agent.searchActors({
q: query,
})
return res.data.actors
},
enabled: enabled && !!query,
})
}
export function useActorSearchPaginated({
query,
enabled,
maintainData,
limit = 25,
}: {
@@ -65,7 +37,7 @@ export function useActorSearchPaginated({
string | undefined
>({
staleTime: STALE.MINUTES.FIVE,
queryKey: RQKEY_PAGINATED(query, limit),
queryKey: RQKEY(query, limit),
queryFn: async ({pageParam}) => {
const res = await agent.searchActors({
q: query,
@@ -78,35 +50,38 @@ export function useActorSearchPaginated({
initialPageParam: undefined,
getNextPageParam: lastPage => lastPage.cursor,
placeholderData: maintainData ? keepPreviousData : undefined,
select,
})
}
function select(data: InfiniteData<AppBskyActorSearchActors.OutputSchema>) {
// enforce uniqueness
const dids = new Set()
return {
...data,
pages: data.pages.map(page => ({
actors: page.actors.filter(actor => {
if (dids.has(actor.did)) {
return false
}
dids.add(actor.did)
return true
}),
})),
}
}
export function* findAllProfilesInQueryData(
queryClient: QueryClient,
did: string,
) {
const queryDatas = queryClient.getQueriesData<AppBskyActorDefs.ProfileView[]>(
{
queryKey: [RQKEY_ROOT],
},
)
for (const [_queryKey, queryData] of queryDatas) {
if (!queryData) {
continue
}
for (const actor of queryData) {
if (actor.did === did) {
yield actor
}
}
}
const queryDatasPaginated = queryClient.getQueriesData<
const queryDatas = queryClient.getQueriesData<
InfiniteData<AppBskyActorSearchActors.OutputSchema>
>({
queryKey: [RQKEY_ROOT_PAGINATED],
queryKey: [RQKEY_ROOT],
})
for (const [_queryKey, queryData] of queryDatasPaginated) {
for (const [_queryKey, queryData] of queryDatas) {
if (!queryData) {
continue
}