Bluesky app fork with some witchin' additions 馃挮 witchsky.app
bluesky fork client
120
fork

Configure Feed

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

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