Bluesky app fork with some witchin' additions 💫

Language select final tweaks (#8914)

* [APP-1303] Redesign/refactor post language select (#8884)

* Nightly source-language update

* Nightly source-language update

* [APP-1303] Redesign/refactor post language select

* update: stylesheets.create to use the latest structure

* update styles to modern structure

* update: dialog breakpoints on web and delete depricated language modals

* remove unused post languages settings dialog

* restructure Post languages dialog

* place the Dialog.Close inside the Dialog.ScrollableInner

* add: language search

* update search and language variables for clarity

* fix: memoize language state lists

* chore: add comments

* update proper colors to the background

* add back older error boundary

* add: tweaks to the mobile and web responsiveness

* add tweaks to center the container

* update labels

* update button and border

* added translation updates

* Update: text input to reuse search input

* remove unused file

* update: web breakpoints

* run eslint and prettier

---------

Co-authored-by: Elijah Seed-Arita <elijaharita@gmail.com>
Co-authored-by: Anastasiya Uraleva <anastasiya@Anastasiyas-MacBook-Pro.local>
Co-authored-by: Anastasiya Uraleva <anastasiya@Mac.localdomain>

* rm old file

* sort out styles, add FlatListFooter component

* rm cancel button in favor of search input X

* get dialog height working on iOS

* delete `DropdownButton`

* hide scroll indicators on android

* ios scroll indicator insets

* get footer sorta working on android

* change button color on press

* rm empty file

---------

Co-authored-by: Anastasiya Uraleva <anastasiyauraleva@gmail.com>
Co-authored-by: Elijah Seed-Arita <elijaharita@gmail.com>
Co-authored-by: Anastasiya Uraleva <anastasiya@Anastasiyas-MacBook-Pro.local>
Co-authored-by: Anastasiya Uraleva <anastasiya@Mac.localdomain>

+489 -747
+3
src/alf/atoms.ts
··· 1016 1016 block: web({ 1017 1017 display: 'block', 1018 1018 }), 1019 + contents: web({ 1020 + display: 'contents', 1021 + }), 1019 1022 1020 1023 /* 1021 1024 * Transition
+55 -6
src/components/Dialog/index.tsx
··· 12 12 import { 13 13 KeyboardAwareScrollView, 14 14 useKeyboardHandler, 15 + useReanimatedKeyboardAnimation, 15 16 } from 'react-native-keyboard-controller' 16 - import {runOnJS} from 'react-native-reanimated' 17 - import {type ReanimatedScrollEvent} from 'react-native-reanimated/lib/typescript/hook/commonTypes' 17 + import Animated, { 18 + runOnJS, 19 + type ScrollEvent, 20 + useAnimatedStyle, 21 + } from 'react-native-reanimated' 18 22 import {useSafeAreaInsets} from 'react-native-safe-area-context' 19 23 import {msg} from '@lingui/macro' 20 24 import {useLingui} from '@lingui/react' ··· 26 30 import {useA11y} from '#/state/a11y' 27 31 import {useDialogStateControlContext} from '#/state/dialogs' 28 32 import {List, type ListMethods, type ListProps} from '#/view/com/util/List' 29 - import {atoms as a, tokens, useTheme} from '#/alf' 33 + import {atoms as a, ios, platform, tokens, useTheme} from '#/alf' 30 34 import {useThemeName} from '#/alf/util/useColorModeTheme' 31 35 import {Context, useDialogContext} from '#/components/Dialog/context' 32 36 import { ··· 256 260 contentContainerStyle, 257 261 ]} 258 262 ref={ref} 263 + showsVerticalScrollIndicator={isAndroid ? false : undefined} 259 264 {...props} 260 265 bounces={nativeSnapPoint === BottomSheetSnapPoint.Full} 261 266 bottomOffset={30} ··· 275 280 ListProps<any> & { 276 281 webInnerStyle?: StyleProp<ViewStyle> 277 282 webInnerContentContainerStyle?: StyleProp<ViewStyle> 283 + footer?: React.ReactNode 278 284 } 279 - >(function InnerFlatList({style, ...props}, ref) { 285 + >(function InnerFlatList({footer, style, ...props}, ref) { 280 286 const insets = useSafeAreaInsets() 281 287 const {nativeSnapPoint, disableDrag, setDisableDrag} = useDialogContext() 282 288 283 - const onScroll = (e: ReanimatedScrollEvent) => { 289 + useEnableKeyboardController(isIOS) 290 + 291 + const onScroll = (e: ScrollEvent) => { 284 292 'worklet' 285 293 if (!isAndroid) { 286 294 return ··· 300 308 bounces={nativeSnapPoint === BottomSheetSnapPoint.Full} 301 309 ListFooterComponent={<View style={{height: insets.bottom + 100}} />} 302 310 ref={ref} 311 + showsVerticalScrollIndicator={isAndroid ? false : undefined} 303 312 {...props} 304 - style={[style]} 313 + style={[a.h_full, style]} 305 314 /> 315 + {footer} 306 316 </ScrollProvider> 307 317 ) 308 318 }) 319 + 320 + export function FlatListFooter({children}: {children: React.ReactNode}) { 321 + const t = useTheme() 322 + const {top, bottom} = useSafeAreaInsets() 323 + const {height} = useReanimatedKeyboardAnimation() 324 + 325 + const animatedStyle = useAnimatedStyle(() => { 326 + if (!isIOS) return {} 327 + return { 328 + transform: [{translateY: Math.min(0, height.get() + bottom - 10)}], 329 + } 330 + }) 331 + 332 + return ( 333 + <Animated.View 334 + style={[ 335 + a.absolute, 336 + a.bottom_0, 337 + a.w_full, 338 + a.z_10, 339 + a.border_t, 340 + t.atoms.bg, 341 + t.atoms.border_contrast_low, 342 + a.px_lg, 343 + a.pt_md, 344 + { 345 + paddingBottom: platform({ 346 + ios: tokens.space.md + bottom, 347 + android: tokens.space.md + bottom + top, 348 + }), 349 + }, 350 + // TODO: had to admit defeat here, but we should 351 + // try and get this to work for Android as well -sfn 352 + ios(animatedStyle), 353 + ]}> 354 + {children} 355 + </Animated.View> 356 + ) 357 + } 309 358 310 359 export function Handle({difference = false}: {difference?: boolean}) { 311 360 const t = useTheme()
+35 -3
src/components/Dialog/index.web.tsx
··· 33 33 export * from '#/components/Dialog/utils' 34 34 export {Input} from '#/components/forms/TextField' 35 35 36 + // 100 minus 10vh of paddingVertical 37 + export const WEB_DIALOG_HEIGHT = '80vh' 38 + 36 39 const stopPropagation = (e: any) => e.stopPropagation() 37 40 const preventDefault = (e: any) => e.preventDefault() 38 41 ··· 215 218 FlatListProps<any> & {label: string} & { 216 219 webInnerStyle?: StyleProp<ViewStyle> 217 220 webInnerContentContainerStyle?: StyleProp<ViewStyle> 221 + footer?: React.ReactNode 218 222 } 219 223 >(function InnerFlatList( 220 - {label, style, webInnerStyle, webInnerContentContainerStyle, ...props}, 224 + { 225 + label, 226 + style, 227 + webInnerStyle, 228 + webInnerContentContainerStyle, 229 + footer, 230 + ...props 231 + }, 221 232 ref, 222 233 ) { 223 234 const {gtMobile} = useBreakpoints() ··· 227 238 style={[ 228 239 a.overflow_hidden, 229 240 a.px_0, 230 - // 100 minus 10vh of paddingVertical 231 - web({maxHeight: '80vh'}), 241 + web({maxHeight: WEB_DIALOG_HEIGHT}), 232 242 webInnerStyle, 233 243 ]} 234 244 contentContainerStyle={[a.h_full, a.px_0, webInnerContentContainerStyle]}> ··· 237 247 style={[a.h_full, gtMobile ? a.px_2xl : a.px_xl, flatten(style)]} 238 248 {...props} 239 249 /> 250 + {footer} 240 251 </Inner> 241 252 ) 242 253 }) 254 + 255 + export function FlatListFooter({children}: {children: React.ReactNode}) { 256 + const t = useTheme() 257 + 258 + return ( 259 + <View 260 + style={[ 261 + a.absolute, 262 + a.bottom_0, 263 + a.w_full, 264 + a.z_10, 265 + t.atoms.bg, 266 + a.border_t, 267 + t.atoms.border_contrast_low, 268 + a.px_lg, 269 + a.py_md, 270 + ]}> 271 + {children} 272 + </View> 273 + ) 274 + } 243 275 244 276 export function Close() { 245 277 const {_} = useLingui()
+4 -2
src/components/forms/Toggle.tsx
··· 1 1 import React from 'react' 2 - import {Pressable, View, type ViewStyle} from 'react-native' 2 + import {Pressable, type StyleProp, View, type ViewStyle} from 'react-native' 3 3 import Animated, {LinearTransition} from 'react-native-reanimated' 4 4 5 5 import {HITSLOP_10} from '#/lib/constants' ··· 59 59 disabled?: boolean 60 60 onChange: (value: string[]) => void 61 61 label: string 62 + style?: StyleProp<ViewStyle> 62 63 }> 63 64 64 65 export type ItemProps = ViewStyleProp & { ··· 84 85 type = 'checkbox', 85 86 maxSelections, 86 87 label, 88 + style, 87 89 }: GroupProps) { 88 90 const groupRole = type === 'radio' ? 'radiogroup' : undefined 89 91 const values = type === 'radio' ? providedValues.slice(0, 1) : providedValues ··· 136 138 return ( 137 139 <GroupContext.Provider value={context}> 138 140 <View 139 - style={[a.w_full]} 141 + style={[a.w_full, style]} 140 142 role={groupRole} 141 143 {...(groupRole === 'radiogroup' 142 144 ? {
+3 -1
src/lib/icons.tsx
··· 7 7 style, 8 8 size, 9 9 strokeWidth = 2, 10 + color = 'currentColor', 10 11 }: { 11 12 style?: StyleProp<ViewStyle> 12 13 size?: string | number 13 14 strokeWidth?: number 15 + color?: string 14 16 }) { 15 17 return ( 16 18 <Svg 17 19 fill="none" 18 20 viewBox="0 0 24 24" 19 21 strokeWidth={strokeWidth} 20 - stroke="currentColor" 22 + stroke={color} 21 23 width={size || 24} 22 24 height={size || 24} 23 25 style={style}>
-5
src/state/modals/index.tsx
··· 35 35 name: 'content-languages-settings' 36 36 } 37 37 38 - export interface PostLanguagesSettingsModal { 39 - name: 'post-languages-settings' 40 - } 41 - 42 38 /** 43 39 * @deprecated DO NOT ADD NEW MODALS 44 40 */ ··· 48 44 49 45 // Curation 50 46 | ContentLanguagesSettingsModal 51 - | PostLanguagesSettingsModal 52 47 53 48 // Lists 54 49 | CreateOrEditListModal
+2 -2
src/view/com/composer/Composer.tsx
··· 110 110 import {Gallery} from '#/view/com/composer/photos/Gallery' 111 111 import {OpenCameraBtn} from '#/view/com/composer/photos/OpenCameraBtn' 112 112 import {SelectGifBtn} from '#/view/com/composer/photos/SelectGifBtn' 113 - import {SelectLangBtn} from '#/view/com/composer/select-language/SelectLangBtn' 113 + import {SelectPostLanguagesBtn} from '#/view/com/composer/select-language/SelectPostLanguagesDialog' 114 114 import {SuggestedLanguage} from '#/view/com/composer/select-language/SuggestedLanguage' 115 115 // TODO: Prevent naming components that coincide with RN primitives 116 116 // due to linting false positives ··· 1453 1453 /> 1454 1454 </Button> 1455 1455 )} 1456 - <SelectLangBtn /> 1456 + <SelectPostLanguagesBtn /> 1457 1457 <CharProgress 1458 1458 count={post.shortenedGraphemeLength} 1459 1459 style={{width: 65}}
-133
src/view/com/composer/select-language/SelectLangBtn.tsx
··· 1 - import {useCallback, useMemo} from 'react' 2 - import {Keyboard, StyleSheet} from 'react-native' 3 - import { 4 - FontAwesomeIcon, 5 - FontAwesomeIconStyle, 6 - } from '@fortawesome/react-native-fontawesome' 7 - import {msg} from '@lingui/macro' 8 - import {useLingui} from '@lingui/react' 9 - 10 - import {LANG_DROPDOWN_HITSLOP} from '#/lib/constants' 11 - import {usePalette} from '#/lib/hooks/usePalette' 12 - import {isNative} from '#/platform/detection' 13 - import {useModalControls} from '#/state/modals' 14 - import { 15 - hasPostLanguage, 16 - toPostLanguages, 17 - useLanguagePrefs, 18 - useLanguagePrefsApi, 19 - } from '#/state/preferences/languages' 20 - import { 21 - DropdownButton, 22 - DropdownItem, 23 - DropdownItemButton, 24 - } from '#/view/com/util/forms/DropdownButton' 25 - import {Text} from '#/view/com/util/text/Text' 26 - import {codeToLanguageName} from '../../../../locale/helpers' 27 - 28 - export function SelectLangBtn() { 29 - const pal = usePalette('default') 30 - const {_} = useLingui() 31 - const {openModal} = useModalControls() 32 - const langPrefs = useLanguagePrefs() 33 - const setLangPrefs = useLanguagePrefsApi() 34 - 35 - const onPressMore = useCallback(async () => { 36 - if (isNative) { 37 - if (Keyboard.isVisible()) { 38 - Keyboard.dismiss() 39 - } 40 - } 41 - openModal({name: 'post-languages-settings'}) 42 - }, [openModal]) 43 - 44 - const postLanguagesPref = toPostLanguages(langPrefs.postLanguage) 45 - const items: DropdownItem[] = useMemo(() => { 46 - let arr: DropdownItemButton[] = [] 47 - 48 - function add(commaSeparatedLangCodes: string) { 49 - const langCodes = commaSeparatedLangCodes.split(',') 50 - const langName = langCodes 51 - .map(code => codeToLanguageName(code, langPrefs.appLanguage)) 52 - .join(' + ') 53 - 54 - /* 55 - * Filter out any duplicates 56 - */ 57 - if (arr.find((item: DropdownItemButton) => item.label === langName)) { 58 - return 59 - } 60 - 61 - arr.push({ 62 - icon: 63 - langCodes.every(code => 64 - hasPostLanguage(langPrefs.postLanguage, code), 65 - ) && langCodes.length === postLanguagesPref.length 66 - ? ['fas', 'circle-dot'] 67 - : ['far', 'circle'], 68 - label: langName, 69 - onPress() { 70 - setLangPrefs.setPostLanguage(commaSeparatedLangCodes) 71 - }, 72 - }) 73 - } 74 - 75 - if (postLanguagesPref.length) { 76 - /* 77 - * Re-join here after sanitization bc postLanguageHistory is an array of 78 - * comma-separated strings too 79 - */ 80 - add(langPrefs.postLanguage) 81 - } 82 - 83 - // comma-separted strings of lang codes that have been used in the past 84 - for (const lang of langPrefs.postLanguageHistory) { 85 - add(lang) 86 - } 87 - 88 - return [ 89 - {heading: true, label: _(msg`Post language`)}, 90 - ...arr.slice(0, 6), 91 - {sep: true}, 92 - { 93 - label: _(msg`Other...`), 94 - onPress: onPressMore, 95 - }, 96 - ] 97 - }, [onPressMore, langPrefs, setLangPrefs, postLanguagesPref, _]) 98 - 99 - return ( 100 - <DropdownButton 101 - type="bare" 102 - testID="selectLangBtn" 103 - items={items} 104 - openUpwards 105 - style={styles.button} 106 - hitSlop={LANG_DROPDOWN_HITSLOP} 107 - accessibilityLabel={_(msg`Language selection`)} 108 - accessibilityHint=""> 109 - {postLanguagesPref.length > 0 ? ( 110 - <Text type="lg-bold" style={[pal.link, styles.label]} numberOfLines={1}> 111 - {postLanguagesPref 112 - .map(lang => codeToLanguageName(lang, langPrefs.appLanguage)) 113 - .join(', ')} 114 - </Text> 115 - ) : ( 116 - <FontAwesomeIcon 117 - icon="language" 118 - style={pal.link as FontAwesomeIconStyle} 119 - size={26} 120 - /> 121 - )} 122 - </DropdownButton> 123 - ) 124 - } 125 - 126 - const styles = StyleSheet.create({ 127 - button: { 128 - marginHorizontal: 15, 129 - }, 130 - label: { 131 - maxWidth: 100, 132 - }, 133 - })
+382
src/view/com/composer/select-language/SelectPostLanguagesDialog.tsx
··· 1 + import {useCallback, useMemo, useState} from 'react' 2 + import {Keyboard, useWindowDimensions, View} from 'react-native' 3 + import {useSafeAreaInsets} from 'react-native-safe-area-context' 4 + import {msg, Trans} from '@lingui/macro' 5 + import {useLingui} from '@lingui/react' 6 + 7 + import {LANG_DROPDOWN_HITSLOP} from '#/lib/constants' 8 + import {languageName} from '#/locale/helpers' 9 + import {codeToLanguageName} from '#/locale/helpers' 10 + import {type Language, LANGUAGES, LANGUAGES_MAP_CODE2} from '#/locale/languages' 11 + import {isNative, isWeb} from '#/platform/detection' 12 + import { 13 + toPostLanguages, 14 + useLanguagePrefs, 15 + useLanguagePrefsApi, 16 + } from '#/state/preferences/languages' 17 + import {ErrorScreen} from '#/view/com/util/error/ErrorScreen' 18 + import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' 19 + import {atoms as a, useTheme, web} from '#/alf' 20 + import {Button, ButtonIcon, ButtonText} from '#/components/Button' 21 + import * as Dialog from '#/components/Dialog' 22 + import {SearchInput} from '#/components/forms/SearchInput' 23 + import * as Toggle from '#/components/forms/Toggle' 24 + import {Globe_Stroke2_Corner0_Rounded as GlobeIcon} from '#/components/icons/Globe' 25 + import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times' 26 + import {Text} from '#/components/Typography' 27 + 28 + export function SelectPostLanguagesBtn() { 29 + const {_} = useLingui() 30 + const langPrefs = useLanguagePrefs() 31 + const t = useTheme() 32 + const control = Dialog.useDialogControl() 33 + 34 + const onPressMore = useCallback(async () => { 35 + if (isNative) { 36 + if (Keyboard.isVisible()) { 37 + Keyboard.dismiss() 38 + } 39 + } 40 + control.open() 41 + }, [control]) 42 + 43 + const postLanguagesPref = toPostLanguages(langPrefs.postLanguage) 44 + 45 + return ( 46 + <> 47 + <Button 48 + testID="selectLangBtn" 49 + onPress={onPressMore} 50 + size="small" 51 + hitSlop={LANG_DROPDOWN_HITSLOP} 52 + label={_( 53 + msg({ 54 + message: `Post language selection`, 55 + comment: `Accessibility label for button that opens dialog to choose post language settings`, 56 + }), 57 + )} 58 + accessibilityHint={_(msg`Opens post language settings`)} 59 + style={[a.mx_md]}> 60 + {({pressed, hovered, focused}) => { 61 + const color = 62 + pressed || hovered || focused 63 + ? t.palette.primary_300 64 + : t.palette.primary_500 65 + if (postLanguagesPref.length > 0) { 66 + return ( 67 + <Text 68 + style={[ 69 + {color}, 70 + a.font_bold, 71 + a.text_sm, 72 + a.leading_snug, 73 + {maxWidth: 100}, 74 + ]} 75 + numberOfLines={1}> 76 + {postLanguagesPref 77 + .map(lang => codeToLanguageName(lang, langPrefs.appLanguage)) 78 + .join(', ')} 79 + </Text> 80 + ) 81 + } else { 82 + return <GlobeIcon size="xs" style={{color}} /> 83 + } 84 + }} 85 + </Button> 86 + 87 + <LanguageDialog control={control} /> 88 + </> 89 + ) 90 + } 91 + 92 + function LanguageDialog({control}: {control: Dialog.DialogControlProps}) { 93 + const {height} = useWindowDimensions() 94 + const insets = useSafeAreaInsets() 95 + 96 + const renderErrorBoundary = useCallback( 97 + (error: any) => <DialogError details={String(error)} />, 98 + [], 99 + ) 100 + 101 + return ( 102 + <Dialog.Outer 103 + control={control} 104 + nativeOptions={{minHeight: height - insets.top}}> 105 + <Dialog.Handle /> 106 + <ErrorBoundary renderError={renderErrorBoundary}> 107 + <PostLanguagesSettingsDialogInner /> 108 + </ErrorBoundary> 109 + </Dialog.Outer> 110 + ) 111 + } 112 + 113 + export function PostLanguagesSettingsDialogInner() { 114 + const control = Dialog.useDialogContext() 115 + const [headerHeight, setHeaderHeight] = useState(0) 116 + 117 + const allowedLanguages = useMemo(() => { 118 + const uniqueLanguagesMap = LANGUAGES.filter(lang => !!lang.code2).reduce( 119 + (acc, lang) => { 120 + acc[lang.code2] = lang 121 + return acc 122 + }, 123 + {} as Record<string, Language>, 124 + ) 125 + 126 + return Object.values(uniqueLanguagesMap) 127 + }, []) 128 + 129 + const langPrefs = useLanguagePrefs() 130 + const [checkedLanguagesCode2, setCheckedLanguagesCode2] = useState<string[]>( 131 + langPrefs.postLanguage.split(',') || [langPrefs.primaryLanguage], 132 + ) 133 + const [search, setSearch] = useState('') 134 + 135 + const setLangPrefs = useLanguagePrefsApi() 136 + const t = useTheme() 137 + const {_} = useLingui() 138 + 139 + const handleClose = () => { 140 + control.close(() => { 141 + let langsString = checkedLanguagesCode2.join(',') 142 + if (!langsString) { 143 + langsString = langPrefs.primaryLanguage 144 + } 145 + setLangPrefs.setPostLanguage(langsString) 146 + }) 147 + } 148 + 149 + // NOTE(@elijaharita): Displayed languages are split into 3 lists for 150 + // ordering. 151 + const displayedLanguages = useMemo(() => { 152 + function mapCode2List(code2List: string[]) { 153 + return code2List.map(code2 => LANGUAGES_MAP_CODE2[code2]).filter(Boolean) 154 + } 155 + 156 + // NOTE(@elijaharita): Get recent language codes and map them to language 157 + // objects. Both the user account's saved language history and the current 158 + // checked languages are displayed here. 159 + const recentLanguagesCode2 = 160 + Array.from( 161 + new Set([...checkedLanguagesCode2, ...langPrefs.postLanguageHistory]), 162 + ).slice(0, 5) || [] 163 + const recentLanguages = mapCode2List(recentLanguagesCode2) 164 + 165 + // NOTE(@elijaharita): helper functions 166 + const matchesSearch = (lang: Language) => 167 + lang.name.toLowerCase().includes(search.toLowerCase()) 168 + const isChecked = (lang: Language) => 169 + checkedLanguagesCode2.includes(lang.code2) 170 + const isInRecents = (lang: Language) => 171 + recentLanguagesCode2.includes(lang.code2) 172 + 173 + const checkedRecent = recentLanguages.filter(isChecked) 174 + 175 + if (search) { 176 + // NOTE(@elijaharita): if a search is active, we ALWAYS show checked 177 + // items, as well as any items that match the search. 178 + const uncheckedRecent = recentLanguages 179 + .filter(lang => !isChecked(lang)) 180 + .filter(matchesSearch) 181 + const unchecked = allowedLanguages.filter(lang => !isChecked(lang)) 182 + const all = unchecked 183 + .filter(matchesSearch) 184 + .filter(lang => !isInRecents(lang)) 185 + 186 + return { 187 + all, 188 + checkedRecent, 189 + uncheckedRecent, 190 + } 191 + } else { 192 + // NOTE(@elijaharita): if no search is active, we show everything. 193 + const uncheckedRecent = recentLanguages.filter(lang => !isChecked(lang)) 194 + const all = allowedLanguages 195 + .filter(lang => !recentLanguagesCode2.includes(lang.code2)) 196 + .filter(lang => !isInRecents(lang)) 197 + 198 + return { 199 + all, 200 + checkedRecent, 201 + uncheckedRecent, 202 + } 203 + } 204 + }, [ 205 + allowedLanguages, 206 + search, 207 + langPrefs.postLanguageHistory, 208 + checkedLanguagesCode2, 209 + ]) 210 + 211 + const listHeader = ( 212 + <View 213 + style={[a.pb_xs, t.atoms.bg, isNative && a.pt_2xl]} 214 + onLayout={evt => setHeaderHeight(evt.nativeEvent.layout.height)}> 215 + <View style={[a.flex_row, a.w_full, a.justify_between]}> 216 + <View> 217 + <Text 218 + nativeID="dialog-title" 219 + style={[ 220 + t.atoms.text, 221 + a.text_left, 222 + a.font_bold, 223 + a.text_xl, 224 + a.mb_sm, 225 + ]}> 226 + <Trans>Choose Post Languages</Trans> 227 + </Text> 228 + <Text 229 + nativeID="dialog-description" 230 + style={[ 231 + t.atoms.text_contrast_medium, 232 + a.text_left, 233 + a.text_md, 234 + a.mb_lg, 235 + ]}> 236 + <Trans>Select up to 3 languages used in this post</Trans> 237 + </Text> 238 + </View> 239 + 240 + {isWeb && ( 241 + <Button 242 + variant="ghost" 243 + size="small" 244 + color="secondary" 245 + shape="round" 246 + label={_(msg`Close dialog`)} 247 + onPress={handleClose}> 248 + <ButtonIcon icon={XIcon} /> 249 + </Button> 250 + )} 251 + </View> 252 + 253 + <View style={[a.w_full, a.flex_row, a.align_stretch, a.gap_xs, a.pb_0]}> 254 + <SearchInput 255 + value={search} 256 + onChangeText={setSearch} 257 + placeholder={_(msg`Search languages`)} 258 + label={_(msg`Search languages`)} 259 + maxLength={50} 260 + onClearText={() => setSearch('')} 261 + /> 262 + </View> 263 + </View> 264 + ) 265 + 266 + const isCheckedRecentEmpty = 267 + displayedLanguages.checkedRecent.length > 0 || 268 + displayedLanguages.uncheckedRecent.length > 0 269 + 270 + const isDisplayedLanguagesEmpty = displayedLanguages.all.length === 0 271 + 272 + const flatListData = [ 273 + ...(isCheckedRecentEmpty 274 + ? [{type: 'header', label: _(msg`Recently used`)}] 275 + : []), 276 + ...displayedLanguages.checkedRecent.map(lang => ({type: 'item', lang})), 277 + ...displayedLanguages.uncheckedRecent.map(lang => ({type: 'item', lang})), 278 + ...(isDisplayedLanguagesEmpty 279 + ? [] 280 + : [{type: 'header', label: _(msg`All languages`)}]), 281 + ...displayedLanguages.all.map(lang => ({type: 'item', lang})), 282 + ] 283 + 284 + return ( 285 + <Toggle.Group 286 + values={checkedLanguagesCode2} 287 + onChange={setCheckedLanguagesCode2} 288 + type="checkbox" 289 + maxSelections={3} 290 + label={_(msg`Select languages`)} 291 + style={web([a.contents])}> 292 + <Dialog.InnerFlatList 293 + data={flatListData} 294 + ListHeaderComponent={listHeader} 295 + stickyHeaderIndices={[0]} 296 + contentContainerStyle={[a.gap_0]} 297 + style={[isNative && a.px_lg, web({paddingBottom: 120})]} 298 + scrollIndicatorInsets={{top: headerHeight}} 299 + renderItem={({item, index}) => { 300 + if (item.type === 'header') { 301 + return ( 302 + <Text 303 + key={index} 304 + style={[ 305 + a.px_0, 306 + a.py_md, 307 + a.font_bold, 308 + a.text_xs, 309 + t.atoms.text_contrast_low, 310 + a.pt_3xl, 311 + ]}> 312 + {item.label} 313 + </Text> 314 + ) 315 + } 316 + const lang = item.lang 317 + 318 + return ( 319 + <Toggle.Item 320 + key={lang.code2} 321 + name={lang.code2} 322 + label={languageName(lang, langPrefs.appLanguage)} 323 + style={[ 324 + t.atoms.border_contrast_low, 325 + a.border_b, 326 + a.rounded_0, 327 + a.px_0, 328 + a.py_md, 329 + ]}> 330 + <Toggle.LabelText style={[a.flex_1]}> 331 + {languageName(lang, langPrefs.appLanguage)} 332 + </Toggle.LabelText> 333 + <Toggle.Checkbox /> 334 + </Toggle.Item> 335 + ) 336 + }} 337 + footer={ 338 + <Dialog.FlatListFooter> 339 + <Button 340 + label={_(msg`Close dialog`)} 341 + onPress={handleClose} 342 + color="primary" 343 + size="large"> 344 + <ButtonText> 345 + <Trans>Done</Trans> 346 + </ButtonText> 347 + </Button> 348 + </Dialog.FlatListFooter> 349 + } 350 + /> 351 + </Toggle.Group> 352 + ) 353 + } 354 + 355 + function DialogError({details}: {details?: string}) { 356 + const {_} = useLingui() 357 + const control = Dialog.useDialogContext() 358 + 359 + return ( 360 + <Dialog.ScrollableInner 361 + style={a.gap_md} 362 + label={_(msg`An error has occurred`)}> 363 + <Dialog.Close /> 364 + <ErrorScreen 365 + title={_(msg`Oh no!`)} 366 + message={_( 367 + msg`There was an unexpected issue in the application. Please let us know if this happened to you!`, 368 + )} 369 + details={details} 370 + /> 371 + <Button 372 + label={_(msg`Close dialog`)} 373 + onPress={() => control.close()} 374 + color="primary" 375 + size="large"> 376 + <ButtonText> 377 + <Trans>Close</Trans> 378 + </ButtonText> 379 + </Button> 380 + </Dialog.ScrollableInner> 381 + ) 382 + }
-4
src/view/com/modals/Modal.tsx
··· 11 11 import * as DeleteAccountModal from './DeleteAccount' 12 12 import * as InviteCodesModal from './InviteCodes' 13 13 import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings' 14 - import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings' 15 14 import * as UserAddRemoveListsModal from './UserAddRemoveLists' 16 15 17 16 const DEFAULT_SNAPPOINTS = ['90%'] ··· 60 59 } else if (activeModal?.name === 'content-languages-settings') { 61 60 snapPoints = ContentLanguagesSettingsModal.snapPoints 62 61 element = <ContentLanguagesSettingsModal.Component /> 63 - } else if (activeModal?.name === 'post-languages-settings') { 64 - snapPoints = PostLanguagesSettingsModal.snapPoints 65 - element = <PostLanguagesSettingsModal.Component /> 66 62 } else { 67 63 return null 68 64 }
-3
src/view/com/modals/Modal.web.tsx
··· 10 10 import * as DeleteAccountModal from './DeleteAccount' 11 11 import * as InviteCodesModal from './InviteCodes' 12 12 import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings' 13 - import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings' 14 13 import * as UserAddRemoveLists from './UserAddRemoveLists' 15 14 16 15 export function ModalsContainer() { ··· 59 58 element = <InviteCodesModal.Component /> 60 59 } else if (modal.name === 'content-languages-settings') { 61 60 element = <ContentLanguagesSettingsModal.Component /> 62 - } else if (modal.name === 'post-languages-settings') { 63 - element = <PostLanguagesSettingsModal.Component /> 64 61 } else { 65 62 return null 66 63 }
-145
src/view/com/modals/lang-settings/PostLanguagesSettings.tsx
··· 1 - import React from 'react' 2 - import {StyleSheet, View} from 'react-native' 3 - import {Trans} from '@lingui/macro' 4 - 5 - import {usePalette} from '#/lib/hooks/usePalette' 6 - import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 7 - import {deviceLanguageCodes} from '#/locale/deviceLocales' 8 - import {languageName} from '#/locale/helpers' 9 - import {useModalControls} from '#/state/modals' 10 - import { 11 - hasPostLanguage, 12 - useLanguagePrefs, 13 - useLanguagePrefsApi, 14 - } from '#/state/preferences/languages' 15 - import {ToggleButton} from '#/view/com/util/forms/ToggleButton' 16 - import {LANGUAGES, LANGUAGES_MAP_CODE2} from '../../../../locale/languages' 17 - import {Text} from '../../util/text/Text' 18 - import {ScrollView} from '../util' 19 - import {ConfirmLanguagesButton} from './ConfirmLanguagesButton' 20 - 21 - export const snapPoints = ['100%'] 22 - 23 - export function Component() { 24 - const {closeModal} = useModalControls() 25 - const langPrefs = useLanguagePrefs() 26 - const setLangPrefs = useLanguagePrefsApi() 27 - const pal = usePalette('default') 28 - const {isMobile} = useWebMediaQueries() 29 - const onPressDone = React.useCallback(() => { 30 - closeModal() 31 - }, [closeModal]) 32 - 33 - const languages = React.useMemo(() => { 34 - const langs = LANGUAGES.filter( 35 - lang => 36 - !!lang.code2.trim() && 37 - LANGUAGES_MAP_CODE2[lang.code2].code3 === lang.code3, 38 - ) 39 - // sort so that device & selected languages are on top, then alphabetically 40 - langs.sort((a, b) => { 41 - const hasA = 42 - hasPostLanguage(langPrefs.postLanguage, a.code2) || 43 - deviceLanguageCodes.includes(a.code2) 44 - const hasB = 45 - hasPostLanguage(langPrefs.postLanguage, b.code2) || 46 - deviceLanguageCodes.includes(b.code2) 47 - if (hasA === hasB) return a.name.localeCompare(b.name) 48 - if (hasA) return -1 49 - return 1 50 - }) 51 - return langs 52 - }, [langPrefs]) 53 - 54 - const onPress = React.useCallback( 55 - (code2: string) => { 56 - setLangPrefs.togglePostLanguage(code2) 57 - }, 58 - [setLangPrefs], 59 - ) 60 - 61 - return ( 62 - <View 63 - testID="postLanguagesModal" 64 - style={[ 65 - pal.view, 66 - styles.container, 67 - // @ts-ignore vh is on web only 68 - isMobile 69 - ? { 70 - paddingTop: 20, 71 - } 72 - : { 73 - maxHeight: '90vh', 74 - }, 75 - ]}> 76 - <Text style={[pal.text, styles.title]}> 77 - <Trans>Post Languages</Trans> 78 - </Text> 79 - <Text style={[pal.text, styles.description]}> 80 - <Trans>Which languages are used in this post?</Trans> 81 - </Text> 82 - <ScrollView style={styles.scrollContainer}> 83 - {languages.map(lang => { 84 - const isSelected = hasPostLanguage(langPrefs.postLanguage, lang.code2) 85 - 86 - // enforce a max of 3 selections for post languages 87 - let isDisabled = false 88 - if (langPrefs.postLanguage.split(',').length >= 3 && !isSelected) { 89 - isDisabled = true 90 - } 91 - 92 - return ( 93 - <ToggleButton 94 - key={lang.code2} 95 - label={languageName(lang, langPrefs.appLanguage)} 96 - isSelected={isSelected} 97 - onPress={() => (isDisabled ? undefined : onPress(lang.code2))} 98 - style={[ 99 - pal.border, 100 - styles.languageToggle, 101 - isDisabled && styles.dimmed, 102 - ]} 103 - /> 104 - ) 105 - })} 106 - <View 107 - style={{ 108 - height: isMobile ? 60 : 0, 109 - }} 110 - /> 111 - </ScrollView> 112 - <ConfirmLanguagesButton onPress={onPressDone} /> 113 - </View> 114 - ) 115 - } 116 - 117 - const styles = StyleSheet.create({ 118 - container: { 119 - flex: 1, 120 - }, 121 - title: { 122 - textAlign: 'center', 123 - fontWeight: '600', 124 - fontSize: 24, 125 - marginBottom: 12, 126 - }, 127 - description: { 128 - textAlign: 'center', 129 - paddingHorizontal: 16, 130 - marginBottom: 10, 131 - }, 132 - scrollContainer: { 133 - flex: 1, 134 - paddingHorizontal: 10, 135 - }, 136 - languageToggle: { 137 - borderTopWidth: 1, 138 - borderRadius: 0, 139 - paddingHorizontal: 6, 140 - paddingVertical: 12, 141 - }, 142 - dimmed: { 143 - opacity: 0.5, 144 - }, 145 - })
-397
src/view/com/util/forms/DropdownButton.tsx
··· 1 - import {type PropsWithChildren} from 'react' 2 - import {useMemo, useRef} from 'react' 3 - import { 4 - Dimensions, 5 - type GestureResponderEvent, 6 - type Insets, 7 - type StyleProp, 8 - StyleSheet, 9 - TouchableOpacity, 10 - TouchableWithoutFeedback, 11 - useWindowDimensions, 12 - View, 13 - type ViewStyle, 14 - } from 'react-native' 15 - import Animated, {FadeIn, FadeInDown, FadeInUp} from 'react-native-reanimated' 16 - import RootSiblings from 'react-native-root-siblings' 17 - import {type IconProp} from '@fortawesome/fontawesome-svg-core' 18 - import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 19 - import {msg} from '@lingui/macro' 20 - import {useLingui} from '@lingui/react' 21 - import type React from 'react' 22 - 23 - import {HITSLOP_10} from '#/lib/constants' 24 - import {usePalette} from '#/lib/hooks/usePalette' 25 - import {colors} from '#/lib/styles' 26 - import {useTheme} from '#/lib/ThemeContext' 27 - import {isWeb} from '#/platform/detection' 28 - import {native} from '#/alf' 29 - import {FullWindowOverlay} from '#/components/FullWindowOverlay' 30 - import {Text} from '../text/Text' 31 - import {Button, type ButtonType} from './Button' 32 - 33 - const ESTIMATED_BTN_HEIGHT = 50 34 - const ESTIMATED_SEP_HEIGHT = 16 35 - const ESTIMATED_HEADING_HEIGHT = 60 36 - 37 - export interface DropdownItemButton { 38 - testID?: string 39 - icon?: IconProp 40 - label: string 41 - onPress: () => void 42 - } 43 - export interface DropdownItemSeparator { 44 - sep: true 45 - } 46 - export interface DropdownItemHeading { 47 - heading: true 48 - label: string 49 - } 50 - export type DropdownItem = 51 - | DropdownItemButton 52 - | DropdownItemSeparator 53 - | DropdownItemHeading 54 - type MaybeDropdownItem = DropdownItem | false | undefined 55 - 56 - export type DropdownButtonType = ButtonType | 'bare' 57 - 58 - interface DropdownButtonProps { 59 - testID?: string 60 - type?: DropdownButtonType 61 - style?: StyleProp<ViewStyle> 62 - items: MaybeDropdownItem[] 63 - label?: string 64 - menuWidth?: number 65 - children?: React.ReactNode 66 - openToRight?: boolean 67 - openUpwards?: boolean 68 - rightOffset?: number 69 - bottomOffset?: number 70 - hitSlop?: Insets 71 - accessibilityLabel?: string 72 - accessibilityHint?: string 73 - } 74 - 75 - /** 76 - * @deprecated use Menu from `#/components/Menu.tsx` instead 77 - */ 78 - export function DropdownButton({ 79 - testID, 80 - type = 'bare', 81 - style, 82 - items, 83 - label, 84 - menuWidth, 85 - children, 86 - openToRight = false, 87 - openUpwards = false, 88 - rightOffset = 0, 89 - bottomOffset = 0, 90 - hitSlop = HITSLOP_10, 91 - accessibilityLabel, 92 - }: PropsWithChildren<DropdownButtonProps>) { 93 - const {_} = useLingui() 94 - 95 - const ref1 = useRef<View>(null) 96 - const ref2 = useRef<View>(null) 97 - 98 - const onPress = (e: GestureResponderEvent) => { 99 - const ref = ref1.current || ref2.current 100 - const {height: winHeight} = Dimensions.get('window') 101 - const pressY = e.nativeEvent.pageY 102 - ref?.measure( 103 - ( 104 - _x: number, 105 - _y: number, 106 - width: number, 107 - _height: number, 108 - pageX: number, 109 - pageY: number, 110 - ) => { 111 - if (!menuWidth) { 112 - menuWidth = 200 113 - } 114 - let estimatedMenuHeight = 0 115 - for (const item of items) { 116 - if (item && isSep(item)) { 117 - estimatedMenuHeight += ESTIMATED_SEP_HEIGHT 118 - } else if (item && isBtn(item)) { 119 - estimatedMenuHeight += ESTIMATED_BTN_HEIGHT 120 - } else if (item && isHeading(item)) { 121 - estimatedMenuHeight += ESTIMATED_HEADING_HEIGHT 122 - } 123 - } 124 - const newX = openToRight 125 - ? pageX + width + rightOffset 126 - : pageX + width - menuWidth 127 - 128 - // Add a bit of additional room 129 - let newY = pressY + bottomOffset + 20 130 - if (openUpwards || newY + estimatedMenuHeight > winHeight) { 131 - newY -= estimatedMenuHeight 132 - } 133 - createDropdownMenu( 134 - newX, 135 - newY, 136 - pageY, 137 - menuWidth, 138 - items.filter(v => !!v) as DropdownItem[], 139 - openUpwards, 140 - ) 141 - }, 142 - ) 143 - } 144 - 145 - const numItems = useMemo( 146 - () => 147 - items.filter(item => { 148 - if (item === undefined || item === false) { 149 - return false 150 - } 151 - 152 - return isBtn(item) 153 - }).length, 154 - [items], 155 - ) 156 - 157 - if (type === 'bare') { 158 - return ( 159 - <TouchableOpacity 160 - testID={testID} 161 - style={style} 162 - onPress={onPress} 163 - hitSlop={hitSlop} 164 - ref={ref1} 165 - accessibilityRole="button" 166 - accessibilityLabel={ 167 - accessibilityLabel || _(msg`Opens ${numItems} options`) 168 - } 169 - accessibilityHint=""> 170 - {children} 171 - </TouchableOpacity> 172 - ) 173 - } 174 - return ( 175 - <View ref={ref2}> 176 - <Button 177 - type={type} 178 - testID={testID} 179 - onPress={onPress} 180 - style={style} 181 - label={label}> 182 - {children} 183 - </Button> 184 - </View> 185 - ) 186 - } 187 - 188 - function createDropdownMenu( 189 - x: number, 190 - y: number, 191 - pageY: number, 192 - width: number, 193 - items: DropdownItem[], 194 - opensUpwards = false, 195 - ): RootSiblings { 196 - const onPressItem = (index: number) => { 197 - sibling.destroy() 198 - const item = items[index] 199 - if (isBtn(item)) { 200 - item.onPress() 201 - } 202 - } 203 - const onOuterPress = () => sibling.destroy() 204 - const sibling = new RootSiblings( 205 - ( 206 - <DropdownItems 207 - onOuterPress={onOuterPress} 208 - x={x} 209 - y={y} 210 - pageY={pageY} 211 - width={width} 212 - items={items} 213 - onPressItem={onPressItem} 214 - openUpwards={opensUpwards} 215 - /> 216 - ), 217 - ) 218 - return sibling 219 - } 220 - 221 - type DropDownItemProps = { 222 - onOuterPress: () => void 223 - x: number 224 - y: number 225 - pageY: number 226 - width: number 227 - items: DropdownItem[] 228 - onPressItem: (index: number) => void 229 - openUpwards: boolean 230 - } 231 - 232 - const DropdownItems = ({ 233 - onOuterPress, 234 - x, 235 - y, 236 - pageY, 237 - width, 238 - items, 239 - onPressItem, 240 - openUpwards, 241 - }: DropDownItemProps) => { 242 - const pal = usePalette('default') 243 - const theme = useTheme() 244 - const {_} = useLingui() 245 - const {height: screenHeight} = useWindowDimensions() 246 - const dropDownBackgroundColor = 247 - theme.colorScheme === 'dark' ? pal.btn : pal.view 248 - const separatorColor = 249 - theme.colorScheme === 'dark' ? pal.borderDark : pal.border 250 - 251 - const numItems = items.filter(isBtn).length 252 - 253 - // TODO: Refactor dropdown components to: 254 - // - (On web, if not handled by React Native) use semantic <select /> 255 - // and <option /> elements for keyboard navigation out of the box 256 - // - (On mobile) be buttons by default, accept `label` and `nativeID` 257 - // props, and always have an explicit label 258 - return ( 259 - <FullWindowOverlay> 260 - {/* This TouchableWithoutFeedback renders the background so if the user clicks outside, the dropdown closes */} 261 - <TouchableWithoutFeedback 262 - onPress={onOuterPress} 263 - accessibilityLabel={_(msg`Toggle dropdown`)} 264 - accessibilityHint=""> 265 - <Animated.View 266 - entering={FadeIn} 267 - style={[ 268 - styles.bg, 269 - // On web we need to adjust the top and bottom relative to the scroll position 270 - isWeb 271 - ? { 272 - top: -pageY, 273 - bottom: pageY - screenHeight, 274 - } 275 - : { 276 - top: 0, 277 - bottom: 0, 278 - }, 279 - ]} 280 - /> 281 - </TouchableWithoutFeedback> 282 - <Animated.View 283 - entering={native( 284 - openUpwards ? FadeInDown.springify(1000) : FadeInUp.springify(1000), 285 - )} 286 - style={[ 287 - styles.menu, 288 - {left: x, top: y, width}, 289 - dropDownBackgroundColor, 290 - ]}> 291 - {items.map((item, index) => { 292 - if (isBtn(item)) { 293 - return ( 294 - <TouchableOpacity 295 - testID={item.testID} 296 - key={index} 297 - style={[styles.menuItem]} 298 - onPress={() => onPressItem(index)} 299 - accessibilityRole="button" 300 - accessibilityLabel={item.label} 301 - accessibilityHint={_( 302 - msg`Selects option ${index + 1} of ${numItems}`, 303 - )}> 304 - {item.icon && ( 305 - <FontAwesomeIcon 306 - style={styles.icon} 307 - icon={item.icon} 308 - color={pal.text.color as string} 309 - /> 310 - )} 311 - <Text style={[styles.label, pal.text]}>{item.label}</Text> 312 - </TouchableOpacity> 313 - ) 314 - } else if (isSep(item)) { 315 - return ( 316 - <View key={index} style={[styles.separator, separatorColor]} /> 317 - ) 318 - } else if (isHeading(item)) { 319 - return ( 320 - <View style={[styles.heading, pal.border]} key={index}> 321 - <Text style={[pal.text, styles.headingLabel]}> 322 - {item.label} 323 - </Text> 324 - </View> 325 - ) 326 - } 327 - return null 328 - })} 329 - </Animated.View> 330 - </FullWindowOverlay> 331 - ) 332 - } 333 - 334 - function isSep(item: DropdownItem): item is DropdownItemSeparator { 335 - return 'sep' in item && item.sep 336 - } 337 - function isHeading(item: DropdownItem): item is DropdownItemHeading { 338 - return 'heading' in item && item.heading 339 - } 340 - function isBtn(item: DropdownItem): item is DropdownItemButton { 341 - return !isSep(item) && !isHeading(item) 342 - } 343 - 344 - const styles = StyleSheet.create({ 345 - bg: { 346 - position: 'absolute', 347 - left: 0, 348 - width: '100%', 349 - backgroundColor: 'rgba(0, 0, 0, 0.1)', 350 - }, 351 - menu: { 352 - position: 'absolute', 353 - backgroundColor: '#fff', 354 - borderRadius: 14, 355 - paddingVertical: 6, 356 - }, 357 - menuItem: { 358 - flexDirection: 'row', 359 - alignItems: 'center', 360 - paddingVertical: 10, 361 - paddingLeft: 15, 362 - paddingRight: 40, 363 - }, 364 - menuItemBorder: { 365 - borderTopWidth: 1, 366 - borderTopColor: colors.gray1, 367 - marginTop: 4, 368 - paddingTop: 12, 369 - }, 370 - icon: { 371 - marginLeft: 2, 372 - marginRight: 8, 373 - flexShrink: 0, 374 - }, 375 - label: { 376 - fontSize: 18, 377 - flexShrink: 1, 378 - flexGrow: 1, 379 - }, 380 - separator: { 381 - borderTopWidth: 1, 382 - marginVertical: 8, 383 - }, 384 - heading: { 385 - flexDirection: 'row', 386 - justifyContent: 'center', 387 - paddingVertical: 10, 388 - paddingLeft: 15, 389 - paddingRight: 20, 390 - borderBottomWidth: 1, 391 - marginBottom: 6, 392 - }, 393 - headingLabel: { 394 - fontSize: 18, 395 - fontWeight: '600', 396 - }, 397 - })
+5 -46
src/view/screens/Debug.tsx
··· 4 4 import {useLingui} from '@lingui/react' 5 5 6 6 import {usePalette} from '#/lib/hooks/usePalette' 7 - import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' 7 + import { 8 + type CommonNavigatorParams, 9 + type NativeStackScreenProps, 10 + } from '#/lib/routes/types' 8 11 import {s} from '#/lib/styles' 9 - import {PaletteColorName, ThemeProvider} from '#/lib/ThemeContext' 12 + import {type PaletteColorName, ThemeProvider} from '#/lib/ThemeContext' 10 13 import {EmptyState} from '#/view/com/util/EmptyState' 11 14 import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' 12 15 import {ErrorScreen} from '#/view/com/util/error/ErrorScreen' 13 16 import {Button} from '#/view/com/util/forms/Button' 14 - import { 15 - DropdownButton, 16 - DropdownItem, 17 - } from '#/view/com/util/forms/DropdownButton' 18 17 import {ToggleButton} from '#/view/com/util/forms/ToggleButton' 19 18 import * as LoadingPlaceholder from '#/view/com/util/LoadingPlaceholder' 20 19 import {Text} from '#/view/com/util/text/Text' ··· 134 133 <ScrollView style={[s.pl10, s.pr10]}> 135 134 <Heading label="Buttons" /> 136 135 <ButtonsView /> 137 - <Heading label="Dropdown Buttons" /> 138 - <DropdownButtonsView /> 139 136 <Heading label="Toggle Buttons" /> 140 137 <ToggleButtonsView /> 141 138 <View style={s.footerSpacer} /> ··· 391 388 label="Default light" 392 389 style={buttonStyles} 393 390 /> 394 - </View> 395 - </View> 396 - ) 397 - } 398 - 399 - const DROPDOWN_ITEMS: DropdownItem[] = [ 400 - { 401 - icon: ['far', 'paste'], 402 - label: 'Copy post text', 403 - onPress() {}, 404 - }, 405 - { 406 - icon: 'share', 407 - label: 'Share...', 408 - onPress() {}, 409 - }, 410 - { 411 - icon: 'circle-exclamation', 412 - label: 'Report post', 413 - onPress() {}, 414 - }, 415 - ] 416 - function DropdownButtonsView() { 417 - const defaultPal = usePalette('default') 418 - return ( 419 - <View style={[defaultPal.view]}> 420 - <View style={s.mb5}> 421 - <DropdownButton 422 - type="primary" 423 - items={DROPDOWN_ITEMS} 424 - menuWidth={200} 425 - label="Primary button" 426 - /> 427 - </View> 428 - <View style={s.mb5}> 429 - <DropdownButton type="bare" items={DROPDOWN_ITEMS} menuWidth={200}> 430 - <Text>Bare</Text> 431 - </DropdownButton> 432 391 </View> 433 392 </View> 434 393 )