import {memo, useCallback, useMemo, useState} from 'react' import {ActivityIndicator, View} from 'react-native' import {type AppBskyFeedDefs} from '@atproto/api' import {Trans, useLingui} from '@lingui/react/macro' import {urls} from '#/lib/constants' import {usePostViewTracking} from '#/lib/hooks/usePostViewTracking' import {useCallOnce} from '#/lib/once' 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' import {useSearchPostsQuery} from '#/state/queries/search-posts' import {useSession} from '#/state/session' import {useLoggedOutViewControls} from '#/state/shell/logged-out' import {useCloseAllActiveElements} from '#/state/util' import {Pager} from '#/view/com/pager/Pager' import {TabBar} from '#/view/com/pager/TabBar' import {Post} from '#/view/com/post/Post' import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard' import {List} from '#/view/com/util/List' 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' import {type Metrics, useAnalytics} from '#/analytics' import type * as bsky from '#/types/bsky' let SearchResults = ({ query, queryWithParams, activeTab, onPageSelected, headerHeight, initialPage = 0, }: { query: string queryWithParams: string activeTab: number onPageSelected: (page: number) => void headerHeight: number initialPage?: number }): React.ReactNode => { const {t: l} = useLingui() const sections = useMemo(() => { if (!queryWithParams) return [] const noParams = queryWithParams === query return [ { title: l`Top`, component: ( ), }, { title: l`Latest`, component: ( ), }, noParams && { title: l`People`, component: ( ), }, noParams && { title: l`Feeds`, component: ( ), }, ].filter(Boolean) as { title: string component: React.ReactNode }[] }, [l, query, queryWithParams, activeTab]) // There may be fewer tabs after changing the search options. const selectedPage = initialPage > sections.length - 1 ? 0 : initialPage return ( ( section.title)} {...props} /> )} initialPage={selectedPage}> {sections.map((section, i) => ( {section.component} ))} ) } SearchResults = memo(SearchResults) export {SearchResults} function Loader() { const t = useTheme() return ( ) } function EmptyState({ messageText, error, children, }: { messageText: React.ReactNode error?: string children?: React.ReactNode }) { const t = useTheme() return ( {messageText} {error && ( <> Error: {error} )} {children} ) } function NoResultsText({ query, }: { sort?: 'top' | 'latest' | 'people' | 'feeds' query: string }) { const t = useTheme() const {t: l} = 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' key: string post: AppBskyFeedDefs.PostView } | { type: 'loadingMore' key: string } let SearchScreenPostResults = ({ query, sort, active, }: { query: string sort?: 'top' | 'latest' active: boolean }): React.ReactNode => { const ax = useAnalytics() const {t: l} = useLingui() const {currentAccount, hasSession} = useSession() const [isPTR, setIsPTR] = useState(false) const trackPostView = usePostViewTracking('SearchResults') const augmentedQuery = useMemo(() => { return augmentSearchQuery(query || '', {did: currentAccount?.did}) }, [query, currentAccount]) const { isFetched, data: results, isFetching, error, refetch, fetchNextPage, isFetchingNextPage, hasNextPage, } = useSearchPostsQuery({query: augmentedQuery, sort, enabled: active}) const t = useTheme() const onPullToRefresh = useCallback(async () => { setIsPTR(true) await refetch() setIsPTR(false) }, [setIsPTR, refetch]) const onEndReached = useCallback(() => { if (isFetching || !hasNextPage || error) return void fetchNextPage() }, [isFetching, error, hasNextPage, fetchNextPage]) const posts = useMemo(() => { return results?.pages.flatMap(page => page.posts) || [] }, [results]) const items = useMemo(() => { let temp: SearchResultSlice[] = [] const seenUris = new Set() for (const post of posts) { if (seenUris.has(post.uri)) { continue } temp.push({ type: 'post', key: post.uri, post, }) seenUris.add(post.uri) } if (isFetchingNextPage) { temp.push({ type: 'loadingMore', key: 'loadingMore', }) } return temp }, [posts, isFetchingNextPage]) const closeAllActiveElements = useCloseAllActiveElements() const {requestSwitchToAccount} = useLoggedOutViewControls() const fireTracking = useCallOnce(() => { if (sort) { // ts only ax.metric('search:results:loaded', { tab: sort, initialCount: items.length, }) } }) if (isFetched && sort) { fireTracking() } const showSignIn = () => { closeAllActiveElements() requestSwitchToAccount({requestedAccount: 'none'}) } const showCreateAccount = () => { closeAllActiveElements() requestSwitchToAccount({requestedAccount: 'new'}) } if (!hasSession) { return ( Sign in or create an account to search for news, sports, politics, and everything else happening on Bluesky. ) } return error ? ( ) : ( <> {isFetched ? ( <> {posts.length ? ( { if (item.type === 'post') { return ( ) } else { return null } }} keyExtractor={(item: SearchResultSlice) => item.key} refreshing={isPTR} onRefresh={() => { void onPullToRefresh() }} onEndReached={onEndReached} onItemSeen={(item: SearchResultSlice) => { if (item.type === 'post') { trackPostView(item.post) } }} desktopFixedHeight ListFooterComponent={ } /> ) : ( } /> )} ) : ( )} ) } SearchScreenPostResults = memo(SearchScreenPostResults) function SearchPost({ from, position, post, }: { from: Metrics['search:result:press']['tab'] position: Metrics['search:result:press']['position'] post: AppBskyFeedDefs.PostView }) { const ax = useAnalytics() const onBeforePress = useCallback(() => { ax.metric('search:result:press', { tab: from, resultType: 'post', position, uri: post.uri, }) }, [ax, from, position, post]) return } let SearchScreenUserResults = ({ query, active, }: { query: string active: boolean }): React.ReactNode => { const ax = useAnalytics() const {t: l} = useLingui() const {hasSession} = useSession() const [isPTR, setIsPTR] = useState(false) const { isFetched, data: results, isFetching, error, refetch, fetchNextPage, isFetchingNextPage, hasNextPage, } = useActorSearch({ query, enabled: active, }) const onPullToRefresh = useCallback(async () => { setIsPTR(true) await refetch() setIsPTR(false) }, [setIsPTR, refetch]) const onEndReached = useCallback(() => { if (!hasSession) return if (isFetching || !hasNextPage || error) return void fetchNextPage() }, [isFetching, error, hasNextPage, fetchNextPage, hasSession]) const profiles = useMemo(() => { return results?.pages.flatMap(page => page.actors) || [] }, [results]) const fireTracking = useCallOnce(() => { ax.metric('search:results:loaded', { tab: 'people', initialCount: profiles.length, }) }) if (isFetched) { fireTracking() } if (error) { return ( ) } return isFetched && profiles ? ( <> {profiles.length ? ( } keyExtractor={(item: bsky.profile.AnyProfileView) => item.did} refreshing={isPTR} onRefresh={() => void onPullToRefresh()} onEndReached={onEndReached} desktopFixedHeight ListFooterComponent={ } /> ) : ( } /> )} ) : ( ) } SearchScreenUserResults = memo(SearchScreenUserResults) function SearchScreenProfileButton({ position, profile, }: { position: number profile: bsky.profile.AnyProfileView }) { const ax = useAnalytics() const handlePress = () => { ax.metric('search:result:press', { tab: 'people', resultType: 'profile', position, uri: profile.did, }) } return } let SearchScreenFeedsResults = ({ query, active, }: { query: string active: boolean }): React.ReactNode => { const ax = useAnalytics() const t = useTheme() const {data: results, isFetched} = usePopularFeedsSearch({ query, enabled: active, }) const fireTracking = useCallOnce(() => { ax.metric('search:results:loaded', { tab: 'feeds', initialCount: results?.length ?? 0, }) }) if (isFetched) { fireTracking() } return isFetched && results ? ( <> {results.length ? ( ( )} keyExtractor={(item: AppBskyFeedDefs.GeneratorView) => item.uri} desktopFixedHeight ListFooterComponent={} /> ) : ( } /> )} ) : ( ) } SearchScreenFeedsResults = memo(SearchScreenFeedsResults) function SearchFeedCard({ position, view, }: { position: number view: AppBskyFeedDefs.GeneratorView }) { const ax = useAnalytics() const handleOnPress = () => { ax.metric('search:result:press', { tab: 'feeds', resultType: 'feed', position, uri: view.uri, }) } return }