import {useCallback, useMemo, useState} from 'react'
import {useWindowDimensions, View} from 'react-native'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
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_LIQUID_GLASS, 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 {height} = useWindowDimensions()
const insets = useSafeAreaInsets()
const renderErrorBoundary = useCallback(
(error: any) => ,
[],
)
return (
)
}
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,
)
return Object.values(uniqueLanguagesMap)
}, [])
const langPrefs = useLanguagePrefs()
const [checkedLanguagesCode2, setCheckedLanguagesCode2] = useState(
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 = (
setHeaderHeight(evt.nativeEvent.layout.height)}>
{titleText ?? Choose languages}
{subtitleText && (
{subtitleText}
)}
{IS_WEB && (
)}
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})),
]
const numItems = flatListData.length
return (
{
if (item.type === 'header') {
return (
{item.label}
)
}
const lang = item.lang
const name = languageName(lang, langPrefs.appLanguage)
const isLastItem = index === numItems - 1
return (
{name}
)
}}
footer={
setFooterHeight(evt.nativeEvent.layout.height)}>
}
/>
)
}
function DialogError({details}: {details?: string}) {
const {_} = useLingui()
const control = Dialog.useDialogContext()
return (
)
}