about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--bskyweb/cmd/bskyweb/server.go1
-rw-r--r--src/Navigation.tsx8
-rw-r--r--src/lib/routes/types.ts1
-rw-r--r--src/routes.ts1
-rw-r--r--src/screens/Profile/ProfileSearch.tsx42
-rw-r--r--src/view/com/profile/ProfileMenu.tsx17
-rw-r--r--src/view/screens/Search/Search.tsx71
7 files changed, 126 insertions, 15 deletions
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'
@@ -208,6 +209,13 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
         options={{title: title(msg`List`), requireAuth: true}}
       />
       <Stack.Screen
+        name="ProfileSearch"
+        getComponent={() => ProfileSearchScreen}
+        options={({route}) => ({
+          title: title(msg`Search @${route.params.name}'s posts`),
+        })}
+      />
+      <Stack.Screen
         name="PostThread"
         getComponent={() => PostThreadScreen}
         options={({route}) => ({
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<CommonNavigatorParams, 'ProfileSearch'>
+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 (
+    <SearchScreenShell
+      navButton="back"
+      inputPlaceholder={
+        profile
+          ? currentAccount?.did === profile.did
+            ? _(msg`Search my posts`)
+            : _(msg`Search @${profile.handle}'s posts`)
+          : _(msg`Search...`)
+      }
+      fixedParams={fixedParams}
+      queryParam={queryParam}
+      testID="searchPostsScreen"
+    />
+  )
+}
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<NavigationProp>()
   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 (
     <EventStopper onKeyDown={false}>
       <Menu.Root>
@@ -215,6 +223,15 @@ let ProfileMenu = ({
               </Menu.ItemText>
               <Menu.ItemIcon icon={Share} />
             </Menu.Item>
+            <Menu.Item
+              testID="profileHeaderDropdownSearchBtn"
+              label={_(msg`Search Posts`)}
+              onPress={onPressSearch}>
+              <Menu.ItemText>
+                <Trans>Search Posts</Trans>
+              </Menu.ItemText>
+              <Menu.ItemIcon icon={SearchIcon} />
+            </Menu.Item>
           </Menu.Group>
 
           {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<SearchTabNavigatorParams, 'Search'>,
 ) {
+  const queryParam = props.route?.params?.q ?? ''
+
+  return <SearchScreenShell queryParam={queryParam} testID="searchScreen" />
+}
+
+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<NavigationProp>()
+  const route = useRoute()
   const textInput = React.useRef<TextInput>(null)
   const {_} = useLingui()
   const setMinimalShellMode = useSetMinimalShellMode()
   const {currentAccount} = useSession()
 
   // Query terms
-  const queryParam = props.route?.params?.q ?? ''
   const [searchText, setSearchText] = React.useState<string>(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 (
-    <Layout.Screen testID="searchScreen">
+    <Layout.Screen testID={testID}>
       <View
         ref={headerRef}
         onLayout={evt => {
@@ -794,14 +828,18 @@ export function SearchScreen(
           }),
         ]}>
         <Layout.Center style={t.atoms.bg}>
-          {!gtMobile && (
+          {showHeader && (
             <View
               // HACK: shift up search input. we can't remove the top padding
               // on the search input because it messes up the layout animation
               // if we add it only when the header is hidden
               style={{marginBottom: tokens.space.xs * -1}}>
               <Layout.Header.Outer noBottomBorder>
-                <Layout.Header.MenuButton />
+                {navButton === 'menu' ? (
+                  <Layout.Header.MenuButton />
+                ) : (
+                  <Layout.Header.BackButton />
+                )}
                 <Layout.Header.Content align="left">
                   <Layout.Header.TitleText>
                     <Trans>Search</Trans>
@@ -829,7 +867,10 @@ export function SearchScreen(
                     onChangeText={onChangeText}
                     onClearText={onPressClearQuery}
                     onSubmitEditing={onSubmit}
-                    placeholder={_(msg`Search for posts, users, or feeds`)}
+                    placeholder={
+                      inputPlaceholder ??
+                      _(msg`Search for posts, users, or feeds`)
+                    }
                     hitSlop={{...HITSLOP_20, top: 0}}
                   />
                 </View>
@@ -849,7 +890,7 @@ export function SearchScreen(
                 )}
               </View>
 
-              {showFilters && gtMobile && (
+              {showFilters && !showHeader && (
                 <View
                   style={[
                     a.flex_row,
@@ -870,7 +911,7 @@ export function SearchScreen(
 
       <View
         style={{
-          display: showAutocomplete ? 'flex' : 'none',
+          display: showAutocomplete && !fixedParams ? 'flex' : 'none',
           flex: 1,
         }}>
         {searchText.length > 0 ? (