this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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