phase 4: migrate account flows, trending queries, notifications, and report subjects off the bridge

This commit is contained in:
Samuel Newman
2026-07-17 12:46:12 +03:00
parent 5a099a8db6
commit bab2b621a9
57 changed files with 724 additions and 494 deletions
+7 -6
View File
@@ -10,7 +10,7 @@ import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
import {logger} from '#/logger'
import {
type SessionAccount,
useAgent,
usePdsClient,
useSession,
useSessionApi,
} from '#/state/session'
@@ -25,6 +25,7 @@ import * as Layout from '#/components/Layout'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
import {IS_WEB} from '#/env'
import {com} from '#/lexicons'
const COL_WIDTH = 400
@@ -36,8 +37,8 @@ export function Deactivated() {
const {onPressSwitchAccount, pendingDid} = useAccountSwitcher()
const {setShowLoggedOut} = useLoggedOutViewControls()
const hasOtherAccounts = accounts.length > 1
const {logoutCurrentAccount} = useSessionApi()
const agent = useAgent()
const {logoutCurrentAccount, refreshSession} = useSessionApi()
const pdsClient = usePdsClient()
const [pending, setPending] = useState(false)
const [error, setError] = useState<string | undefined>()
const queryClient = useQueryClient()
@@ -70,9 +71,9 @@ export function Deactivated() {
const handleActivate = useCallback(async () => {
try {
setPending(true)
await agent.com.atproto.server.activateAccount()
await pdsClient.call(com.atproto.server.activateAccount)
await queryClient.resetQueries()
await agent.resumeSession(agent.session!)
await refreshSession()
} catch (e: any) {
switch (e.message) {
case 'Bad token scope':
@@ -93,7 +94,7 @@ export function Deactivated() {
} finally {
setPending(false)
}
}, [_, agent, setPending, setError, queryClient])
}, [_, pdsClient, refreshSession, setPending, setError, queryClient])
return (
<View style={[a.util_screen_outer, a.flex_1]}>
+4 -4
View File
@@ -1,11 +1,11 @@
import {useCallback, useState} from 'react'
import {Keyboard, View} from 'react-native'
import {Client} from '@atproto/lex-client'
import {Trans, useLingui} from '@lingui/react/macro'
import * as EmailValidator from 'email-validator'
import {cleanError, isNetworkError} from '#/lib/strings/errors'
import {logger} from '#/logger'
import {Agent} from '#/state/session/agent'
import {atoms as a, useTheme, web} from '#/alf'
import {Admonition} from '#/components/Admonition'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
@@ -15,7 +15,7 @@ import {At_Stroke2_Corner0_Rounded as At} from '#/components/icons/At'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
import {IS_WEB} from '#/env'
import {type com} from '#/lexicons'
import {com} from '#/lexicons'
import {FormContainer} from './FormContainer'
type ServiceDescription = com.atproto.server.describeServer.$OutputBody
@@ -55,8 +55,8 @@ export const ForgotPasswordForm = ({
setIsProcessing(true)
try {
const agent = new Agent(null, {service: serviceUrl})
await agent.com.atproto.server.requestPasswordReset({email})
const client = new Client({service: serviceUrl})
await client.call(com.atproto.server.requestPasswordReset, {email})
onEmailSent()
} catch (err) {
logger.warn('Failed to request password reset', {error: err})
+4 -3
View File
@@ -1,11 +1,11 @@
import {useState} from 'react'
import {View} from 'react-native'
import {Client} from '@atproto/lex-client'
import {Trans, useLingui} from '@lingui/react/macro'
import {cleanError, isNetworkError} from '#/lib/strings/errors'
import {checkAndFormatResetCode} from '#/lib/strings/password'
import {logger} from '#/logger'
import {Agent} from '#/state/session/agent'
import {atoms as a, web} from '#/alf'
import {Admonition} from '#/components/Admonition'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
@@ -16,6 +16,7 @@ import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
import {IS_WEB} from '#/env'
import {com} from '#/lexicons'
import {FormContainer} from './FormContainer'
export const SetNewPasswordForm = ({
@@ -61,8 +62,8 @@ export const SetNewPasswordForm = ({
setIsProcessing(true)
try {
const agent = new Agent(null, {service: serviceUrl})
await agent.com.atproto.server.resetPassword({
const client = new Client({service: serviceUrl})
await client.call(com.atproto.server.resetPassword, {
token: formattedCode,
password,
})
@@ -169,16 +169,14 @@ function keyExtractor(item: Item) {
return item.key
}
/*
* The member list now comes from the migrated lexicon-typed query, but the
* narrowed `GroupConvoMember` target is still the old-typed shape from
* `#/components/dms/util` (migrates in a later task) - the guard doubles as
* the mixed-world bridge. TODO(phase4): retype to the lexicon member types
* once dms/util migrates.
/**
* Narrows a lexicon `ProfileViewBasic` member to a `GroupConvoMember` (a
* `ProfileViewBasic` whose `kind` is a group member, or absent when the
* account has been deleted).
*/
function isGroupMember(
member: chat.bsky.actor.defs.ProfileViewBasic,
): member is chat.bsky.actor.defs.ProfileViewBasic & GroupConvoMember {
): member is GroupConvoMember {
// Kind is missing when the account has been deleted.
return (
member.kind === undefined ||
@@ -205,15 +203,7 @@ function GroupSettings({
const {data: memberListData = [], refetch} = useListConvoMembersQuery({
convoId: convo.view.id,
/*
* `convo.members` comes from the still-old-typed `#/components/dms/util`
* (migrates in a later task) while the member-list query is now typed on
* the lexicon ProfileViewBasic. TODO(phase4): drop toLex once dms/util
* migrates.
*/
placeholderData: bsky.toLex<chat.bsky.actor.defs.ProfileViewBasic[]>(
convo.members,
),
placeholderData: convo.members,
})
const {data: joinRequestsData, hasNextPage: hasMoreRequests} =
@@ -11,10 +11,10 @@ import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
import {Loader} from '#/components/Loader'
import {accountReportSubject} from '#/components/moderation/ReportDialog/utils/reportSubject'
import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography'
import {com, tools} from '#/lexicons'
import {toLex} from '#/types/bsky'
export function ChatDisabled({
shape = 'pill',
@@ -102,14 +102,11 @@ function DialogInner() {
throw new Error('No current account, should be unreachable')
await pdsClient.call(
com.atproto.moderation.createReport,
toLex<com.atproto.moderation.createReport.$InputBody>({
{
reasonType: tools.ozone.report.defs.reasonAppeal.value,
subject: {
$type: 'com.atproto.admin.defs#repoRef',
did: currentAccount.did,
},
subject: accountReportSubject(currentAccount.did),
reason: details,
}),
},
{
service: api.moderation.service,
},
+8 -10
View File
@@ -1,6 +1,7 @@
import {useCallback, useEffect, useState} from 'react'
import {type ListRenderItemInfo, View} from 'react-native'
import * as Contacts from 'expo-contacts'
import {type DidString} from '@atproto/syntax'
import {type ModerationOpts} from '@bsky.app/sdk/moderation'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
@@ -28,12 +29,7 @@ import {
useContactsMatchesQuery,
useContactsSyncStatusQuery,
} from '#/state/queries/find-contacts'
import {
useAgent,
useAppviewClient,
usePdsClient,
useSession,
} from '#/state/session'
import {useAppviewClient, usePdsClient, useSession} from '#/state/session'
import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
import {List} from '#/view/com/util/List'
import {atoms as a, tokens, useGutters, useTheme} from '#/alf'
@@ -196,7 +192,7 @@ function SyncStatus({
refetchStatus: () => Promise<any>
}) {
const ax = useAnalytics()
const agent = useAgent()
const appviewClient = useAppviewClient()
const queryClient = useQueryClient()
const {_} = useLingui()
const moderationOpts = useModerationOpts()
@@ -221,7 +217,9 @@ function SyncStatus({
const {mutate: dismissMatch} = useMutation({
mutationFn: async (did: string) => {
await agent.app.bsky.contact.dismissMatch({subject: did})
await appviewClient.call(app.bsky.contact.dismissMatch, {
subject: did as DidString,
})
},
onMutate: async (did: string) => {
ax.metric('contacts:settings:dismiss', {})
@@ -493,12 +491,12 @@ function StatusFooter({syncedAt}: {syncedAt: string}) {
const {_, i18n} = useLingui()
const t = useTheme()
const ax = useAnalytics()
const agent = useAgent()
const appviewClient = useAppviewClient()
const queryClient = useQueryClient()
const {mutate: removeData, isPending} = useMutation({
mutationFn: async () => {
await agent.app.bsky.contact.removeData({})
await appviewClient.call(app.bsky.contact.removeData, {})
},
onMutate: () => ax.metric('contacts:settings:removeData', {}),
onSuccess: () => {
+5 -4
View File
@@ -1,5 +1,6 @@
import {useMemo, useState} from 'react'
import {type TextStyle, View, type ViewStyle} from 'react-native'
import {setInterestsPref} from '@bsky.app/sdk'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
@@ -23,7 +24,7 @@ import {createGetSuggestedUsersForDiscoverQueryKey} from '#/state/queries/trendi
import {createGetSuggestedUsersForExploreQueryKey} from '#/state/queries/trending/useGetSuggestedUsersForExploreQuery'
import {createGetSuggestedUsersForSeeMoreQueryKey} from '#/state/queries/trending/useGetSuggestedUsersForSeeMoreQuery'
import {createSuggestedStarterPacksQueryKey} from '#/state/queries/useSuggestedStarterPacksQuery'
import {useAgent} from '#/state/session'
import {usePdsClient} from '#/state/session'
import {atoms as a, useGutters, useTheme} from '#/alf'
import {Admonition} from '#/components/Admonition'
import {Divider} from '#/components/Divider'
@@ -88,7 +89,7 @@ function Inner({
setIsSaving: (isSaving: boolean) => void
}) {
const {_} = useLingui()
const agent = useAgent()
const pdsClient = usePdsClient()
const qc = useQueryClient()
const interestsDisplayNames = useInterestsDisplayNames()
const preselectedInterests = useMemo(
@@ -110,7 +111,7 @@ function Inner({
setIsSaving(true)
try {
await agent.setInterestsPref({tags: interests})
await pdsClient.call(setInterestsPref, {tags: interests})
qc.setQueriesData(
{queryKey: preferencesQueryKey},
(old?: UsePreferencesQueryResponse) => {
@@ -157,7 +158,7 @@ function Inner({
setIsSaving(false)
}
}, 1500)
}, [_, agent, setIsSaving, qc, preselectedInterests])
}, [_, pdsClient, setIsSaving, qc, preselectedInterests])
const onChangeInterests = async (interests: string[]) => {
setInterests(interests)
+9 -4
View File
@@ -1,6 +1,7 @@
import {useState} from 'react'
import {Alert, LayoutAnimation, Linking, Pressable, View} from 'react-native'
import {useReducedMotion} from 'react-native-reanimated'
import {removeNuxs} from '@bsky.app/sdk'
import {moderateProfile} from '@bsky.app/sdk/moderation'
import {Trans, useLingui} from '@lingui/react/macro'
import {useNavigation} from '@react-navigation/native'
@@ -21,8 +22,12 @@ import {clearStorage} from '#/state/persisted'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useDeleteActorDeclaration} from '#/state/queries/messages/actor-declaration'
import {useProfileQuery, useProfilesQuery} from '#/state/queries/profile'
import {useAgent} from '#/state/session'
import {type SessionAccount, useSession, useSessionApi} from '#/state/session'
import {
type SessionAccount,
usePdsClient,
useSession,
useSessionApi,
} from '#/state/session'
import {useOnboardingDispatch} from '#/state/shell'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {useCloseAllActiveElements} from '#/state/util'
@@ -386,7 +391,7 @@ function ProfilePreview({
function DevOptions() {
const {t: l} = useLingui()
const agent = useAgent()
const pdsClient = usePdsClient()
const [override, setOverride] = useStorage(device, [
'policyUpdateDebugOverride',
])
@@ -561,7 +566,7 @@ function DevOptions() {
<Button
onPress={() => {
device.set([PolicyUpdate202508], false)
void agent.bskyAppRemoveNuxs([PolicyUpdate202508])
void pdsClient.call(removeNuxs, [PolicyUpdate202508])
Toast.show(`Done`, {
type: 'info',
})
@@ -26,7 +26,7 @@ import {useFetchDid, useUpdateHandleMutation} from '#/state/queries/handle'
import {RQKEY as RQKEY_PROFILE} from '#/state/queries/profile'
import {useServiceQuery} from '#/state/queries/service'
import {useCurrentAccountProfile} from '#/state/queries/useCurrentAccountProfile'
import {useAgent, useSession} from '#/state/session'
import {useSession, useSessionApi} from '#/state/session'
import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
import {atoms as a, native, useBreakpoints, useTheme} from '#/alf'
import {Admonition} from '#/components/Admonition'
@@ -63,12 +63,12 @@ export function ChangeHandleDialog({
function ChangeHandleDialogInner() {
const control = Dialog.useDialogContext()
const {_} = useLingui()
const agent = useAgent()
const {currentAccount} = useSession()
const {
data: serviceInfo,
error: serviceInfoError,
refetch,
} = useServiceQuery(agent.serviceUrl.toString())
} = useServiceQuery(currentAccount?.service ?? '')
const [page, setPage] = useState<'provided-handle' | 'own-handle'>(
'provided-handle',
@@ -152,7 +152,7 @@ function ProvidedHandlePage({
}) {
const {_} = useLingui()
const [subdomain, setSubdomain] = useState('')
const agent = useAgent()
const {refreshSession} = useSessionApi()
const control = Dialog.useDialogContext()
const {currentAccount} = useSession()
const queryClient = useQueryClient()
@@ -173,7 +173,7 @@ function ProvidedHandlePage({
queryKey: RQKEY_PROFILE(currentAccount.did),
})
}
agent.resumeSession(agent.session!).then(() => control.close())
refreshSession().then(() => control.close())
},
})
@@ -311,7 +311,7 @@ function OwnHandlePage({goToServiceHandle}: {goToServiceHandle: () => void}) {
const {currentAccount} = useSession()
const [dnsPanel, setDNSPanel] = useState(true)
const [domain, setDomain] = useState('')
const agent = useAgent()
const {refreshSession} = useSessionApi()
const control = Dialog.useDialogContext()
const fetchDid = useFetchDid()
const queryClient = useQueryClient()
@@ -328,7 +328,7 @@ function OwnHandlePage({goToServiceHandle}: {goToServiceHandle: () => void}) {
queryKey: RQKEY_PROFILE(currentAccount.did),
})
}
agent.resumeSession(agent.session!).then(() => control.close())
refreshSession().then(() => control.close())
},
})
@@ -8,7 +8,7 @@ import * as EmailValidator from 'email-validator'
import {cleanError, isNetworkError} from '#/lib/strings/errors'
import {checkAndFormatResetCode} from '#/lib/strings/password'
import {logger} from '#/logger'
import {useAgent, useSession} from '#/state/session'
import {usePdsClient, useSession} from '#/state/session'
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
import {android, atoms as a, web} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
@@ -17,6 +17,7 @@ import * as TextField from '#/components/forms/TextField'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
import {IS_NATIVE} from '#/env'
import {com} from '#/lexicons'
enum Stages {
RequestCode = 'RequestCode',
@@ -44,7 +45,7 @@ export function ChangePasswordDialog({
function Inner() {
const {_} = useLingui()
const {currentAccount} = useSession()
const agent = useAgent()
const pdsClient = usePdsClient()
const control = Dialog.useDialogContext()
const [stage, setStage] = useState(Stages.RequestCode)
@@ -85,7 +86,7 @@ function Inner() {
setError('')
setIsProcessing(true)
try {
await agent.com.atproto.server.requestPasswordReset({
await pdsClient.call(com.atproto.server.requestPasswordReset, {
email: currentAccount.email,
})
setStage(Stages.ChangePassword)
@@ -129,7 +130,7 @@ function Inner() {
setError('')
setIsProcessing(true)
try {
await agent.com.atproto.server.resetPassword({
await pdsClient.call(com.atproto.server.resetPassword, {
token: formattedCode,
password: newPassword,
})
@@ -5,7 +5,7 @@ import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
import {logger} from '#/logger'
import {useAgent, useSessionApi} from '#/state/session'
import {usePdsClient, useSessionApi} from '#/state/session'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {type DialogOuterProps} from '#/components/Dialog'
@@ -14,6 +14,7 @@ import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/ico
import {Loader} from '#/components/Loader'
import * as Prompt from '#/components/Prompt'
import {Text} from '#/components/Typography'
import {com} from '#/lexicons'
export function DeactivateAccountDialog({
control,
@@ -34,7 +35,7 @@ function DeactivateAccountDialogInner({
}) {
const t = useTheme()
const {_} = useLingui()
const agent = useAgent()
const pdsClient = usePdsClient()
const {logoutCurrentAccount} = useSessionApi()
const [pending, setPending] = useState(false)
const [error, setError] = useState<string | undefined>()
@@ -42,7 +43,7 @@ function DeactivateAccountDialogInner({
const handleDeactivate = useCallback(async () => {
try {
setPending(true)
await agent.com.atproto.server.deactivateAccount({})
await pdsClient.call(com.atproto.server.deactivateAccount, {})
control.close(() => {
logoutCurrentAccount('Deactivated')
})
@@ -66,7 +67,7 @@ function DeactivateAccountDialogInner({
} finally {
setPending(false)
}
}, [agent, control, logoutCurrentAccount, _, setPending])
}, [pdsClient, control, logoutCurrentAccount, _, setPending])
return (
<>
@@ -1,5 +1,6 @@
import {useCallback, useRef, useState} from 'react'
import {type TextInput, View} from 'react-native'
import {type DidString} from '@atproto/syntax'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
@@ -8,8 +9,8 @@ import {useCleanError} from '#/lib/hooks/useCleanError'
import {sanitizeHandle} from '#/lib/strings/handles'
import {logger} from '#/logger'
import {
useAgent,
useChatClient,
usePdsClient,
useSession,
useSessionApi,
} from '#/state/session'
@@ -28,7 +29,7 @@ import {Loader} from '#/components/Loader'
import * as Prompt from '#/components/Prompt'
import * as toast from '#/components/Toast'
import {Span, Text} from '#/components/Typography'
import {chat} from '#/lexicons'
import {chat, com} from '#/lexicons'
import {resetToTab} from '#/Navigation'
const WHITESPACE_RE = /\s/gu
@@ -77,7 +78,7 @@ function DeleteAccountDialogInner({
const t = useTheme()
const {_} = useLingui()
const cleanError = useCleanError()
const agent = useAgent()
const pdsClient = usePdsClient()
const chatClient = useChatClient()
const {currentAccount} = useSession()
const {removeAccount} = useSessionApi()
@@ -95,7 +96,7 @@ function DeleteAccountDialogInner({
}
try {
setEmailState(EmailState.PENDING)
await agent.com.atproto.server.requestAccountDelete()
await pdsClient.call(com.atproto.server.requestAccountDelete)
setError('')
setEmailSentCount(prevCount => prevCount + 1)
setStep(Step.VERIFY_CODE)
@@ -109,7 +110,7 @@ function DeleteAccountDialogInner({
} finally {
setEmailState(EmailState.DEFAULT)
}
}, [agent, cleanError, emailState, setEmailState])
}, [pdsClient, cleanError, emailState, setEmailState])
const confirmDeletion = useCallback(async () => {
try {
@@ -121,8 +122,8 @@ function DeleteAccountDialogInner({
// Inform chat service of intent to delete account. The chat client is
// proxied to the chat service; a failure throws.
await chatClient.call(chat.bsky.actor.deleteAccount)
await agent.com.atproto.server.deleteAccount({
did: currentAccount.did,
await pdsClient.call(com.atproto.server.deleteAccount, {
did: currentAccount.did as DidString,
password,
token,
})
@@ -144,7 +145,8 @@ function DeleteAccountDialogInner({
}
}, [
_,
agent,
pdsClient,
chatClient,
cleanError,
confirmCode,
control,
@@ -5,7 +5,7 @@ import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
import {cleanError} from '#/lib/strings/errors'
import {useAgent, useSession} from '#/state/session'
import {usePdsClient, useSession, useSessionApi} from '#/state/session'
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
@@ -16,6 +16,7 @@ import {Loader} from '#/components/Loader'
import * as Toast from '#/components/Toast'
import {P, Text} from '#/components/Typography'
import {IS_NATIVE} from '#/env'
import {com} from '#/lexicons'
enum Stages {
Email,
@@ -31,7 +32,8 @@ export function DisableEmail2FADialog({
const t = useTheme()
const {gtMobile} = useBreakpoints()
const {currentAccount} = useSession()
const agent = useAgent()
const pdsClient = usePdsClient()
const {refreshSession} = useSessionApi()
const [stage, setStage] = useState<Stages>(Stages.Email)
const [confirmationCode, setConfirmationCode] = useState<string>('')
@@ -42,7 +44,7 @@ export function DisableEmail2FADialog({
setError('')
setIsProcessing(true)
try {
await agent.com.atproto.server.requestEmailUpdate()
await pdsClient.call(com.atproto.server.requestEmailUpdate)
setStage(Stages.ConfirmCode)
} catch (e) {
setError(cleanError(String(e)))
@@ -56,12 +58,12 @@ export function DisableEmail2FADialog({
setIsProcessing(true)
try {
if (currentAccount?.email) {
await agent.com.atproto.server.updateEmail({
await pdsClient.call(com.atproto.server.updateEmail, {
email: currentAccount.email,
token: confirmationCode.trim(),
emailAuthFactor: false,
})
await agent.resumeSession(agent.session!)
await refreshSession()
Toast.show(_(msg({message: 'Email 2FA disabled', context: 'toast'})))
}
control.close()
@@ -1,10 +1,11 @@
import {useCallback, useState} from 'react'
import {View} from 'react-native'
import {type DidString} from '@atproto/syntax'
import {Trans, useLingui} from '@lingui/react/macro'
import {saveBytesToDisk} from '#/lib/media/manip'
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 {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
@@ -13,7 +14,7 @@ import {InlineLinkText} from '#/components/Link'
import {Loader} from '#/components/Loader'
import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography'
import {CHAT_PROXY_DID} from '#/env'
import {com} from '#/lexicons'
export function ExportCarDialog({
control,
@@ -22,21 +23,28 @@ export function ExportCarDialog({
}) {
const {t: l} = useLingui()
const t = useTheme()
const agent = useAgent()
const {currentAccount} = useSession()
const pdsClient = usePdsClient()
const chatClient = useChatClient()
const [loading, setLoading] = useState<'repo' | 'chat' | false>(false)
const download = useCallback(async () => {
if (!agent.session) {
if (!currentAccount) {
return // shouldn't ever happen
}
try {
setLoading('repo')
const did = agent.session.did
const downloadRes = await agent.com.atproto.sync.getRepo({did})
const did = currentAccount.did as DidString
const data = await pdsClient.call(com.atproto.sync.getRepo, {did})
/*
* getRepo returns raw bytes; the lex client does not surface the response
* content-type, and this endpoint always returns CAR data, so the constant
* matches the old header-derived value in practice.
*/
const saveRes = await saveBytesToDisk(
'repo.car',
downloadRes.data,
downloadRes.headers['content-type'] || 'application/vnd.ipld.car',
data,
'application/vnd.ipld.car',
)
if (saveRes) {
@@ -48,21 +56,24 @@ export function ExportCarDialog({
} finally {
setLoading(false)
}
}, [l, agent])
}, [l, currentAccount, pdsClient])
const downloadChatData = useCallback(async () => {
if (!agent.session) {
if (!currentAccount) {
return
}
try {
setLoading('chat')
// Using raw fetch because the XRPC client incorrectly tries to JSON-parse
// application/jsonl responses (substring match on application/json).
// The chat-service proxy header is inlined here (the endpoint is proxied
// to `did:web:api.bsky.chat`); this raw path bypasses the lex client.
const res = await agent.sessionManager.fetchHandler(
/*
* Using the client's low-level fetchHandler because the XRPC client
* incorrectly tries to JSON-parse application/jsonl responses (substring
* match on application/json). The chat client already proxies to
* `did:web:api.bsky.chat`, so no manual proxy header is needed - it would
* otherwise be double-set.
*/
const res = await chatClient.fetchHandler(
'/xrpc/chat.bsky.actor.exportAccountData',
{headers: {'atproto-proxy': `${CHAT_PROXY_DID}#bsky_chat`}},
{},
)
if (!res.ok) {
throw new Error(`HTTP ${res.status}`)
@@ -83,7 +94,7 @@ export function ExportCarDialog({
} finally {
setLoading(false)
}
}, [l, agent])
}, [l, currentAccount, chatClient])
return (
<Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}>
+13 -11
View File
@@ -7,7 +7,7 @@ import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
import {logger} from '#/logger'
import {isSignupQueued, useAgent, useSessionApi} from '#/state/session'
import {isSignupQueued, usePdsClient, useSessionApi} from '#/state/session'
import {useOnboardingDispatch} from '#/state/shell'
import {Logo} from '#/view/icons/Logo'
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 {P, Text} from '#/components/Typography'
import {IS_IOS, IS_LIQUID_GLASS, IS_WEB} from '#/env'
import {com} from '#/lexicons'
const COL_WIDTH = 400
@@ -24,8 +25,8 @@ export function SignupQueued() {
const insets = useSafeAreaInsets()
const {gtMobile} = useBreakpoints()
const onboardingDispatch = useOnboardingDispatch()
const {logoutCurrentAccount} = useSessionApi()
const agent = useAgent()
const {logoutCurrentAccount, refreshSession} = useSessionApi()
const pdsClient = usePdsClient()
const [isProcessing, setProcessing] = useState(false)
const [estimatedTime, setEstimatedTime] = useState<string | undefined>(
@@ -38,18 +39,18 @@ export function SignupQueued() {
const checkStatus = useCallback(async () => {
setProcessing(true)
try {
const res = await agent.com.atproto.temp.checkSignupQueue()
if (res.data.activated) {
const res = await pdsClient.call(com.atproto.temp.checkSignupQueue)
if (res.activated) {
// ready to go, exchange the access token for a usable one and kick off onboarding
await agent.resumeSession()
if (!isSignupQueued(agent.session?.accessJwt)) {
const refreshed = await refreshSession()
if (!isSignupQueued(refreshed?.accessJwt)) {
onboardingDispatch({type: 'start'})
}
} else {
// not ready, update UI
setEstimatedTime(msToString(res.data.estimatedTimeMs))
if (typeof res.data.placeInQueue !== 'undefined') {
setPlaceInQueue(Math.max(res.data.placeInQueue, 1))
setEstimatedTime(msToString(res.estimatedTimeMs))
if (typeof res.placeInQueue !== 'undefined') {
setPlaceInQueue(Math.max(res.placeInQueue, 1))
}
}
} catch (e: any) {
@@ -62,7 +63,8 @@ export function SignupQueued() {
setEstimatedTime,
setPlaceInQueue,
onboardingDispatch,
agent,
pdsClient,
refreshSession,
])
useEffect(() => {
+4 -7
View File
@@ -19,10 +19,10 @@ import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as TextField from '#/components/forms/TextField'
import {SimpleInlineLinkText} from '#/components/Link'
import {Loader} from '#/components/Loader'
import {accountReportSubject} from '#/components/moderation/ReportDialog/utils/reportSubject'
import {P, Text} from '#/components/Typography'
import {IS_WEB} from '#/env'
import {com, tools} from '#/lexicons'
import {toLex} from '#/types/bsky'
const COL_WIDTH = 400
@@ -51,14 +51,11 @@ export function Takendown() {
if (!currentAccount) throw new Error('No session')
await pdsClient.call(
com.atproto.moderation.createReport,
toLex<com.atproto.moderation.createReport.$InputBody>({
{
reasonType: tools.ozone.report.defs.reasonAppeal.value,
subject: {
$type: 'com.atproto.admin.defs#repoRef',
did: currentAccount.did,
},
subject: accountReportSubject(currentAccount.did),
reason: appealText,
}),
},
{
service: api.moderation.service,
},