Add new search results tab for starter packs (#11226)

This commit is contained in:
DS Boyce
2026-07-21 13:18:12 -07:00
committed by GitHub
parent 26143c1a48
commit 86f0bedfbe
6 changed files with 226 additions and 12 deletions
+1 -1
View File
@@ -96,7 +96,7 @@
"prettier": "prettier --check ."
},
"dependencies": {
"@atproto/api": "0.20.28",
"@atproto/api": "0.20.31",
"@atproto/common-web": "0.5.6",
"@atproto/syntax": "0.7.2",
"@bitdrift/react-native": "^0.6.8",
+5 -5
View File
@@ -242,8 +242,8 @@ importers:
.:
dependencies:
'@atproto/api':
specifier: 0.20.28
version: 0.20.28
specifier: 0.20.31
version: 0.20.31
'@atproto/common-web':
specifier: 0.5.6
version: 0.5.6
@@ -865,8 +865,8 @@ packages:
graphql:
optional: true
'@atproto/api@0.20.28':
resolution: {integrity: sha512-/Rvk8zt9mtRi9tlMD2Qg+NG2lMj3B0HDjmfswR5724pH7GsOEMDDHwleVlmOBSrgACIyVSa5tdCDJ+R+SEhwww==}
'@atproto/api@0.20.31':
resolution: {integrity: sha512-TovCQLQv5ti1jqh8UH6jJ0EFuWRjGdUtFyFR5xYC/IkIulwHDuyrVaXdCv7VLWiHftp92DevtZnFRm+BZsZZdw==}
engines: {node: '>=22'}
'@atproto/common-web@0.5.6':
@@ -9208,7 +9208,7 @@ snapshots:
'@0no-co/graphql.web@1.2.0': {}
'@atproto/api@0.20.28':
'@atproto/api@0.20.31':
dependencies:
'@atproto/common-web': 0.5.6
'@atproto/lexicon': 0.7.7
+3 -3
View File
@@ -779,13 +779,13 @@ export type Events = {
}
'search:results:loaded': {
tab: 'top' | 'latest' | 'people' | 'feeds'
tab: 'top' | 'latest' | 'people' | 'feeds' | 'starterPacks'
initialCount: number
}
'search:result:press': {
tab?: 'top' | 'latest' | 'people' | 'feeds'
resultType: 'post' | 'profile' | 'feed'
tab?: 'top' | 'latest' | 'people' | 'feeds' | 'starterPacks'
resultType: 'post' | 'profile' | 'feed' | 'starterPack'
position: number
uri: string
}
+132 -1
View File
@@ -1,6 +1,6 @@
import {memo, useCallback, useMemo, useState} from 'react'
import {ActivityIndicator, View} from 'react-native'
import {type AppBskyFeedDefs} from '@atproto/api'
import {type AppBskyFeedDefs, type AppBskyGraphDefs} from '@atproto/api'
import {Trans, useLingui} from '@lingui/react/macro'
import {urls} from '#/lib/constants'
@@ -15,6 +15,7 @@ import {augmentSearchQuery} from '#/lib/strings/helpers'
import {useActorSearch} from '#/state/queries/actor-search'
import {usePopularFeedsSearch} from '#/state/queries/feed'
import {useSearchPostsV2Query} from '#/state/queries/search-posts-v2'
import {useStarterPackSearch} from '#/state/queries/starter-pack-search'
import {useSession} from '#/state/session'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {useCloseAllActiveElements} from '#/state/util'
@@ -23,6 +24,7 @@ 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 {StarterPackCard} from '#/screens/Search/components/StarterPackCard'
import {
hasPostOnlyFilters,
type SearchFilters,
@@ -106,6 +108,15 @@ let SearchResults = ({
<SearchScreenFeedsResults query={query} active={activePage === 3} />
),
},
noFilters && {
title: l`Starter packs`,
component: (
<SearchScreenStarterPackResults
query={query}
active={activePage === 4}
/>
),
},
].filter(Boolean) as {
title: string
component: React.ReactNode
@@ -692,3 +703,123 @@ function SearchFeedCard({
return <FeedCard.Default view={view} onPress={handleOnPress} />
}
let SearchScreenStarterPackResults = ({
query,
active,
}: {
query: string
active: boolean
}): React.ReactNode => {
const ax = useAnalytics()
const {t: l} = useLingui()
const [isPTR, setIsPTR] = useState(false)
const {
isFetched,
data: results,
isFetching,
error,
refetch,
fetchNextPage,
isFetchingNextPage,
hasNextPage,
} = useStarterPackSearch({
query,
enabled: active,
})
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 starterPacks = useMemo(() => {
return results?.pages.flatMap(page => page.starterPacks) || []
}, [results])
const fireTracking = useCallOnce(() => {
ax.metric('search:results:loaded', {
tab: 'starterPacks',
initialCount: starterPacks.length,
})
})
if (isFetched) {
fireTracking()
}
if (error) {
return (
<EmptyState
messageText={
shouldRetryError(error) || isNetworkError(error)
? l`Were sorry, but your search could not be completed. Please try again in a few minutes.`
: l`Were sorry, but your search could not be completed.`
}
error={cleanError(error)}
/>
)
}
return isFetched ? (
<>
{starterPacks.length ? (
<List
data={starterPacks}
renderItem={({
item,
index,
}: {
item: AppBskyGraphDefs.StarterPackView
index: number
}) => (
<View style={[a.px_lg, a.pb_lg, index === 0 && a.pt_lg]}>
<SearchStarterPack position={index} view={item} />
</View>
)}
keyExtractor={(item: AppBskyGraphDefs.StarterPackView) => item.uri}
refreshing={isPTR}
onRefresh={() => void onPullToRefresh()}
onEndReached={onEndReached}
desktopFixedHeight
ListFooterComponent={
<ListFooter
hasNextPage={hasNextPage}
isFetchingNextPage={isFetchingNextPage}
/>
}
/>
) : (
<EmptyState messageText={<NoResultsText query={query} />} />
)}
</>
) : (
<Loader />
)
}
SearchScreenStarterPackResults = memo(SearchScreenStarterPackResults)
function SearchStarterPack({
position,
view,
}: {
position: number
view: AppBskyGraphDefs.StarterPackView
}) {
const ax = useAnalytics()
const handleOnPress = () => {
ax.metric('search:result:press', {
tab: 'starterPacks',
resultType: 'starterPack',
position,
uri: view.uri,
})
}
return <StarterPackCard view={view} onPress={handleOnPress} />
}
@@ -26,8 +26,10 @@ import * as bsky from '#/types/bsky'
export function StarterPackCard({
view,
onPress,
}: {
view: AppBskyGraphDefs.StarterPackView
onPress?: () => void
}) {
const t = useTheme()
const {_} = useLingui()
@@ -55,7 +57,10 @@ export function StarterPackCard({
to={link.to}
label={link.label}
onHoverIn={link.precache}
onPress={link.precache}>
onPress={() => {
link.precache()
onPress?.()
}}>
{s => (
<>
<SubtleHover hover={s.hovered || s.pressed} />
@@ -111,7 +116,10 @@ export function StarterPackCard({
to={link.to}
label={link.label}
onHoverIn={link.precache}
onPress={link.precache}
onPress={() => {
link.precache()
onPress?.()
}}
variant="solid"
color="secondary"
size="small"
+75
View File
@@ -0,0 +1,75 @@
import {type AppBskyGraphSearchStarterPacksV2} from '@atproto/api'
import {
type InfiniteData,
keepPreviousData,
type QueryKey,
useInfiniteQuery,
} from '@tanstack/react-query'
import {STALE} from '#/state/queries'
import {useAgent} from '#/state/session'
export const RQKEY_ROOT = 'starter-pack-search'
export const RQKEY = (query: string, limit?: number) => [
RQKEY_ROOT,
query,
limit,
]
export function useStarterPackSearch({
query,
enabled,
maintainData,
limit = 25,
}: {
query: string
enabled?: boolean
maintainData?: boolean
limit?: number
}) {
const agent = useAgent()
return useInfiniteQuery<
AppBskyGraphSearchStarterPacksV2.OutputSchema,
Error,
InfiniteData<AppBskyGraphSearchStarterPacksV2.OutputSchema>,
QueryKey,
string | undefined
>({
staleTime: STALE.MINUTES.FIVE,
queryKey: RQKEY(query, limit),
queryFn: async ({pageParam}) => {
const res = await agent.app.bsky.graph.searchStarterPacksV2({
q: query,
limit,
cursor: pageParam,
})
return res.data
},
enabled: enabled && !!query,
initialPageParam: undefined,
getNextPageParam: lastPage => lastPage.cursor,
placeholderData: maintainData ? keepPreviousData : undefined,
select,
})
}
function select(
data: InfiniteData<AppBskyGraphSearchStarterPacksV2.OutputSchema>,
) {
// enforce uniqueness
const uris = new Set()
return {
...data,
pages: data.pages.map(page => ({
...page,
starterPacks: page.starterPacks.filter(starterPack => {
if (uris.has(starterPack.uri)) {
return false
}
uris.add(starterPack.uri)
return true
}),
})),
}
}