about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--src/components/Link.tsx13
-rw-r--r--src/lib/api/debug-appview-proxy-header.ts60
-rw-r--r--src/view/icons/index.tsx2
-rw-r--r--src/view/screens/Settings/ExportCarDialog.tsx103
-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]}