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 {logEvent} from '#/lib/statsig/statsig'
import {isWeb} from '#/platform/detection' import {isWeb} from '#/platform/detection'
import {useModerationOpts} from '#/state/preferences/moderation-opts' 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 {usePreferencesQuery} from '#/state/queries/preferences'
import {useGetSuggestedUsersQuery} from '#/state/queries/trending/useGetSuggestedUsersQuery' import {useGetSuggestedUsersQuery} from '#/state/queries/trending/useGetSuggestedUsersQuery'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
@@ -128,7 +128,7 @@ function DialogInner({guide}: {guide: Follow10ProgressGuide}) {
isFetching: isFetchingSearchResults, isFetching: isFetchingSearchResults,
error: searchResultsError, error: searchResultsError,
isError: isSearchResultsError, isError: isSearchResultsError,
} = useActorSearchPaginated({ } = useActorSearch({
enabled: !!searchText, enabled: !!searchText,
query: searchText, query: searchText,
}) })
+1
View File
@@ -204,6 +204,7 @@ export const urls = {
website: { website: {
blog: { blog: {
initialVerificationAnnouncement: `https://bsky.social/about/blog/04-21-2025-verification`, 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 {type MetricEvents} from '#/logger/metrics'
import {useLanguagePrefs} from '#/state/preferences/languages' import {useLanguagePrefs} from '#/state/preferences/languages'
import {useModerationOpts} from '#/state/preferences/moderation-opts' 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 { import {
type FeedPreviewItem, type FeedPreviewItem,
useFeedPreviews, useFeedPreviews,
@@ -308,7 +308,7 @@ export function Explore({
queryKey: [getSuggestedUsersQueryKeyRoot], queryKey: [getSuggestedUsersQueryKeyRoot],
}), }),
qc.resetQueries({ qc.resetQueries({
queryKey: [useActorSearchPaginatedQueryKeyRoot], queryKey: [useActorSearchQueryKeyRoot],
}), }),
qc.resetQueries({ qc.resetQueries({
queryKey: createGetSuggestedFeedsQueryKey(), queryKey: createGetSuggestedFeedsQueryKey(),
+106 -24
View File
@@ -4,7 +4,8 @@ import {type AppBskyFeedDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' 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 {augmentSearchQuery} from '#/lib/strings/helpers'
import {useActorSearch} from '#/state/queries/actor-search' import {useActorSearch} from '#/state/queries/actor-search'
import {usePopularFeedsSearch} from '#/state/queries/feed' 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 FeedCard from '#/components/FeedCard'
import * as Layout from '#/components/Layout' import * as Layout from '#/components/Layout'
import {InlineLinkText} from '#/components/Link' import {InlineLinkText} from '#/components/Link'
import {ListFooter} from '#/components/Lists'
import {SearchError} from '#/components/SearchError' import {SearchError} from '#/components/SearchError'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
@@ -112,11 +114,11 @@ function Loader() {
} }
function EmptyState({ function EmptyState({
message, messageText,
error, error,
children, children,
}: { }: {
message: string messageText: React.ReactNode
error?: string error?: string
children?: React.ReactNode children?: React.ReactNode
}) { }) {
@@ -126,7 +128,7 @@ function EmptyState({
<Layout.Content> <Layout.Content>
<View style={[a.p_xl]}> <View style={[a.p_xl]}>
<View style={[t.atoms.bg_contrast_25, a.rounded_sm, a.p_lg]}> <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 && ( {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 SearchResultSlice =
| { | {
type: 'post' type: 'post'
@@ -176,9 +213,8 @@ let SearchScreenPostResults = ({
active: boolean active: boolean
}): React.ReactNode => { }): React.ReactNode => {
const {_} = useLingui() const {_} = useLingui()
const {currentAccount} = useSession() const {currentAccount, hasSession} = useSession()
const [isPTR, setIsPTR] = useState(false) const [isPTR, setIsPTR] = useState(false)
const isLoggedin = Boolean(currentAccount?.did)
const augmentedQuery = useMemo(() => { const augmentedQuery = useMemo(() => {
return augmentSearchQuery(query || '', {did: currentAccount?.did}) return augmentSearchQuery(query || '', {did: currentAccount?.did})
@@ -195,7 +231,6 @@ let SearchScreenPostResults = ({
hasNextPage, hasNextPage,
} = useSearchPostsQuery({query: augmentedQuery, sort, enabled: active}) } = useSearchPostsQuery({query: augmentedQuery, sort, enabled: active})
const pal = usePalette('default')
const t = useTheme() const t = useTheme()
const onPullToRefresh = useCallback(async () => { const onPullToRefresh = useCallback(async () => {
setIsPTR(true) setIsPTR(true)
@@ -249,14 +284,13 @@ let SearchScreenPostResults = ({
requestSwitchToAccount({requestedAccount: 'new'}) requestSwitchToAccount({requestedAccount: 'new'})
} }
if (!isLoggedin) { if (!hasSession) {
return ( return (
<SearchError <SearchError
title={_(msg`Search is currently unavailable when logged out`)}> title={_(msg`Search is currently unavailable when logged out`)}>
<Text style={[a.text_md, a.text_center, a.leading_snug]}> <Text style={[a.text_md, a.text_center, a.leading_snug]}>
<Trans> <Trans>
<InlineLinkText <InlineLinkText
style={[pal.link]}
label={_(msg`Sign in`)} label={_(msg`Sign in`)}
to={'#'} to={'#'}
onPress={showSignIn}> onPress={showSignIn}>
@@ -264,7 +298,6 @@ let SearchScreenPostResults = ({
</InlineLinkText> </InlineLinkText>
<Text style={t.atoms.text_contrast_medium}> or </Text> <Text style={t.atoms.text_contrast_medium}> or </Text>
<InlineLinkText <InlineLinkText
style={[pal.link]}
label={_(msg`Create an account`)} label={_(msg`Create an account`)}
to={'#'} to={'#'}
onPress={showCreateAccount}> onPress={showCreateAccount}>
@@ -283,10 +316,10 @@ let SearchScreenPostResults = ({
return error ? ( return error ? (
<EmptyState <EmptyState
message={_( messageText={_(
msg`We're sorry, but your search could not be completed. Please try again in a few minutes.`, 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} onRefresh={onPullToRefresh}
onEndReached={onEndReached} onEndReached={onEndReached}
desktopFixedHeight 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 active: boolean
}): React.ReactNode => { }): React.ReactNode => {
const {_} = useLingui() 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, query,
enabled: active, 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 <List
data={results} data={profiles}
renderItem={({item}) => <ProfileCardWithFollowBtn profile={item} />} renderItem={({item}) => <ProfileCardWithFollowBtn profile={item} />}
keyExtractor={item => item.did} keyExtractor={item => item.did}
refreshing={isPTR}
onRefresh={onPullToRefresh}
onEndReached={onEndReached}
desktopFixedHeight 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 active: boolean
}): React.ReactNode => { }): React.ReactNode => {
const t = useTheme() const t = useTheme()
const {_} = useLingui()
const {data: results, isFetched} = usePopularFeedsSearch({ const {data: results, isFetched} = usePopularFeedsSearch({
query, query,
@@ -378,7 +460,7 @@ let SearchScreenFeedsResults = ({
renderItem={({item}) => ( renderItem={({item}) => (
<View <View
style={[ style={[
a.border_b, a.border_t,
t.atoms.border_contrast_low, t.atoms.border_contrast_low,
a.px_lg, a.px_lg,
a.py_lg, a.py_lg,
@@ -388,10 +470,10 @@ let SearchScreenFeedsResults = ({
)} )}
keyExtractor={item => item.uri} keyExtractor={item => item.uri}
desktopFixedHeight 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 {useMemo} from 'react'
import {useInterestsDisplayNames} from '#/lib/interests' 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' import {useGetSuggestedUsersQuery} from '#/state/queries/trending/useGetSuggestedUsersQuery'
/** /**
@@ -31,7 +31,7 @@ export function useSuggestedUsers({
category, category,
overrideInterests, overrideInterests,
}) })
const searched = useActorSearchPaginated({ const searched = useActorSearch({
enabled: !!search, enabled: !!search,
// use user's app language translation for this value // use user's app language translation for this value
query: category ? interestsDisplayNames[category] : '', query: category ? interestsDisplayNames[category] : '',
@@ -7,7 +7,7 @@ import {Trans} from '@lingui/macro'
import {isNative} from '#/platform/detection' import {isNative} from '#/platform/detection'
import {useA11y} from '#/state/a11y' import {useA11y} from '#/state/a11y'
import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete' 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 {List} from '#/view/com/util/List'
import {useWizardState} from '#/screens/StarterPack/Wizard/State' import {useWizardState} from '#/screens/StarterPack/Wizard/State'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
@@ -36,7 +36,7 @@ export function StepProfiles({
data: topPages, data: topPages,
fetchNextPage, fetchNextPage,
isLoading: isLoadingTopPages, isLoading: isLoadingTopPages,
} = useActorSearchPaginated({ } = useActorSearch({
query: encodeURIComponent('*'), query: encodeURIComponent('*'),
}) })
const topFollowers = topPages?.pages const topFollowers = topPages?.pages
+27 -52
View File
@@ -1,25 +1,18 @@
import { import {type AppBskyActorSearchActors} from '@atproto/api'
type AppBskyActorDefs,
type AppBskyActorSearchActors,
} from '@atproto/api'
import { import {
type InfiniteData, type InfiniteData,
keepPreviousData, keepPreviousData,
type QueryClient, type QueryClient,
type QueryKey, type QueryKey,
useInfiniteQuery, useInfiniteQuery,
useQuery,
} from '@tanstack/react-query' } from '@tanstack/react-query'
import {STALE} from '#/state/queries' import {STALE} from '#/state/queries'
import {useAgent} from '#/state/session' import {useAgent} from '#/state/session'
const RQKEY_ROOT = 'actor-search' export const RQKEY_ROOT = 'actor-search'
export const RQKEY = (query: string) => [RQKEY_ROOT, query] export const RQKEY = (query: string, limit?: number) => [
RQKEY_ROOT,
export const RQKEY_ROOT_PAGINATED = `${RQKEY_ROOT}_paginated`
export const RQKEY_PAGINATED = (query: string, limit?: number) => [
RQKEY_ROOT_PAGINATED,
query, query,
limit, limit,
] ]
@@ -27,27 +20,6 @@ export const RQKEY_PAGINATED = (query: string, limit?: number) => [
export function useActorSearch({ export function useActorSearch({
query, query,
enabled, 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, maintainData,
limit = 25, limit = 25,
}: { }: {
@@ -65,7 +37,7 @@ export function useActorSearchPaginated({
string | undefined string | undefined
>({ >({
staleTime: STALE.MINUTES.FIVE, staleTime: STALE.MINUTES.FIVE,
queryKey: RQKEY_PAGINATED(query, limit), queryKey: RQKEY(query, limit),
queryFn: async ({pageParam}) => { queryFn: async ({pageParam}) => {
const res = await agent.searchActors({ const res = await agent.searchActors({
q: query, q: query,
@@ -78,35 +50,38 @@ export function useActorSearchPaginated({
initialPageParam: undefined, initialPageParam: undefined,
getNextPageParam: lastPage => lastPage.cursor, getNextPageParam: lastPage => lastPage.cursor,
placeholderData: maintainData ? keepPreviousData : undefined, 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( export function* findAllProfilesInQueryData(
queryClient: QueryClient, queryClient: QueryClient,
did: string, did: string,
) { ) {
const queryDatas = queryClient.getQueriesData<AppBskyActorDefs.ProfileView[]>( const queryDatas = queryClient.getQueriesData<
{
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<
InfiniteData<AppBskyActorSearchActors.OutputSchema> InfiniteData<AppBskyActorSearchActors.OutputSchema>
>({ >({
queryKey: [RQKEY_ROOT_PAGINATED], queryKey: [RQKEY_ROOT],
}) })
for (const [_queryKey, queryData] of queryDatasPaginated) { for (const [_queryKey, queryData] of queryDatas) {
if (!queryData) { if (!queryData) {
continue continue
} }