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}