Bluesky app fork with some witchin' additions 💫

ALF content language dialog (#9471)

Co-authored-by: Eric Bailey <git@esb.lol>

authored by samuel.fm

Eric Bailey and committed by
GitHub
512ce3d5 bdf143ce

+170 -659
+4 -2
modules/bottom-sheet/src/BottomSheetNativeComponent.tsx
··· 16 16 type BottomSheetState, 17 17 type BottomSheetViewProps, 18 18 } from './BottomSheet.types' 19 - import {BottomSheetPortalProvider} from './BottomSheetPortal' 20 - import {Context as PortalContext} from './BottomSheetPortal' 19 + import { 20 + BottomSheetPortalProvider, 21 + Context as PortalContext, 22 + } from './BottomSheetPortal' 21 23 22 24 const screenHeight = Dimensions.get('screen').height 23 25
+2 -2
src/components/Select/index.tsx
··· 103 103 <Button 104 104 label={label} 105 105 onPress={control.open} 106 - style={[a.flex_1, a.justify_between]} 106 + style={[a.flex_1, a.justify_between, a.pl_lg, a.pr_md]} 107 107 color="secondary" 108 - size="small" 108 + size="large" 109 109 shape="rectangular"> 110 110 <>{children}</> 111 111 </Button>
+88 -43
src/screens/Settings/LanguageSettings.tsx
··· 1 - import {useCallback, useMemo} from 'react' 1 + import {useCallback, useMemo, useState} from 'react' 2 2 import {View} from 'react-native' 3 3 import {msg, Trans} from '@lingui/macro' 4 4 import {useLingui} from '@lingui/react' 5 5 6 - import {APP_LANGUAGES, LANGUAGES} from '#/lib/../locale/languages' 7 6 import { 8 7 type CommonNavigatorParams, 9 8 type NativeStackScreenProps, 10 9 } from '#/lib/routes/types' 11 10 import {languageName, sanitizeAppLanguageSetting} from '#/locale/helpers' 12 - import {useModalControls} from '#/state/modals' 11 + import {APP_LANGUAGES, LANGUAGES} from '#/locale/languages' 13 12 import {useLanguagePrefs, useLanguagePrefsApi} from '#/state/preferences' 14 - import {atoms as a, useTheme, web} from '#/alf' 15 - import {Button, ButtonIcon, ButtonText} from '#/components/Button' 16 - import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check' 13 + import {atoms as a, web} from '#/alf' 14 + import {Admonition} from '#/components/Admonition' 15 + import {Button} from '#/components/Button' 16 + import {useDialogControl} from '#/components/Dialog' 17 + import {LanguageSelectDialog} from '#/components/dialogs/LanguageSelectDialog' 18 + import * as Toggle from '#/components/forms/Toggle' 17 19 import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus' 18 20 import * as Layout from '#/components/Layout' 19 21 import * as Select from '#/components/Select' ··· 30 32 const {_} = useLingui() 31 33 const langPrefs = useLanguagePrefs() 32 34 const setLangPrefs = useLanguagePrefsApi() 33 - const t = useTheme() 34 35 35 - const {openModal} = useModalControls() 36 - 37 - const onPressContentLanguages = useCallback(() => { 38 - openModal({name: 'content-languages-settings'}) 39 - }, [openModal]) 36 + const contentLanguagePrefsControl = useDialogControl() 40 37 41 38 const onChangePrimaryLanguage = useCallback( 42 39 (value: string) => { ··· 58 55 [langPrefs, setLangPrefs], 59 56 ) 60 57 61 - const myLanguages = useMemo(() => { 62 - return ( 63 - langPrefs.contentLanguages 64 - .map(lang => LANGUAGES.find(l => l.code2 === lang)) 65 - .filter(Boolean) 66 - // @ts-ignore 67 - .map(l => languageName(l, langPrefs.appLanguage)) 68 - .join(', ') 69 - ) 70 - }, [langPrefs.appLanguage, langPrefs.contentLanguages]) 58 + const [recentLanguages, setRecentLanguages] = useState<string[]>( 59 + langPrefs.contentLanguages, 60 + ) 61 + 62 + const possibleLanguages = useMemo(() => { 63 + return [ 64 + ...new Set([ 65 + ...recentLanguages, 66 + ...langPrefs.contentLanguages, 67 + ...langPrefs.primaryLanguage, 68 + ]), 69 + ] 70 + .map(lang => LANGUAGES.find(l => l.code2 === lang)) 71 + .filter(x => !!x) 72 + }, [recentLanguages, langPrefs.contentLanguages, langPrefs.primaryLanguage]) 71 73 72 74 return ( 73 75 <Layout.Screen testID="PreferencesLanguagesScreen"> ··· 84 86 <SettingsList.Container> 85 87 <SettingsList.Group iconInset={false}> 86 88 <SettingsList.ItemText> 87 - <Trans>App Language</Trans> 89 + <Trans>App language</Trans> 88 90 </SettingsList.ItemText> 89 91 <View style={[a.gap_md, a.w_full]}> 90 92 <Text style={[a.leading_snug]}> ··· 118 120 <SettingsList.Divider /> 119 121 <SettingsList.Group iconInset={false}> 120 122 <SettingsList.ItemText> 121 - <Trans>Primary Language</Trans> 123 + <Trans>Primary language</Trans> 122 124 </SettingsList.ItemText> 123 125 <View style={[a.gap_md, a.w_full]}> 124 126 <Text style={[a.leading_snug]}> ··· 152 154 <SettingsList.Divider /> 153 155 <SettingsList.Group iconInset={false}> 154 156 <SettingsList.ItemText> 155 - <Trans>Content Languages</Trans> 157 + <Trans>Content languages</Trans> 156 158 </SettingsList.ItemText> 157 159 <View style={[a.gap_md]}> 158 160 <Text style={[a.leading_snug]}> ··· 162 164 </Trans> 163 165 </Text> 164 166 165 - <Button 166 - label={_(msg`Select content languages`)} 167 - size="small" 168 - color="secondary" 169 - shape="rectangular" 170 - onPress={onPressContentLanguages} 171 - style={[a.justify_start, web({maxWidth: 400})]}> 172 - <ButtonIcon 173 - icon={myLanguages.length > 0 ? CheckIcon : PlusIcon} 174 - /> 175 - <ButtonText 176 - style={[t.atoms.text, a.text_md, a.flex_1, a.text_left]} 177 - numberOfLines={1}> 178 - {myLanguages.length > 0 179 - ? myLanguages 180 - : _(msg`Select languages`)} 181 - </ButtonText> 182 - </Button> 167 + {langPrefs.contentLanguages.length === 0 && ( 168 + <Admonition type="info"> 169 + <Trans>All languages will be shown in your feeds.</Trans> 170 + </Admonition> 171 + )} 172 + 173 + <View style={[a.w_full, web({maxWidth: 400})]}> 174 + <Toggle.Group 175 + label={_(msg`Select content languages`)} 176 + values={langPrefs.contentLanguages} 177 + onChange={setLangPrefs.setContentLanguages}> 178 + <Toggle.PanelGroup> 179 + {possibleLanguages.map((language, index) => { 180 + const name = languageName(language, langPrefs.appLanguage) 181 + return ( 182 + <Toggle.Item 183 + key={language.code2} 184 + name={language.code2} 185 + label={name}> 186 + {({selected}) => ( 187 + <Toggle.Panel 188 + active={selected} 189 + adjacent={index === 0 ? 'trailing' : 'both'}> 190 + <Toggle.Checkbox /> 191 + <Toggle.PanelText>{name}</Toggle.PanelText> 192 + </Toggle.Panel> 193 + )} 194 + </Toggle.Item> 195 + ) 196 + })} 197 + <Button 198 + label={_(msg`Add more languages...`)} 199 + onPress={contentLanguagePrefsControl.open}> 200 + <Toggle.Panel adjacent="leading"> 201 + <Toggle.PanelIcon icon={PlusIcon} /> 202 + <Toggle.PanelText> 203 + Add more languages... 204 + </Toggle.PanelText> 205 + </Toggle.Panel> 206 + </Button> 207 + </Toggle.PanelGroup> 208 + </Toggle.Group> 209 + </View> 210 + 211 + <LanguageSelectDialog 212 + control={contentLanguagePrefsControl} 213 + titleText={<Trans>Select content languages</Trans>} 214 + subtitleText={ 215 + <Trans> 216 + If none are selected, all languages will be shown in your 217 + feeds. 218 + </Trans> 219 + } 220 + currentLanguages={langPrefs.contentLanguages} 221 + onSelectLanguages={languages => { 222 + setLangPrefs.setContentLanguages(languages) 223 + setRecentLanguages(recent => [ 224 + ...new Set([...recent, ...languages]), 225 + ]) 226 + }} 227 + /> 183 228 </View> 184 229 </SettingsList.Group> 185 230 </SettingsList.Container>
+6 -45
src/state/preferences/languages.tsx
··· 11 11 type ApiContext = { 12 12 setPrimaryLanguage: (code2: string) => void 13 13 setPostLanguage: (commaSeparatedLangCodes: string) => void 14 - setContentLanguage: (code2: string) => void 15 - toggleContentLanguage: (code2: string) => void 16 - togglePostLanguage: (code2: string) => void 14 + setContentLanguages: (code2s: string[]) => void 17 15 savePostLanguageToHistory: () => void 18 16 setAppLanguage: (code2: AppLanguage) => void 19 17 } ··· 25 23 const apiContext = React.createContext<ApiContext>({ 26 24 setPrimaryLanguage: (_: string) => {}, 27 25 setPostLanguage: (_: string) => {}, 28 - setContentLanguage: (_: string) => {}, 29 - toggleContentLanguage: (_: string) => {}, 30 - togglePostLanguage: (_: string) => {}, 26 + setContentLanguages: (_: string[]) => {}, 31 27 savePostLanguageToHistory: () => {}, 32 28 setAppLanguage: (_: AppLanguage) => {}, 33 29 }) 34 30 apiContext.displayName = 'LanguagePrefsApiContext' 35 31 36 32 export function Provider({children}: React.PropsWithChildren<{}>) { 37 - const [state, setState] = React.useState(persisted.get('languagePrefs')) 33 + const [state, setState] = React.useState(() => persisted.get('languagePrefs')) 38 34 39 35 const setStateWrapped = React.useCallback( 40 36 (fn: SetStateCb) => { ··· 59 55 setPostLanguage(commaSeparatedLangCodes: string) { 60 56 setStateWrapped(s => ({...s, postLanguage: commaSeparatedLangCodes})) 61 57 }, 62 - setContentLanguage(code2: string) { 63 - setStateWrapped(s => ({...s, contentLanguages: [code2]})) 64 - }, 65 - toggleContentLanguage(code2: string) { 66 - setStateWrapped(s => { 67 - const exists = s.contentLanguages.includes(code2) 68 - const next = exists 69 - ? s.contentLanguages.filter(lang => lang !== code2) 70 - : s.contentLanguages.concat(code2) 71 - return { 72 - ...s, 73 - contentLanguages: next, 74 - } 75 - }) 76 - }, 77 - togglePostLanguage(code2: string) { 78 - setStateWrapped(s => { 79 - const exists = hasPostLanguage(state.postLanguage, code2) 80 - let next = s.postLanguage 81 - 82 - if (exists) { 83 - next = toPostLanguages(s.postLanguage) 84 - .filter(lang => lang !== code2) 85 - .join(',') 86 - } else { 87 - // sort alphabetically for deterministic comparison in context menu 88 - next = toPostLanguages(s.postLanguage) 89 - .concat([code2]) 90 - .sort((a, b) => a.localeCompare(b)) 91 - .join(',') 92 - } 93 - 94 - return { 95 - ...s, 96 - postLanguage: next, 97 - } 98 - }) 58 + setContentLanguages(code2s: string[]) { 59 + setStateWrapped(s => ({...s, contentLanguages: code2s})) 99 60 }, 100 61 /** 101 62 * Saves whatever language codes are currently selected into a history array, ··· 120 81 setStateWrapped(s => ({...s, appLanguage: code2})) 121 82 }, 122 83 }), 123 - [state, setStateWrapped], 84 + [setStateWrapped], 124 85 ) 125 86 126 87 return (
+6 -2
src/state/shell/color-mode.tsx
··· 20 20 setContext.displayName = 'ColorModeSetContext' 21 21 22 22 export function Provider({children}: React.PropsWithChildren<{}>) { 23 - const [colorMode, setColorMode] = React.useState(persisted.get('colorMode')) 24 - const [darkTheme, setDarkTheme] = React.useState(persisted.get('darkTheme')) 23 + const [colorMode, setColorMode] = React.useState(() => 24 + persisted.get('colorMode'), 25 + ) 26 + const [darkTheme, setDarkTheme] = React.useState(() => 27 + persisted.get('darkTheme'), 28 + ) 25 29 26 30 const stateContextValue = React.useMemo( 27 31 () => ({
+22 -4
src/view/com/composer/select-language/PostLanguageSelect.tsx
··· 11 11 import {atoms as a, useTheme} from '#/alf' 12 12 import {Button, type ButtonProps} from '#/components/Button' 13 13 import * as Dialog from '#/components/Dialog' 14 + import {LanguageSelectDialog} from '#/components/dialogs/LanguageSelectDialog' 14 15 import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRightIcon} from '#/components/icons/Chevron' 15 16 import {Globe_Stroke2_Corner0_Rounded as GlobeIcon} from '#/components/icons/Globe' 16 17 import * as Menu from '#/components/Menu' 17 18 import {Text} from '#/components/Typography' 18 - import {PostLanguageSelectDialog} from './PostLanguageSelectDialog' 19 19 20 20 export function PostLanguageSelect({ 21 21 currentLanguages: currentLanguagesProp, ··· 36 36 const currentLanguages = 37 37 currentLanguagesProp ?? toPostLanguages(langPrefs.postLanguage) 38 38 39 + const onSelectLanguages = (languages: string[]) => { 40 + let langsString = languages.join(',') 41 + if (!langsString) { 42 + langsString = langPrefs.primaryLanguage 43 + } 44 + setLangPrefs.setPostLanguage(langsString) 45 + onSelectLanguage?.(langsString) 46 + } 47 + 39 48 if ( 40 49 dedupedHistory.length === 1 && 41 50 dedupedHistory[0] === langPrefs.postLanguage ··· 43 52 return ( 44 53 <> 45 54 <LanguageBtn onPress={languageDialogControl.open} /> 46 - <PostLanguageSelectDialog 55 + <LanguageSelectDialog 56 + titleText={<Trans>Choose post languages</Trans>} 57 + subtitleText={ 58 + <Trans>Select up to 3 languages used in this post</Trans> 59 + } 47 60 control={languageDialogControl} 48 61 currentLanguages={currentLanguages} 62 + onSelectLanguages={onSelectLanguages} 63 + maxLanguages={3} 49 64 /> 50 65 </> 51 66 ) ··· 94 109 </Menu.Outer> 95 110 </Menu.Root> 96 111 97 - <PostLanguageSelectDialog 112 + <LanguageSelectDialog 113 + titleText={<Trans>Choose post languages</Trans>} 114 + subtitleText={<Trans>Select up to 3 languages used in this post</Trans>} 98 115 control={languageDialogControl} 99 116 currentLanguages={currentLanguages} 100 - onSelectLanguage={onSelectLanguage} 117 + onSelectLanguages={onSelectLanguages} 118 + maxLanguages={3} 101 119 /> 102 120 </> 103 121 )
+41 -34
src/view/com/composer/select-language/PostLanguageSelectDialog.tsx src/components/dialogs/LanguageSelectDialog.tsx
··· 6 6 7 7 import {languageName} from '#/locale/helpers' 8 8 import {type Language, LANGUAGES, LANGUAGES_MAP_CODE2} from '#/locale/languages' 9 - import { 10 - toPostLanguages, 11 - useLanguagePrefs, 12 - useLanguagePrefsApi, 13 - } from '#/state/preferences/languages' 9 + import {useLanguagePrefs} from '#/state/preferences/languages' 14 10 import {ErrorScreen} from '#/view/com/util/error/ErrorScreen' 15 11 import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' 16 12 import {atoms as a, useTheme, web} from '#/alf' ··· 22 18 import {Text} from '#/components/Typography' 23 19 import {IS_NATIVE, IS_WEB} from '#/env' 24 20 25 - export function PostLanguageSelectDialog({ 21 + export function LanguageSelectDialog({ 22 + titleText, 23 + subtitleText, 26 24 control, 27 25 /** 28 26 * Optionally can be passed to show different values than what is saved in 29 27 * langPrefs. 30 28 */ 31 29 currentLanguages, 32 - onSelectLanguage, 30 + onSelectLanguages, 31 + maxLanguages, 33 32 }: { 34 33 control: Dialog.DialogControlProps 34 + titleText?: React.ReactNode 35 + subtitleText?: React.ReactNode 36 + /** 37 + * Defaults to the primary language 38 + */ 35 39 currentLanguages?: string[] 36 - onSelectLanguage?: (language: string) => void 40 + onSelectLanguages: (languages: string[]) => void 41 + maxLanguages?: number 37 42 }) { 38 43 const {height} = useWindowDimensions() 39 44 const insets = useSafeAreaInsets() ··· 50 55 <Dialog.Handle /> 51 56 <ErrorBoundary renderError={renderErrorBoundary}> 52 57 <DialogInner 58 + titleText={titleText} 59 + subtitleText={subtitleText} 53 60 currentLanguages={currentLanguages} 54 - onSelectLanguage={onSelectLanguage} 61 + onSelectLanguages={onSelectLanguages} 62 + maxLanguages={maxLanguages} 55 63 /> 56 64 </ErrorBoundary> 57 65 </Dialog.Outer> ··· 59 67 } 60 68 61 69 export function DialogInner({ 70 + titleText, 71 + subtitleText, 62 72 currentLanguages, 63 - onSelectLanguage, 73 + onSelectLanguages, 74 + maxLanguages, 64 75 }: { 76 + titleText?: React.ReactNode 77 + subtitleText?: React.ReactNode 65 78 currentLanguages?: string[] 66 - onSelectLanguage?: (language: string) => void 79 + onSelectLanguages?: (languages: string[]) => void 80 + maxLanguages?: number 67 81 }) { 68 82 const control = Dialog.useDialogContext() 69 83 const [headerHeight, setHeaderHeight] = useState(0) ··· 81 95 }, []) 82 96 83 97 const langPrefs = useLanguagePrefs() 84 - const postLanguagesPref = 85 - currentLanguages ?? toPostLanguages(langPrefs.postLanguage) 86 - 87 98 const [checkedLanguagesCode2, setCheckedLanguagesCode2] = useState<string[]>( 88 - postLanguagesPref || [langPrefs.primaryLanguage], 99 + currentLanguages || [langPrefs.primaryLanguage], 89 100 ) 90 101 const [search, setSearch] = useState('') 91 102 92 - const setLangPrefs = useLanguagePrefsApi() 93 103 const t = useTheme() 94 104 const {_} = useLingui() 95 105 96 106 const handleClose = () => { 97 107 control.close(() => { 98 - let langsString = checkedLanguagesCode2.join(',') 99 - if (!langsString) { 100 - langsString = langPrefs.primaryLanguage 101 - } 102 - setLangPrefs.setPostLanguage(langsString) 103 - onSelectLanguage?.(langsString) 108 + onSelectLanguages?.(checkedLanguagesCode2) 104 109 }) 105 110 } 106 111 ··· 181 186 a.text_xl, 182 187 a.mb_sm, 183 188 ]}> 184 - <Trans>Choose Post Languages</Trans> 189 + {titleText ?? <Trans>Choose languages</Trans>} 185 190 </Text> 186 - <Text 187 - nativeID="dialog-description" 188 - style={[ 189 - t.atoms.text_contrast_medium, 190 - a.text_left, 191 - a.text_md, 192 - a.mb_lg, 193 - ]}> 194 - <Trans>Select up to 3 languages used in this post</Trans> 195 - </Text> 191 + {subtitleText && ( 192 + <Text 193 + nativeID="dialog-description" 194 + style={[ 195 + t.atoms.text_contrast_medium, 196 + a.text_left, 197 + a.text_md, 198 + a.mb_lg, 199 + ]}> 200 + {subtitleText} 201 + </Text> 202 + )} 196 203 </View> 197 204 198 205 {IS_WEB && ( ··· 244 251 values={checkedLanguagesCode2} 245 252 onChange={setCheckedLanguagesCode2} 246 253 type="checkbox" 247 - maxSelections={3} 254 + maxSelections={maxLanguages} 248 255 label={_(msg`Select languages`)} 249 256 style={web([a.contents])}> 250 257 <Dialog.InnerFlatList
-4
src/view/com/modals/Modal.tsx
··· 7 7 import {useModalControls, useModals} from '#/state/modals' 8 8 import {FullWindowOverlay} from '#/components/FullWindowOverlay' 9 9 import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop' 10 - import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings' 11 10 import * as UserAddRemoveListsModal from './UserAddRemoveLists' 12 11 13 12 const DEFAULT_SNAPPOINTS = ['90%'] ··· 44 43 if (activeModal?.name === 'user-add-remove-lists') { 45 44 snapPoints = UserAddRemoveListsModal.snapPoints 46 45 element = <UserAddRemoveListsModal.Component {...activeModal} /> 47 - } else if (activeModal?.name === 'content-languages-settings') { 48 - snapPoints = ContentLanguagesSettingsModal.snapPoints 49 - element = <ContentLanguagesSettingsModal.Component /> 50 46 } else { 51 47 return null 52 48 }
-3
src/view/com/modals/Modal.web.tsx
··· 6 6 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 7 7 import {type Modal as ModalIface} from '#/state/modals' 8 8 import {useModalControls, useModals} from '#/state/modals' 9 - import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings' 10 9 import * as UserAddRemoveLists from './UserAddRemoveLists' 11 10 12 11 export function ModalsContainer() { ··· 47 46 let element 48 47 if (modal.name === 'user-add-remove-lists') { 49 48 element = <UserAddRemoveLists.Component {...modal} /> 50 - } else if (modal.name === 'content-languages-settings') { 51 - element = <ContentLanguagesSettingsModal.Component /> 52 49 } else { 53 50 return null 54 51 }
-64
src/view/com/modals/lang-settings/ConfirmLanguagesButton.tsx
··· 1 - import {Pressable, StyleSheet, Text, View} from 'react-native' 2 - import {LinearGradient} from 'expo-linear-gradient' 3 - import {msg, Trans} from '@lingui/macro' 4 - import {useLingui} from '@lingui/react' 5 - 6 - import {usePalette} from '#/lib/hooks/usePalette' 7 - import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 8 - import {colors, gradients, s} from '#/lib/styles' 9 - 10 - export const ConfirmLanguagesButton = ({ 11 - onPress, 12 - extraText, 13 - }: { 14 - onPress: () => void 15 - extraText?: string 16 - }) => { 17 - const pal = usePalette('default') 18 - const {_} = useLingui() 19 - const {isMobile} = useWebMediaQueries() 20 - return ( 21 - <View 22 - style={[ 23 - styles.btnContainer, 24 - pal.borderDark, 25 - isMobile && { 26 - paddingBottom: 40, 27 - borderTopWidth: 1, 28 - }, 29 - ]}> 30 - <Pressable 31 - testID="confirmContentLanguagesBtn" 32 - onPress={onPress} 33 - accessibilityRole="button" 34 - accessibilityLabel={_(msg`Confirm content language settings`)} 35 - accessibilityHint=""> 36 - <LinearGradient 37 - colors={[gradients.blueLight.start, gradients.blueLight.end]} 38 - start={{x: 0, y: 0}} 39 - end={{x: 1, y: 1}} 40 - style={[styles.btn]}> 41 - <Text style={[s.white, s.bold, s.f18]}> 42 - <Trans>Done{extraText}</Trans> 43 - </Text> 44 - </LinearGradient> 45 - </Pressable> 46 - </View> 47 - ) 48 - } 49 - 50 - const styles = StyleSheet.create({ 51 - btnContainer: { 52 - paddingTop: 10, 53 - paddingHorizontal: 10, 54 - }, 55 - btn: { 56 - flexDirection: 'row', 57 - alignItems: 'center', 58 - justifyContent: 'center', 59 - width: '100%', 60 - borderRadius: 32, 61 - padding: 14, 62 - backgroundColor: colors.gray1, 63 - }, 64 - })
-128
src/view/com/modals/lang-settings/ContentLanguagesSettings.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 - useLanguagePrefs, 12 - useLanguagePrefsApi, 13 - } from '#/state/preferences/languages' 14 - import {LANGUAGES, LANGUAGES_MAP_CODE2} from '../../../../locale/languages' 15 - import {Text} from '../../util/text/Text' 16 - import {ScrollView} from '../util' 17 - import {ConfirmLanguagesButton} from './ConfirmLanguagesButton' 18 - import {LanguageToggle} from './LanguageToggle' 19 - 20 - export const snapPoints = ['100%'] 21 - 22 - export function Component({}: {}) { 23 - const {closeModal} = useModalControls() 24 - const langPrefs = useLanguagePrefs() 25 - const setLangPrefs = useLanguagePrefsApi() 26 - const pal = usePalette('default') 27 - const {isMobile} = useWebMediaQueries() 28 - const onPressDone = React.useCallback(() => { 29 - closeModal() 30 - }, [closeModal]) 31 - 32 - const languages = React.useMemo(() => { 33 - const langs = LANGUAGES.filter( 34 - lang => 35 - !!lang.code2.trim() && 36 - LANGUAGES_MAP_CODE2[lang.code2].code3 === lang.code3, 37 - ) 38 - // sort so that device & selected languages are on top, then alphabetically 39 - langs.sort((a, b) => { 40 - const hasA = 41 - langPrefs.contentLanguages.includes(a.code2) || 42 - deviceLanguageCodes.includes(a.code2) 43 - const hasB = 44 - langPrefs.contentLanguages.includes(b.code2) || 45 - deviceLanguageCodes.includes(b.code2) 46 - if (hasA === hasB) return a.name.localeCompare(b.name) 47 - if (hasA) return -1 48 - return 1 49 - }) 50 - return langs 51 - }, [langPrefs]) 52 - 53 - const onPress = React.useCallback( 54 - (code2: string) => { 55 - setLangPrefs.toggleContentLanguage(code2) 56 - }, 57 - [setLangPrefs], 58 - ) 59 - 60 - return ( 61 - <View 62 - testID="contentLanguagesModal" 63 - style={[ 64 - pal.view, 65 - styles.container, 66 - // @ts-ignore vh is web only 67 - isMobile 68 - ? { 69 - paddingTop: 20, 70 - } 71 - : { 72 - maxHeight: '90vh', 73 - }, 74 - ]}> 75 - <Text style={[pal.text, styles.title]}> 76 - <Trans>Content Languages</Trans> 77 - </Text> 78 - <Text style={[pal.text, styles.description]}> 79 - <Trans> 80 - Which languages would you like to see in your algorithmic feeds? 81 - </Trans> 82 - </Text> 83 - <Text style={[pal.textLight, styles.description]}> 84 - <Trans>Leave them all unselected to see any language.</Trans> 85 - </Text> 86 - <ScrollView style={styles.scrollContainer}> 87 - {languages.map(lang => ( 88 - <LanguageToggle 89 - key={lang.code2} 90 - code2={lang.code2} 91 - langType="contentLanguages" 92 - name={languageName(lang, langPrefs.appLanguage)} 93 - onPress={() => { 94 - onPress(lang.code2) 95 - }} 96 - /> 97 - ))} 98 - <View 99 - style={{ 100 - height: isMobile ? 60 : 0, 101 - }} 102 - /> 103 - </ScrollView> 104 - <ConfirmLanguagesButton onPress={onPressDone} /> 105 - </View> 106 - ) 107 - } 108 - 109 - const styles = StyleSheet.create({ 110 - container: { 111 - flex: 1, 112 - }, 113 - title: { 114 - textAlign: 'center', 115 - fontWeight: '600', 116 - fontSize: 24, 117 - marginBottom: 12, 118 - }, 119 - description: { 120 - textAlign: 'center', 121 - paddingHorizontal: 16, 122 - marginBottom: 10, 123 - }, 124 - scrollContainer: { 125 - flex: 1, 126 - paddingHorizontal: 10, 127 - }, 128 - })
-53
src/view/com/modals/lang-settings/LanguageToggle.tsx
··· 1 - import {StyleSheet} from 'react-native' 2 - 3 - import {usePalette} from '#/lib/hooks/usePalette' 4 - import {toPostLanguages, useLanguagePrefs} from '#/state/preferences/languages' 5 - import {ToggleButton} from '#/view/com/util/forms/ToggleButton' 6 - 7 - export function LanguageToggle({ 8 - code2, 9 - name, 10 - onPress, 11 - langType, 12 - }: { 13 - code2: string 14 - name: string 15 - onPress: () => void 16 - langType: 'contentLanguages' | 'postLanguages' 17 - }) { 18 - const pal = usePalette('default') 19 - const langPrefs = useLanguagePrefs() 20 - 21 - const values = 22 - langType === 'contentLanguages' 23 - ? langPrefs.contentLanguages 24 - : toPostLanguages(langPrefs.postLanguage) 25 - const isSelected = values.includes(code2) 26 - 27 - // enforce a max of 3 selections for post languages 28 - let isDisabled = false 29 - if (langType === 'postLanguages' && values.length >= 3 && !isSelected) { 30 - isDisabled = true 31 - } 32 - 33 - return ( 34 - <ToggleButton 35 - label={name} 36 - isSelected={isSelected} 37 - onPress={isDisabled ? undefined : onPress} 38 - style={[pal.border, styles.languageToggle, isDisabled && styles.dimmed]} 39 - /> 40 - ) 41 - } 42 - 43 - const styles = StyleSheet.create({ 44 - languageToggle: { 45 - borderTopWidth: 1, 46 - borderRadius: 0, 47 - paddingHorizontal: 6, 48 - paddingVertical: 12, 49 - }, 50 - dimmed: { 51 - opacity: 0.5, 52 - }, 53 - })
-193
src/view/com/util/forms/ToggleButton.tsx
··· 1 - import { 2 - type StyleProp, 3 - StyleSheet, 4 - type TextStyle, 5 - View, 6 - type ViewStyle, 7 - } from 'react-native' 8 - 9 - import {choose} from '#/lib/functions' 10 - import {colors} from '#/lib/styles' 11 - import {useTheme} from '#/lib/ThemeContext' 12 - import {type TypographyVariant} from '#/lib/ThemeContext' 13 - import {Text} from '../text/Text' 14 - import {Button, type ButtonType} from './Button' 15 - 16 - /** 17 - * @deprecated use Toggle from `#/components/form/Toggle.tsx` instead 18 - */ 19 - export function ToggleButton({ 20 - testID, 21 - type = 'default-light', 22 - label, 23 - isSelected, 24 - style, 25 - labelType, 26 - onPress, 27 - }: { 28 - testID?: string 29 - type?: ButtonType 30 - label: string 31 - isSelected: boolean 32 - style?: StyleProp<ViewStyle> 33 - labelType?: TypographyVariant 34 - onPress?: () => void 35 - }) { 36 - const theme = useTheme() 37 - const circleStyle = choose<TextStyle, Record<ButtonType, TextStyle>>(type, { 38 - primary: { 39 - borderColor: theme.palette.primary.text, 40 - }, 41 - secondary: { 42 - borderColor: theme.palette.secondary.text, 43 - }, 44 - inverted: { 45 - borderColor: theme.palette.inverted.text, 46 - }, 47 - 'primary-outline': { 48 - borderColor: theme.palette.primary.border, 49 - }, 50 - 'secondary-outline': { 51 - borderColor: theme.palette.secondary.border, 52 - }, 53 - 'primary-light': { 54 - borderColor: theme.palette.primary.border, 55 - }, 56 - 'secondary-light': { 57 - borderColor: theme.palette.secondary.border, 58 - }, 59 - default: { 60 - borderColor: theme.palette.default.border, 61 - }, 62 - 'default-light': { 63 - borderColor: theme.palette.default.border, 64 - }, 65 - }) 66 - const circleFillStyle = choose<TextStyle, Record<ButtonType, TextStyle>>( 67 - type, 68 - { 69 - primary: { 70 - backgroundColor: theme.palette.primary.text, 71 - opacity: isSelected ? 1 : 0.33, 72 - }, 73 - secondary: { 74 - backgroundColor: theme.palette.secondary.text, 75 - opacity: isSelected ? 1 : 0.33, 76 - }, 77 - inverted: { 78 - backgroundColor: theme.palette.inverted.text, 79 - opacity: isSelected ? 1 : 0.33, 80 - }, 81 - 'primary-outline': { 82 - backgroundColor: theme.palette.primary.background, 83 - opacity: isSelected ? 1 : 0.5, 84 - }, 85 - 'secondary-outline': { 86 - backgroundColor: theme.palette.secondary.background, 87 - opacity: isSelected ? 1 : 0.5, 88 - }, 89 - 'primary-light': { 90 - backgroundColor: theme.palette.primary.background, 91 - opacity: isSelected ? 1 : 0.5, 92 - }, 93 - 'secondary-light': { 94 - backgroundColor: theme.palette.secondary.background, 95 - opacity: isSelected ? 1 : 0.5, 96 - }, 97 - default: { 98 - backgroundColor: isSelected 99 - ? theme.palette.primary.background 100 - : colors.gray3, 101 - }, 102 - 'default-light': { 103 - backgroundColor: isSelected 104 - ? theme.palette.primary.background 105 - : colors.gray3, 106 - }, 107 - }, 108 - ) 109 - const labelStyle = choose<TextStyle, Record<ButtonType, TextStyle>>(type, { 110 - primary: { 111 - color: theme.palette.primary.text, 112 - fontWeight: theme.palette.primary.isLowContrast ? '600' : undefined, 113 - }, 114 - secondary: { 115 - color: theme.palette.secondary.text, 116 - fontWeight: theme.palette.secondary.isLowContrast ? '600' : undefined, 117 - }, 118 - inverted: { 119 - color: theme.palette.inverted.text, 120 - fontWeight: theme.palette.inverted.isLowContrast ? '600' : undefined, 121 - }, 122 - 'primary-outline': { 123 - color: theme.palette.primary.textInverted, 124 - fontWeight: theme.palette.primary.isLowContrast ? '600' : undefined, 125 - }, 126 - 'secondary-outline': { 127 - color: theme.palette.secondary.textInverted, 128 - fontWeight: theme.palette.secondary.isLowContrast ? '600' : undefined, 129 - }, 130 - 'primary-light': { 131 - color: theme.palette.primary.textInverted, 132 - fontWeight: theme.palette.primary.isLowContrast ? '600' : undefined, 133 - }, 134 - 'secondary-light': { 135 - color: theme.palette.secondary.textInverted, 136 - fontWeight: theme.palette.secondary.isLowContrast ? '600' : undefined, 137 - }, 138 - default: { 139 - color: theme.palette.default.text, 140 - fontWeight: theme.palette.default.isLowContrast ? '600' : undefined, 141 - }, 142 - 'default-light': { 143 - color: theme.palette.default.text, 144 - fontWeight: theme.palette.default.isLowContrast ? '600' : undefined, 145 - }, 146 - }) 147 - return ( 148 - <Button testID={testID} type={type} onPress={onPress} style={style}> 149 - <View style={styles.outer}> 150 - <View style={[circleStyle, styles.circle]}> 151 - <View 152 - style={[ 153 - circleFillStyle, 154 - styles.circleFill, 155 - isSelected ? styles.circleFillSelected : undefined, 156 - ]} 157 - /> 158 - </View> 159 - {label === '' ? null : ( 160 - <Text type={labelType || 'button'} style={[labelStyle, styles.label]}> 161 - {label} 162 - </Text> 163 - )} 164 - </View> 165 - </Button> 166 - ) 167 - } 168 - 169 - const styles = StyleSheet.create({ 170 - outer: { 171 - flexDirection: 'row', 172 - alignItems: 'center', 173 - gap: 10, 174 - }, 175 - circle: { 176 - width: 42, 177 - height: 26, 178 - borderRadius: 15, 179 - padding: 3, 180 - borderWidth: 2, 181 - }, 182 - circleFill: { 183 - width: 16, 184 - height: 16, 185 - borderRadius: 10, 186 - }, 187 - circleFillSelected: { 188 - marginLeft: 16, 189 - }, 190 - label: { 191 - flex: 1, 192 - }, 193 - })
+1 -82
src/view/screens/Debug.tsx
··· 14 14 import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' 15 15 import {ErrorScreen} from '#/view/com/util/error/ErrorScreen' 16 16 import {Button} from '#/view/com/util/forms/Button' 17 - import {ToggleButton} from '#/view/com/util/forms/ToggleButton' 18 17 import * as LoadingPlaceholder from '#/view/com/util/LoadingPlaceholder' 19 18 import {Text} from '#/view/com/util/text/Text' 20 19 import * as Toast from '#/view/com/util/Toast' ··· 47 46 ) 48 47 } 49 48 50 - function DebugInner({ 51 - colorScheme, 52 - onToggleColorScheme, 53 - }: { 49 + function DebugInner({}: { 54 50 colorScheme: 'light' | 'dark' 55 51 onToggleColorScheme: () => void 56 52 }) { ··· 61 57 const renderItem = (item: any) => { 62 58 return ( 63 59 <View key={`view-${item.currentView}`}> 64 - <View style={[s.pt10, s.pl10, s.pr10]}> 65 - <ToggleButton 66 - type="default-light" 67 - onPress={onToggleColorScheme} 68 - isSelected={colorScheme === 'dark'} 69 - label={_(msg`Dark mode`)} 70 - /> 71 - </View> 72 60 {item.currentView === 3 ? ( 73 61 <NotifsView /> 74 62 ) : item.currentView === 2 ? ( ··· 134 122 <ScrollView style={[s.pl10, s.pr10]}> 135 123 <Heading label="Buttons" /> 136 124 <ButtonsView /> 137 - <Heading label="Toggle Buttons" /> 138 - <ToggleButtonsView /> 139 125 <View style={s.footerSpacer} /> 140 126 </ScrollView> 141 127 ) ··· 401 387 </View> 402 388 ) 403 389 } 404 - 405 - function ToggleButtonsView() { 406 - const defaultPal = usePalette('default') 407 - const buttonStyles = s.mb5 408 - const [isSelected, setIsSelected] = React.useState(false) 409 - const onToggle = () => setIsSelected(!isSelected) 410 - return ( 411 - <View style={[defaultPal.view]}> 412 - <ToggleButton 413 - type="primary" 414 - label="Primary solid" 415 - style={buttonStyles} 416 - isSelected={isSelected} 417 - onPress={onToggle} 418 - /> 419 - <ToggleButton 420 - type="secondary" 421 - label="Secondary solid" 422 - style={buttonStyles} 423 - isSelected={isSelected} 424 - onPress={onToggle} 425 - /> 426 - <ToggleButton 427 - type="inverted" 428 - label="Inverted solid" 429 - style={buttonStyles} 430 - isSelected={isSelected} 431 - onPress={onToggle} 432 - /> 433 - <ToggleButton 434 - type="primary-outline" 435 - label="Primary outline" 436 - style={buttonStyles} 437 - isSelected={isSelected} 438 - onPress={onToggle} 439 - /> 440 - <ToggleButton 441 - type="secondary-outline" 442 - label="Secondary outline" 443 - style={buttonStyles} 444 - isSelected={isSelected} 445 - onPress={onToggle} 446 - /> 447 - <ToggleButton 448 - type="primary-light" 449 - label="Primary light" 450 - style={buttonStyles} 451 - isSelected={isSelected} 452 - onPress={onToggle} 453 - /> 454 - <ToggleButton 455 - type="secondary-light" 456 - label="Secondary light" 457 - style={buttonStyles} 458 - isSelected={isSelected} 459 - onPress={onToggle} 460 - /> 461 - <ToggleButton 462 - type="default-light" 463 - label="Default light" 464 - style={buttonStyles} 465 - isSelected={isSelected} 466 - onPress={onToggle} 467 - /> 468 - </View> 469 - ) 470 - }