Update advanced search UI (#11088)

This commit is contained in:
DS Boyce
2026-07-08 10:14:39 -07:00
committed by GitHub
parent decac43c34
commit 510dc51d7a
8 changed files with 75 additions and 75 deletions
-3
View File
@@ -671,9 +671,6 @@
} }
}, },
"src/components/forms/TextField.tsx": { "src/components/forms/TextField.tsx": {
"@typescript-eslint/no-unsafe-member-access": {
"count": 3
},
"react-hooks/refs": { "react-hooks/refs": {
"count": 1 "count": 1
} }
+1 -1
View File
@@ -207,7 +207,7 @@ export function createInput(Component: typeof TextInput) {
const refs = mergeRefs([ctx.inputRef, inputRef!].filter(Boolean)) const refs = mergeRefs([ctx.inputRef, inputRef!].filter(Boolean))
const flattened = StyleSheet.flatten([ const flattened = StyleSheet.flatten<TextStyle>([
a.relative, a.relative,
a.z_20, a.z_20,
a.flex_1, a.flex_1,
+40 -22
View File
@@ -540,6 +540,24 @@ export function SearchScreenShell({
value={filters.lang ?? ''} value={filters.lang ?? ''}
onChange={onChangeLang} onChange={onChangeLang}
/> />
) : showFilters && advancedSearchV2Enabled ? (
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
<AdvancedSearchDialog
disabled={activeTab > 1}
q={searchText}
filters={filters}
onSubmit={onSubmitAdvanced}
/>
<Button
accessibilityRole="button"
size="small"
color="secondary"
shape="round"
label={l`Share this search`}
onPress={onShareSearch}>
<ButtonIcon icon={ShareIcon} />
</Button>
</View>
) : ( ) : (
<Layout.Header.Slot /> <Layout.Header.Slot />
)} )}
@@ -558,19 +576,19 @@ export function SearchScreenShell({
/> />
)} )}
<View style={[a.w_full, a.flex_row, a.align_stretch, a.gap_xs]}> <View style={[a.w_full, a.flex_row, a.align_stretch, a.gap_sm]}>
<View style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}> <View style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
{showAutocomplete && ( {showAutocomplete && (
<Button <Button
label={l`Cancel search`} label={l`Cancel search`}
size="large" size="small"
variant="ghost" variant="ghost"
color="secondary" color="secondary"
shape="round" shape="round"
style={[a.px_sm]} style={[a.px_sm]}
onPress={onPressCancelSearch} onPress={onPressCancelSearch}
hitSlop={HITSLOP_10}> hitSlop={HITSLOP_10}>
<ButtonIcon icon={ArrowLeftIcon} /> <ButtonIcon icon={ArrowLeftIcon} size="lg" />
</Button> </Button>
)} )}
<View style={[a.flex_1]}> <View style={[a.flex_1]}>
@@ -591,6 +609,25 @@ export function SearchScreenShell({
/> />
</View> </View>
</View> </View>
{showFilters && !showHeader && advancedSearchV2Enabled ? (
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
<AdvancedSearchDialog
disabled={activeTab > 1}
q={searchText}
filters={filters}
onSubmit={onSubmitAdvanced}
/>
<Button
accessibilityRole="button"
size="small"
color="secondary"
shape="round"
label={l`Share this search`}
onPress={onShareSearch}>
<ButtonIcon icon={ShareIcon} />
</Button>
</View>
) : null}
</View> </View>
{showFilters && !showHeader && !advancedSearchV2Enabled && ( {showFilters && !showHeader && !advancedSearchV2Enabled && (
@@ -601,25 +638,6 @@ export function SearchScreenShell({
/> />
</View> </View>
)} )}
{showFilters && advancedSearchV2Enabled ? (
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
<AdvancedSearchDialog
q={searchText}
filters={filters}
onSubmit={onSubmitAdvanced}
/>
<Button
accessibilityRole="button"
size="small"
color="secondary"
shape="round"
label={l`Share this search`}
onPress={onShareSearch}>
<ButtonIcon icon={ShareIcon} />
</Button>
</View>
) : null}
</View> </View>
</View> </View>
</Layout.Center> </Layout.Center>
@@ -41,7 +41,7 @@ export function ClearableDateField({
*/ */
value={active ? value : ''} value={active ? value : ''}
placeholder={l({ placeholder={l({
message: 'Any time', message: 'Any date',
comment: 'Placeholder text for a date picker', comment: 'Placeholder text for a date picker',
})} })}
accessibilityHint={accessibilityHint} accessibilityHint={accessibilityHint}
@@ -19,11 +19,10 @@ export function FollowingDropdown({
const {t: l} = useLingui() const {t: l} = useLingui()
const options: {value: FollowingFilter; label: string}[] = [ const options: {value: FollowingFilter; label: string}[] = [
{value: 'everyone', label: l`Everyone`}, {value: 'anyone', label: l`Anyone`},
{value: 'following', label: l`People you follow`}, {value: 'following', label: l`People you follow`},
] ]
const currentLabel = const currentLabel = options.find(o => o.value === value)?.label ?? l`Anyone`
options.find(o => o.value === value)?.label ?? l`Everyone`
return ( return (
<Menu.Root> <Menu.Root>
@@ -14,7 +14,7 @@ const emptySerializeState = {
language: '', language: '',
replies: 'all' as const, replies: 'all' as const,
media: 'all' as const, media: 'all' as const,
following: 'everyone' as const, following: 'anyone' as const,
dateSince: '', dateSince: '',
dateSinceActive: false, dateSinceActive: false,
dateUntil: '', dateUntil: '',
@@ -145,10 +145,10 @@ describe(`AdvancedSearchDialog serialize/parse`, () => {
expect(out.filters.following).toBe('true') expect(out.filters.following).toBe('true')
}) })
it(`leaves the following param unset for everyone`, () => { it(`leaves the following param unset for anyone`, () => {
const out = serializeAdvancedSearch({ const out = serializeAdvancedSearch({
...emptySerializeState, ...emptySerializeState,
following: 'everyone', following: 'anyone',
}) })
expect(out.filters.following).toBeUndefined() expect(out.filters.following).toBeUndefined()
}) })
@@ -157,7 +157,7 @@ describe(`AdvancedSearchDialog serialize/parse`, () => {
expect(parseAdvancedSearch('', {following: 'true'}).following).toBe( expect(parseAdvancedSearch('', {following: 'true'}).following).toBe(
'following', 'following',
) )
expect(parseAdvancedSearch('', {}).following).toBe('everyone') expect(parseAdvancedSearch('', {}).following).toBe('anyone')
}) })
it(`strips redundant markers from filter values on serialize`, () => { it(`strips redundant markers from filter values on serialize`, () => {
@@ -177,7 +177,7 @@ describe(`AdvancedSearchDialog serialize/parse`, () => {
language: '', language: '',
replies: 'all', replies: 'all',
media: 'all', media: 'all',
following: 'everyone', following: 'anyone',
dateSince: '', dateSince: '',
dateSinceActive: false, dateSinceActive: false,
dateUntil: '', dateUntil: '',
@@ -38,17 +38,18 @@ import {
const MAX_FILTERS = 20 const MAX_FILTERS = 20
export function AdvancedSearchDialog({ export function AdvancedSearchDialog({
disabled,
q, q,
filters, filters,
onSubmit, onSubmit,
}: { }: {
disabled: boolean
q: string q: string
filters: SearchFilters filters: SearchFilters
onSubmit: (q: string, filters: SearchFilters) => void onSubmit: (q: string, filters: SearchFilters) => void
}) { }) {
const ax = useAnalytics() const ax = useAnalytics()
const {t: l} = useLingui() const {t: l} = useLingui()
const t = useTheme()
const control = Dialog.useDialogControl() const control = Dialog.useDialogControl()
const filtersActive = hasActiveFilters(filters) const filtersActive = hasActiveFilters(filters)
const stateKey = useMemo( const stateKey = useMemo(
@@ -61,9 +62,10 @@ export function AdvancedSearchDialog({
<> <>
<View style={[a.relative]}> <View style={[a.relative]}>
<Button <Button
disabled={disabled}
label={l`Open advanced search options`} label={l`Open advanced search options`}
size="small" size="small"
color="secondary" color={filtersActive ? 'primary_subtle' : 'secondary'}
style={native([a.py_sm, a.px_sm])} style={native([a.py_sm, a.px_sm])}
onPress={() => { onPress={() => {
ax.metric('search:advanced:press', { ax.metric('search:advanced:press', {
@@ -73,25 +75,9 @@ export function AdvancedSearchDialog({
}}> }}>
<ButtonIcon icon={SettingsSliderIcon} /> <ButtonIcon icon={SettingsSliderIcon} />
<ButtonText> <ButtonText>
<Trans>Advanced search</Trans> <Trans context="search">Filters</Trans>
</ButtonText> </ButtonText>
</Button> </Button>
{filtersActive && (
<View
accessible={false}
style={[
a.absolute,
a.rounded_full,
{
top: -2,
right: -2,
width: 10,
height: 10,
backgroundColor: t.palette.primary_500,
},
]}
/>
)}
</View> </View>
<Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}> <Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}>
@@ -244,12 +230,12 @@ function DialogInner({
return ( return (
<Dialog.ScrollableInner <Dialog.ScrollableInner
ref={scrollRef} ref={scrollRef}
label={l`Dialog: Set advanced search options`} label={l`Dialog: Set search filters`}
contentContainerStyle={[a.px_0, a.pt_0]} contentContainerStyle={[a.px_0, a.pt_0]}
header={ header={
<Dialog.Header renderLeft={cancelButton} renderRight={searchButton}> <Dialog.Header renderLeft={cancelButton} renderRight={searchButton}>
<Dialog.HeaderText> <Dialog.HeaderText>
<Trans>Advanced search</Trans> <Trans context="search">Filters</Trans>
</Dialog.HeaderText> </Dialog.HeaderText>
</Dialog.Header> </Dialog.Header>
}> }>
@@ -272,22 +258,6 @@ function DialogInner({
</View> </View>
<View style={[twoColumn ? a.flex_row : a.flex_col, a.gap_xl]}> <View style={[twoColumn ? a.flex_row : a.flex_col, a.gap_xl]}>
<View style={[a.flex_1]}>
<TextField.LabelText>
<Trans>This exact phrase</Trans>
</TextField.LabelText>
<ClearableInput
label={l`This exact phrase`}
defaultValue={exactPhrase}
placeholder={l({
message: 'what’s up',
comment: 'Advanced search: Example of an “exact phrase” search',
})}
onChangeText={setExactPhrase}
onSubmitEditing={handlePressSearch}
/>
</View>
<View style={[a.flex_1]}> <View style={[a.flex_1]}>
<TextField.LabelText> <TextField.LabelText>
<Trans>None of these words</Trans> <Trans>None of these words</Trans>
@@ -304,6 +274,22 @@ function DialogInner({
onSubmitEditing={handlePressSearch} onSubmitEditing={handlePressSearch}
/> />
</View> </View>
<View style={[a.flex_1]}>
<TextField.LabelText>
<Trans>This exact phrase</Trans>
</TextField.LabelText>
<ClearableInput
label={l`This exact phrase`}
defaultValue={exactPhrase}
placeholder={l({
message: 'what’s up',
comment: 'Advanced search: Example of an “exact phrase” search',
})}
onChangeText={setExactPhrase}
onSubmitEditing={handlePressSearch}
/>
</View>
</View> </View>
<View> <View>
@@ -15,9 +15,9 @@ export type MediaFilter = 'all' | 'media' | 'video'
/** /**
* Whether to limit results to authors the user follows. Serializes into the * Whether to limit results to authors the user follows. Serializes into the
* `following` sibling param ('following' -> following:true, everyone -> unset). * `following` sibling param ('following' -> following:true, anyone -> unset).
*/ */
export type FollowingFilter = 'everyone' | 'following' export type FollowingFilter = 'anyone' | 'following'
export type FilterField = 'authors' | 'mentions' | 'domains' | 'urls' | 'tags' export type FilterField = 'authors' | 'mentions' | 'domains' | 'urls' | 'tags'
@@ -260,7 +260,7 @@ export function parseAdvancedSearch(
language: lang, language: lang,
replies, replies,
media, media,
following: filters.following === 'true' ? 'following' : 'everyone', following: filters.following === 'true' ? 'following' : 'anyone',
since: since && isValidDate(since) ? since : '', since: since && isValidDate(since) ? since : '',
until: until && isValidDate(until) ? until : '', until: until && isValidDate(until) ? until : '',
filters: filterRows, filters: filterRows,