Skip to content

Commit

Permalink
basic export repository link in settings (#2641)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
bnewbold and pfrazee authored Feb 12, 2024
1 parent b308d7e commit d7a3246
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 96 deletions.
13 changes: 12 additions & 1 deletion src/components/Link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

/**
Expand All @@ -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 : '',
Expand All @@ -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
Expand Down
60 changes: 0 additions & 60 deletions src/lib/api/debug-appview-proxy-header.ts

This file was deleted.

2 changes: 2 additions & 0 deletions src/view/icons/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -143,6 +144,7 @@ library.add(
faCommentSlash,
faComments,
faCompass,
faDownload,
faEllipsis,
faEnvelope,
faEye,
Expand Down
103 changes: 103 additions & 0 deletions src/view/screens/Settings/ExportCarDialog.tsx
Original file line number Diff line number Diff line change
@@ -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>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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 {
Expand All @@ -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'
Expand All @@ -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')
Expand Down Expand Up @@ -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},
Expand Down Expand Up @@ -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'})
Expand Down Expand Up @@ -282,6 +277,8 @@ export function SettingsScreen({}: Props) {

return (
<View style={s.hContentRegion} testID="settingsScreen">
<ExportCarDialog control={exportCarControl} />

<SimpleViewHeader
showBackButton={isMobile}
style={[
Expand Down Expand Up @@ -735,6 +732,29 @@ export function SettingsScreen({}: Props) {
<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}
Expand All @@ -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}
Expand All @@ -769,14 +786,6 @@ export function SettingsScreen({}: Props) {
<Trans>System log</Trans>
</Text>
</TouchableOpacity>
{__DEV__ ? (
<ToggleButton
type="default-light"
label="Experiment: Use AppView Proxy"
isSelected={debugHeaderEnabled}
onPress={toggleDebugHeader}
/>
) : null}
{__DEV__ ? (
<>
<TouchableOpacity
Expand Down

0 comments on commit d7a3246

Please sign in to comment.