Search lists cleanup, pagninate actor search (#9009)
This commit is contained in:
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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} />} />
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user