Add new search results tab for starter packs (#11226)
This commit is contained in:
+1
-1
@@ -96,7 +96,7 @@
|
|||||||
"prettier": "prettier --check ."
|
"prettier": "prettier --check ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@atproto/api": "0.20.28",
|
"@atproto/api": "0.20.31",
|
||||||
"@atproto/common-web": "0.5.6",
|
"@atproto/common-web": "0.5.6",
|
||||||
"@atproto/syntax": "0.7.2",
|
"@atproto/syntax": "0.7.2",
|
||||||
"@bitdrift/react-native": "^0.6.8",
|
"@bitdrift/react-native": "^0.6.8",
|
||||||
|
|||||||
Generated
+5
-5
@@ -242,8 +242,8 @@ importers:
|
|||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@atproto/api':
|
'@atproto/api':
|
||||||
specifier: 0.20.28
|
specifier: 0.20.31
|
||||||
version: 0.20.28
|
version: 0.20.31
|
||||||
'@atproto/common-web':
|
'@atproto/common-web':
|
||||||
specifier: 0.5.6
|
specifier: 0.5.6
|
||||||
version: 0.5.6
|
version: 0.5.6
|
||||||
@@ -865,8 +865,8 @@ packages:
|
|||||||
graphql:
|
graphql:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@atproto/api@0.20.28':
|
'@atproto/api@0.20.31':
|
||||||
resolution: {integrity: sha512-/Rvk8zt9mtRi9tlMD2Qg+NG2lMj3B0HDjmfswR5724pH7GsOEMDDHwleVlmOBSrgACIyVSa5tdCDJ+R+SEhwww==}
|
resolution: {integrity: sha512-TovCQLQv5ti1jqh8UH6jJ0EFuWRjGdUtFyFR5xYC/IkIulwHDuyrVaXdCv7VLWiHftp92DevtZnFRm+BZsZZdw==}
|
||||||
engines: {node: '>=22'}
|
engines: {node: '>=22'}
|
||||||
|
|
||||||
'@atproto/common-web@0.5.6':
|
'@atproto/common-web@0.5.6':
|
||||||
@@ -9208,7 +9208,7 @@ snapshots:
|
|||||||
|
|
||||||
'@0no-co/graphql.web@1.2.0': {}
|
'@0no-co/graphql.web@1.2.0': {}
|
||||||
|
|
||||||
'@atproto/api@0.20.28':
|
'@atproto/api@0.20.31':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@atproto/common-web': 0.5.6
|
'@atproto/common-web': 0.5.6
|
||||||
'@atproto/lexicon': 0.7.7
|
'@atproto/lexicon': 0.7.7
|
||||||
|
|||||||
@@ -779,13 +779,13 @@ export type Events = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
'search:results:loaded': {
|
'search:results:loaded': {
|
||||||
tab: 'top' | 'latest' | 'people' | 'feeds'
|
tab: 'top' | 'latest' | 'people' | 'feeds' | 'starterPacks'
|
||||||
initialCount: number
|
initialCount: number
|
||||||
}
|
}
|
||||||
|
|
||||||
'search:result:press': {
|
'search:result:press': {
|
||||||
tab?: 'top' | 'latest' | 'people' | 'feeds'
|
tab?: 'top' | 'latest' | 'people' | 'feeds' | 'starterPacks'
|
||||||
resultType: 'post' | 'profile' | 'feed'
|
resultType: 'post' | 'profile' | 'feed' | 'starterPack'
|
||||||
position: number
|
position: number
|
||||||
uri: string
|
uri: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {memo, useCallback, useMemo, useState} from 'react'
|
import {memo, useCallback, useMemo, useState} from 'react'
|
||||||
import {ActivityIndicator, View} from 'react-native'
|
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 {Trans, useLingui} from '@lingui/react/macro'
|
||||||
|
|
||||||
import {urls} from '#/lib/constants'
|
import {urls} from '#/lib/constants'
|
||||||
@@ -15,6 +15,7 @@ import {augmentSearchQuery} from '#/lib/strings/helpers'
|
|||||||
import {useActorSearch} from '#/state/queries/actor-search'
|
import {useActorSearch} from '#/state/queries/actor-search'
|
||||||
import {usePopularFeedsSearch} from '#/state/queries/feed'
|
import {usePopularFeedsSearch} from '#/state/queries/feed'
|
||||||
import {useSearchPostsV2Query} from '#/state/queries/search-posts-v2'
|
import {useSearchPostsV2Query} from '#/state/queries/search-posts-v2'
|
||||||
|
import {useStarterPackSearch} from '#/state/queries/starter-pack-search'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
|
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
|
||||||
import {useCloseAllActiveElements} from '#/state/util'
|
import {useCloseAllActiveElements} from '#/state/util'
|
||||||
@@ -23,6 +24,7 @@ import {TabBar} from '#/view/com/pager/TabBar'
|
|||||||
import {Post} from '#/view/com/post/Post'
|
import {Post} from '#/view/com/post/Post'
|
||||||
import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
|
import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
|
||||||
import {List} from '#/view/com/util/List'
|
import {List} from '#/view/com/util/List'
|
||||||
|
import {StarterPackCard} from '#/screens/Search/components/StarterPackCard'
|
||||||
import {
|
import {
|
||||||
hasPostOnlyFilters,
|
hasPostOnlyFilters,
|
||||||
type SearchFilters,
|
type SearchFilters,
|
||||||
@@ -106,6 +108,15 @@ let SearchResults = ({
|
|||||||
<SearchScreenFeedsResults query={query} active={activePage === 3} />
|
<SearchScreenFeedsResults query={query} active={activePage === 3} />
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
noFilters && {
|
||||||
|
title: l`Starter packs`,
|
||||||
|
component: (
|
||||||
|
<SearchScreenStarterPackResults
|
||||||
|
query={query}
|
||||||
|
active={activePage === 4}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
].filter(Boolean) as {
|
].filter(Boolean) as {
|
||||||
title: string
|
title: string
|
||||||
component: React.ReactNode
|
component: React.ReactNode
|
||||||
@@ -692,3 +703,123 @@ function SearchFeedCard({
|
|||||||
|
|
||||||
return <FeedCard.Default view={view} onPress={handleOnPress} />
|
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`We’re sorry, but your search could not be completed. Please try again in a few minutes.`
|
||||||
|
: l`We’re 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({
|
export function StarterPackCard({
|
||||||
view,
|
view,
|
||||||
|
onPress,
|
||||||
}: {
|
}: {
|
||||||
view: AppBskyGraphDefs.StarterPackView
|
view: AppBskyGraphDefs.StarterPackView
|
||||||
|
onPress?: () => void
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
@@ -55,7 +57,10 @@ export function StarterPackCard({
|
|||||||
to={link.to}
|
to={link.to}
|
||||||
label={link.label}
|
label={link.label}
|
||||||
onHoverIn={link.precache}
|
onHoverIn={link.precache}
|
||||||
onPress={link.precache}>
|
onPress={() => {
|
||||||
|
link.precache()
|
||||||
|
onPress?.()
|
||||||
|
}}>
|
||||||
{s => (
|
{s => (
|
||||||
<>
|
<>
|
||||||
<SubtleHover hover={s.hovered || s.pressed} />
|
<SubtleHover hover={s.hovered || s.pressed} />
|
||||||
@@ -111,7 +116,10 @@ export function StarterPackCard({
|
|||||||
to={link.to}
|
to={link.to}
|
||||||
label={link.label}
|
label={link.label}
|
||||||
onHoverIn={link.precache}
|
onHoverIn={link.precache}
|
||||||
onPress={link.precache}
|
onPress={() => {
|
||||||
|
link.precache()
|
||||||
|
onPress?.()
|
||||||
|
}}
|
||||||
variant="solid"
|
variant="solid"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
size="small"
|
size="small"
|
||||||
|
|||||||
@@ -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
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user