Migrate all search to v2 and remove v1 (#11125)
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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}
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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}
|
||||||
|
|||||||
Vendored
+1
-1
@@ -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'
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user