diff --git a/src/components/forms/TextField.tsx b/src/components/forms/TextField.tsx index 51bf0ead00..21928d3df3 100644 --- a/src/components/forms/TextField.tsx +++ b/src/components/forms/TextField.tsx @@ -196,8 +196,8 @@ export function createInput(Component: typeof TextInput) { a.px_xs, { // paddingVertical doesn't work w/multiline - esb - paddingTop: 10, - paddingBottom: 11, + paddingTop: 12, + paddingBottom: 13, lineHeight: a.text_md.fontSize * 1.1875, textAlignVertical: rest.multiline ? 'top' : undefined, minHeight: rest.multiline ? 80 : undefined, @@ -211,7 +211,8 @@ export function createInput(Component: typeof TextInput) { marginBottom: 2, }), android({ - paddingBottom: 16, + paddingTop: 8, + paddingBottom: 8, }), style, ]} diff --git a/src/screens/Search/__tests__/utils.test.ts b/src/screens/Search/__tests__/utils.test.ts new file mode 100644 index 0000000000..5edacce5ba --- /dev/null +++ b/src/screens/Search/__tests__/utils.test.ts @@ -0,0 +1,32 @@ +import {describe, expect,it} from '@jest/globals' + +import {parseSearchQuery} from '#/screens/Search/utils' + +describe(`parseSearchQuery`, () => { + const tests = [ + { + input: `bluesky`, + output: {query: `bluesky`, params: {}}, + }, + { + input: `bluesky from:esb.lol`, + output: {query: `bluesky`, params: {from: `esb.lol`}}, + }, + { + input: `bluesky "from:esb.lol"`, + output: {query: `bluesky "from:esb.lol"`, params: {}}, + }, + { + input: `bluesky mentions:@esb.lol`, + output: {query: `bluesky`, params: {mentions: `@esb.lol`}}, + }, + { + input: `bluesky since:2021-01-01:00:00:00`, + output: {query: `bluesky`, params: {since: `2021-01-01:00:00:00`}}, + }, + ] + + it.each(tests)(`%p`, ({input, output}) => { + expect(parseSearchQuery(input)).toEqual(output) + }) +}) diff --git a/src/screens/Search/utils.ts b/src/screens/Search/utils.ts new file mode 100644 index 0000000000..6ade389717 --- /dev/null +++ b/src/screens/Search/utils.ts @@ -0,0 +1,43 @@ +export type Params = Record + +export function parseSearchQuery(rawQuery: string) { + let base = rawQuery + const rawLiterals = rawQuery.match(/".+"/gi) || [] + + // remove literals from base + for (const literal of rawLiterals) { + base = base.replace(literal, '') + } + + // find remaining params in base + const rawParams = base.match(/[a-z]+:[a-z-\.@\d:]+/gi) || [] + + for (const param of rawParams) { + base = base.replace(param, '') + } + + base = base.trim() + + const params = rawParams.reduce((params, param) => { + const [name, ...value] = param.split(/:/) + params[name] = value.join(':') // dates can contain additional colons + return params + }, {} as Params) + const literals = rawLiterals.map(l => String(l)) + + return { + query: [base, literals.join(' ')].filter(Boolean).join(' '), + params, + } +} + +export function makeSearchQuery(query: string, params: Params) { + return [ + query, + Object.entries(params) + .map(([name, value]) => `${name}:${value}`) + .join(' '), + ] + .filter(Boolean) + .join(' ') +} diff --git a/src/view/screens/Search/Search.tsx b/src/view/screens/Search/Search.tsx index e954fd1c14..3feca8fa6d 100644 --- a/src/view/screens/Search/Search.tsx +++ b/src/view/screens/Search/Search.tsx @@ -59,6 +59,7 @@ import {Text} from '#/view/com/util/text/Text' import {CenteredView, ScrollView} from '#/view/com/util/Views' import {Explore} from '#/view/screens/Search/Explore' import {SearchLinkCard, SearchProfileCard} from '#/view/shell/desktop/Search' +import {makeSearchQuery,parseSearchQuery} from '#/screens/Search/utils' import {atoms as a, useBreakpoints, useTheme as useThemeNew, web} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as FeedCard from '#/components/FeedCard' @@ -67,7 +68,6 @@ import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components import {MagnifyingGlass2_Stroke2_Corner0_Rounded as MagnifyingGlass} from '#/components/icons/MagnifyingGlass2' import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu' import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' -import {Text as NewText} from '#/components/Typography' const HEADER_HEIGHT = 56 @@ -261,7 +261,7 @@ let SearchScreenUserResults = ({ const {_} = useLingui() const {data: results, isFetched} = useActorSearch({ - query: sanitizeLangFromQuery(query), + query, enabled: active, }) @@ -299,7 +299,7 @@ let SearchScreenFeedsResults = ({ const {_} = useLingui() const {data: results, isFetched} = usePopularFeedsSearch({ - query: sanitizeLangFromQuery(query), + query, enabled: active, }) @@ -361,10 +361,12 @@ function SearchLanguageDropdown({ fontSize: a.text_xs.fontSize, fontFamily: 'inherit', fontWeight: a.font_bold.fontWeight, - paddingHorizontal: 12, + paddingHorizontal: 14, paddingRight: 32, - paddingVertical: 6, + paddingVertical: 8, borderRadius: a.rounded_full.borderRadius, + borderWidth: a.border.borderWidth, + borderColor: t.atoms.border_contrast_low.borderColor, } return ( @@ -376,6 +378,7 @@ function SearchLanguageDropdown({ Icon={() => ( )} + useNativeAndroidPickerStyle={false} style={{ iconContainer: { pointerEvents: 'none', @@ -387,6 +390,7 @@ function SearchLanguageDropdown({ }, inputAndroid: { ...style, + paddingVertical: 2, }, inputIOS: { ...style, @@ -409,7 +413,53 @@ function SearchLanguageDropdown({ ) } -let SearchScreenInner = ({query}: {query?: string}): React.ReactNode => { +function useQueryManager({initialQuery}: {initialQuery: string}) { + const {contentLanguages} = useLanguagePrefs() + const {query, params: initialParams} = React.useMemo( + () => parseSearchQuery(initialQuery || ''), + [initialQuery], + ) + + const [lang, setLang] = React.useState( + initialParams.lang || contentLanguages[0], + ) + + const params = React.useMemo( + () => ({ + lang, + }), + [lang], + ) + const handlers = React.useMemo( + () => ({ + setLang, + }), + [setLang], + ) + + console.log({ + query, + initialParams, + params, + }) + + return React.useMemo(() => { + return { + query, + queryWithParams: makeSearchQuery(query, params), + params: { + ...params, + ...handlers, + }, + } + }, [query, params, handlers]) +} + +let SearchScreenInner = ({ + query: initialQuery, +}: { + query: string +}): React.ReactNode => { const t = useThemeNew() const pal = usePalette('default') const setMinimalShellMode = useSetMinimalShellMode() @@ -420,12 +470,7 @@ let SearchScreenInner = ({query}: {query?: string}): React.ReactNode => { const {_} = useLingui() const {gtMobile} = useBreakpoints() - const {contentLanguages} = useLanguagePrefs() - const [language, setLanguage] = React.useState( - parseLanguageFromQuery(query || '') || contentLanguages[0], - ) - const queryWithoutLang = sanitizeLangFromQuery(query || '') - const queryWithLang = queryWithoutLang + ` lang:${language}` + const {params, query, queryWithParams} = useQueryManager({initialQuery}) const onPageSelected = React.useCallback( (index: number) => { @@ -437,13 +482,13 @@ let SearchScreenInner = ({query}: {query?: string}): React.ReactNode => { ) const sections = React.useMemo(() => { - if (!queryWithoutLang) return [] + if (!query) return [] return [ { title: _(msg`Top`), component: ( @@ -453,7 +498,7 @@ let SearchScreenInner = ({query}: {query?: string}): React.ReactNode => { title: _(msg`Latest`), component: ( @@ -462,40 +507,34 @@ let SearchScreenInner = ({query}: {query?: string}): React.ReactNode => { { title: _(msg`People`), component: ( - + ), }, { title: _(msg`Feeds`), component: ( - + ), }, ] - }, [_, queryWithoutLang, queryWithLang, activeTab]) + }, [_, query, queryWithParams, activeTab]) - return queryWithoutLang ? ( + return query ? ( <> - - - Language: - - - + + + @@ -563,7 +602,6 @@ let SearchScreenInner = ({query}: {query?: string}): React.ReactNode => { ) } -SearchScreenInner = React.memo(SearchScreenInner) export function SearchScreen( props: NativeStackScreenProps, @@ -809,7 +847,7 @@ export function SearchScreen( label={_(msg`Menu`)} accessibilityHint={_(msg`Access navigation links and settings`)} size="large" - variant="ghost" + variant="solid" color="secondary" shape="square"> @@ -870,7 +908,7 @@ export function SearchScreen( display: showAutocomplete ? 'none' : 'flex', flex: 1, }}> - + ) @@ -1151,18 +1189,6 @@ function scrollToTopWeb() { } } -function sanitizeLangFromQuery(query: string) { - return query.replace(/lang:[a-z-]+/gi, '').trim() -} - -function parseLanguageFromQuery(query: string) { - const match = query.match(/lang:[a-z-]+/gi) - if (match && match[0]) { - return match[0].replace('lang:', '').split('-')[0] - } - return '' -} - const styles = StyleSheet.create({ header: { flexDirection: 'row', diff --git a/src/view/screens/Storybook/Forms.tsx b/src/view/screens/Storybook/Forms.tsx index fc414d31f3..8ec118ae3e 100644 --- a/src/view/screens/Storybook/Forms.tsx +++ b/src/view/screens/Storybook/Forms.tsx @@ -32,7 +32,7 @@ export function Forms() { label="Text field" /> - +