diff --git a/src/components/ProgressGuide/FollowDialog.tsx b/src/components/ProgressGuide/FollowDialog.tsx index 18afa462af..511cee1a68 100644 --- a/src/components/ProgressGuide/FollowDialog.tsx +++ b/src/components/ProgressGuide/FollowDialog.tsx @@ -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, }) diff --git a/src/lib/constants.ts b/src/lib/constants.ts index c777480fc0..97a7679615 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -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', }, }, } diff --git a/src/screens/Search/Explore.tsx b/src/screens/Search/Explore.tsx index 478cbba2c0..e512454fce 100644 --- a/src/screens/Search/Explore.tsx +++ b/src/screens/Search/Explore.tsx @@ -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(), diff --git a/src/screens/Search/SearchResults.tsx b/src/screens/Search/SearchResults.tsx index fcee3ab863..63decdad47 100644 --- a/src/screens/Search/SearchResults.tsx +++ b/src/screens/Search/SearchResults.tsx @@ -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({ - {message} + {messageText} {error && ( <> @@ -155,6 +157,41 @@ function EmptyState({ ) } +function NoResultsText({query}: {query: string}) { + const t = useTheme() + const {_} = useLingui() + + return ( + <> + + + No results found for " + {query} + ". + + + {'\n\n'} + + + Try a different search term, or{' '} + + read about how to use search filters + + . + + + + ) +} + 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 ( @@ -264,7 +298,6 @@ let SearchScreenPostResults = ({ or @@ -283,10 +316,10 @@ let SearchScreenPostResults = ({ return error ? ( ) : ( <> @@ -307,10 +340,15 @@ let SearchScreenPostResults = ({ onRefresh={onPullToRefresh} onEndReached={onEndReached} desktopFixedHeight - contentContainerStyle={{paddingBottom: 100}} + ListFooterComponent={ + + } /> ) : ( - + } /> )} ) : ( @@ -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 ( + + ) + } + + return isFetched && profiles ? ( <> - {results.length ? ( + {profiles.length ? ( } keyExtractor={item => item.did} + refreshing={isPTR} + onRefresh={onPullToRefresh} + onEndReached={onEndReached} desktopFixedHeight - contentContainerStyle={{paddingBottom: 100}} + ListFooterComponent={ + + } /> ) : ( - + } /> )} ) : ( @@ -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}) => ( item.uri} desktopFixedHeight - contentContainerStyle={{paddingBottom: 100}} + ListFooterComponent={} /> ) : ( - + } /> )} ) : ( diff --git a/src/screens/Search/util/useSuggestedUsers.ts b/src/screens/Search/util/useSuggestedUsers.ts index b1f9064096..0f7bc2607c 100644 --- a/src/screens/Search/util/useSuggestedUsers.ts +++ b/src/screens/Search/util/useSuggestedUsers.ts @@ -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] : '', diff --git a/src/screens/StarterPack/Wizard/StepProfiles.tsx b/src/screens/StarterPack/Wizard/StepProfiles.tsx index fa4a42f253..3d4fee4b5b 100644 --- a/src/screens/StarterPack/Wizard/StepProfiles.tsx +++ b/src/screens/StarterPack/Wizard/StepProfiles.tsx @@ -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 diff --git a/src/state/queries/actor-search.ts b/src/state/queries/actor-search.ts index 5347ca0a15..84e6d88f29 100644 --- a/src/state/queries/actor-search.ts +++ b/src/state/queries/actor-search.ts @@ -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({ - 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) { + // 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( - { - 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 >({ - queryKey: [RQKEY_ROOT_PAGINATED], + queryKey: [RQKEY_ROOT], }) - for (const [_queryKey, queryData] of queryDatasPaginated) { + for (const [_queryKey, queryData] of queryDatas) { if (!queryData) { continue }