mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import {useCallback, useMemo, useState} from 'react'
2import {useWindowDimensions, View} from 'react-native'
3import {useSafeAreaInsets} from 'react-native-safe-area-context'
4import {msg, Trans} from '@lingui/macro'
5import {useLingui} from '@lingui/react'
6
7import {languageName} from '#/locale/helpers'
8import {type Language, LANGUAGES, LANGUAGES_MAP_CODE2} from '#/locale/languages'
9import {isNative, isWeb} from '#/platform/detection'
10import {
11 useLanguagePrefs,
12 useLanguagePrefsApi,
13} from '#/state/preferences/languages'
14import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
15import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
16import {atoms as a, useTheme, web} from '#/alf'
17import {Button, ButtonIcon, ButtonText} from '#/components/Button'
18import * as Dialog from '#/components/Dialog'
19import {SearchInput} from '#/components/forms/SearchInput'
20import * as Toggle from '#/components/forms/Toggle'
21import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
22import {Text} from '#/components/Typography'
23
24export function PostLanguageSelectDialog({
25 control,
26}: {
27 control: Dialog.DialogControlProps
28}) {
29 const {height} = useWindowDimensions()
30 const insets = useSafeAreaInsets()
31
32 const renderErrorBoundary = useCallback(
33 (error: any) => <DialogError details={String(error)} />,
34 [],
35 )
36
37 return (
38 <Dialog.Outer
39 control={control}
40 nativeOptions={{minHeight: height - insets.top}}>
41 <Dialog.Handle />
42 <ErrorBoundary renderError={renderErrorBoundary}>
43 <DialogInner />
44 </ErrorBoundary>
45 </Dialog.Outer>
46 )
47}
48
49export function DialogInner() {
50 const control = Dialog.useDialogContext()
51 const [headerHeight, setHeaderHeight] = useState(0)
52
53 const allowedLanguages = useMemo(() => {
54 const uniqueLanguagesMap = LANGUAGES.filter(lang => !!lang.code2).reduce(
55 (acc, lang) => {
56 acc[lang.code2] = lang
57 return acc
58 },
59 {} as Record<string, Language>,
60 )
61
62 return Object.values(uniqueLanguagesMap)
63 }, [])
64
65 const langPrefs = useLanguagePrefs()
66 const [checkedLanguagesCode2, setCheckedLanguagesCode2] = useState<string[]>(
67 langPrefs.postLanguage.split(',') || [langPrefs.primaryLanguage],
68 )
69 const [search, setSearch] = useState('')
70
71 const setLangPrefs = useLanguagePrefsApi()
72 const t = useTheme()
73 const {_} = useLingui()
74
75 const handleClose = () => {
76 control.close(() => {
77 let langsString = checkedLanguagesCode2.join(',')
78 if (!langsString) {
79 langsString = langPrefs.primaryLanguage
80 }
81 setLangPrefs.setPostLanguage(langsString)
82 })
83 }
84
85 // NOTE(@elijaharita): Displayed languages are split into 3 lists for
86 // ordering.
87 const displayedLanguages = useMemo(() => {
88 function mapCode2List(code2List: string[]) {
89 return code2List.map(code2 => LANGUAGES_MAP_CODE2[code2]).filter(Boolean)
90 }
91
92 // NOTE(@elijaharita): Get recent language codes and map them to language
93 // objects. Both the user account's saved language history and the current
94 // checked languages are displayed here.
95 const recentLanguagesCode2 =
96 Array.from(
97 new Set([...checkedLanguagesCode2, ...langPrefs.postLanguageHistory]),
98 ).slice(0, 5) || []
99 const recentLanguages = mapCode2List(recentLanguagesCode2)
100
101 // NOTE(@elijaharita): helper functions
102 const matchesSearch = (lang: Language) =>
103 lang.name.toLowerCase().includes(search.toLowerCase())
104 const isChecked = (lang: Language) =>
105 checkedLanguagesCode2.includes(lang.code2)
106 const isInRecents = (lang: Language) =>
107 recentLanguagesCode2.includes(lang.code2)
108
109 const checkedRecent = recentLanguages.filter(isChecked)
110
111 if (search) {
112 // NOTE(@elijaharita): if a search is active, we ALWAYS show checked
113 // items, as well as any items that match the search.
114 const uncheckedRecent = recentLanguages
115 .filter(lang => !isChecked(lang))
116 .filter(matchesSearch)
117 const unchecked = allowedLanguages.filter(lang => !isChecked(lang))
118 const all = unchecked
119 .filter(matchesSearch)
120 .filter(lang => !isInRecents(lang))
121
122 return {
123 all,
124 checkedRecent,
125 uncheckedRecent,
126 }
127 } else {
128 // NOTE(@elijaharita): if no search is active, we show everything.
129 const uncheckedRecent = recentLanguages.filter(lang => !isChecked(lang))
130 const all = allowedLanguages
131 .filter(lang => !recentLanguagesCode2.includes(lang.code2))
132 .filter(lang => !isInRecents(lang))
133
134 return {
135 all,
136 checkedRecent,
137 uncheckedRecent,
138 }
139 }
140 }, [
141 allowedLanguages,
142 search,
143 langPrefs.postLanguageHistory,
144 checkedLanguagesCode2,
145 ])
146
147 const listHeader = (
148 <View
149 style={[a.pb_xs, t.atoms.bg, isNative && a.pt_2xl]}
150 onLayout={evt => setHeaderHeight(evt.nativeEvent.layout.height)}>
151 <View style={[a.flex_row, a.w_full, a.justify_between]}>
152 <View>
153 <Text
154 nativeID="dialog-title"
155 style={[
156 t.atoms.text,
157 a.text_left,
158 a.font_semi_bold,
159 a.text_xl,
160 a.mb_sm,
161 ]}>
162 <Trans>Choose Post Languages</Trans>
163 </Text>
164 <Text
165 nativeID="dialog-description"
166 style={[
167 t.atoms.text_contrast_medium,
168 a.text_left,
169 a.text_md,
170 a.mb_lg,
171 ]}>
172 <Trans>Select up to 3 languages used in this post</Trans>
173 </Text>
174 </View>
175
176 {isWeb && (
177 <Button
178 variant="ghost"
179 size="small"
180 color="secondary"
181 shape="round"
182 label={_(msg`Close dialog`)}
183 onPress={handleClose}>
184 <ButtonIcon icon={XIcon} />
185 </Button>
186 )}
187 </View>
188
189 <View style={[a.w_full, a.flex_row, a.align_stretch, a.gap_xs, a.pb_0]}>
190 <SearchInput
191 value={search}
192 onChangeText={setSearch}
193 placeholder={_(msg`Search languages`)}
194 label={_(msg`Search languages`)}
195 maxLength={50}
196 onClearText={() => setSearch('')}
197 />
198 </View>
199 </View>
200 )
201
202 const isCheckedRecentEmpty =
203 displayedLanguages.checkedRecent.length > 0 ||
204 displayedLanguages.uncheckedRecent.length > 0
205
206 const isDisplayedLanguagesEmpty = displayedLanguages.all.length === 0
207
208 const flatListData = [
209 ...(isCheckedRecentEmpty
210 ? [{type: 'header', label: _(msg`Recently used`)}]
211 : []),
212 ...displayedLanguages.checkedRecent.map(lang => ({type: 'item', lang})),
213 ...displayedLanguages.uncheckedRecent.map(lang => ({type: 'item', lang})),
214 ...(isDisplayedLanguagesEmpty
215 ? []
216 : [{type: 'header', label: _(msg`All languages`)}]),
217 ...displayedLanguages.all.map(lang => ({type: 'item', lang})),
218 ]
219
220 return (
221 <Toggle.Group
222 values={checkedLanguagesCode2}
223 onChange={setCheckedLanguagesCode2}
224 type="checkbox"
225 maxSelections={3}
226 label={_(msg`Select languages`)}
227 style={web([a.contents])}>
228 <Dialog.InnerFlatList
229 data={flatListData}
230 ListHeaderComponent={listHeader}
231 stickyHeaderIndices={[0]}
232 contentContainerStyle={[a.gap_0]}
233 style={[isNative && a.px_lg, web({paddingBottom: 120})]}
234 scrollIndicatorInsets={{top: headerHeight}}
235 renderItem={({item, index}) => {
236 if (item.type === 'header') {
237 return (
238 <Text
239 key={index}
240 style={[
241 a.px_0,
242 a.py_md,
243 a.font_semi_bold,
244 a.text_xs,
245 t.atoms.text_contrast_low,
246 a.pt_3xl,
247 ]}>
248 {item.label}
249 </Text>
250 )
251 }
252 const lang = item.lang
253
254 return (
255 <Toggle.Item
256 key={lang.code2}
257 name={lang.code2}
258 label={languageName(lang, langPrefs.appLanguage)}
259 style={[
260 t.atoms.border_contrast_low,
261 a.border_b,
262 a.rounded_0,
263 a.px_0,
264 a.py_md,
265 ]}>
266 <Toggle.LabelText style={[a.flex_1]}>
267 {languageName(lang, langPrefs.appLanguage)}
268 </Toggle.LabelText>
269 <Toggle.Checkbox />
270 </Toggle.Item>
271 )
272 }}
273 footer={
274 <Dialog.FlatListFooter>
275 <Button
276 label={_(msg`Close dialog`)}
277 onPress={handleClose}
278 color="primary"
279 size="large">
280 <ButtonText>
281 <Trans>Done</Trans>
282 </ButtonText>
283 </Button>
284 </Dialog.FlatListFooter>
285 }
286 />
287 </Toggle.Group>
288 )
289}
290
291function DialogError({details}: {details?: string}) {
292 const {_} = useLingui()
293 const control = Dialog.useDialogContext()
294
295 return (
296 <Dialog.ScrollableInner
297 style={a.gap_md}
298 label={_(msg`An error has occurred`)}>
299 <Dialog.Close />
300 <ErrorScreen
301 title={_(msg`Oh no!`)}
302 message={_(
303 msg`There was an unexpected issue in the application. Please let us know if this happened to you!`,
304 )}
305 details={details}
306 />
307 <Button
308 label={_(msg`Close dialog`)}
309 onPress={() => control.close()}
310 color="primary"
311 size="large">
312 <ButtonText>
313 <Trans>Close</Trans>
314 </ButtonText>
315 </Button>
316 </Dialog.ScrollableInner>
317 )
318}