about summary refs log tree commit diff
diff options
context:
space:
mode:
authorSamuel Newman <mozzius@protonmail.com>2025-02-13 00:00:45 +0000
committerGitHub <noreply@github.com>2025-02-13 00:00:45 +0000
commitdb25f95c33121da9d04a02dc2e77929a5d24a5ce (patch)
tree8d86126e83f032ac6b20e3de8f1a346ebf93a863
parentb37199a5a02e9957d3f564035004a39190c91a62 (diff)
downloadvoidsky-db25f95c33121da9d04a02dc2e77929a5d24a5ce.tar.zst
Improved search language select (#7591)
* replace with Menu

* new icon for native

* hackfix radix dropdown height

* fix jsx

* reduce language names with lots of variants to what firefox returns from Intl.DisplayNames

* more language label simplifications

* add collision padding

* adjust spacing around and left align title
-rw-r--r--assets/icons/chevronTopBottom_stroke2_corner0_rounded.svg1
-rw-r--r--src/alf/atoms.ts6
-rw-r--r--src/components/Menu/index.web.tsx7
-rw-r--r--src/components/icons/Chevron.tsx4
-rw-r--r--src/locale/languages.ts30
-rw-r--r--src/view/screens/Search/Search.tsx221
-rw-r--r--src/view/shell/desktop/LeftNav.tsx7
7 files changed, 141 insertions, 135 deletions
diff --git a/assets/icons/chevronTopBottom_stroke2_corner0_rounded.svg b/assets/icons/chevronTopBottom_stroke2_corner0_rounded.svg
new file mode 100644
index 000000000..249846bc3
--- /dev/null
+++ b/assets/icons/chevronTopBottom_stroke2_corner0_rounded.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="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" clip-rule="evenodd"/></svg>
diff --git a/src/alf/atoms.ts b/src/alf/atoms.ts
index ae89fa2cf..c9db8accc 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 eb52895d4..eb91e014f 100644
--- a/src/components/Menu/index.web.tsx
+++ b/src/components/Menu/index.web.tsx
@@ -184,6 +184,7 @@ export function Outer({
     <DropdownMenu.Portal>
       <DropdownMenu.Content
         sideOffset={5}
+        collisionPadding={{left: 5, right: 5, bottom: 5}}
         loop
         aria-label="Test"
         className="dropdown-menu-transform-origin">
@@ -195,6 +196,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,
             !reduceMotionEnabled && a.zoom_fade_in,
             style,
           ]}>
@@ -380,9 +382,8 @@ export function Divider() {
       style={flatten([
         a.my_xs,
         t.atoms.bg_contrast_100,
-        {
-          height: 1,
-        },
+        a.flex_shrink_0,
+        {height: 1},
       ])}
     />
   )
diff --git a/src/components/icons/Chevron.tsx b/src/components/icons/Chevron.tsx
index a04e6e009..4d252ee3c 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/locale/languages.ts b/src/locale/languages.ts
index 288032169..a059fcc0a 100644
--- a/src/locale/languages.ts
+++ b/src/locale/languages.ts
@@ -179,11 +179,7 @@ export const LANGUAGES: Language[] = [
   {code3: 'cho', code2: '', name: 'Choctaw'},
   {code3: 'chp', code2: '', name: 'Chipewyan; Dene Suline'},
   {code3: 'chr', code2: '', name: 'Cherokee'},
-  {
-    code3: 'chu',
-    code2: 'cu',
-    name: 'Church Slavic; Old Slavonic; Church Slavonic; Old Bulgarian; Old Church Slavonic',
-  },
+  {code3: 'chu', code2: 'cu', name: 'Church Slavic'},
   {code3: 'chv', code2: 'cv', name: 'Chuvash'},
   {code3: 'chy', code2: '', name: 'Cheyenne'},
   {code3: 'cmc', code2: '', name: 'Chamic languages'},
@@ -301,13 +297,9 @@ export const LANGUAGES: Language[] = [
   {code3: 'iii', code2: 'ii', name: 'Sichuan Yi; Nuosu'},
   {code3: 'ijo', code2: '', name: 'Ijo languages'},
   {code3: 'iku', code2: 'iu', name: 'Inuktitut'},
-  {code3: 'ile', code2: 'ie', name: 'Interlingue; Occidental'},
+  {code3: 'ile', code2: 'ie', name: 'Interlingue'},
   {code3: 'ilo', code2: '', name: 'Iloko'},
-  {
-    code3: 'ina',
-    code2: 'ia',
-    name: 'Interlingua (International Auxiliary Language Association)',
-  },
+  {code3: 'ina', code2: 'ia', name: 'Interlingua'},
   {code3: 'inc', code2: '', name: 'Indic languages'},
   {code3: 'ind', code2: 'id', name: 'Indonesian'},
   {code3: 'ine', code2: '', name: 'Indo-European languages'},
@@ -325,7 +317,7 @@ export const LANGUAGES: Language[] = [
   {code3: 'kaa', code2: '', name: 'Kara-Kalpak'},
   {code3: 'kab', code2: '', name: 'Kabyle'},
   {code3: 'kac', code2: '', name: 'Kachin; Jingpho'},
-  {code3: 'kal', code2: 'kl', name: 'Kalaallisut; Greenlandic'},
+  {code3: 'kal', code2: 'kl', name: 'Kalaallisut'},
   {code3: 'kam', code2: '', name: 'Kamba'},
   {code3: 'kan', code2: 'kn', name: 'Kannada'},
   {code3: 'kar', code2: '', name: 'Karen languages'},
@@ -364,7 +356,7 @@ export const LANGUAGES: Language[] = [
   {code3: 'lat', code2: 'la', name: 'Latin'},
   {code3: 'lav', code2: 'lv', name: 'Latvian'},
   {code3: 'lez', code2: '', name: 'Lezghian'},
-  {code3: 'lim', code2: 'li', name: 'Limburgan; Limburger; Limburgish'},
+  {code3: 'lim', code2: 'li', name: 'Limburgish'},
   {code3: 'lin', code2: 'ln', name: 'Lingala'},
   {code3: 'lit', code2: 'lt', name: 'Lithuanian'},
   {code3: 'lol', code2: '', name: 'Mongo'},
@@ -425,9 +417,9 @@ export const LANGUAGES: Language[] = [
   {code3: 'nai', code2: '', name: 'North American Indian languages'},
   {code3: 'nap', code2: '', name: 'Neapolitan'},
   {code3: 'nau', code2: 'na', name: 'Nauru'},
-  {code3: 'nav', code2: 'nv', name: 'Navajo; Navaho'},
-  {code3: 'nbl', code2: 'nr', name: 'Ndebele, South; South Ndebele'},
-  {code3: 'nde', code2: 'nd', name: 'Ndebele, North; North Ndebele'},
+  {code3: 'nav', code2: 'nv', name: 'Navajo'},
+  {code3: 'nbl', code2: 'nr', name: 'South Ndebele'},
+  {code3: 'nde', code2: 'nd', name: 'North Ndebele'},
   {code3: 'ndo', code2: 'ng', name: 'Ndonga'},
   {
     code3: 'nds',
@@ -440,8 +432,8 @@ export const LANGUAGES: Language[] = [
   {code3: 'nic', code2: '', name: 'Niger-Kordofanian languages'},
   {code3: 'niu', code2: '', name: 'Niuean'},
   {code3: 'nld', code2: 'nl', name: 'Dutch; Flemish'},
-  {code3: 'nno', code2: 'nn', name: 'Norwegian Nynorsk; Nynorsk, Norwegian'},
-  {code3: 'nob', code2: 'nb', name: 'Bokmål, Norwegian; Norwegian Bokmål'},
+  {code3: 'nno', code2: 'nn', name: 'Norwegian Nynorsk'},
+  {code3: 'nob', code2: 'nb', name: 'Norwegian Bokmål'},
   {code3: 'nog', code2: '', name: 'Nogai'},
   {code3: 'non', code2: '', name: 'Norse, Old'},
   {code3: 'nor', code2: 'no', name: 'Norwegian'},
@@ -463,7 +455,7 @@ export const LANGUAGES: Language[] = [
   {code3: 'ori', code2: 'or', name: 'Oriya'},
   {code3: 'orm', code2: 'om', name: 'Oromo'},
   {code3: 'osa', code2: '', name: 'Osage'},
-  {code3: 'oss', code2: 'os', name: 'Ossetian; Ossetic'},
+  {code3: 'oss', code2: 'os', name: 'Ossetic'},
   {code3: 'ota', code2: '', name: 'Turkish, Ottoman (1500-1928)'},
   {code3: 'oto', code2: '', name: 'Otomian languages'},
   {code3: 'paa', code2: '', name: 'Papuan languages'},
diff --git a/src/view/screens/Search/Search.tsx b/src/view/screens/Search/Search.tsx
index 3e711ab56..f16b4fff2 100644
--- a/src/view/screens/Search/Search.tsx
+++ b/src/view/screens/Search/Search.tsx
@@ -1,4 +1,4 @@
-import React, {useCallback, useLayoutEffect} from 'react'
+import React, {useCallback, useLayoutEffect, useMemo} from 'react'
 import {
   ActivityIndicator,
   Image,
@@ -10,7 +10,6 @@ import {
   View,
 } from 'react-native'
 import {ScrollView as RNGHScrollView} from 'react-native-gesture-handler'
-import RNPickerSelect from 'react-native-picker-select'
 import {AppBskyActorDefs, AppBskyFeedDefs, moderateProfile} from '@atproto/api'
 import {
   FontAwesomeIcon,
@@ -57,12 +56,25 @@ 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 {atoms as a, tokens, useBreakpoints, useTheme, web} from '#/alf'
-import {Button, ButtonText} from '#/components/Button'
+import {
+  atoms as a,
+  native,
+  platform,
+  tokens,
+  useBreakpoints,
+  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 {
+  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'
 import {account, useStorage} from '#/storage'
 
 function Loader() {
@@ -315,103 +327,95 @@ function SearchLanguageDropdown({
   value: string
   onChange(value: string): void
 }) {
-  const t = useTheme()
   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 (
-    <RNPickerSelect
-      darkTheme={t.scheme === 'dark'}
-      placeholder={{}}
-      value={value}
-      onValueChange={onChange}
-      items={items}
-      Icon={() => (
-        <ChevronDown fill={t.atoms.text_contrast_low.color} size="sm" />
-      )}
-      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',
-        }),
-      }}
-    />
+    <Menu.Root>
+      <Menu.Trigger
+        label={_(
+          msg`Filter search by language (currently: ${currentLanguageLabel})`,
+        )}>
+        {({props}) => (
+          <Button
+            {...props}
+            label={props.accessibilityLabel}
+            size="small"
+            color={platform({native: 'primary', default: 'secondary'})}
+            variant={platform({native: 'ghost', default: 'solid'})}
+            style={native([
+              a.py_sm,
+              a.px_sm,
+              {marginRight: tokens.space.sm * -1},
+            ])}>
+            <ButtonIcon icon={EarthIcon} />
+            <ButtonText>{currentLanguageLabel}</ButtonText>
+            <ButtonIcon
+              icon={platform({
+                native: ChevronUpDownIcon,
+                default: ChevronDownIcon,
+              })}
+            />
+          </Button>
+        )}
+      </Menu.Trigger>
+      <Menu.Outer
+        // HACKFIX: Currently there is no height limit for Radix dropdowns,
+        // so if it's too tall it just goes off screen. TODO: fix internally -sfn
+        style={web({maxHeight: '70vh'})}>
+        <Menu.LabelText>
+          <Trans>Filter search by language</Trans>
+        </Menu.LabelText>
+        <Menu.Item label={_(msg`All languages`)} onPress={() => onChange('')}>
+          <Menu.ItemText>
+            <Trans>All languages</Trans>
+          </Menu.ItemText>
+          <Menu.ItemRadio selected={value === ''} />
+        </Menu.Item>
+        <Menu.Divider />
+        <Menu.Group>
+          {languages.map(lang => (
+            <Menu.Item
+              key={lang.key}
+              label={lang.label}
+              onPress={() => onChange(lang.value)}>
+              <Menu.ItemText>{lang.label}</Menu.ItemText>
+              <Menu.ItemRadio selected={value === lang.value} />
+            </Menu.Item>
+          ))}
+        </Menu.Group>
+      </Menu.Outer>
+    </Menu.Root>
   )
 }
 
@@ -795,22 +799,19 @@ export function SearchScreen(
               // 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.sm * -1}}>
+              style={{marginBottom: tokens.space.xs * -1}}>
               <Layout.Header.Outer noBottomBorder>
                 <Layout.Header.MenuButton />
-                <Layout.Header.Content
-                  align={showFilters ? 'left' : 'platform'}>
+                <Layout.Header.Content align="left">
                   <Layout.Header.TitleText>
                     <Trans>Search</Trans>
                   </Layout.Header.TitleText>
                 </Layout.Header.Content>
                 {showFilters ? (
-                  <View style={[{minWidth: 140}]}>
-                    <SearchLanguageDropdown
-                      value={params.lang}
-                      onChange={params.setLang}
-                    />
-                  </View>
+                  <SearchLanguageDropdown
+                    value={params.lang}
+                    onChange={params.setLang}
+                  />
                 ) : (
                   <Layout.Header.Slot />
                 )}
@@ -856,12 +857,10 @@ export function SearchScreen(
                     a.justify_between,
                     a.gap_sm,
                   ]}>
-                  <View style={[{width: 140}]}>
-                    <SearchLanguageDropdown
-                      value={params.lang}
-                      onChange={params.setLang}
-                    />
-                  </View>
+                  <SearchLanguageDropdown
+                    value={params.lang}
+                    onChange={params.setLang}
+                  />
                 </View>
               )}
             </View>
diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx
index e9ba65ed0..bb5de2eb4 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 (
-    <Menu.Outer>
+    <Menu.Outer
+      // HACKFIX: Currently there is no height limit for Radix dropdowns,
+      // so if it's too tall it just goes off screen. TODO: fix internally -sfn
+      style={web({maxHeight: '70vh'})}>
       {accounts && accounts.length > 0 && (
         <>
           <Menu.Group>