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
@@ -6,14 +6,14 @@ import {isJustAMute, moduiContainsHideableOffense} from '#/lib/moderation'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {STALE} from '#/state/queries'
import {DEFAULT_LOGGED_OUT_PREFERENCES} from '#/state/queries/preferences'
import {useAgent} from '#/state/session'
import {useAppviewClient} from '#/state/session'
import {
type AutocompleteApi,
type AutocompleteItem,
type AutocompleteItemType,
type AutocompleteProfile,
} from '#/components/Autocomplete/types'
import {toLex} from '#/types/bsky'
import {app} from '#/lexicons'
import {useEmojiSearch} from './useEmojiSearch'
const DEFAULT_MOD_OPTS = {
@@ -32,7 +32,7 @@ export function useAutocomplete({
limit?: number
showSearchFallback?: boolean
}): AutocompleteApi {
const agent = useAgent()
const appviewClient = useAppviewClient()
const moderationOpts = useModerationOpts()
const emojiSearch = useEmojiSearch()
@@ -53,17 +53,19 @@ export function useAutocomplete({
// Going from "foo" to "foo." should not clear matches.
q = q.toLowerCase().trim().replace(/\.$/, '')
const res = await agent.searchActorsTypeahead({
q,
limit: limit || 8,
})
const res = await appviewClient.call(
app.bsky.actor.searchActorsTypeahead,
{
q,
limit: limit || 8,
},
)
return (res?.data.actors || []).map(profile => ({
return (res?.actors || []).map(profile => ({
key: profile.did,
type: 'profile' as const,
value: '@' + profile.handle,
// emits #/lexicons views
profile: toLex<AutocompleteProfile['profile']>(profile),
profile,
}))
} else if (type === 'emoji') {
return emojiSearch(q, limit || 8)
@@ -16,7 +16,7 @@ import {cleanError} from '#/lib/strings/errors'
import {sanitizeHandle} from '#/lib/strings/handles'
import {updateProfileShadow} from '#/state/cache/profile-shadow'
import {RQKEY_getActivitySubscriptions} from '#/state/queries/activity-subscriptions'
import {useAgent} from '#/state/session'
import {useAppviewClient} from '#/state/session'
import {atoms as a, platform, useTheme, web} from '#/alf'
import {Admonition} from '#/components/Admonition'
import {
@@ -33,7 +33,7 @@ import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
import {IS_WEB} from '#/env'
import {type app} from '#/lexicons'
import {app} from '#/lexicons'
import type * as bsky from '#/types/bsky'
export function SubscribeProfileDialog({
@@ -71,7 +71,7 @@ function DialogInner({
const ax = useAnalytics()
const {_} = useLingui()
const t = useTheme()
const agent = useAgent()
const appviewClient = useAppviewClient()
const control = Dialog.useDialogContext()
const queryClient = useQueryClient()
const initialState = parseActivitySubscription(
@@ -119,7 +119,7 @@ function DialogInner({
mutationFn: async (
activitySubscription: Un$Typed<app.bsky.notification.defs.ActivitySubscription>,
) => {
await agent.app.bsky.notification.putActivitySubscription({
await appviewClient.call(app.bsky.notification.putActivitySubscription, {
subject: profile.did,
activitySubscription,
})
@@ -12,12 +12,12 @@ import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
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 {logger} from '#/ageAssurance'
import {useAnalytics} from '#/analytics'
import {com, tools} from '#/lexicons'
import {toLex} from '#/types/bsky'
export function AgeAssuranceAppealDialog({
control,
@@ -54,14 +54,11 @@ function Inner({control}: {control: Dialog.DialogControlProps}) {
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: `AGE_ASSURANCE_INQUIRY: ` + details,
}),
},
{
service: api.moderation.service,
},
@@ -15,7 +15,7 @@ import {
import {cleanError, isNetworkError} from '#/lib/strings/errors'
import {getErrorName} from '#/lib/xrpc-error'
import {logger} from '#/logger'
import {useAgent} from '#/state/session'
import {useAppviewClient} from '#/state/session'
import {OnboardingPosition} from '#/screens/Onboarding/Layout'
import {
android,
@@ -34,6 +34,7 @@ import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
import {useGeolocation} from '#/geolocation'
import {app} from '#/lexicons'
import {isFindContactsFeatureEnabled} from '../country-allowlist'
import {
constructFullPhoneNumber,
@@ -56,7 +57,7 @@ export function PhoneInput({
const {_} = useLingui()
const ax = useAnalytics()
const t = useTheme()
const agent = useAgent()
const appviewClient = useAppviewClient()
const location = useGeolocation()
const [countryCode, setCountryCode] = useState(
() => state.phoneCountryCode ?? getDefaultCountry(location),
@@ -78,7 +79,7 @@ export function PhoneInput({
phoneNumber: string
}) => {
// sends a onetime code to the user's phone number
await agent.app.bsky.contact.startPhoneVerification({
await appviewClient.call(app.bsky.contact.startPhoneVerification, {
phone: constructFullPhoneNumber(phoneCountryCode, phoneNumber),
})
},
@@ -9,7 +9,7 @@ import {clamp} from '#/lib/numbers'
import {cleanError, isNetworkError} from '#/lib/strings/errors'
import {getErrorName} from '#/lib/xrpc-error'
import {logger} from '#/logger'
import {useAgent} from '#/state/session'
import {useAppviewClient} from '#/state/session'
import {OnboardingPosition} from '#/screens/Onboarding/Layout'
import {atoms as a, useGutters, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
@@ -22,6 +22,7 @@ import {Loader} from '#/components/Loader'
import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
import {app} from '#/lexicons'
import {OTPInput} from '../components/OTPInput'
import {constructFullPhoneNumber, prettyPhoneNumber} from '../phone-number'
import {type Action, type State, useOnPressBackButton} from '../state'
@@ -40,7 +41,7 @@ export function VerifyNumber({
const t = useTheme()
const {_} = useLingui()
const ax = useAnalytics()
const agent = useAgent()
const appviewClient = useAppviewClient()
const gutters = useGutters([0, 'wide'])
const [otpCode, setOtpCode] = useState('')
@@ -69,8 +70,11 @@ export function VerifyNumber({
isSuccess,
} = useMutation({
mutationFn: async (code: string) => {
const res = await agent.app.bsky.contact.verifyPhone({code, phone})
return res.data.token
const res = await appviewClient.call(app.bsky.contact.verifyPhone, {
code,
phone,
})
return res.token
},
onSuccess: async token => {
// let the success state show for a moment
@@ -131,7 +135,9 @@ export function VerifyNumber({
const {mutate: resendCode, isPending: isResendingCode} = useMutation({
mutationFn: async () => {
await agent.app.bsky.contact.startPhoneVerification({phone: phone})
await appviewClient.call(app.bsky.contact.startPhoneVerification, {
phone: phone,
})
},
onSuccess: () => {
dispatch({type: 'RESEND_VERIFICATION_CODE'})
@@ -2,6 +2,7 @@ import {useCallback, useMemo, useRef, useState} from 'react'
import {View} from 'react-native'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import * as SMS from 'expo-sms'
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'
@@ -20,12 +21,7 @@ import {
optimisticRemoveMatch,
useMatchesPassthroughQuery,
} from '#/state/queries/find-contacts'
import {
useAgent,
useAppviewClient,
usePdsClient,
useSession,
} from '#/state/session'
import {useAppviewClient, usePdsClient, useSession} from '#/state/session'
import {List, type ListMethods} from '#/view/com/util/List'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {OnboardingPosition} from '#/screens/Onboarding/Layout'
@@ -46,6 +42,7 @@ import * as ProfileCard from '#/components/ProfileCard'
import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
import {app} from '#/lexicons'
import type * as bsky from '#/types/bsky'
import {InviteInfo} from '../components/InviteInfo'
import {type Action, type Contact, type Match, type State} from '../state'
@@ -94,7 +91,6 @@ export function ViewMatches({
const gutter = useGutters([0, 'wide'])
const moderationOpts = useModerationOpts()
const queryClient = useQueryClient()
const agent = useAgent()
const pdsClient = usePdsClient()
const appviewClient = useAppviewClient()
const insets = useSafeAreaInsets()
@@ -228,7 +224,9 @@ export function ViewMatches({
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: did => {
ax.metric('contacts:matches:dismiss', {entryPoint: context})
@@ -1,13 +1,15 @@
import {useMutation} from '@tanstack/react-query'
import {useAgent, useSession} from '#/state/session'
import {usePdsClient, useSession, useSessionApi} from '#/state/session'
import {com} from '#/lexicons'
export function useConfirmEmail({
onSuccess,
onError,
}: {onSuccess?: () => void; onError?: () => void} = {}) {
const agent = useAgent()
const pdsClient = usePdsClient()
const {currentAccount} = useSession()
const {refreshSession} = useSessionApi()
return useMutation({
mutationFn: async ({token}: {token: string}) => {
@@ -15,12 +17,12 @@ export function useConfirmEmail({
throw new Error('No email found for the current account')
}
await agent.com.atproto.server.confirmEmail({
await pdsClient.call(com.atproto.server.confirmEmail, {
email: currentAccount.email.trim(),
token: token.trim(),
})
// will update session state at root of app
await agent.resumeSession(agent.session!)
await refreshSession()
},
onSuccess,
onError,
@@ -1,10 +1,12 @@
import {useMutation} from '@tanstack/react-query'
import {useAgent, useSession} from '#/state/session'
import {usePdsClient, useSession, useSessionApi} from '#/state/session'
import {com} from '#/lexicons'
export function useManageEmail2FA() {
const agent = useAgent()
const pdsClient = usePdsClient()
const {currentAccount} = useSession()
const {refreshSession} = useSessionApi()
return useMutation({
mutationFn: async ({
@@ -17,13 +19,13 @@ export function useManageEmail2FA() {
throw new Error('No email found for the current account')
}
await agent.com.atproto.server.updateEmail({
await pdsClient.call(com.atproto.server.updateEmail, {
email: currentAccount.email,
emailAuthFactor: enabled,
token,
})
// will update session state at root of app
await agent.resumeSession(agent.session!)
await refreshSession()
},
})
}
@@ -1,13 +1,14 @@
import {useMutation} from '@tanstack/react-query'
import {useAgent} from '#/state/session'
import {usePdsClient} from '#/state/session'
import {com} from '#/lexicons'
export function useRequestEmailUpdate() {
const agent = useAgent()
const pdsClient = usePdsClient()
return useMutation({
mutationFn: async () => {
return (await agent.com.atproto.server.requestEmailUpdate()).data
return await pdsClient.call(com.atproto.server.requestEmailUpdate)
},
})
}
@@ -1,13 +1,14 @@
import {useMutation} from '@tanstack/react-query'
import {useAgent} from '#/state/session'
import {usePdsClient} from '#/state/session'
import {com} from '#/lexicons'
export function useRequestEmailVerification() {
const agent = useAgent()
const pdsClient = usePdsClient()
return useMutation({
mutationFn: async () => {
await agent.com.atproto.server.requestEmailConfirmation()
await pdsClient.call(com.atproto.server.requestEmailConfirmation)
},
})
}
@@ -1,19 +1,26 @@
import {type Client} from '@atproto/lex-client'
import {useMutation} from '@tanstack/react-query'
import {useAgent} from '#/state/session'
import {usePdsClient, useSessionApi} from '#/state/session'
import {useRequestEmailUpdate} from '#/components/dialogs/EmailDialog/data/useRequestEmailUpdate'
import {com} from '#/lexicons'
async function updateEmailAndRefreshSession(
agent: ReturnType<typeof useAgent>,
pdsClient: Client,
refreshSession: () => Promise<unknown>,
email: string,
token?: string,
) {
await agent.com.atproto.server.updateEmail({email: email.trim(), token})
await agent.resumeSession(agent.session!)
await pdsClient.call(com.atproto.server.updateEmail, {
email: email.trim(),
token,
})
await refreshSession()
}
export function useUpdateEmail() {
const agent = useAgent()
const pdsClient = usePdsClient()
const {refreshSession} = useSessionApi()
const {mutateAsync: requestEmailUpdate} = useRequestEmailUpdate()
return useMutation<
@@ -23,7 +30,12 @@ export function useUpdateEmail() {
>({
mutationFn: async ({email, token}: {email: string; token?: string}) => {
if (token) {
await updateEmailAndRefreshSession(agent, email, token)
await updateEmailAndRefreshSession(
pdsClient,
refreshSession,
email,
token,
)
return {
status: 'success',
}
@@ -34,7 +46,12 @@ export function useUpdateEmail() {
status: 'tokenRequired',
}
} else {
await updateEmailAndRefreshSession(agent, email, token)
await updateEmailAndRefreshSession(
pdsClient,
refreshSession,
email,
token,
)
return {
status: 'success',
}
@@ -4,7 +4,7 @@ import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
import {useAgent, useSession} from '#/state/session'
import {usePdsClient, useSession} from '#/state/session'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
@@ -16,6 +16,7 @@ import {useIntentDialogs} from '#/components/intents/IntentDialogs'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
import {IS_NATIVE} from '#/env'
import {com} from '#/lexicons'
export function VerifyEmailIntentDialog() {
const {verifyEmailDialogControl: control} = useIntentDialogs()
@@ -37,7 +38,7 @@ function Inner({}: {control: DialogControlProps}) {
'loading' | 'success' | 'failure' | 'resent'
>('loading')
const [sending, setSending] = useState(false)
const agent = useAgent()
const pdsClient = usePdsClient()
const {currentAccount} = useSession()
const {mutate: confirmEmail} = useConfirmEmail({
onSuccess: () => setStatus('success'),
@@ -52,7 +53,7 @@ function Inner({}: {control: DialogControlProps}) {
const onPressResendEmail = async () => {
setSending(true)
await agent.com.atproto.server.requestEmailConfirmation()
await pdsClient.call(com.atproto.server.requestEmailConfirmation)
setSending(false)
setStatus('resent')
}
+10 -11
View File
@@ -19,11 +19,14 @@ import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {InlineLinkText} from '#/components/Link'
import {Loader} from '#/components/Loader'
import {
accountReportSubject,
recordReportSubject,
} from '#/components/moderation/ReportDialog/utils/reportSubject'
import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography'
import {IS_ANDROID} from '#/env'
import {com, tools} from '#/lexicons'
import {toLex} from '#/types/bsky'
export function AppealForm({
label,
@@ -39,7 +42,6 @@ export function AppealForm({
const {gtMobile} = useBreakpoints()
const [details, setDetails] = useState('')
const {subject} = useLabelSubject({label})
const isAccountReport = 'did' in subject
const pdsClient = usePdsClient()
const sourceName = labeler
? sanitizeHandle(labeler.creator.handle, '@')
@@ -48,19 +50,16 @@ export function AppealForm({
const {mutate, isPending} = useMutation({
mutationFn: async () => {
const $type = !isAccountReport
? 'com.atproto.repo.strongRef'
: 'com.atproto.admin.defs#repoRef'
await pdsClient.call(
com.atproto.moderation.createReport,
toLex<com.atproto.moderation.createReport.$InputBody>({
{
reasonType: tools.ozone.report.defs.reasonAppeal.value,
subject: {
$type,
...subject,
},
subject:
'did' in subject
? accountReportSubject(subject.did)
: recordReportSubject(subject.uri, subject.cid),
reason: details,
}),
},
{
service: `${label.src}#atproto_labeler` as Service,
},
@@ -6,10 +6,14 @@ import {useMutation} from '@tanstack/react-query'
import {logger} from '#/logger'
import {usePdsClient} from '#/state/session'
import {com} from '#/lexicons'
import {toLex} from '#/types/bsky'
import {NEW_TO_OLD_REASONS_MAP} from './const'
import {type ReportState} from './state'
import {type ParsedReportSubject} from './types'
import {
accountReportSubject,
chatReportSubject,
recordReportSubject,
} from './utils/reportSubject'
type CreateReportBody = com.atproto.moderation.createReport.$InputBody
@@ -54,25 +58,20 @@ export function useSubmitReportMutation() {
/*
* The generated `createReport` subject union only declares repoRef and
* strongRef with branded did/uri strings; chat subjects (message/convo
* refs) are accepted on the wire but not in the lexicon, and the subject
* ids we hold here are plain strings. We build the body against a loose
* subject shape and `toLex` it to the schema body at the call boundary
* (matching the old widened-InputSchema shape).
* strongRef; chat subjects (message/convo refs) are accepted on the wire
* but not in the lexicon. The builders in `./utils/reportSubject` brand
* the plain-string ids into the schema `subject` slot, keeping the
* runtime values exact and confining the chat-subject assertion to one
* place.
*/
let report: Omit<CreateReportBody, 'subject'> & {
subject: {$type: string} & Record<string, unknown>
}
let report: CreateReportBody
switch (subject.type) {
case 'account': {
report = {
reasonType,
reason: state.details,
subject: {
$type: 'com.atproto.admin.defs#repoRef',
did: subject.did,
},
subject: accountReportSubject(subject.did),
}
break
}
@@ -84,11 +83,7 @@ export function useSubmitReportMutation() {
report = {
reasonType,
reason: state.details,
subject: {
$type: 'com.atproto.repo.strongRef',
uri: subject.uri,
cid: subject.cid,
},
subject: recordReportSubject(subject.uri, subject.cid),
}
break
}
@@ -96,12 +91,12 @@ export function useSubmitReportMutation() {
report = {
reasonType,
reason: state.details,
subject: {
subject: chatReportSubject({
$type: 'chat.bsky.convo.defs#messageRef',
messageId: subject.message.id,
convoId: subject.convoId,
did: subject.message.sender.did,
},
}),
}
break
}
@@ -109,11 +104,11 @@ export function useSubmitReportMutation() {
report = {
reasonType,
reason: state.details,
subject: {
subject: chatReportSubject({
$type: 'chat.bsky.convo.defs#convoRef',
convoId: subject.convoId,
did: subject.did,
},
}),
}
break
}
@@ -133,13 +128,9 @@ export function useSubmitReportMutation() {
* per-call `service` option (previously an explicit header on the
* bridge agent).
*/
await pdsClient.call(
com.atproto.moderation.createReport,
toLex<CreateReportBody>(report),
{
service: `${labeler.creator.did}#atproto_labeler` as Service,
},
)
await pdsClient.call(com.atproto.moderation.createReport, report, {
service: `${labeler.creator.did}#atproto_labeler` as Service,
})
}
},
})
@@ -0,0 +1,64 @@
import {
accountReportSubject,
chatReportSubject,
recordReportSubject,
} from '#/components/moderation/ReportDialog/utils/reportSubject'
/*
* These tests pin the wire shape of report subjects. The builders exist only
* to brand plain strings into the generated `createReport` union; the runtime
* object must stay byte-identical to the inline literals the consumers used
* before the migration. Each expected object is hand-written (not derived from
* the builder) so a shape change fails the test.
*/
describe('reportSubject builders', () => {
it('accountReportSubject produces a repoRef', () => {
expect(accountReportSubject('did:plc:abc123')).toEqual({
$type: 'com.atproto.admin.defs#repoRef',
did: 'did:plc:abc123',
})
})
it('recordReportSubject produces a strongRef', () => {
expect(
recordReportSubject(
'at://did:plc:abc123/app.bsky.feed.post/xyz',
'bafyreiexamplecid',
),
).toEqual({
$type: 'com.atproto.repo.strongRef',
uri: 'at://did:plc:abc123/app.bsky.feed.post/xyz',
cid: 'bafyreiexamplecid',
})
})
it('chatReportSubject preserves a messageRef verbatim', () => {
expect(
chatReportSubject({
$type: 'chat.bsky.convo.defs#messageRef',
messageId: 'msg-1',
convoId: 'convo-1',
did: 'did:plc:sender',
}),
).toEqual({
$type: 'chat.bsky.convo.defs#messageRef',
messageId: 'msg-1',
convoId: 'convo-1',
did: 'did:plc:sender',
})
})
it('chatReportSubject preserves a convoRef verbatim', () => {
expect(
chatReportSubject({
$type: 'chat.bsky.convo.defs#convoRef',
convoId: 'convo-1',
did: 'did:plc:owner',
}),
).toEqual({
$type: 'chat.bsky.convo.defs#convoRef',
convoId: 'convo-1',
did: 'did:plc:owner',
})
})
})
@@ -0,0 +1,63 @@
import {type com} from '#/lexicons'
/**
* The `subject` union of the generated `createReport` input body. The lexicon
* declares only `com.atproto.admin.defs#repoRef` and `com.atproto.repo.strongRef`
* with branded string fields (`did: l.DidString`, `uri: l.AtUriString`,
* `cid: l.CidString`).
*/
type ReportSubject = com.atproto.moderation.createReport.$InputBody['subject']
/** The repoRef arm of the subject union, carrying the branded `did`. */
type RepoRefSubject = Extract<
ReportSubject,
{$type: 'com.atproto.admin.defs#repoRef'}
>
/** The strongRef arm of the subject union, carrying the branded `uri`/`cid`. */
type StrongRefSubject = Extract<
ReportSubject,
{$type: 'com.atproto.repo.strongRef'}
>
/**
* Branded repoRef subject from a plain did string. The app holds dids as plain
* strings; brand the single field to the lexicon's `did` slot.
*/
export function accountReportSubject(did: string): ReportSubject {
return {
$type: 'com.atproto.admin.defs#repoRef',
did: did as RepoRefSubject['did'],
}
}
/**
* Branded strongRef subject from plain uri/cid strings. The app holds these as
* plain strings; brand `uri` to the lexicon's `uri` slot (`cid` is a plain
* string in the generated type, so it needs no assertion).
*/
export function recordReportSubject(uri: string, cid: string): ReportSubject {
return {
$type: 'com.atproto.repo.strongRef',
uri: uri as StrongRefSubject['uri'],
cid,
}
}
/**
* Chat subjects (messageRef/convoRef) are accepted by the moderation service on
* the wire but are not part of the `createReport` lexicon union. This is the ONE
* place that asserts them into the body type - keep the runtime value exact.
*/
export function chatReportSubject(
v:
| {
$type: 'chat.bsky.convo.defs#messageRef'
messageId: string
convoId: string
did: string
}
| {$type: 'chat.bsky.convo.defs#convoRef'; convoId: string; did: string},
): ReportSubject {
return v as unknown as ReportSubject
}
+14 -4
View File
@@ -1,10 +1,17 @@
import {type Insets, Platform} from 'react-native'
import {type Service} from '@atproto/lex-client'
import {api} from '@bsky.app/sdk'
import {type ProxyHeaderValue} from '#/state/session/agent'
import {BLUESKY_PROXY_DID, IS_DEV} from '#/env'
import {type app} from '#/lexicons'
/**
* The `atproto-proxy` header value: a DID plus a service fragment, e.g.
* `did:web:api.bsky.app#bsky_appview`. Kept local to this module (previously
* lived in the now-removed session `agent.ts` compat layer).
*/
type ProxyHeaderValue = `did:${string}:${string}#${string}`
export const LOCAL_DEV_SERVICE =
Platform.OS === 'android' ? 'http://10.0.2.2:2583' : 'http://localhost:2583'
export const STAGING_SERVICE = 'https://staging.bsky.dev'
@@ -251,9 +258,12 @@ export const BLUESKY_MOD_SERVICE_HEADERS = {
'atproto-proxy': `${api.moderation.did}#atproto_labeler`,
}
export const BLUESKY_NOTIF_SERVICE_HEADERS = {
'atproto-proxy': `${BLUESKY_PROXY_DID}#bsky_notif`,
}
/**
* Service proxy identifier for the notification/entryway service. Passed as the
* per-call `service` option on the account client so lex-client emits the
* `atproto-proxy` header (replaces the old `BLUESKY_NOTIF_SERVICE_HEADERS`).
*/
export const NOTIF_SERVICE = `${BLUESKY_PROXY_DID}#bsky_notif` as Service
export const webLinks = {
tos: `https://bsky.social/about/support/tos`,
@@ -0,0 +1,68 @@
import {Client} from '@atproto/lex-client'
import {describe, expect, it} from '@jest/globals'
import {NOTIF_SERVICE} from '#/lib/constants'
import {app} from '#/lexicons'
/*
* Proxy-emission guard for the push-notification register/unregister calls.
*
* registerPush/unregisterPush move from an explicit `atproto-proxy` header on
* the old bridge agent to a per-call `service` option on the lex account
* client (see notifications.ts). This test proves the notif service DID
* actually reaches the wire as the `atproto-proxy` header when that option is
* used, so a wrong proxy target cannot fail silently (design Risk #2).
*
* The technique mirrors clients-bundle-test.ts: build a Client over a fake
* `fetchHandler` agent (no session/native chain), issue a real `Client.call`
* with the same per-call `service: NOTIF_SERVICE` option the notification
* calls use, and assert the emitted request header. Procedure request bodies
* cannot be encoded under the jest CID interop, so the call uses a query - the
* `service` -> `atproto-proxy` header path is shared by queries and procedures
* alike, so this faithfully exercises what registerPush/unregisterPush emit.
*/
const DID = 'did:plc:example123'
const HANDLE = 'alice.test'
const SERVICE_ORIGIN = 'https://bsky.social'
function makeCapturingClient() {
const seen: {path: string; headers: Headers}[] = []
const client = new Client({
did: DID,
fetchHandler: (path, init) => {
seen.push({path, headers: new Headers(init.headers)})
return Promise.resolve(
new Response(JSON.stringify({did: DID, handle: HANDLE}), {
status: 200,
headers: {'content-type': 'application/json'},
}),
)
},
})
return {seen, client}
}
describe('notifications proxy emission', () => {
it('NOTIF_SERVICE targets the notif service fragment', () => {
/* the constant is the single source of the proxy DID reaching the wire */
expect(NOTIF_SERVICE).toMatch(/#bsky_notif$/)
})
it('emits atproto-proxy: <NOTIF_SERVICE> when the per-call service option is set', async () => {
const {seen, client} = makeCapturingClient()
await client
.call(
app.bsky.actor.getProfile,
{actor: HANDLE},
{service: NOTIF_SERVICE},
)
.catch(() => {})
expect(seen.length).toBe(1)
expect(seen[0].headers.get('atproto-proxy')).toBe(NOTIF_SERVICE)
/* the account origin is never the proxy target */
expect(seen[0].headers.get('atproto-proxy')).not.toContain(SERVICE_ORIGIN)
})
})
+31 -17
View File
@@ -2,34 +2,47 @@ import {useCallback, useEffect} from 'react'
import {Platform} from 'react-native'
import * as Notifications from 'expo-notifications'
import {getBadgeCountAsync, setBadgeCountAsync} from 'expo-notifications'
import {type Client} from '@atproto/lex-client'
import debounce from 'lodash.debounce'
import {
BLUESKY_NOTIF_SERVICE_HEADERS,
NOTIF_SERVICE,
PUBLIC_APPVIEW_DID,
PUBLIC_STAGING_APPVIEW_DID,
} from '#/lib/constants'
import {logger as notyLogger} from '#/lib/notifications/util'
import {isNetworkError} from '#/lib/strings/errors'
import {type SessionAccount, useAgent, useSession} from '#/state/session'
import {type SessionAgent} from '#/state/session/session-core'
import {type SessionAccount, usePdsClient, useSession} from '#/state/session'
import BackgroundNotificationHandler from '#/../modules/expo-background-notification-handler'
import {useAgeAssurance} from '#/ageAssurance'
import {useAnalytics} from '#/analytics'
import {IS_DEV, IS_NATIVE} from '#/env'
import {type app} from '#/lexicons'
import {app} from '#/lexicons'
/**
* A resumed throwaway account client paired with the account's service origin
* and handle. Produced by `createTemporaryClientsAndResume` (session util) and
* consumed by {@link unregisterPushToken}, which needs the service host to pick
* the correct appview DID and the handle for a debug log line without reaching
* into the session internals.
*/
export type TemporaryPushClient = {
client: Client
service: string
handle: string
}
/**
* @private
* Registers the device's push notification token with the Bluesky server.
*/
async function _registerPushToken({
agent,
client,
currentAccount,
token,
extra = {},
}: {
agent: SessionAgent
client: Client
currentAccount: SessionAccount
token: Notifications.DevicePushToken
extra?: {
@@ -49,8 +62,8 @@ async function _registerPushToken({
notyLogger.debug(`registerPushToken: registering`, {...payload})
await agent.app.bsky.notification.registerPush(payload, {
headers: BLUESKY_NOTIF_SERVICE_HEADERS,
await client.call(app.bsky.notification.registerPush, payload, {
service: NOTIF_SERVICE,
})
notyLogger.debug(`registerPushToken: success`)
@@ -75,7 +88,7 @@ const _registerPushTokenDebounced = debounce(_registerPushToken, 100)
* `_registerPushTokenDebounced` directly.
*/
export function useRegisterPushToken() {
const agent = useAgent()
const client = usePdsClient()
const {currentAccount} = useSession()
return useCallback(
@@ -88,7 +101,7 @@ export function useRegisterPushToken() {
}) => {
if (!currentAccount) return
return _registerPushTokenDebounced({
agent,
client,
currentAccount,
token,
extra: {
@@ -96,7 +109,7 @@ export function useRegisterPushToken() {
},
})
},
[agent, currentAccount],
[client, currentAccount],
)
}
@@ -327,16 +340,17 @@ export async function resetBadgeCount() {
await setBadgeCountAsync(0)
}
export async function unregisterPushToken(agents: SessionAgent[]) {
export async function unregisterPushToken(clients: TemporaryPushClient[]) {
if (!IS_NATIVE) return
try {
const token = await getPushToken()
if (token) {
for (const agent of agents) {
await agent.app.bsky.notification.unregisterPush(
for (const {client, service, handle} of clients) {
await client.call(
app.bsky.notification.unregisterPush,
{
serviceDid: agent.serviceUrl.hostname.includes('staging')
serviceDid: service.includes('staging')
? PUBLIC_STAGING_APPVIEW_DID
: PUBLIC_APPVIEW_DID,
platform: Platform.OS,
@@ -344,10 +358,10 @@ export async function unregisterPushToken(agents: SessionAgent[]) {
appId: 'xyz.blueskyweb.app',
},
{
headers: BLUESKY_NOTIF_SERVICE_HEADERS,
service: NOTIF_SERVICE,
},
)
notyLogger.debug(`Push token unregistered for ${agent.session?.handle}`)
notyLogger.debug(`Push token unregistered for ${handle}`)
}
} else {
notyLogger.debug('Tried to unregister push token, but could not find one')
+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,
},
+7 -4
View File
@@ -1,9 +1,10 @@
import {useMemo} from 'react'
import {setPersonalDetails} from '@bsky.app/sdk'
import {useMutation, useQueryClient} from '@tanstack/react-query'
import {restrictChatSettings} from '#/state/queries/messages/restrictChatSettings'
import {preferencesQueryKey} from '#/state/queries/preferences'
import {useAgent, usePdsClient, useSession} from '#/state/session'
import {usePdsClient, useSession} from '#/state/session'
import {usePatchAgeAssuranceOtherRequiredData} from '#/ageAssurance'
import {isUnderAge} from '#/ageAssurance/util'
import {IS_DEV} from '#/env'
@@ -54,14 +55,14 @@ export function useIsBirthdateUpdateAllowed() {
export function useBirthdateMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
const {currentAccount} = useSession()
const pdsClient = usePdsClient()
const patchOtherRequiredData = usePatchAgeAssuranceOtherRequiredData()
return useMutation<void, unknown, {birthDate: Date}>({
mutationFn: async ({birthDate}: {birthDate: Date}) => {
const bday = birthDate.toISOString()
await agent.setPersonalDetails({birthDate: bday})
await pdsClient.call(setPersonalDetails, {birthDate})
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
@@ -80,7 +81,9 @@ export function useBirthdateMutation() {
* birthdate, which may change the user's age assurance access level.
*/
void patchOtherRequiredData({birthdate: bday})
snoozeBirthdateUpdateAllowedForDid(agent.sessionManager.did!)
if (currentAccount) {
snoozeBirthdateUpdateAllowedForDid(currentAccount.did)
}
},
})
}
+7 -5
View File
@@ -5,9 +5,11 @@ import {
useEffect,
useState,
} from 'react'
import {type AtUriString} from '@atproto/syntax'
import * as persisted from '#/state/persisted'
import {useAgent, useSession} from '../session'
import {app} from '#/lexicons'
import {usePdsClient, useSession} from '../session'
type StateContext = Map<string, boolean>
type SetStateContext = (uri: string, value: boolean) => void
@@ -56,7 +58,7 @@ export function useSetThreadMute() {
}
function useMigrateMutes(setThreadMute: SetStateContext) {
const agent = useAgent()
const pdsClient = usePdsClient()
const {currentAccount} = useSession()
useEffect(() => {
@@ -87,8 +89,8 @@ function useMigrateMutes(setThreadMute: SetStateContext) {
setThreadMute(root, true)
await agent.api.app.bsky.graph
.muteThread({root})
await pdsClient
.call(app.bsky.graph.muteThread, {root: root as AtUriString})
// not a big deal if this fails, since the post might have been deleted
.catch(console.error)
}
@@ -100,5 +102,5 @@ function useMigrateMutes(setThreadMute: SetStateContext) {
cancelled = true
}
}
}, [agent, currentAccount, setThreadMute])
}, [pdsClient, currentAccount, setThreadMute])
}
+18 -12
View File
@@ -7,6 +7,7 @@ import {
useRef,
} from 'react'
import {AppState, type AppStateStatus} from 'react-native'
import {type Service} from '@atproto/lex-client'
import {type AtUriString} from '@atproto/syntax'
import throttle from 'lodash.throttle'
@@ -22,8 +23,8 @@ import {
} from '#/state/queries/post-feed'
import {getItemsForFeedback} from '#/view/com/posts/PostFeed'
import {useAnalytics} from '#/analytics'
import {type app} from '#/lexicons'
import {useAgent} from './session'
import {app} from '#/lexicons'
import {useAppviewClient} from './session'
export const FEEDBACK_FEEDS = [...PROD_FEEDS, ...STAGING_FEEDS]
@@ -66,7 +67,7 @@ export function useFeedFeedback(
) {
const ax = useAnalytics()
const logger = ax.logger.useChild(ax.logger.Context.FeedFeedback)
const agent = useAgent()
const appviewClient = useAppviewClient()
const feed =
!!feedSourceInfo && isFeedSourceFeedInfo(feedSourceInfo)
@@ -151,15 +152,20 @@ export function useFeedFeedback(
return
}
// Send to the feed
agent.app.bsky.feed
.sendInteractions(
{interactions: interactionsToSend, feed: feed?.uri},
/*
* Send to the feed generator via a per-call `service` option, which
* lex-client turns into the `atproto-proxy` header. This overrides the
* appview client's own service target for this one call.
*/
appviewClient
.call(
app.bsky.feed.sendInteractions,
{
encoding: 'application/json',
headers: {
'atproto-proxy': `${proxyDid}#bsky_fg`,
},
interactions: interactionsToSend,
feed: feed?.uri as AtUriString | undefined,
},
{
service: `${proxyDid}#bsky_fg` as Service,
},
)
.catch(() => {}) // ignore upstream errors
@@ -173,7 +179,7 @@ export function useFeedFeedback(
)
throttledFlushAggregatedStats()
logger.debug('flushed')
}, [agent, throttledFlushAggregatedStats, proxyDid, enabled, feed])
}, [appviewClient, throttledFlushAggregatedStats, proxyDid, enabled, feed])
const sendToFeed = useMemo(
() =>
+7 -24
View File
@@ -595,42 +595,25 @@ export class Convo {
this.convo = parseConvoView(convo, this.senderUserDid) ?? this.convo
if (this.convo) {
for (const member of this.convo.members) {
// `this.convo` comes from `parseConvoView` in the still-old-typed
// `#/components/dms/util` (migrates in a later task); bridge its member
// shape to the lexicon `ProfileViewBasic` we store. TODO(phase4): drop
// toLex once dms/util migrates.
this.relatedProfiles.set(
member.did,
bsky.toLex<chat.bsky.actor.defs.ProfileViewBasic>(member),
)
this.relatedProfiles.set(member.did, member)
}
}
this.applyProfileShadows()
}
/*
* The partial merges into `this.convo.view` and is re-parsed by the
* still-old-typed `parseConvoView` (`#/components/dms/util`, migrates in a
* later task), and its callers build it from old-typed `this.convo.details` /
* members. So this boundary stays in the old view world - typing the param
* off `ConvoWithDetails['view']` keeps it internally consistent without a
* per-call `toLex`. TODO(phase4): flip to `chat.bsky.convo.defs.ConvoView`
* once dms/util migrates.
* The partial merges into `this.convo.view` and is re-parsed by
* `parseConvoView`, and its callers build it from `this.convo.details` /
* members. The stored view is the lexicon `ConvoView`, so the partial is
* typed off it directly.
*/
private updateConvo(convo: Partial<ConvoWithDetails['view']>) {
private updateConvo(convo: Partial<chat.bsky.convo.defs.ConvoView>) {
if (this.convo) {
this.convo =
parseConvoView({...this.convo.view, ...convo}, this.senderUserDid) ??
this.convo
for (const member of this.convo.members) {
// `this.convo` comes from `parseConvoView` in the still-old-typed
// `#/components/dms/util` (migrates in a later task); bridge its member
// shape to the lexicon `ProfileViewBasic` we store. TODO(phase4): drop
// toLex once dms/util migrates.
this.relatedProfiles.set(
member.did,
bsky.toLex<chat.bsky.actor.defs.ProfileViewBasic>(member),
)
this.relatedProfiles.set(member.did, member)
}
this.applyProfileShadows()
}
+15 -12
View File
@@ -1,6 +1,8 @@
import {type AtIdentifierString} from '@atproto/syntax'
import {type QueryClient, useInfiniteQuery} from '@tanstack/react-query'
import {useAgent} from '#/state/session'
import {useAppviewClient} from '#/state/session'
import {app} from '#/lexicons'
export const RQKEY_ROOT = 'actor-starter-packs'
export const RQKEY_WITH_MEMBERSHIP_ROOT = 'actor-starter-packs-with-membership'
@@ -17,17 +19,16 @@ export function useActorStarterPacksQuery({
did?: string
enabled?: boolean
}) {
const agent = useAgent()
const appviewClient = useAppviewClient()
return useInfiniteQuery({
queryKey: RQKEY(did),
queryFn: async ({pageParam}: {pageParam?: string}) => {
const res = await agent.app.bsky.graph.getActorStarterPacks({
actor: did!,
return await appviewClient.call(app.bsky.graph.getActorStarterPacks, {
actor: did! as AtIdentifierString,
limit: 10,
cursor: pageParam,
})
return res.data
},
enabled: Boolean(did) && enabled,
initialPageParam: undefined,
@@ -42,17 +43,19 @@ export function useActorStarterPacksWithMembershipsQuery({
did?: string
enabled?: boolean
}) {
const agent = useAgent()
const appviewClient = useAppviewClient()
return useInfiniteQuery({
queryKey: RQKEY_WITH_MEMBERSHIP(did),
queryFn: async ({pageParam}: {pageParam?: string}) => {
const res = await agent.app.bsky.graph.getStarterPacksWithMembership({
actor: did!,
limit: 10,
cursor: pageParam,
})
return res.data
return await appviewClient.call(
app.bsky.graph.getStarterPacksWithMembership,
{
actor: did! as AtIdentifierString,
limit: 10,
cursor: pageParam,
},
)
},
enabled: Boolean(did) && enabled,
initialPageParam: undefined,
+21 -27
View File
@@ -1,3 +1,5 @@
import {Client} from '@atproto/lex-client'
import {type DatetimeString, type HandleString} from '@atproto/syntax'
import {useQuery} from '@tanstack/react-query'
import {
@@ -8,20 +10,8 @@ import {
import {useDebouncedValue} from '#/lib/hooks/useDebouncedValue'
import {createFullHandle} from '#/lib/strings/handles'
import {useAnalytics} from '#/analytics'
import {Agent} from '../session/agent'
/*
* `com.atproto.temp.checkHandleAvailability` is an entryway-only endpoint that
* isn't generated into `#/lexicons`, so we describe its result union locally.
* The response is discriminated by `$type`; we narrow against these shapes
* rather than a branded lexicon guard.
*/
type CheckHandleAvailabilityResult =
| {$type: 'com.atproto.temp.checkHandleAvailability#resultAvailable'}
| {
$type: 'com.atproto.temp.checkHandleAvailability#resultUnavailable'
suggestions: {handle: string; method: string}[]
}
import {com} from '#/lexicons'
import * as bsky from '#/types/bsky'
export const RQKEY_handleAvailability = (
handle: string,
@@ -90,24 +80,28 @@ export async function checkHandleAvailability(
},
) {
if (serviceDid === BSKY_SERVICE_DID) {
const agent = new Agent(null, {service: BSKY_SERVICE})
// entryway has a special API for handle availability
const {data} = await agent.com.atproto.temp.checkHandleAvailability({
handle,
birthDate,
const client = new Client({service: BSKY_SERVICE})
const data = await client.call(com.atproto.temp.checkHandleAvailability, {
handle: handle as HandleString,
birthDate: birthDate as DatetimeString | undefined,
email,
})
const result = data.result as CheckHandleAvailabilityResult
const result = data.result
if (
result.$type ===
'com.atproto.temp.checkHandleAvailability#resultAvailable'
bsky.isType(
com.atproto.temp.checkHandleAvailability.resultAvailable,
result,
)
) {
return {available: true} as const
} else if (
result.$type ===
'com.atproto.temp.checkHandleAvailability#resultUnavailable'
bsky.isType(
com.atproto.temp.checkHandleAvailability.resultUnavailable,
result,
)
) {
return {
available: false,
@@ -120,13 +114,13 @@ export async function checkHandleAvailability(
}
} else {
// 3rd party PDSes won't have this API so just try and resolve the handle
const agent = new Agent(null, {service: PUBLIC_BSKY_SERVICE})
const client = new Client({service: PUBLIC_BSKY_SERVICE})
try {
const res = await agent.resolveHandle({
handle,
const res = await client.call(com.atproto.identity.resolveHandle, {
handle: handle as HandleString,
})
if (res.data.did) {
if (res.did) {
return {available: false} as const
}
} catch {}
+21 -11
View File
@@ -1,8 +1,10 @@
import {useCallback} from 'react'
import {type AtIdentifierString, type HandleString} from '@atproto/syntax'
import {useMutation, useQueryClient} from '@tanstack/react-query'
import {STALE} from '#/state/queries'
import {useAgent} from '#/state/session'
import {useAppviewClient, usePdsClient} from '#/state/session'
import {app, com} from '#/lexicons'
const handleQueryKeyRoot = 'handle'
const fetchHandleQueryKey = (handleOrDid: string) => [
@@ -14,7 +16,7 @@ const fetchDidQueryKey = (handleOrDid: string) => [didQueryKeyRoot, handleOrDid]
export function useFetchHandle() {
const queryClient = useQueryClient()
const agent = useAgent()
const appviewClient = useAppviewClient()
return useCallback(
async (handleOrDid: string) => {
@@ -22,13 +24,16 @@ export function useFetchHandle() {
const res = await queryClient.fetchQuery({
staleTime: STALE.MINUTES.FIVE,
queryKey: fetchHandleQueryKey(handleOrDid),
queryFn: () => agent.getProfile({actor: handleOrDid}),
queryFn: () =>
appviewClient.call(app.bsky.actor.getProfile, {
actor: handleOrDid as AtIdentifierString,
}),
})
return res.data.handle
return res.handle
}
return handleOrDid
},
[queryClient, agent],
[queryClient, appviewClient],
)
}
@@ -36,11 +41,13 @@ export function useUpdateHandleMutation(opts?: {
onSuccess?: (handle: string) => void
}) {
const queryClient = useQueryClient()
const agent = useAgent()
const pdsClient = usePdsClient()
return useMutation({
mutationFn: async ({handle}: {handle: string}) => {
await agent.updateHandle({handle})
await pdsClient.call(com.atproto.identity.updateHandle, {
handle: handle as HandleString,
})
},
onSuccess(_data, variables) {
opts?.onSuccess?.(variables.handle)
@@ -53,7 +60,7 @@ export function useUpdateHandleMutation(opts?: {
export function useFetchDid() {
const queryClient = useQueryClient()
const agent = useAgent()
const appviewClient = useAppviewClient()
return useCallback(
async (handleOrDid: string) => {
@@ -63,13 +70,16 @@ export function useFetchDid() {
queryFn: async () => {
let identifier = handleOrDid
if (!identifier.startsWith('did:')) {
const res = await agent.resolveHandle({handle: identifier})
identifier = res.data.did
const res = await appviewClient.call(
com.atproto.identity.resolveHandle,
{handle: identifier as HandleString},
)
identifier = res.did
}
return identifier
},
})
},
[queryClient, agent],
[queryClient, appviewClient],
)
}
+10 -5
View File
@@ -1,22 +1,27 @@
import {type AtUriString} from '@atproto/syntax'
import {deleteLike, like} from '@bsky.app/sdk'
import {useMutation} from '@tanstack/react-query'
import {useAgent} from '#/state/session'
import {usePdsClient} from '#/state/session'
export function useLikeMutation() {
const agent = useAgent()
const pdsClient = usePdsClient()
return useMutation({
mutationFn: async ({uri, cid}: {uri: string; cid: string}) => {
const res = await agent.like(uri, cid)
const res = await pdsClient.call(like, {
uri: uri as AtUriString,
cid,
})
return {uri: res.uri}
},
})
}
export function useUnlikeMutation() {
const agent = useAgent()
const pdsClient = usePdsClient()
return useMutation({
mutationFn: async ({uri}: {uri: string}) => {
await agent.deleteLike(uri)
await pdsClient.call(deleteLike, uri as AtUriString)
},
})
}
@@ -745,17 +745,18 @@ export function ListConvosProviderInner({
/*
* `log.message` can also be a deleted-message view per the
* log union, which the strict MessageAndReactionView type
* rejects - the old types absorbed this via the open-union
* catch-all. Keep the pre-migration runtime behavior (always
* store the view we got) and assert the cache type.
* rejects - the raw ConvoView cache type only admits a live
* MessageView here. Keep the pre-migration runtime behavior
* (always store the view we got) and assert into the cache
* type; the runtime value is unchanged.
*/
lastReaction: bsky.toLex<
NonNullable<chat.bsky.convo.defs.ConvoView['lastReaction']>
>({
lastReaction: {
$type: 'chat.bsky.convo.defs#messageAndReactionView',
reaction: log.reaction,
message: log.message,
}),
} as NonNullable<
chat.bsky.convo.defs.ConvoView['lastReaction']
>,
rev: log.rev,
}
}),
+8 -7
View File
@@ -1,3 +1,4 @@
import {removeNuxs, upsertNux} from '@bsky.app/sdk'
import {useMutation, useQueryClient} from '@tanstack/react-query'
import {type AppNux, type Nux} from '#/state/queries/nuxs/definitions'
@@ -6,7 +7,7 @@ import {
preferencesQueryKey,
usePreferencesQuery,
} from '#/state/queries/preferences'
import {useAgent} from '#/state/session'
import {usePdsClient} from '#/state/session'
export {Nux} from '#/state/queries/nuxs/definitions'
@@ -42,11 +43,11 @@ export function useNuxs():
// if (__DEV__) {
// const queryClient = useQueryClient()
// const agent = useAgent()
// const pdsClient = usePdsClient()
// // @ts-ignore
// window.clearNux = async (ids: string[]) => {
// await agent.bskyAppRemoveNuxs(ids)
// await pdsClient.call(removeNuxs, ids)
// // triggers a refetch
// await queryClient.invalidateQueries({
// queryKey: preferencesQueryKey,
@@ -97,12 +98,12 @@ export function useNux<T extends Nux>(
export function useSaveNux() {
const queryClient = useQueryClient()
const agent = useAgent()
const pdsClient = usePdsClient()
return useMutation({
retry: 3,
mutationFn: async (nux: AppNux) => {
await agent.bskyAppUpsertNux(serializeAppNux(nux))
await pdsClient.call(upsertNux, serializeAppNux(nux))
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
@@ -113,12 +114,12 @@ export function useSaveNux() {
export function useResetNuxs() {
const queryClient = useQueryClient()
const agent = useAgent()
const pdsClient = usePdsClient()
return useMutation({
retry: 3,
mutationFn: async (ids: string[]) => {
await agent.bskyAppRemoveNuxs(ids)
await pdsClient.call(removeNuxs, ids)
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
+14 -4
View File
@@ -1,5 +1,7 @@
import {useState} from 'react'
import {type DidDocument, getPdsEndpoint} from '@atproto/common-web'
import {Client} from '@atproto/lex-client'
import {type HandleString} from '@atproto/syntax'
import {useQuery, useQueryClient} from '@tanstack/react-query'
import {DEFAULT_SERVICE, PUBLIC_BSKY_SERVICE} from '#/lib/constants'
@@ -7,7 +9,7 @@ import {useDebouncedValue} from '#/lib/hooks/useDebouncedValue'
import {isNetworkError} from '#/lib/strings/errors'
import {logger} from '#/logger'
import {STALE} from '#/state/queries'
import {Agent} from '#/state/session/agent'
import {com} from '#/lexicons'
const RQKEY_ROOT = 'pds-detection'
export const RQKEY = (identifier: string) => [RQKEY_ROOT, identifier]
@@ -147,16 +149,24 @@ export async function resolvePdsForIdentifier(
identifier: string,
): Promise<{did: string; pdsUrl: string | null} | null> {
const norm = normalizeIdentifier(identifier)
const agent = new Agent(null, {service: PUBLIC_BSKY_SERVICE})
/*
* Unauthenticated throwaway client pointed at the public appview -
* resolveHandle is a public read.
*/
const client = new Client({service: PUBLIC_BSKY_SERVICE})
try {
let did: string
if (norm.startsWith('did:')) {
did = norm
} else {
const res = await withResolveTimeout(signal =>
agent.resolveHandle({handle: norm}, {signal}),
client.call(
com.atproto.identity.resolveHandle,
{handle: norm as HandleString},
{signal},
),
)
did = res.data.did
did = res.did
}
logger.debug('pds-detection: resolved identifier to DID', {
identifier: norm,
+12 -5
View File
@@ -1,7 +1,8 @@
import {useQuery} from '@tanstack/react-query'
import {STALE} from '#/state/queries'
import {useAgent} from '#/state/session'
import {useAppviewClient} from '#/state/session'
import {app} from '#/lexicons'
type ServiceConfig = {
checkEmailConfirmed: boolean
@@ -13,18 +14,24 @@ type ServiceConfig = {
}
export function useServiceConfigQuery() {
const agent = useAgent()
const appviewClient = useAppviewClient()
return useQuery<ServiceConfig>({
refetchOnWindowFocus: true,
staleTime: STALE.MINUTES.FIVE,
queryKey: ['service-config'],
queryFn: async () => {
try {
const {data} = await agent.api.app.bsky.unspecced.getConfig()
const data = await appviewClient.call(app.bsky.unspecced.getConfig)
return {
checkEmailConfirmed: Boolean(data.checkEmailConfirmed),
// @ts-expect-error not included in types atm
topicsEnabled: Boolean(data.topicsEnabled),
/*
* `topicsEnabled` is served by the appview but is not (yet) part of
* the getConfig lexicon schema, so read it through a narrow local
* cast rather than the generated body type.
*/
topicsEnabled: Boolean(
(data as {topicsEnabled?: boolean}).topicsEnabled,
),
liveNow: data.liveNow ?? [],
}
} catch (e) {
@@ -7,16 +7,15 @@ import {
import {getContentLanguages} from '#/state/preferences/languages'
import {STALE} from '#/state/queries'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {useAgent} from '#/state/session'
import {type app} from '#/lexicons'
import {toLex} from '#/types/bsky'
import {useAppviewClient} from '#/state/session'
import {app} from '#/lexicons'
export const DEFAULT_LIMIT = 15
export const createGetSuggestedFeedsQueryKey = () => ['suggested-feeds']
export function useGetSuggestedFeedsQuery({enabled}: {enabled?: boolean}) {
const agent = useAgent()
const appviewClient = useAppviewClient()
const {data: preferences} = usePreferencesQuery()
const savedFeeds = preferences?.savedFeeds
@@ -26,7 +25,8 @@ export function useGetSuggestedFeedsQuery({enabled}: {enabled?: boolean}) {
queryKey: createGetSuggestedFeedsQueryKey(),
queryFn: async () => {
const contentLangs = getContentLanguages().join(',')
const {data} = await agent.app.bsky.unspecced.getSuggestedFeeds(
const data = await appviewClient.call(
app.bsky.unspecced.getSuggestedFeeds,
{
limit: DEFAULT_LIMIT,
},
@@ -38,17 +38,11 @@ export function useGetSuggestedFeedsQuery({enabled}: {enabled?: boolean}) {
},
)
/*
* TODO(phase4): drop toLex once getSuggestedFeeds migrates off the bridge
* agent (intentionally left on the bridge in Phase 3).
*/
return {
feeds: toLex<app.bsky.feed.defs.GeneratorView[]>(
data.feeds.filter(feed => {
const isSaved = !!savedFeeds?.find(s => s.value === feed.uri)
return !isSaved
}),
),
feeds: data.feeds.filter(feed => {
const isSaved = !!savedFeeds?.find(s => s.value === feed.uri)
return !isSaved
}),
}
},
})
@@ -5,9 +5,8 @@ import {logger} from '#/logger'
import {getContentLanguages} from '#/state/preferences/languages'
import {STALE} from '#/state/queries'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {useAgent} from '#/state/session'
import {type app} from '#/lexicons'
import {toLex} from '#/types/bsky'
import {useAppviewClient} from '#/state/session'
import {app} from '#/lexicons'
export type QueryProps = {
category?: string | null
@@ -28,7 +27,7 @@ export const createGetSuggestedOnboardingUsersQueryKey = (
]
export function useGetSuggestedOnboardingUsersQuery(props: QueryProps) {
const agent = useAgent()
const appviewClient = useAppviewClient()
const {data: preferences} = usePreferencesQuery()
return useQuery({
@@ -40,7 +39,8 @@ export function useGetSuggestedOnboardingUsersQuery(props: QueryProps) {
const overrideInterests = props.overrideInterests.join(',')
const {data} = await agent.app.bsky.unspecced.getSuggestedOnboardingUsers(
const data = await appviewClient.call(
app.bsky.unspecced.getSuggestedOnboardingUsers,
{
category: props.category ?? undefined,
limit: props.limit || 10,
@@ -56,15 +56,7 @@ export function useGetSuggestedOnboardingUsersQuery(props: QueryProps) {
if (!data.recIdStr) {
logger.debug('getSuggestedOnboardingUsers response missing recIdStr')
}
/*
* TODO(phase4): drop toLex once getSuggestedOnboardingUsers migrates off
* the bridge agent (this unspecced endpoint is intentionally left on the
* bridge in Phase 3, so it returns old `@atproto/api` view types).
*/
return toLex<{
actors: app.bsky.actor.defs.ProfileView[]
recId: string | undefined
}>({...data, recId: data.recIdStr})
return {...data, recId: data.recIdStr}
},
})
}
@@ -8,9 +8,8 @@ import {logger} from '#/logger'
import {getContentLanguages} from '#/state/preferences/languages'
import {STALE} from '#/state/queries'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {useAgent} from '#/state/session'
import {type app} from '#/lexicons'
import {toLex} from '#/types/bsky'
import {useAppviewClient} from '#/state/session'
import {app} from '#/lexicons'
export type QueryProps = {
limit?: number
@@ -23,7 +22,7 @@ export const createGetSuggestedUsersForDiscoverQueryKey = (props: {
}) => [getSuggestedUsersForDiscoverQueryKeyRoot, props.limit]
export function useGetSuggestedUsersForDiscoverQuery(props: QueryProps = {}) {
const agent = useAgent()
const appviewClient = useAppviewClient()
const {data: preferences} = usePreferencesQuery()
return useQuery({
@@ -33,30 +32,22 @@ export function useGetSuggestedUsersForDiscoverQuery(props: QueryProps = {}) {
const contentLangs = getContentLanguages().join(',')
const userInterests = aggregateUserInterests(preferences)
const {data} =
await agent.app.bsky.unspecced.getSuggestedUsersForDiscover(
{
limit: props.limit || 10,
const data = await appviewClient.call(
app.bsky.unspecced.getSuggestedUsersForDiscover,
{
limit: props.limit || 10,
},
{
headers: {
...createBskyTopicsHeader(userInterests),
'Accept-Language': contentLangs,
},
{
headers: {
...createBskyTopicsHeader(userInterests),
'Accept-Language': contentLangs,
},
},
)
},
)
if (!data.recIdStr) {
logger.debug('getSuggestedUsersForDiscover response missing recIdStr')
}
/*
* TODO(phase4): drop toLex once getSuggestedUsersForDiscover migrates off
* the bridge agent (this unspecced endpoint is intentionally left on the
* bridge in Phase 3, so it returns old `@atproto/api` view types).
*/
return toLex<{
actors: app.bsky.actor.defs.ProfileView[]
recId: string | undefined
}>({...data, recId: data.recIdStr})
return {...data, recId: data.recIdStr}
},
})
}
@@ -8,9 +8,8 @@ import {logger} from '#/logger'
import {getContentLanguages} from '#/state/preferences/languages'
import {STALE} from '#/state/queries'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {useAgent} from '#/state/session'
import {type app} from '#/lexicons'
import {toLex} from '#/types/bsky'
import {useAppviewClient} from '#/state/session'
import {app} from '#/lexicons'
export type QueryProps = {
category?: string | null
@@ -24,7 +23,7 @@ export const createGetSuggestedUsersForExploreQueryKey = (
) => [getSuggestedUsersForExploreQueryKeyRoot, props.category, props.limit]
export function useGetSuggestedUsersForExploreQuery(props: QueryProps = {}) {
const agent = useAgent()
const appviewClient = useAppviewClient()
const {data: preferences} = usePreferencesQuery()
return useQuery({
@@ -34,7 +33,8 @@ export function useGetSuggestedUsersForExploreQuery(props: QueryProps = {}) {
const contentLangs = getContentLanguages().join(',')
const userInterests = aggregateUserInterests(preferences)
const {data} = await agent.app.bsky.unspecced.getSuggestedUsersForExplore(
const data = await appviewClient.call(
app.bsky.unspecced.getSuggestedUsersForExplore,
{
category: props.category ?? undefined,
limit: props.limit || 10,
@@ -50,14 +50,7 @@ export function useGetSuggestedUsersForExploreQuery(props: QueryProps = {}) {
if (!data.recIdStr) {
logger.debug('getSuggestedUsersForExplore response missing recIdStr')
}
/*
* TODO(phase4): drop toLex once getSuggestedUsersForExplore migrates off
* the bridge agent (intentionally left on the bridge in Phase 3).
*/
return toLex<{
actors: app.bsky.actor.defs.ProfileView[]
recId: string | undefined
}>({...data, recId: data.recIdStr})
return {...data, recId: data.recIdStr}
},
})
}
@@ -8,9 +8,8 @@ import {logger} from '#/logger'
import {getContentLanguages} from '#/state/preferences/languages'
import {STALE} from '#/state/queries'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {useAgent} from '#/state/session'
import {type app} from '#/lexicons'
import {toLex} from '#/types/bsky'
import {useAppviewClient} from '#/state/session'
import {app} from '#/lexicons'
export type QueryProps = {
category?: string | null
@@ -26,7 +25,7 @@ export const createGetSuggestedUsersForSeeMoreQueryKey = (props: {
}) => [getSuggestedUsersForSeeMoreQueryKeyRoot, props.category, props.limit]
export function useGetSuggestedUsersForSeeMoreQuery(props: QueryProps = {}) {
const agent = useAgent()
const appviewClient = useAppviewClient()
const {data: preferences} = usePreferencesQuery()
return useQuery({
@@ -40,7 +39,8 @@ export function useGetSuggestedUsersForSeeMoreQuery(props: QueryProps = {}) {
const contentLangs = getContentLanguages().join(',')
const userInterests = aggregateUserInterests(preferences)
const {data} = await agent.app.bsky.unspecced.getSuggestedUsersForSeeMore(
const data = await appviewClient.call(
app.bsky.unspecced.getSuggestedUsersForSeeMore,
{
category: props.category ?? undefined,
limit: props.limit || 50,
@@ -56,14 +56,7 @@ export function useGetSuggestedUsersForSeeMoreQuery(props: QueryProps = {}) {
if (!data.recIdStr) {
logger.debug('getSuggestedUsersForSeeMore response missing recIdStr')
}
/*
* TODO(phase4): drop toLex once this unspecced endpoint migrates off the
* bridge agent (intentionally left on the bridge in Phase 3).
*/
return toLex<{
actors: app.bsky.actor.defs.ProfileView[]
recId: string | undefined
}>({...data, recId: data.recIdStr})
return {...data, recId: data.recIdStr}
},
})
}
@@ -7,9 +7,8 @@ import {
import {getContentLanguages} from '#/state/preferences/languages'
import {STALE} from '#/state/queries'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {useAgent} from '#/state/session'
import {type app} from '#/lexicons'
import {toLex} from '#/types/bsky'
import {useAppviewClient} from '#/state/session'
import {app} from '#/lexicons'
export const createOnboardingSuggestedStarterPacksQueryKey = (
interests?: string[],
@@ -22,7 +21,7 @@ export function useOnboardingSuggestedStarterPacksQuery({
enabled?: boolean
overrideInterests?: string[]
}) {
const agent = useAgent()
const appviewClient = useAppviewClient()
const {data: preferences} = usePreferencesQuery()
const contentLangs = getContentLanguages().join(',')
@@ -31,27 +30,19 @@ export function useOnboardingSuggestedStarterPacksQuery({
staleTime: STALE.MINUTES.THREE,
queryKey: createOnboardingSuggestedStarterPacksQueryKey(overrideInterests),
queryFn: async () => {
const {data} =
await agent.app.bsky.unspecced.getOnboardingSuggestedStarterPacks(
{limit: 6},
{
headers: {
...createBskyTopicsHeader(
overrideInterests
? overrideInterests.join(',')
: aggregateUserInterests(preferences),
),
'Accept-Language': contentLangs,
},
return await appviewClient.call(
app.bsky.unspecced.getOnboardingSuggestedStarterPacks,
{limit: 6},
{
headers: {
...createBskyTopicsHeader(
overrideInterests
? overrideInterests.join(',')
: aggregateUserInterests(preferences),
),
'Accept-Language': contentLangs,
},
)
/*
* TODO(phase4): drop toLex once getOnboardingSuggestedStarterPacks
* migrates off the bridge agent (intentionally left on the bridge in
* Phase 3).
*/
return toLex<app.bsky.unspecced.getOnboardingSuggestedStarterPacks.$OutputBody>(
data,
},
)
},
})
@@ -7,9 +7,8 @@ import {
import {getContentLanguages} from '#/state/preferences/languages'
import {STALE} from '#/state/queries'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {useAgent} from '#/state/session'
import {type app} from '#/lexicons'
import {toLex} from '#/types/bsky'
import {useAppviewClient} from '#/state/session'
import {app} from '#/lexicons'
export const createSuggestedStarterPacksQueryKey = (interests?: string[]) => [
'suggested-starter-packs',
@@ -23,7 +22,7 @@ export function useSuggestedStarterPacksQuery({
enabled?: boolean
overrideInterests?: string[]
}) {
const agent = useAgent()
const appviewClient = useAppviewClient()
const {data: preferences} = usePreferencesQuery()
const contentLangs = getContentLanguages().join(',')
@@ -32,8 +31,9 @@ export function useSuggestedStarterPacksQuery({
staleTime: STALE.MINUTES.THREE,
queryKey: createSuggestedStarterPacksQueryKey(overrideInterests),
queryFn: async () => {
const {data} = await agent.app.bsky.unspecced.getSuggestedStarterPacks(
undefined,
return await appviewClient.call(
app.bsky.unspecced.getSuggestedStarterPacks,
{},
{
headers: {
...createBskyTopicsHeader(
@@ -45,13 +45,6 @@ export function useSuggestedStarterPacksQuery({
},
},
)
/*
* TODO(phase4): drop toLex once getSuggestedStarterPacks migrates off the
* bridge agent (intentionally left on the bridge in Phase 3).
*/
return toLex<app.bsky.unspecced.getSuggestedStarterPacks.$OutputBody>(
data,
)
},
})
}
+14 -8
View File
@@ -1,12 +1,10 @@
import {Client} from '@atproto/lex-client'
import {PasswordSession} from '@atproto/lex-password-session'
import {isJwtExpired} from '#/lib/jwt'
import {type TemporaryPushClient} from '#/lib/notifications/notifications'
import * as persisted from '#/state/persisted'
import {
networkAwareFetch,
sessionAccountToSessionData,
SessionAgent,
} from './session-core'
import {networkAwareFetch, sessionAccountToSessionData} from './session-core'
import {type SessionAccount} from './types'
/*
@@ -33,22 +31,30 @@ export function isSessionExpired(account: SessionAccount) {
* Creates and resumes a throwaway session for every stored account.
* Intended to send push token revocations just before logout.
*
* Each returned {@link SessionAgent} wraps a temporary `PasswordSession`
* Each returned {@link TemporaryPushClient} wraps a temporary `PasswordSession`
* resumed over the network to obtain a valid access token. These sessions are
* deliberately hook-free (no `onUpdated`/`onDeleted`): they must NEVER persist
* or race the active session. They are used once for the unregister call and
* discarded (reclaimed by GC), so we never call `logout()` on them.
*
* Each session is wrapped in a plain account-shaped `Client` (no proxy header)
* paired with the account's service origin and handle, matching the contract
* {@link unregisterPushToken} consumes.
*/
export async function createTemporaryAgentsAndResume(
accounts: SessionAccount[],
): Promise<SessionAgent[]> {
): Promise<TemporaryPushClient[]> {
const settled = await Promise.allSettled(
accounts.map(async account => {
const session = await PasswordSession.resume(
sessionAccountToSessionData(account),
{fetch: networkAwareFetch},
)
return new SessionAgent(session)
return {
client: new Client(session),
service: session.session.service,
handle: session.session.handle,
} satisfies TemporaryPushClient
}),
)
+8 -5
View File
@@ -1,13 +1,14 @@
import {useCallback, useEffect, useState} from 'react'
import {View} from 'react-native'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {type AtIdentifierString} from '@atproto/syntax'
import {useLingui} from '@lingui/react/macro'
import {useQueryClient} from '@tanstack/react-query'
import {PressableScale} from '#/lib/custom-animations/PressableScale'
import {STALE} from '#/state/queries'
import {profilesQueryKey} from '#/state/queries/profile'
import {useAgent, useSession} from '#/state/session'
import {useAppviewClient, useSession} from '#/state/session'
import {useSetActiveLanding} from '#/state/shell/landing'
import {
useLoggedOutView,
@@ -23,6 +24,7 @@ import {atoms as a, native, tokens, useTheme} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button'
import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
import {useAnalytics} from '#/analytics'
import {app} from '#/lexicons'
import {SplashScreen} from './SplashScreen'
enum ScreenState {
@@ -65,7 +67,7 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
const queryClient = useQueryClient()
const {accounts} = useSession()
const agent = useAgent()
const appviewClient = useAppviewClient()
useEffect(() => {
const actors = accounts.map(acc => acc.did)
if (actors.length === 0) return
@@ -73,11 +75,12 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
queryKey: profilesQueryKey(actors),
staleTime: STALE.MINUTES.FIVE,
queryFn: async () => {
const res = await agent.getProfiles({actors})
return res.data
return await appviewClient.call(app.bsky.actor.getProfiles, {
actors: actors as AtIdentifierString[],
})
},
})
}, [accounts, agent, queryClient])
}, [accounts, appviewClient, queryClient])
const onPressDismiss = useCallback(() => {
if (onDismiss) {
+6 -3
View File
@@ -3,7 +3,7 @@ import {LogBox, Pressable, TextInput, View} from 'react-native'
import {useQueryClient} from '@tanstack/react-query'
import {BLUESKY_PROXY_HEADER} from '#/lib/constants'
import {useAgent, useSessionApi} from '#/state/session'
import {useSessionApi} from '#/state/session'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {useOnboardingDispatch} from '#/state/shell/onboarding'
import {navigate} from '../../../Navigation'
@@ -31,7 +31,6 @@ const BTN = {height: 1, width: 1, backgroundColor: 'red'}
let hasConfiguredProxy = false
export function TestCtrls() {
const agent = useAgent()
const queryClient = useQueryClient()
const {logoutEveryAccount, login} = useSessionApi()
const onboardingDispatch = useOnboardingDispatch()
@@ -74,8 +73,12 @@ export function TestCtrls() {
autoCapitalize="none"
onSubmitEditing={() => {
const header = `${proxyHeader}#bsky_appview`
/*
* The appview lex client reads BLUESKY_PROXY_HEADER.get() at build
* time (see clients.ts), so setting it here retargets the proxy for
* subsequent sign-ins without an explicit client reconfigure.
*/
BLUESKY_PROXY_HEADER.set(header)
agent.configureProxy(header as any)
hasConfiguredProxy = true
setIsProxyConfigured(true)
}}