diff --git a/package.json b/package.json
index a343c063d9..4ed2b933fe 100644
--- a/package.json
+++ b/package.json
@@ -51,7 +51,7 @@
},
"dependencies": {
"@atproto-labs/api": "^0.12.8-clipclops.0",
- "@atproto/api": "^0.12.6",
+ "@atproto/api": "^0.12.9",
"@bam.tech/react-native-image-resizer": "^3.0.4",
"@braintree/sanitize-url": "^6.0.2",
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
diff --git a/src/components/moderation/LabelsOnMe.tsx b/src/components/moderation/LabelsOnMe.tsx
index ea5c74f9e2..77d0e2d939 100644
--- a/src/components/moderation/LabelsOnMe.tsx
+++ b/src/components/moderation/LabelsOnMe.tsx
@@ -32,9 +32,7 @@ export function LabelsOnMe({
if (!labels || !currentAccount) {
return null
}
- labels = labels.filter(
- l => !l.val.startsWith('!') && l.src !== currentAccount.did,
- )
+ labels = labels.filter(l => !l.val.startsWith('!'))
if (!labels.length) {
return null
}
diff --git a/src/components/moderation/LabelsOnMeDialog.tsx b/src/components/moderation/LabelsOnMeDialog.tsx
index 176b04941e..858ac9ce4a 100644
--- a/src/components/moderation/LabelsOnMeDialog.tsx
+++ b/src/components/moderation/LabelsOnMeDialog.tsx
@@ -7,7 +7,7 @@ import {useLingui} from '@lingui/react'
import {useLabelInfo} from '#/lib/moderation/useLabelInfo'
import {makeProfileLink} from '#/lib/routes/links'
import {sanitizeHandle} from '#/lib/strings/handles'
-import {useAgent} from '#/state/session'
+import {useAgent, useSession} from '#/state/session'
import * as Toast from '#/view/com/util/Toast'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
@@ -33,13 +33,28 @@ export interface LabelsOnMeDialogProps {
labels: ComAtprotoLabelDefs.Label[]
}
-export function LabelsOnMeDialogInner(props: LabelsOnMeDialogProps) {
+export function LabelsOnMeDialog(props: LabelsOnMeDialogProps) {
+ return (
+
+
+
+
+
+ )
+}
+
+function LabelsOnMeDialogInner(props: LabelsOnMeDialogProps) {
const {_} = useLingui()
+ const {currentAccount} = useSession()
const [appealingLabel, setAppealingLabel] = React.useState<
ComAtprotoLabelDefs.Label | undefined
>(undefined)
const {subject, labels} = props
const isAccount = 'did' in subject
+ const containsSelfLabel = React.useMemo(
+ () => labels.some(l => l.src === currentAccount?.did),
+ [currentAccount?.did, labels],
+ )
return (
-
- You may appeal these labels if you feel they were placed in error.
-
+ {containsSelfLabel ? (
+
+ You may appeal non-self labels if you feel they were placed in
+ error.
+
+ ) : (
+
+ You may appeal these labels if you feel they were placed in
+ error.
+
+ )}
@@ -75,6 +98,7 @@ export function LabelsOnMeDialogInner(props: LabelsOnMeDialogProps) {
-
-
-
+ {!isSelfLabel && (
+
+
+
+ )}
- Source:{' '}
- control.close()}>
- {labeler ? sanitizeHandle(labeler.creator.handle, '@') : label.src}
-
+ {isSelfLabel ? (
+ This label was applied by you
+ ) : (
+ <>
+ Source:{' '}
+ control.close()}>
+ {labeler
+ ? sanitizeHandle(labeler.creator.handle, '@')
+ : label.src}
+
+ >
+ )}
diff --git a/src/lib/api/api-polyfill.ts b/src/lib/api/api-polyfill.ts
index ea1d975985..e3aec76316 100644
--- a/src/lib/api/api-polyfill.ts
+++ b/src/lib/api/api-polyfill.ts
@@ -1,5 +1,5 @@
-import {BskyAgent, stringifyLex, jsonToLex} from '@atproto/api'
import RNFS from 'react-native-fs'
+import {BskyAgent, jsonToLex, stringifyLex} from '@atproto/api'
const GET_TIMEOUT = 15e3 // 15s
const POST_TIMEOUT = 60e3 // 60s
@@ -68,8 +68,10 @@ async function fetchHandler(
resBody = jsonToLex(await res.json())
} else if (resMimeType.startsWith('text/')) {
resBody = await res.text()
+ } else if (resMimeType === 'application/vnd.ipld.car') {
+ resBody = await res.arrayBuffer()
} else {
- throw new Error('TODO: non-textual response body')
+ throw new Error('Non-supported mime type')
}
}
diff --git a/src/lib/api/feed-manip.ts b/src/lib/api/feed-manip.ts
index 85089608a7..3902a56599 100644
--- a/src/lib/api/feed-manip.ts
+++ b/src/lib/api/feed-manip.ts
@@ -71,6 +71,10 @@ export class FeedViewPostsSlice {
?.__source as ReasonFeedSource
}
+ get feedContext() {
+ return this.items.find(item => item.feedContext)?.feedContext
+ }
+
containsUri(uri: string) {
return !!this.items.find(item => item.post.uri === uri)
}
diff --git a/src/lib/media/manip.ts b/src/lib/media/manip.ts
index 9cd4abc626..71d5c701f8 100644
--- a/src/lib/media/manip.ts
+++ b/src/lib/media/manip.ts
@@ -1,12 +1,23 @@
import {Image as RNImage, Share as RNShare} from 'react-native'
import {Image} from 'react-native-image-crop-picker'
import uuid from 'react-native-uuid'
-import {cacheDirectory, copyAsync, deleteAsync} from 'expo-file-system'
+import {
+ cacheDirectory,
+ copyAsync,
+ deleteAsync,
+ documentDirectory,
+ EncodingType,
+ makeDirectoryAsync,
+ StorageAccessFramework,
+ writeAsStringAsync,
+} from 'expo-file-system'
import * as MediaLibrary from 'expo-media-library'
import * as Sharing from 'expo-sharing'
import ImageResizer from '@bam.tech/react-native-image-resizer'
+import {Buffer} from 'buffer'
import RNFetchBlob from 'rn-fetch-blob'
+import {logger} from '#/logger'
import {isAndroid, isIOS} from 'platform/detection'
import {Dimensions} from './types'
@@ -240,3 +251,64 @@ function normalizePath(str: string, allPlatforms = false): string {
}
return str
}
+
+export async function saveBytesToDisk(
+ filename: string,
+ bytes: Uint8Array,
+ type: string,
+) {
+ const encoded = Buffer.from(bytes).toString('base64')
+ return await saveToDevice(filename, encoded, type)
+}
+
+export async function saveToDevice(
+ filename: string,
+ encoded: string,
+ type: string,
+) {
+ try {
+ if (isIOS) {
+ const tmpFileUrl = await withTempFile(filename, encoded)
+ await Sharing.shareAsync(tmpFileUrl, {UTI: type})
+ safeDeleteAsync(tmpFileUrl)
+ return true
+ } else {
+ const permissions =
+ await StorageAccessFramework.requestDirectoryPermissionsAsync()
+
+ if (!permissions.granted) {
+ return false
+ }
+
+ const fileUrl = await StorageAccessFramework.createFileAsync(
+ permissions.directoryUri,
+ filename,
+ type,
+ )
+
+ await writeAsStringAsync(fileUrl, encoded, {
+ encoding: EncodingType.Base64,
+ })
+ return true
+ }
+ } catch (e) {
+ logger.error('Error occurred while saving file', {message: e})
+ return false
+ }
+}
+
+async function withTempFile(
+ filename: string,
+ encoded: string,
+): Promise {
+ // Using a directory so that the file name is not a random string
+ // documentDirectory will always be available on native, so we assert as a string.
+ const tmpDirUri = joinPath(documentDirectory as string, String(uuid.v4()))
+ await makeDirectoryAsync(tmpDirUri, {intermediates: true})
+
+ const tmpFileUrl = joinPath(tmpDirUri, filename)
+ await writeAsStringAsync(tmpFileUrl, encoded, {
+ encoding: EncodingType.Base64,
+ })
+ return tmpFileUrl
+}
diff --git a/src/lib/media/manip.web.ts b/src/lib/media/manip.web.ts
index 522aa2e51b..25315ebbd8 100644
--- a/src/lib/media/manip.web.ts
+++ b/src/lib/media/manip.web.ts
@@ -1,6 +1,7 @@
-import {Dimensions} from './types'
import {Image as RNImage} from 'react-native-image-crop-picker'
-import {getDataUriSize, blobToDataUri} from './util'
+
+import {Dimensions} from './types'
+import {blobToDataUri, getDataUriSize} from './util'
export async function compressIfNeeded(
img: RNImage,
@@ -138,3 +139,23 @@ function createResizedImage(
img.src = dataUri
})
}
+
+export async function saveBytesToDisk(
+ filename: string,
+ bytes: Uint8Array,
+ type: string,
+) {
+ const blob = new Blob([bytes], {type})
+ const url = URL.createObjectURL(blob)
+ await downloadUrl(url, filename)
+ // Firefox requires a small delay
+ setTimeout(() => URL.revokeObjectURL(url), 100)
+ return true
+}
+
+async function downloadUrl(href: string, filename: string) {
+ const a = document.createElement('a')
+ a.href = href
+ a.download = filename
+ a.click()
+}
diff --git a/src/screens/Onboarding/StepFinished.tsx b/src/screens/Onboarding/StepFinished.tsx
index 1480696214..51793777ee 100644
--- a/src/screens/Onboarding/StepFinished.tsx
+++ b/src/screens/Onboarding/StepFinished.tsx
@@ -3,13 +3,18 @@ import {View} from 'react-native'
import {TID} from '@atproto/common-web'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
+import {useQueryClient} from '@tanstack/react-query'
import {useAnalytics} from '#/lib/analytics/analytics'
import {BSKY_APP_ACCOUNT_DID, IS_PROD_SERVICE} from '#/lib/constants'
import {DISCOVER_SAVED_FEED, TIMELINE_SAVED_FEED} from '#/lib/constants'
import {logEvent, useGate} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
-import {useOverwriteSavedFeedsMutation} from '#/state/queries/preferences'
+import {
+ preferencesQueryKey,
+ useOverwriteSavedFeedsMutation,
+} from '#/state/queries/preferences'
+import {RQKEY as profileRQKey} from '#/state/queries/profile'
import {useAgent} from '#/state/session'
import {useOnboardingDispatch} from '#/state/shell'
import {uploadBlob} from 'lib/api'
@@ -41,6 +46,7 @@ export function StepFinished() {
const onboardDispatch = useOnboardingDispatch()
const [saving, setSaving] = React.useState(false)
const {mutateAsync: overwriteSavedFeeds} = useOverwriteSavedFeedsMutation()
+ const queryClient = useQueryClient()
const {getAgent} = useAgent()
const gate = useGate()
@@ -112,33 +118,41 @@ export function StepFinished() {
])
}
})(),
- ])
- if (gate('reduced_onboarding_and_home_algo')) {
- await getAgent().upsertProfile(async existing => {
- existing = existing ?? {}
-
- if (profileStepResults.imageUri && profileStepResults.imageMime) {
- const res = await uploadBlob(
- getAgent(),
- profileStepResults.imageUri,
- profileStepResults.imageMime,
- )
-
- if (res.data.blob) {
- existing.avatar = res.data.blob
- }
+ (async () => {
+ const {imageUri, imageMime} = profileStepResults
+ if (imageUri && imageMime) {
+ const blobPromise = uploadBlob(getAgent(), imageUri, imageMime)
+ await getAgent().upsertProfile(async existing => {
+ existing = existing ?? {}
+ const res = await blobPromise
+ if (res.data.blob) {
+ existing.avatar = res.data.blob
+ }
+ return existing
+ })
}
-
- return existing
- })
- }
+ })(),
+ ])
} catch (e: any) {
logger.info(`onboarding: bulk save failed`)
logger.error(e)
// don't alert the user, just let them into their account
}
+ // Try to ensure that prefs and profile are up-to-date by the time we render Home.
+ await Promise.all([
+ queryClient.invalidateQueries({
+ queryKey: preferencesQueryKey,
+ }),
+ queryClient.invalidateQueries({
+ queryKey: profileRQKey(getAgent().session?.did ?? ''),
+ }),
+ ]).catch(e => {
+ logger.error(e)
+ // Keep going.
+ })
+
setSaving(false)
dispatch({type: 'finish'})
onboardDispatch({type: 'finish'})
@@ -154,6 +168,7 @@ export function StepFinished() {
track,
getAgent,
gate,
+ queryClient,
])
React.useEffect(() => {
diff --git a/src/state/queries/post-feed.ts b/src/state/queries/post-feed.ts
index 7b312edfe5..e670e9da4a 100644
--- a/src/state/queries/post-feed.ts
+++ b/src/state/queries/post-feed.ts
@@ -303,7 +303,7 @@ export function usePostFeedQuery(
i === 0 && slice.source
? slice.source
: item.reason,
- feedContext: item.feedContext,
+ feedContext: item.feedContext || slice.feedContext,
moderation: moderations[i],
}
}
diff --git a/src/state/queries/preferences/index.ts b/src/state/queries/preferences/index.ts
index b3d2fa9ecd..555fd85a49 100644
--- a/src/state/queries/preferences/index.ts
+++ b/src/state/queries/preferences/index.ts
@@ -6,6 +6,7 @@ import {
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
import {track} from '#/lib/analytics/analytics'
+import {PROD_DEFAULT_FEED} from '#/lib/constants'
import {replaceEqualDeep} from '#/lib/functions'
import {getAge} from '#/lib/strings/time'
import {STALE} from '#/state/queries'
@@ -244,6 +245,45 @@ export function useRemoveFeedMutation() {
})
}
+export function useReplaceForYouWithDiscoverFeedMutation() {
+ const queryClient = useQueryClient()
+ const {getAgent} = useAgent()
+
+ return useMutation({
+ mutationFn: async ({
+ forYouFeedConfig,
+ discoverFeedConfig,
+ }: {
+ forYouFeedConfig: AppBskyActorDefs.SavedFeed | undefined
+ discoverFeedConfig: AppBskyActorDefs.SavedFeed | undefined
+ }) => {
+ if (forYouFeedConfig) {
+ await getAgent().removeSavedFeeds([forYouFeedConfig.id])
+ }
+ if (!discoverFeedConfig) {
+ await getAgent().addSavedFeeds([
+ {
+ type: 'feed',
+ value: PROD_DEFAULT_FEED('whats-hot'),
+ pinned: true,
+ },
+ ])
+ } else {
+ await getAgent().updateSavedFeeds([
+ {
+ ...discoverFeedConfig,
+ pinned: true,
+ },
+ ])
+ }
+ // triggers a refetch
+ await queryClient.invalidateQueries({
+ queryKey: preferencesQueryKey,
+ })
+ },
+ })
+}
+
export function useUpdateSavedFeedsMutation() {
const queryClient = useQueryClient()
const {getAgent} = useAgent()
diff --git a/src/state/queries/profile.ts b/src/state/queries/profile.ts
index 103d34733c..3e25359166 100644
--- a/src/state/queries/profile.ts
+++ b/src/state/queries/profile.ts
@@ -6,6 +6,7 @@ import {
AppBskyActorProfile,
AtUri,
BskyAgent,
+ ComAtprotoRepoUploadBlob,
} from '@atproto/api'
import {
QueryClient,
@@ -124,6 +125,26 @@ export function useProfileUpdateMutation() {
newUserBanner,
checkCommitted,
}) => {
+ let newUserAvatarPromise:
+ | Promise
+ | undefined
+ if (newUserAvatar) {
+ newUserAvatarPromise = uploadBlob(
+ getAgent(),
+ newUserAvatar.path,
+ newUserAvatar.mime,
+ )
+ }
+ let newUserBannerPromise:
+ | Promise
+ | undefined
+ if (newUserBanner) {
+ newUserBannerPromise = uploadBlob(
+ getAgent(),
+ newUserBanner.path,
+ newUserBanner.mime,
+ )
+ }
await getAgent().upsertProfile(async existing => {
existing = existing || {}
if (typeof updates === 'function') {
@@ -132,22 +153,14 @@ export function useProfileUpdateMutation() {
existing.displayName = updates.displayName
existing.description = updates.description
}
- if (newUserAvatar) {
- const res = await uploadBlob(
- getAgent(),
- newUserAvatar.path,
- newUserAvatar.mime,
- )
+ if (newUserAvatarPromise) {
+ const res = await newUserAvatarPromise
existing.avatar = res.data.blob
} else if (newUserAvatar === null) {
existing.avatar = undefined
}
- if (newUserBanner) {
- const res = await uploadBlob(
- getAgent(),
- newUserBanner.path,
- newUserBanner.mime,
- )
+ if (newUserBannerPromise) {
+ const res = await newUserBannerPromise
existing.banner = res.data.blob
} else if (newUserBanner === null) {
existing.banner = undefined
diff --git a/src/view/com/posts/FeedShutdownMsg.tsx b/src/view/com/posts/FeedShutdownMsg.tsx
index bc047e8311..47f8941e2a 100644
--- a/src/view/com/posts/FeedShutdownMsg.tsx
+++ b/src/view/com/posts/FeedShutdownMsg.tsx
@@ -6,10 +6,9 @@ import {useLingui} from '@lingui/react'
import {PROD_DEFAULT_FEED} from '#/lib/constants'
import {logger} from '#/logger'
import {
- useAddSavedFeedsMutation,
usePreferencesQuery,
useRemoveFeedMutation,
- useUpdateSavedFeedsMutation,
+ useReplaceForYouWithDiscoverFeedMutation,
} from '#/state/queries/preferences'
import {useSetSelectedFeed} from '#/state/shell/selected-feed'
import * as Toast from '#/view/com/util/Toast'
@@ -24,12 +23,10 @@ export function FeedShutdownMsg({feedUri}: {feedUri: string}) {
const {_} = useLingui()
const setSelectedFeed = useSetSelectedFeed()
const {data: preferences} = usePreferencesQuery()
- const {mutateAsync: addSavedFeeds, isPending: isAddSavedFeedPending} =
- useAddSavedFeedsMutation()
const {mutateAsync: removeFeed, isPending: isRemovePending} =
useRemoveFeedMutation()
- const {mutateAsync: updateSavedFeeds, isPending: isUpdateFeedPending} =
- useUpdateSavedFeedsMutation()
+ const {mutateAsync: replaceFeedWithDiscover, isPending: isReplacePending} =
+ useReplaceForYouWithDiscoverFeedMutation()
const feedConfig = preferences?.savedFeeds?.find(
f => f.value === feedUri && f.pinned,
@@ -46,6 +43,9 @@ export function FeedShutdownMsg({feedUri}: {feedUri: string}) {
await removeFeed(feedConfig)
Toast.show(_(msg`Removed from your feeds`))
}
+ if (hasDiscoverPinned) {
+ setSelectedFeed(`feedgen|${PROD_DEFAULT_FEED('whats-hot')}`)
+ }
} catch (err: any) {
Toast.show(
_(
@@ -54,30 +54,15 @@ export function FeedShutdownMsg({feedUri}: {feedUri: string}) {
)
logger.error('Failed up update feeds', {message: err})
}
- }, [removeFeed, feedConfig, _])
+ }, [removeFeed, feedConfig, _, hasDiscoverPinned, setSelectedFeed])
const onReplaceFeed = React.useCallback(async () => {
try {
- if (!discoverFeedConfig) {
- await addSavedFeeds([
- {
- type: 'feed',
- value: PROD_DEFAULT_FEED('whats-hot'),
- pinned: true,
- },
- ])
- } else {
- await updateSavedFeeds([
- {
- ...discoverFeedConfig,
- pinned: true,
- },
- ])
- }
+ await replaceFeedWithDiscover({
+ forYouFeedConfig: feedConfig,
+ discoverFeedConfig,
+ })
setSelectedFeed(`feedgen|${PROD_DEFAULT_FEED('whats-hot')}`)
- if (feedConfig) {
- await removeFeed(feedConfig)
- }
Toast.show(_(msg`The feed has been replaced with Discover.`))
} catch (err: any) {
Toast.show(
@@ -88,17 +73,14 @@ export function FeedShutdownMsg({feedUri}: {feedUri: string}) {
logger.error('Failed up update feeds', {message: err})
}
}, [
- addSavedFeeds,
- updateSavedFeeds,
- removeFeed,
+ replaceFeedWithDiscover,
discoverFeedConfig,
feedConfig,
setSelectedFeed,
_,
])
- const isProcessing =
- isAddSavedFeedPending || isUpdateFeedPending || isRemovePending
+ const isProcessing = isReplacePending || isRemovePending
return (
Replace with Discover
- {(isAddSavedFeedPending || isUpdateFeedPending) && (
-
- )}
+ {isReplacePending && }
)}
diff --git a/src/view/com/util/List.tsx b/src/view/com/util/List.tsx
index 0064a7b80c..90f5905f10 100644
--- a/src/view/com/util/List.tsx
+++ b/src/view/com/util/List.tsx
@@ -90,7 +90,7 @@ function ListImpl(
},
{
itemVisiblePercentThreshold: 40,
- minimumViewTime: 2e3,
+ minimumViewTime: 1.5e3,
},
]
}, [onItemSeen])
diff --git a/src/view/com/util/List.web.tsx b/src/view/com/util/List.web.tsx
index a64f7acf31..df097bafab 100644
--- a/src/view/com/util/List.web.tsx
+++ b/src/view/com/util/List.web.tsx
@@ -27,7 +27,7 @@ export type ListProps = Omit<
}
export type ListRef = React.MutableRefObject // TODO: Better types.
-const ON_ITEM_SEEN_WAIT_DURATION = 2e3 // post must be "seen" 2 seconds before capturing
+const ON_ITEM_SEEN_WAIT_DURATION = 1.5e3 // when we consider post to be "seen"
const ON_ITEM_SEEN_INTERSECTION_OPTS = {
rootMargin: '-200px 0px -200px 0px',
} // post must be 200px visible to be "seen"
diff --git a/src/view/screens/Settings/ExportCarDialog.tsx b/src/view/screens/Settings/ExportCarDialog.tsx
index 1b8d430b2a..af835cb620 100644
--- a/src/view/screens/Settings/ExportCarDialog.tsx
+++ b/src/view/screens/Settings/ExportCarDialog.tsx
@@ -3,12 +3,16 @@ import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
-import {useAgent, useSession} from '#/state/session'
-import {atoms as a, useBreakpoints, useTheme} from '#/alf'
-import {Button, ButtonText} from '#/components/Button'
+import {saveBytesToDisk} from '#/lib/media/manip'
+import {logger} from '#/logger'
+import {useAgent} from '#/state/session'
+import * as Toast from '#/view/com/util/Toast'
+import {atoms as a, useTheme} from '#/alf'
+import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
-import {InlineLinkText, Link} from '#/components/Link'
-import {P, Text} from '#/components/Typography'
+import {InlineLinkText} from '#/components/Link'
+import {Loader} from '#/components/Loader'
+import {Text} from '#/components/Typography'
export function ExportCarDialog({
control,
@@ -17,21 +21,35 @@ export function ExportCarDialog({
}) {
const {_} = useLingui()
const t = useTheme()
- const {gtMobile} = useBreakpoints()
- const {currentAccount} = useSession()
const {getAgent} = useAgent()
+ const [loading, setLoading] = React.useState(false)
- const downloadUrl = React.useMemo(() => {
+ const download = React.useCallback(async () => {
const agent = getAgent()
- if (!currentAccount || !agent.session) {
- return '' // shouldnt ever happen
+ if (!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, getAgent])
+ try {
+ setLoading(true)
+ const did = agent.session.did
+ const downloadRes = await agent.com.atproto.sync.getRepo({did})
+ const saveRes = await saveBytesToDisk(
+ 'repo.car',
+ downloadRes.data,
+ downloadRes.headers['content-type'],
+ )
+
+ if (saveRes) {
+ Toast.show(_(msg`File saved successfully!`))
+ }
+ } catch (e) {
+ logger.error('Error occurred while downloading CAR file', {message: e})
+ Toast.show(_(msg`Error occurred while saving file`))
+ } finally {
+ setLoading(false)
+ control.close()
+ }
+ }, [_, control, getAgent])
return (
@@ -40,34 +58,34 @@ export function ExportCarDialog({
-
+ 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.
-