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>
This commit is contained in:
@@ -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}}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user