diff options
Diffstat (limited to 'src/view/screens/Settings')
-rw-r--r-- | src/view/screens/Settings/ExportCarDialog.tsx | 103 | ||||
-rw-r--r-- | src/view/screens/Settings/index.tsx | 1032 |
2 files changed, 1135 insertions, 0 deletions
diff --git a/src/view/screens/Settings/ExportCarDialog.tsx b/src/view/screens/Settings/ExportCarDialog.tsx new file mode 100644 index 000000000..720cd4f09 --- /dev/null +++ b/src/view/screens/Settings/ExportCarDialog.tsx @@ -0,0 +1,103 @@ +import React from 'react' +import {View} from 'react-native' +import {useLingui} from '@lingui/react' +import {Trans, msg} from '@lingui/macro' + +import {atoms as a, useBreakpoints, useTheme} from '#/alf' +import * as Dialog from '#/components/Dialog' +import {Text, P} from '#/components/Typography' +import {Button, ButtonText} from '#/components/Button' +import {InlineLink, Link} from '#/components/Link' +import {getAgent, useSession} from '#/state/session' + +export function ExportCarDialog({ + control, +}: { + control: Dialog.DialogOuterProps['control'] +}) { + const {_} = useLingui() + const t = useTheme() + const {gtMobile} = useBreakpoints() + const {currentAccount} = useSession() + + const downloadUrl = React.useMemo(() => { + const agent = getAgent() + if (!currentAccount || !agent.session) { + return '' // shouldnt ever happen + } + // eg: https://bsky.social/xrpc/com.atproto.sync.getRepo?did=did:plc:ewvi7nxzyoun6zhxrhs64oiz + const url = new URL(agent.pdsUrl || agent.service) + url.pathname = '/xrpc/com.atproto.sync.getRepo' + url.searchParams.set('did', agent.session.did) + return url.toString() + }, [currentAccount]) + + 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]}> + <Trans>Export My Data</Trans> + </Text> + <P 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> + </P> + + <Link + variant="solid" + color="primary" + size="large" + label={_(msg`Download CAR file`)} + to={downloadUrl} + download="repo.car"> + <ButtonText> + <Trans>Download CAR file</Trans> + </ButtonText> + </Link> + + <P + style={[ + a.py_xs, + 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{' '} + <InlineLink + to="https://atproto.com/blog/repo-export" + style={[a.text_sm]}> + this blogpost. + </InlineLink> + </Trans> + </P> + + <View style={gtMobile && [a.flex_row, a.justify_end]}> + <Button + testID="doneBtn" + variant="outline" + color="primary" + size={gtMobile ? 'small' : 'large'} + onPress={() => control.close()} + label={_(msg`Done`)}> + {_(msg`Done`)} + </Button> + </View> + + {!gtMobile && <View style={{height: 40}} />} + </View> + </Dialog.ScrollableInner> + </Dialog.Outer> + ) +} diff --git a/src/view/screens/Settings/index.tsx b/src/view/screens/Settings/index.tsx new file mode 100644 index 000000000..458952527 --- /dev/null +++ b/src/view/screens/Settings/index.tsx @@ -0,0 +1,1032 @@ +import React from 'react' +import { + ActivityIndicator, + Linking, + Platform, + StyleSheet, + Pressable, + TextStyle, + TouchableOpacity, + View, + ViewStyle, +} from 'react-native' +import {useFocusEffect, useNavigation} from '@react-navigation/native' +import { + FontAwesomeIcon, + FontAwesomeIconStyle, +} from '@fortawesome/react-native-fontawesome' +import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' +import * as AppInfo from 'lib/app-info' +import {usePalette} from 'lib/hooks/usePalette' +import {useCustomPalette} from 'lib/hooks/useCustomPalette' +import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' +import {useAccountSwitcher} from 'lib/hooks/useAccountSwitcher' +import {useAnalytics} from 'lib/analytics/analytics' +import {NavigationProp} from 'lib/routes/types' +import {HandIcon, HashtagIcon} from 'lib/icons' +import Clipboard from '@react-native-clipboard/clipboard' +import {makeProfileLink} from 'lib/routes/links' +import {RQKEY as RQKEY_PROFILE} from '#/state/queries/profile' +import {useModalControls} from '#/state/modals' +import { + useSetMinimalShellMode, + useThemePrefs, + useSetThemePrefs, + useOnboardingDispatch, +} from '#/state/shell' +import { + useRequireAltTextEnabled, + useSetRequireAltTextEnabled, +} from '#/state/preferences' +import {useSession, useSessionApi, SessionAccount} from '#/state/session' +import {useProfileQuery} from '#/state/queries/profile' +import {useClearPreferencesMutation} from '#/state/queries/preferences' +import {useInviteCodesQuery} from '#/state/queries/invites' +import {clear as clearStorage} from '#/state/persisted/store' +import {clearLegacyStorage} from '#/state/persisted/legacy' +import {STATUS_PAGE_URL} from 'lib/constants' +import {Trans, msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useQueryClient} from '@tanstack/react-query' +import {useLoggedOutViewControls} from '#/state/shell/logged-out' +import {useCloseAllActiveElements} from '#/state/util' +import { + useInAppBrowser, + useSetInAppBrowser, +} from '#/state/preferences/in-app-browser' +import {isNative} from '#/platform/detection' +import {useDialogControl} from '#/components/Dialog' + +import {s, colors} from 'lib/styles' +import {ScrollView} from 'view/com/util/Views' +import {Link, TextLink} from 'view/com/util/Link' +import {Text} from 'view/com/util/text/Text' +import * as Toast from 'view/com/util/Toast' +import {UserAvatar} from 'view/com/util/UserAvatar' +import {ToggleButton} from 'view/com/util/forms/ToggleButton' +import {SelectableBtn} from 'view/com/util/forms/SelectableBtn' +import {AccountDropdownBtn} from 'view/com/util/AccountDropdownBtn' +import {SimpleViewHeader} from 'view/com/util/SimpleViewHeader' +import {ExportCarDialog} from './ExportCarDialog' + +function SettingsAccountCard({account}: {account: SessionAccount}) { + const pal = usePalette('default') + const {_} = useLingui() + const {isSwitchingAccounts, currentAccount} = useSession() + const {logout} = useSessionApi() + const {data: profile} = useProfileQuery({did: account.did}) + const isCurrentAccount = account.did === currentAccount?.did + const {onPressSwitchAccount} = useAccountSwitcher() + + const contents = ( + <View style={[pal.view, styles.linkCard]}> + <View style={styles.avi}> + <UserAvatar size={40} avatar={profile?.avatar} /> + </View> + <View style={[s.flex1]}> + <Text type="md-bold" style={pal.text}> + {profile?.displayName || account.handle} + </Text> + <Text type="sm" style={pal.textLight}> + {account.handle} + </Text> + </View> + + {isCurrentAccount ? ( + <TouchableOpacity + testID="signOutBtn" + onPress={logout} + accessibilityRole="button" + accessibilityLabel={_(msg`Sign out`)} + accessibilityHint={`Signs ${profile?.displayName} out of Bluesky`}> + <Text type="lg" style={pal.link}> + <Trans>Sign out</Trans> + </Text> + </TouchableOpacity> + ) : ( + <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={ + isSwitchingAccounts ? undefined : () => onPressSwitchAccount(account) + } + accessibilityRole="button" + accessibilityLabel={_(msg`Switch to ${account.handle}`)} + accessibilityHint={_(msg`Switches the account you are logged in to`)}> + {contents} + </TouchableOpacity> + ) +} + +type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'> +export function SettingsScreen({}: Props) { + const queryClient = useQueryClient() + const {colorMode, darkTheme} = useThemePrefs() + const {setColorMode, setDarkTheme} = useSetThemePrefs() + const pal = usePalette('default') + const {_} = useLingui() + const setMinimalShellMode = useSetMinimalShellMode() + const requireAltTextEnabled = useRequireAltTextEnabled() + const setRequireAltTextEnabled = useSetRequireAltTextEnabled() + const inAppBrowserPref = useInAppBrowser() + const setUseInAppBrowser = useSetInAppBrowser() + const onboardingDispatch = useOnboardingDispatch() + const navigation = useNavigation<NavigationProp>() + const {isMobile} = useWebMediaQueries() + const {screen, track} = useAnalytics() + const {openModal} = useModalControls() + const {isSwitchingAccounts, accounts, currentAccount} = useSession() + const {mutate: clearPreferences} = useClearPreferencesMutation() + const {data: invites} = useInviteCodesQuery() + const invitesAvailable = invites?.available?.length ?? 0 + const {setShowLoggedOut} = useLoggedOutViewControls() + const closeAllActiveElements = useCloseAllActiveElements() + const exportCarControl = useDialogControl() + + 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(() => { + screen('Settings') + setMinimalShellMode(false) + }, [screen, setMinimalShellMode]), + ) + + const onPressAddAccount = React.useCallback(() => { + track('Settings:AddAccountButtonClicked') + setShowLoggedOut(true) + closeAllActiveElements() + }, [track, setShowLoggedOut, closeAllActiveElements]) + + const onPressChangeHandle = React.useCallback(() => { + track('Settings:ChangeHandleButtonClicked') + openModal({ + name: 'change-handle', + onChanged() { + if (currentAccount) { + // refresh my profile + queryClient.invalidateQueries({ + queryKey: RQKEY_PROFILE(currentAccount.did), + }) + } + }, + }) + }, [track, queryClient, openModal, currentAccount]) + + const onPressExportRepository = React.useCallback(() => { + exportCarControl.open() + }, [exportCarControl]) + + const onPressInviteCodes = React.useCallback(() => { + track('Settings:InvitecodesButtonClicked') + openModal({name: 'invite-codes'}) + }, [track, openModal]) + + const onPressLanguageSettings = React.useCallback(() => { + navigation.navigate('LanguageSettings') + }, [navigation]) + + const onPressDeleteAccount = React.useCallback(() => { + openModal({name: 'delete-account'}) + }, [openModal]) + + const onPressResetPreferences = React.useCallback(async () => { + clearPreferences() + }, [clearPreferences]) + + const onPressResetOnboarding = React.useCallback(async () => { + onboardingDispatch({type: 'start'}) + Toast.show(_(msg`Onboarding reset`)) + }, [onboardingDispatch, _]) + + const onPressBuildInfo = React.useCallback(() => { + Clipboard.setString( + `Build version: ${AppInfo.appVersion}; Platform: ${Platform.OS}`, + ) + Toast.show(_(msg`Copied build version to clipboard`)) + }, [_]) + + const openHomeFeedPreferences = React.useCallback(() => { + navigation.navigate('PreferencesHomeFeed') + }, [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 onPressSavedFeeds = React.useCallback(() => { + navigation.navigate('SavedFeeds') + }, [navigation]) + + const onPressStatusPage = React.useCallback(() => { + Linking.openURL(STATUS_PAGE_URL) + }, []) + + const clearAllStorage = React.useCallback(async () => { + await clearStorage() + Toast.show(_(msg`Storage cleared, you need to restart the app now.`)) + }, [_]) + const clearAllLegacyStorage = React.useCallback(async () => { + await clearLegacyStorage() + Toast.show(_(msg`Legacy storage cleared, you need to restart the app now.`)) + }, [_]) + + return ( + <View style={s.hContentRegion} testID="settingsScreen"> + <ExportCarDialog control={exportCarControl} /> + + <SimpleViewHeader + showBackButton={isMobile} + style={[ + pal.border, + {borderBottomWidth: 1}, + !isMobile && {borderLeftWidth: 1, borderRightWidth: 1}, + ]}> + <View style={{flex: 1}}> + <Text type="title-lg" style={[pal.text, {fontWeight: 'bold'}]}> + <Trans>Settings</Trans> + </Text> + </View> + </SimpleViewHeader> + <ScrollView + style={[s.hContentRegion]} + contentContainerStyle={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={() => openModal({name: 'birth-date-settings'})}> + <Text type="lg" style={pal.link}> + <Trans>Show</Trans> + </Text> + </Link> + </View> + <View style={styles.spacer20} /> + + {!currentAccount.emailConfirmed && <EmailConfirmationNotice />} + </> + ) : null} + <View style={[s.flexRow, styles.heading]}> + <Text type="xl-bold" style={pal.text}> + <Trans>Signed in as</Trans> + </Text> + <View style={s.flex1} /> + </View> + + {isSwitchingAccounts ? ( + <View style={[pal.view, styles.linkCard]}> + <ActivityIndicator /> + </View> + ) : ( + <SettingsAccountCard account={currentAccount!} /> + )} + + {accounts + .filter(a => a.did !== currentAccount?.did) + .map(account => ( + <SettingsAccountCard key={account.did} account={account} /> + ))} + + <TouchableOpacity + testID="switchToNewAccountBtn" + style={[ + styles.linkCard, + pal.view, + isSwitchingAccounts && styles.dimmed, + ]} + 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> + + <View style={styles.spacer20} /> + + <Text type="xl-bold" style={[pal.text, styles.heading]}> + <Trans>Invite a Friend</Trans> + </Text> + + <TouchableOpacity + testID="inviteFriendBtn" + style={[ + styles.linkCard, + pal.view, + isSwitchingAccounts && styles.dimmed, + ]} + onPress={isSwitchingAccounts ? undefined : onPressInviteCodes} + accessibilityRole="button" + accessibilityLabel={_(msg`Invite`)} + accessibilityHint={_(msg`Opens invite code list`)} + disabled={invites?.disabled}> + <View + style={[ + styles.iconContainer, + invitesAvailable > 0 ? primaryBg : pal.btn, + ]}> + <FontAwesomeIcon + icon="ticket" + style={ + (invitesAvailable > 0 + ? primaryText + : pal.text) as FontAwesomeIconStyle + } + /> + </View> + <Text type="lg" style={invitesAvailable > 0 ? pal.link : pal.text}> + {invites?.disabled ? ( + <Trans> + Your invite codes are hidden when logged in using an App + Password + </Trans> + ) : invitesAvailable === 1 ? ( + <Trans>{invitesAvailable} invite code available</Trans> + ) : ( + <Trans>{invitesAvailable} invite codes available</Trans> + )} + </Text> + </TouchableOpacity> + + <View style={styles.spacer20} /> + + <Text type="xl-bold" style={[pal.text, styles.heading]}> + <Trans>Accessibility</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)} + /> + </View> + + <View style={styles.spacer20} /> + + <Text type="xl-bold" style={[pal.text, styles.heading]}> + <Trans>Appearance</Trans> + </Text> + <View> + <View style={[styles.linkCard, pal.view, styles.selectableBtns]}> + <SelectableBtn + selected={colorMode === 'system'} + label={_(msg`System`)} + left + onSelect={() => setColorMode('system')} + accessibilityHint={_(msg`Set color theme to system setting`)} + /> + <SelectableBtn + selected={colorMode === 'light'} + label={_(msg`Light`)} + onSelect={() => setColorMode('light')} + accessibilityHint={_(msg`Set color theme to light`)} + /> + <SelectableBtn + selected={colorMode === 'dark'} + label={_(msg`Dark`)} + right + onSelect={() => setColorMode('dark')} + accessibilityHint={_(msg`Set color theme to dark`)} + /> + </View> + </View> + + <View style={styles.spacer20} /> + + {colorMode !== 'light' && ( + <> + <Text type="xl-bold" style={[pal.text, styles.heading]}> + <Trans>Dark Theme</Trans> + </Text> + <View> + <View style={[styles.linkCard, pal.view, styles.selectableBtns]}> + <SelectableBtn + selected={!darkTheme || darkTheme === 'dim'} + label={_(msg`Dim`)} + left + onSelect={() => setDarkTheme('dim')} + accessibilityHint={_(msg`Set dark theme to the dim theme`)} + /> + <SelectableBtn + selected={darkTheme === 'dark'} + label={_(msg`Dark`)} + right + onSelect={() => setDarkTheme('dark')} + accessibilityHint={_(msg`Set dark theme to the dark theme`)} + /> + </View> + </View> + <View style={styles.spacer20} /> + </> + )} + + <Text type="xl-bold" style={[pal.text, styles.heading]}> + <Trans>Basics</Trans> + </Text> + <TouchableOpacity + testID="preferencesHomeFeedButton" + style={[ + styles.linkCard, + pal.view, + isSwitchingAccounts && styles.dimmed, + ]} + onPress={openHomeFeedPreferences} + accessibilityRole="button" + accessibilityHint="" + accessibilityLabel={_(msg`Opens the home feed preferences`)}> + <View style={[styles.iconContainer, pal.btn]}> + <FontAwesomeIcon + icon="sliders" + style={pal.text as FontAwesomeIconStyle} + /> + </View> + <Text type="lg" style={pal.text}> + <Trans>Home Feed Preferences</Trans> + </Text> + </TouchableOpacity> + <TouchableOpacity + testID="preferencesThreadsButton" + style={[ + styles.linkCard, + pal.view, + isSwitchingAccounts && styles.dimmed, + ]} + onPress={openThreadsPreferences} + accessibilityRole="button" + accessibilityHint="" + accessibilityLabel={_(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, + ]} + accessibilityHint="My Saved Feeds" + accessibilityLabel={_(msg`Opens screen with all saved feeds`)} + onPress={onPressSavedFeeds}> + <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="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> + + <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 page`)}> + <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`Choose a new Bluesky username or create`)}> + <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>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`Change 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`Download 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={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" + accessibilityHint="Open system log" + accessibilityLabel={_(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={onPressResetPreferences} + accessibilityRole="button" + accessibilityLabel={_(msg`Reset preferences`)} + 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={onPressResetOnboarding} + accessibilityRole="button" + accessibilityLabel={_(msg`Reset onboarding`)} + 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={clearAllLegacyStorage} + accessibilityRole="button" + accessibilityLabel={_(msg`Clear all legacy storage data`)} + accessibilityHint={_(msg`Clear all legacy storage data`)}> + <Text type="lg" style={pal.text}> + <Trans> + Clear all legacy storage data (restart after this) + </Trans> + </Text> + </TouchableOpacity> + <TouchableOpacity + style={[pal.view, styles.linkCardNoIcon]} + onPress={clearAllStorage} + accessibilityRole="button" + accessibilityLabel={_(msg`Clear all storage data`)} + accessibilityHint={_(msg`Clear 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> + Build version {AppInfo.appVersion} {AppInfo.updateChannel} + </Trans> + </Text> + </TouchableOpacity> + <Text type="sm" style={[pal.textLight]}> + · + </Text> + <TouchableOpacity + accessibilityRole="button" + onPress={onPressStatusPage}> + <Text type="sm" style={[styles.buildInfo, pal.textLight]}> + <Trans>Status page</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`)} + /> + </View> + <View style={s.footerSpacer} /> + </ScrollView> + </View> + ) +} + +function EmailConfirmationNotice() { + const pal = usePalette('default') + const palInverted = usePalette('inverted') + const {_} = useLingui() + const {isMobile} = useWebMediaQueries() + const {openModal} = useModalControls() + + 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="" + onPress={() => openModal({name: 'verify-email'})}> + <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> + </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', + alignItems: 'center', + paddingLeft: 18, + }, +}) |