migrate the data export dialog and signup queue off raw transport
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,11 @@
|
|||||||
import {useCallback, useState} from 'react'
|
import {useCallback, useState} from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
|
import {type DidString} from '@atproto/syntax'
|
||||||
import {Trans, useLingui} from '@lingui/react/macro'
|
import {Trans, useLingui} from '@lingui/react/macro'
|
||||||
|
|
||||||
import {DM_SERVICE_HEADERS} from '#/lib/constants'
|
|
||||||
import {saveBytesToDisk} from '#/lib/media/manip'
|
import {saveBytesToDisk} from '#/lib/media/manip'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {useAgent} from '#/state/session'
|
import {useChatClient, usePdsClient, useSession} from '#/state/session'
|
||||||
import {atoms as a, useTheme, web} from '#/alf'
|
import {atoms as a, useTheme, web} from '#/alf'
|
||||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
@@ -14,6 +14,7 @@ import {InlineLinkText} from '#/components/Link'
|
|||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
import * as Toast from '#/components/Toast'
|
import * as Toast from '#/components/Toast'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import {chat, com} from '#/lexicons'
|
||||||
|
|
||||||
export function ExportCarDialog({
|
export function ExportCarDialog({
|
||||||
control,
|
control,
|
||||||
@@ -22,21 +23,29 @@ export function ExportCarDialog({
|
|||||||
}) {
|
}) {
|
||||||
const {t: l} = useLingui()
|
const {t: l} = useLingui()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const agent = useAgent()
|
const {currentAccount} = useSession()
|
||||||
|
const pdsClient = usePdsClient()
|
||||||
|
const chatClient = useChatClient()
|
||||||
const [loading, setLoading] = useState<'repo' | 'chat' | false>(false)
|
const [loading, setLoading] = useState<'repo' | 'chat' | false>(false)
|
||||||
|
|
||||||
const download = useCallback(async () => {
|
const download = useCallback(async () => {
|
||||||
if (!agent.session) {
|
if (!currentAccount) {
|
||||||
return // shouldn't ever happen
|
return // shouldn't ever happen
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
setLoading('repo')
|
setLoading('repo')
|
||||||
const did = agent.session.did
|
const did = currentAccount.did as DidString
|
||||||
const downloadRes = await agent.com.atproto.sync.getRepo({did})
|
const data = await pdsClient.call(com.atproto.sync.getRepo, {did})
|
||||||
|
/*
|
||||||
|
* getRepo declares `application/vnd.ipld.car`, so lex-client hands back
|
||||||
|
* the raw bytes unparsed and does not surface the response content-type.
|
||||||
|
* The old code already fell back to this same constant when the header was
|
||||||
|
* absent, and the endpoint always returns CAR.
|
||||||
|
*/
|
||||||
const saveRes = await saveBytesToDisk(
|
const saveRes = await saveBytesToDisk(
|
||||||
'repo.car',
|
'repo.car',
|
||||||
downloadRes.data,
|
data,
|
||||||
downloadRes.headers['content-type'] || 'application/vnd.ipld.car',
|
'application/vnd.ipld.car',
|
||||||
)
|
)
|
||||||
|
|
||||||
if (saveRes) {
|
if (saveRes) {
|
||||||
@@ -48,28 +57,26 @@ export function ExportCarDialog({
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}, [l, agent])
|
}, [l, currentAccount, pdsClient])
|
||||||
|
|
||||||
const downloadChatData = useCallback(async () => {
|
const downloadChatData = useCallback(async () => {
|
||||||
if (!agent.session) {
|
if (!currentAccount) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
setLoading('chat')
|
setLoading('chat')
|
||||||
// Using raw fetch because the XRPC client incorrectly tries to JSON-parse
|
/*
|
||||||
// application/jsonl responses (substring match on application/json).
|
* lex-client only JSON-parses a response when the declared output encoding
|
||||||
const res = await agent.sessionManager.fetchHandler(
|
* is `application/json`; this endpoint declares `application/jsonl`, so it
|
||||||
'/xrpc/chat.bsky.actor.exportAccountData',
|
* returns the raw bytes. That removes the reason for the old low-level
|
||||||
{headers: DM_SERVICE_HEADERS},
|
* fetchHandler workaround, and the chat client emits the proxy header
|
||||||
)
|
* itself, so the per-call DM headers go away too.
|
||||||
if (!res.ok) {
|
*/
|
||||||
throw new Error(`HTTP ${res.status}`)
|
const data = await chatClient.call(chat.bsky.actor.exportAccountData)
|
||||||
}
|
|
||||||
const data = new Uint8Array(await res.arrayBuffer())
|
|
||||||
const saveRes = await saveBytesToDisk(
|
const saveRes = await saveBytesToDisk(
|
||||||
'chat.jsonl',
|
'chat.jsonl',
|
||||||
data,
|
data,
|
||||||
res.headers.get('content-type') || 'application/jsonl',
|
'application/jsonl',
|
||||||
)
|
)
|
||||||
|
|
||||||
if (saveRes) {
|
if (saveRes) {
|
||||||
@@ -81,7 +88,7 @@ export function ExportCarDialog({
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}, [l, agent])
|
}, [l, currentAccount, chatClient])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}>
|
<Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {useLingui} from '@lingui/react'
|
|||||||
import {Trans} from '@lingui/react/macro'
|
import {Trans} from '@lingui/react/macro'
|
||||||
|
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {isSignupQueued, useAgent, useSessionApi} from '#/state/session'
|
import {isSignupQueued, usePdsClient, useSessionApi} from '#/state/session'
|
||||||
import {useOnboardingDispatch} from '#/state/shell'
|
import {useOnboardingDispatch} from '#/state/shell'
|
||||||
import {Logo} from '#/view/icons/Logo'
|
import {Logo} from '#/view/icons/Logo'
|
||||||
import {atoms as a, native, useBreakpoints, useTheme, web} from '#/alf'
|
import {atoms as a, native, useBreakpoints, useTheme, web} from '#/alf'
|
||||||
@@ -15,6 +15,7 @@ import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
|||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
import {P, Text} from '#/components/Typography'
|
import {P, Text} from '#/components/Typography'
|
||||||
import {IS_IOS, IS_LIQUID_GLASS, IS_WEB} from '#/env'
|
import {IS_IOS, IS_LIQUID_GLASS, IS_WEB} from '#/env'
|
||||||
|
import {com} from '#/lexicons'
|
||||||
|
|
||||||
const COL_WIDTH = 400
|
const COL_WIDTH = 400
|
||||||
|
|
||||||
@@ -24,8 +25,8 @@ export function SignupQueued() {
|
|||||||
const insets = useSafeAreaInsets()
|
const insets = useSafeAreaInsets()
|
||||||
const {gtMobile} = useBreakpoints()
|
const {gtMobile} = useBreakpoints()
|
||||||
const onboardingDispatch = useOnboardingDispatch()
|
const onboardingDispatch = useOnboardingDispatch()
|
||||||
const {logoutCurrentAccount} = useSessionApi()
|
const {logoutCurrentAccount, refreshSession} = useSessionApi()
|
||||||
const agent = useAgent()
|
const pdsClient = usePdsClient()
|
||||||
|
|
||||||
const [isProcessing, setProcessing] = useState(false)
|
const [isProcessing, setProcessing] = useState(false)
|
||||||
const [estimatedTime, setEstimatedTime] = useState<string | undefined>(
|
const [estimatedTime, setEstimatedTime] = useState<string | undefined>(
|
||||||
@@ -38,18 +39,23 @@ export function SignupQueued() {
|
|||||||
const checkStatus = useCallback(async () => {
|
const checkStatus = useCallback(async () => {
|
||||||
setProcessing(true)
|
setProcessing(true)
|
||||||
try {
|
try {
|
||||||
const res = await agent.com.atproto.temp.checkSignupQueue()
|
const res = await pdsClient.call(com.atproto.temp.checkSignupQueue)
|
||||||
if (res.data.activated) {
|
if (res.activated) {
|
||||||
// ready to go, exchange the access token for a usable one and kick off onboarding
|
/*
|
||||||
await agent.sessionManager.refreshSession()
|
* Ready to go, exchange the access token for a usable one and kick off
|
||||||
if (!isSignupQueued(agent.session?.accessJwt)) {
|
* onboarding. The refreshed snapshot carries the new scope; reading
|
||||||
|
* `currentAccount` here would still see the pre-refresh token, since the
|
||||||
|
* session's update hook dispatches a render away.
|
||||||
|
*/
|
||||||
|
const refreshed = await refreshSession()
|
||||||
|
if (!isSignupQueued(refreshed?.accessJwt)) {
|
||||||
onboardingDispatch({type: 'start'})
|
onboardingDispatch({type: 'start'})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// not ready, update UI
|
// not ready, update UI
|
||||||
setEstimatedTime(msToString(res.data.estimatedTimeMs))
|
setEstimatedTime(msToString(res.estimatedTimeMs))
|
||||||
if (typeof res.data.placeInQueue !== 'undefined') {
|
if (typeof res.placeInQueue !== 'undefined') {
|
||||||
setPlaceInQueue(Math.max(res.data.placeInQueue, 1))
|
setPlaceInQueue(Math.max(res.placeInQueue, 1))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@@ -62,7 +68,8 @@ export function SignupQueued() {
|
|||||||
setEstimatedTime,
|
setEstimatedTime,
|
||||||
setPlaceInQueue,
|
setPlaceInQueue,
|
||||||
onboardingDispatch,
|
onboardingDispatch,
|
||||||
agent,
|
pdsClient,
|
||||||
|
refreshSession,
|
||||||
])
|
])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user