From ee3e08393882a9d72ae9cab5f765ed2885c5a98d Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Fri, 5 Sep 2025 18:34:00 +0300 Subject: Restore quick language select (#8981) * restore quick language select * rm showCancel * rm margin from thread button * alf composer icons * stop hiding keyboard * use trans --- src/state/persisted/schema.ts | 4 +- src/view/com/composer/Composer.tsx | 29 +- .../select-language/PostLanguageSelect.tsx | 131 +++++++ .../select-language/PostLanguageSelectDialog.tsx | 318 +++++++++++++++++ .../select-language/SelectPostLanguagesDialog.tsx | 382 --------------------- 5 files changed, 464 insertions(+), 400 deletions(-) create mode 100644 src/view/com/composer/select-language/PostLanguageSelect.tsx create mode 100644 src/view/com/composer/select-language/PostLanguageSelectDialog.tsx delete mode 100644 src/view/com/composer/select-language/SelectPostLanguagesDialog.tsx diff --git a/src/state/persisted/schema.ts b/src/state/persisted/schema.ts index f840081f3..11204f309 100644 --- a/src/state/persisted/schema.ts +++ b/src/state/persisted/schema.ts @@ -78,8 +78,8 @@ const schema = z.object({ postLanguage: z.string(), /** * The user's post language history, used to pre-populate the post language - * selector in the composer. Within each value, multiple languages are - * separated by values. + * selector in the composer. Within each value, multiple languages are separated + * by commas. * * BCP-47 2-letter language codes without region. */ diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 20f2549ad..61715a988 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -110,7 +110,6 @@ 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 {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 @@ -123,14 +122,16 @@ import {Text} from '#/view/com/util/text/Text' import {UserAvatar} from '#/view/com/util/UserAvatar' import {atoms as a, native, useTheme, web} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' -import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' -import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons/Emoji' -import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' +import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfoIcon} from '#/components/icons/CircleInfo' +import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmileIcon} from '#/components/icons/Emoji' +import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus' +import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times' import {LazyQuoteEmbed} from '#/components/Post/Embed/LazyQuoteEmbed' import * as Prompt from '#/components/Prompt' import * as Toast from '#/components/Toast' import {Text as NewText} from '#/components/Typography' import {BottomSheetPortalProvider} from '../../../../modules/bottom-sheet' +import {PostLanguageSelect} from './select-language/PostLanguageSelect' import { type AssetType, SelectMediaButton, @@ -941,7 +942,7 @@ let ComposerPost = React.memo(function ComposerPost({ }) } }}> - + - + ) : null} @@ -1440,20 +1441,16 @@ function ComposerFooter({ {showAddButton && ( )} - + - + {error} @@ -1765,7 +1762,7 @@ function ErrorBanner({ shape="round" style={[a.absolute, {top: 0, right: 0}]} onPress={onClearError}> - + {videoError && videoState.jobId && ( diff --git a/src/view/com/composer/select-language/PostLanguageSelect.tsx b/src/view/com/composer/select-language/PostLanguageSelect.tsx new file mode 100644 index 000000000..6291e8422 --- /dev/null +++ b/src/view/com/composer/select-language/PostLanguageSelect.tsx @@ -0,0 +1,131 @@ +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {LANG_DROPDOWN_HITSLOP} from '#/lib/constants' +import {codeToLanguageName} from '#/locale/helpers' +import { + toPostLanguages, + useLanguagePrefs, + useLanguagePrefsApi, +} from '#/state/preferences/languages' +import {atoms as a, useTheme} from '#/alf' +import {Button, type ButtonProps} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRightIcon} from '#/components/icons/Chevron' +import {Globe_Stroke2_Corner0_Rounded as GlobeIcon} from '#/components/icons/Globe' +import * as Menu from '#/components/Menu' +import {Text} from '#/components/Typography' +import {PostLanguageSelectDialog} from './PostLanguageSelectDialog' + +export function PostLanguageSelect() { + const {_} = useLingui() + const langPrefs = useLanguagePrefs() + const setLangPrefs = useLanguagePrefsApi() + const languageDialogControl = Dialog.useDialogControl() + + const dedupedHistory = Array.from( + new Set([...langPrefs.postLanguageHistory, langPrefs.postLanguage]), + ) + + if ( + dedupedHistory.length === 1 && + dedupedHistory[0] === langPrefs.postLanguage + ) { + return ( + <> + + + + ) + } + + return ( + <> + + + {({props}) => } + + + + {dedupedHistory.map(historyItem => { + const langCodes = historyItem.split(',') + const langName = langCodes + .map(code => codeToLanguageName(code, langPrefs.appLanguage)) + .join(' + ') + return ( + setLangPrefs.setPostLanguage(historyItem)}> + {langName} + + + ) + })} + + + + + More languages... + + + + + + + + + ) +} + +function LanguageBtn(props: Omit) { + const {_} = useLingui() + const langPrefs = useLanguagePrefs() + const t = useTheme() + + const postLanguagesPref = toPostLanguages(langPrefs.postLanguage) + + return ( + + ) +} diff --git a/src/view/com/composer/select-language/PostLanguageSelectDialog.tsx b/src/view/com/composer/select-language/PostLanguageSelectDialog.tsx new file mode 100644 index 000000000..1137415e6 --- /dev/null +++ b/src/view/com/composer/select-language/PostLanguageSelectDialog.tsx @@ -0,0 +1,318 @@ +import {useCallback, useMemo, useState} from 'react' +import {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 {languageName} from '#/locale/helpers' +import {type Language, LANGUAGES, LANGUAGES_MAP_CODE2} from '#/locale/languages' +import {isNative, isWeb} from '#/platform/detection' +import { + 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 {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times' +import {Text} from '#/components/Typography' + +export function PostLanguageSelectDialog({ + control, +}: { + control: Dialog.DialogControlProps +}) { + const {height} = useWindowDimensions() + const insets = useSafeAreaInsets() + + const renderErrorBoundary = useCallback( + (error: any) => , + [], + ) + + return ( + + + + + + + ) +} + +export function DialogInner() { + 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, + ) + + return Object.values(uniqueLanguagesMap) + }, []) + + const langPrefs = useLanguagePrefs() + const [checkedLanguagesCode2, setCheckedLanguagesCode2] = useState( + 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 = ( + setHeaderHeight(evt.nativeEvent.layout.height)}> + + + + Choose Post Languages + + + Select up to 3 languages used in this post + + + + {isWeb && ( + + )} + + + + setSearch('')} + /> + + + ) + + 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 ( + + { + if (item.type === 'header') { + return ( + + {item.label} + + ) + } + const lang = item.lang + + return ( + + + {languageName(lang, langPrefs.appLanguage)} + + + + ) + }} + footer={ + + + + } + /> + + ) +} + +function DialogError({details}: {details?: string}) { + const {_} = useLingui() + const control = Dialog.useDialogContext() + + return ( + + + + + + ) +} diff --git a/src/view/com/composer/select-language/SelectPostLanguagesDialog.tsx b/src/view/com/composer/select-language/SelectPostLanguagesDialog.tsx deleted file mode 100644 index c8ecc2b89..000000000 --- a/src/view/com/composer/select-language/SelectPostLanguagesDialog.tsx +++ /dev/null @@ -1,382 +0,0 @@ -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 ( - <> - - - - - ) -} - -function LanguageDialog({control}: {control: Dialog.DialogControlProps}) { - const {height} = useWindowDimensions() - const insets = useSafeAreaInsets() - - const renderErrorBoundary = useCallback( - (error: any) => , - [], - ) - - return ( - - - - - - - ) -} - -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, - ) - - return Object.values(uniqueLanguagesMap) - }, []) - - const langPrefs = useLanguagePrefs() - const [checkedLanguagesCode2, setCheckedLanguagesCode2] = useState( - 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 = ( - setHeaderHeight(evt.nativeEvent.layout.height)}> - - - - Choose Post Languages - - - Select up to 3 languages used in this post - - - - {isWeb && ( - - )} - - - - setSearch('')} - /> - - - ) - - 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 ( - - { - if (item.type === 'header') { - return ( - - {item.label} - - ) - } - const lang = item.lang - - return ( - - - {languageName(lang, langPrefs.appLanguage)} - - - - ) - }} - footer={ - - - - } - /> - - ) -} - -function DialogError({details}: {details?: string}) { - const {_} = useLingui() - const control = Dialog.useDialogContext() - - return ( - - - - - - ) -} -- cgit 1.4.1