about summary refs log tree commit diff
path: root/src/view
diff options
context:
space:
mode:
authorSamuel Newman <mozzius@protonmail.com>2025-08-27 19:00:36 +0300
committerGitHub <noreply@github.com>2025-08-27 09:00:36 -0700
commit39d460db510d6545794f6acba8226fb52b506b40 (patch)
treeb3ca00a3dfb4c790fb78369942c122cee79ba9d0 /src/view
parenteac02901435d7bc79a28e0bff665352b814f9508 (diff)
downloadvoidsky-39d460db510d6545794f6acba8226fb52b506b40.tar.zst
Language select final tweaks (#8914)
* [APP-1303] Redesign/refactor post language select (#8884)

* Nightly source-language update

* Nightly source-language update

* [APP-1303] Redesign/refactor post language select

* update: stylesheets.create to use the latest structure

* update styles to modern structure

* update: dialog breakpoints on web and delete depricated language modals

* remove unused post languages settings dialog

* restructure Post languages dialog

* place the Dialog.Close inside the Dialog.ScrollableInner

* add: language search

* update search and language variables for clarity

* fix: memoize language state lists

* chore: add comments

* update proper colors to the background

* add back older error boundary

* add: tweaks to the mobile and web responsiveness

* add tweaks to center the container

* update labels

* update button and border

* added translation updates

* Update: text input to reuse search input

* remove unused file

* update: web breakpoints

* run eslint and prettier

---------

Co-authored-by: Elijah Seed-Arita <elijaharita@gmail.com>
Co-authored-by: Anastasiya Uraleva <anastasiya@Anastasiyas-MacBook-Pro.local>
Co-authored-by: Anastasiya Uraleva <anastasiya@Mac.localdomain>

* rm old file

* sort out styles, add FlatListFooter component

* rm cancel button in favor of search input X

* get dialog height working on iOS

* delete `DropdownButton`

* hide scroll indicators on android

* ios scroll indicator insets

* get footer sorta working on android

* change button color on press

* rm empty file

---------

Co-authored-by: Anastasiya Uraleva <anastasiyauraleva@gmail.com>
Co-authored-by: Elijah Seed-Arita <elijaharita@gmail.com>
Co-authored-by: Anastasiya Uraleva <anastasiya@Anastasiyas-MacBook-Pro.local>
Co-authored-by: Anastasiya Uraleva <anastasiya@Mac.localdomain>
Diffstat (limited to 'src/view')
-rw-r--r--src/view/com/composer/Composer.tsx4
-rw-r--r--src/view/com/composer/select-language/SelectLangBtn.tsx133
-rw-r--r--src/view/com/composer/select-language/SelectPostLanguagesDialog.tsx382
-rw-r--r--src/view/com/modals/Modal.tsx4
-rw-r--r--src/view/com/modals/Modal.web.tsx3
-rw-r--r--src/view/com/modals/lang-settings/PostLanguagesSettings.tsx145
-rw-r--r--src/view/com/util/forms/DropdownButton.tsx397
-rw-r--r--src/view/screens/Debug.tsx51
8 files changed, 389 insertions, 730 deletions
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx
index 6d22e4b54..b533510ec 100644
--- a/src/view/com/composer/Composer.tsx
+++ b/src/view/com/composer/Composer.tsx
@@ -110,7 +110,7 @@ import {LabelsBtn} from '#/view/com/composer/labels/LabelsBtn'
 import {Gallery} from '#/view/com/composer/photos/Gallery'
 import {OpenCameraBtn} from '#/view/com/composer/photos/OpenCameraBtn'
 import {SelectGifBtn} from '#/view/com/composer/photos/SelectGifBtn'
-import {SelectLangBtn} from '#/view/com/composer/select-language/SelectLangBtn'
+import {SelectPostLanguagesBtn} from '#/view/com/composer/select-language/SelectPostLanguagesDialog'
 import {SuggestedLanguage} from '#/view/com/composer/select-language/SuggestedLanguage'
 // TODO: Prevent naming components that coincide with RN primitives
 // due to linting false positives
@@ -1453,7 +1453,7 @@ function ComposerFooter({
             />
           </Button>
         )}
-        <SelectLangBtn />
+        <SelectPostLanguagesBtn />
         <CharProgress
           count={post.shortenedGraphemeLength}
           style={{width: 65}}
diff --git a/src/view/com/composer/select-language/SelectLangBtn.tsx b/src/view/com/composer/select-language/SelectLangBtn.tsx
deleted file mode 100644
index f487b1244..000000000
--- a/src/view/com/composer/select-language/SelectLangBtn.tsx
+++ /dev/null
@@ -1,133 +0,0 @@
-import {useCallback, useMemo} from 'react'
-import {Keyboard, StyleSheet} from 'react-native'
-import {
-  FontAwesomeIcon,
-  FontAwesomeIconStyle,
-} from '@fortawesome/react-native-fontawesome'
-import {msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-
-import {LANG_DROPDOWN_HITSLOP} from '#/lib/constants'
-import {usePalette} from '#/lib/hooks/usePalette'
-import {isNative} from '#/platform/detection'
-import {useModalControls} from '#/state/modals'
-import {
-  hasPostLanguage,
-  toPostLanguages,
-  useLanguagePrefs,
-  useLanguagePrefsApi,
-} from '#/state/preferences/languages'
-import {
-  DropdownButton,
-  DropdownItem,
-  DropdownItemButton,
-} from '#/view/com/util/forms/DropdownButton'
-import {Text} from '#/view/com/util/text/Text'
-import {codeToLanguageName} from '../../../../locale/helpers'
-
-export function SelectLangBtn() {
-  const pal = usePalette('default')
-  const {_} = useLingui()
-  const {openModal} = useModalControls()
-  const langPrefs = useLanguagePrefs()
-  const setLangPrefs = useLanguagePrefsApi()
-
-  const onPressMore = useCallback(async () => {
-    if (isNative) {
-      if (Keyboard.isVisible()) {
-        Keyboard.dismiss()
-      }
-    }
-    openModal({name: 'post-languages-settings'})
-  }, [openModal])
-
-  const postLanguagesPref = toPostLanguages(langPrefs.postLanguage)
-  const items: DropdownItem[] = useMemo(() => {
-    let arr: DropdownItemButton[] = []
-
-    function add(commaSeparatedLangCodes: string) {
-      const langCodes = commaSeparatedLangCodes.split(',')
-      const langName = langCodes
-        .map(code => codeToLanguageName(code, langPrefs.appLanguage))
-        .join(' + ')
-
-      /*
-       * Filter out any duplicates
-       */
-      if (arr.find((item: DropdownItemButton) => item.label === langName)) {
-        return
-      }
-
-      arr.push({
-        icon:
-          langCodes.every(code =>
-            hasPostLanguage(langPrefs.postLanguage, code),
-          ) && langCodes.length === postLanguagesPref.length
-            ? ['fas', 'circle-dot']
-            : ['far', 'circle'],
-        label: langName,
-        onPress() {
-          setLangPrefs.setPostLanguage(commaSeparatedLangCodes)
-        },
-      })
-    }
-
-    if (postLanguagesPref.length) {
-      /*
-       * Re-join here after sanitization bc postLanguageHistory is an array of
-       * comma-separated strings too
-       */
-      add(langPrefs.postLanguage)
-    }
-
-    // comma-separted strings of lang codes that have been used in the past
-    for (const lang of langPrefs.postLanguageHistory) {
-      add(lang)
-    }
-
-    return [
-      {heading: true, label: _(msg`Post language`)},
-      ...arr.slice(0, 6),
-      {sep: true},
-      {
-        label: _(msg`Other...`),
-        onPress: onPressMore,
-      },
-    ]
-  }, [onPressMore, langPrefs, setLangPrefs, postLanguagesPref, _])
-
-  return (
-    <DropdownButton
-      type="bare"
-      testID="selectLangBtn"
-      items={items}
-      openUpwards
-      style={styles.button}
-      hitSlop={LANG_DROPDOWN_HITSLOP}
-      accessibilityLabel={_(msg`Language selection`)}
-      accessibilityHint="">
-      {postLanguagesPref.length > 0 ? (
-        <Text type="lg-bold" style={[pal.link, styles.label]} numberOfLines={1}>
-          {postLanguagesPref
-            .map(lang => codeToLanguageName(lang, langPrefs.appLanguage))
-            .join(', ')}
-        </Text>
-      ) : (
-        <FontAwesomeIcon
-          icon="language"
-          style={pal.link as FontAwesomeIconStyle}
-          size={26}
-        />
-      )}
-    </DropdownButton>
-  )
-}
-
-const styles = StyleSheet.create({
-  button: {
-    marginHorizontal: 15,
-  },
-  label: {
-    maxWidth: 100,
-  },
-})
diff --git a/src/view/com/composer/select-language/SelectPostLanguagesDialog.tsx b/src/view/com/composer/select-language/SelectPostLanguagesDialog.tsx
new file mode 100644
index 000000000..c8ecc2b89
--- /dev/null
+++ b/src/view/com/composer/select-language/SelectPostLanguagesDialog.tsx
@@ -0,0 +1,382 @@
+import {useCallback, useMemo, useState} from 'react'
+import {Keyboard, useWindowDimensions, View} from 'react-native'
+import {useSafeAreaInsets} from 'react-native-safe-area-context'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {LANG_DROPDOWN_HITSLOP} from '#/lib/constants'
+import {languageName} from '#/locale/helpers'
+import {codeToLanguageName} from '#/locale/helpers'
+import {type Language, LANGUAGES, LANGUAGES_MAP_CODE2} from '#/locale/languages'
+import {isNative, isWeb} from '#/platform/detection'
+import {
+  toPostLanguages,
+  useLanguagePrefs,
+  useLanguagePrefsApi,
+} from '#/state/preferences/languages'
+import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
+import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
+import {atoms as a, useTheme, web} from '#/alf'
+import {Button, ButtonIcon, ButtonText} from '#/components/Button'
+import * as Dialog from '#/components/Dialog'
+import {SearchInput} from '#/components/forms/SearchInput'
+import * as Toggle from '#/components/forms/Toggle'
+import {Globe_Stroke2_Corner0_Rounded as GlobeIcon} from '#/components/icons/Globe'
+import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
+import {Text} from '#/components/Typography'
+
+export function SelectPostLanguagesBtn() {
+  const {_} = useLingui()
+  const langPrefs = useLanguagePrefs()
+  const t = useTheme()
+  const control = Dialog.useDialogControl()
+
+  const onPressMore = useCallback(async () => {
+    if (isNative) {
+      if (Keyboard.isVisible()) {
+        Keyboard.dismiss()
+      }
+    }
+    control.open()
+  }, [control])
+
+  const postLanguagesPref = toPostLanguages(langPrefs.postLanguage)
+
+  return (
+    <>
+      <Button
+        testID="selectLangBtn"
+        onPress={onPressMore}
+        size="small"
+        hitSlop={LANG_DROPDOWN_HITSLOP}
+        label={_(
+          msg({
+            message: `Post language selection`,
+            comment: `Accessibility label for button that opens dialog to choose post language settings`,
+          }),
+        )}
+        accessibilityHint={_(msg`Opens post language settings`)}
+        style={[a.mx_md]}>
+        {({pressed, hovered, focused}) => {
+          const color =
+            pressed || hovered || focused
+              ? t.palette.primary_300
+              : t.palette.primary_500
+          if (postLanguagesPref.length > 0) {
+            return (
+              <Text
+                style={[
+                  {color},
+                  a.font_bold,
+                  a.text_sm,
+                  a.leading_snug,
+                  {maxWidth: 100},
+                ]}
+                numberOfLines={1}>
+                {postLanguagesPref
+                  .map(lang => codeToLanguageName(lang, langPrefs.appLanguage))
+                  .join(', ')}
+              </Text>
+            )
+          } else {
+            return <GlobeIcon size="xs" style={{color}} />
+          }
+        }}
+      </Button>
+
+      <LanguageDialog control={control} />
+    </>
+  )
+}
+
+function LanguageDialog({control}: {control: Dialog.DialogControlProps}) {
+  const {height} = useWindowDimensions()
+  const insets = useSafeAreaInsets()
+
+  const renderErrorBoundary = useCallback(
+    (error: any) => <DialogError details={String(error)} />,
+    [],
+  )
+
+  return (
+    <Dialog.Outer
+      control={control}
+      nativeOptions={{minHeight: height - insets.top}}>
+      <Dialog.Handle />
+      <ErrorBoundary renderError={renderErrorBoundary}>
+        <PostLanguagesSettingsDialogInner />
+      </ErrorBoundary>
+    </Dialog.Outer>
+  )
+}
+
+export function PostLanguagesSettingsDialogInner() {
+  const control = Dialog.useDialogContext()
+  const [headerHeight, setHeaderHeight] = useState(0)
+
+  const allowedLanguages = useMemo(() => {
+    const uniqueLanguagesMap = LANGUAGES.filter(lang => !!lang.code2).reduce(
+      (acc, lang) => {
+        acc[lang.code2] = lang
+        return acc
+      },
+      {} as Record<string, Language>,
+    )
+
+    return Object.values(uniqueLanguagesMap)
+  }, [])
+
+  const langPrefs = useLanguagePrefs()
+  const [checkedLanguagesCode2, setCheckedLanguagesCode2] = useState<string[]>(
+    langPrefs.postLanguage.split(',') || [langPrefs.primaryLanguage],
+  )
+  const [search, setSearch] = useState('')
+
+  const setLangPrefs = useLanguagePrefsApi()
+  const t = useTheme()
+  const {_} = useLingui()
+
+  const handleClose = () => {
+    control.close(() => {
+      let langsString = checkedLanguagesCode2.join(',')
+      if (!langsString) {
+        langsString = langPrefs.primaryLanguage
+      }
+      setLangPrefs.setPostLanguage(langsString)
+    })
+  }
+
+  // NOTE(@elijaharita): Displayed languages are split into 3 lists for
+  // ordering.
+  const displayedLanguages = useMemo(() => {
+    function mapCode2List(code2List: string[]) {
+      return code2List.map(code2 => LANGUAGES_MAP_CODE2[code2]).filter(Boolean)
+    }
+
+    // NOTE(@elijaharita): Get recent language codes and map them to language
+    // objects. Both the user account's saved language history and the current
+    // checked languages are displayed here.
+    const recentLanguagesCode2 =
+      Array.from(
+        new Set([...checkedLanguagesCode2, ...langPrefs.postLanguageHistory]),
+      ).slice(0, 5) || []
+    const recentLanguages = mapCode2List(recentLanguagesCode2)
+
+    // NOTE(@elijaharita): helper functions
+    const matchesSearch = (lang: Language) =>
+      lang.name.toLowerCase().includes(search.toLowerCase())
+    const isChecked = (lang: Language) =>
+      checkedLanguagesCode2.includes(lang.code2)
+    const isInRecents = (lang: Language) =>
+      recentLanguagesCode2.includes(lang.code2)
+
+    const checkedRecent = recentLanguages.filter(isChecked)
+
+    if (search) {
+      // NOTE(@elijaharita): if a search is active, we ALWAYS show checked
+      // items, as well as any items that match the search.
+      const uncheckedRecent = recentLanguages
+        .filter(lang => !isChecked(lang))
+        .filter(matchesSearch)
+      const unchecked = allowedLanguages.filter(lang => !isChecked(lang))
+      const all = unchecked
+        .filter(matchesSearch)
+        .filter(lang => !isInRecents(lang))
+
+      return {
+        all,
+        checkedRecent,
+        uncheckedRecent,
+      }
+    } else {
+      // NOTE(@elijaharita): if no search is active, we show everything.
+      const uncheckedRecent = recentLanguages.filter(lang => !isChecked(lang))
+      const all = allowedLanguages
+        .filter(lang => !recentLanguagesCode2.includes(lang.code2))
+        .filter(lang => !isInRecents(lang))
+
+      return {
+        all,
+        checkedRecent,
+        uncheckedRecent,
+      }
+    }
+  }, [
+    allowedLanguages,
+    search,
+    langPrefs.postLanguageHistory,
+    checkedLanguagesCode2,
+  ])
+
+  const listHeader = (
+    <View
+      style={[a.pb_xs, t.atoms.bg, isNative && a.pt_2xl]}
+      onLayout={evt => setHeaderHeight(evt.nativeEvent.layout.height)}>
+      <View style={[a.flex_row, a.w_full, a.justify_between]}>
+        <View>
+          <Text
+            nativeID="dialog-title"
+            style={[
+              t.atoms.text,
+              a.text_left,
+              a.font_bold,
+              a.text_xl,
+              a.mb_sm,
+            ]}>
+            <Trans>Choose Post Languages</Trans>
+          </Text>
+          <Text
+            nativeID="dialog-description"
+            style={[
+              t.atoms.text_contrast_medium,
+              a.text_left,
+              a.text_md,
+              a.mb_lg,
+            ]}>
+            <Trans>Select up to 3 languages used in this post</Trans>
+          </Text>
+        </View>
+
+        {isWeb && (
+          <Button
+            variant="ghost"
+            size="small"
+            color="secondary"
+            shape="round"
+            label={_(msg`Close dialog`)}
+            onPress={handleClose}>
+            <ButtonIcon icon={XIcon} />
+          </Button>
+        )}
+      </View>
+
+      <View style={[a.w_full, a.flex_row, a.align_stretch, a.gap_xs, a.pb_0]}>
+        <SearchInput
+          value={search}
+          onChangeText={setSearch}
+          placeholder={_(msg`Search languages`)}
+          label={_(msg`Search languages`)}
+          maxLength={50}
+          onClearText={() => setSearch('')}
+        />
+      </View>
+    </View>
+  )
+
+  const isCheckedRecentEmpty =
+    displayedLanguages.checkedRecent.length > 0 ||
+    displayedLanguages.uncheckedRecent.length > 0
+
+  const isDisplayedLanguagesEmpty = displayedLanguages.all.length === 0
+
+  const flatListData = [
+    ...(isCheckedRecentEmpty
+      ? [{type: 'header', label: _(msg`Recently used`)}]
+      : []),
+    ...displayedLanguages.checkedRecent.map(lang => ({type: 'item', lang})),
+    ...displayedLanguages.uncheckedRecent.map(lang => ({type: 'item', lang})),
+    ...(isDisplayedLanguagesEmpty
+      ? []
+      : [{type: 'header', label: _(msg`All languages`)}]),
+    ...displayedLanguages.all.map(lang => ({type: 'item', lang})),
+  ]
+
+  return (
+    <Toggle.Group
+      values={checkedLanguagesCode2}
+      onChange={setCheckedLanguagesCode2}
+      type="checkbox"
+      maxSelections={3}
+      label={_(msg`Select languages`)}
+      style={web([a.contents])}>
+      <Dialog.InnerFlatList
+        data={flatListData}
+        ListHeaderComponent={listHeader}
+        stickyHeaderIndices={[0]}
+        contentContainerStyle={[a.gap_0]}
+        style={[isNative && a.px_lg, web({paddingBottom: 120})]}
+        scrollIndicatorInsets={{top: headerHeight}}
+        renderItem={({item, index}) => {
+          if (item.type === 'header') {
+            return (
+              <Text
+                key={index}
+                style={[
+                  a.px_0,
+                  a.py_md,
+                  a.font_bold,
+                  a.text_xs,
+                  t.atoms.text_contrast_low,
+                  a.pt_3xl,
+                ]}>
+                {item.label}
+              </Text>
+            )
+          }
+          const lang = item.lang
+
+          return (
+            <Toggle.Item
+              key={lang.code2}
+              name={lang.code2}
+              label={languageName(lang, langPrefs.appLanguage)}
+              style={[
+                t.atoms.border_contrast_low,
+                a.border_b,
+                a.rounded_0,
+                a.px_0,
+                a.py_md,
+              ]}>
+              <Toggle.LabelText style={[a.flex_1]}>
+                {languageName(lang, langPrefs.appLanguage)}
+              </Toggle.LabelText>
+              <Toggle.Checkbox />
+            </Toggle.Item>
+          )
+        }}
+        footer={
+          <Dialog.FlatListFooter>
+            <Button
+              label={_(msg`Close dialog`)}
+              onPress={handleClose}
+              color="primary"
+              size="large">
+              <ButtonText>
+                <Trans>Done</Trans>
+              </ButtonText>
+            </Button>
+          </Dialog.FlatListFooter>
+        }
+      />
+    </Toggle.Group>
+  )
+}
+
+function DialogError({details}: {details?: string}) {
+  const {_} = useLingui()
+  const control = Dialog.useDialogContext()
+
+  return (
+    <Dialog.ScrollableInner
+      style={a.gap_md}
+      label={_(msg`An error has occurred`)}>
+      <Dialog.Close />
+      <ErrorScreen
+        title={_(msg`Oh no!`)}
+        message={_(
+          msg`There was an unexpected issue in the application. Please let us know if this happened to you!`,
+        )}
+        details={details}
+      />
+      <Button
+        label={_(msg`Close dialog`)}
+        onPress={() => control.close()}
+        color="primary"
+        size="large">
+        <ButtonText>
+          <Trans>Close</Trans>
+        </ButtonText>
+      </Button>
+    </Dialog.ScrollableInner>
+  )
+}
diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx
index c3628f939..79971e660 100644
--- a/src/view/com/modals/Modal.tsx
+++ b/src/view/com/modals/Modal.tsx
@@ -11,7 +11,6 @@ import * as CreateOrEditListModal from './CreateOrEditList'
 import * as DeleteAccountModal from './DeleteAccount'
 import * as InviteCodesModal from './InviteCodes'
 import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
-import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
 import * as UserAddRemoveListsModal from './UserAddRemoveLists'
 
 const DEFAULT_SNAPPOINTS = ['90%']
@@ -60,9 +59,6 @@ export function ModalsContainer() {
   } else if (activeModal?.name === 'content-languages-settings') {
     snapPoints = ContentLanguagesSettingsModal.snapPoints
     element = <ContentLanguagesSettingsModal.Component />
-  } else if (activeModal?.name === 'post-languages-settings') {
-    snapPoints = PostLanguagesSettingsModal.snapPoints
-    element = <PostLanguagesSettingsModal.Component />
   } else {
     return null
   }
diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx
index 08f0e2f85..d0799a390 100644
--- a/src/view/com/modals/Modal.web.tsx
+++ b/src/view/com/modals/Modal.web.tsx
@@ -10,7 +10,6 @@ import * as CreateOrEditListModal from './CreateOrEditList'
 import * as DeleteAccountModal from './DeleteAccount'
 import * as InviteCodesModal from './InviteCodes'
 import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
-import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
 import * as UserAddRemoveLists from './UserAddRemoveLists'
 
 export function ModalsContainer() {
@@ -59,8 +58,6 @@ function Modal({modal}: {modal: ModalIface}) {
     element = <InviteCodesModal.Component />
   } else if (modal.name === 'content-languages-settings') {
     element = <ContentLanguagesSettingsModal.Component />
-  } else if (modal.name === 'post-languages-settings') {
-    element = <PostLanguagesSettingsModal.Component />
   } else {
     return null
   }
diff --git a/src/view/com/modals/lang-settings/PostLanguagesSettings.tsx b/src/view/com/modals/lang-settings/PostLanguagesSettings.tsx
deleted file mode 100644
index 8c2969674..000000000
--- a/src/view/com/modals/lang-settings/PostLanguagesSettings.tsx
+++ /dev/null
@@ -1,145 +0,0 @@
-import React from 'react'
-import {StyleSheet, View} from 'react-native'
-import {Trans} from '@lingui/macro'
-
-import {usePalette} from '#/lib/hooks/usePalette'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
-import {deviceLanguageCodes} from '#/locale/deviceLocales'
-import {languageName} from '#/locale/helpers'
-import {useModalControls} from '#/state/modals'
-import {
-  hasPostLanguage,
-  useLanguagePrefs,
-  useLanguagePrefsApi,
-} from '#/state/preferences/languages'
-import {ToggleButton} from '#/view/com/util/forms/ToggleButton'
-import {LANGUAGES, LANGUAGES_MAP_CODE2} from '../../../../locale/languages'
-import {Text} from '../../util/text/Text'
-import {ScrollView} from '../util'
-import {ConfirmLanguagesButton} from './ConfirmLanguagesButton'
-
-export const snapPoints = ['100%']
-
-export function Component() {
-  const {closeModal} = useModalControls()
-  const langPrefs = useLanguagePrefs()
-  const setLangPrefs = useLanguagePrefsApi()
-  const pal = usePalette('default')
-  const {isMobile} = useWebMediaQueries()
-  const onPressDone = React.useCallback(() => {
-    closeModal()
-  }, [closeModal])
-
-  const languages = React.useMemo(() => {
-    const langs = LANGUAGES.filter(
-      lang =>
-        !!lang.code2.trim() &&
-        LANGUAGES_MAP_CODE2[lang.code2].code3 === lang.code3,
-    )
-    // sort so that device & selected languages are on top, then alphabetically
-    langs.sort((a, b) => {
-      const hasA =
-        hasPostLanguage(langPrefs.postLanguage, a.code2) ||
-        deviceLanguageCodes.includes(a.code2)
-      const hasB =
-        hasPostLanguage(langPrefs.postLanguage, b.code2) ||
-        deviceLanguageCodes.includes(b.code2)
-      if (hasA === hasB) return a.name.localeCompare(b.name)
-      if (hasA) return -1
-      return 1
-    })
-    return langs
-  }, [langPrefs])
-
-  const onPress = React.useCallback(
-    (code2: string) => {
-      setLangPrefs.togglePostLanguage(code2)
-    },
-    [setLangPrefs],
-  )
-
-  return (
-    <View
-      testID="postLanguagesModal"
-      style={[
-        pal.view,
-        styles.container,
-        // @ts-ignore vh is on web only
-        isMobile
-          ? {
-              paddingTop: 20,
-            }
-          : {
-              maxHeight: '90vh',
-            },
-      ]}>
-      <Text style={[pal.text, styles.title]}>
-        <Trans>Post Languages</Trans>
-      </Text>
-      <Text style={[pal.text, styles.description]}>
-        <Trans>Which languages are used in this post?</Trans>
-      </Text>
-      <ScrollView style={styles.scrollContainer}>
-        {languages.map(lang => {
-          const isSelected = hasPostLanguage(langPrefs.postLanguage, lang.code2)
-
-          // enforce a max of 3 selections for post languages
-          let isDisabled = false
-          if (langPrefs.postLanguage.split(',').length >= 3 && !isSelected) {
-            isDisabled = true
-          }
-
-          return (
-            <ToggleButton
-              key={lang.code2}
-              label={languageName(lang, langPrefs.appLanguage)}
-              isSelected={isSelected}
-              onPress={() => (isDisabled ? undefined : onPress(lang.code2))}
-              style={[
-                pal.border,
-                styles.languageToggle,
-                isDisabled && styles.dimmed,
-              ]}
-            />
-          )
-        })}
-        <View
-          style={{
-            height: isMobile ? 60 : 0,
-          }}
-        />
-      </ScrollView>
-      <ConfirmLanguagesButton onPress={onPressDone} />
-    </View>
-  )
-}
-
-const styles = StyleSheet.create({
-  container: {
-    flex: 1,
-  },
-  title: {
-    textAlign: 'center',
-    fontWeight: '600',
-    fontSize: 24,
-    marginBottom: 12,
-  },
-  description: {
-    textAlign: 'center',
-    paddingHorizontal: 16,
-    marginBottom: 10,
-  },
-  scrollContainer: {
-    flex: 1,
-    paddingHorizontal: 10,
-  },
-  languageToggle: {
-    borderTopWidth: 1,
-    borderRadius: 0,
-    paddingHorizontal: 6,
-    paddingVertical: 12,
-  },
-  dimmed: {
-    opacity: 0.5,
-  },
-})
diff --git a/src/view/com/util/forms/DropdownButton.tsx b/src/view/com/util/forms/DropdownButton.tsx
deleted file mode 100644
index e20dadb49..000000000
--- a/src/view/com/util/forms/DropdownButton.tsx
+++ /dev/null
@@ -1,397 +0,0 @@
-import {type PropsWithChildren} from 'react'
-import {useMemo, useRef} from 'react'
-import {
-  Dimensions,
-  type GestureResponderEvent,
-  type Insets,
-  type StyleProp,
-  StyleSheet,
-  TouchableOpacity,
-  TouchableWithoutFeedback,
-  useWindowDimensions,
-  View,
-  type ViewStyle,
-} from 'react-native'
-import Animated, {FadeIn, FadeInDown, FadeInUp} from 'react-native-reanimated'
-import RootSiblings from 'react-native-root-siblings'
-import {type IconProp} from '@fortawesome/fontawesome-svg-core'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import type React from 'react'
-
-import {HITSLOP_10} from '#/lib/constants'
-import {usePalette} from '#/lib/hooks/usePalette'
-import {colors} from '#/lib/styles'
-import {useTheme} from '#/lib/ThemeContext'
-import {isWeb} from '#/platform/detection'
-import {native} from '#/alf'
-import {FullWindowOverlay} from '#/components/FullWindowOverlay'
-import {Text} from '../text/Text'
-import {Button, type ButtonType} from './Button'
-
-const ESTIMATED_BTN_HEIGHT = 50
-const ESTIMATED_SEP_HEIGHT = 16
-const ESTIMATED_HEADING_HEIGHT = 60
-
-export interface DropdownItemButton {
-  testID?: string
-  icon?: IconProp
-  label: string
-  onPress: () => void
-}
-export interface DropdownItemSeparator {
-  sep: true
-}
-export interface DropdownItemHeading {
-  heading: true
-  label: string
-}
-export type DropdownItem =
-  | DropdownItemButton
-  | DropdownItemSeparator
-  | DropdownItemHeading
-type MaybeDropdownItem = DropdownItem | false | undefined
-
-export type DropdownButtonType = ButtonType | 'bare'
-
-interface DropdownButtonProps {
-  testID?: string
-  type?: DropdownButtonType
-  style?: StyleProp<ViewStyle>
-  items: MaybeDropdownItem[]
-  label?: string
-  menuWidth?: number
-  children?: React.ReactNode
-  openToRight?: boolean
-  openUpwards?: boolean
-  rightOffset?: number
-  bottomOffset?: number
-  hitSlop?: Insets
-  accessibilityLabel?: string
-  accessibilityHint?: string
-}
-
-/**
- * @deprecated use Menu from `#/components/Menu.tsx` instead
- */
-export function DropdownButton({
-  testID,
-  type = 'bare',
-  style,
-  items,
-  label,
-  menuWidth,
-  children,
-  openToRight = false,
-  openUpwards = false,
-  rightOffset = 0,
-  bottomOffset = 0,
-  hitSlop = HITSLOP_10,
-  accessibilityLabel,
-}: PropsWithChildren<DropdownButtonProps>) {
-  const {_} = useLingui()
-
-  const ref1 = useRef<View>(null)
-  const ref2 = useRef<View>(null)
-
-  const onPress = (e: GestureResponderEvent) => {
-    const ref = ref1.current || ref2.current
-    const {height: winHeight} = Dimensions.get('window')
-    const pressY = e.nativeEvent.pageY
-    ref?.measure(
-      (
-        _x: number,
-        _y: number,
-        width: number,
-        _height: number,
-        pageX: number,
-        pageY: number,
-      ) => {
-        if (!menuWidth) {
-          menuWidth = 200
-        }
-        let estimatedMenuHeight = 0
-        for (const item of items) {
-          if (item && isSep(item)) {
-            estimatedMenuHeight += ESTIMATED_SEP_HEIGHT
-          } else if (item && isBtn(item)) {
-            estimatedMenuHeight += ESTIMATED_BTN_HEIGHT
-          } else if (item && isHeading(item)) {
-            estimatedMenuHeight += ESTIMATED_HEADING_HEIGHT
-          }
-        }
-        const newX = openToRight
-          ? pageX + width + rightOffset
-          : pageX + width - menuWidth
-
-        // Add a bit of additional room
-        let newY = pressY + bottomOffset + 20
-        if (openUpwards || newY + estimatedMenuHeight > winHeight) {
-          newY -= estimatedMenuHeight
-        }
-        createDropdownMenu(
-          newX,
-          newY,
-          pageY,
-          menuWidth,
-          items.filter(v => !!v) as DropdownItem[],
-          openUpwards,
-        )
-      },
-    )
-  }
-
-  const numItems = useMemo(
-    () =>
-      items.filter(item => {
-        if (item === undefined || item === false) {
-          return false
-        }
-
-        return isBtn(item)
-      }).length,
-    [items],
-  )
-
-  if (type === 'bare') {
-    return (
-      <TouchableOpacity
-        testID={testID}
-        style={style}
-        onPress={onPress}
-        hitSlop={hitSlop}
-        ref={ref1}
-        accessibilityRole="button"
-        accessibilityLabel={
-          accessibilityLabel || _(msg`Opens ${numItems} options`)
-        }
-        accessibilityHint="">
-        {children}
-      </TouchableOpacity>
-    )
-  }
-  return (
-    <View ref={ref2}>
-      <Button
-        type={type}
-        testID={testID}
-        onPress={onPress}
-        style={style}
-        label={label}>
-        {children}
-      </Button>
-    </View>
-  )
-}
-
-function createDropdownMenu(
-  x: number,
-  y: number,
-  pageY: number,
-  width: number,
-  items: DropdownItem[],
-  opensUpwards = false,
-): RootSiblings {
-  const onPressItem = (index: number) => {
-    sibling.destroy()
-    const item = items[index]
-    if (isBtn(item)) {
-      item.onPress()
-    }
-  }
-  const onOuterPress = () => sibling.destroy()
-  const sibling = new RootSiblings(
-    (
-      <DropdownItems
-        onOuterPress={onOuterPress}
-        x={x}
-        y={y}
-        pageY={pageY}
-        width={width}
-        items={items}
-        onPressItem={onPressItem}
-        openUpwards={opensUpwards}
-      />
-    ),
-  )
-  return sibling
-}
-
-type DropDownItemProps = {
-  onOuterPress: () => void
-  x: number
-  y: number
-  pageY: number
-  width: number
-  items: DropdownItem[]
-  onPressItem: (index: number) => void
-  openUpwards: boolean
-}
-
-const DropdownItems = ({
-  onOuterPress,
-  x,
-  y,
-  pageY,
-  width,
-  items,
-  onPressItem,
-  openUpwards,
-}: DropDownItemProps) => {
-  const pal = usePalette('default')
-  const theme = useTheme()
-  const {_} = useLingui()
-  const {height: screenHeight} = useWindowDimensions()
-  const dropDownBackgroundColor =
-    theme.colorScheme === 'dark' ? pal.btn : pal.view
-  const separatorColor =
-    theme.colorScheme === 'dark' ? pal.borderDark : pal.border
-
-  const numItems = items.filter(isBtn).length
-
-  // TODO: Refactor dropdown components to:
-  // - (On web, if not handled by React Native) use semantic <select />
-  // and <option /> elements for keyboard navigation out of the box
-  // - (On mobile) be buttons by default, accept `label` and `nativeID`
-  // props, and always have an explicit label
-  return (
-    <FullWindowOverlay>
-      {/* This TouchableWithoutFeedback renders the background so if the user clicks outside, the dropdown closes */}
-      <TouchableWithoutFeedback
-        onPress={onOuterPress}
-        accessibilityLabel={_(msg`Toggle dropdown`)}
-        accessibilityHint="">
-        <Animated.View
-          entering={FadeIn}
-          style={[
-            styles.bg,
-            // On web we need to adjust the top and bottom relative to the scroll position
-            isWeb
-              ? {
-                  top: -pageY,
-                  bottom: pageY - screenHeight,
-                }
-              : {
-                  top: 0,
-                  bottom: 0,
-                },
-          ]}
-        />
-      </TouchableWithoutFeedback>
-      <Animated.View
-        entering={native(
-          openUpwards ? FadeInDown.springify(1000) : FadeInUp.springify(1000),
-        )}
-        style={[
-          styles.menu,
-          {left: x, top: y, width},
-          dropDownBackgroundColor,
-        ]}>
-        {items.map((item, index) => {
-          if (isBtn(item)) {
-            return (
-              <TouchableOpacity
-                testID={item.testID}
-                key={index}
-                style={[styles.menuItem]}
-                onPress={() => onPressItem(index)}
-                accessibilityRole="button"
-                accessibilityLabel={item.label}
-                accessibilityHint={_(
-                  msg`Selects option ${index + 1} of ${numItems}`,
-                )}>
-                {item.icon && (
-                  <FontAwesomeIcon
-                    style={styles.icon}
-                    icon={item.icon}
-                    color={pal.text.color as string}
-                  />
-                )}
-                <Text style={[styles.label, pal.text]}>{item.label}</Text>
-              </TouchableOpacity>
-            )
-          } else if (isSep(item)) {
-            return (
-              <View key={index} style={[styles.separator, separatorColor]} />
-            )
-          } else if (isHeading(item)) {
-            return (
-              <View style={[styles.heading, pal.border]} key={index}>
-                <Text style={[pal.text, styles.headingLabel]}>
-                  {item.label}
-                </Text>
-              </View>
-            )
-          }
-          return null
-        })}
-      </Animated.View>
-    </FullWindowOverlay>
-  )
-}
-
-function isSep(item: DropdownItem): item is DropdownItemSeparator {
-  return 'sep' in item && item.sep
-}
-function isHeading(item: DropdownItem): item is DropdownItemHeading {
-  return 'heading' in item && item.heading
-}
-function isBtn(item: DropdownItem): item is DropdownItemButton {
-  return !isSep(item) && !isHeading(item)
-}
-
-const styles = StyleSheet.create({
-  bg: {
-    position: 'absolute',
-    left: 0,
-    width: '100%',
-    backgroundColor: 'rgba(0, 0, 0, 0.1)',
-  },
-  menu: {
-    position: 'absolute',
-    backgroundColor: '#fff',
-    borderRadius: 14,
-    paddingVertical: 6,
-  },
-  menuItem: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    paddingVertical: 10,
-    paddingLeft: 15,
-    paddingRight: 40,
-  },
-  menuItemBorder: {
-    borderTopWidth: 1,
-    borderTopColor: colors.gray1,
-    marginTop: 4,
-    paddingTop: 12,
-  },
-  icon: {
-    marginLeft: 2,
-    marginRight: 8,
-    flexShrink: 0,
-  },
-  label: {
-    fontSize: 18,
-    flexShrink: 1,
-    flexGrow: 1,
-  },
-  separator: {
-    borderTopWidth: 1,
-    marginVertical: 8,
-  },
-  heading: {
-    flexDirection: 'row',
-    justifyContent: 'center',
-    paddingVertical: 10,
-    paddingLeft: 15,
-    paddingRight: 20,
-    borderBottomWidth: 1,
-    marginBottom: 6,
-  },
-  headingLabel: {
-    fontSize: 18,
-    fontWeight: '600',
-  },
-})
diff --git a/src/view/screens/Debug.tsx b/src/view/screens/Debug.tsx
index 1a236f8bc..8b81cee10 100644
--- a/src/view/screens/Debug.tsx
+++ b/src/view/screens/Debug.tsx
@@ -4,17 +4,16 @@ import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
 import {usePalette} from '#/lib/hooks/usePalette'
-import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
+import {
+  type CommonNavigatorParams,
+  type NativeStackScreenProps,
+} from '#/lib/routes/types'
 import {s} from '#/lib/styles'
-import {PaletteColorName, ThemeProvider} from '#/lib/ThemeContext'
+import {type PaletteColorName, ThemeProvider} from '#/lib/ThemeContext'
 import {EmptyState} from '#/view/com/util/EmptyState'
 import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
 import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
 import {Button} from '#/view/com/util/forms/Button'
-import {
-  DropdownButton,
-  DropdownItem,
-} from '#/view/com/util/forms/DropdownButton'
 import {ToggleButton} from '#/view/com/util/forms/ToggleButton'
 import * as LoadingPlaceholder from '#/view/com/util/LoadingPlaceholder'
 import {Text} from '#/view/com/util/text/Text'
@@ -134,8 +133,6 @@ function ControlsView() {
     <ScrollView style={[s.pl10, s.pr10]}>
       <Heading label="Buttons" />
       <ButtonsView />
-      <Heading label="Dropdown Buttons" />
-      <DropdownButtonsView />
       <Heading label="Toggle Buttons" />
       <ToggleButtonsView />
       <View style={s.footerSpacer} />
@@ -396,44 +393,6 @@ function ButtonsView() {
   )
 }
 
-const DROPDOWN_ITEMS: DropdownItem[] = [
-  {
-    icon: ['far', 'paste'],
-    label: 'Copy post text',
-    onPress() {},
-  },
-  {
-    icon: 'share',
-    label: 'Share...',
-    onPress() {},
-  },
-  {
-    icon: 'circle-exclamation',
-    label: 'Report post',
-    onPress() {},
-  },
-]
-function DropdownButtonsView() {
-  const defaultPal = usePalette('default')
-  return (
-    <View style={[defaultPal.view]}>
-      <View style={s.mb5}>
-        <DropdownButton
-          type="primary"
-          items={DROPDOWN_ITEMS}
-          menuWidth={200}
-          label="Primary button"
-        />
-      </View>
-      <View style={s.mb5}>
-        <DropdownButton type="bare" items={DROPDOWN_ITEMS} menuWidth={200}>
-          <Text>Bare</Text>
-        </DropdownButton>
-      </View>
-    </View>
-  )
-}
-
 function ToggleButtonsView() {
   const defaultPal = usePalette('default')
   const buttonStyles = s.mb5