diff options
-rw-r--r-- | src/components/Link.tsx | 13 | ||||
-rw-r--r-- | src/lib/api/debug-appview-proxy-header.ts | 60 | ||||
-rw-r--r-- | src/view/icons/index.tsx | 2 | ||||
-rw-r--r-- | src/view/screens/Settings/ExportCarDialog.tsx | 103 | ||||
-rw-r--r-- | src/view/screens/Settings/index.tsx (renamed from src/view/screens/Settings.tsx) | 79 |
5 files changed, 161 insertions, 96 deletions
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<BaseLinkProps, 'warnOnMismatchingTextChild'> & * 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<BaseLinkProps, 'warnOnMismatchingTextChild'> & * 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<boolean>(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/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.tsx b/src/view/screens/Settings/index.tsx index d5531108d..458952527 100644 --- a/src/view/screens/Settings.tsx +++ b/src/view/screens/Settings/index.tsx @@ -17,14 +17,6 @@ import { } 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' @@ -34,8 +26,6 @@ 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 { @@ -48,22 +38,12 @@ import { useRequireAltTextEnabled, useSetRequireAltTextEnabled, } from '#/state/preferences' -import { - useSession, - useSessionApi, - SessionAccount, - getAgent, -} from '#/state/session' +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' - -// 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' @@ -75,6 +55,19 @@ import { 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') @@ -159,14 +152,12 @@ export function SettingsScreen({}: Props) { 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 exportCarControl = useDialogControl() const primaryBg = useCustomPalette<ViewStyle>({ light: {backgroundColor: colors.blue0}, @@ -214,6 +205,10 @@ export function SettingsScreen({}: Props) { }) }, [track, queryClient, openModal, currentAccount]) + const onPressExportRepository = React.useCallback(() => { + exportCarControl.open() + }, [exportCarControl]) + const onPressInviteCodes = React.useCallback(() => { track('Settings:InvitecodesButtonClicked') openModal({name: 'invite-codes'}) @@ -282,6 +277,8 @@ export function SettingsScreen({}: Props) { return ( <View style={s.hContentRegion} testID="settingsScreen"> + <ExportCarDialog control={exportCarControl} /> + <SimpleViewHeader showBackButton={isMobile} style={[ @@ -736,6 +733,29 @@ export function SettingsScreen({}: Props) { </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} @@ -756,9 +776,6 @@ export function SettingsScreen({}: Props) { </Text> </TouchableOpacity> <View style={styles.spacer20} /> - <Text type="xl-bold" style={[pal.text, styles.heading]}> - <Trans>Developer Tools</Trans> - </Text> <TouchableOpacity style={[pal.view, styles.linkCardNoIcon]} onPress={onPressSystemLog} @@ -770,14 +787,6 @@ export function SettingsScreen({}: Props) { </Text> </TouchableOpacity> {__DEV__ ? ( - <ToggleButton - type="default-light" - label="Experiment: Use AppView Proxy" - isSelected={debugHeaderEnabled} - onPress={toggleDebugHeader} - /> - ) : null} - {__DEV__ ? ( <> <TouchableOpacity style={[pal.view, styles.linkCardNoIcon]} |