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
}