From 1e3b7feccfb59c70a97ed51b26bee8c6ea33348f Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Fri, 14 Feb 2025 23:22:32 +0000 Subject: 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 --- bskyweb/cmd/bskyweb/server.go | 1 + src/Navigation.tsx | 8 ++++ src/lib/routes/types.ts | 1 + src/routes.ts | 1 + src/screens/Profile/ProfileSearch.tsx | 42 +++++++++++++++++++++ src/view/com/profile/ProfileMenu.tsx | 17 +++++++++ src/view/screens/Search/Search.tsx | 71 +++++++++++++++++++++++++++-------- 7 files changed, 126 insertions(+), 15 deletions(-) create mode 100644 src/screens/Profile/ProfileSearch.tsx diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go index 8792f553b..6543b33a8 100644 --- a/bskyweb/cmd/bskyweb/server.go +++ b/bskyweb/cmd/bskyweb/server.go @@ -283,6 +283,7 @@ func serve(cctx *cli.Context) error { e.GET("/profile/:handleOrDID/follows", server.WebGeneric) e.GET("/profile/:handleOrDID/followers", server.WebGeneric) e.GET("/profile/:handleOrDID/known-followers", server.WebGeneric) + e.GET("/profile/:handleOrDID/search", server.WebGeneric) e.GET("/profile/:handleOrDID/lists/:rkey", server.WebGeneric) e.GET("/profile/:handleOrDID/feed/:rkey", server.WebGeneric) e.GET("/profile/:handleOrDID/feed/:rkey/liked-by", server.WebGeneric) diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 709a7b9ff..cf09406a6 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -91,6 +91,7 @@ import {VideoFeed} from '#/screens/VideoFeed' import {useTheme} from '#/alf' import {router} from '#/routes' import {Referrer} from '../modules/expo-bluesky-swiss-army' +import {ProfileSearchScreen} from './screens/Profile/ProfileSearch' import {AboutSettingsScreen} from './screens/Settings/AboutSettings' import {AccessibilitySettingsScreen} from './screens/Settings/AccessibilitySettings' import {AccountSettingsScreen} from './screens/Settings/AccountSettings' @@ -207,6 +208,13 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) { getComponent={() => ProfileListScreen} options={{title: title(msg`List`), requireAuth: true}} /> + ProfileSearchScreen} + options={({route}) => ({ + title: title(msg`Search @${route.params.name}'s posts`), + })} + /> PostThreadScreen} diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index 8b69a66c4..51f196d09 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -18,6 +18,7 @@ export type CommonNavigatorParams = { ProfileFollowers: {name: string} ProfileFollows: {name: string} ProfileKnownFollowers: {name: string} + ProfileSearch: {name: string; q?: string} ProfileList: {name: string; rkey: string} PostThread: {name: string; rkey: string} PostLikedBy: {name: string; rkey: string} diff --git a/src/routes.ts b/src/routes.ts index 576ac92d1..568f88bb8 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -19,6 +19,7 @@ export const router = new Router({ ProfileFollowers: '/profile/:name/followers', ProfileFollows: '/profile/:name/follows', ProfileKnownFollowers: '/profile/:name/known-followers', + ProfileSearch: '/profile/:name/search', ProfileList: '/profile/:name/lists/:rkey', PostThread: '/profile/:name/post/:rkey', PostLikedBy: '/profile/:name/post/:rkey/liked-by', diff --git a/src/screens/Profile/ProfileSearch.tsx b/src/screens/Profile/ProfileSearch.tsx new file mode 100644 index 000000000..d91dc973e --- /dev/null +++ b/src/screens/Profile/ProfileSearch.tsx @@ -0,0 +1,42 @@ +import {useMemo} from 'react' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' +import {useProfileQuery} from '#/state/queries/profile' +import {useResolveDidQuery} from '#/state/queries/resolve-uri' +import {useSession} from '#/state/session' +import {SearchScreenShell} from '#/view/screens/Search/Search' + +type Props = NativeStackScreenProps +export const ProfileSearchScreen = ({route}: Props) => { + const {name, q: queryParam = ''} = route.params + const {_} = useLingui() + const {currentAccount} = useSession() + + const {data: resolvedDid} = useResolveDidQuery(name) + const {data: profile} = useProfileQuery({did: resolvedDid}) + + const fixedParams = useMemo( + () => ({ + from: profile?.handle ?? name, + }), + [profile?.handle, name], + ) + + return ( + + ) +} diff --git a/src/view/com/profile/ProfileMenu.tsx b/src/view/com/profile/ProfileMenu.tsx index 770d17f48..102b34922 100644 --- a/src/view/com/profile/ProfileMenu.tsx +++ b/src/view/com/profile/ProfileMenu.tsx @@ -2,10 +2,12 @@ import React, {memo} from 'react' import {AppBskyActorDefs} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {useNavigation} from '@react-navigation/native' import {useQueryClient} from '@tanstack/react-query' import {HITSLOP_20} from '#/lib/constants' import {makeProfileLink} from '#/lib/routes/links' +import {NavigationProp} from '#/lib/routes/types' import {shareText, shareUrl} from '#/lib/sharing' import {toShareUrl} from '#/lib/strings/url-helpers' import {logger} from '#/logger' @@ -26,6 +28,7 @@ import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons import {DotGrid_Stroke2_Corner0_Rounded as Ellipsis} from '#/components/icons/DotGrid' import {Flag_Stroke2_Corner0_Rounded as Flag} from '#/components/icons/Flag' import {ListSparkle_Stroke2_Corner0_Rounded as List} from '#/components/icons/ListSparkle' +import {MagnifyingGlass2_Stroke2_Corner0_Rounded as SearchIcon} from '#/components/icons/MagnifyingGlass2' import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute' import {PeopleRemove2_Stroke2_Corner0_Rounded as UserMinus} from '#/components/icons/PeopleRemove2' import { @@ -48,6 +51,7 @@ let ProfileMenu = ({ const {openModal} = useModalControls() const reportDialogControl = useReportDialogControl() const queryClient = useQueryClient() + const navigation = useNavigation() const isSelf = currentAccount?.did === profile.did const isFollowing = profile.viewer?.following const isBlocked = profile.viewer?.blocking || profile.viewer?.blockedBy @@ -177,6 +181,10 @@ let ProfileMenu = ({ shareText(profile.did) }, [profile.did]) + const onPressSearch = React.useCallback(() => { + navigation.navigate('ProfileSearch', {name: profile.handle}) + }, [navigation, profile.handle]) + return ( @@ -215,6 +223,15 @@ let ProfileMenu = ({ + + + Search Posts + + + {hasSession && ( diff --git a/src/view/screens/Search/Search.tsx b/src/view/screens/Search/Search.tsx index f16b4fff2..83503a706 100644 --- a/src/view/screens/Search/Search.tsx +++ b/src/view/screens/Search/Search.tsx @@ -17,7 +17,7 @@ import { } from '@fortawesome/react-native-fontawesome' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {useFocusEffect, useNavigation} from '@react-navigation/native' +import {useFocusEffect, useNavigation, useRoute} from '@react-navigation/native' import {APP_LANGUAGES, LANGUAGES} from '#/lib/../locale/languages' import {createHitslop, HITSLOP_20} from '#/lib/constants' @@ -55,7 +55,7 @@ import {List} from '#/view/com/util/List' import {Text} from '#/view/com/util/text/Text' import {Explore} from '#/view/screens/Search/Explore' import {SearchLinkCard, SearchProfileCard} from '#/view/shell/desktop/Search' -import {makeSearchQuery, parseSearchQuery} from '#/screens/Search/utils' +import {makeSearchQuery, Params, parseSearchQuery} from '#/screens/Search/utils' import { atoms as a, native, @@ -419,7 +419,13 @@ function SearchLanguageDropdown({ ) } -function useQueryManager({initialQuery}: {initialQuery: string}) { +function useQueryManager({ + initialQuery, + fixedParams, +}: { + initialQuery: string + fixedParams?: Params +}) { const {query, params: initialParams} = React.useMemo(() => { return parseSearchQuery(initialQuery || '') }, [initialQuery]) @@ -438,8 +444,9 @@ function useQueryManager({initialQuery}: {initialQuery: string}) { ...initialParams, // managed stuff lang, + ...fixedParams, }), - [lang, initialParams], + [lang, initialParams, fixedParams], ) const handlers = React.useMemo( () => ({ @@ -588,16 +595,34 @@ SearchScreenInner = React.memo(SearchScreenInner) export function SearchScreen( props: NativeStackScreenProps, ) { + const queryParam = props.route?.params?.q ?? '' + + return +} + +export function SearchScreenShell({ + queryParam, + testID, + fixedParams, + navButton = 'menu', + inputPlaceholder, +}: { + queryParam: string + testID: string + fixedParams?: Params + navButton?: 'back' | 'menu' + inputPlaceholder?: string +}) { const t = useTheme() const {gtMobile} = useBreakpoints() const navigation = useNavigation() + const route = useRoute() const textInput = React.useRef(null) const {_} = useLingui() const setMinimalShellMode = useSetMinimalShellMode() const {currentAccount} = useSession() // Query terms - const queryParam = props.route?.params?.q ?? '' const [searchText, setSearchText] = React.useState(queryParam) const {data: autocompleteData, isFetching: isAutocompleteFetching} = useActorAutocompleteQuery(searchText, true) @@ -656,6 +681,7 @@ export function SearchScreen( const {params, query, queryWithParams} = useQueryManager({ initialQuery: queryParam, + fixedParams, }) const showFilters = Boolean(queryWithParams && !showAutocomplete) @@ -696,13 +722,14 @@ export function SearchScreen( updateSearchHistory(item) if (isWeb) { - navigation.push('Search', {q: item}) + // @ts-expect-error route is not typesafe + navigation.push(route.name, {...route.params, q: item}) } else { textInput.current?.blur() navigation.setParams({q: item}) } }, - [updateSearchHistory, navigation], + [updateSearchHistory, navigation, route], ) const onPressCancelSearch = React.useCallback(() => { @@ -751,13 +778,18 @@ export function SearchScreen( const onSoftReset = React.useCallback(() => { if (isWeb) { // Empty params resets the URL to be /search rather than /search?q= - navigation.replace('Search', {}) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const {q: _q, ...parameters} = (route.params ?? {}) as { + [key: string]: string + } + // @ts-expect-error route is not typesafe + navigation.replace(route.name, parameters) } else { setSearchText('') navigation.setParams({q: ''}) textInput.current?.focus() } - }, [navigation]) + }, [navigation, route]) useFocusEffect( React.useCallback(() => { @@ -778,8 +810,10 @@ export function SearchScreen( } }, [setShowAutocomplete]) + const showHeader = !gtMobile || navButton !== 'menu' + return ( - + { @@ -794,14 +828,18 @@ export function SearchScreen( }), ]}> - {!gtMobile && ( + {showHeader && ( @@ -849,7 +890,7 @@ export function SearchScreen( )} - {showFilters && gtMobile && ( + {showFilters && !showHeader && ( {searchText.length > 0 ? ( -- cgit 1.4.1