forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 💫
1import {useCallback, useMemo, useState} from 'react'
2import {View} from 'react-native'
3import {msg} from '@lingui/core/macro'
4import {useLingui} from '@lingui/react'
5import {Trans} from '@lingui/react/macro'
6
7import {
8 type CommonNavigatorParams,
9 type NativeStackScreenProps,
10} from '#/lib/routes/types'
11import {languageName, sanitizeAppLanguageSetting} from '#/locale/helpers'
12import {APP_LANGUAGES, LANGUAGES} from '#/locale/languages'
13import {useLanguagePrefs, useLanguagePrefsApi} from '#/state/preferences'
14import {atoms as a, web} from '#/alf'
15import {Admonition} from '#/components/Admonition'
16import {Button} from '#/components/Button'
17import {useDialogControl} from '#/components/Dialog'
18import {LanguageSelectDialog} from '#/components/dialogs/LanguageSelectDialog'
19import * as Toggle from '#/components/forms/Toggle'
20import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus'
21import * as Layout from '#/components/Layout'
22import * as Select from '#/components/Select'
23import {Text} from '#/components/Typography'
24import * as SettingsList from './components/SettingsList'
25
26const DEDUPED_LANGUAGES = LANGUAGES.filter(
27 (lang, i, arr) =>
28 lang.code2 && arr.findIndex(l => l.code2 === lang.code2) === i,
29)
30
31type Props = NativeStackScreenProps<CommonNavigatorParams, 'LanguageSettings'>
32export function LanguageSettingsScreen({}: Props) {
33 const {_} = useLingui()
34 const langPrefs = useLanguagePrefs()
35 const setLangPrefs = useLanguagePrefsApi()
36
37 // changing langPrefs causes a slow re-render, so we use a local state copy
38 // and update that first to drive the UI on this screen to keep it snappy
39 const [contentLanguages, _setContentLanguages] = useState(
40 langPrefs.contentLanguages,
41 )
42 const setContentLanguages = useCallback(
43 (languages: string[]) => {
44 _setContentLanguages(languages)
45 // TODO: try using startTransition/useOptimistic when we switch to New Arch
46 // Old arch doesn't support concurrent react features so use rAF instead
47 requestAnimationFrame(() => {
48 setLangPrefs.setContentLanguages(languages)
49 })
50 },
51 [setLangPrefs],
52 )
53
54 const contentLanguagePrefsControl = useDialogControl()
55
56 const onChangePrimaryLanguage = useCallback(
57 (value: string) => {
58 if (!value) return
59 if (langPrefs.primaryLanguage !== value) {
60 setLangPrefs.setPrimaryLanguage(value)
61 }
62 },
63 [langPrefs, setLangPrefs],
64 )
65
66 const onChangeAppLanguage = useCallback(
67 (value: string) => {
68 if (!value) return
69 if (langPrefs.appLanguage !== value) {
70 setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value))
71 }
72 },
73 [langPrefs, setLangPrefs],
74 )
75
76 const [recentLanguages, setRecentLanguages] = useState<string[]>(
77 langPrefs.contentLanguages,
78 )
79
80 const possibleLanguages = useMemo(() => {
81 return [
82 ...new Set([
83 ...recentLanguages,
84 ...contentLanguages,
85 ...langPrefs.primaryLanguage,
86 ]),
87 ]
88 .map(lang => LANGUAGES.find(l => l.code2 === lang))
89 .filter(x => !!x)
90 }, [recentLanguages, contentLanguages, langPrefs.primaryLanguage])
91
92 return (
93 <Layout.Screen testID="PreferencesLanguagesScreen">
94 <Layout.Header.Outer>
95 <Layout.Header.BackButton />
96 <Layout.Header.Content>
97 <Layout.Header.TitleText>
98 <Trans>Languages</Trans>
99 </Layout.Header.TitleText>
100 </Layout.Header.Content>
101 <Layout.Header.Slot />
102 </Layout.Header.Outer>
103 <Layout.Content>
104 <SettingsList.Container>
105 <SettingsList.Group iconInset={false}>
106 <SettingsList.ItemText>
107 <Trans>App language</Trans>
108 </SettingsList.ItemText>
109 <View style={[a.gap_md, a.w_full]}>
110 <Text style={[a.leading_snug]}>
111 <Trans>
112 Select which language to use for the app's user interface.
113 </Trans>
114 </Text>
115 <Select.Root
116 value={sanitizeAppLanguageSetting(langPrefs.appLanguage)}
117 onValueChange={onChangeAppLanguage}>
118 <Select.Trigger label={_(msg`Select app language`)}>
119 <Select.ValueText />
120 <Select.Icon />
121 </Select.Trigger>
122 <Select.Content
123 label={_(msg`App language`)}
124 renderItem={({label, value}) => (
125 <Select.Item value={value} label={label}>
126 <Select.ItemIndicator />
127 <Select.ItemText>{label}</Select.ItemText>
128 </Select.Item>
129 )}
130 items={APP_LANGUAGES.map(l => ({
131 label: l.name,
132 value: l.code2,
133 }))}
134 />
135 </Select.Root>
136 </View>
137 </SettingsList.Group>
138 <SettingsList.Divider />
139 <SettingsList.Group iconInset={false}>
140 <SettingsList.ItemText>
141 <Trans>Primary language</Trans>
142 </SettingsList.ItemText>
143 <View style={[a.gap_md, a.w_full]}>
144 <Text style={[a.leading_snug]}>
145 <Trans>
146 Select your preferred language for translations in your feed.
147 </Trans>
148 </Text>
149 <Select.Root
150 value={langPrefs.primaryLanguage}
151 onValueChange={onChangePrimaryLanguage}>
152 <Select.Trigger label={_(msg`Select primary language`)}>
153 <Select.ValueText />
154 <Select.Icon />
155 </Select.Trigger>
156 <Select.Content
157 label={_(msg`Primary language`)}
158 renderItem={({label, value}) => (
159 <Select.Item value={value} label={label}>
160 <Select.ItemIndicator />
161 <Select.ItemText>{label}</Select.ItemText>
162 </Select.Item>
163 )}
164 items={DEDUPED_LANGUAGES.map(l => ({
165 label: languageName(l, langPrefs.appLanguage),
166 value: l.code2,
167 })).sort((a, b) =>
168 a.label.localeCompare(b.label, langPrefs.appLanguage),
169 )}
170 />
171 </Select.Root>
172 </View>
173 </SettingsList.Group>
174 <SettingsList.Divider />
175 <SettingsList.Group iconInset={false}>
176 <SettingsList.ItemText>
177 <Trans>Content languages</Trans>
178 </SettingsList.ItemText>
179 <View style={[a.gap_md]}>
180 <Text style={[a.leading_snug]}>
181 <Trans>
182 Select which languages you want your subscribed feeds to
183 include. If none are selected, all languages will be shown.
184 </Trans>
185 </Text>
186
187 {contentLanguages.length === 0 && (
188 <Admonition type="info">
189 <Trans>All languages will be shown in your feeds.</Trans>
190 </Admonition>
191 )}
192
193 <View style={[a.w_full, web({maxWidth: 400})]}>
194 <Toggle.Group
195 label={_(msg`Select content languages`)}
196 values={contentLanguages}
197 onChange={setContentLanguages}>
198 <Toggle.PanelGroup>
199 {possibleLanguages.map((language, index) => {
200 const name = languageName(language, langPrefs.appLanguage)
201 return (
202 <Toggle.Item
203 key={language.code2}
204 name={language.code2}
205 label={name}>
206 {({selected}) => (
207 <Toggle.Panel
208 active={selected}
209 adjacent={index === 0 ? 'trailing' : 'both'}>
210 <Toggle.Checkbox />
211 <Toggle.PanelText>{name}</Toggle.PanelText>
212 </Toggle.Panel>
213 )}
214 </Toggle.Item>
215 )
216 })}
217 <Button
218 label={_(msg`Add more languages…`)}
219 onPress={contentLanguagePrefsControl.open}>
220 <Toggle.Panel adjacent="leading">
221 <Toggle.PanelIcon icon={PlusIcon} />
222 <Toggle.PanelText>
223 <Trans>Add more languages…</Trans>
224 </Toggle.PanelText>
225 </Toggle.Panel>
226 </Button>
227 </Toggle.PanelGroup>
228 </Toggle.Group>
229 </View>
230
231 <LanguageSelectDialog
232 control={contentLanguagePrefsControl}
233 titleText={<Trans>Select content languages</Trans>}
234 subtitleText={
235 <Trans>
236 If none are selected, all languages will be shown in your
237 feeds.
238 </Trans>
239 }
240 currentLanguages={contentLanguages}
241 onSelectLanguages={languages => {
242 setContentLanguages(languages)
243 setRecentLanguages(recent => [
244 ...new Set([...recent, ...languages]),
245 ])
246 }}
247 />
248 </View>
249 </SettingsList.Group>
250 </SettingsList.Container>
251 </Layout.Content>
252 </Layout.Screen>
253 )
254}