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