Files
bsky-social-app/src/screens/Settings/LanguageSettings.tsx
T
Samuel Newman 4d45c037ec fix slow re-renders when toggling content languages
Use local state copy for content language toggles so the UI stays
responsive, deferring the expensive langPrefs update to the next frame
via requestAnimationFrame.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 16:15:02 +02:00

255 lines
9.5 KiB
TypeScript

import {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 {
type CommonNavigatorParams,
type NativeStackScreenProps,
} from '#/lib/routes/types'
import {languageName, sanitizeAppLanguageSetting} from '#/locale/helpers'
import {APP_LANGUAGES, LANGUAGES} from '#/locale/languages'
import {useLanguagePrefs, useLanguagePrefsApi} from '#/state/preferences'
import {atoms as a, web} from '#/alf'
import {Admonition} from '#/components/Admonition'
import {Button} from '#/components/Button'
import {useDialogControl} from '#/components/Dialog'
import {LanguageSelectDialog} from '#/components/dialogs/LanguageSelectDialog'
import * as Toggle from '#/components/forms/Toggle'
import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus'
import * as Layout from '#/components/Layout'
import * as Select from '#/components/Select'
import {Text} from '#/components/Typography'
import * as SettingsList from './components/SettingsList'
const DEDUPED_LANGUAGES = LANGUAGES.filter(
(lang, i, arr) =>
lang.code2 && arr.findIndex(l => l.code2 === lang.code2) === i,
)
type Props = NativeStackScreenProps<CommonNavigatorParams, 'LanguageSettings'>
export function LanguageSettingsScreen({}: Props) {
const {_} = useLingui()
const langPrefs = useLanguagePrefs()
const setLangPrefs = useLanguagePrefsApi()
// changing langPrefs causes a slow re-render, so we use a local state copy
// and update that first to drive the UI on this screen to keep it snappy
const [contentLanguages, _setContentLanguages] = useState(
langPrefs.contentLanguages,
)
const setContentLanguages = useCallback(
(languages: string[]) => {
_setContentLanguages(languages)
// TODO: try using startTransition/useOptimistic when we switch to New Arch
// Old arch doesn't support concurrent react features so use rAF instead
requestAnimationFrame(() => {
setLangPrefs.setContentLanguages(languages)
})
},
[setLangPrefs],
)
const contentLanguagePrefsControl = useDialogControl()
const onChangePrimaryLanguage = useCallback(
(value: string) => {
if (!value) return
if (langPrefs.primaryLanguage !== value) {
setLangPrefs.setPrimaryLanguage(value)
}
},
[langPrefs, setLangPrefs],
)
const onChangeAppLanguage = useCallback(
(value: string) => {
if (!value) return
if (langPrefs.appLanguage !== value) {
setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value))
}
},
[langPrefs, setLangPrefs],
)
const [recentLanguages, setRecentLanguages] = useState<string[]>(
langPrefs.contentLanguages,
)
const possibleLanguages = useMemo(() => {
return [
...new Set([
...recentLanguages,
...langPrefs.contentLanguages,
...langPrefs.primaryLanguage,
]),
]
.map(lang => LANGUAGES.find(l => l.code2 === lang))
.filter(x => !!x)
}, [recentLanguages, langPrefs.contentLanguages, langPrefs.primaryLanguage])
return (
<Layout.Screen testID="PreferencesLanguagesScreen">
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
<Layout.Header.TitleText>
<Trans>Languages</Trans>
</Layout.Header.TitleText>
</Layout.Header.Content>
<Layout.Header.Slot />
</Layout.Header.Outer>
<Layout.Content>
<SettingsList.Container>
<SettingsList.Group iconInset={false}>
<SettingsList.ItemText>
<Trans>App language</Trans>
</SettingsList.ItemText>
<View style={[a.gap_md, a.w_full]}>
<Text style={[a.leading_snug]}>
<Trans>
Select which language to use for the app's user interface.
</Trans>
</Text>
<Select.Root
value={sanitizeAppLanguageSetting(langPrefs.appLanguage)}
onValueChange={onChangeAppLanguage}>
<Select.Trigger label={_(msg`Select app language`)}>
<Select.ValueText />
<Select.Icon />
</Select.Trigger>
<Select.Content
label={_(msg`App language`)}
renderItem={({label, value}) => (
<Select.Item value={value} label={label}>
<Select.ItemIndicator />
<Select.ItemText>{label}</Select.ItemText>
</Select.Item>
)}
items={APP_LANGUAGES.map(l => ({
label: l.name,
value: l.code2,
}))}
/>
</Select.Root>
</View>
</SettingsList.Group>
<SettingsList.Divider />
<SettingsList.Group iconInset={false}>
<SettingsList.ItemText>
<Trans>Primary language</Trans>
</SettingsList.ItemText>
<View style={[a.gap_md, a.w_full]}>
<Text style={[a.leading_snug]}>
<Trans>
Select your preferred language for translations in your feed.
</Trans>
</Text>
<Select.Root
value={langPrefs.primaryLanguage}
onValueChange={onChangePrimaryLanguage}>
<Select.Trigger label={_(msg`Select primary language`)}>
<Select.ValueText />
<Select.Icon />
</Select.Trigger>
<Select.Content
label={_(msg`Primary language`)}
renderItem={({label, value}) => (
<Select.Item value={value} label={label}>
<Select.ItemIndicator />
<Select.ItemText>{label}</Select.ItemText>
</Select.Item>
)}
items={DEDUPED_LANGUAGES.map(l => ({
label: languageName(l, langPrefs.appLanguage),
value: l.code2,
})).sort((a, b) =>
a.label.localeCompare(b.label, langPrefs.appLanguage),
)}
/>
</Select.Root>
</View>
</SettingsList.Group>
<SettingsList.Divider />
<SettingsList.Group iconInset={false}>
<SettingsList.ItemText>
<Trans>Content languages</Trans>
</SettingsList.ItemText>
<View style={[a.gap_md]}>
<Text style={[a.leading_snug]}>
<Trans>
Select which languages you want your subscribed feeds to
include. If none are selected, all languages will be shown.
</Trans>
</Text>
{contentLanguages.length === 0 && (
<Admonition type="info">
<Trans>All languages will be shown in your feeds.</Trans>
</Admonition>
)}
<View style={[a.w_full, web({maxWidth: 400})]}>
<Toggle.Group
label={_(msg`Select content languages`)}
values={contentLanguages}
onChange={setContentLanguages}>
<Toggle.PanelGroup>
{possibleLanguages.map((language, index) => {
const name = languageName(language, langPrefs.appLanguage)
return (
<Toggle.Item
key={language.code2}
name={language.code2}
label={name}>
{({selected}) => (
<Toggle.Panel
active={selected}
adjacent={index === 0 ? 'trailing' : 'both'}>
<Toggle.Checkbox />
<Toggle.PanelText>{name}</Toggle.PanelText>
</Toggle.Panel>
)}
</Toggle.Item>
)
})}
<Button
label={_(msg`Add more languages…`)}
onPress={contentLanguagePrefsControl.open}>
<Toggle.Panel adjacent="leading">
<Toggle.PanelIcon icon={PlusIcon} />
<Toggle.PanelText>
<Trans>Add more languages…</Trans>
</Toggle.PanelText>
</Toggle.Panel>
</Button>
</Toggle.PanelGroup>
</Toggle.Group>
</View>
<LanguageSelectDialog
control={contentLanguagePrefsControl}
titleText={<Trans>Select content languages</Trans>}
subtitleText={
<Trans>
If none are selected, all languages will be shown in your
feeds.
</Trans>
}
currentLanguages={langPrefs.contentLanguages}
onSelectLanguages={languages => {
setContentLanguages(languages)
setRecentLanguages(recent => [
...new Set([...recent, ...languages]),
])
}}
/>
</View>
</SettingsList.Group>
</SettingsList.Container>
</Layout.Content>
</Layout.Screen>
)
}