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:
@@ -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)
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
Reference in New Issue
Block a user