Bluesky app fork with some witchin' additions 💫 witchsky.app
bluesky fork client

Fix infinite loading spinner when changing search terms (#9950)

Co-authored-by: Samuel Newman <mozzius@protonmail.com>

authored by

DS Boyce
Samuel Newman
and committed by
GitHub
8a1f8997 6cb88ce1

+83 -92
+41 -45
src/screens/Search/SearchResults.tsx
··· 1 1 import {memo, useCallback, useMemo, useState} from 'react' 2 2 import {ActivityIndicator, View} from 'react-native' 3 3 import {type AppBskyFeedDefs} from '@atproto/api' 4 - import {msg} from '@lingui/core/macro' 5 - import {useLingui} from '@lingui/react' 6 - import {Trans} from '@lingui/react/macro' 4 + import {Trans, useLingui} from '@lingui/react/macro' 7 5 8 6 import {urls} from '#/lib/constants' 9 7 import {usePostViewTracking} from '#/lib/hooks/usePostViewTracking' ··· 27 25 import {ListFooter} from '#/components/Lists' 28 26 import {SearchError} from '#/components/SearchError' 29 27 import {Text} from '#/components/Typography' 28 + import type * as bsky from '#/types/bsky' 30 29 31 30 let SearchResults = ({ 32 31 query, ··· 43 42 headerHeight: number 44 43 initialPage?: number 45 44 }): React.ReactNode => { 46 - const {_} = useLingui() 45 + const {t: l} = useLingui() 47 46 48 47 const sections = useMemo(() => { 49 48 if (!queryWithParams) return [] 50 49 const noParams = queryWithParams === query 51 50 return [ 52 51 { 53 - title: _(msg`Top`), 52 + title: l`Top`, 54 53 component: ( 55 54 <SearchScreenPostResults 56 55 query={queryWithParams} ··· 60 59 ), 61 60 }, 62 61 { 63 - title: _(msg`Latest`), 62 + title: l`Latest`, 64 63 component: ( 65 64 <SearchScreenPostResults 66 65 query={queryWithParams} ··· 70 69 ), 71 70 }, 72 71 noParams && { 73 - title: _(msg`People`), 72 + title: l`People`, 74 73 component: ( 75 74 <SearchScreenUserResults query={query} active={activeTab === 2} /> 76 75 ), 77 76 }, 78 77 noParams && { 79 - title: _(msg`Feeds`), 78 + title: l`Feeds`, 80 79 component: ( 81 80 <SearchScreenFeedsResults query={query} active={activeTab === 3} /> 82 81 ), ··· 85 84 title: string 86 85 component: React.ReactNode 87 86 }[] 88 - }, [_, query, queryWithParams, activeTab]) 87 + }, [l, query, queryWithParams, activeTab]) 88 + 89 + // There may be fewer tabs after changing the search options. 90 + const selectedPage = initialPage > sections.length - 1 ? 0 : initialPage 89 91 90 92 return ( 91 93 <Pager ··· 95 97 <TabBar items={sections.map(section => section.title)} {...props} /> 96 98 </Layout.Center> 97 99 )} 98 - initialPage={initialPage}> 100 + initialPage={selectedPage}> 99 101 {sections.map((section, i) => ( 100 102 <View key={i}>{section.component}</View> 101 103 ))} ··· 161 163 162 164 function NoResultsText({query}: {query: string}) { 163 165 const t = useTheme() 164 - const {_} = useLingui() 166 + const {t: l} = useLingui() 165 167 166 168 return ( 167 169 <> 168 170 <Text style={[a.text_lg, t.atoms.text_contrast_high]}> 169 171 <Trans> 170 - No results found for " 172 + No results found for “ 171 173 <Text style={[a.text_lg, t.atoms.text, a.font_medium]}>{query}</Text> 172 - ". 174 + ”. 173 175 </Trans> 174 176 </Text> 175 177 {'\n\n'} ··· 177 179 <Trans context="english-only-resource"> 178 180 Try a different search term, or{' '} 179 181 <InlineLinkText 180 - label={_( 181 - msg({ 182 - message: 'read about how to use search filters', 183 - context: 'english-only-resource', 184 - }), 185 - )} 182 + label={l({ 183 + message: 'read about how to use search filters', 184 + context: 'english-only-resource', 185 + })} 186 186 to={urls.website.blog.searchTipsAndTricks} 187 187 style={[a.text_md, a.leading_snug]}> 188 188 read about how to use search filters ··· 214 214 sort?: 'top' | 'latest' 215 215 active: boolean 216 216 }): React.ReactNode => { 217 - const {_} = useLingui() 217 + const {t: l} = useLingui() 218 218 const {currentAccount, hasSession} = useSession() 219 219 const [isPTR, setIsPTR] = useState(false) 220 220 const trackPostView = usePostViewTracking('SearchResults') ··· 242 242 }, [setIsPTR, refetch]) 243 243 const onEndReached = useCallback(() => { 244 244 if (isFetching || !hasNextPage || error) return 245 - fetchNextPage() 245 + void fetchNextPage() 246 246 }, [isFetching, error, hasNextPage, fetchNextPage]) 247 247 248 248 const posts = useMemo(() => { ··· 289 289 290 290 if (!hasSession) { 291 291 return ( 292 - <SearchError 293 - title={_(msg`Search is currently unavailable when logged out`)}> 292 + <SearchError title={l`Search is currently unavailable when logged out`}> 294 293 <Text style={[a.text_md, a.text_center, a.leading_snug]}> 295 294 <Trans> 296 - <InlineLinkText 297 - label={_(msg`Sign in`)} 298 - to={'#'} 299 - onPress={showSignIn}> 295 + <InlineLinkText label={l`Sign in`} to={'#'} onPress={showSignIn}> 300 296 Sign in 301 297 </InlineLinkText> 302 298 <Text style={t.atoms.text_contrast_medium}> or </Text> 303 299 <InlineLinkText 304 - label={_(msg`Create an account`)} 300 + label={l`Create an account`} 305 301 to={'#'} 306 302 onPress={showCreateAccount}> 307 303 create an account ··· 319 315 320 316 return error ? ( 321 317 <EmptyState 322 - messageText={_( 323 - msg`We're sorry, but your search could not be completed. Please try again in a few minutes.`, 324 - )} 318 + messageText={l`We're sorry, but your search could not be completed. Please try again in a few minutes.`} 325 319 error={cleanError(error)} 326 320 /> 327 321 ) : ( ··· 331 325 {posts.length ? ( 332 326 <List 333 327 data={items} 334 - renderItem={({item}) => { 328 + renderItem={({item}: {item: SearchResultSlice}) => { 335 329 if (item.type === 'post') { 336 330 return <Post post={item.post} /> 337 331 } else { 338 332 return null 339 333 } 340 334 }} 341 - keyExtractor={item => item.key} 335 + keyExtractor={(item: SearchResultSlice) => item.key} 342 336 refreshing={isPTR} 343 - onRefresh={onPullToRefresh} 337 + onRefresh={() => { 338 + void onPullToRefresh() 339 + }} 344 340 onEndReached={onEndReached} 345 - onItemSeen={item => { 341 + onItemSeen={(item: SearchResultSlice) => { 346 342 if (item.type === 'post') { 347 343 trackPostView(item.post) 348 344 } ··· 374 370 query: string 375 371 active: boolean 376 372 }): React.ReactNode => { 377 - const {_} = useLingui() 373 + const {t: l} = useLingui() 378 374 const {hasSession} = useSession() 379 375 const [isPTR, setIsPTR] = useState(false) 380 376 ··· 400 396 const onEndReached = useCallback(() => { 401 397 if (!hasSession) return 402 398 if (isFetching || !hasNextPage || error) return 403 - fetchNextPage() 399 + void fetchNextPage() 404 400 }, [isFetching, error, hasNextPage, fetchNextPage, hasSession]) 405 401 406 402 const profiles = useMemo(() => { ··· 410 406 if (error) { 411 407 return ( 412 408 <EmptyState 413 - messageText={_( 414 - msg`We're sorry, but your search could not be completed. Please try again in a few minutes.`, 415 - )} 409 + messageText={l`We’re sorry, but your search could not be completed. Please try again in a few minutes.`} 416 410 error={error.toString()} 417 411 /> 418 412 ) ··· 423 417 {profiles.length ? ( 424 418 <List 425 419 data={profiles} 426 - renderItem={({item}) => <ProfileCardWithFollowBtn profile={item} />} 427 - keyExtractor={item => item.did} 420 + renderItem={({item}: {item: bsky.profile.AnyProfileView}) => ( 421 + <ProfileCardWithFollowBtn profile={item} /> 422 + )} 423 + keyExtractor={(item: bsky.profile.AnyProfileView) => item.did} 428 424 refreshing={isPTR} 429 - onRefresh={onPullToRefresh} 425 + onRefresh={() => void onPullToRefresh()} 430 426 onEndReached={onEndReached} 431 427 desktopFixedHeight 432 428 ListFooterComponent={ ··· 465 461 {results.length ? ( 466 462 <List 467 463 data={results} 468 - renderItem={({item}) => ( 464 + renderItem={({item}: {item: AppBskyFeedDefs.GeneratorView}) => ( 469 465 <View 470 466 style={[ 471 467 a.border_t, ··· 476 472 <FeedCard.Default view={item} /> 477 473 </View> 478 474 )} 479 - keyExtractor={item => item.uri} 475 + keyExtractor={(item: AppBskyFeedDefs.GeneratorView) => item.uri} 480 476 desktopFixedHeight 481 477 ListFooterComponent={<ListFooter />} 482 478 />
+40 -45
src/screens/Search/Shell.tsx
··· 12 12 View, 13 13 type ViewStyle, 14 14 } from 'react-native' 15 - import {msg} from '@lingui/core/macro' 16 - import {useLingui} from '@lingui/react' 17 - import {Trans} from '@lingui/react/macro' 15 + import {Trans, useLingui} from '@lingui/react/macro' 18 16 import {useFocusEffect, useNavigation, useRoute} from '@react-navigation/native' 19 17 import {useQueryClient} from '@tanstack/react-query' 20 18 ··· 49 47 import {Explore} from './Explore' 50 48 import {SearchResults} from './SearchResults' 51 49 50 + type TabParam = 'user' | 'profile' | 'feed' | 'latest' 51 + 52 + // Map tab parameter to tab index 53 + function getTabIndex(tabParam?: TabParam) { 54 + switch (tabParam) { 55 + case 'feed': 56 + return 3 // Feeds tab 57 + case 'user': 58 + case 'profile': 59 + return 2 // People tab 60 + case 'latest': 61 + return 1 // Latest tab 62 + default: 63 + return 0 // Top tab 64 + } 65 + } 66 + 52 67 export function SearchScreenShell({ 53 68 queryParam, 54 69 testID, ··· 69 84 const navigation = useNavigation<NavigationProp>() 70 85 const route = useRoute() 71 86 const textInput = useRef<TextInput>(null) 72 - const {_} = useLingui() 87 + const {t: l} = useLingui() 73 88 const setMinimalShellMode = useSetMinimalShellMode() 74 89 const {currentAccount} = useSession() 75 90 const queryClient = useQueryClient() 91 + 92 + // Get tab parameter from route params 93 + const tabParam = (route.params as {q?: string; tab?: TabParam})?.tab 94 + const [activeTab, setActiveTab] = useState(() => getTabIndex(tabParam)) 76 95 77 96 // Query terms 78 97 const [searchText, setSearchText] = useState<string>(queryParam) ··· 96 115 }) 97 116 98 117 const updateSearchHistory = useCallback( 99 - async (item: string) => { 118 + (item: string) => { 100 119 if (!item) return 101 120 const newSearchHistory = [ 102 121 item, ··· 108 127 ) 109 128 110 129 const updateProfileHistory = useCallback( 111 - async (item: bsky.profile.AnyProfileView) => { 130 + (item: bsky.profile.AnyProfileView) => { 112 131 const newAccountHistory = [ 113 132 item.did, 114 133 ...accountHistory.filter(p => p !== item.did), ··· 119 138 ) 120 139 121 140 const deleteSearchHistoryItem = useCallback( 122 - async (item: string) => { 141 + (item: string) => { 123 142 setTermHistory(termHistory.filter(search => search !== item)) 124 143 }, 125 144 [termHistory, setTermHistory], 126 145 ) 127 146 const deleteProfileHistoryItem = useCallback( 128 - async (item: bsky.profile.AnyProfileView) => { 147 + (item: bsky.profile.AnyProfileView) => { 129 148 setAccountHistory(accountHistory.filter(p => p !== item.did)) 130 149 }, 131 150 [accountHistory, setAccountHistory], ··· 162 181 textInput.current?.focus() 163 182 }, []) 164 183 165 - const onChangeText = useCallback(async (text: string) => { 184 + const onChangeText = useCallback((text: string) => { 166 185 scrollToTopWeb() 167 186 setSearchText(text) 168 187 }, []) ··· 277 296 }, [setShowAutocomplete]) 278 297 279 298 const focusSearchInput = useCallback( 280 - (tab?: 'user' | 'profile' | 'feed') => { 299 + (tab?: TabParam) => { 281 300 textInput.current?.focus() 282 301 283 302 // If a tab is specified, set the tab parameter ··· 350 369 onClearText={onPressClearQuery} 351 370 onSubmitEditing={onSubmit} 352 371 placeholder={ 353 - inputPlaceholder ?? 354 - _(msg`Search for posts, users, or feeds`) 372 + inputPlaceholder ?? l`Search for posts, users, or feeds` 355 373 } 356 374 hitSlop={{...HITSLOP_20, top: 0}} 357 375 /> 358 376 </View> 359 377 {showAutocomplete && ( 360 378 <Button 361 - label={_(msg`Cancel search`)} 379 + label={l`Cancel search`} 362 380 size="large" 363 381 variant="ghost" 364 382 color="secondary" ··· 423 441 flex: 1, 424 442 }}> 425 443 <SearchScreenInner 444 + key={params.lang} 445 + activeTab={activeTab} 446 + setActiveTab={setActiveTab} 426 447 query={query} 427 448 queryWithParams={queryWithParams} 428 449 headerHeight={headerHeight} ··· 434 455 } 435 456 436 457 let SearchScreenInner = ({ 458 + activeTab, 459 + setActiveTab, 437 460 query, 438 461 queryWithParams, 439 462 headerHeight, 440 463 focusSearchInput, 441 464 }: { 465 + activeTab: number 466 + setActiveTab: React.Dispatch<React.SetStateAction<number>> 442 467 query: string 443 468 queryWithParams: string 444 469 headerHeight: number 445 - focusSearchInput: (tab?: 'user' | 'profile' | 'feed') => void 470 + focusSearchInput: (tab?: TabParam) => void 446 471 }): React.ReactNode => { 447 472 const t = useTheme() 448 473 const setMinimalShellMode = useSetMinimalShellMode() 449 474 const {hasSession} = useSession() 450 475 const {gtTablet} = useBreakpoints() 451 - const route = useRoute() 452 - 453 - // Get tab parameter from route params 454 - const tabParam = ( 455 - route.params as {q?: string; tab?: 'user' | 'profile' | 'feed'} 456 - )?.tab 457 - 458 - // Map tab parameter to tab index 459 - const getInitialTabIndex = useCallback(() => { 460 - if (!tabParam) return 0 461 - switch (tabParam) { 462 - case 'user': 463 - case 'profile': 464 - return 2 // People tab 465 - case 'feed': 466 - return 3 // Feeds tab 467 - default: 468 - return 0 469 - } 470 - }, [tabParam]) 471 - 472 - const [activeTab, setActiveTab] = useState(getInitialTabIndex()) 473 - 474 - // Update activeTab when tabParam changes 475 - useLayoutEffect(() => { 476 - const newTabIndex = getInitialTabIndex() 477 - if (newTabIndex !== activeTab) { 478 - setActiveTab(newTabIndex) 479 - } 480 - }, [tabParam, activeTab, getInitialTabIndex]) 481 476 482 477 const onPageSelected = useCallback( 483 478 (index: number) => { 484 479 setMinimalShellMode(false) 485 480 setActiveTab(index) 486 481 }, 487 - [setMinimalShellMode], 482 + [setActiveTab, setMinimalShellMode], 488 483 ) 489 484 490 485 return queryWithParams ? (
+2 -2
src/view/com/profile/ProfileCard.tsx
··· 1 1 import {View} from 'react-native' 2 - import {type AppBskyActorDefs} from '@atproto/api' 3 2 4 3 import {useModerationOpts} from '#/state/preferences/moderation-opts' 5 4 import {atoms as a, useTheme} from '#/alf' 6 5 import * as ProfileCard from '#/components/ProfileCard' 6 + import type * as bsky from '#/types/bsky' 7 7 8 8 export function ProfileCardWithFollowBtn({ 9 9 profile, ··· 12 12 position, 13 13 contextProfileDid, 14 14 }: { 15 - profile: AppBskyActorDefs.ProfileView 15 + profile: bsky.profile.AnyProfileView 16 16 noBorder?: boolean 17 17 logContext?: 'ProfileCard' | 'StarterPackProfilesList' 18 18 position?: number