diff options
Diffstat (limited to 'src/view/screens')
-rw-r--r-- | src/view/screens/AccessibilitySettings.tsx | 157 | ||||
-rw-r--r-- | src/view/screens/AppPasswords.tsx | 375 | ||||
-rw-r--r-- | src/view/screens/LanguageSettings.tsx | 336 | ||||
-rw-r--r-- | src/view/screens/PreferencesExternalEmbeds.tsx | 147 | ||||
-rw-r--r-- | src/view/screens/PreferencesFollowingFeed.tsx | 249 | ||||
-rw-r--r-- | src/view/screens/PreferencesThreads.tsx | 198 | ||||
-rw-r--r-- | src/view/screens/Settings/DisableEmail2FADialog.tsx | 201 | ||||
-rw-r--r-- | src/view/screens/Settings/Email2FAToggle.tsx | 58 | ||||
-rw-r--r-- | src/view/screens/Settings/ExportCarDialog.tsx | 110 | ||||
-rw-r--r-- | src/view/screens/Settings/index.tsx | 1077 |
10 files changed, 0 insertions, 2908 deletions
diff --git a/src/view/screens/AccessibilitySettings.tsx b/src/view/screens/AccessibilitySettings.tsx deleted file mode 100644 index 4dd5aa97b..000000000 --- a/src/view/screens/AccessibilitySettings.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import React from 'react' -import {StyleSheet, View} from 'react-native' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {useFocusEffect} from '@react-navigation/native' - -import {IS_INTERNAL} from '#/lib/app-info' -import {usePalette} from '#/lib/hooks/usePalette' -import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' -import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' -import {s} from '#/lib/styles' -import {isNative} from '#/platform/detection' -import { - useAutoplayDisabled, - useHapticsDisabled, - useRequireAltTextEnabled, - useSetAutoplayDisabled, - useSetHapticsDisabled, - useSetRequireAltTextEnabled, -} from '#/state/preferences' -import { - useLargeAltBadgeEnabled, - useSetLargeAltBadgeEnabled, -} from '#/state/preferences/large-alt-badge' -import {useSetMinimalShellMode} from '#/state/shell' -import {ToggleButton} from '#/view/com/util/forms/ToggleButton' -import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader' -import {Text} from '#/view/com/util/text/Text' -import {ScrollView} from '#/view/com/util/Views' -import {AccessibilitySettingsScreen as NewAccessibilitySettingsScreen} from '#/screens/Settings/AccessibilitySettings' -import {atoms as a} from '#/alf' -import * as Layout from '#/components/Layout' - -type Props = NativeStackScreenProps< - CommonNavigatorParams, - 'AccessibilitySettings' -> -export function AccessibilitySettingsScreen(props: Props) { - return IS_INTERNAL ? ( - <NewAccessibilitySettingsScreen {...props} /> - ) : ( - <LegacyAccessibilitySettingsScreen {...props} /> - ) -} - -function LegacyAccessibilitySettingsScreen({}: Props) { - const pal = usePalette('default') - const setMinimalShellMode = useSetMinimalShellMode() - const {isMobile, isTabletOrMobile} = useWebMediaQueries() - const {_} = useLingui() - - const requireAltTextEnabled = useRequireAltTextEnabled() - const setRequireAltTextEnabled = useSetRequireAltTextEnabled() - const autoplayDisabled = useAutoplayDisabled() - const setAutoplayDisabled = useSetAutoplayDisabled() - const hapticsDisabled = useHapticsDisabled() - const setHapticsDisabled = useSetHapticsDisabled() - const largeAltBadgeEnabled = useLargeAltBadgeEnabled() - const setLargeAltBadgeEnabled = useSetLargeAltBadgeEnabled() - - useFocusEffect( - React.useCallback(() => { - setMinimalShellMode(false) - }, [setMinimalShellMode]), - ) - - return ( - <Layout.Screen testID="accessibilitySettingsScreen"> - <SimpleViewHeader - showBackButton={isTabletOrMobile} - style={[ - pal.border, - a.border_b, - !isMobile && { - borderLeftWidth: StyleSheet.hairlineWidth, - borderRightWidth: StyleSheet.hairlineWidth, - }, - ]}> - <View style={a.flex_1}> - <Text type="title-lg" style={[pal.text, {fontWeight: '600'}]}> - <Trans>Accessibility Settings</Trans> - </Text> - </View> - </SimpleViewHeader> - <ScrollView - // @ts-ignore web only -prf - dataSet={{'stable-gutters': 1}} - style={s.flex1} - contentContainerStyle={[ - s.flex1, - {paddingBottom: 100}, - isMobile && pal.viewLight, - ]}> - <Text type="xl-bold" style={[pal.text, styles.heading]}> - <Trans>Alt text</Trans> - </Text> - <View style={[pal.view, styles.toggleCard]}> - <ToggleButton - type="default-light" - label={_(msg`Require alt text before posting`)} - labelType="lg" - isSelected={requireAltTextEnabled} - onPress={() => setRequireAltTextEnabled(!requireAltTextEnabled)} - /> - <ToggleButton - type="default-light" - label={_(msg`Display larger alt text badges`)} - labelType="lg" - isSelected={!!largeAltBadgeEnabled} - onPress={() => setLargeAltBadgeEnabled(!largeAltBadgeEnabled)} - /> - </View> - <Text type="xl-bold" style={[pal.text, styles.heading]}> - <Trans>Media</Trans> - </Text> - <View style={[pal.view, styles.toggleCard]}> - <ToggleButton - type="default-light" - label={_(msg`Disable autoplay for videos and GIFs`)} - labelType="lg" - isSelected={autoplayDisabled} - onPress={() => setAutoplayDisabled(!autoplayDisabled)} - /> - </View> - {isNative && ( - <> - <Text type="xl-bold" style={[pal.text, styles.heading]}> - <Trans>Haptics</Trans> - </Text> - <View style={[pal.view, styles.toggleCard]}> - <ToggleButton - type="default-light" - label={_(msg`Disable haptic feedback`)} - labelType="lg" - isSelected={hapticsDisabled} - onPress={() => setHapticsDisabled(!hapticsDisabled)} - /> - </View> - </> - )} - </ScrollView> - </Layout.Screen> - ) -} - -const styles = StyleSheet.create({ - heading: { - paddingHorizontal: 18, - paddingTop: 14, - paddingBottom: 6, - }, - toggleCard: { - paddingVertical: 8, - paddingHorizontal: 6, - marginBottom: 1, - }, -}) diff --git a/src/view/screens/AppPasswords.tsx b/src/view/screens/AppPasswords.tsx deleted file mode 100644 index 09da3c1d2..000000000 --- a/src/view/screens/AppPasswords.tsx +++ /dev/null @@ -1,375 +0,0 @@ -import React from 'react' -import { - ActivityIndicator, - StyleSheet, - TouchableOpacity, - View, -} from 'react-native' -import {ScrollView} from 'react-native-gesture-handler' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {useFocusEffect} from '@react-navigation/native' -import {NativeStackScreenProps} from '@react-navigation/native-stack' - -import {IS_INTERNAL} from '#/lib/app-info' -import {usePalette} from '#/lib/hooks/usePalette' -import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' -import {CommonNavigatorParams} from '#/lib/routes/types' -import {cleanError} from '#/lib/strings/errors' -import {useModalControls} from '#/state/modals' -import { - useAppPasswordDeleteMutation, - useAppPasswordsQuery, -} from '#/state/queries/app-passwords' -import {useSetMinimalShellMode} from '#/state/shell' -import {ErrorScreen} from '#/view/com/util/error/ErrorScreen' -import {Button} from '#/view/com/util/forms/Button' -import {Text} from '#/view/com/util/text/Text' -import * as Toast from '#/view/com/util/Toast' -import {ViewHeader} from '#/view/com/util/ViewHeader' -import {CenteredView} from '#/view/com/util/Views' -import {AppPasswordsScreen as NewAppPasswordsScreen} from '#/screens/Settings/AppPasswords' -import {atoms as a} from '#/alf' -import {useDialogControl} from '#/components/Dialog' -import * as Layout from '#/components/Layout' -import * as Prompt from '#/components/Prompt' - -type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppPasswords'> -export function AppPasswords(props: Props) { - return IS_INTERNAL ? ( - <NewAppPasswordsScreen {...props} /> - ) : ( - <Layout.Screen testID="AppPasswordsScreen"> - <AppPasswordsInner /> - </Layout.Screen> - ) -} - -function AppPasswordsInner() { - const pal = usePalette('default') - const {_} = useLingui() - const setMinimalShellMode = useSetMinimalShellMode() - const {isTabletOrDesktop} = useWebMediaQueries() - const {openModal} = useModalControls() - const {data: appPasswords, error} = useAppPasswordsQuery() - - useFocusEffect( - React.useCallback(() => { - setMinimalShellMode(false) - }, [setMinimalShellMode]), - ) - - const onAdd = React.useCallback(async () => { - openModal({name: 'add-app-password'}) - }, [openModal]) - - if (error) { - return ( - <CenteredView - style={[ - styles.container, - isTabletOrDesktop && styles.containerDesktop, - pal.view, - pal.border, - ]} - testID="appPasswordsScreen"> - <ErrorScreen - title={_(msg`Oops!`)} - message={_(msg`There was an issue with fetching your app passwords`)} - details={cleanError(error)} - /> - </CenteredView> - ) - } - - // no app passwords (empty) state - if (appPasswords?.length === 0) { - return ( - <CenteredView - style={[ - styles.container, - isTabletOrDesktop && styles.containerDesktop, - pal.view, - pal.border, - ]} - testID="appPasswordsScreen"> - <AppPasswordsHeader /> - <View style={[styles.empty, pal.viewLight]}> - <Text type="lg" style={[pal.text, styles.emptyText]}> - <Trans> - You have not created any app passwords yet. You can create one by - pressing the button below. - </Trans> - </Text> - </View> - {!isTabletOrDesktop && <View style={styles.flex1} />} - <View - style={[ - styles.btnContainer, - isTabletOrDesktop && styles.btnContainerDesktop, - ]}> - <Button - testID="appPasswordBtn" - type="primary" - label={_(msg`Add App Password`)} - style={styles.btn} - labelStyle={styles.btnLabel} - onPress={onAdd} - /> - </View> - </CenteredView> - ) - } - - if (appPasswords?.length) { - // has app passwords - return ( - <CenteredView - style={[ - styles.container, - isTabletOrDesktop && styles.containerDesktop, - pal.view, - pal.border, - ]} - testID="appPasswordsScreen"> - <AppPasswordsHeader /> - <ScrollView - style={[ - styles.scrollContainer, - pal.border, - !isTabletOrDesktop && styles.flex1, - ]}> - {appPasswords.map((password, i) => ( - <AppPassword - key={password.name} - testID={`appPassword-${i}`} - name={password.name} - createdAt={password.createdAt} - privileged={password.privileged} - /> - ))} - {isTabletOrDesktop && ( - <View style={[styles.btnContainer, styles.btnContainerDesktop]}> - <Button - testID="appPasswordBtn" - type="primary" - label={_(msg`Add App Password`)} - style={styles.btn} - labelStyle={styles.btnLabel} - onPress={onAdd} - /> - </View> - )} - </ScrollView> - {!isTabletOrDesktop && ( - <View style={styles.btnContainer}> - <Button - testID="appPasswordBtn" - type="primary" - label={_(msg`Add App Password`)} - style={styles.btn} - labelStyle={styles.btnLabel} - onPress={onAdd} - /> - </View> - )} - </CenteredView> - ) - } - - return ( - <CenteredView - style={[ - styles.container, - isTabletOrDesktop && styles.containerDesktop, - pal.view, - pal.border, - ]} - testID="appPasswordsScreen"> - <ActivityIndicator /> - </CenteredView> - ) -} - -function AppPasswordsHeader() { - const {isTabletOrDesktop} = useWebMediaQueries() - const pal = usePalette('default') - const {_} = useLingui() - return ( - <> - <ViewHeader title={_(msg`App Passwords`)} showOnDesktop /> - <Text - type="sm" - style={[ - styles.description, - pal.text, - isTabletOrDesktop && styles.descriptionDesktop, - ]}> - <Trans> - Use app passwords to login to other Bluesky clients without giving - full access to your account or password. - </Trans> - </Text> - </> - ) -} - -function AppPassword({ - testID, - name, - createdAt, - privileged, -}: { - testID: string - name: string - createdAt: string - privileged?: boolean -}) { - const pal = usePalette('default') - const {_, i18n} = useLingui() - const control = useDialogControl() - const deleteMutation = useAppPasswordDeleteMutation() - - const onDelete = React.useCallback(async () => { - await deleteMutation.mutateAsync({name}) - Toast.show(_(msg`App password deleted`)) - }, [deleteMutation, name, _]) - - const onPress = React.useCallback(() => { - control.open() - }, [control]) - - return ( - <TouchableOpacity - testID={testID} - style={[styles.item, pal.border]} - onPress={onPress} - accessibilityRole="button" - accessibilityLabel={_(msg`Delete app password`)} - accessibilityHint=""> - <View> - <Text type="md-bold" style={pal.text}> - {name} - </Text> - <Text type="md" style={[pal.text, styles.pr10]} numberOfLines={1}> - <Trans> - Created{' '} - {i18n.date(createdAt, { - year: 'numeric', - month: 'numeric', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - })} - </Trans> - </Text> - {privileged && ( - <View style={[a.flex_row, a.gap_sm, a.align_center, a.mt_xs]}> - <FontAwesomeIcon - icon="circle-exclamation" - color={pal.colors.textLight} - size={14} - /> - <Text type="md" style={pal.textLight}> - <Trans>Allows access to direct messages</Trans> - </Text> - </View> - )} - </View> - <FontAwesomeIcon icon={['far', 'trash-can']} style={styles.trashIcon} /> - - <Prompt.Basic - control={control} - title={_(msg`Delete app password?`)} - description={_( - msg`Are you sure you want to delete the app password "${name}"?`, - )} - onConfirm={onDelete} - confirmButtonCta={_(msg`Delete`)} - confirmButtonColor="negative" - /> - </TouchableOpacity> - ) -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - paddingBottom: 100, - }, - containerDesktop: { - borderLeftWidth: 1, - borderRightWidth: 1, - paddingBottom: 0, - }, - title: { - textAlign: 'center', - marginTop: 12, - marginBottom: 12, - }, - description: { - textAlign: 'center', - paddingHorizontal: 20, - marginBottom: 14, - }, - descriptionDesktop: { - marginTop: 14, - }, - - scrollContainer: { - borderTopWidth: 1, - marginTop: 4, - marginBottom: 16, - }, - - flex1: { - flex: 1, - }, - empty: { - paddingHorizontal: 20, - paddingVertical: 20, - borderRadius: 16, - marginHorizontal: 24, - marginTop: 10, - }, - emptyText: { - textAlign: 'center', - }, - - item: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - borderBottomWidth: 1, - paddingHorizontal: 20, - paddingVertical: 14, - }, - pr10: { - marginRight: 10, - }, - btnContainer: { - flexDirection: 'row', - justifyContent: 'center', - }, - btnContainerDesktop: { - marginTop: 14, - }, - btn: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - borderRadius: 32, - paddingHorizontal: 60, - paddingVertical: 14, - }, - btnLabel: { - fontSize: 18, - }, - - trashIcon: { - color: 'red', - minWidth: 16, - }, -}) diff --git a/src/view/screens/LanguageSettings.tsx b/src/view/screens/LanguageSettings.tsx deleted file mode 100644 index f99cccee9..000000000 --- a/src/view/screens/LanguageSettings.tsx +++ /dev/null @@ -1,336 +0,0 @@ -import React from 'react' -import {StyleSheet, View} from 'react-native' -import RNPickerSelect, {PickerSelectProps} from 'react-native-picker-select' -import { - FontAwesomeIcon, - FontAwesomeIconStyle, -} from '@fortawesome/react-native-fontawesome' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {useFocusEffect} from '@react-navigation/native' - -import {APP_LANGUAGES, LANGUAGES} from '#/lib/../locale/languages' -import {IS_INTERNAL} from '#/lib/app-info' -import {usePalette} from '#/lib/hooks/usePalette' -import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' -import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' -import {s} from '#/lib/styles' -import {sanitizeAppLanguageSetting} from '#/locale/helpers' -import {useModalControls} from '#/state/modals' -import {useLanguagePrefs, useLanguagePrefsApi} from '#/state/preferences' -import {useSetMinimalShellMode} from '#/state/shell' -import {Button} from '#/view/com/util/forms/Button' -import {Text} from '#/view/com/util/text/Text' -import {ViewHeader} from '#/view/com/util/ViewHeader' -import {CenteredView} from '#/view/com/util/Views' -import {LanguageSettingsScreen as NewLanguageSettingsScreen} from '#/screens/Settings/LanguageSettings' -import * as Layout from '#/components/Layout' - -type Props = NativeStackScreenProps<CommonNavigatorParams, 'LanguageSettings'> - -export function LanguageSettingsScreen(props: Props) { - return IS_INTERNAL ? ( - <NewLanguageSettingsScreen {...props} /> - ) : ( - <LegacyLanguageSettingsScreen {...props} /> - ) -} - -function LegacyLanguageSettingsScreen(_props: Props) { - const pal = usePalette('default') - const {_} = useLingui() - const langPrefs = useLanguagePrefs() - const setLangPrefs = useLanguagePrefsApi() - const {isTabletOrDesktop} = useWebMediaQueries() - const setMinimalShellMode = useSetMinimalShellMode() - const {openModal} = useModalControls() - - useFocusEffect( - React.useCallback(() => { - setMinimalShellMode(false) - }, [setMinimalShellMode]), - ) - - const onPressContentLanguages = React.useCallback(() => { - openModal({name: 'content-languages-settings'}) - }, [openModal]) - - const onChangePrimaryLanguage = React.useCallback( - (value: Parameters<PickerSelectProps['onValueChange']>[0]) => { - if (!value) return - if (langPrefs.primaryLanguage !== value) { - setLangPrefs.setPrimaryLanguage(value) - } - }, - [langPrefs, setLangPrefs], - ) - - const onChangeAppLanguage = React.useCallback( - (value: Parameters<PickerSelectProps['onValueChange']>[0]) => { - if (!value) return - if (langPrefs.appLanguage !== value) { - setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value)) - } - }, - [langPrefs, setLangPrefs], - ) - - const myLanguages = React.useMemo(() => { - return ( - langPrefs.contentLanguages - .map(lang => LANGUAGES.find(l => l.code2 === lang)) - .filter(Boolean) - // @ts-ignore - .map(l => l.name) - .join(', ') - ) - }, [langPrefs.contentLanguages]) - - return ( - <Layout.Screen testID="PreferencesLanguagesScreen"> - <CenteredView - style={[ - pal.view, - pal.border, - styles.container, - isTabletOrDesktop && styles.desktopContainer, - ]}> - <ViewHeader title={_(msg`Language Settings`)} showOnDesktop /> - - <View style={{paddingTop: 20, paddingHorizontal: 20}}> - {/* APP LANGUAGE */} - <View style={{paddingBottom: 20}}> - <Text type="title-sm" style={[pal.text, s.pb5]}> - <Trans>App Language</Trans> - </Text> - <Text style={[pal.text, s.pb10]}> - <Trans> - Select your app language for the default text to display in the - app. - </Trans> - </Text> - - <View style={{position: 'relative'}}> - <RNPickerSelect - placeholder={{}} - value={sanitizeAppLanguageSetting(langPrefs.appLanguage)} - onValueChange={onChangeAppLanguage} - items={APP_LANGUAGES.filter(l => Boolean(l.code2)).map(l => ({ - label: l.name, - value: l.code2, - key: l.code2, - }))} - style={{ - inputAndroid: { - backgroundColor: pal.viewLight.backgroundColor, - color: pal.text.color, - fontSize: 14, - letterSpacing: 0.5, - fontWeight: '600', - paddingHorizontal: 14, - paddingVertical: 8, - borderRadius: 24, - }, - inputIOS: { - backgroundColor: pal.viewLight.backgroundColor, - color: pal.text.color, - fontSize: 14, - letterSpacing: 0.5, - fontWeight: '600', - paddingHorizontal: 14, - paddingVertical: 8, - borderRadius: 24, - }, - - inputWeb: { - cursor: 'pointer', - // @ts-ignore web only - '-moz-appearance': 'none', - '-webkit-appearance': 'none', - appearance: 'none', - outline: 0, - borderWidth: 0, - backgroundColor: pal.viewLight.backgroundColor, - color: pal.text.color, - fontSize: 14, - fontFamily: 'inherit', - letterSpacing: 0.5, - fontWeight: '600', - paddingHorizontal: 14, - paddingVertical: 8, - borderRadius: 24, - }, - }} - /> - - <View - style={{ - position: 'absolute', - top: 1, - right: 1, - bottom: 1, - width: 40, - backgroundColor: pal.viewLight.backgroundColor, - borderRadius: 24, - pointerEvents: 'none', - alignItems: 'center', - justifyContent: 'center', - }}> - <FontAwesomeIcon - icon="chevron-down" - style={pal.text as FontAwesomeIconStyle} - /> - </View> - </View> - </View> - - <View - style={{ - height: 1, - backgroundColor: pal.border.borderColor, - marginBottom: 20, - }} - /> - - {/* PRIMARY LANGUAGE */} - <View style={{paddingBottom: 20}}> - <Text type="title-sm" style={[pal.text, s.pb5]}> - <Trans>Primary Language</Trans> - </Text> - <Text style={[pal.text, s.pb10]}> - <Trans> - Select your preferred language for translations in your feed. - </Trans> - </Text> - - <View style={{position: 'relative'}}> - <RNPickerSelect - placeholder={{}} - value={langPrefs.primaryLanguage} - onValueChange={onChangePrimaryLanguage} - items={LANGUAGES.filter(l => Boolean(l.code2)).map(l => ({ - label: l.name, - value: l.code2, - key: l.code2 + l.code3, - }))} - style={{ - inputAndroid: { - backgroundColor: pal.viewLight.backgroundColor, - color: pal.text.color, - fontSize: 14, - letterSpacing: 0.5, - fontWeight: '600', - paddingHorizontal: 14, - paddingVertical: 8, - borderRadius: 24, - }, - inputIOS: { - backgroundColor: pal.viewLight.backgroundColor, - color: pal.text.color, - fontSize: 14, - letterSpacing: 0.5, - fontWeight: '600', - paddingHorizontal: 14, - paddingVertical: 8, - borderRadius: 24, - }, - inputWeb: { - cursor: 'pointer', - // @ts-ignore web only - '-moz-appearance': 'none', - '-webkit-appearance': 'none', - appearance: 'none', - outline: 0, - borderWidth: 0, - backgroundColor: pal.viewLight.backgroundColor, - color: pal.text.color, - fontSize: 14, - fontFamily: 'inherit', - letterSpacing: 0.5, - fontWeight: '600', - paddingHorizontal: 14, - paddingVertical: 8, - borderRadius: 24, - }, - }} - /> - - <View - style={{ - position: 'absolute', - top: 1, - right: 1, - bottom: 1, - width: 40, - backgroundColor: pal.viewLight.backgroundColor, - borderRadius: 24, - pointerEvents: 'none', - alignItems: 'center', - justifyContent: 'center', - }}> - <FontAwesomeIcon - icon="chevron-down" - style={pal.text as FontAwesomeIconStyle} - /> - </View> - </View> - </View> - - <View - style={{ - height: 1, - backgroundColor: pal.border.borderColor, - marginBottom: 20, - }} - /> - - {/* CONTENT LANGUAGES */} - <View style={{paddingBottom: 20}}> - <Text type="title-sm" style={[pal.text, s.pb5]}> - <Trans>Content Languages</Trans> - </Text> - <Text style={[pal.text, s.pb10]}> - <Trans> - Select which languages you want your subscribed feeds to - include. If none are selected, all languages will be shown. - </Trans> - </Text> - - <Button - type="default" - onPress={onPressContentLanguages} - style={styles.button}> - <FontAwesomeIcon - icon={myLanguages.length ? 'check' : 'plus'} - style={pal.text as FontAwesomeIconStyle} - /> - <Text - type="button" - style={[pal.text, {flexShrink: 1, overflow: 'hidden'}]} - numberOfLines={1}> - {myLanguages.length ? myLanguages : _(msg`Select languages`)} - </Text> - </Button> - </View> - </View> - </CenteredView> - </Layout.Screen> - ) -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - paddingBottom: 90, - }, - desktopContainer: { - borderLeftWidth: 1, - borderRightWidth: 1, - paddingBottom: 40, - }, - button: { - flexDirection: 'row', - alignItems: 'center', - gap: 8, - }, -}) diff --git a/src/view/screens/PreferencesExternalEmbeds.tsx b/src/view/screens/PreferencesExternalEmbeds.tsx deleted file mode 100644 index ef3f73b3c..000000000 --- a/src/view/screens/PreferencesExternalEmbeds.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import React from 'react' -import {StyleSheet, View} from 'react-native' -import {Trans} from '@lingui/macro' -import {useFocusEffect} from '@react-navigation/native' - -import {IS_INTERNAL} from '#/lib/app-info' -import {usePalette} from '#/lib/hooks/usePalette' -import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' -import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' -import { - EmbedPlayerSource, - externalEmbedLabels, -} from '#/lib/strings/embed-player' -import { - useExternalEmbedsPrefs, - useSetExternalEmbedPref, -} from '#/state/preferences' -import {useSetMinimalShellMode} from '#/state/shell' -import {ToggleButton} from '#/view/com/util/forms/ToggleButton' -import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader' -import {Text} from '#/view/com/util/text/Text' -import {ScrollView} from '#/view/com/util/Views' -import {ExternalMediaPreferencesScreen} from '#/screens/Settings/ExternalMediaPreferences' -import {atoms as a} from '#/alf' -import * as Layout from '#/components/Layout' - -type Props = NativeStackScreenProps< - CommonNavigatorParams, - 'PreferencesExternalEmbeds' -> -export function PreferencesExternalEmbeds(props: Props) { - return IS_INTERNAL ? ( - <ExternalMediaPreferencesScreen {...props} /> - ) : ( - <LegacyPreferencesExternalEmbeds {...props} /> - ) -} - -function LegacyPreferencesExternalEmbeds({}: Props) { - const pal = usePalette('default') - const setMinimalShellMode = useSetMinimalShellMode() - const {isTabletOrMobile} = useWebMediaQueries() - - useFocusEffect( - React.useCallback(() => { - setMinimalShellMode(false) - }, [setMinimalShellMode]), - ) - - return ( - <Layout.Screen testID="preferencesExternalEmbedsScreen"> - <ScrollView - // @ts-ignore web only -prf - dataSet={{'stable-gutters': 1}} - contentContainerStyle={[pal.viewLight, {paddingBottom: 75}]}> - <SimpleViewHeader - showBackButton={isTabletOrMobile} - style={[pal.border, a.border_b]}> - <View style={a.flex_1}> - <Text type="title-lg" style={[pal.text, {fontWeight: '600'}]}> - <Trans>External Media Preferences</Trans> - </Text> - <Text style={pal.textLight}> - <Trans>Customize media from external sites.</Trans> - </Text> - </View> - </SimpleViewHeader> - - <View style={[pal.view]}> - <View style={styles.infoCard}> - <Text style={pal.text}> - <Trans> - External media may allow websites to collect information about - you and your device. No information is sent or requested until - you press the "play" button. - </Trans> - </Text> - </View> - </View> - <Text type="xl-bold" style={[pal.text, styles.heading]}> - <Trans>Enable media players for</Trans> - </Text> - {Object.entries(externalEmbedLabels) - // TODO: Remove special case when we disable the old integration. - .filter(([key]) => key !== 'tenor') - .map(([key, label]) => ( - <PrefSelector - source={key as EmbedPlayerSource} - label={label} - key={key} - /> - ))} - </ScrollView> - </Layout.Screen> - ) -} - -function PrefSelector({ - source, - label, -}: { - source: EmbedPlayerSource - label: string -}) { - const pal = usePalette('default') - const setExternalEmbedPref = useSetExternalEmbedPref() - const sources = useExternalEmbedsPrefs() - - return ( - <View> - <View style={[pal.view, styles.toggleCard]}> - <ToggleButton - type="default-light" - label={label} - labelType="lg" - isSelected={sources?.[source] === 'show'} - onPress={() => - setExternalEmbedPref( - source, - sources?.[source] === 'show' ? 'hide' : 'show', - ) - } - /> - </View> - </View> - ) -} - -const styles = StyleSheet.create({ - heading: { - paddingHorizontal: 18, - paddingTop: 14, - paddingBottom: 14, - }, - spacer: { - height: 8, - }, - infoCard: { - paddingHorizontal: 20, - paddingVertical: 14, - }, - toggleCard: { - paddingVertical: 8, - paddingHorizontal: 6, - marginBottom: 1, - }, -}) diff --git a/src/view/screens/PreferencesFollowingFeed.tsx b/src/view/screens/PreferencesFollowingFeed.tsx deleted file mode 100644 index c31a23c49..000000000 --- a/src/view/screens/PreferencesFollowingFeed.tsx +++ /dev/null @@ -1,249 +0,0 @@ -import React from 'react' -import {StyleSheet, View} from 'react-native' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {IS_INTERNAL} from '#/lib/app-info' -import {usePalette} from '#/lib/hooks/usePalette' -import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' -import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' -import {colors, s} from '#/lib/styles' -import { - usePreferencesQuery, - useSetFeedViewPreferencesMutation, -} from '#/state/queries/preferences' -import {ToggleButton} from '#/view/com/util/forms/ToggleButton' -import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader' -import {Text} from '#/view/com/util/text/Text' -import {ScrollView} from '#/view/com/util/Views' -import {FollowingFeedPreferencesScreen} from '#/screens/Settings/FollowingFeedPreferences' -import {atoms as a} from '#/alf' -import * as Layout from '#/components/Layout' - -type Props = NativeStackScreenProps< - CommonNavigatorParams, - 'PreferencesFollowingFeed' -> -export function PreferencesFollowingFeed(props: Props) { - return IS_INTERNAL ? ( - <FollowingFeedPreferencesScreen {...props} /> - ) : ( - <LegacyPreferencesFollowingFeed {...props} /> - ) -} - -function LegacyPreferencesFollowingFeed({}: Props) { - const pal = usePalette('default') - const {_} = useLingui() - const {isTabletOrMobile} = useWebMediaQueries() - const {data: preferences} = usePreferencesQuery() - const {mutate: setFeedViewPref, variables} = - useSetFeedViewPreferencesMutation() - - const showReplies = !( - variables?.hideReplies ?? preferences?.feedViewPrefs?.hideReplies - ) - - return ( - <Layout.Screen testID="preferencesHomeFeedScreen"> - <ScrollView - // @ts-ignore web only -sfn - dataSet={{'stable-gutters': 1}} - contentContainerStyle={{paddingBottom: 75}}> - <SimpleViewHeader - showBackButton={isTabletOrMobile} - style={[pal.border, a.border_b]}> - <View style={a.flex_1}> - <Text type="title-lg" style={[pal.text, {fontWeight: '600'}]}> - <Trans>Following Feed Preferences</Trans> - </Text> - <Text style={pal.textLight}> - <Trans> - Fine-tune the content you see on your Following feed. - </Trans> - </Text> - </View> - </SimpleViewHeader> - <View style={styles.cardsContainer}> - <View style={[pal.viewLight, styles.card]}> - <Text type="title-sm" style={[pal.text, s.pb5]}> - <Trans>Show Replies</Trans> - </Text> - <Text style={[pal.text, s.pb10]}> - <Trans> - Set this setting to "No" to hide all replies from your feed. - </Trans> - </Text> - <ToggleButton - testID="toggleRepliesBtn" - type="default-light" - label={showReplies ? _(msg`Yes`) : _(msg`No`)} - isSelected={showReplies} - onPress={() => - setFeedViewPref({ - hideReplies: !( - variables?.hideReplies ?? - preferences?.feedViewPrefs?.hideReplies - ), - }) - } - /> - </View> - <View style={[pal.viewLight, styles.card]}> - <Text type="title-sm" style={[pal.text, s.pb5]}> - <Trans>Show Reposts</Trans> - </Text> - <Text style={[pal.text, s.pb10]}> - <Trans> - Set this setting to "No" to hide all reposts from your feed. - </Trans> - </Text> - <ToggleButton - type="default-light" - label={ - variables?.hideReposts ?? - preferences?.feedViewPrefs?.hideReposts - ? _(msg`No`) - : _(msg`Yes`) - } - isSelected={ - !( - variables?.hideReposts ?? - preferences?.feedViewPrefs?.hideReposts - ) - } - onPress={() => - setFeedViewPref({ - hideReposts: !( - variables?.hideReposts ?? - preferences?.feedViewPrefs?.hideReposts - ), - }) - } - /> - </View> - - <View style={[pal.viewLight, styles.card]}> - <Text type="title-sm" style={[pal.text, s.pb5]}> - <Trans>Show Quote Posts</Trans> - </Text> - <Text style={[pal.text, s.pb10]}> - <Trans> - Set this setting to "No" to hide all quote posts from your feed. - Reposts will still be visible. - </Trans> - </Text> - <ToggleButton - type="default-light" - label={ - variables?.hideQuotePosts ?? - preferences?.feedViewPrefs?.hideQuotePosts - ? _(msg`No`) - : _(msg`Yes`) - } - isSelected={ - !( - variables?.hideQuotePosts ?? - preferences?.feedViewPrefs?.hideQuotePosts - ) - } - onPress={() => - setFeedViewPref({ - hideQuotePosts: !( - variables?.hideQuotePosts ?? - preferences?.feedViewPrefs?.hideQuotePosts - ), - }) - } - /> - </View> - - <View style={[pal.viewLight, styles.card]}> - <Text type="title-sm" style={[pal.text, s.pb5]}> - <FontAwesomeIcon icon="flask" color={pal.colors.text} />{' '} - <Trans>Show Posts from My Feeds</Trans> - </Text> - <Text style={[pal.text, s.pb10]}> - <Trans> - Set this setting to "Yes" to show samples of your saved feeds in - your Following feed. This is an experimental feature. - </Trans> - </Text> - <ToggleButton - type="default-light" - label={ - variables?.lab_mergeFeedEnabled ?? - preferences?.feedViewPrefs?.lab_mergeFeedEnabled - ? _(msg`Yes`) - : _(msg`No`) - } - isSelected={ - !!( - variables?.lab_mergeFeedEnabled ?? - preferences?.feedViewPrefs?.lab_mergeFeedEnabled - ) - } - onPress={() => - setFeedViewPref({ - lab_mergeFeedEnabled: !( - variables?.lab_mergeFeedEnabled ?? - preferences?.feedViewPrefs?.lab_mergeFeedEnabled - ), - }) - } - /> - </View> - </View> - </ScrollView> - </Layout.Screen> - ) -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - }, - desktopContainer: { - borderLeftWidth: 1, - borderRightWidth: 1, - }, - titleSection: { - paddingBottom: 30, - }, - title: { - textAlign: 'center', - marginBottom: 5, - }, - description: { - textAlign: 'center', - paddingHorizontal: 32, - }, - cardsContainer: { - paddingHorizontal: 20, - paddingVertical: 16, - }, - card: { - padding: 16, - borderRadius: 10, - marginBottom: 20, - }, - btn: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - borderRadius: 32, - padding: 14, - backgroundColor: colors.blue3, - }, - btnDesktop: { - marginHorizontal: 'auto', - paddingHorizontal: 80, - }, - btnContainer: { - paddingTop: 20, - }, - dimmed: { - opacity: 0.3, - }, -}) diff --git a/src/view/screens/PreferencesThreads.tsx b/src/view/screens/PreferencesThreads.tsx deleted file mode 100644 index f511f4c59..000000000 --- a/src/view/screens/PreferencesThreads.tsx +++ /dev/null @@ -1,198 +0,0 @@ -import React from 'react' -import {ActivityIndicator, StyleSheet, View} from 'react-native' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {IS_INTERNAL} from '#/lib/app-info' -import {usePalette} from '#/lib/hooks/usePalette' -import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' -import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' -import {colors, s} from '#/lib/styles' -import { - usePreferencesQuery, - useSetThreadViewPreferencesMutation, -} from '#/state/queries/preferences' -import {RadioGroup} from '#/view/com/util/forms/RadioGroup' -import {ToggleButton} from '#/view/com/util/forms/ToggleButton' -import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader' -import {Text} from '#/view/com/util/text/Text' -import {ScrollView} from '#/view/com/util/Views' -import {ThreadPreferencesScreen} from '#/screens/Settings/ThreadPreferences' -import {atoms as a} from '#/alf' -import * as Layout from '#/components/Layout' - -type Props = NativeStackScreenProps<CommonNavigatorParams, 'PreferencesThreads'> -export function PreferencesThreads(props: Props) { - return IS_INTERNAL ? ( - <ThreadPreferencesScreen {...props} /> - ) : ( - <LegacyPreferencesThreads {...props} /> - ) -} - -function LegacyPreferencesThreads({}: Props) { - const pal = usePalette('default') - const {_} = useLingui() - const {isTabletOrMobile} = useWebMediaQueries() - const {data: preferences} = usePreferencesQuery() - const {mutate: setThreadViewPrefs, variables} = - useSetThreadViewPreferencesMutation() - - const prioritizeFollowedUsers = Boolean( - variables?.prioritizeFollowedUsers ?? - preferences?.threadViewPrefs?.prioritizeFollowedUsers, - ) - const treeViewEnabled = Boolean( - variables?.lab_treeViewEnabled ?? - preferences?.threadViewPrefs?.lab_treeViewEnabled, - ) - - return ( - <Layout.Screen testID="preferencesThreadsScreen"> - <ScrollView - // @ts-ignore web only -prf - dataSet={{'stable-gutters': 1}} - contentContainerStyle={{paddingBottom: 75}}> - <SimpleViewHeader - showBackButton={isTabletOrMobile} - style={[pal.border, a.border_b]}> - <View style={a.flex_1}> - <Text type="title-lg" style={[pal.text, {fontWeight: '600'}]}> - <Trans>Thread Preferences</Trans> - </Text> - <Text style={pal.textLight}> - <Trans>Fine-tune the discussion threads.</Trans> - </Text> - </View> - </SimpleViewHeader> - - {preferences ? ( - <View style={styles.cardsContainer}> - <View style={[pal.viewLight, styles.card]}> - <Text type="title-sm" style={[pal.text, s.pb5]}> - <Trans>Sort Replies</Trans> - </Text> - <Text style={[pal.text, s.pb10]}> - <Trans>Sort replies to the same post by:</Trans> - </Text> - <View style={[pal.view, {borderRadius: 8, paddingVertical: 6}]}> - <RadioGroup - type="default-light" - items={[ - {key: 'oldest', label: _(msg`Oldest replies first`)}, - {key: 'newest', label: _(msg`Newest replies first`)}, - { - key: 'most-likes', - label: _(msg`Most-liked replies first`), - }, - { - key: 'random', - label: _(msg`Random (aka "Poster's Roulette")`), - }, - ]} - onSelect={key => setThreadViewPrefs({sort: key})} - initialSelection={preferences?.threadViewPrefs?.sort} - /> - </View> - </View> - - <View style={[pal.viewLight, styles.card]}> - <Text type="title-sm" style={[pal.text, s.pb5]}> - <Trans>Prioritize Your Follows</Trans> - </Text> - <Text style={[pal.text, s.pb10]}> - <Trans> - Show replies by people you follow before all other replies. - </Trans> - </Text> - <ToggleButton - type="default-light" - label={prioritizeFollowedUsers ? _(msg`Yes`) : _(msg`No`)} - isSelected={prioritizeFollowedUsers} - onPress={() => - setThreadViewPrefs({ - prioritizeFollowedUsers: !prioritizeFollowedUsers, - }) - } - /> - </View> - - <View style={[pal.viewLight, styles.card]}> - <Text type="title-sm" style={[pal.text, s.pb5]}> - <FontAwesomeIcon icon="flask" color={pal.colors.text} />{' '} - <Trans>Threaded Mode</Trans> - </Text> - <Text style={[pal.text, s.pb10]}> - <Trans> - Set this setting to "Yes" to show replies in a threaded view. - This is an experimental feature. - </Trans> - </Text> - <ToggleButton - type="default-light" - label={treeViewEnabled ? _(msg`Yes`) : _(msg`No`)} - isSelected={treeViewEnabled} - onPress={() => - setThreadViewPrefs({ - lab_treeViewEnabled: !treeViewEnabled, - }) - } - /> - </View> - </View> - ) : ( - <ActivityIndicator style={a.flex_1} /> - )} - </ScrollView> - </Layout.Screen> - ) -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - }, - desktopContainer: { - borderLeftWidth: 1, - borderRightWidth: 1, - }, - titleSection: { - paddingBottom: 30, - }, - title: { - textAlign: 'center', - marginBottom: 5, - }, - description: { - textAlign: 'center', - paddingHorizontal: 32, - }, - cardsContainer: { - paddingHorizontal: 20, - paddingVertical: 16, - }, - card: { - padding: 16, - borderRadius: 10, - marginBottom: 20, - }, - btn: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - borderRadius: 32, - padding: 14, - backgroundColor: colors.blue3, - }, - btnDesktop: { - marginHorizontal: 'auto', - paddingHorizontal: 80, - }, - btnContainer: { - paddingTop: 20, - }, - dimmed: { - opacity: 0.3, - }, -}) diff --git a/src/view/screens/Settings/DisableEmail2FADialog.tsx b/src/view/screens/Settings/DisableEmail2FADialog.tsx deleted file mode 100644 index 1378759b0..000000000 --- a/src/view/screens/Settings/DisableEmail2FADialog.tsx +++ /dev/null @@ -1,201 +0,0 @@ -import React, {useState} from 'react' -import {View} from 'react-native' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {cleanError} from '#/lib/strings/errors' -import {isNative} from '#/platform/detection' -import {useAgent, useSession} from '#/state/session' -import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' -import * as Toast from '#/view/com/util/Toast' -import {atoms as a, useBreakpoints, useTheme} from '#/alf' -import {Button, ButtonIcon, ButtonText} from '#/components/Button' -import * as Dialog from '#/components/Dialog' -import * as TextField from '#/components/forms/TextField' -import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock' -import {Loader} from '#/components/Loader' -import {P, Text} from '#/components/Typography' - -enum Stages { - Email, - ConfirmCode, -} - -export function DisableEmail2FADialog({ - control, -}: { - control: Dialog.DialogOuterProps['control'] -}) { - const {_} = useLingui() - const t = useTheme() - const {gtMobile} = useBreakpoints() - const {currentAccount} = useSession() - const agent = useAgent() - - const [stage, setStage] = useState<Stages>(Stages.Email) - const [confirmationCode, setConfirmationCode] = useState<string>('') - const [isProcessing, setIsProcessing] = useState<boolean>(false) - const [error, setError] = useState<string>('') - - const onSendEmail = async () => { - setError('') - setIsProcessing(true) - try { - await agent.com.atproto.server.requestEmailUpdate() - setStage(Stages.ConfirmCode) - } catch (e) { - setError(cleanError(String(e))) - } finally { - setIsProcessing(false) - } - } - - const onConfirmDisable = async () => { - setError('') - setIsProcessing(true) - try { - if (currentAccount?.email) { - await agent.com.atproto.server.updateEmail({ - email: currentAccount!.email, - token: confirmationCode.trim(), - emailAuthFactor: false, - }) - await agent.resumeSession(agent.session!) - Toast.show(_(msg`Email 2FA disabled`)) - } - control.close() - } catch (e) { - const errMsg = String(e) - if (errMsg.includes('Token is invalid')) { - setError(_(msg`Invalid 2FA confirmation code.`)) - } else { - setError(cleanError(errMsg)) - } - } finally { - setIsProcessing(false) - } - } - - return ( - <Dialog.Outer control={control}> - <Dialog.Handle /> - <Dialog.ScrollableInner - accessibilityDescribedBy="dialog-description" - accessibilityLabelledBy="dialog-title"> - <View style={[a.relative, a.gap_md, a.w_full]}> - <Text - nativeID="dialog-title" - style={[a.text_2xl, a.font_bold, t.atoms.text]}> - <Trans>Disable Email 2FA</Trans> - </Text> - <P nativeID="dialog-description"> - {stage === Stages.ConfirmCode ? ( - <Trans> - An email has been sent to{' '} - {currentAccount?.email || '(no email)'}. It includes a - confirmation code which you can enter below. - </Trans> - ) : ( - <Trans> - To disable the email 2FA method, please verify your access to - the email address. - </Trans> - )} - </P> - - {error ? <ErrorMessage message={error} /> : undefined} - - {stage === Stages.Email ? ( - <View - style={[ - a.gap_sm, - gtMobile && [a.flex_row, a.justify_end, a.gap_md], - ]}> - <Button - testID="sendEmailButton" - variant="solid" - color="primary" - size={gtMobile ? 'small' : 'large'} - onPress={onSendEmail} - label={_(msg`Send verification email`)} - disabled={isProcessing}> - <ButtonText> - <Trans>Send verification email</Trans> - </ButtonText> - {isProcessing && <ButtonIcon icon={Loader} />} - </Button> - <Button - testID="haveCodeButton" - variant="ghost" - color="primary" - size={gtMobile ? 'small' : 'large'} - onPress={() => setStage(Stages.ConfirmCode)} - label={_(msg`I have a code`)} - disabled={isProcessing}> - <ButtonText> - <Trans>I have a code</Trans> - </ButtonText> - </Button> - </View> - ) : stage === Stages.ConfirmCode ? ( - <View> - <View style={[a.mb_md]}> - <TextField.LabelText> - <Trans>Confirmation code</Trans> - </TextField.LabelText> - <TextField.Root> - <TextField.Icon icon={Lock} /> - <Dialog.Input - testID="confirmationCode" - label={_(msg`Confirmation code`)} - autoCapitalize="none" - autoFocus - autoCorrect={false} - autoComplete="off" - value={confirmationCode} - onChangeText={setConfirmationCode} - onSubmitEditing={onConfirmDisable} - editable={!isProcessing} - /> - </TextField.Root> - </View> - <View - style={[ - a.gap_sm, - gtMobile && [a.flex_row, a.justify_end, a.gap_md], - ]}> - <Button - testID="resendCodeBtn" - variant="ghost" - color="primary" - size={gtMobile ? 'small' : 'large'} - onPress={onSendEmail} - label={_(msg`Resend email`)} - disabled={isProcessing}> - <ButtonText> - <Trans>Resend email</Trans> - </ButtonText> - </Button> - <Button - testID="confirmBtn" - variant="solid" - color="primary" - size={gtMobile ? 'small' : 'large'} - onPress={onConfirmDisable} - label={_(msg`Confirm`)} - disabled={isProcessing}> - <ButtonText> - <Trans>Confirm</Trans> - </ButtonText> - {isProcessing && <ButtonIcon icon={Loader} />} - </Button> - </View> - </View> - ) : undefined} - - {!gtMobile && isNative && <View style={{height: 40}} />} - </View> - </Dialog.ScrollableInner> - </Dialog.Outer> - ) -} diff --git a/src/view/screens/Settings/Email2FAToggle.tsx b/src/view/screens/Settings/Email2FAToggle.tsx deleted file mode 100644 index f6ed19a21..000000000 --- a/src/view/screens/Settings/Email2FAToggle.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import React from 'react' -import {msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {useModalControls} from '#/state/modals' -import {useAgent, useSession} from '#/state/session' -import {ToggleButton} from '#/view/com/util/forms/ToggleButton' -import {useDialogControl} from '#/components/Dialog' -import {DisableEmail2FADialog} from './DisableEmail2FADialog' - -export function Email2FAToggle() { - const {_} = useLingui() - const {currentAccount} = useSession() - const {openModal} = useModalControls() - const disableDialogCtrl = useDialogControl() - const agent = useAgent() - - const enableEmailAuthFactor = React.useCallback(async () => { - if (currentAccount?.email) { - await agent.com.atproto.server.updateEmail({ - email: currentAccount.email, - emailAuthFactor: true, - }) - await agent.resumeSession(agent.session!) - } - }, [currentAccount, agent]) - - const onToggle = React.useCallback(() => { - if (!currentAccount) { - return - } - if (currentAccount.emailAuthFactor) { - disableDialogCtrl.open() - } else { - if (!currentAccount.emailConfirmed) { - openModal({ - name: 'verify-email', - onSuccess: enableEmailAuthFactor, - }) - return - } - enableEmailAuthFactor() - } - }, [currentAccount, enableEmailAuthFactor, openModal, disableDialogCtrl]) - - return ( - <> - <DisableEmail2FADialog control={disableDialogCtrl} /> - <ToggleButton - type="default-light" - label={_(msg`Require email code to log into your account`)} - labelType="lg" - isSelected={!!currentAccount?.emailAuthFactor} - onPress={onToggle} - /> - </> - ) -} diff --git a/src/view/screens/Settings/ExportCarDialog.tsx b/src/view/screens/Settings/ExportCarDialog.tsx deleted file mode 100644 index 2de3895d3..000000000 --- a/src/view/screens/Settings/ExportCarDialog.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import React from 'react' -import {View} from 'react-native' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {saveBytesToDisk} from '#/lib/media/manip' -import {logger} from '#/logger' -import {useAgent} from '#/state/session' -import * as Toast from '#/view/com/util/Toast' -import {atoms as a, useTheme} from '#/alf' -import {Button, ButtonIcon, ButtonText} from '#/components/Button' -import * as Dialog from '#/components/Dialog' -import {Download_Stroke2_Corner0_Rounded as DownloadIcon} from '#/components/icons/Download' -import {InlineLinkText} from '#/components/Link' -import {Loader} from '#/components/Loader' -import {Text} from '#/components/Typography' - -export function ExportCarDialog({ - control, -}: { - control: Dialog.DialogOuterProps['control'] -}) { - const {_} = useLingui() - const t = useTheme() - const agent = useAgent() - const [loading, setLoading] = React.useState(false) - - const download = React.useCallback(async () => { - if (!agent.session) { - return // shouldnt ever happen - } - try { - setLoading(true) - const did = agent.session.did - const downloadRes = await agent.com.atproto.sync.getRepo({did}) - const saveRes = await saveBytesToDisk( - 'repo.car', - downloadRes.data, - downloadRes.headers['content-type'], - ) - - if (saveRes) { - Toast.show(_(msg`File saved successfully!`)) - } - } catch (e) { - logger.error('Error occurred while downloading CAR file', {message: e}) - Toast.show(_(msg`Error occurred while saving file`), 'xmark') - } finally { - setLoading(false) - control.close() - } - }, [_, control, agent]) - - return ( - <Dialog.Outer control={control}> - <Dialog.Handle /> - <Dialog.ScrollableInner - accessibilityDescribedBy="dialog-description" - accessibilityLabelledBy="dialog-title"> - <View style={[a.relative, a.gap_lg, a.w_full]}> - <Text nativeID="dialog-title" style={[a.text_2xl, a.font_bold]}> - <Trans>Export My Data</Trans> - </Text> - <Text nativeID="dialog-description" style={[a.text_sm]}> - <Trans> - Your account repository, containing all public data records, can - be downloaded as a "CAR" file. This file does not include media - embeds, such as images, or your private data, which must be - fetched separately. - </Trans> - </Text> - - <Button - variant="solid" - color="primary" - size="large" - label={_(msg`Download CAR file`)} - disabled={loading} - onPress={download}> - <ButtonIcon icon={DownloadIcon} /> - <ButtonText> - <Trans>Download CAR file</Trans> - </ButtonText> - {loading && <ButtonIcon icon={Loader} />} - </Button> - - <Text - style={[ - t.atoms.text_contrast_medium, - a.text_sm, - a.leading_snug, - a.flex_1, - ]}> - <Trans> - This feature is in beta. You can read more about repository - exports in{' '} - <InlineLinkText - label={_(msg`View blogpost for more details`)} - to="https://docs.bsky.app/blog/repo-export" - style={[a.text_sm]}> - this blogpost - </InlineLinkText> - . - </Trans> - </Text> - </View> - </Dialog.ScrollableInner> - </Dialog.Outer> - ) -} diff --git a/src/view/screens/Settings/index.tsx b/src/view/screens/Settings/index.tsx deleted file mode 100644 index 7ec7b5dce..000000000 --- a/src/view/screens/Settings/index.tsx +++ /dev/null @@ -1,1077 +0,0 @@ -import React from 'react' -import { - Platform, - Pressable, - StyleSheet, - TextStyle, - TouchableOpacity, - View, - ViewStyle, -} from 'react-native' -import {setStringAsync} from 'expo-clipboard' -import { - FontAwesomeIcon, - FontAwesomeIconStyle, -} from '@fortawesome/react-native-fontawesome' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {useFocusEffect, useNavigation} from '@react-navigation/native' -import {useQueryClient} from '@tanstack/react-query' - -import {appVersion, BUNDLE_DATE, bundleInfo, IS_INTERNAL} from '#/lib/app-info' -import {STATUS_PAGE_URL} from '#/lib/constants' -import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher' -import {useCustomPalette} from '#/lib/hooks/useCustomPalette' -import {usePalette} from '#/lib/hooks/usePalette' -import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' -import {HandIcon, HashtagIcon} from '#/lib/icons' -import {makeProfileLink} from '#/lib/routes/links' -import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' -import {NavigationProp} from '#/lib/routes/types' -import {colors, s} from '#/lib/styles' -import {isNative} from '#/platform/detection' -import {useModalControls} from '#/state/modals' -import {clearStorage} from '#/state/persisted' -import { - useInAppBrowser, - useSetInAppBrowser, -} from '#/state/preferences/in-app-browser' -import {useDeleteActorDeclaration} from '#/state/queries/messages/actor-declaration' -import {useClearPreferencesMutation} from '#/state/queries/preferences' -import {RQKEY as RQKEY_PROFILE} from '#/state/queries/profile' -import {useProfileQuery} from '#/state/queries/profile' -import {SessionAccount, useSession, useSessionApi} from '#/state/session' -import {useOnboardingDispatch, useSetMinimalShellMode} from '#/state/shell' -import {useLoggedOutViewControls} from '#/state/shell/logged-out' -import {useCloseAllActiveElements} from '#/state/util' -import {AccountDropdownBtn} from '#/view/com/util/AccountDropdownBtn' -import {ToggleButton} from '#/view/com/util/forms/ToggleButton' -import {Link, TextLink} from '#/view/com/util/Link' -import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader' -import {Text} from '#/view/com/util/text/Text' -import * as Toast from '#/view/com/util/Toast' -import {UserAvatar} from '#/view/com/util/UserAvatar' -import {ScrollView} from '#/view/com/util/Views' -import {DeactivateAccountDialog} from '#/screens/Settings/components/DeactivateAccountDialog' -import {SettingsScreen as NewSettingsScreen} from '#/screens/Settings/Settings' -import {atoms as a, useTheme} from '#/alf' -import {useDialogControl} from '#/components/Dialog' -import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings' -import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog' -import * as Layout from '#/components/Layout' -import {Email2FAToggle} from './Email2FAToggle' -import {ExportCarDialog} from './ExportCarDialog' - -function SettingsAccountCard({ - account, - pendingDid, - onPressSwitchAccount, -}: { - account: SessionAccount - pendingDid: string | null - onPressSwitchAccount: ( - account: SessionAccount, - logContext: 'Settings', - ) => void -}) { - const pal = usePalette('default') - const {_} = useLingui() - const t = useTheme() - const {currentAccount} = useSession() - const {data: profile} = useProfileQuery({did: account.did}) - const isCurrentAccount = account.did === currentAccount?.did - - const contents = ( - <View - style={[ - pal.view, - styles.linkCard, - account.did === pendingDid && t.atoms.bg_contrast_25, - ]}> - <View style={styles.avi}> - <UserAvatar - size={40} - avatar={profile?.avatar} - type={profile?.associated?.labeler ? 'labeler' : 'user'} - /> - </View> - <View style={[s.flex1]}> - <Text - emoji - type="md-bold" - style={[pal.text, a.self_start]} - numberOfLines={1}> - {profile?.displayName || account.handle} - </Text> - <Text emoji type="sm" style={pal.textLight} numberOfLines={1}> - {account.handle} - </Text> - </View> - <AccountDropdownBtn account={account} /> - </View> - ) - - return isCurrentAccount ? ( - <Link - href={makeProfileLink({ - did: currentAccount?.did, - handle: currentAccount?.handle, - })} - title={_(msg`Your profile`)} - noFeedback> - {contents} - </Link> - ) : ( - <TouchableOpacity - testID={`switchToAccountBtn-${account.handle}`} - key={account.did} - onPress={ - pendingDid ? undefined : () => onPressSwitchAccount(account, 'Settings') - } - accessibilityRole="button" - accessibilityLabel={_(msg`Switch to ${account.handle}`)} - accessibilityHint={_(msg`Switches the account you are logged in to`)} - activeOpacity={0.8}> - {contents} - </TouchableOpacity> - ) -} - -type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'> -export function SettingsScreen(props: Props) { - return IS_INTERNAL ? ( - <NewSettingsScreen {...props} /> - ) : ( - <LegacySettingsScreen {...props} /> - ) -} - -function LegacySettingsScreen({}: Props) { - const queryClient = useQueryClient() - const pal = usePalette('default') - const {_} = useLingui() - const setMinimalShellMode = useSetMinimalShellMode() - const inAppBrowserPref = useInAppBrowser() - const setUseInAppBrowser = useSetInAppBrowser() - const onboardingDispatch = useOnboardingDispatch() - const navigation = useNavigation<NavigationProp>() - const {isMobile} = useWebMediaQueries() - const {openModal} = useModalControls() - const {accounts, currentAccount} = useSession() - const {mutate: clearPreferences} = useClearPreferencesMutation() - const {setShowLoggedOut} = useLoggedOutViewControls() - const {logoutEveryAccount} = useSessionApi() - const closeAllActiveElements = useCloseAllActiveElements() - const exportCarControl = useDialogControl() - const birthdayControl = useDialogControl() - const {pendingDid, onPressSwitchAccount} = useAccountSwitcher() - const isSwitchingAccounts = !!pendingDid - - // const primaryBg = useCustomPalette<ViewStyle>({ - // light: {backgroundColor: colors.blue0}, - // dark: {backgroundColor: colors.blue6}, - // }) - // const primaryText = useCustomPalette<TextStyle>({ - // light: {color: colors.blue3}, - // dark: {color: colors.blue2}, - // }) - - const dangerBg = useCustomPalette<ViewStyle>({ - light: {backgroundColor: colors.red1}, - dark: {backgroundColor: colors.red7}, - }) - const dangerText = useCustomPalette<TextStyle>({ - light: {color: colors.red4}, - dark: {color: colors.red2}, - }) - - useFocusEffect( - React.useCallback(() => { - setMinimalShellMode(false) - }, [setMinimalShellMode]), - ) - - const onPressAddAccount = React.useCallback(() => { - setShowLoggedOut(true) - closeAllActiveElements() - }, [setShowLoggedOut, closeAllActiveElements]) - - const onPressChangeHandle = React.useCallback(() => { - openModal({ - name: 'change-handle', - onChanged() { - if (currentAccount) { - // refresh my profile - queryClient.invalidateQueries({ - queryKey: RQKEY_PROFILE(currentAccount.did), - }) - } - }, - }) - }, [queryClient, openModal, currentAccount]) - - const onPressExportRepository = React.useCallback(() => { - exportCarControl.open() - }, [exportCarControl]) - - const onPressLanguageSettings = React.useCallback(() => { - navigation.navigate('LanguageSettings') - }, [navigation]) - - const onPressDeleteAccount = React.useCallback(() => { - openModal({name: 'delete-account'}) - }, [openModal]) - - const onPressLogoutEveryAccount = React.useCallback(() => { - logoutEveryAccount('Settings') - }, [logoutEveryAccount]) - - const onPressResetPreferences = React.useCallback(async () => { - clearPreferences() - }, [clearPreferences]) - - const onPressResetOnboarding = React.useCallback(async () => { - navigation.navigate('Home') - onboardingDispatch({type: 'start'}) - Toast.show(_(msg`Onboarding reset`)) - }, [navigation, onboardingDispatch, _]) - - const onPressBuildInfo = React.useCallback(() => { - setStringAsync( - `Build version: ${appVersion}; Bundle info: ${bundleInfo}; Bundle date: ${BUNDLE_DATE}; Platform: ${Platform.OS}`, - ) - Toast.show(_(msg`Copied build version to clipboard`)) - }, [_]) - - const openFollowingFeedPreferences = React.useCallback(() => { - navigation.navigate('PreferencesFollowingFeed') - }, [navigation]) - - const openThreadsPreferences = React.useCallback(() => { - navigation.navigate('PreferencesThreads') - }, [navigation]) - - const onPressAppPasswords = React.useCallback(() => { - navigation.navigate('AppPasswords') - }, [navigation]) - - const onPressSystemLog = React.useCallback(() => { - navigation.navigate('Log') - }, [navigation]) - - const onPressStorybook = React.useCallback(() => { - navigation.navigate('Debug') - }, [navigation]) - - const onPressDebugModeration = React.useCallback(() => { - navigation.navigate('DebugMod') - }, [navigation]) - - const onPressSavedFeeds = React.useCallback(() => { - navigation.navigate('SavedFeeds') - }, [navigation]) - - const onPressAccessibilitySettings = React.useCallback(() => { - navigation.navigate('AccessibilitySettings') - }, [navigation]) - - const onPressAppearanceSettings = React.useCallback(() => { - navigation.navigate('AppearanceSettings') - }, [navigation]) - - const onPressBirthday = React.useCallback(() => { - birthdayControl.open() - }, [birthdayControl]) - - const clearAllStorage = React.useCallback(async () => { - await clearStorage() - Toast.show(_(msg`Storage cleared, you need to restart the app now.`)) - }, [_]) - - const deactivateAccountControl = useDialogControl() - const onPressDeactivateAccount = React.useCallback(() => { - deactivateAccountControl.open() - }, [deactivateAccountControl]) - - const {mutate: onPressDeleteChatDeclaration} = useDeleteActorDeclaration() - - return ( - <Layout.Screen testID="settingsScreen"> - <ExportCarDialog control={exportCarControl} /> - <BirthDateSettingsDialog control={birthdayControl} /> - - <SimpleViewHeader - showBackButton={isMobile} - style={[ - pal.border, - {borderBottomWidth: StyleSheet.hairlineWidth}, - !isMobile && {borderLeftWidth: 1, borderRightWidth: 1}, - ]}> - <View style={{flex: 1}}> - <Text type="title-lg" style={[pal.text, {fontWeight: '600'}]}> - <Trans>Settings</Trans> - </Text> - </View> - </SimpleViewHeader> - <ScrollView - style={[isMobile && pal.viewLight]} - scrollIndicatorInsets={{right: 1}} - // @ts-ignore web only -prf - dataSet={{'stable-gutters': 1}}> - <View style={styles.spacer20} /> - {currentAccount ? ( - <> - <Text type="xl-bold" style={[pal.text, styles.heading]}> - <Trans>Account</Trans> - </Text> - <View style={[styles.infoLine]}> - <Text type="lg-medium" style={pal.text}> - <Trans>Email:</Trans>{' '} - </Text> - {currentAccount.emailConfirmed && ( - <> - <FontAwesomeIcon - icon="check" - size={10} - style={{color: colors.green3, marginRight: 2}} - /> - </> - )} - <Text - type="lg" - numberOfLines={1} - style={[ - pal.text, - {overflow: 'hidden', marginRight: 4, flex: 1}, - ]}> - {currentAccount.email || '(no email)'} - </Text> - <Link onPress={() => openModal({name: 'change-email'})}> - <Text type="lg" style={pal.link}> - <Trans context="action">Change</Trans> - </Text> - </Link> - </View> - <View style={[styles.infoLine]}> - <Text type="lg-medium" style={pal.text}> - <Trans>Birthday:</Trans>{' '} - </Text> - <Link onPress={onPressBirthday}> - <Text type="lg" style={pal.link}> - <Trans>Show</Trans> - </Text> - </Link> - </View> - <View style={styles.spacer20} /> - - {!currentAccount.emailConfirmed && <EmailConfirmationNotice />} - - <View style={[s.flexRow, styles.heading]}> - <Text type="xl-bold" style={pal.text} numberOfLines={1}> - <Trans>Signed in as</Trans> - </Text> - <View style={s.flex1} /> - </View> - <View pointerEvents={pendingDid ? 'none' : 'auto'}> - <SettingsAccountCard - account={currentAccount} - onPressSwitchAccount={onPressSwitchAccount} - pendingDid={pendingDid} - /> - </View> - </> - ) : null} - - <View pointerEvents={pendingDid ? 'none' : 'auto'}> - {accounts.length > 1 && ( - <View style={[s.flexRow, styles.heading, a.mt_sm]}> - <Text type="xl-bold" style={pal.text} numberOfLines={1}> - <Trans>Other accounts</Trans> - </Text> - <View style={s.flex1} /> - </View> - )} - - {accounts - .filter(a => a.did !== currentAccount?.did) - .map(account => ( - <SettingsAccountCard - key={account.did} - account={account} - onPressSwitchAccount={onPressSwitchAccount} - pendingDid={pendingDid} - /> - ))} - - <TouchableOpacity - testID="switchToNewAccountBtn" - style={[styles.linkCard, pal.view]} - onPress={isSwitchingAccounts ? undefined : onPressAddAccount} - accessibilityRole="button" - accessibilityLabel={_(msg`Add account`)} - accessibilityHint={_(msg`Create a new Bluesky account`)}> - <View style={[styles.iconContainer, pal.btn]}> - <FontAwesomeIcon - icon="plus" - style={pal.text as FontAwesomeIconStyle} - /> - </View> - <Text type="lg" style={pal.text}> - <Trans>Add account</Trans> - </Text> - </TouchableOpacity> - - <TouchableOpacity - style={[styles.linkCard, pal.view]} - onPress={ - isSwitchingAccounts ? undefined : onPressLogoutEveryAccount - } - accessibilityRole="button" - accessibilityLabel={_(msg`Sign out of all accounts`)} - accessibilityHint={undefined}> - <View style={[styles.iconContainer, pal.btn]}> - <FontAwesomeIcon - icon="arrow-right-from-bracket" - style={pal.text as FontAwesomeIconStyle} - /> - </View> - <Text type="lg" style={pal.text}> - {accounts.length > 1 ? ( - <Trans>Sign out of all accounts</Trans> - ) : ( - <Trans>Sign out</Trans> - )} - </Text> - </TouchableOpacity> - </View> - - <View style={styles.spacer20} /> - - <Text type="xl-bold" style={[pal.text, styles.heading]}> - <Trans>Basics</Trans> - </Text> - <TouchableOpacity - testID="accessibilitySettingsBtn" - style={[ - styles.linkCard, - pal.view, - isSwitchingAccounts && styles.dimmed, - ]} - onPress={ - isSwitchingAccounts ? undefined : onPressAccessibilitySettings - } - accessibilityRole="button" - accessibilityLabel={_(msg`Accessibility settings`)} - accessibilityHint={_(msg`Opens accessibility settings`)}> - <View style={[styles.iconContainer, pal.btn]}> - <FontAwesomeIcon - icon="universal-access" - style={pal.text as FontAwesomeIconStyle} - /> - </View> - <Text type="lg" style={pal.text}> - <Trans>Accessibility</Trans> - </Text> - </TouchableOpacity> - <TouchableOpacity - testID="appearanceSettingsBtn" - style={[ - styles.linkCard, - pal.view, - isSwitchingAccounts && styles.dimmed, - ]} - onPress={isSwitchingAccounts ? undefined : onPressAppearanceSettings} - accessibilityRole="button" - accessibilityLabel={_(msg`Appearance settings`)} - accessibilityHint={_(msg`Opens appearance settings`)}> - <View style={[styles.iconContainer, pal.btn]}> - <FontAwesomeIcon - icon="paint-roller" - style={pal.text as FontAwesomeIconStyle} - /> - </View> - <Text type="lg" style={pal.text}> - <Trans>Appearance</Trans> - </Text> - </TouchableOpacity> - <TouchableOpacity - testID="languageSettingsBtn" - style={[ - styles.linkCard, - pal.view, - isSwitchingAccounts && styles.dimmed, - ]} - onPress={isSwitchingAccounts ? undefined : onPressLanguageSettings} - accessibilityRole="button" - accessibilityLabel={_(msg`Language settings`)} - accessibilityHint={_(msg`Opens configurable language settings`)}> - <View style={[styles.iconContainer, pal.btn]}> - <FontAwesomeIcon - icon="language" - style={pal.text as FontAwesomeIconStyle} - /> - </View> - <Text type="lg" style={pal.text}> - <Trans>Languages</Trans> - </Text> - </TouchableOpacity> - <TouchableOpacity - testID="moderationBtn" - style={[ - styles.linkCard, - pal.view, - isSwitchingAccounts && styles.dimmed, - ]} - onPress={ - isSwitchingAccounts - ? undefined - : () => navigation.navigate('Moderation') - } - accessibilityRole="button" - accessibilityLabel={_(msg`Moderation settings`)} - accessibilityHint={_(msg`Opens moderation settings`)}> - <View style={[styles.iconContainer, pal.btn]}> - <HandIcon style={pal.text} size={18} strokeWidth={6} /> - </View> - <Text type="lg" style={pal.text}> - <Trans>Moderation</Trans> - </Text> - </TouchableOpacity> - <TouchableOpacity - testID="preferencesHomeFeedButton" - style={[ - styles.linkCard, - pal.view, - isSwitchingAccounts && styles.dimmed, - ]} - onPress={openFollowingFeedPreferences} - accessibilityRole="button" - accessibilityLabel={_(msg`Following feed preferences`)} - accessibilityHint={_(msg`Opens the Following feed preferences`)}> - <View style={[styles.iconContainer, pal.btn]}> - <FontAwesomeIcon - icon="sliders" - style={pal.text as FontAwesomeIconStyle} - /> - </View> - <Text type="lg" style={pal.text}> - <Trans>Following Feed Preferences</Trans> - </Text> - </TouchableOpacity> - <TouchableOpacity - testID="preferencesThreadsButton" - style={[ - styles.linkCard, - pal.view, - isSwitchingAccounts && styles.dimmed, - ]} - onPress={openThreadsPreferences} - accessibilityRole="button" - accessibilityLabel={_(msg`Thread preferences`)} - accessibilityHint={_(msg`Opens the threads preferences`)}> - <View style={[styles.iconContainer, pal.btn]}> - <FontAwesomeIcon - icon={['far', 'comments']} - style={pal.text as FontAwesomeIconStyle} - size={18} - /> - </View> - <Text type="lg" style={pal.text}> - <Trans>Thread Preferences</Trans> - </Text> - </TouchableOpacity> - <TouchableOpacity - testID="savedFeedsBtn" - style={[ - styles.linkCard, - pal.view, - isSwitchingAccounts && styles.dimmed, - ]} - onPress={onPressSavedFeeds} - accessibilityRole="button" - accessibilityLabel={_(msg`My saved feeds`)} - accessibilityHint={_(msg`Opens screen with all saved feeds`)}> - <View style={[styles.iconContainer, pal.btn]}> - <HashtagIcon style={pal.text} size={18} strokeWidth={3} /> - </View> - <Text type="lg" style={pal.text}> - <Trans>My Saved Feeds</Trans> - </Text> - </TouchableOpacity> - <TouchableOpacity - testID="linkToChatSettingsBtn" - style={[ - styles.linkCard, - pal.view, - isSwitchingAccounts && styles.dimmed, - ]} - onPress={ - isSwitchingAccounts - ? undefined - : () => navigation.navigate('MessagesSettings') - } - accessibilityRole="button" - accessibilityLabel={_(msg`Chat settings`)} - accessibilityHint={_(msg`Opens chat settings`)}> - <View style={[styles.iconContainer, pal.btn]}> - <FontAwesomeIcon - icon={['far', 'comment-dots']} - style={pal.text as FontAwesomeIconStyle} - /> - </View> - <Text type="lg" style={pal.text}> - <Trans>Chat Settings</Trans> - </Text> - </TouchableOpacity> - - <View style={styles.spacer20} /> - - <Text type="xl-bold" style={[pal.text, styles.heading]}> - <Trans>Privacy</Trans> - </Text> - - <TouchableOpacity - testID="externalEmbedsBtn" - style={[ - styles.linkCard, - pal.view, - isSwitchingAccounts && styles.dimmed, - ]} - onPress={ - isSwitchingAccounts - ? undefined - : () => navigation.navigate('PreferencesExternalEmbeds') - } - accessibilityRole="button" - accessibilityLabel={_(msg`External media settings`)} - accessibilityHint={_(msg`Opens external embeds settings`)}> - <View style={[styles.iconContainer, pal.btn]}> - <FontAwesomeIcon - icon={['far', 'circle-play']} - style={pal.text as FontAwesomeIconStyle} - /> - </View> - <Text type="lg" style={pal.text}> - <Trans>External Media Preferences</Trans> - </Text> - </TouchableOpacity> - - <View style={styles.spacer20} /> - - <Text type="xl-bold" style={[pal.text, styles.heading]}> - <Trans>Advanced</Trans> - </Text> - <TouchableOpacity - testID="appPasswordBtn" - style={[ - styles.linkCard, - pal.view, - isSwitchingAccounts && styles.dimmed, - ]} - onPress={onPressAppPasswords} - accessibilityRole="button" - accessibilityLabel={_(msg`App password settings`)} - accessibilityHint={_(msg`Opens the app password settings`)}> - <View style={[styles.iconContainer, pal.btn]}> - <FontAwesomeIcon - icon="lock" - style={pal.text as FontAwesomeIconStyle} - /> - </View> - <Text type="lg" style={pal.text}> - <Trans>App Passwords</Trans> - </Text> - </TouchableOpacity> - <TouchableOpacity - testID="changeHandleBtn" - style={[ - styles.linkCard, - pal.view, - isSwitchingAccounts && styles.dimmed, - ]} - onPress={isSwitchingAccounts ? undefined : onPressChangeHandle} - accessibilityRole="button" - accessibilityLabel={_(msg`Change handle`)} - accessibilityHint={_( - msg`Opens modal for choosing a new Bluesky handle`, - )}> - <View style={[styles.iconContainer, pal.btn]}> - <FontAwesomeIcon - icon="at" - style={pal.text as FontAwesomeIconStyle} - /> - </View> - <Text type="lg" style={pal.text} numberOfLines={1}> - <Trans>Change Handle</Trans> - </Text> - </TouchableOpacity> - {isNative && ( - <View style={[pal.view, styles.toggleCard]}> - <ToggleButton - type="default-light" - label={_(msg`Open links with in-app browser`)} - labelType="lg" - isSelected={inAppBrowserPref ?? false} - onPress={() => setUseInAppBrowser(!inAppBrowserPref)} - /> - </View> - )} - <View style={styles.spacer20} /> - <Text type="xl-bold" style={[pal.text, styles.heading]}> - <Trans>Two-factor authentication</Trans> - </Text> - <View style={[pal.view, styles.toggleCard]}> - <Email2FAToggle /> - </View> - <View style={styles.spacer20} /> - <Text type="xl-bold" style={[pal.text, styles.heading]}> - <Trans>Account</Trans> - </Text> - <TouchableOpacity - testID="changePasswordBtn" - style={[ - styles.linkCard, - pal.view, - isSwitchingAccounts && styles.dimmed, - ]} - onPress={() => openModal({name: 'change-password'})} - accessibilityRole="button" - accessibilityLabel={_(msg`Change password`)} - accessibilityHint={_( - msg`Opens modal for changing your Bluesky password`, - )}> - <View style={[styles.iconContainer, pal.btn]}> - <FontAwesomeIcon - icon="lock" - style={pal.text as FontAwesomeIconStyle} - /> - </View> - <Text type="lg" style={pal.text} numberOfLines={1}> - <Trans>Change Password</Trans> - </Text> - </TouchableOpacity> - <TouchableOpacity - testID="exportRepositoryBtn" - style={[ - styles.linkCard, - pal.view, - isSwitchingAccounts && styles.dimmed, - ]} - onPress={isSwitchingAccounts ? undefined : onPressExportRepository} - accessibilityRole="button" - accessibilityLabel={_(msg`Export my data`)} - accessibilityHint={_( - msg`Opens modal for downloading your Bluesky account data (repository)`, - )}> - <View style={[styles.iconContainer, pal.btn]}> - <FontAwesomeIcon - icon="download" - style={pal.text as FontAwesomeIconStyle} - /> - </View> - <Text type="lg" style={pal.text} numberOfLines={1}> - <Trans>Export My Data</Trans> - </Text> - </TouchableOpacity> - - <TouchableOpacity - style={[pal.view, styles.linkCard]} - onPress={onPressDeactivateAccount} - accessible={true} - accessibilityRole="button" - accessibilityLabel={_(msg`Deactivate account`)} - accessibilityHint={_( - msg`Opens modal for account deactivation confirmation`, - )}> - <View style={[styles.iconContainer, dangerBg]}> - <FontAwesomeIcon - icon={'users-slash'} - style={dangerText as FontAwesomeIconStyle} - size={18} - /> - </View> - <Text type="lg" style={dangerText}> - <Trans>Deactivate my account</Trans> - </Text> - </TouchableOpacity> - <DeactivateAccountDialog control={deactivateAccountControl} /> - - <TouchableOpacity - style={[pal.view, styles.linkCard]} - onPress={onPressDeleteAccount} - accessible={true} - accessibilityRole="button" - accessibilityLabel={_(msg`Delete account`)} - accessibilityHint={_( - msg`Opens modal for account deletion confirmation. Requires email code`, - )}> - <View style={[styles.iconContainer, dangerBg]}> - <FontAwesomeIcon - icon={['far', 'trash-can']} - style={dangerText as FontAwesomeIconStyle} - size={18} - /> - </View> - <Text type="lg" style={dangerText}> - <Trans>Delete My Account…</Trans> - </Text> - </TouchableOpacity> - <View style={styles.spacer20} /> - <TouchableOpacity - style={[pal.view, styles.linkCardNoIcon]} - onPress={onPressSystemLog} - accessibilityRole="button" - accessibilityLabel={_(msg`Open system log`)} - accessibilityHint={_(msg`Opens the system log page`)}> - <Text type="lg" style={pal.text}> - <Trans>System log</Trans> - </Text> - </TouchableOpacity> - {__DEV__ ? ( - <> - <TouchableOpacity - style={[pal.view, styles.linkCardNoIcon]} - onPress={onPressStorybook} - accessibilityRole="button" - accessibilityLabel={_(msg`Open storybook page`)} - accessibilityHint={_(msg`Opens the storybook page`)}> - <Text type="lg" style={pal.text}> - <Trans>Storybook</Trans> - </Text> - </TouchableOpacity> - <TouchableOpacity - style={[pal.view, styles.linkCardNoIcon]} - onPress={onPressDebugModeration} - accessibilityRole="button" - accessibilityLabel={_(msg`Open storybook page`)} - accessibilityHint={_(msg`Opens the storybook page`)}> - <Text type="lg" style={pal.text}> - <Trans>Debug Moderation</Trans> - </Text> - </TouchableOpacity> - <TouchableOpacity - style={[pal.view, styles.linkCardNoIcon]} - onPress={onPressResetPreferences} - accessibilityRole="button" - accessibilityLabel={_(msg`Reset preferences state`)} - accessibilityHint={_(msg`Resets the preferences state`)}> - <Text type="lg" style={pal.text}> - <Trans>Reset preferences state</Trans> - </Text> - </TouchableOpacity> - <TouchableOpacity - style={[pal.view, styles.linkCardNoIcon]} - onPress={() => onPressDeleteChatDeclaration()} - accessibilityRole="button" - accessibilityLabel={_(msg`Delete chat declaration record`)} - accessibilityHint={_(msg`Deletes the chat declaration record`)}> - <Text type="lg" style={pal.text}> - <Trans>Delete chat declaration record</Trans> - </Text> - </TouchableOpacity> - <TouchableOpacity - style={[pal.view, styles.linkCardNoIcon]} - onPress={onPressResetOnboarding} - accessibilityRole="button" - accessibilityLabel={_(msg`Reset onboarding state`)} - accessibilityHint={_(msg`Resets the onboarding state`)}> - <Text type="lg" style={pal.text}> - <Trans>Reset onboarding state</Trans> - </Text> - </TouchableOpacity> - <TouchableOpacity - style={[pal.view, styles.linkCardNoIcon]} - onPress={clearAllStorage} - accessibilityRole="button" - accessibilityLabel={_(msg`Clear all storage data`)} - accessibilityHint={_(msg`Clears all storage data`)}> - <Text type="lg" style={pal.text}> - <Trans>Clear all storage data (restart after this)</Trans> - </Text> - </TouchableOpacity> - </> - ) : null} - <View style={[styles.footer]}> - <TouchableOpacity - accessibilityRole="button" - onPress={onPressBuildInfo}> - <Text type="sm" style={[styles.buildInfo, pal.textLight]}> - <Trans> - Version {appVersion} {bundleInfo} - </Trans> - </Text> - </TouchableOpacity> - </View> - - <View - style={[ - {flexWrap: 'wrap', gap: 12, paddingHorizontal: 18}, - s.flexRow, - ]}> - <TextLink - type="md" - style={pal.link} - href="https://bsky.social/about/support/tos" - text={_(msg`Terms of Service`)} - /> - <TextLink - type="md" - style={pal.link} - href="https://bsky.social/about/support/privacy-policy" - text={_(msg`Privacy Policy`)} - /> - <TextLink - type="md" - style={pal.link} - href={STATUS_PAGE_URL} - text={_(msg`Status Page`)} - /> - </View> - <View style={s.footerSpacer} /> - </ScrollView> - </Layout.Screen> - ) -} - -function EmailConfirmationNotice() { - const pal = usePalette('default') - const palInverted = usePalette('inverted') - const {_} = useLingui() - const {isMobile} = useWebMediaQueries() - const verifyEmailDialogControl = useDialogControl() - - return ( - <View style={{marginBottom: 20}}> - <Text type="xl-bold" style={[pal.text, styles.heading]}> - <Trans>Verify email</Trans> - </Text> - <View - style={[ - { - paddingVertical: isMobile ? 12 : 0, - paddingHorizontal: 18, - }, - pal.view, - ]}> - <View style={{flexDirection: 'row', marginBottom: 8}}> - <Pressable - style={[ - palInverted.view, - { - flexDirection: 'row', - gap: 6, - borderRadius: 6, - paddingHorizontal: 12, - paddingVertical: 10, - alignItems: 'center', - }, - isMobile && {flex: 1}, - ]} - accessibilityRole="button" - accessibilityLabel={_(msg`Verify my email`)} - accessibilityHint={_(msg`Opens modal for email verification`)} - onPress={() => verifyEmailDialogControl.open()}> - <FontAwesomeIcon - icon="envelope" - color={palInverted.colors.text} - size={16} - /> - <Text type="button" style={palInverted.text}> - <Trans>Verify My Email</Trans> - </Text> - </Pressable> - </View> - <Text style={pal.textLight}> - <Trans>Protect your account by verifying your email.</Trans> - </Text> - </View> - <VerifyEmailDialog control={verifyEmailDialogControl} /> - </View> - ) -} - -const styles = StyleSheet.create({ - dimmed: { - opacity: 0.5, - }, - spacer20: { - height: 20, - }, - heading: { - paddingHorizontal: 18, - paddingBottom: 6, - }, - infoLine: { - flexDirection: 'row', - alignItems: 'center', - paddingHorizontal: 18, - paddingBottom: 6, - }, - profile: { - flexDirection: 'row', - marginVertical: 6, - borderRadius: 4, - paddingVertical: 10, - paddingHorizontal: 10, - }, - linkCard: { - flexDirection: 'row', - alignItems: 'center', - paddingVertical: 12, - paddingHorizontal: 18, - marginBottom: 1, - }, - linkCardNoIcon: { - flexDirection: 'row', - alignItems: 'center', - paddingVertical: 20, - paddingHorizontal: 18, - marginBottom: 1, - }, - toggleCard: { - paddingVertical: 8, - paddingHorizontal: 6, - marginBottom: 1, - }, - avi: { - marginRight: 12, - }, - iconContainer: { - alignItems: 'center', - justifyContent: 'center', - width: 40, - height: 40, - borderRadius: 30, - marginRight: 12, - }, - buildInfo: { - paddingVertical: 8, - }, - - colorModeText: { - marginLeft: 10, - marginBottom: 6, - }, - - selectableBtns: { - flexDirection: 'row', - }, - - btn: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - width: '100%', - borderRadius: 32, - padding: 14, - backgroundColor: colors.gray1, - }, - toggleBtn: { - paddingHorizontal: 0, - }, - footer: { - flex: 1, - flexDirection: 'row', - paddingLeft: 18, - }, -}) |