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
This commit is contained in:
Samuel Newman
2025-02-14 23:22:32 +00:00
committed by GitHub
parent ab4be7a9e1
commit 1e3b7feccf
7 changed files with 126 additions and 15 deletions
+1
View File
@@ -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)
+8
View File
@@ -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}}
/>
<Stack.Screen
name="ProfileSearch"
getComponent={() => ProfileSearchScreen}
options={({route}) => ({
title: title(msg`Search @${route.params.name}'s posts`),
})}
/>
<Stack.Screen
name="PostThread"
getComponent={() => PostThreadScreen}
+1
View File
@@ -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}
+1
View File
@@ -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',
+42
View File
@@ -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"
/>
)
}
+17
View File
@@ -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 && (
+56 -15
View File
@@ -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 ? (