From d7a3246fe3bd95adfcc43762e0276b375dce026a Mon Sep 17 00:00:00 2001 From: bnewbold Date: Mon, 12 Feb 2024 15:22:03 -0800 Subject: basic export repository link in settings (#2641) * basic export repository link in settings Absolutely no prior React experience, and limited TypeScript, so probably doing all kinds of things wrong! I tried to make it a download button instead of link but that didn't work. There is probably a safer way to construct the URL string. I think having the download open in the browser is reasonable, as opposed to an in-app save flow in mobile. But i'm not sure. * Remove appview proxy toggle * Move Settings screen to a subfolder * Add support for the download attribute on links in web * Rewrite ExportRepository modal using ALF * Mobile ui tweaks --------- Co-authored-by: Paul Frazee --- src/components/Link.tsx | 13 +- src/lib/api/debug-appview-proxy-header.ts | 60 -- src/view/icons/index.tsx | 2 + src/view/screens/Settings.tsx | 1023 ------------------------ src/view/screens/Settings/ExportCarDialog.tsx | 103 +++ src/view/screens/Settings/index.tsx | 1032 +++++++++++++++++++++++++ 6 files changed, 1149 insertions(+), 1084 deletions(-) delete mode 100644 src/lib/api/debug-appview-proxy-header.ts delete mode 100644 src/view/screens/Settings.tsx create mode 100644 src/view/screens/Settings/ExportCarDialog.tsx create mode 100644 src/view/screens/Settings/index.tsx (limited to 'src') diff --git a/src/components/Link.tsx b/src/components/Link.tsx index 63b0c73f1..763f07ca9 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -148,6 +148,10 @@ export type LinkProps = Omit & * Label for a11y. Defaults to the href. */ label?: string + /** + * Web-only attribute. Sets `download` attr on web. + */ + download?: string } /** @@ -158,7 +162,13 @@ export type LinkProps = Omit & * Intended to behave as a web anchor tag. For more complex routing, use a * `Button`. */ -export function Link({children, to, action = 'push', ...rest}: LinkProps) { +export function Link({ + children, + to, + action = 'push', + download, + ...rest +}: LinkProps) { const {href, isExternal, onPress} = useLink({ to, displayText: typeof children === 'string' ? children : '', @@ -177,6 +187,7 @@ export function Link({children, to, action = 'push', ...rest}: LinkProps) { hrefAttrs: { target: isExternal ? 'blank' : undefined, rel: isExternal ? 'noopener noreferrer' : undefined, + download, }, dataSet: { // default to no underline, apply this ourselves diff --git a/src/lib/api/debug-appview-proxy-header.ts b/src/lib/api/debug-appview-proxy-header.ts deleted file mode 100644 index 44363cde2..000000000 --- a/src/lib/api/debug-appview-proxy-header.ts +++ /dev/null @@ -1,60 +0,0 @@ -/** - * APP-700 - * - * This is a temporary debug setting we're running on the Web build to - * help the protocol team test some changes. - * - * It should be removed in ~2 weeks. It should only be used on the Web - * version of the app. - */ - -import {useState, useCallback, useEffect} from 'react' -import {BskyAgent} from '@atproto/api' -import * as Storage from 'lib/storage' - -export function useDebugHeaderSetting(agent: BskyAgent): [boolean, () => void] { - const [enabled, setEnabled] = useState(false) - - useEffect(() => { - async function check() { - if (await isEnabled()) { - setEnabled(true) - } - } - check() - }, []) - - const toggle = useCallback(() => { - if (!enabled) { - Storage.saveString('set-header-x-appview-proxy', 'yes') - agent.api.xrpc.setHeader('x-appview-proxy', 'true') - setEnabled(true) - } else { - Storage.remove('set-header-x-appview-proxy') - agent.api.xrpc.unsetHeader('x-appview-proxy') - setEnabled(false) - } - }, [setEnabled, enabled, agent]) - - return [enabled, toggle] -} - -export function setDebugHeader(agent: BskyAgent, enabled: boolean) { - if (enabled) { - Storage.saveString('set-header-x-appview-proxy', 'yes') - agent.api.xrpc.setHeader('x-appview-proxy', 'true') - } else { - Storage.remove('set-header-x-appview-proxy') - agent.api.xrpc.unsetHeader('x-appview-proxy') - } -} - -export async function applyDebugHeader(agent: BskyAgent) { - if (await isEnabled()) { - agent.api.xrpc.setHeader('x-appview-proxy', 'true') - } -} - -async function isEnabled() { - return (await Storage.loadString('set-header-x-appview-proxy')) === 'yes' -} diff --git a/src/view/icons/index.tsx b/src/view/icons/index.tsx index be139d2f2..b7bbf1600 100644 --- a/src/view/icons/index.tsx +++ b/src/view/icons/index.tsx @@ -39,6 +39,7 @@ import {faComment} from '@fortawesome/free-regular-svg-icons/faComment' import {faCommentSlash} from '@fortawesome/free-solid-svg-icons/faCommentSlash' import {faComments} from '@fortawesome/free-regular-svg-icons/faComments' import {faCompass} from '@fortawesome/free-regular-svg-icons/faCompass' +import {faDownload} from '@fortawesome/free-solid-svg-icons/faDownload' import {faEllipsis} from '@fortawesome/free-solid-svg-icons/faEllipsis' import {faEnvelope} from '@fortawesome/free-solid-svg-icons/faEnvelope' import {faExclamation} from '@fortawesome/free-solid-svg-icons/faExclamation' @@ -143,6 +144,7 @@ library.add( faCommentSlash, faComments, faCompass, + faDownload, faEllipsis, faEnvelope, faEye, diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx deleted file mode 100644 index d5531108d..000000000 --- a/src/view/screens/Settings.tsx +++ /dev/null @@ -1,1023 +0,0 @@ -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 {s, colors} from 'lib/styles' -import {ScrollView} from '../com/util/Views' -import {Link, TextLink} from '../com/util/Link' -import {Text} from '../com/util/text/Text' -import * as Toast from '../com/util/Toast' -import {UserAvatar} from '../com/util/UserAvatar' -import {ToggleButton} from 'view/com/util/forms/ToggleButton' -import {SelectableBtn} from 'view/com/util/forms/SelectableBtn' -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 {AccountDropdownBtn} from 'view/com/util/AccountDropdownBtn' -import {SimpleViewHeader} from 'view/com/util/SimpleViewHeader' -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, - getAgent, -} 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' - -// TEMPORARY (APP-700) -// remove after backend testing finishes -// -prf -import {useDebugHeaderSetting} from 'lib/api/debug-appview-proxy-header' -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' - -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 = ( - - - - - - - {profile?.displayName || account.handle} - - - {account.handle} - - - - {isCurrentAccount ? ( - - - Sign out - - - ) : ( - - )} - - ) - - return isCurrentAccount ? ( - - {contents} - - ) : ( - onPressSwitchAccount(account) - } - accessibilityRole="button" - accessibilityLabel={_(msg`Switch to ${account.handle}`)} - accessibilityHint={_(msg`Switches the account you are logged in to`)}> - {contents} - - ) -} - -type Props = NativeStackScreenProps -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() - const {isMobile} = useWebMediaQueries() - const {screen, track} = useAnalytics() - const {openModal} = useModalControls() - const {isSwitchingAccounts, accounts, currentAccount} = useSession() - const [debugHeaderEnabled, toggleDebugHeader] = useDebugHeaderSetting( - getAgent(), - ) - const {mutate: clearPreferences} = useClearPreferencesMutation() - const {data: invites} = useInviteCodesQuery() - const invitesAvailable = invites?.available?.length ?? 0 - const {setShowLoggedOut} = useLoggedOutViewControls() - const closeAllActiveElements = useCloseAllActiveElements() - - const primaryBg = useCustomPalette({ - light: {backgroundColor: colors.blue0}, - dark: {backgroundColor: colors.blue6}, - }) - const primaryText = useCustomPalette({ - light: {color: colors.blue3}, - dark: {color: colors.blue2}, - }) - - const dangerBg = useCustomPalette({ - light: {backgroundColor: colors.red1}, - dark: {backgroundColor: colors.red7}, - }) - const dangerText = useCustomPalette({ - 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 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 ( - - - - - Settings - - - - - - {currentAccount ? ( - <> - - Account - - - - Email:{' '} - - {currentAccount.emailConfirmed && ( - <> - - - )} - - {currentAccount.email || '(no email)'} - - openModal({name: 'change-email'})}> - - Change - - - - - - Birthday:{' '} - - openModal({name: 'birth-date-settings'})}> - - Show - - - - - - {!currentAccount.emailConfirmed && } - - ) : null} - - - Signed in as - - - - - {isSwitchingAccounts ? ( - - - - ) : ( - - )} - - {accounts - .filter(a => a.did !== currentAccount?.did) - .map(account => ( - - ))} - - - - - - - Add account - - - - - - - Invite a Friend - - - - 0 ? primaryBg : pal.btn, - ]}> - 0 - ? primaryText - : pal.text) as FontAwesomeIconStyle - } - /> - - 0 ? pal.link : pal.text}> - {invites?.disabled ? ( - - Your invite codes are hidden when logged in using an App - Password - - ) : invitesAvailable === 1 ? ( - {invitesAvailable} invite code available - ) : ( - {invitesAvailable} invite codes available - )} - - - - - - - Accessibility - - - setRequireAltTextEnabled(!requireAltTextEnabled)} - /> - - - - - - Appearance - - - - setColorMode('system')} - accessibilityHint={_(msg`Set color theme to system setting`)} - /> - setColorMode('light')} - accessibilityHint={_(msg`Set color theme to light`)} - /> - setColorMode('dark')} - accessibilityHint={_(msg`Set color theme to dark`)} - /> - - - - - - {colorMode !== 'light' && ( - <> - - Dark Theme - - - - setDarkTheme('dim')} - accessibilityHint={_(msg`Set dark theme to the dim theme`)} - /> - setDarkTheme('dark')} - accessibilityHint={_(msg`Set dark theme to the dark theme`)} - /> - - - - - )} - - - Basics - - - - - - - Home Feed Preferences - - - - - - - - Thread Preferences - - - - - - - - My Saved Feeds - - - - - - - - Languages - - - navigation.navigate('Moderation') - } - accessibilityRole="button" - accessibilityLabel={_(msg`Moderation settings`)} - accessibilityHint={_(msg`Opens moderation settings`)}> - - - - - Moderation - - - - - - - Privacy - - - navigation.navigate('PreferencesExternalEmbeds') - } - accessibilityRole="button" - accessibilityLabel={_(msg`External media settings`)} - accessibilityHint={_(msg`Opens external embeds settings`)}> - - - - - External Media Preferences - - - - - - - Advanced - - - - - - - App Passwords - - - - - - - - Change Handle - - - {isNative && ( - - setUseInAppBrowser(!inAppBrowserPref)} - /> - - )} - - - Account - - openModal({name: 'change-password'})} - accessibilityRole="button" - accessibilityLabel={_(msg`Change password`)} - accessibilityHint={_(msg`Change your Bluesky password`)}> - - - - - Change Password - - - - - - - - Delete My Account… - - - - - Developer Tools - - - - System log - - - {__DEV__ ? ( - - ) : null} - {__DEV__ ? ( - <> - - - Storybook - - - - - Reset preferences state - - - - - Reset onboarding state - - - - - - Clear all legacy storage data (restart after this) - - - - - - Clear all storage data (restart after this) - - - - ) : null} - - - - - Build version {AppInfo.appVersion} {AppInfo.updateChannel} - - - - -   ·   - - - - Status page - - - - - - - - - - - - ) -} - -function EmailConfirmationNotice() { - const pal = usePalette('default') - const palInverted = usePalette('inverted') - const {_} = useLingui() - const {isMobile} = useWebMediaQueries() - const {openModal} = useModalControls() - - return ( - - - Verify email - - - - openModal({name: 'verify-email'})}> - - - Verify My Email - - - - - Protect your account by verifying your email. - - - - ) -} - -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, - }, -}) 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 ( + + + + + + + Export My Data + +

+ + 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. + +

+ + + + Download CAR file + + + +

+ + This feature is in beta. You can read more about repository + exports in{' '} + + this blogpost. + + +

+ + + + + + {!gtMobile && } + +
+
+ ) +} 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 = ( + + + + + + + {profile?.displayName || account.handle} + + + {account.handle} + + + + {isCurrentAccount ? ( + + + Sign out + + + ) : ( + + )} + + ) + + return isCurrentAccount ? ( + + {contents} + + ) : ( + onPressSwitchAccount(account) + } + accessibilityRole="button" + accessibilityLabel={_(msg`Switch to ${account.handle}`)} + accessibilityHint={_(msg`Switches the account you are logged in to`)}> + {contents} + + ) +} + +type Props = NativeStackScreenProps +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() + 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({ + light: {backgroundColor: colors.blue0}, + dark: {backgroundColor: colors.blue6}, + }) + const primaryText = useCustomPalette({ + light: {color: colors.blue3}, + dark: {color: colors.blue2}, + }) + + const dangerBg = useCustomPalette({ + light: {backgroundColor: colors.red1}, + dark: {backgroundColor: colors.red7}, + }) + const dangerText = useCustomPalette({ + 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 ( + + + + + + + Settings + + + + + + {currentAccount ? ( + <> + + Account + + + + Email:{' '} + + {currentAccount.emailConfirmed && ( + <> + + + )} + + {currentAccount.email || '(no email)'} + + openModal({name: 'change-email'})}> + + Change + + + + + + Birthday:{' '} + + openModal({name: 'birth-date-settings'})}> + + Show + + + + + + {!currentAccount.emailConfirmed && } + + ) : null} + + + Signed in as + + + + + {isSwitchingAccounts ? ( + + + + ) : ( + + )} + + {accounts + .filter(a => a.did !== currentAccount?.did) + .map(account => ( + + ))} + + + + + + + Add account + + + + + + + Invite a Friend + + + + 0 ? primaryBg : pal.btn, + ]}> + 0 + ? primaryText + : pal.text) as FontAwesomeIconStyle + } + /> + + 0 ? pal.link : pal.text}> + {invites?.disabled ? ( + + Your invite codes are hidden when logged in using an App + Password + + ) : invitesAvailable === 1 ? ( + {invitesAvailable} invite code available + ) : ( + {invitesAvailable} invite codes available + )} + + + + + + + Accessibility + + + setRequireAltTextEnabled(!requireAltTextEnabled)} + /> + + + + + + Appearance + + + + setColorMode('system')} + accessibilityHint={_(msg`Set color theme to system setting`)} + /> + setColorMode('light')} + accessibilityHint={_(msg`Set color theme to light`)} + /> + setColorMode('dark')} + accessibilityHint={_(msg`Set color theme to dark`)} + /> + + + + + + {colorMode !== 'light' && ( + <> + + Dark Theme + + + + setDarkTheme('dim')} + accessibilityHint={_(msg`Set dark theme to the dim theme`)} + /> + setDarkTheme('dark')} + accessibilityHint={_(msg`Set dark theme to the dark theme`)} + /> + + + + + )} + + + Basics + + + + + + + Home Feed Preferences + + + + + + + + Thread Preferences + + + + + + + + My Saved Feeds + + + + + + + + Languages + + + navigation.navigate('Moderation') + } + accessibilityRole="button" + accessibilityLabel={_(msg`Moderation settings`)} + accessibilityHint={_(msg`Opens moderation settings`)}> + + + + + Moderation + + + + + + + Privacy + + + navigation.navigate('PreferencesExternalEmbeds') + } + accessibilityRole="button" + accessibilityLabel={_(msg`External media settings`)} + accessibilityHint={_(msg`Opens external embeds settings`)}> + + + + + External Media Preferences + + + + + + + Advanced + + + + + + + App Passwords + + + + + + + + Change Handle + + + {isNative && ( + + setUseInAppBrowser(!inAppBrowserPref)} + /> + + )} + + + Account + + openModal({name: 'change-password'})} + accessibilityRole="button" + accessibilityLabel={_(msg`Change password`)} + accessibilityHint={_(msg`Change your Bluesky password`)}> + + + + + Change Password + + + + + + + + Export My Data + + + + + + + + Delete My Account… + + + + + + System log + + + {__DEV__ ? ( + <> + + + Storybook + + + + + Reset preferences state + + + + + Reset onboarding state + + + + + + Clear all legacy storage data (restart after this) + + + + + + Clear all storage data (restart after this) + + + + ) : null} + + + + + Build version {AppInfo.appVersion} {AppInfo.updateChannel} + + + + +   ·   + + + + Status page + + + + + + + + + + + + ) +} + +function EmailConfirmationNotice() { + const pal = usePalette('default') + const palInverted = usePalette('inverted') + const {_} = useLingui() + const {isMobile} = useWebMediaQueries() + const {openModal} = useModalControls() + + return ( + + + Verify email + + + + openModal({name: 'verify-email'})}> + + + Verify My Email + + + + + Protect your account by verifying your email. + + + + ) +} + +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, + }, +}) -- cgit 1.4.1