Migrate all search to v2 and remove v1 (#11125)

This commit is contained in:
DS Boyce
2026-07-09 15:08:20 -07:00
committed by GitHub
parent 65e10f13f7
commit 3499d3646e
14 changed files with 114 additions and 475 deletions
-16
View File
@@ -1087,14 +1087,6 @@
"count": 1 "count": 1
} }
}, },
"src/screens/Hashtag.tsx": {
"@typescript-eslint/no-floating-promises": {
"count": 2
},
"@typescript-eslint/no-misused-promises": {
"count": 1
}
},
"src/screens/Home/NoFeedsPinned.tsx": { "src/screens/Home/NoFeedsPinned.tsx": {
"@typescript-eslint/no-misused-promises": { "@typescript-eslint/no-misused-promises": {
"count": 1 "count": 1
@@ -1489,14 +1481,6 @@
"count": 1 "count": 1
} }
}, },
"src/screens/Topic.tsx": {
"@typescript-eslint/no-floating-promises": {
"count": 2
},
"@typescript-eslint/no-misused-promises": {
"count": 1
}
},
"src/state/a11y.tsx": { "src/state/a11y.tsx": {
"@typescript-eslint/no-floating-promises": { "@typescript-eslint/no-floating-promises": {
"count": 1 "count": 1
-2
View File
@@ -12,8 +12,6 @@ export enum Features {
GroupChatsDisable = 'group_chats:disable', GroupChatsDisable = 'group_chats:disable',
ComposerLanguageDetectionEnable = 'composer:language_detection:enable', ComposerLanguageDetectionEnable = 'composer:language_detection:enable',
PostGalleryEmbedEnable = 'post_gallery_embed:enable', PostGalleryEmbedEnable = 'post_gallery_embed:enable',
SearchV2Enable = 'search_v2:enable',
AdvancedSearchV2Enable = 'advanced_search_v2:enable',
AATest = 'aa-test', AATest = 'aa-test',
} }
+25 -24
View File
@@ -1,9 +1,7 @@
import {useCallback, useMemo, useState} from 'react' import {useCallback, useMemo, useState} from 'react'
import {type ListRenderItemInfo, View} from 'react-native' import {type ListRenderItemInfo, View} from 'react-native'
import {type AppBskyFeedDefs} from '@atproto/api' import {type AppBskyFeedDefs} from '@atproto/api'
import {msg} from '@lingui/core/macro' import {Trans, useLingui} from '@lingui/react/macro'
import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
import {type NativeStackScreenProps} from '@react-navigation/native-stack' import {type NativeStackScreenProps} from '@react-navigation/native-stack'
import {HITSLOP_10} from '#/lib/constants' import {HITSLOP_10} from '#/lib/constants'
@@ -14,7 +12,7 @@ import {shareUrl} from '#/lib/sharing'
import {cleanError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors'
import {sanitizeHandle} from '#/lib/strings/handles' import {sanitizeHandle} from '#/lib/strings/handles'
import {enforceLen} from '#/lib/strings/helpers' import {enforceLen} from '#/lib/strings/helpers'
import {useSearchPostsQuery} from '#/state/queries/search-posts' import {useSearchPostsV2Query} from '#/state/queries/search-posts-v2'
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'
@@ -43,7 +41,7 @@ export default function HashtagScreen({
route, route,
}: NativeStackScreenProps<CommonNavigatorParams, 'Hashtag'>) { }: NativeStackScreenProps<CommonNavigatorParams, 'Hashtag'>) {
const {tag, author} = route.params const {tag, author} = route.params
const {_} = useLingui() const {t: l} = useLingui()
const decodedTag = useMemo(() => { const decodedTag = useMemo(() => {
return decodeURIComponent(tag) return decodeURIComponent(tag)
@@ -73,7 +71,7 @@ export default function HashtagScreen({
if (author) { if (author) {
url.searchParams.set('author', author) url.searchParams.set('author', author)
} }
shareUrl(url.toString()) void shareUrl(url.toString())
}, [tag, author]) }, [tag, author])
const [activeTab, setActiveTab] = useState(0) const [activeTab, setActiveTab] = useState(0)
@@ -85,7 +83,7 @@ export default function HashtagScreen({
const sections = useMemo(() => { const sections = useMemo(() => {
return [ return [
{ {
title: _(msg`Top`), title: l`Top`,
component: ( component: (
<HashtagScreenTab <HashtagScreenTab
fullTag={fullTag} fullTag={fullTag}
@@ -96,7 +94,7 @@ export default function HashtagScreen({
), ),
}, },
{ {
title: _(msg`Latest`), title: l`Latest`,
component: ( component: (
<HashtagScreenTab <HashtagScreenTab
fullTag={fullTag} fullTag={fullTag}
@@ -107,7 +105,7 @@ export default function HashtagScreen({
), ),
}, },
] ]
}, [_, fullTag, author, activeTab]) }, [l, fullTag, author, activeTab])
return ( return (
<Layout.Screen> <Layout.Screen>
@@ -122,14 +120,14 @@ export default function HashtagScreen({
{author && ( {author && (
<Layout.Header.SubtitleText> <Layout.Header.SubtitleText>
{author.startsWith('did:') {author.startsWith('did:')
? _(msg`From ${sanitizedAuthor}`) ? l`From ${sanitizedAuthor}`
: _(msg`From @${sanitizedAuthor}`)} : l`From @${sanitizedAuthor}`}
</Layout.Header.SubtitleText> </Layout.Header.SubtitleText>
)} )}
</Layout.Header.Content> </Layout.Header.Content>
<Layout.Header.Slot> <Layout.Header.Slot>
<Button <Button
label={_(msg`Share`)} label={l`Share`}
size="small" size="small"
variant="ghost" variant="ghost"
color="primary" color="primary"
@@ -164,7 +162,7 @@ function HashtagScreenTab({
sort: 'top' | 'latest' sort: 'top' | 'latest'
active: boolean active: boolean
}) { }) {
const {_} = useLingui() const {t: l} = useLingui()
const initialNumToRender = useInitialNumToRender() const initialNumToRender = useInitialNumToRender()
const [isPTR, setIsPTR] = useState(false) const [isPTR, setIsPTR] = useState(false)
const t = useTheme() const t = useTheme()
@@ -188,7 +186,14 @@ function HashtagScreenTab({
refetch, refetch,
fetchNextPage, fetchNextPage,
hasNextPage, hasNextPage,
} = useSearchPostsQuery({query: queryParam, sort, enabled: active, author}) } = useSearchPostsV2Query({
query: queryParam,
sort,
enabled: active,
filters: {
author,
},
})
const posts = useMemo(() => { const posts = useMemo(() => {
return data?.pages.flatMap(page => page.posts) || [] return data?.pages.flatMap(page => page.posts) || []
@@ -202,7 +207,7 @@ function HashtagScreenTab({
const onEndReached = useCallback(() => { const onEndReached = useCallback(() => {
if (isFetchingNextPage || !hasNextPage || error) return if (isFetchingNextPage || !hasNextPage || error) return
fetchNextPage() void fetchNextPage()
}, [isFetchingNextPage, hasNextPage, error, fetchNextPage]) }, [isFetchingNextPage, hasNextPage, error, fetchNextPage])
const closeAllActiveElements = useCloseAllActiveElements() const closeAllActiveElements = useCloseAllActiveElements()
@@ -220,19 +225,15 @@ function HashtagScreenTab({
if (!hasSession) { if (!hasSession) {
return ( return (
<SearchError <SearchError title={l`Search is currently unavailable when logged out`}>
title={_(msg`Search is currently unavailable when logged out`)}>
<Text style={[a.text_md, a.text_center, a.leading_snug]}> <Text style={[a.text_md, a.text_center, a.leading_snug]}>
<Trans> <Trans>
<InlineLinkText <InlineLinkText label={l`Sign in`} to={'#'} onPress={showSignIn}>
label={_(msg`Sign in`)}
to={'#'}
onPress={showSignIn}>
Sign in Sign in
</InlineLinkText> </InlineLinkText>
<Text style={t.atoms.text_contrast_medium}> or </Text> <Text style={t.atoms.text_contrast_medium}> or </Text>
<InlineLinkText <InlineLinkText
label={_(msg`Create an account`)} label={l`Create an account`}
to={'#'} to={'#'}
onPress={showCreateAccount}> onPress={showCreateAccount}>
create an account create an account
@@ -256,7 +257,7 @@ function HashtagScreenTab({
isError={isError} isError={isError}
onRetry={refetch} onRetry={refetch}
emptyType="results" emptyType="results"
emptyMessage={_(msg`We couldn't find any results for that tag.`)} emptyMessage={l`We couldn't find any results for that tag.`}
/> />
) : ( ) : (
<List <List
@@ -264,7 +265,7 @@ function HashtagScreenTab({
renderItem={renderItem} renderItem={renderItem}
keyExtractor={keyExtractor} keyExtractor={keyExtractor}
refreshing={isPTR} refreshing={isPTR}
onRefresh={onRefresh} onRefresh={() => void onRefresh()}
onEndReached={onEndReached} onEndReached={onEndReached}
onEndReachedThreshold={4} onEndReachedThreshold={4}
onItemSeen={trackPostView} onItemSeen={trackPostView}
+16 -67
View File
@@ -10,7 +10,6 @@ import {cleanError} from '#/lib/strings/errors'
import {augmentSearchQuery} from '#/lib/strings/helpers' 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 {useSearchPostsQuery} from '#/state/queries/search-posts'
import {useSearchPostsV2Query} from '#/state/queries/search-posts-v2' import {useSearchPostsV2Query} from '#/state/queries/search-posts-v2'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {useLoggedOutViewControls} from '#/state/shell/logged-out' import {useLoggedOutViewControls} from '#/state/shell/logged-out'
@@ -36,7 +35,6 @@ import type * as bsky from '#/types/bsky'
let SearchResults = ({ let SearchResults = ({
query, query,
queryWithParams,
filters, filters,
hasFilters, hasFilters,
activeTab, activeTab,
@@ -44,7 +42,6 @@ let SearchResults = ({
headerHeight, headerHeight,
}: { }: {
query: string query: string
queryWithParams: string
filters: SearchFilters filters: SearchFilters
hasFilters: boolean hasFilters: boolean
activeTab: number activeTab: number
@@ -75,7 +72,6 @@ let SearchResults = ({
<SearchScreenPostResults <SearchScreenPostResults
hasFilters={hasFilters} hasFilters={hasFilters}
query={query} query={query}
queryWithParams={queryWithParams}
filters={filters} filters={filters}
sort="top" sort="top"
active={activePage === 0} active={activePage === 0}
@@ -88,7 +84,6 @@ let SearchResults = ({
<SearchScreenPostResults <SearchScreenPostResults
hasFilters={hasFilters} hasFilters={hasFilters}
query={query} query={query}
queryWithParams={queryWithParams}
filters={filters} filters={filters}
sort="latest" sort="latest"
active={activePage === 1} active={activePage === 1}
@@ -111,15 +106,7 @@ let SearchResults = ({
title: string title: string
component: React.ReactNode component: React.ReactNode
}[] }[]
}, [ }, [l, query, filters, hasFilters, hasPostFilters, activePage])
l,
query,
queryWithParams,
filters,
hasFilters,
hasPostFilters,
activePage,
])
// There may be fewer tabs after changing the search options. // There may be fewer tabs after changing the search options.
const selectedPage = activePage > sections.length - 1 ? 0 : activePage const selectedPage = activePage > sections.length - 1 ? 0 : activePage
@@ -204,14 +191,9 @@ function NoResultsText({
hasFilters?: boolean hasFilters?: boolean
query: string query: string
}) { }) {
const ax = useAnalytics()
const t = useTheme() const t = useTheme()
const {t: l} = useLingui() const {t: l} = useLingui()
const searchV2Enabled = ax.features.enabled(ax.features.SearchV2Enable)
const advancedSearchV2Enabled =
searchV2Enabled && ax.features.enabled(ax.features.AdvancedSearchV2Enable)
return ( return (
<> <>
<Text style={[a.text_lg, t.atoms.text_contrast_high]}> <Text style={[a.text_lg, t.atoms.text_contrast_high]}>
@@ -257,35 +239,19 @@ function NoResultsText({
a.leading_snug, a.leading_snug,
t.atoms.text_contrast_high, t.atoms.text_contrast_high,
]}> ]}>
{advancedSearchV2Enabled ? ( <Trans context="english-only-resource">
<Trans context="english-only-resource"> Learn more about{' '}
Learn more about{' '} <InlineLinkText
<InlineLinkText label={l({
label={l({ message: 'Read about how to use advanced search filters',
message: 'Read about how to use advanced search filters', context: 'english-only-resource',
context: 'english-only-resource', })}
})} to={urls.website.blog.searchTipsAndTricks}
to={urls.website.blog.searchTipsAndTricks} style={[a.text_md, a.leading_snug]}>
style={[a.text_md, a.leading_snug]}> how to use advanced search
how to use advanced search </InlineLinkText>
</InlineLinkText> .
. </Trans>
</Trans>
) : (
<Trans context="english-only-resource">
Learn more about{' '}
<InlineLinkText
label={l({
message: 'Read about how to use search filters',
context: 'english-only-resource',
})}
to={urls.website.blog.searchTipsAndTricks}
style={[a.text_md, a.leading_snug]}>
how to use search filters
</InlineLinkText>
.
</Trans>
)}
</Text> </Text>
</> </>
) )
@@ -305,14 +271,12 @@ type SearchResultSlice =
let SearchScreenPostResults = ({ let SearchScreenPostResults = ({
hasFilters = false, hasFilters = false,
query, query,
queryWithParams,
filters, filters,
sort, sort,
active, active,
}: { }: {
hasFilters: boolean hasFilters: boolean
query: string query: string
queryWithParams: string
filters?: SearchFilters filters?: SearchFilters
sort?: 'top' | 'latest' sort?: 'top' | 'latest'
active: boolean active: boolean
@@ -323,30 +287,15 @@ let SearchScreenPostResults = ({
const [isPTR, setIsPTR] = useState(false) const [isPTR, setIsPTR] = useState(false)
const trackPostView = usePostViewTracking('SearchResults') const trackPostView = usePostViewTracking('SearchResults')
const searchV2Enabled = ax.features.enabled(ax.features.SearchV2Enable)
const augmentedV2Query = useMemo(() => { const augmentedV2Query = useMemo(() => {
return augmentSearchQuery(query || '') return augmentSearchQuery(query || '')
}, [query]) }, [query])
const augmentedV1Query = useMemo(() => {
return augmentSearchQuery(queryWithParams || '')
}, [queryWithParams])
/*
* Both hooks are called to keep hook order stable; `enabled` ensures only the
* gated one actually fetches. V2 sends structured `filters` as separate
* params, V1 keeps the existing single-`q` behavior.
*/
const v1 = useSearchPostsQuery({
query: augmentedV1Query,
sort,
enabled: active && !searchV2Enabled,
})
const v2 = useSearchPostsV2Query({ const v2 = useSearchPostsV2Query({
query: augmentedV2Query, query: augmentedV2Query,
filters, filters,
sort, sort,
enabled: active && searchV2Enabled, enabled: active,
}) })
const { const {
isFetched, isFetched,
@@ -357,7 +306,7 @@ let SearchScreenPostResults = ({
fetchNextPage, fetchNextPage,
isFetchingNextPage, isFetchingNextPage,
hasNextPage, hasNextPage,
} = searchV2Enabled ? v2 : v1 } = v2
const t = useTheme() const t = useTheme()
const onPullToRefresh = useCallback(async () => { const onPullToRefresh = useCallback(async () => {
+7 -34
View File
@@ -36,7 +36,6 @@ import {useSession} from '#/state/session'
import { import {
countActiveFilters, countActiveFilters,
definedFilterParams, definedFilterParams,
filtersToLegacyParams,
filtersToRouteParams, filtersToRouteParams,
hasActiveFilters, hasActiveFilters,
parseHistoryEntry, parseHistoryEntry,
@@ -45,7 +44,6 @@ import {
serializeHistoryEntry, serializeHistoryEntry,
withoutFilterParams, withoutFilterParams,
} from '#/screens/Search/searchParams' } from '#/screens/Search/searchParams'
import {makeSearchQuery} from '#/screens/Search/utils'
import { import {
atoms as a, atoms as a,
native, native,
@@ -71,7 +69,6 @@ import {AutocompleteResults} from './components/AutocompleteResults'
import {DetectedLanguagesAdmonition} from './components/DetectedLanguagesAdmonition' import {DetectedLanguagesAdmonition} from './components/DetectedLanguagesAdmonition'
import {SearchAutocompleteInput} from './components/SearchAutocompleteInput' import {SearchAutocompleteInput} from './components/SearchAutocompleteInput'
import {SearchHistory} from './components/SearchHistory' import {SearchHistory} from './components/SearchHistory'
import {SearchLanguageDropdown} from './components/SearchLanguageDropdown'
import {Explore} from './Explore' import {Explore} from './Explore'
import {SearchResults} from './SearchResults' import {SearchResults} from './SearchResults'
@@ -122,10 +119,6 @@ export function SearchScreenShell({
const {currentAccount} = useSession() const {currentAccount} = useSession()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const searchV2Enabled = ax.features.enabled(ax.features.SearchV2Enable)
const advancedSearchV2Enabled =
searchV2Enabled && ax.features.enabled(ax.features.AdvancedSearchV2Enable)
// Get tab parameter from route params // Get tab parameter from route params
const tabParam = (route.params as {q?: string; tab?: TabParam})?.tab const tabParam = (route.params as {q?: string; tab?: TabParam})?.tab
const [activeTab, setActiveTab] = useState(() => getTabIndex(tabParam)) const [activeTab, setActiveTab] = useState(() => getTabIndex(tabParam))
@@ -207,11 +200,10 @@ export function SearchScreenShell({
[accountHistory, setAccountHistory], [accountHistory, setAccountHistory],
) )
const {query, queryWithParams, filters, setFilters, hasFilters} = const {query, filters, setFilters, hasFilters} = useQueryManager({
useQueryManager({ initialQuery: queryParam,
initialQuery: queryParam, fixedParams,
fixedParams, })
})
const showFilters = Boolean((query || hasFilters) && !showAutocomplete) const showFilters = Boolean((query || hasFilters) && !showAutocomplete)
const onChangeLang = useCallback( const onChangeLang = useCallback(
@@ -560,12 +552,7 @@ export function SearchScreenShell({
{isExplore ? <Trans>Explore</Trans> : <Trans>Search</Trans>} {isExplore ? <Trans>Explore</Trans> : <Trans>Search</Trans>}
</Layout.Header.TitleText> </Layout.Header.TitleText>
</Layout.Header.Content> </Layout.Header.Content>
{showFilters && !advancedSearchV2Enabled ? ( {showFilters ? (
<SearchLanguageDropdown
value={filters.lang ?? ''}
onChange={onChangeLang}
/>
) : showFilters && advancedSearchV2Enabled ? (
<View style={[a.flex_row, a.align_center, a.gap_sm]}> <View style={[a.flex_row, a.align_center, a.gap_sm]}>
<AdvancedSearchDialog <AdvancedSearchDialog
disabled={activeTab > 1} disabled={activeTab > 1}
@@ -591,7 +578,7 @@ export function SearchScreenShell({
)} )}
<View style={[a.px_lg, a.pt_sm, a.pb_sm, a.overflow_hidden]}> <View style={[a.px_lg, a.pt_sm, a.pb_sm, a.overflow_hidden]}>
<View style={[a.gap_sm]}> <View style={[a.gap_sm]}>
{searchV2Enabled && query && !showAutocomplete && ( {query && !showAutocomplete && (
<DetectedLanguagesAdmonition <DetectedLanguagesAdmonition
query={query} query={query}
filters={filters} filters={filters}
@@ -635,7 +622,7 @@ export function SearchScreenShell({
/> />
</View> </View>
</View> </View>
{showFilters && !showHeader && advancedSearchV2Enabled ? ( {showFilters && !showHeader ? (
<View style={[a.flex_row, a.align_center, a.gap_sm]}> <View style={[a.flex_row, a.align_center, a.gap_sm]}>
<AdvancedSearchDialog <AdvancedSearchDialog
disabled={activeTab > 1} disabled={activeTab > 1}
@@ -655,15 +642,6 @@ export function SearchScreenShell({
</View> </View>
) : null} ) : null}
</View> </View>
{showFilters && !showHeader && !advancedSearchV2Enabled && (
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
<SearchLanguageDropdown
value={filters.lang ?? ''}
onChange={onChangeLang}
/>
</View>
)}
</View> </View>
</View> </View>
</Layout.Center> </Layout.Center>
@@ -676,7 +654,6 @@ export function SearchScreenShell({
activeTab={activeTab} activeTab={activeTab}
setActiveTab={setActiveTab} setActiveTab={setActiveTab}
query={query} query={query}
queryWithParams={queryWithParams}
filters={filters} filters={filters}
hasFilters={hasFilters} hasFilters={hasFilters}
headerHeight={headerHeight} headerHeight={headerHeight}
@@ -728,7 +705,6 @@ let SearchScreenInner = ({
activeTab, activeTab,
setActiveTab, setActiveTab,
query, query,
queryWithParams,
filters, filters,
hasFilters, hasFilters,
headerHeight, headerHeight,
@@ -737,7 +713,6 @@ let SearchScreenInner = ({
activeTab: number activeTab: number
setActiveTab: React.Dispatch<React.SetStateAction<number>> setActiveTab: React.Dispatch<React.SetStateAction<number>>
query: string query: string
queryWithParams: string
filters: SearchFilters filters: SearchFilters
hasFilters: boolean hasFilters: boolean
headerHeight: number headerHeight: number
@@ -754,7 +729,6 @@ let SearchScreenInner = ({
return query || hasFilters ? ( return query || hasFilters ? (
<SearchResults <SearchResults
query={query} query={query}
queryWithParams={queryWithParams}
filters={filters} filters={filters}
hasFilters={hasFilters} hasFilters={hasFilters}
activeTab={activeTab} activeTab={activeTab}
@@ -840,7 +814,6 @@ function useQueryManager({
return useMemo( return useMemo(
() => ({ () => ({
query, query,
queryWithParams: makeSearchQuery(query, filtersToLegacyParams(filters)),
filters, filters,
setFilters, setFilters,
hasFilters: hasActiveFilters(filters), hasFilters: hasActiveFilters(filters),
@@ -4,7 +4,6 @@ import {
countActiveFilters, countActiveFilters,
definedFilterParams, definedFilterParams,
filtersToApiParams, filtersToApiParams,
filtersToLegacyParams,
hasPostOnlyFilters, hasPostOnlyFilters,
parseHistoryEntry, parseHistoryEntry,
readSearchFilters, readSearchFilters,
@@ -76,34 +75,6 @@ describe(`searchParams`, () => {
}) })
}) })
describe(`filtersToLegacyParams`, () => {
it(`maps structured filters back to legacy query operators`, () => {
expect(
filtersToLegacyParams({
author: 'alice',
mentions: 'bob',
domain: 'bsky.app',
url: 'bsky.app/x',
tag: 'atproto',
lang: 'en',
since: '2024-01-01',
until: '2024-02-01',
media: 'true',
replies: 'none',
}),
).toEqual({
from: 'alice',
mentions: 'bob',
domain: 'bsky.app',
url: 'bsky.app/x',
tag: 'atproto',
lang: 'en',
since: '2024-01-01',
until: '2024-02-01',
})
})
})
describe(`filtersToApiParams`, () => { describe(`filtersToApiParams`, () => {
it(`splits list fields into arrays and maps v2-only filters`, () => { it(`splits list fields into arrays and maps v2-only filters`, () => {
expect( expect(
@@ -1,12 +1,10 @@
import {useMemo} from 'react' import {useMemo} from 'react'
import {msg} from '@lingui/core/macro' import {Trans, useLingui} from '@lingui/react/macro'
import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
import {languageName} from '#/locale/helpers' import {languageName} from '#/locale/helpers'
import {APP_LANGUAGES, LANGUAGES} from '#/locale/languages' import {APP_LANGUAGES, LANGUAGES} from '#/locale/languages'
import {useLanguagePrefs} from '#/state/preferences' import {useLanguagePrefs} from '#/state/preferences'
import {atoms as a, native, platform, tokens} from '#/alf' import {atoms as a, native, platform} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import { import {
ChevronBottom_Stroke2_Corner0_Rounded as ChevronDownIcon, ChevronBottom_Stroke2_Corner0_Rounded as ChevronDownIcon,
@@ -14,7 +12,6 @@ import {
} from '#/components/icons/Chevron' } from '#/components/icons/Chevron'
import {Earth_Stroke2_Corner0_Rounded as EarthIcon} from '#/components/icons/Globe' import {Earth_Stroke2_Corner0_Rounded as EarthIcon} from '#/components/icons/Globe'
import * as Menu from '#/components/Menu' import * as Menu from '#/components/Menu'
import {useAnalytics} from '#/analytics'
export function SearchLanguageDropdown({ export function SearchLanguageDropdown({
showIcon = true, showIcon = true,
@@ -25,14 +22,9 @@ export function SearchLanguageDropdown({
value: string value: string
onChange(value: string): void onChange(value: string): void
}) { }) {
const ax = useAnalytics() const {t: l} = useLingui()
const {_} = useLingui()
const {appLanguage, contentLanguages, primaryLanguage} = useLanguagePrefs() const {appLanguage, contentLanguages, primaryLanguage} = useLanguagePrefs()
const searchV2Enabled = ax.features.enabled(ax.features.SearchV2Enable)
const advancedSearchV2Enabled =
searchV2Enabled && ax.features.enabled(ax.features.AdvancedSearchV2Enable)
const languages = useMemo(() => { const languages = useMemo(() => {
return LANGUAGES.filter( return LANGUAGES.filter(
(lang, index, self) => (lang, index, self) =>
@@ -70,40 +62,20 @@ export function SearchLanguageDropdown({
}, [appLanguage, contentLanguages, primaryLanguage]) }, [appLanguage, contentLanguages, primaryLanguage])
const currentLanguageLabel = const currentLanguageLabel =
languages.find(lang => lang.value === value)?.label ?? _(msg`All languages`) languages.find(lang => lang.value === value)?.label ?? l`All languages`
return ( return (
<Menu.Root> <Menu.Root>
<Menu.Trigger <Menu.Trigger
label={_( label={l`Filter search by language (currently: ${currentLanguageLabel})`}>
msg`Filter search by language (currently: ${currentLanguageLabel})`,
)}>
{({props}) => ( {({props}) => (
<Button <Button
{...props} {...props}
label={props.accessibilityLabel} label={props.accessibilityLabel}
size="small" size="small"
color={ color="secondary"
advancedSearchV2Enabled variant="solid"
? 'secondary' style={showIcon ? native([a.py_sm, a.px_sm]) : null}>
: platform({native: 'primary', default: 'secondary'})
}
variant={
advancedSearchV2Enabled
? 'solid'
: platform({native: 'ghost', default: 'solid'})
}
style={
showIcon
? native([
a.py_sm,
a.px_sm,
advancedSearchV2Enabled
? null
: {marginRight: tokens.space.sm * -1},
])
: null
}>
{showIcon ? <ButtonIcon icon={EarthIcon} /> : null} {showIcon ? <ButtonIcon icon={EarthIcon} /> : null}
<ButtonText>{currentLanguageLabel}</ButtonText> <ButtonText>{currentLanguageLabel}</ButtonText>
<ButtonIcon <ButtonIcon
@@ -119,7 +91,7 @@ export function SearchLanguageDropdown({
<Menu.LabelText> <Menu.LabelText>
<Trans>Filter search by language</Trans> <Trans>Filter search by language</Trans>
</Menu.LabelText> </Menu.LabelText>
<Menu.Item label={_(msg`All languages`)} onPress={() => onChange('')}> <Menu.Item label={l`All languages`} onPress={() => onChange('')}>
<Menu.ItemText> <Menu.ItemText>
<Trans>All languages</Trans> <Trans>All languages</Trans>
</Menu.ItemText> </Menu.ItemText>
-21
View File
@@ -188,27 +188,6 @@ export function withoutFilterParams(
} }
return base return base
} }
/**
* Converts structured filters back into the legacy free-text operators used by
* search v1. UI-only filters are intentionally dropped because the old path
* could not apply them.
*/
export function filtersToLegacyParams(
filters: SearchFilters,
): Record<string, string> {
const params: Record<string, string> = {}
if (filters.author) params.from = filters.author
if (filters.mentions) params.mentions = filters.mentions
if (filters.domain) params.domain = filters.domain
if (filters.url) params.url = filters.url
if (filters.tag) params.tag = filters.tag
if (filters.lang) params.lang = filters.lang
if (filters.since) params.since = filters.since
if (filters.until) params.until = filters.until
return params
}
/** /**
* Maps each multi-value SearchFilters key to its `app.bsky.feed.searchPostsV2` * Maps each multi-value SearchFilters key to its `app.bsky.feed.searchPostsV2`
* param name. Search v1 only honored the first value for the singular lexicon * param name. Search v1 only honored the first value for the singular lexicon
-12
View File
@@ -30,15 +30,3 @@ export function parseSearchQuery(rawQuery: string) {
params, params,
} }
} }
export function makeSearchQuery(query: string, params: Params) {
return [
query,
Object.entries(params)
.filter(([_, value]) => value)
.map(([name, value]) => `${name}:${value}`)
.join(' '),
]
.filter(Boolean)
.join(' ')
}
+13 -14
View File
@@ -1,8 +1,7 @@
import {useCallback, useMemo, useState} from 'react' import {useCallback, useMemo, useState} from 'react'
import {type ListRenderItemInfo, View} from 'react-native' import {type ListRenderItemInfo, View} from 'react-native'
import {type AppBskyFeedDefs} from '@atproto/api' import {type AppBskyFeedDefs} from '@atproto/api'
import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react/macro'
import {useLingui} from '@lingui/react'
import {type NativeStackScreenProps} from '@react-navigation/native-stack' import {type NativeStackScreenProps} from '@react-navigation/native-stack'
import {HITSLOP_10} from '#/lib/constants' import {HITSLOP_10} from '#/lib/constants'
@@ -12,7 +11,7 @@ import {type CommonNavigatorParams} from '#/lib/routes/types'
import {shareUrl} from '#/lib/sharing' import {shareUrl} from '#/lib/sharing'
import {cleanError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors'
import {enforceLen} from '#/lib/strings/helpers' import {enforceLen} from '#/lib/strings/helpers'
import {useSearchPostsQuery} from '#/state/queries/search-posts' import {useSearchPostsV2Query} from '#/state/queries/search-posts-v2'
import {Pager} from '#/view/com/pager/Pager' import {Pager} from '#/view/com/pager/Pager'
import {TabBar} from '#/view/com/pager/TabBar' import {TabBar} from '#/view/com/pager/TabBar'
import {Post} from '#/view/com/post/Post' import {Post} from '#/view/com/post/Post'
@@ -35,7 +34,7 @@ export default function TopicScreen({
route, route,
}: NativeStackScreenProps<CommonNavigatorParams, 'Topic'>) { }: NativeStackScreenProps<CommonNavigatorParams, 'Topic'>) {
const {topic} = route.params const {topic} = route.params
const {_} = useLingui() const {t: l} = useLingui()
const headerTitle = useMemo(() => { const headerTitle = useMemo(() => {
return enforceLen(decodeURIComponent(topic), 24, true, 'middle') return enforceLen(decodeURIComponent(topic), 24, true, 'middle')
@@ -44,7 +43,7 @@ export default function TopicScreen({
const onShare = useCallback(() => { const onShare = useCallback(() => {
const url = new URL('https://bsky.app') const url = new URL('https://bsky.app')
url.pathname = `/topic/${topic}` url.pathname = `/topic/${topic}`
shareUrl(url.toString()) void shareUrl(url.toString())
}, [topic]) }, [topic])
const [activeTab, setActiveTab] = useState(0) const [activeTab, setActiveTab] = useState(0)
@@ -56,13 +55,13 @@ export default function TopicScreen({
const sections = useMemo(() => { const sections = useMemo(() => {
return [ return [
{ {
title: _(msg`Top`), title: l`Top`,
component: ( component: (
<TopicScreenTab topic={topic} sort="top" active={activeTab === 0} /> <TopicScreenTab topic={topic} sort="top" active={activeTab === 0} />
), ),
}, },
{ {
title: _(msg`Latest`), title: l`Latest`,
component: ( component: (
<TopicScreenTab <TopicScreenTab
topic={topic} topic={topic}
@@ -72,7 +71,7 @@ export default function TopicScreen({
), ),
}, },
] ]
}, [_, topic, activeTab]) }, [l, topic, activeTab])
return ( return (
<Layout.Screen> <Layout.Screen>
@@ -87,7 +86,7 @@ export default function TopicScreen({
</Layout.Header.Content> </Layout.Header.Content>
<Layout.Header.Slot> <Layout.Header.Slot>
<Button <Button
label={_(msg`Share`)} label={l`Share`}
size="small" size="small"
variant="ghost" variant="ghost"
color="primary" color="primary"
@@ -120,7 +119,7 @@ function TopicScreenTab({
sort: 'top' | 'latest' sort: 'top' | 'latest'
active: boolean active: boolean
}) { }) {
const {_} = useLingui() const {t: l} = useLingui()
const initialNumToRender = useInitialNumToRender() const initialNumToRender = useInitialNumToRender()
const [isPTR, setIsPTR] = useState(false) const [isPTR, setIsPTR] = useState(false)
const trackPostView = usePostViewTracking('Topic') const trackPostView = usePostViewTracking('Topic')
@@ -135,7 +134,7 @@ function TopicScreenTab({
refetch, refetch,
fetchNextPage, fetchNextPage,
hasNextPage, hasNextPage,
} = useSearchPostsQuery({ } = useSearchPostsV2Query({
query: decodeURIComponent(topic), query: decodeURIComponent(topic),
sort, sort,
enabled: active, enabled: active,
@@ -153,7 +152,7 @@ function TopicScreenTab({
const onEndReached = useCallback(() => { const onEndReached = useCallback(() => {
if (isFetchingNextPage || !hasNextPage || error) return if (isFetchingNextPage || !hasNextPage || error) return
fetchNextPage() void fetchNextPage()
}, [isFetchingNextPage, hasNextPage, error, fetchNextPage]) }, [isFetchingNextPage, hasNextPage, error, fetchNextPage])
return ( return (
@@ -164,7 +163,7 @@ function TopicScreenTab({
isError={isError} isError={isError}
onRetry={refetch} onRetry={refetch}
emptyType="results" emptyType="results"
emptyMessage={_(msg`We couldn't find any results for that topic.`)} emptyMessage={l`We couldn't find any results for that topic.`}
/> />
) : ( ) : (
<List <List
@@ -172,7 +171,7 @@ function TopicScreenTab({
renderItem={renderItem} renderItem={renderItem}
keyExtractor={keyExtractor} keyExtractor={keyExtractor}
refreshing={isPTR} refreshing={isPTR}
onRefresh={onRefresh} onRefresh={() => void onRefresh()}
onEndReached={onEndReached} onEndReached={onEndReached}
onEndReachedThreshold={4} onEndReachedThreshold={4}
onItemSeen={trackPostView} onItemSeen={trackPostView}
+1 -1
View File
@@ -13,7 +13,7 @@ import {findAllPostsInQueryData as findAllPostsInExploreFeedPreviewsQueryData} f
import {findAllPostsInQueryData as findAllPostsInNotifsQueryData} from '#/state/queries/notifications/feed' import {findAllPostsInQueryData as findAllPostsInNotifsQueryData} from '#/state/queries/notifications/feed'
import {findAllPostsInQueryData as findAllPostsInFeedQueryData} from '#/state/queries/post-feed' import {findAllPostsInQueryData as findAllPostsInFeedQueryData} from '#/state/queries/post-feed'
import {findAllPostsInQueryData as findAllPostsInQuoteQueryData} from '#/state/queries/post-quotes' import {findAllPostsInQueryData as findAllPostsInQuoteQueryData} from '#/state/queries/post-quotes'
import {findAllPostsInQueryData as findAllPostsInSearchQueryData} from '#/state/queries/search-posts' import {findAllPostsInQueryData as findAllPostsInSearchQueryData} from '#/state/queries/search-posts-v2'
import {findAllPostsInQueryData as findAllPostsInThreadV2QueryData} from '#/state/queries/usePostThread/queryCache' import {findAllPostsInQueryData as findAllPostsInThreadV2QueryData} from '#/state/queries/usePostThread/queryCache'
import {castAsShadow, type Shadow} from './types' import {castAsShadow, type Shadow} from './types'
export type {Shadow} from './types' export type {Shadow} from './types'
+42 -8
View File
@@ -1,7 +1,13 @@
import {useCallback, useMemo, useRef} from 'react' import {useCallback, useMemo, useRef} from 'react'
import {type AppBskyFeedSearchPostsV2, moderatePost} from '@atproto/api' import {
type AppBskyFeedDefs,
type AppBskyFeedSearchPostsV2,
AtUri,
moderatePost,
} from '@atproto/api'
import { import {
type InfiniteData, type InfiniteData,
type QueryClient,
type QueryKey, type QueryKey,
useInfiniteQuery, useInfiniteQuery,
} from '@tanstack/react-query' } from '@tanstack/react-query'
@@ -13,14 +19,12 @@ import {
buildSearchPostsV2Filters, buildSearchPostsV2Filters,
extractSearchPostsParams, extractSearchPostsParams,
} from './search-posts-params' } from './search-posts-params'
import {
didOrHandleUriMatches,
embedViewRecordToPostView,
getEmbeddedPost,
} from './util'
/**
* V2 search shares the `'search-posts'` query-key root with the original hook
* (src/state/queries/search-posts.ts) so the shadow-cache generators there -
* findAllPostsInQueryData / findAllProfilesInQueryData - discover V2 results
* too. This module is only used behind the AdvancedSearchV2Enable gate; the
* original hook is unchanged.
*/
const searchPostsQueryKeyRoot = 'search-posts' const searchPostsQueryKeyRoot = 'search-posts'
const searchPostsV2QueryKey = ({ const searchPostsV2QueryKey = ({
query, query,
@@ -164,3 +168,33 @@ export function useSearchPostsV2Query({
), ),
}) })
} }
export function* findAllPostsInQueryData(
queryClient: QueryClient,
uri: string,
): Generator<AppBskyFeedDefs.PostView, undefined> {
const queryDatas = queryClient.getQueriesData<
InfiniteData<AppBskyFeedSearchPostsV2.OutputSchema>
>({
queryKey: [searchPostsQueryKeyRoot],
})
const atUri = new AtUri(uri)
for (const [_queryKey, queryData] of queryDatas) {
if (!queryData?.pages) {
continue
}
for (const page of queryData?.pages) {
for (const post of page.posts) {
if (didOrHandleUriMatches(atUri, post)) {
yield post
}
const quotedPost = getEmbeddedPost(post.embed)
if (quotedPost && didOrHandleUriMatches(atUri, quotedPost)) {
yield embedViewRecordToPostView(quotedPost)
}
}
}
}
}
-209
View File
@@ -1,209 +0,0 @@
import {useCallback, useMemo, useRef} from 'react'
import {
type AppBskyActorDefs,
type AppBskyFeedDefs,
type AppBskyFeedSearchPosts,
AtUri,
moderatePost,
} from '@atproto/api'
import {
type InfiniteData,
type QueryClient,
type QueryKey,
useInfiniteQuery,
} from '@tanstack/react-query'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useAgent} from '#/state/session'
import {
didOrHandleUriMatches,
embedViewRecordToPostView,
getEmbeddedPost,
} from './util'
const searchPostsQueryKeyRoot = 'search-posts'
const searchPostsQueryKey = ({
query,
sort,
author,
}: {
query: string
sort?: string
author?: string
}) => [searchPostsQueryKeyRoot, query, sort, author]
export function useSearchPostsQuery({
query,
sort,
enabled,
author,
}: {
query: string
sort?: 'top' | 'latest'
enabled?: boolean
author?: string
}) {
const agent = useAgent()
const moderationOpts = useModerationOpts()
const selectArgs = useMemo(
() => ({
isSearchingSpecificUser: !!author || /from:(\w+)/.test(query),
moderationOpts,
}),
[query, author, moderationOpts],
)
const lastRun = useRef<{
data: InfiniteData<AppBskyFeedSearchPosts.OutputSchema>
args: typeof selectArgs
result: InfiniteData<AppBskyFeedSearchPosts.OutputSchema>
} | null>(null)
return useInfiniteQuery<
AppBskyFeedSearchPosts.OutputSchema,
Error,
InfiniteData<AppBskyFeedSearchPosts.OutputSchema>,
QueryKey,
string | undefined
>({
queryKey: searchPostsQueryKey({query, sort, author}),
queryFn: async ({pageParam}) => {
const res = await agent.app.bsky.feed.searchPosts({
q: query,
limit: 25,
cursor: pageParam,
sort,
author,
})
return res.data
},
initialPageParam: undefined,
getNextPageParam: lastPage => lastPage.cursor,
enabled: enabled ?? !!moderationOpts,
select: useCallback(
(data: InfiniteData<AppBskyFeedSearchPosts.OutputSchema>) => {
const {moderationOpts, isSearchingSpecificUser} = selectArgs
/*
* If a user applies the `from:<user>` filter, don't apply any
* moderation. Note that if we add any more filtering logic below, we
* may need to adjust this.
*/
if (isSearchingSpecificUser) {
return data
}
// Keep track of the last run and whether we can reuse
// some already selected pages from there.
let reusedPages = []
if (lastRun.current) {
const {
data: lastData,
args: lastArgs,
result: lastResult,
} = lastRun.current
let canReuse = true
for (let key in selectArgs) {
if (selectArgs.hasOwnProperty(key)) {
if (
(selectArgs as Record<string, unknown>)[key] !==
(lastArgs as Record<string, unknown>)[key]
) {
// Can't do reuse anything if any input has changed.
canReuse = false
break
}
}
}
if (canReuse) {
for (let i = 0; i < data.pages.length; i++) {
if (data.pages[i] && lastData.pages[i] === data.pages[i]) {
reusedPages.push(lastResult.pages[i])
continue
}
// Stop as soon as pages stop matching up.
break
}
}
}
const result = {
...data,
pages: [
...reusedPages,
...data.pages.slice(reusedPages.length).map(page => {
return {
...page,
posts: page.posts.filter(post => {
const mod = moderatePost(post, moderationOpts!)
return !mod.ui('contentList').filter
}),
}
}),
],
}
lastRun.current = {data, result, args: selectArgs}
return result
},
[selectArgs],
),
})
}
export function* findAllPostsInQueryData(
queryClient: QueryClient,
uri: string,
): Generator<AppBskyFeedDefs.PostView, undefined> {
const queryDatas = queryClient.getQueriesData<
InfiniteData<AppBskyFeedSearchPosts.OutputSchema>
>({
queryKey: [searchPostsQueryKeyRoot],
})
const atUri = new AtUri(uri)
for (const [_queryKey, queryData] of queryDatas) {
if (!queryData?.pages) {
continue
}
for (const page of queryData?.pages) {
for (const post of page.posts) {
if (didOrHandleUriMatches(atUri, post)) {
yield post
}
const quotedPost = getEmbeddedPost(post.embed)
if (quotedPost && didOrHandleUriMatches(atUri, quotedPost)) {
yield embedViewRecordToPostView(quotedPost)
}
}
}
}
}
export function* findAllProfilesInQueryData(
queryClient: QueryClient,
did: string,
): Generator<AppBskyActorDefs.ProfileViewBasic, undefined> {
const queryDatas = queryClient.getQueriesData<
InfiniteData<AppBskyFeedSearchPosts.OutputSchema>
>({
queryKey: [searchPostsQueryKeyRoot],
})
for (const [_queryKey, queryData] of queryDatas) {
if (!queryData?.pages) {
continue
}
for (const page of queryData?.pages) {
for (const post of page.posts) {
if (post.author.did === did) {
yield post.author
}
const quotedPost = getEmbeddedPost(post.embed)
if (quotedPost?.author.did === did) {
yield quotedPost.author
}
}
}
}
}
@@ -19,7 +19,7 @@ import {findAllPostsInQueryData as findAllPostsInExploreFeedPreviewsQueryData} f
import {findAllPostsInQueryData as findAllPostsInNotifsQueryData} from '#/state/queries/notifications/feed' import {findAllPostsInQueryData as findAllPostsInNotifsQueryData} from '#/state/queries/notifications/feed'
import {findAllPostsInQueryData as findAllPostsInFeedQueryData} from '#/state/queries/post-feed' import {findAllPostsInQueryData as findAllPostsInFeedQueryData} from '#/state/queries/post-feed'
import {findAllPostsInQueryData as findAllPostsInQuoteQueryData} from '#/state/queries/post-quotes' import {findAllPostsInQueryData as findAllPostsInQuoteQueryData} from '#/state/queries/post-quotes'
import {findAllPostsInQueryData as findAllPostsInSearchQueryData} from '#/state/queries/search-posts' import {findAllPostsInQueryData as findAllPostsInSearchQueryData} from '#/state/queries/search-posts-v2'
import {usePostThreadContext} from '#/state/queries/usePostThread' import {usePostThreadContext} from '#/state/queries/usePostThread'
import {getBranch} from '#/state/queries/usePostThread/traversal' import {getBranch} from '#/state/queries/usePostThread/traversal'
import { import {