diff --git a/assets/icons/chevronTopBottom_stroke2_corner0_rounded.svg b/assets/icons/chevronTopBottom_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..249846bc3d --- /dev/null +++ b/assets/icons/chevronTopBottom_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/src/alf/atoms.ts b/src/alf/atoms.ts index a7cf6cb3f4..eba3335b4f 100644 --- a/src/alf/atoms.ts +++ b/src/alf/atoms.ts @@ -50,6 +50,12 @@ export const atoms = { overflow_hidden: { overflow: 'hidden', }, + /** + * @platform web + */ + overflow_auto: web({ + overflow: 'auto', + }), /* * Width diff --git a/src/components/Menu/index.web.tsx b/src/components/Menu/index.web.tsx index dc91161682..e89e0b0321 100644 --- a/src/components/Menu/index.web.tsx +++ b/src/components/Menu/index.web.tsx @@ -189,6 +189,7 @@ export function Outer({ t.name === 'light' ? t.atoms.bg : t.atoms.bg_contrast_25, t.atoms.shadow_md, t.atoms.border_contrast_low, + a.overflow_auto, style, ]}> {children} diff --git a/src/components/forms/TextField.tsx b/src/components/forms/TextField.tsx index 410cc654e9..d28700df95 100644 --- a/src/components/forms/TextField.tsx +++ b/src/components/forms/TextField.tsx @@ -226,6 +226,7 @@ export function createInput(Component: typeof TextInput) { <> diff --git a/src/components/icons/Chevron.tsx b/src/components/icons/Chevron.tsx index a04e6e0092..4d252ee3ca 100644 --- a/src/components/icons/Chevron.tsx +++ b/src/components/icons/Chevron.tsx @@ -15,3 +15,7 @@ export const ChevronTop_Stroke2_Corner0_Rounded = createSinglePathSVG({ export const ChevronBottom_Stroke2_Corner0_Rounded = createSinglePathSVG({ path: 'M3.293 8.293a1 1 0 0 1 1.414 0L12 15.586l7.293-7.293a1 1 0 1 1 1.414 1.414l-8 8a1 1 0 0 1-1.414 0l-8-8a1 1 0 0 1 0-1.414Z', }) + +export const ChevronTopBottom_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M11.293 4.293a1 1 0 0 1 1.414 0l4 4a1 1 0 0 1-1.414 1.414L12 6.414 8.707 9.707a1 1 0 0 1-1.414-1.414l4-4Zm-4 10a1 1 0 0 1 1.414 0L12 17.586l3.293-3.293a1 1 0 0 1 1.414 1.414l-4 4a1 1 0 0 1-1.414 0l-4-4a1 1 0 0 1 0-1.414Z', +}) diff --git a/src/view/screens/Search/Search.tsx b/src/view/screens/Search/Search.tsx index b11bb0510a..c5a1a8145d 100644 --- a/src/view/screens/Search/Search.tsx +++ b/src/view/screens/Search/Search.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, {useEffect, useLayoutEffect, useMemo} from 'react' import { ActivityIndicator, Image, @@ -10,7 +10,15 @@ import { View, } from 'react-native' import {ScrollView as RNGHScrollView} from 'react-native-gesture-handler' -import RNPickerSelect from 'react-native-picker-select' +import Animated, { + FadeIn, + FadeOut, + LayoutAnimationConfig, + LinearTransition, + useAnimatedStyle, + useSharedValue, + withTiming, +} from 'react-native-reanimated' import {AppBskyActorDefs, AppBskyFeedDefs, moderateProfile} from '@atproto/api' import { FontAwesomeIcon, @@ -22,7 +30,7 @@ import AsyncStorage from '@react-native-async-storage/async-storage' import {useFocusEffect, useNavigation} from '@react-navigation/native' import {APP_LANGUAGES, LANGUAGES} from '#/lib/../locale/languages' -import {createHitslop} from '#/lib/constants' +import {createHitslop, HITSLOP_20} from '#/lib/constants' import {HITSLOP_10} from '#/lib/constants' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {usePalette} from '#/lib/hooks/usePalette' @@ -47,7 +55,6 @@ import {useActorSearch} from '#/state/queries/actor-search' import {usePopularFeedsSearch} from '#/state/queries/feed' import {useSearchPostsQuery} from '#/state/queries/search-posts' import {useSession} from '#/state/session' -import {useSetDrawerOpen} from '#/state/shell' import {useSetMinimalShellMode} from '#/state/shell' import {Pager} from '#/view/com/pager/Pager' import {TabBar} from '#/view/com/pager/TabBar' @@ -61,17 +68,23 @@ import {SearchLinkCard, SearchProfileCard} from '#/view/shell/desktop/Search' import {makeSearchQuery, parseSearchQuery} from '#/screens/Search/utils' import { atoms as a, + native, + platform, tokens, useBreakpoints, - useTheme as useThemeNew, + useTheme, web, } from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as FeedCard from '#/components/FeedCard' import {SearchInput} from '#/components/forms/SearchInput' -import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron' -import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu' +import { + ChevronBottom_Stroke2_Corner0_Rounded as ChevronDownIcon, + ChevronTopBottom_Stroke2_Corner0_Rounded as ChevronUpDownIcon, +} from '#/components/icons/Chevron' +import {Earth_Stroke2_Corner0_Rounded as EarthIcon} from '#/components/icons/Globe' import * as Layout from '#/components/Layout' +import * as Menu from '#/components/Menu' function Loader() { return ( @@ -278,7 +291,7 @@ let SearchScreenFeedsResults = ({ query: string active: boolean }): React.ReactNode => { - const t = useThemeNew() + const t = useTheme() const {_} = useLingui() const {data: results, isFetched} = usePopularFeedsSearch({ @@ -323,103 +336,90 @@ function SearchLanguageDropdown({ value: string onChange(value: string): void }) { - const t = useThemeNew() const {_} = useLingui() const {appLanguage, contentLanguages} = useLanguagePrefs() - const items = React.useMemo(() => { - return [ - { - label: _(msg`Any language`), - inputLabel: _(msg`Any language`), - value: '', - key: '*', - }, - ].concat( - LANGUAGES.filter( - (lang, index, self) => - Boolean(lang.code2) && // reduce to the code2 varieties - index === self.findIndex(t => t.code2 === lang.code2), // remove dupes (which will happen) - ) - .map(l => ({ - label: languageName(l, appLanguage), - inputLabel: languageName(l, appLanguage), - value: l.code2, - key: l.code2 + l.code3, - })) - .sort((a, b) => { - // prioritize user's languages - const aIsUser = contentLanguages.includes(a.value) - const bIsUser = contentLanguages.includes(b.value) - if (aIsUser && !bIsUser) return -1 - if (bIsUser && !aIsUser) return 1 - // prioritize "common" langs in the network - const aIsCommon = !!APP_LANGUAGES.find(al => al.code2 === a.value) - const bIsCommon = !!APP_LANGUAGES.find(al => al.code2 === b.value) - if (aIsCommon && !bIsCommon) return -1 - if (bIsCommon && !aIsCommon) return 1 - // fall back to alphabetical - return a.label.localeCompare(b.label) - }), + const languages = useMemo(() => { + return LANGUAGES.filter( + (lang, index, self) => + Boolean(lang.code2) && // reduce to the code2 varieties + index === self.findIndex(t => t.code2 === lang.code2), // remove dupes (which will happen) ) - }, [_, appLanguage, contentLanguages]) - - const style = { - backgroundColor: t.atoms.bg_contrast_25.backgroundColor, - color: t.atoms.text.color, - fontSize: a.text_xs.fontSize, - fontFamily: 'inherit', - fontWeight: a.font_bold.fontWeight, - paddingHorizontal: 14, - paddingRight: 32, - paddingVertical: 8, - borderRadius: a.rounded_full.borderRadius, - borderWidth: a.border.borderWidth, - borderColor: t.atoms.border_contrast_low.borderColor, - } + .map(l => ({ + label: languageName(l, appLanguage), + value: l.code2, + key: l.code2 + l.code3, + })) + .sort((a, b) => { + // prioritize user's languages + const aIsUser = contentLanguages.includes(a.value) + const bIsUser = contentLanguages.includes(b.value) + if (aIsUser && !bIsUser) return -1 + if (bIsUser && !aIsUser) return 1 + // prioritize "common" langs in the network + const aIsCommon = !!APP_LANGUAGES.find(al => al.code2 === a.value) + const bIsCommon = !!APP_LANGUAGES.find(al => al.code2 === b.value) + if (aIsCommon && !bIsCommon) return -1 + if (bIsCommon && !aIsCommon) return 1 + // fall back to alphabetical + return a.label.localeCompare(b.label) + }) + }, [appLanguage, contentLanguages]) + + const currentLanguageLabel = + languages.find(lang => lang.value === value)?.label ?? _(msg`All languages`) return ( - ( - - )} - useNativeAndroidPickerStyle={false} - style={{ - iconContainer: { - pointerEvents: 'none', - right: a.px_sm.paddingRight, - top: 0, - bottom: 0, - display: 'flex', - justifyContent: 'center', - }, - inputAndroid: { - ...style, - paddingVertical: 2, - }, - inputIOS: { - ...style, - }, - inputWeb: web({ - ...style, - cursor: 'pointer', - // @ts-ignore web only - '-moz-appearance': 'none', - '-webkit-appearance': 'none', - appearance: 'none', - outline: 0, - borderWidth: 0, - overflow: 'hidden', - whiteSpace: 'nowrap', - textOverflow: 'ellipsis', - }), - }} - /> + + + {({props}) => ( + + )} + + + + Filter search by language + + onChange('')}> + + All languages + + + + + + {languages.map(lang => ( + onChange(lang.value)}> + {lang.label} + + + ))} + + + ) } @@ -534,12 +534,7 @@ let SearchScreenInner = ({ ( - + section.title)} {...props} /> )} @@ -597,12 +592,11 @@ SearchScreenInner = React.memo(SearchScreenInner) export function SearchScreen( props: NativeStackScreenProps, ) { - const t = useThemeNew() + const t = useTheme() const {gtMobile} = useBreakpoints() const navigation = useNavigation() const textInput = React.useRef(null) const {_} = useLingui() - const setDrawerOpen = useSetDrawerOpen() const setMinimalShellMode = useSetMinimalShellMode() // Query terms @@ -621,11 +615,17 @@ export function SearchScreen( initialQuery: queryParam, }) const showFilters = Boolean(queryWithParams && !showAutocomplete) - /* - * Arbitrary sizing, so guess and check, used for sticky header alignment and - * sizing. - */ - const headerHeight = 60 + (showFilters ? 40 : 0) + + // web only - measure header height for sticky positioning + const [headerHeight, setHeaderHeight] = React.useState(0) + const headerRef = React.useRef(null) + useLayoutEffect(() => { + if (isWeb) { + if (!headerRef.current) return + const measurement = (headerRef.current as Element).getBoundingClientRect() + setHeaderHeight(measurement.height) + } + }, []) useFocusEffect( useNonReactiveCallback(() => { @@ -654,11 +654,6 @@ export function SearchScreen( loadSearchHistory() }, []) - const onPressMenu = React.useCallback(() => { - textInput.current?.blur() - setDrawerOpen(true) - }, [setDrawerOpen]) - const onPressClearQuery = React.useCallback(() => { scrollToTopWeb() setSearchText('') @@ -834,37 +829,76 @@ export function SearchScreen( } }, [setShowAutocomplete]) + const cancelWidth = useSharedValue(70) + const showCancelBtnAnimation = useSharedValue(0) + + useEffect(() => { + if (showAutocomplete) { + showCancelBtnAnimation.set(withTiming(1)) + } else { + showCancelBtnAnimation.set(withTiming(0)) + } + }, [showAutocomplete, showCancelBtnAnimation]) + + const animatedInputContainerStyle = useAnimatedStyle(() => ({ + marginRight: showCancelBtnAnimation.get() * cancelWidth.get(), + })) + + const animatedCancelBtnStyle = useAnimatedStyle(() => ({ + opacity: showCancelBtnAnimation.get(), + right: cancelWidth.get() * -1, + })) + return ( { + if (isWeb) setHeaderHeight(evt.nativeEvent.layout.height) + }} style={[ + a.relative, + a.z_10, web({ - height: headerHeight, position: 'sticky', top: 0, - zIndex: 1, }), ]}> - - - - {!gtMobile && !showAutocomplete && ( - - )} - + + + )} + + + + - {showAutocomplete && ( + cancelWidth.set(evt.nativeEvent.layout.width)} + aria-hidden={!showAutocomplete}> - )} - + + - {showFilters && ( + {showFilters && gtMobile && ( - - - + )} - + diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx index e9ba65ed02..bb5de2eb43 100644 --- a/src/view/shell/desktop/LeftNav.tsx +++ b/src/view/shell/desktop/LeftNav.tsx @@ -33,7 +33,7 @@ import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' import {PressableWithHover} from '#/view/com/util/PressableWithHover' import {UserAvatar} from '#/view/com/util/UserAvatar' import {NavSignupCard} from '#/view/shell/NavSignupCard' -import {atoms as a, tokens, useBreakpoints, useTheme} from '#/alf' +import {atoms as a, tokens, useBreakpoints, useTheme, web} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {DialogControlProps} from '#/components/Dialog' import {ArrowBoxLeft_Stroke2_Corner0_Rounded as LeaveIcon} from '#/components/icons/ArrowBoxLeft' @@ -235,7 +235,10 @@ function SwitchMenuItems({ closeEverything() } return ( - + {accounts && accounts.length > 0 && ( <>