Something went wrong. Try again.
Bluesky app fork with some witchin' additions 💫
Something went wrong. Try again.
11 kB · 367 lines
TSX
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368import {useCallback, useMemo, useState} from 'react'import {View} from 'react-native'import {msg} from '@lingui/core/macro'import {useLingui} from '@lingui/react'import {Trans} from '@lingui/react/macro'
import {languageName} from '#/locale/helpers'import {type Language, LANGUAGES, LANGUAGES_MAP_CODE2} from '#/locale/languages'import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'import {useLanguagePrefs} from '#/state/preferences/languages'import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'import {atoms as a, tokens, 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'import {IS_NATIVE, IS_WEB} from '#/env'
type FlatListItem = | { type: 'header' label: string } | { type: 'item' lang: Language }
export function LanguageSelectDialog({ titleText, subtitleText, control, /** * Optionally can be passed to show different values than what is saved in * langPrefs. */ currentLanguages, onSelectLanguages, maxLanguages,}: { control: Dialog.DialogControlProps titleText?: React.ReactNode subtitleText?: React.ReactNode /** * Defaults to the primary language */ currentLanguages?: string[] onSelectLanguages: (languages: string[]) => void maxLanguages?: number}) { const renderErrorBoundary = useCallback( (error: any) => <DialogError details={String(error)} />, [], )
return ( <Dialog.Outer control={control} nativeOptions={{fullHeight: true}}> <Dialog.Handle /> <ErrorBoundary renderError={renderErrorBoundary}> <DialogInner titleText={titleText} subtitleText={subtitleText} currentLanguages={currentLanguages} onSelectLanguages={onSelectLanguages} maxLanguages={maxLanguages} /> </ErrorBoundary> </Dialog.Outer> )}
export function DialogInner({ titleText, subtitleText, currentLanguages, onSelectLanguages, maxLanguages,}: { titleText?: React.ReactNode subtitleText?: React.ReactNode currentLanguages?: string[] onSelectLanguages?: (languages: string[]) => void maxLanguages?: number}) { const control = Dialog.useDialogContext() const [headerHeight, setHeaderHeight] = useState(0) const [footerHeight, setFooterHeight] = 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[]>( currentLanguages || [langPrefs.primaryLanguage], ) const [search, setSearch] = useState('')
const t = useTheme() const {_} = useLingui()
const enableSquareButtons = useEnableSquareButtons()
const handleClose = () => { control.close(() => { onSelectLanguages?.(checkedLanguagesCode2) }) }
// 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 searchLower = search.toLowerCase() const matchesSearch = (lang: Language) => languageName(lang, langPrefs.appLanguage) .toLowerCase() .includes(searchLower) || lang.name.toLowerCase().includes(searchLower) 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, langPrefs.appLanguage, ])
const listHeader = ( <View style={[a.pb_xs, t.atoms.bg, IS_NATIVE && 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_semi_bold, a.text_xl, a.mb_sm, ]}> {titleText ?? <Trans>Choose languages</Trans>} </Text> {subtitleText && ( <Text nativeID="dialog-description" style={[ t.atoms.text_contrast_medium, a.text_left, a.text_md, a.mb_lg, ]}> {subtitleText} </Text> )} </View>
{IS_WEB && ( <Button variant="ghost" size="small" color="secondary" shape={enableSquareButtons ? 'square' : '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})), ]
const numItems = flatListData.length
return ( <Toggle.Group values={checkedLanguagesCode2} onChange={setCheckedLanguagesCode2} type="checkbox" maxSelections={maxLanguages} label={_(msg`Select languages`)} style={web([a.contents])}> <Dialog.InnerFlatList data={flatListData} ListHeaderComponent={listHeader} stickyHeaderIndices={[0]} contentContainerStyle={[ a.gap_0, IS_NATIVE && {paddingBottom: footerHeight + tokens.space.xl}, ]} style={[IS_NATIVE && a.px_lg, IS_WEB && {paddingBottom: 120}]} scrollIndicatorInsets={{top: headerHeight, bottom: footerHeight}} renderItem={({item, index}: {item: FlatListItem; index: number}) => { if (item.type === 'header') { return ( <Text key={index} style={[ a.px_0, a.py_md, a.font_semi_bold, a.text_xs, t.atoms.text_contrast_low, a.pt_3xl, ]}> {item.label} </Text> ) } const lang = item.lang const name = languageName(lang, langPrefs.appLanguage)
const isLastItem = index === numItems - 1
return ( <Toggle.Item key={lang.code2} name={lang.code2} label={name} style={[ t.atoms.border_contrast_low, !isLastItem && a.border_b, a.rounded_0, a.px_0, a.py_md, ]}> <Toggle.LabelText style={[a.flex_1]}>{name}</Toggle.LabelText> <Toggle.Checkbox /> </Toggle.Item> ) }} footer={ <Dialog.FlatListFooter onLayout={evt => setFooterHeight(evt.nativeEvent.layout.height)}> <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> )}