Fix infinite loading spinner when changing search terms (#9950)
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
This commit is contained in:
@@ -1,9 +1,7 @@
|
||||
import {memo, useCallback, useMemo, useState} from 'react'
|
||||
import {ActivityIndicator, View} from 'react-native'
|
||||
import {type AppBskyFeedDefs} from '@atproto/api'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Trans} from '@lingui/react/macro'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {urls} from '#/lib/constants'
|
||||
import {usePostViewTracking} from '#/lib/hooks/usePostViewTracking'
|
||||
@@ -27,6 +25,7 @@ import {InlineLinkText} from '#/components/Link'
|
||||
import {ListFooter} from '#/components/Lists'
|
||||
import {SearchError} from '#/components/SearchError'
|
||||
import {Text} from '#/components/Typography'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
|
||||
let SearchResults = ({
|
||||
query,
|
||||
@@ -43,14 +42,14 @@ let SearchResults = ({
|
||||
headerHeight: number
|
||||
initialPage?: number
|
||||
}): React.ReactNode => {
|
||||
const {_} = useLingui()
|
||||
const {t: l} = useLingui()
|
||||
|
||||
const sections = useMemo(() => {
|
||||
if (!queryWithParams) return []
|
||||
const noParams = queryWithParams === query
|
||||
return [
|
||||
{
|
||||
title: _(msg`Top`),
|
||||
title: l`Top`,
|
||||
component: (
|
||||
<SearchScreenPostResults
|
||||
query={queryWithParams}
|
||||
@@ -60,7 +59,7 @@ let SearchResults = ({
|
||||
),
|
||||
},
|
||||
{
|
||||
title: _(msg`Latest`),
|
||||
title: l`Latest`,
|
||||
component: (
|
||||
<SearchScreenPostResults
|
||||
query={queryWithParams}
|
||||
@@ -70,13 +69,13 @@ let SearchResults = ({
|
||||
),
|
||||
},
|
||||
noParams && {
|
||||
title: _(msg`People`),
|
||||
title: l`People`,
|
||||
component: (
|
||||
<SearchScreenUserResults query={query} active={activeTab === 2} />
|
||||
),
|
||||
},
|
||||
noParams && {
|
||||
title: _(msg`Feeds`),
|
||||
title: l`Feeds`,
|
||||
component: (
|
||||
<SearchScreenFeedsResults query={query} active={activeTab === 3} />
|
||||
),
|
||||
@@ -85,7 +84,10 @@ let SearchResults = ({
|
||||
title: string
|
||||
component: React.ReactNode
|
||||
}[]
|
||||
}, [_, query, queryWithParams, activeTab])
|
||||
}, [l, query, queryWithParams, activeTab])
|
||||
|
||||
// There may be fewer tabs after changing the search options.
|
||||
const selectedPage = initialPage > sections.length - 1 ? 0 : initialPage
|
||||
|
||||
return (
|
||||
<Pager
|
||||
@@ -95,7 +97,7 @@ let SearchResults = ({
|
||||
<TabBar items={sections.map(section => section.title)} {...props} />
|
||||
</Layout.Center>
|
||||
)}
|
||||
initialPage={initialPage}>
|
||||
initialPage={selectedPage}>
|
||||
{sections.map((section, i) => (
|
||||
<View key={i}>{section.component}</View>
|
||||
))}
|
||||
@@ -161,15 +163,15 @@ function EmptyState({
|
||||
|
||||
function NoResultsText({query}: {query: string}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const {t: l} = useLingui()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Text style={[a.text_lg, t.atoms.text_contrast_high]}>
|
||||
<Trans>
|
||||
No results found for "
|
||||
No results found for “
|
||||
<Text style={[a.text_lg, t.atoms.text, a.font_medium]}>{query}</Text>
|
||||
".
|
||||
”.
|
||||
</Trans>
|
||||
</Text>
|
||||
{'\n\n'}
|
||||
@@ -177,12 +179,10 @@ function NoResultsText({query}: {query: string}) {
|
||||
<Trans context="english-only-resource">
|
||||
Try a different search term, or{' '}
|
||||
<InlineLinkText
|
||||
label={_(
|
||||
msg({
|
||||
message: 'read about how to use search filters',
|
||||
context: 'english-only-resource',
|
||||
}),
|
||||
)}
|
||||
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]}>
|
||||
read about how to use search filters
|
||||
@@ -214,7 +214,7 @@ let SearchScreenPostResults = ({
|
||||
sort?: 'top' | 'latest'
|
||||
active: boolean
|
||||
}): React.ReactNode => {
|
||||
const {_} = useLingui()
|
||||
const {t: l} = useLingui()
|
||||
const {currentAccount, hasSession} = useSession()
|
||||
const [isPTR, setIsPTR] = useState(false)
|
||||
const trackPostView = usePostViewTracking('SearchResults')
|
||||
@@ -242,7 +242,7 @@ let SearchScreenPostResults = ({
|
||||
}, [setIsPTR, refetch])
|
||||
const onEndReached = useCallback(() => {
|
||||
if (isFetching || !hasNextPage || error) return
|
||||
fetchNextPage()
|
||||
void fetchNextPage()
|
||||
}, [isFetching, error, hasNextPage, fetchNextPage])
|
||||
|
||||
const posts = useMemo(() => {
|
||||
@@ -289,19 +289,15 @@ let SearchScreenPostResults = ({
|
||||
|
||||
if (!hasSession) {
|
||||
return (
|
||||
<SearchError
|
||||
title={_(msg`Search is currently unavailable when logged out`)}>
|
||||
<SearchError title={l`Search is currently unavailable when logged out`}>
|
||||
<Text style={[a.text_md, a.text_center, a.leading_snug]}>
|
||||
<Trans>
|
||||
<InlineLinkText
|
||||
label={_(msg`Sign in`)}
|
||||
to={'#'}
|
||||
onPress={showSignIn}>
|
||||
<InlineLinkText label={l`Sign in`} to={'#'} onPress={showSignIn}>
|
||||
Sign in
|
||||
</InlineLinkText>
|
||||
<Text style={t.atoms.text_contrast_medium}> or </Text>
|
||||
<InlineLinkText
|
||||
label={_(msg`Create an account`)}
|
||||
label={l`Create an account`}
|
||||
to={'#'}
|
||||
onPress={showCreateAccount}>
|
||||
create an account
|
||||
@@ -319,9 +315,7 @@ let SearchScreenPostResults = ({
|
||||
|
||||
return error ? (
|
||||
<EmptyState
|
||||
messageText={_(
|
||||
msg`We're sorry, but your search could not be completed. Please try again in a few minutes.`,
|
||||
)}
|
||||
messageText={l`We're sorry, but your search could not be completed. Please try again in a few minutes.`}
|
||||
error={cleanError(error)}
|
||||
/>
|
||||
) : (
|
||||
@@ -331,18 +325,20 @@ let SearchScreenPostResults = ({
|
||||
{posts.length ? (
|
||||
<List
|
||||
data={items}
|
||||
renderItem={({item}) => {
|
||||
renderItem={({item}: {item: SearchResultSlice}) => {
|
||||
if (item.type === 'post') {
|
||||
return <Post post={item.post} />
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}}
|
||||
keyExtractor={item => item.key}
|
||||
keyExtractor={(item: SearchResultSlice) => item.key}
|
||||
refreshing={isPTR}
|
||||
onRefresh={onPullToRefresh}
|
||||
onRefresh={() => {
|
||||
void onPullToRefresh()
|
||||
}}
|
||||
onEndReached={onEndReached}
|
||||
onItemSeen={item => {
|
||||
onItemSeen={(item: SearchResultSlice) => {
|
||||
if (item.type === 'post') {
|
||||
trackPostView(item.post)
|
||||
}
|
||||
@@ -374,7 +370,7 @@ let SearchScreenUserResults = ({
|
||||
query: string
|
||||
active: boolean
|
||||
}): React.ReactNode => {
|
||||
const {_} = useLingui()
|
||||
const {t: l} = useLingui()
|
||||
const {hasSession} = useSession()
|
||||
const [isPTR, setIsPTR] = useState(false)
|
||||
|
||||
@@ -400,7 +396,7 @@ let SearchScreenUserResults = ({
|
||||
const onEndReached = useCallback(() => {
|
||||
if (!hasSession) return
|
||||
if (isFetching || !hasNextPage || error) return
|
||||
fetchNextPage()
|
||||
void fetchNextPage()
|
||||
}, [isFetching, error, hasNextPage, fetchNextPage, hasSession])
|
||||
|
||||
const profiles = useMemo(() => {
|
||||
@@ -410,9 +406,7 @@ let SearchScreenUserResults = ({
|
||||
if (error) {
|
||||
return (
|
||||
<EmptyState
|
||||
messageText={_(
|
||||
msg`We're sorry, but your search could not be completed. Please try again in a few minutes.`,
|
||||
)}
|
||||
messageText={l`We’re sorry, but your search could not be completed. Please try again in a few minutes.`}
|
||||
error={error.toString()}
|
||||
/>
|
||||
)
|
||||
@@ -423,10 +417,12 @@ let SearchScreenUserResults = ({
|
||||
{profiles.length ? (
|
||||
<List
|
||||
data={profiles}
|
||||
renderItem={({item}) => <ProfileCardWithFollowBtn profile={item} />}
|
||||
keyExtractor={item => item.did}
|
||||
renderItem={({item}: {item: bsky.profile.AnyProfileView}) => (
|
||||
<ProfileCardWithFollowBtn profile={item} />
|
||||
)}
|
||||
keyExtractor={(item: bsky.profile.AnyProfileView) => item.did}
|
||||
refreshing={isPTR}
|
||||
onRefresh={onPullToRefresh}
|
||||
onRefresh={() => void onPullToRefresh()}
|
||||
onEndReached={onEndReached}
|
||||
desktopFixedHeight
|
||||
ListFooterComponent={
|
||||
@@ -465,7 +461,7 @@ let SearchScreenFeedsResults = ({
|
||||
{results.length ? (
|
||||
<List
|
||||
data={results}
|
||||
renderItem={({item}) => (
|
||||
renderItem={({item}: {item: AppBskyFeedDefs.GeneratorView}) => (
|
||||
<View
|
||||
style={[
|
||||
a.border_t,
|
||||
@@ -476,7 +472,7 @@ let SearchScreenFeedsResults = ({
|
||||
<FeedCard.Default view={item} />
|
||||
</View>
|
||||
)}
|
||||
keyExtractor={item => item.uri}
|
||||
keyExtractor={(item: AppBskyFeedDefs.GeneratorView) => item.uri}
|
||||
desktopFixedHeight
|
||||
ListFooterComponent={<ListFooter />}
|
||||
/>
|
||||
|
||||
@@ -12,9 +12,7 @@ import {
|
||||
View,
|
||||
type ViewStyle,
|
||||
} from 'react-native'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Trans} from '@lingui/react/macro'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
import {useFocusEffect, useNavigation, useRoute} from '@react-navigation/native'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
@@ -49,6 +47,23 @@ import {SearchLanguageDropdown} from './components/SearchLanguageDropdown'
|
||||
import {Explore} from './Explore'
|
||||
import {SearchResults} from './SearchResults'
|
||||
|
||||
type TabParam = 'user' | 'profile' | 'feed' | 'latest'
|
||||
|
||||
// Map tab parameter to tab index
|
||||
function getTabIndex(tabParam?: TabParam) {
|
||||
switch (tabParam) {
|
||||
case 'feed':
|
||||
return 3 // Feeds tab
|
||||
case 'user':
|
||||
case 'profile':
|
||||
return 2 // People tab
|
||||
case 'latest':
|
||||
return 1 // Latest tab
|
||||
default:
|
||||
return 0 // Top tab
|
||||
}
|
||||
}
|
||||
|
||||
export function SearchScreenShell({
|
||||
queryParam,
|
||||
testID,
|
||||
@@ -69,11 +84,15 @@ export function SearchScreenShell({
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const route = useRoute()
|
||||
const textInput = useRef<TextInput>(null)
|
||||
const {_} = useLingui()
|
||||
const {t: l} = useLingui()
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const {currentAccount} = useSession()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
// Get tab parameter from route params
|
||||
const tabParam = (route.params as {q?: string; tab?: TabParam})?.tab
|
||||
const [activeTab, setActiveTab] = useState(() => getTabIndex(tabParam))
|
||||
|
||||
// Query terms
|
||||
const [searchText, setSearchText] = useState<string>(queryParam)
|
||||
const {data: autocompleteData, isFetching: isAutocompleteFetching} =
|
||||
@@ -96,7 +115,7 @@ export function SearchScreenShell({
|
||||
})
|
||||
|
||||
const updateSearchHistory = useCallback(
|
||||
async (item: string) => {
|
||||
(item: string) => {
|
||||
if (!item) return
|
||||
const newSearchHistory = [
|
||||
item,
|
||||
@@ -108,7 +127,7 @@ export function SearchScreenShell({
|
||||
)
|
||||
|
||||
const updateProfileHistory = useCallback(
|
||||
async (item: bsky.profile.AnyProfileView) => {
|
||||
(item: bsky.profile.AnyProfileView) => {
|
||||
const newAccountHistory = [
|
||||
item.did,
|
||||
...accountHistory.filter(p => p !== item.did),
|
||||
@@ -119,13 +138,13 @@ export function SearchScreenShell({
|
||||
)
|
||||
|
||||
const deleteSearchHistoryItem = useCallback(
|
||||
async (item: string) => {
|
||||
(item: string) => {
|
||||
setTermHistory(termHistory.filter(search => search !== item))
|
||||
},
|
||||
[termHistory, setTermHistory],
|
||||
)
|
||||
const deleteProfileHistoryItem = useCallback(
|
||||
async (item: bsky.profile.AnyProfileView) => {
|
||||
(item: bsky.profile.AnyProfileView) => {
|
||||
setAccountHistory(accountHistory.filter(p => p !== item.did))
|
||||
},
|
||||
[accountHistory, setAccountHistory],
|
||||
@@ -162,7 +181,7 @@ export function SearchScreenShell({
|
||||
textInput.current?.focus()
|
||||
}, [])
|
||||
|
||||
const onChangeText = useCallback(async (text: string) => {
|
||||
const onChangeText = useCallback((text: string) => {
|
||||
scrollToTopWeb()
|
||||
setSearchText(text)
|
||||
}, [])
|
||||
@@ -277,7 +296,7 @@ export function SearchScreenShell({
|
||||
}, [setShowAutocomplete])
|
||||
|
||||
const focusSearchInput = useCallback(
|
||||
(tab?: 'user' | 'profile' | 'feed') => {
|
||||
(tab?: TabParam) => {
|
||||
textInput.current?.focus()
|
||||
|
||||
// If a tab is specified, set the tab parameter
|
||||
@@ -350,15 +369,14 @@ export function SearchScreenShell({
|
||||
onClearText={onPressClearQuery}
|
||||
onSubmitEditing={onSubmit}
|
||||
placeholder={
|
||||
inputPlaceholder ??
|
||||
_(msg`Search for posts, users, or feeds`)
|
||||
inputPlaceholder ?? l`Search for posts, users, or feeds`
|
||||
}
|
||||
hitSlop={{...HITSLOP_20, top: 0}}
|
||||
/>
|
||||
</View>
|
||||
{showAutocomplete && (
|
||||
<Button
|
||||
label={_(msg`Cancel search`)}
|
||||
label={l`Cancel search`}
|
||||
size="large"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
@@ -423,6 +441,9 @@ export function SearchScreenShell({
|
||||
flex: 1,
|
||||
}}>
|
||||
<SearchScreenInner
|
||||
key={params.lang}
|
||||
activeTab={activeTab}
|
||||
setActiveTab={setActiveTab}
|
||||
query={query}
|
||||
queryWithParams={queryWithParams}
|
||||
headerHeight={headerHeight}
|
||||
@@ -434,57 +455,31 @@ export function SearchScreenShell({
|
||||
}
|
||||
|
||||
let SearchScreenInner = ({
|
||||
activeTab,
|
||||
setActiveTab,
|
||||
query,
|
||||
queryWithParams,
|
||||
headerHeight,
|
||||
focusSearchInput,
|
||||
}: {
|
||||
activeTab: number
|
||||
setActiveTab: React.Dispatch<React.SetStateAction<number>>
|
||||
query: string
|
||||
queryWithParams: string
|
||||
headerHeight: number
|
||||
focusSearchInput: (tab?: 'user' | 'profile' | 'feed') => void
|
||||
focusSearchInput: (tab?: TabParam) => void
|
||||
}): React.ReactNode => {
|
||||
const t = useTheme()
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const {hasSession} = useSession()
|
||||
const {gtTablet} = useBreakpoints()
|
||||
const route = useRoute()
|
||||
|
||||
// Get tab parameter from route params
|
||||
const tabParam = (
|
||||
route.params as {q?: string; tab?: 'user' | 'profile' | 'feed'}
|
||||
)?.tab
|
||||
|
||||
// Map tab parameter to tab index
|
||||
const getInitialTabIndex = useCallback(() => {
|
||||
if (!tabParam) return 0
|
||||
switch (tabParam) {
|
||||
case 'user':
|
||||
case 'profile':
|
||||
return 2 // People tab
|
||||
case 'feed':
|
||||
return 3 // Feeds tab
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}, [tabParam])
|
||||
|
||||
const [activeTab, setActiveTab] = useState(getInitialTabIndex())
|
||||
|
||||
// Update activeTab when tabParam changes
|
||||
useLayoutEffect(() => {
|
||||
const newTabIndex = getInitialTabIndex()
|
||||
if (newTabIndex !== activeTab) {
|
||||
setActiveTab(newTabIndex)
|
||||
}
|
||||
}, [tabParam, activeTab, getInitialTabIndex])
|
||||
|
||||
const onPageSelected = useCallback(
|
||||
(index: number) => {
|
||||
setMinimalShellMode(false)
|
||||
setActiveTab(index)
|
||||
},
|
||||
[setMinimalShellMode],
|
||||
[setActiveTab, setMinimalShellMode],
|
||||
)
|
||||
|
||||
return queryWithParams ? (
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {View} from 'react-native'
|
||||
import {type AppBskyActorDefs} from '@atproto/api'
|
||||
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import * as ProfileCard from '#/components/ProfileCard'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
|
||||
export function ProfileCardWithFollowBtn({
|
||||
profile,
|
||||
@@ -12,7 +12,7 @@ export function ProfileCardWithFollowBtn({
|
||||
position,
|
||||
contextProfileDid,
|
||||
}: {
|
||||
profile: AppBskyActorDefs.ProfileView
|
||||
profile: bsky.profile.AnyProfileView
|
||||
noBorder?: boolean
|
||||
logContext?: 'ProfileCard' | 'StarterPackProfilesList'
|
||||
position?: number
|
||||
|
||||
Reference in New Issue
Block a user