Restore quick language select (#8981)

* restore quick language select

* rm showCancel

* rm margin from thread button

* alf composer icons

* stop hiding keyboard

* use trans
This commit is contained in:
Samuel Newman
2025-09-05 18:34:00 +03:00
committed by GitHub
parent 0f089060d2
commit ee3e083938
4 changed files with 154 additions and 90 deletions
+2 -2
View File
@@ -78,8 +78,8 @@ const schema = z.object({
postLanguage: z.string(), postLanguage: z.string(),
/** /**
* The user's post language history, used to pre-populate the post language * The user's post language history, used to pre-populate the post language
* selector in the composer. Within each value, multiple languages are * selector in the composer. Within each value, multiple languages are separated
* separated by values. * by commas.
* *
* BCP-47 2-letter language codes without region. * BCP-47 2-letter language codes without region.
*/ */
+13 -16
View File
@@ -110,7 +110,6 @@ import {LabelsBtn} from '#/view/com/composer/labels/LabelsBtn'
import {Gallery} from '#/view/com/composer/photos/Gallery' import {Gallery} from '#/view/com/composer/photos/Gallery'
import {OpenCameraBtn} from '#/view/com/composer/photos/OpenCameraBtn' import {OpenCameraBtn} from '#/view/com/composer/photos/OpenCameraBtn'
import {SelectGifBtn} from '#/view/com/composer/photos/SelectGifBtn' 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' import {SuggestedLanguage} from '#/view/com/composer/select-language/SuggestedLanguage'
// TODO: Prevent naming components that coincide with RN primitives // TODO: Prevent naming components that coincide with RN primitives
// due to linting false positives // 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 {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, native, useTheme, web} from '#/alf' import {atoms as a, native, useTheme, web} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfoIcon} from '#/components/icons/CircleInfo'
import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons/Emoji' import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmileIcon} from '#/components/icons/Emoji'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' 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 {LazyQuoteEmbed} from '#/components/Post/Embed/LazyQuoteEmbed'
import * as Prompt from '#/components/Prompt' import * as Prompt from '#/components/Prompt'
import * as Toast from '#/components/Toast' import * as Toast from '#/components/Toast'
import {Text as NewText} from '#/components/Typography' import {Text as NewText} from '#/components/Typography'
import {BottomSheetPortalProvider} from '../../../../modules/bottom-sheet' import {BottomSheetPortalProvider} from '../../../../modules/bottom-sheet'
import {PostLanguageSelect} from './select-language/PostLanguageSelect'
import { import {
type AssetType, type AssetType,
SelectMediaButton, SelectMediaButton,
@@ -941,7 +942,7 @@ let ComposerPost = React.memo(function ComposerPost({
}) })
} }
}}> }}>
<ButtonIcon icon={X} /> <ButtonIcon icon={XIcon} />
</Button> </Button>
<Prompt.Basic <Prompt.Basic
control={discardPromptControl} control={discardPromptControl}
@@ -1430,7 +1431,7 @@ function ComposerFooter({
variant="ghost" variant="ghost"
shape="round" shape="round"
color="primary"> color="primary">
<EmojiSmile size="lg" /> <EmojiSmileIcon size="lg" />
</Button> </Button>
) : null} ) : null}
</ToolbarWrapper> </ToolbarWrapper>
@@ -1440,20 +1441,16 @@ function ComposerFooter({
<View style={[a.flex_row, a.align_center, a.justify_between]}> <View style={[a.flex_row, a.align_center, a.justify_between]}>
{showAddButton && ( {showAddButton && (
<Button <Button
label={_(msg`Add new post`)} label={_(msg`Add another post to thread`)}
onPress={onAddPost} onPress={onAddPost}
style={[a.p_sm, a.m_2xs]} style={[a.p_sm]}
variant="ghost" variant="ghost"
shape="round" shape="round"
color="primary"> color="primary">
<FontAwesomeIcon <PlusIcon size="lg" />
icon="add"
size={20}
color={t.palette.primary_500}
/>
</Button> </Button>
)} )}
<SelectPostLanguagesBtn /> <PostLanguageSelect />
<CharProgress <CharProgress
count={post.shortenedGraphemeLength} count={post.shortenedGraphemeLength}
style={{width: 65}} style={{width: 65}}
@@ -1753,7 +1750,7 @@ function ErrorBanner({
t.atoms.bg_contrast_25, t.atoms.bg_contrast_25,
]}> ]}>
<View style={[a.relative, a.flex_row, a.gap_sm, {paddingRight: 48}]}> <View style={[a.relative, a.flex_row, a.gap_sm, {paddingRight: 48}]}>
<CircleInfo fill={t.palette.negative_400} /> <CircleInfoIcon fill={t.palette.negative_400} />
<NewText style={[a.flex_1, a.leading_snug, {paddingTop: 1}]}> <NewText style={[a.flex_1, a.leading_snug, {paddingTop: 1}]}>
{error} {error}
</NewText> </NewText>
@@ -1765,7 +1762,7 @@ function ErrorBanner({
shape="round" shape="round"
style={[a.absolute, {top: 0, right: 0}]} style={[a.absolute, {top: 0, right: 0}]}
onPress={onClearError}> onPress={onClearError}>
<ButtonIcon icon={X} /> <ButtonIcon icon={XIcon} />
</Button> </Button>
</View> </View>
{videoError && videoState.jobId && ( {videoError && videoState.jobId && (
@@ -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 (
<>
<LanguageBtn onPress={languageDialogControl.open} />
<PostLanguageSelectDialog control={languageDialogControl} />
</>
)
}
return (
<>
<Menu.Root>
<Menu.Trigger label={_(msg`Select post language`)}>
{({props}) => <LanguageBtn {...props} />}
</Menu.Trigger>
<Menu.Outer>
<Menu.Group>
{dedupedHistory.map(historyItem => {
const langCodes = historyItem.split(',')
const langName = langCodes
.map(code => codeToLanguageName(code, langPrefs.appLanguage))
.join(' + ')
return (
<Menu.Item
key={historyItem}
label={_(msg`Select ${langName}`)}
onPress={() => setLangPrefs.setPostLanguage(historyItem)}>
<Menu.ItemText>{langName}</Menu.ItemText>
<Menu.ItemRadio
selected={historyItem === langPrefs.postLanguage}
/>
</Menu.Item>
)
})}
</Menu.Group>
<Menu.Divider />
<Menu.Item
label={_(msg`More languages...`)}
onPress={languageDialogControl.open}>
<Menu.ItemText>
<Trans>More languages...</Trans>
</Menu.ItemText>
<Menu.ItemIcon icon={ChevronRightIcon} />
</Menu.Item>
</Menu.Outer>
</Menu.Root>
<PostLanguageSelectDialog control={languageDialogControl} />
</>
)
}
function LanguageBtn(props: Omit<ButtonProps, 'label' | 'children'>) {
const {_} = useLingui()
const langPrefs = useLanguagePrefs()
const t = useTheme()
const postLanguagesPref = toPostLanguages(langPrefs.postLanguage)
return (
<Button
testID="selectLangBtn"
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.mr_xs]}
{...props}>
{({pressed, hovered}) => {
const color =
pressed || hovered ? 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>
)
}
@@ -1,16 +1,13 @@
import {useCallback, useMemo, useState} from 'react' import {useCallback, useMemo, useState} from 'react'
import {Keyboard, useWindowDimensions, View} from 'react-native' import {useWindowDimensions, View} from 'react-native'
import {useSafeAreaInsets} from 'react-native-safe-area-context' import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {LANG_DROPDOWN_HITSLOP} from '#/lib/constants'
import {languageName} from '#/locale/helpers' import {languageName} from '#/locale/helpers'
import {codeToLanguageName} from '#/locale/helpers'
import {type Language, LANGUAGES, LANGUAGES_MAP_CODE2} from '#/locale/languages' import {type Language, LANGUAGES, LANGUAGES_MAP_CODE2} from '#/locale/languages'
import {isNative, isWeb} from '#/platform/detection' import {isNative, isWeb} from '#/platform/detection'
import { import {
toPostLanguages,
useLanguagePrefs, useLanguagePrefs,
useLanguagePrefsApi, useLanguagePrefsApi,
} from '#/state/preferences/languages' } from '#/state/preferences/languages'
@@ -21,75 +18,14 @@ import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog' import * as Dialog from '#/components/Dialog'
import {SearchInput} from '#/components/forms/SearchInput' import {SearchInput} from '#/components/forms/SearchInput'
import * as Toggle from '#/components/forms/Toggle' 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 {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
export function SelectPostLanguagesBtn() { export function PostLanguageSelectDialog({
const {_} = useLingui() control,
const langPrefs = useLanguagePrefs() }: {
const t = useTheme() control: Dialog.DialogControlProps
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 {height} = useWindowDimensions()
const insets = useSafeAreaInsets() const insets = useSafeAreaInsets()
@@ -104,13 +40,13 @@ function LanguageDialog({control}: {control: Dialog.DialogControlProps}) {
nativeOptions={{minHeight: height - insets.top}}> nativeOptions={{minHeight: height - insets.top}}>
<Dialog.Handle /> <Dialog.Handle />
<ErrorBoundary renderError={renderErrorBoundary}> <ErrorBoundary renderError={renderErrorBoundary}>
<PostLanguagesSettingsDialogInner /> <DialogInner />
</ErrorBoundary> </ErrorBoundary>
</Dialog.Outer> </Dialog.Outer>
) )
} }
export function PostLanguagesSettingsDialogInner() { export function DialogInner() {
const control = Dialog.useDialogContext() const control = Dialog.useDialogContext()
const [headerHeight, setHeaderHeight] = useState(0) const [headerHeight, setHeaderHeight] = useState(0)