Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Screen for searching user's posts (#7622)

* search user's posts screen

* custom placeholder copy if self

* navigate to /profile/:handle

* add name to title

* show header on desktop

authored by

Samuel Newman and committed by
GitHub
1e3b7fec ab4be7a9

+126 -15
+1
bskyweb/cmd/bskyweb/server.go
··· 283 283 e.GET("/profile/:handleOrDID/follows", server.WebGeneric) 284 284 e.GET("/profile/:handleOrDID/followers", server.WebGeneric) 285 285 e.GET("/profile/:handleOrDID/known-followers", server.WebGeneric) 286 + e.GET("/profile/:handleOrDID/search", server.WebGeneric) 286 287 e.GET("/profile/:handleOrDID/lists/:rkey", server.WebGeneric) 287 288 e.GET("/profile/:handleOrDID/feed/:rkey", server.WebGeneric) 288 289 e.GET("/profile/:handleOrDID/feed/:rkey/liked-by", server.WebGeneric)
+8
src/Navigation.tsx
··· 91 91 import {useTheme} from '#/alf' 92 92 import {router} from '#/routes' 93 93 import {Referrer} from '../modules/expo-bluesky-swiss-army' 94 + import {ProfileSearchScreen} from './screens/Profile/ProfileSearch' 94 95 import {AboutSettingsScreen} from './screens/Settings/AboutSettings' 95 96 import {AccessibilitySettingsScreen} from './screens/Settings/AccessibilitySettings' 96 97 import {AccountSettingsScreen} from './screens/Settings/AccountSettings' ··· 206 207 name="ProfileList" 207 208 getComponent={() => ProfileListScreen} 208 209 options={{title: title(msg`List`), requireAuth: true}} 210 + /> 211 + <Stack.Screen 212 + name="ProfileSearch" 213 + getComponent={() => ProfileSearchScreen} 214 + options={({route}) => ({ 215 + title: title(msg`Search @${route.params.name}'s posts`), 216 + })} 209 217 /> 210 218 <Stack.Screen 211 219 name="PostThread"
+1
src/lib/routes/types.ts
··· 18 18 ProfileFollowers: {name: string} 19 19 ProfileFollows: {name: string} 20 20 ProfileKnownFollowers: {name: string} 21 + ProfileSearch: {name: string; q?: string} 21 22 ProfileList: {name: string; rkey: string} 22 23 PostThread: {name: string; rkey: string} 23 24 PostLikedBy: {name: string; rkey: string}
+1
src/routes.ts
··· 19 19 ProfileFollowers: '/profile/:name/followers', 20 20 ProfileFollows: '/profile/:name/follows', 21 21 ProfileKnownFollowers: '/profile/:name/known-followers', 22 + ProfileSearch: '/profile/:name/search', 22 23 ProfileList: '/profile/:name/lists/:rkey', 23 24 PostThread: '/profile/:name/post/:rkey', 24 25 PostLikedBy: '/profile/:name/post/:rkey/liked-by',
+42
src/screens/Profile/ProfileSearch.tsx
··· 1 + import {useMemo} from 'react' 2 + import {msg} from '@lingui/macro' 3 + import {useLingui} from '@lingui/react' 4 + 5 + import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' 6 + import {useProfileQuery} from '#/state/queries/profile' 7 + import {useResolveDidQuery} from '#/state/queries/resolve-uri' 8 + import {useSession} from '#/state/session' 9 + import {SearchScreenShell} from '#/view/screens/Search/Search' 10 + 11 + type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileSearch'> 12 + export const ProfileSearchScreen = ({route}: Props) => { 13 + const {name, q: queryParam = ''} = route.params 14 + const {_} = useLingui() 15 + const {currentAccount} = useSession() 16 + 17 + const {data: resolvedDid} = useResolveDidQuery(name) 18 + const {data: profile} = useProfileQuery({did: resolvedDid}) 19 + 20 + const fixedParams = useMemo( 21 + () => ({ 22 + from: profile?.handle ?? name, 23 + }), 24 + [profile?.handle, name], 25 + ) 26 + 27 + return ( 28 + <SearchScreenShell 29 + navButton="back" 30 + inputPlaceholder={ 31 + profile 32 + ? currentAccount?.did === profile.did 33 + ? _(msg`Search my posts`) 34 + : _(msg`Search @${profile.handle}'s posts`) 35 + : _(msg`Search...`) 36 + } 37 + fixedParams={fixedParams} 38 + queryParam={queryParam} 39 + testID="searchPostsScreen" 40 + /> 41 + ) 42 + }
+17
src/view/com/profile/ProfileMenu.tsx
··· 2 2 import {AppBskyActorDefs} from '@atproto/api' 3 3 import {msg, Trans} from '@lingui/macro' 4 4 import {useLingui} from '@lingui/react' 5 + import {useNavigation} from '@react-navigation/native' 5 6 import {useQueryClient} from '@tanstack/react-query' 6 7 7 8 import {HITSLOP_20} from '#/lib/constants' 8 9 import {makeProfileLink} from '#/lib/routes/links' 10 + import {NavigationProp} from '#/lib/routes/types' 9 11 import {shareText, shareUrl} from '#/lib/sharing' 10 12 import {toShareUrl} from '#/lib/strings/url-helpers' 11 13 import {logger} from '#/logger' ··· 26 28 import {DotGrid_Stroke2_Corner0_Rounded as Ellipsis} from '#/components/icons/DotGrid' 27 29 import {Flag_Stroke2_Corner0_Rounded as Flag} from '#/components/icons/Flag' 28 30 import {ListSparkle_Stroke2_Corner0_Rounded as List} from '#/components/icons/ListSparkle' 31 + import {MagnifyingGlass2_Stroke2_Corner0_Rounded as SearchIcon} from '#/components/icons/MagnifyingGlass2' 29 32 import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute' 30 33 import {PeopleRemove2_Stroke2_Corner0_Rounded as UserMinus} from '#/components/icons/PeopleRemove2' 31 34 import { ··· 48 51 const {openModal} = useModalControls() 49 52 const reportDialogControl = useReportDialogControl() 50 53 const queryClient = useQueryClient() 54 + const navigation = useNavigation<NavigationProp>() 51 55 const isSelf = currentAccount?.did === profile.did 52 56 const isFollowing = profile.viewer?.following 53 57 const isBlocked = profile.viewer?.blocking || profile.viewer?.blockedBy ··· 177 181 shareText(profile.did) 178 182 }, [profile.did]) 179 183 184 + const onPressSearch = React.useCallback(() => { 185 + navigation.navigate('ProfileSearch', {name: profile.handle}) 186 + }, [navigation, profile.handle]) 187 + 180 188 return ( 181 189 <EventStopper onKeyDown={false}> 182 190 <Menu.Root> ··· 214 222 <Trans>Share</Trans> 215 223 </Menu.ItemText> 216 224 <Menu.ItemIcon icon={Share} /> 225 + </Menu.Item> 226 + <Menu.Item 227 + testID="profileHeaderDropdownSearchBtn" 228 + label={_(msg`Search Posts`)} 229 + onPress={onPressSearch}> 230 + <Menu.ItemText> 231 + <Trans>Search Posts</Trans> 232 + </Menu.ItemText> 233 + <Menu.ItemIcon icon={SearchIcon} /> 217 234 </Menu.Item> 218 235 </Menu.Group> 219 236
+56 -15
src/view/screens/Search/Search.tsx
··· 17 17 } from '@fortawesome/react-native-fontawesome' 18 18 import {msg, Trans} from '@lingui/macro' 19 19 import {useLingui} from '@lingui/react' 20 - import {useFocusEffect, useNavigation} from '@react-navigation/native' 20 + import {useFocusEffect, useNavigation, useRoute} from '@react-navigation/native' 21 21 22 22 import {APP_LANGUAGES, LANGUAGES} from '#/lib/../locale/languages' 23 23 import {createHitslop, HITSLOP_20} from '#/lib/constants' ··· 55 55 import {Text} from '#/view/com/util/text/Text' 56 56 import {Explore} from '#/view/screens/Search/Explore' 57 57 import {SearchLinkCard, SearchProfileCard} from '#/view/shell/desktop/Search' 58 - import {makeSearchQuery, parseSearchQuery} from '#/screens/Search/utils' 58 + import {makeSearchQuery, Params, parseSearchQuery} from '#/screens/Search/utils' 59 59 import { 60 60 atoms as a, 61 61 native, ··· 419 419 ) 420 420 } 421 421 422 - function useQueryManager({initialQuery}: {initialQuery: string}) { 422 + function useQueryManager({ 423 + initialQuery, 424 + fixedParams, 425 + }: { 426 + initialQuery: string 427 + fixedParams?: Params 428 + }) { 423 429 const {query, params: initialParams} = React.useMemo(() => { 424 430 return parseSearchQuery(initialQuery || '') 425 431 }, [initialQuery]) ··· 438 444 ...initialParams, 439 445 // managed stuff 440 446 lang, 447 + ...fixedParams, 441 448 }), 442 - [lang, initialParams], 449 + [lang, initialParams, fixedParams], 443 450 ) 444 451 const handlers = React.useMemo( 445 452 () => ({ ··· 588 595 export function SearchScreen( 589 596 props: NativeStackScreenProps<SearchTabNavigatorParams, 'Search'>, 590 597 ) { 598 + const queryParam = props.route?.params?.q ?? '' 599 + 600 + return <SearchScreenShell queryParam={queryParam} testID="searchScreen" /> 601 + } 602 + 603 + export function SearchScreenShell({ 604 + queryParam, 605 + testID, 606 + fixedParams, 607 + navButton = 'menu', 608 + inputPlaceholder, 609 + }: { 610 + queryParam: string 611 + testID: string 612 + fixedParams?: Params 613 + navButton?: 'back' | 'menu' 614 + inputPlaceholder?: string 615 + }) { 591 616 const t = useTheme() 592 617 const {gtMobile} = useBreakpoints() 593 618 const navigation = useNavigation<NavigationProp>() 619 + const route = useRoute() 594 620 const textInput = React.useRef<TextInput>(null) 595 621 const {_} = useLingui() 596 622 const setMinimalShellMode = useSetMinimalShellMode() 597 623 const {currentAccount} = useSession() 598 624 599 625 // Query terms 600 - const queryParam = props.route?.params?.q ?? '' 601 626 const [searchText, setSearchText] = React.useState<string>(queryParam) 602 627 const {data: autocompleteData, isFetching: isAutocompleteFetching} = 603 628 useActorAutocompleteQuery(searchText, true) ··· 656 681 657 682 const {params, query, queryWithParams} = useQueryManager({ 658 683 initialQuery: queryParam, 684 + fixedParams, 659 685 }) 660 686 const showFilters = Boolean(queryWithParams && !showAutocomplete) 661 687 ··· 696 722 updateSearchHistory(item) 697 723 698 724 if (isWeb) { 699 - navigation.push('Search', {q: item}) 725 + // @ts-expect-error route is not typesafe 726 + navigation.push(route.name, {...route.params, q: item}) 700 727 } else { 701 728 textInput.current?.blur() 702 729 navigation.setParams({q: item}) 703 730 } 704 731 }, 705 - [updateSearchHistory, navigation], 732 + [updateSearchHistory, navigation, route], 706 733 ) 707 734 708 735 const onPressCancelSearch = React.useCallback(() => { ··· 751 778 const onSoftReset = React.useCallback(() => { 752 779 if (isWeb) { 753 780 // Empty params resets the URL to be /search rather than /search?q= 754 - navigation.replace('Search', {}) 781 + // eslint-disable-next-line @typescript-eslint/no-unused-vars 782 + const {q: _q, ...parameters} = (route.params ?? {}) as { 783 + [key: string]: string 784 + } 785 + // @ts-expect-error route is not typesafe 786 + navigation.replace(route.name, parameters) 755 787 } else { 756 788 setSearchText('') 757 789 navigation.setParams({q: ''}) 758 790 textInput.current?.focus() 759 791 } 760 - }, [navigation]) 792 + }, [navigation, route]) 761 793 762 794 useFocusEffect( 763 795 React.useCallback(() => { ··· 778 810 } 779 811 }, [setShowAutocomplete]) 780 812 813 + const showHeader = !gtMobile || navButton !== 'menu' 814 + 781 815 return ( 782 - <Layout.Screen testID="searchScreen"> 816 + <Layout.Screen testID={testID}> 783 817 <View 784 818 ref={headerRef} 785 819 onLayout={evt => { ··· 794 828 }), 795 829 ]}> 796 830 <Layout.Center style={t.atoms.bg}> 797 - {!gtMobile && ( 831 + {showHeader && ( 798 832 <View 799 833 // HACK: shift up search input. we can't remove the top padding 800 834 // on the search input because it messes up the layout animation 801 835 // if we add it only when the header is hidden 802 836 style={{marginBottom: tokens.space.xs * -1}}> 803 837 <Layout.Header.Outer noBottomBorder> 804 - <Layout.Header.MenuButton /> 838 + {navButton === 'menu' ? ( 839 + <Layout.Header.MenuButton /> 840 + ) : ( 841 + <Layout.Header.BackButton /> 842 + )} 805 843 <Layout.Header.Content align="left"> 806 844 <Layout.Header.TitleText> 807 845 <Trans>Search</Trans> ··· 829 867 onChangeText={onChangeText} 830 868 onClearText={onPressClearQuery} 831 869 onSubmitEditing={onSubmit} 832 - placeholder={_(msg`Search for posts, users, or feeds`)} 870 + placeholder={ 871 + inputPlaceholder ?? 872 + _(msg`Search for posts, users, or feeds`) 873 + } 833 874 hitSlop={{...HITSLOP_20, top: 0}} 834 875 /> 835 876 </View> ··· 849 890 )} 850 891 </View> 851 892 852 - {showFilters && gtMobile && ( 893 + {showFilters && !showHeader && ( 853 894 <View 854 895 style={[ 855 896 a.flex_row, ··· 870 911 871 912 <View 872 913 style={{ 873 - display: showAutocomplete ? 'flex' : 'none', 914 + display: showAutocomplete && !fixedParams ? 'flex' : 'none', 874 915 flex: 1, 875 916 }}> 876 917 {searchText.length > 0 ? (