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 {useModerationOpts} from '#/state/preferences/moderation-opts'
import {STALE} from '#/state/queries' import {STALE} from '#/state/queries'
import {DEFAULT_LOGGED_OUT_PREFERENCES} from '#/state/queries/preferences' import {DEFAULT_LOGGED_OUT_PREFERENCES} from '#/state/queries/preferences'
import {useAgent} from '#/state/session' import {useAppviewClient} from '#/state/session'
import { import {
type AutocompleteApi, type AutocompleteApi,
type AutocompleteItem, type AutocompleteItem,
type AutocompleteItemType, type AutocompleteItemType,
type AutocompleteProfile, type AutocompleteProfile,
} from '#/components/Autocomplete/types' } from '#/components/Autocomplete/types'
import {toLex} from '#/types/bsky' import {app} from '#/lexicons'
import {useEmojiSearch} from './useEmojiSearch' import {useEmojiSearch} from './useEmojiSearch'
const DEFAULT_MOD_OPTS = { const DEFAULT_MOD_OPTS = {
@@ -32,7 +32,7 @@ export function useAutocomplete({
limit?: number limit?: number
showSearchFallback?: boolean showSearchFallback?: boolean
}): AutocompleteApi { }): AutocompleteApi {
const agent = useAgent() const appviewClient = useAppviewClient()
const moderationOpts = useModerationOpts() const moderationOpts = useModerationOpts()
const emojiSearch = useEmojiSearch() const emojiSearch = useEmojiSearch()
@@ -53,17 +53,19 @@ export function useAutocomplete({
// Going from "foo" to "foo." should not clear matches. // Going from "foo" to "foo." should not clear matches.
q = q.toLowerCase().trim().replace(/\.$/, '') q = q.toLowerCase().trim().replace(/\.$/, '')
const res = await agent.searchActorsTypeahead({ const res = await appviewClient.call(
q, app.bsky.actor.searchActorsTypeahead,
limit: limit || 8, {
}) q,
limit: limit || 8,
},
)
return (res?.data.actors || []).map(profile => ({ return (res?.actors || []).map(profile => ({
key: profile.did, key: profile.did,
type: 'profile' as const, type: 'profile' as const,
value: '@' + profile.handle, value: '@' + profile.handle,
// emits #/lexicons views profile,
profile: toLex<AutocompleteProfile['profile']>(profile),
})) }))
} else if (type === 'emoji') { } else if (type === 'emoji') {
return emojiSearch(q, limit || 8) return emojiSearch(q, limit || 8)
@@ -16,7 +16,7 @@ import {cleanError} from '#/lib/strings/errors'
import {sanitizeHandle} from '#/lib/strings/handles' import {sanitizeHandle} from '#/lib/strings/handles'
import {updateProfileShadow} from '#/state/cache/profile-shadow' import {updateProfileShadow} from '#/state/cache/profile-shadow'
import {RQKEY_getActivitySubscriptions} from '#/state/queries/activity-subscriptions' 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 {atoms as a, platform, useTheme, web} from '#/alf'
import {Admonition} from '#/components/Admonition' import {Admonition} from '#/components/Admonition'
import { import {
@@ -33,7 +33,7 @@ import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics' import {useAnalytics} from '#/analytics'
import {IS_WEB} from '#/env' import {IS_WEB} from '#/env'
import {type app} from '#/lexicons' import {app} from '#/lexicons'
import type * as bsky from '#/types/bsky' import type * as bsky from '#/types/bsky'
export function SubscribeProfileDialog({ export function SubscribeProfileDialog({
@@ -71,7 +71,7 @@ function DialogInner({
const ax = useAnalytics() const ax = useAnalytics()
const {_} = useLingui() const {_} = useLingui()
const t = useTheme() const t = useTheme()
const agent = useAgent() const appviewClient = useAppviewClient()
const control = Dialog.useDialogContext() const control = Dialog.useDialogContext()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const initialState = parseActivitySubscription( const initialState = parseActivitySubscription(
@@ -119,7 +119,7 @@ function DialogInner({
mutationFn: async ( mutationFn: async (
activitySubscription: Un$Typed<app.bsky.notification.defs.ActivitySubscription>, activitySubscription: Un$Typed<app.bsky.notification.defs.ActivitySubscription>,
) => { ) => {
await agent.app.bsky.notification.putActivitySubscription({ await appviewClient.call(app.bsky.notification.putActivitySubscription, {
subject: profile.did, subject: profile.did,
activitySubscription, activitySubscription,
}) })
@@ -12,12 +12,12 @@ import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge'
import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog' import * as Dialog from '#/components/Dialog'
import {Loader} from '#/components/Loader' import {Loader} from '#/components/Loader'
import {accountReportSubject} from '#/components/moderation/ReportDialog/utils/reportSubject'
import * as Toast from '#/components/Toast' import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {logger} from '#/ageAssurance' import {logger} from '#/ageAssurance'
import {useAnalytics} from '#/analytics' import {useAnalytics} from '#/analytics'
import {com, tools} from '#/lexicons' import {com, tools} from '#/lexicons'
import {toLex} from '#/types/bsky'
export function AgeAssuranceAppealDialog({ export function AgeAssuranceAppealDialog({
control, control,
@@ -54,14 +54,11 @@ function Inner({control}: {control: Dialog.DialogControlProps}) {
await pdsClient.call( await pdsClient.call(
com.atproto.moderation.createReport, com.atproto.moderation.createReport,
toLex<com.atproto.moderation.createReport.$InputBody>({ {
reasonType: tools.ozone.report.defs.reasonAppeal.value, reasonType: tools.ozone.report.defs.reasonAppeal.value,
subject: { subject: accountReportSubject(currentAccount?.did ?? ''),
$type: 'com.atproto.admin.defs#repoRef',
did: currentAccount?.did,
},
reason: `AGE_ASSURANCE_INQUIRY: ` + details, reason: `AGE_ASSURANCE_INQUIRY: ` + details,
}), },
{ {
service: api.moderation.service, service: api.moderation.service,
}, },
@@ -15,7 +15,7 @@ import {
import {cleanError, isNetworkError} from '#/lib/strings/errors' import {cleanError, isNetworkError} from '#/lib/strings/errors'
import {getErrorName} from '#/lib/xrpc-error' import {getErrorName} from '#/lib/xrpc-error'
import {logger} from '#/logger' import {logger} from '#/logger'
import {useAgent} from '#/state/session' import {useAppviewClient} from '#/state/session'
import {OnboardingPosition} from '#/screens/Onboarding/Layout' import {OnboardingPosition} from '#/screens/Onboarding/Layout'
import { import {
android, android,
@@ -34,6 +34,7 @@ import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics' import {useAnalytics} from '#/analytics'
import {useGeolocation} from '#/geolocation' import {useGeolocation} from '#/geolocation'
import {app} from '#/lexicons'
import {isFindContactsFeatureEnabled} from '../country-allowlist' import {isFindContactsFeatureEnabled} from '../country-allowlist'
import { import {
constructFullPhoneNumber, constructFullPhoneNumber,
@@ -56,7 +57,7 @@ export function PhoneInput({
const {_} = useLingui() const {_} = useLingui()
const ax = useAnalytics() const ax = useAnalytics()
const t = useTheme() const t = useTheme()
const agent = useAgent() const appviewClient = useAppviewClient()
const location = useGeolocation() const location = useGeolocation()
const [countryCode, setCountryCode] = useState( const [countryCode, setCountryCode] = useState(
() => state.phoneCountryCode ?? getDefaultCountry(location), () => state.phoneCountryCode ?? getDefaultCountry(location),
@@ -78,7 +79,7 @@ export function PhoneInput({
phoneNumber: string phoneNumber: string
}) => { }) => {
// sends a onetime code to the user's phone number // 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), phone: constructFullPhoneNumber(phoneCountryCode, phoneNumber),
}) })
}, },
@@ -9,7 +9,7 @@ import {clamp} from '#/lib/numbers'
import {cleanError, isNetworkError} from '#/lib/strings/errors' import {cleanError, isNetworkError} from '#/lib/strings/errors'
import {getErrorName} from '#/lib/xrpc-error' import {getErrorName} from '#/lib/xrpc-error'
import {logger} from '#/logger' import {logger} from '#/logger'
import {useAgent} from '#/state/session' import {useAppviewClient} from '#/state/session'
import {OnboardingPosition} from '#/screens/Onboarding/Layout' import {OnboardingPosition} from '#/screens/Onboarding/Layout'
import {atoms as a, useGutters, useTheme} from '#/alf' import {atoms as a, useGutters, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {Button, ButtonIcon, ButtonText} from '#/components/Button'
@@ -22,6 +22,7 @@ import {Loader} from '#/components/Loader'
import * as Toast from '#/components/Toast' import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics' import {useAnalytics} from '#/analytics'
import {app} from '#/lexicons'
import {OTPInput} from '../components/OTPInput' import {OTPInput} from '../components/OTPInput'
import {constructFullPhoneNumber, prettyPhoneNumber} from '../phone-number' import {constructFullPhoneNumber, prettyPhoneNumber} from '../phone-number'
import {type Action, type State, useOnPressBackButton} from '../state' import {type Action, type State, useOnPressBackButton} from '../state'
@@ -40,7 +41,7 @@ export function VerifyNumber({
const t = useTheme() const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
const ax = useAnalytics() const ax = useAnalytics()
const agent = useAgent() const appviewClient = useAppviewClient()
const gutters = useGutters([0, 'wide']) const gutters = useGutters([0, 'wide'])
const [otpCode, setOtpCode] = useState('') const [otpCode, setOtpCode] = useState('')
@@ -69,8 +70,11 @@ export function VerifyNumber({
isSuccess, isSuccess,
} = useMutation({ } = useMutation({
mutationFn: async (code: string) => { mutationFn: async (code: string) => {
const res = await agent.app.bsky.contact.verifyPhone({code, phone}) const res = await appviewClient.call(app.bsky.contact.verifyPhone, {
return res.data.token code,
phone,
})
return res.token
}, },
onSuccess: async token => { onSuccess: async token => {
// let the success state show for a moment // let the success state show for a moment
@@ -131,7 +135,9 @@ export function VerifyNumber({
const {mutate: resendCode, isPending: isResendingCode} = useMutation({ const {mutate: resendCode, isPending: isResendingCode} = useMutation({
mutationFn: async () => { mutationFn: async () => {
await agent.app.bsky.contact.startPhoneVerification({phone: phone}) await appviewClient.call(app.bsky.contact.startPhoneVerification, {
phone: phone,
})
}, },
onSuccess: () => { onSuccess: () => {
dispatch({type: 'RESEND_VERIFICATION_CODE'}) dispatch({type: 'RESEND_VERIFICATION_CODE'})
@@ -2,6 +2,7 @@ import {useCallback, useMemo, useRef, useState} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {useSafeAreaInsets} from 'react-native-safe-area-context' import {useSafeAreaInsets} from 'react-native-safe-area-context'
import * as SMS from 'expo-sms' import * as SMS from 'expo-sms'
import {type DidString} from '@atproto/syntax'
import {type ModerationOpts} from '@bsky.app/sdk/moderation' import {type ModerationOpts} from '@bsky.app/sdk/moderation'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
@@ -20,12 +21,7 @@ import {
optimisticRemoveMatch, optimisticRemoveMatch,
useMatchesPassthroughQuery, useMatchesPassthroughQuery,
} from '#/state/queries/find-contacts' } from '#/state/queries/find-contacts'
import { import {useAppviewClient, usePdsClient, useSession} from '#/state/session'
useAgent,
useAppviewClient,
usePdsClient,
useSession,
} from '#/state/session'
import {List, type ListMethods} from '#/view/com/util/List' import {List, type ListMethods} from '#/view/com/util/List'
import {UserAvatar} from '#/view/com/util/UserAvatar' import {UserAvatar} from '#/view/com/util/UserAvatar'
import {OnboardingPosition} from '#/screens/Onboarding/Layout' import {OnboardingPosition} from '#/screens/Onboarding/Layout'
@@ -46,6 +42,7 @@ import * as ProfileCard from '#/components/ProfileCard'
import * as Toast from '#/components/Toast' import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics' import {useAnalytics} from '#/analytics'
import {app} from '#/lexicons'
import type * as bsky from '#/types/bsky' import type * as bsky from '#/types/bsky'
import {InviteInfo} from '../components/InviteInfo' import {InviteInfo} from '../components/InviteInfo'
import {type Action, type Contact, type Match, type State} from '../state' import {type Action, type Contact, type Match, type State} from '../state'
@@ -94,7 +91,6 @@ export function ViewMatches({
const gutter = useGutters([0, 'wide']) const gutter = useGutters([0, 'wide'])
const moderationOpts = useModerationOpts() const moderationOpts = useModerationOpts()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const agent = useAgent()
const pdsClient = usePdsClient() const pdsClient = usePdsClient()
const appviewClient = useAppviewClient() const appviewClient = useAppviewClient()
const insets = useSafeAreaInsets() const insets = useSafeAreaInsets()
@@ -228,7 +224,9 @@ export function ViewMatches({
const {mutate: dismissMatch} = useMutation({ const {mutate: dismissMatch} = useMutation({
mutationFn: async (did: string) => { 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 => { onMutate: did => {
ax.metric('contacts:matches:dismiss', {entryPoint: context}) ax.metric('contacts:matches:dismiss', {entryPoint: context})
@@ -1,13 +1,15 @@
import {useMutation} from '@tanstack/react-query' 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({ export function useConfirmEmail({
onSuccess, onSuccess,
onError, onError,
}: {onSuccess?: () => void; onError?: () => void} = {}) { }: {onSuccess?: () => void; onError?: () => void} = {}) {
const agent = useAgent() const pdsClient = usePdsClient()
const {currentAccount} = useSession() const {currentAccount} = useSession()
const {refreshSession} = useSessionApi()
return useMutation({ return useMutation({
mutationFn: async ({token}: {token: string}) => { mutationFn: async ({token}: {token: string}) => {
@@ -15,12 +17,12 @@ export function useConfirmEmail({
throw new Error('No email found for the current account') 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(), email: currentAccount.email.trim(),
token: token.trim(), token: token.trim(),
}) })
// will update session state at root of app // will update session state at root of app
await agent.resumeSession(agent.session!) await refreshSession()
}, },
onSuccess, onSuccess,
onError, onError,
@@ -1,10 +1,12 @@
import {useMutation} from '@tanstack/react-query' 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() { export function useManageEmail2FA() {
const agent = useAgent() const pdsClient = usePdsClient()
const {currentAccount} = useSession() const {currentAccount} = useSession()
const {refreshSession} = useSessionApi()
return useMutation({ return useMutation({
mutationFn: async ({ mutationFn: async ({
@@ -17,13 +19,13 @@ export function useManageEmail2FA() {
throw new Error('No email found for the current account') 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, email: currentAccount.email,
emailAuthFactor: enabled, emailAuthFactor: enabled,
token, token,
}) })
// will update session state at root of app // 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 {useMutation} from '@tanstack/react-query'
import {useAgent} from '#/state/session' import {usePdsClient} from '#/state/session'
import {com} from '#/lexicons'
export function useRequestEmailUpdate() { export function useRequestEmailUpdate() {
const agent = useAgent() const pdsClient = usePdsClient()
return useMutation({ return useMutation({
mutationFn: async () => { 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 {useMutation} from '@tanstack/react-query'
import {useAgent} from '#/state/session' import {usePdsClient} from '#/state/session'
import {com} from '#/lexicons'
export function useRequestEmailVerification() { export function useRequestEmailVerification() {
const agent = useAgent() const pdsClient = usePdsClient()
return useMutation({ return useMutation({
mutationFn: async () => { 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 {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 {useRequestEmailUpdate} from '#/components/dialogs/EmailDialog/data/useRequestEmailUpdate'
import {com} from '#/lexicons'
async function updateEmailAndRefreshSession( async function updateEmailAndRefreshSession(
agent: ReturnType<typeof useAgent>, pdsClient: Client,
refreshSession: () => Promise<unknown>,
email: string, email: string,
token?: string, token?: string,
) { ) {
await agent.com.atproto.server.updateEmail({email: email.trim(), token}) await pdsClient.call(com.atproto.server.updateEmail, {
await agent.resumeSession(agent.session!) email: email.trim(),
token,
})
await refreshSession()
} }
export function useUpdateEmail() { export function useUpdateEmail() {
const agent = useAgent() const pdsClient = usePdsClient()
const {refreshSession} = useSessionApi()
const {mutateAsync: requestEmailUpdate} = useRequestEmailUpdate() const {mutateAsync: requestEmailUpdate} = useRequestEmailUpdate()
return useMutation< return useMutation<
@@ -23,7 +30,12 @@ export function useUpdateEmail() {
>({ >({
mutationFn: async ({email, token}: {email: string; token?: string}) => { mutationFn: async ({email, token}: {email: string; token?: string}) => {
if (token) { if (token) {
await updateEmailAndRefreshSession(agent, email, token) await updateEmailAndRefreshSession(
pdsClient,
refreshSession,
email,
token,
)
return { return {
status: 'success', status: 'success',
} }
@@ -34,7 +46,12 @@ export function useUpdateEmail() {
status: 'tokenRequired', status: 'tokenRequired',
} }
} else { } else {
await updateEmailAndRefreshSession(agent, email, token) await updateEmailAndRefreshSession(
pdsClient,
refreshSession,
email,
token,
)
return { return {
status: 'success', status: 'success',
} }
@@ -4,7 +4,7 @@ import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro' 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 {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog' import * as Dialog from '#/components/Dialog'
@@ -16,6 +16,7 @@ import {useIntentDialogs} from '#/components/intents/IntentDialogs'
import {Loader} from '#/components/Loader' import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {IS_NATIVE} from '#/env' import {IS_NATIVE} from '#/env'
import {com} from '#/lexicons'
export function VerifyEmailIntentDialog() { export function VerifyEmailIntentDialog() {
const {verifyEmailDialogControl: control} = useIntentDialogs() const {verifyEmailDialogControl: control} = useIntentDialogs()
@@ -37,7 +38,7 @@ function Inner({}: {control: DialogControlProps}) {
'loading' | 'success' | 'failure' | 'resent' 'loading' | 'success' | 'failure' | 'resent'
>('loading') >('loading')
const [sending, setSending] = useState(false) const [sending, setSending] = useState(false)
const agent = useAgent() const pdsClient = usePdsClient()
const {currentAccount} = useSession() const {currentAccount} = useSession()
const {mutate: confirmEmail} = useConfirmEmail({ const {mutate: confirmEmail} = useConfirmEmail({
onSuccess: () => setStatus('success'), onSuccess: () => setStatus('success'),
@@ -52,7 +53,7 @@ function Inner({}: {control: DialogControlProps}) {
const onPressResendEmail = async () => { const onPressResendEmail = async () => {
setSending(true) setSending(true)
await agent.com.atproto.server.requestEmailConfirmation() await pdsClient.call(com.atproto.server.requestEmailConfirmation)
setSending(false) setSending(false)
setStatus('resent') setStatus('resent')
} }
+10 -11
View File
@@ -19,11 +19,14 @@ import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog' import * as Dialog from '#/components/Dialog'
import {InlineLinkText} from '#/components/Link' import {InlineLinkText} from '#/components/Link'
import {Loader} from '#/components/Loader' import {Loader} from '#/components/Loader'
import {
accountReportSubject,
recordReportSubject,
} from '#/components/moderation/ReportDialog/utils/reportSubject'
import * as Toast from '#/components/Toast' import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {IS_ANDROID} from '#/env' import {IS_ANDROID} from '#/env'
import {com, tools} from '#/lexicons' import {com, tools} from '#/lexicons'
import {toLex} from '#/types/bsky'
export function AppealForm({ export function AppealForm({
label, label,
@@ -39,7 +42,6 @@ export function AppealForm({
const {gtMobile} = useBreakpoints() const {gtMobile} = useBreakpoints()
const [details, setDetails] = useState('') const [details, setDetails] = useState('')
const {subject} = useLabelSubject({label}) const {subject} = useLabelSubject({label})
const isAccountReport = 'did' in subject
const pdsClient = usePdsClient() const pdsClient = usePdsClient()
const sourceName = labeler const sourceName = labeler
? sanitizeHandle(labeler.creator.handle, '@') ? sanitizeHandle(labeler.creator.handle, '@')
@@ -48,19 +50,16 @@ export function AppealForm({
const {mutate, isPending} = useMutation({ const {mutate, isPending} = useMutation({
mutationFn: async () => { mutationFn: async () => {
const $type = !isAccountReport
? 'com.atproto.repo.strongRef'
: 'com.atproto.admin.defs#repoRef'
await pdsClient.call( await pdsClient.call(
com.atproto.moderation.createReport, com.atproto.moderation.createReport,
toLex<com.atproto.moderation.createReport.$InputBody>({ {
reasonType: tools.ozone.report.defs.reasonAppeal.value, reasonType: tools.ozone.report.defs.reasonAppeal.value,
subject: { subject:
$type, 'did' in subject
...subject, ? accountReportSubject(subject.did)
}, : recordReportSubject(subject.uri, subject.cid),
reason: details, reason: details,
}), },
{ {
service: `${label.src}#atproto_labeler` as Service, service: `${label.src}#atproto_labeler` as Service,
}, },
@@ -6,10 +6,14 @@ import {useMutation} from '@tanstack/react-query'
import {logger} from '#/logger' import {logger} from '#/logger'
import {usePdsClient} from '#/state/session' import {usePdsClient} from '#/state/session'
import {com} from '#/lexicons' import {com} from '#/lexicons'
import {toLex} from '#/types/bsky'
import {NEW_TO_OLD_REASONS_MAP} from './const' import {NEW_TO_OLD_REASONS_MAP} from './const'
import {type ReportState} from './state' import {type ReportState} from './state'
import {type ParsedReportSubject} from './types' import {type ParsedReportSubject} from './types'
import {
accountReportSubject,
chatReportSubject,
recordReportSubject,
} from './utils/reportSubject'
type CreateReportBody = com.atproto.moderation.createReport.$InputBody type CreateReportBody = com.atproto.moderation.createReport.$InputBody
@@ -54,25 +58,20 @@ export function useSubmitReportMutation() {
/* /*
* The generated `createReport` subject union only declares repoRef and * The generated `createReport` subject union only declares repoRef and
* strongRef with branded did/uri strings; chat subjects (message/convo * strongRef; chat subjects (message/convo refs) are accepted on the wire
* refs) are accepted on the wire but not in the lexicon, and the subject * but not in the lexicon. The builders in `./utils/reportSubject` brand
* ids we hold here are plain strings. We build the body against a loose * the plain-string ids into the schema `subject` slot, keeping the
* subject shape and `toLex` it to the schema body at the call boundary * runtime values exact and confining the chat-subject assertion to one
* (matching the old widened-InputSchema shape). * place.
*/ */
let report: Omit<CreateReportBody, 'subject'> & { let report: CreateReportBody
subject: {$type: string} & Record<string, unknown>
}
switch (subject.type) { switch (subject.type) {
case 'account': { case 'account': {
report = { report = {
reasonType, reasonType,
reason: state.details, reason: state.details,
subject: { subject: accountReportSubject(subject.did),
$type: 'com.atproto.admin.defs#repoRef',
did: subject.did,
},
} }
break break
} }
@@ -84,11 +83,7 @@ export function useSubmitReportMutation() {
report = { report = {
reasonType, reasonType,
reason: state.details, reason: state.details,
subject: { subject: recordReportSubject(subject.uri, subject.cid),
$type: 'com.atproto.repo.strongRef',
uri: subject.uri,
cid: subject.cid,
},
} }
break break
} }
@@ -96,12 +91,12 @@ export function useSubmitReportMutation() {
report = { report = {
reasonType, reasonType,
reason: state.details, reason: state.details,
subject: { subject: chatReportSubject({
$type: 'chat.bsky.convo.defs#messageRef', $type: 'chat.bsky.convo.defs#messageRef',
messageId: subject.message.id, messageId: subject.message.id,
convoId: subject.convoId, convoId: subject.convoId,
did: subject.message.sender.did, did: subject.message.sender.did,
}, }),
} }
break break
} }
@@ -109,11 +104,11 @@ export function useSubmitReportMutation() {
report = { report = {
reasonType, reasonType,
reason: state.details, reason: state.details,
subject: { subject: chatReportSubject({
$type: 'chat.bsky.convo.defs#convoRef', $type: 'chat.bsky.convo.defs#convoRef',
convoId: subject.convoId, convoId: subject.convoId,
did: subject.did, did: subject.did,
}, }),
} }
break break
} }
@@ -133,13 +128,9 @@ export function useSubmitReportMutation() {
* per-call `service` option (previously an explicit header on the * per-call `service` option (previously an explicit header on the
* bridge agent). * bridge agent).
*/ */
await pdsClient.call( await pdsClient.call(com.atproto.moderation.createReport, report, {
com.atproto.moderation.createReport, service: `${labeler.creator.did}#atproto_labeler` as Service,
toLex<CreateReportBody>(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 Insets, Platform} from 'react-native'
import {type Service} from '@atproto/lex-client'
import {api} from '@bsky.app/sdk' import {api} from '@bsky.app/sdk'
import {type ProxyHeaderValue} from '#/state/session/agent'
import {BLUESKY_PROXY_DID, IS_DEV} from '#/env' import {BLUESKY_PROXY_DID, IS_DEV} from '#/env'
import {type app} from '#/lexicons' 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 = export const LOCAL_DEV_SERVICE =
Platform.OS === 'android' ? 'http://10.0.2.2:2583' : 'http://localhost:2583' Platform.OS === 'android' ? 'http://10.0.2.2:2583' : 'http://localhost:2583'
export const STAGING_SERVICE = 'https://staging.bsky.dev' 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`, '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 = { export const webLinks = {
tos: `https://bsky.social/about/support/tos`, 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 {Platform} from 'react-native'
import * as Notifications from 'expo-notifications' import * as Notifications from 'expo-notifications'
import {getBadgeCountAsync, setBadgeCountAsync} from 'expo-notifications' import {getBadgeCountAsync, setBadgeCountAsync} from 'expo-notifications'
import {type Client} from '@atproto/lex-client'
import debounce from 'lodash.debounce' import debounce from 'lodash.debounce'
import { import {
BLUESKY_NOTIF_SERVICE_HEADERS, NOTIF_SERVICE,
PUBLIC_APPVIEW_DID, PUBLIC_APPVIEW_DID,
PUBLIC_STAGING_APPVIEW_DID, PUBLIC_STAGING_APPVIEW_DID,
} from '#/lib/constants' } from '#/lib/constants'
import {logger as notyLogger} from '#/lib/notifications/util' import {logger as notyLogger} from '#/lib/notifications/util'
import {isNetworkError} from '#/lib/strings/errors' import {isNetworkError} from '#/lib/strings/errors'
import {type SessionAccount, useAgent, useSession} from '#/state/session' import {type SessionAccount, usePdsClient, useSession} from '#/state/session'
import {type SessionAgent} from '#/state/session/session-core'
import BackgroundNotificationHandler from '#/../modules/expo-background-notification-handler' import BackgroundNotificationHandler from '#/../modules/expo-background-notification-handler'
import {useAgeAssurance} from '#/ageAssurance' import {useAgeAssurance} from '#/ageAssurance'
import {useAnalytics} from '#/analytics' import {useAnalytics} from '#/analytics'
import {IS_DEV, IS_NATIVE} from '#/env' 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 * @private
* Registers the device's push notification token with the Bluesky server. * Registers the device's push notification token with the Bluesky server.
*/ */
async function _registerPushToken({ async function _registerPushToken({
agent, client,
currentAccount, currentAccount,
token, token,
extra = {}, extra = {},
}: { }: {
agent: SessionAgent client: Client
currentAccount: SessionAccount currentAccount: SessionAccount
token: Notifications.DevicePushToken token: Notifications.DevicePushToken
extra?: { extra?: {
@@ -49,8 +62,8 @@ async function _registerPushToken({
notyLogger.debug(`registerPushToken: registering`, {...payload}) notyLogger.debug(`registerPushToken: registering`, {...payload})
await agent.app.bsky.notification.registerPush(payload, { await client.call(app.bsky.notification.registerPush, payload, {
headers: BLUESKY_NOTIF_SERVICE_HEADERS, service: NOTIF_SERVICE,
}) })
notyLogger.debug(`registerPushToken: success`) notyLogger.debug(`registerPushToken: success`)
@@ -75,7 +88,7 @@ const _registerPushTokenDebounced = debounce(_registerPushToken, 100)
* `_registerPushTokenDebounced` directly. * `_registerPushTokenDebounced` directly.
*/ */
export function useRegisterPushToken() { export function useRegisterPushToken() {
const agent = useAgent() const client = usePdsClient()
const {currentAccount} = useSession() const {currentAccount} = useSession()
return useCallback( return useCallback(
@@ -88,7 +101,7 @@ export function useRegisterPushToken() {
}) => { }) => {
if (!currentAccount) return if (!currentAccount) return
return _registerPushTokenDebounced({ return _registerPushTokenDebounced({
agent, client,
currentAccount, currentAccount,
token, token,
extra: { extra: {
@@ -96,7 +109,7 @@ export function useRegisterPushToken() {
}, },
}) })
}, },
[agent, currentAccount], [client, currentAccount],
) )
} }
@@ -327,16 +340,17 @@ export async function resetBadgeCount() {
await setBadgeCountAsync(0) await setBadgeCountAsync(0)
} }
export async function unregisterPushToken(agents: SessionAgent[]) { export async function unregisterPushToken(clients: TemporaryPushClient[]) {
if (!IS_NATIVE) return if (!IS_NATIVE) return
try { try {
const token = await getPushToken() const token = await getPushToken()
if (token) { if (token) {
for (const agent of agents) { for (const {client, service, handle} of clients) {
await agent.app.bsky.notification.unregisterPush( await client.call(
app.bsky.notification.unregisterPush,
{ {
serviceDid: agent.serviceUrl.hostname.includes('staging') serviceDid: service.includes('staging')
? PUBLIC_STAGING_APPVIEW_DID ? PUBLIC_STAGING_APPVIEW_DID
: PUBLIC_APPVIEW_DID, : PUBLIC_APPVIEW_DID,
platform: Platform.OS, platform: Platform.OS,
@@ -344,10 +358,10 @@ export async function unregisterPushToken(agents: SessionAgent[]) {
appId: 'xyz.blueskyweb.app', 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 { } else {
notyLogger.debug('Tried to unregister push token, but could not find one') 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 {logger} from '#/logger'
import { import {
type SessionAccount, type SessionAccount,
useAgent, usePdsClient,
useSession, useSession,
useSessionApi, useSessionApi,
} from '#/state/session' } from '#/state/session'
@@ -25,6 +25,7 @@ import * as Layout from '#/components/Layout'
import {Loader} from '#/components/Loader' import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {IS_WEB} from '#/env' import {IS_WEB} from '#/env'
import {com} from '#/lexicons'
const COL_WIDTH = 400 const COL_WIDTH = 400
@@ -36,8 +37,8 @@ export function Deactivated() {
const {onPressSwitchAccount, pendingDid} = useAccountSwitcher() const {onPressSwitchAccount, pendingDid} = useAccountSwitcher()
const {setShowLoggedOut} = useLoggedOutViewControls() const {setShowLoggedOut} = useLoggedOutViewControls()
const hasOtherAccounts = accounts.length > 1 const hasOtherAccounts = accounts.length > 1
const {logoutCurrentAccount} = useSessionApi() const {logoutCurrentAccount, refreshSession} = useSessionApi()
const agent = useAgent() const pdsClient = usePdsClient()
const [pending, setPending] = useState(false) const [pending, setPending] = useState(false)
const [error, setError] = useState<string | undefined>() const [error, setError] = useState<string | undefined>()
const queryClient = useQueryClient() const queryClient = useQueryClient()
@@ -70,9 +71,9 @@ export function Deactivated() {
const handleActivate = useCallback(async () => { const handleActivate = useCallback(async () => {
try { try {
setPending(true) setPending(true)
await agent.com.atproto.server.activateAccount() await pdsClient.call(com.atproto.server.activateAccount)
await queryClient.resetQueries() await queryClient.resetQueries()
await agent.resumeSession(agent.session!) await refreshSession()
} catch (e: any) { } catch (e: any) {
switch (e.message) { switch (e.message) {
case 'Bad token scope': case 'Bad token scope':
@@ -93,7 +94,7 @@ export function Deactivated() {
} finally { } finally {
setPending(false) setPending(false)
} }
}, [_, agent, setPending, setError, queryClient]) }, [_, pdsClient, refreshSession, setPending, setError, queryClient])
return ( return (
<View style={[a.util_screen_outer, a.flex_1]}> <View style={[a.util_screen_outer, a.flex_1]}>
+4 -4
View File
@@ -1,11 +1,11 @@
import {useCallback, useState} from 'react' import {useCallback, useState} from 'react'
import {Keyboard, View} from 'react-native' import {Keyboard, View} from 'react-native'
import {Client} from '@atproto/lex-client'
import {Trans, useLingui} from '@lingui/react/macro' import {Trans, useLingui} from '@lingui/react/macro'
import * as EmailValidator from 'email-validator' import * as EmailValidator from 'email-validator'
import {cleanError, isNetworkError} from '#/lib/strings/errors' import {cleanError, isNetworkError} from '#/lib/strings/errors'
import {logger} from '#/logger' import {logger} from '#/logger'
import {Agent} from '#/state/session/agent'
import {atoms as a, useTheme, web} from '#/alf' import {atoms as a, useTheme, web} from '#/alf'
import {Admonition} from '#/components/Admonition' import {Admonition} from '#/components/Admonition'
import {Button, ButtonIcon, ButtonText} from '#/components/Button' 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 {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {IS_WEB} from '#/env' import {IS_WEB} from '#/env'
import {type com} from '#/lexicons' import {com} from '#/lexicons'
import {FormContainer} from './FormContainer' import {FormContainer} from './FormContainer'
type ServiceDescription = com.atproto.server.describeServer.$OutputBody type ServiceDescription = com.atproto.server.describeServer.$OutputBody
@@ -55,8 +55,8 @@ export const ForgotPasswordForm = ({
setIsProcessing(true) setIsProcessing(true)
try { try {
const agent = new Agent(null, {service: serviceUrl}) const client = new Client({service: serviceUrl})
await agent.com.atproto.server.requestPasswordReset({email}) await client.call(com.atproto.server.requestPasswordReset, {email})
onEmailSent() onEmailSent()
} catch (err) { } catch (err) {
logger.warn('Failed to request password reset', {error: err}) logger.warn('Failed to request password reset', {error: err})
+4 -3
View File
@@ -1,11 +1,11 @@
import {useState} from 'react' import {useState} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {Client} from '@atproto/lex-client'
import {Trans, useLingui} from '@lingui/react/macro' import {Trans, useLingui} from '@lingui/react/macro'
import {cleanError, isNetworkError} from '#/lib/strings/errors' import {cleanError, isNetworkError} from '#/lib/strings/errors'
import {checkAndFormatResetCode} from '#/lib/strings/password' import {checkAndFormatResetCode} from '#/lib/strings/password'
import {logger} from '#/logger' import {logger} from '#/logger'
import {Agent} from '#/state/session/agent'
import {atoms as a, web} from '#/alf' import {atoms as a, web} from '#/alf'
import {Admonition} from '#/components/Admonition' import {Admonition} from '#/components/Admonition'
import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {Button, ButtonIcon, ButtonText} from '#/components/Button'
@@ -16,6 +16,7 @@ import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics' import {useAnalytics} from '#/analytics'
import {IS_WEB} from '#/env' import {IS_WEB} from '#/env'
import {com} from '#/lexicons'
import {FormContainer} from './FormContainer' import {FormContainer} from './FormContainer'
export const SetNewPasswordForm = ({ export const SetNewPasswordForm = ({
@@ -61,8 +62,8 @@ export const SetNewPasswordForm = ({
setIsProcessing(true) setIsProcessing(true)
try { try {
const agent = new Agent(null, {service: serviceUrl}) const client = new Client({service: serviceUrl})
await agent.com.atproto.server.resetPassword({ await client.call(com.atproto.server.resetPassword, {
token: formattedCode, token: formattedCode,
password, password,
}) })
@@ -169,16 +169,14 @@ function keyExtractor(item: Item) {
return item.key return item.key
} }
/* /**
* The member list now comes from the migrated lexicon-typed query, but the * Narrows a lexicon `ProfileViewBasic` member to a `GroupConvoMember` (a
* narrowed `GroupConvoMember` target is still the old-typed shape from * `ProfileViewBasic` whose `kind` is a group member, or absent when the
* `#/components/dms/util` (migrates in a later task) - the guard doubles as * account has been deleted).
* the mixed-world bridge. TODO(phase4): retype to the lexicon member types
* once dms/util migrates.
*/ */
function isGroupMember( function isGroupMember(
member: chat.bsky.actor.defs.ProfileViewBasic, 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. // Kind is missing when the account has been deleted.
return ( return (
member.kind === undefined || member.kind === undefined ||
@@ -205,15 +203,7 @@ function GroupSettings({
const {data: memberListData = [], refetch} = useListConvoMembersQuery({ const {data: memberListData = [], refetch} = useListConvoMembersQuery({
convoId: convo.view.id, convoId: convo.view.id,
/* placeholderData: convo.members,
* `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,
),
}) })
const {data: joinRequestsData, hasNextPage: hasMoreRequests} = const {data: joinRequestsData, hasNextPage: hasMoreRequests} =
@@ -11,10 +11,10 @@ import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog' import * as Dialog from '#/components/Dialog'
import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
import {Loader} from '#/components/Loader' import {Loader} from '#/components/Loader'
import {accountReportSubject} from '#/components/moderation/ReportDialog/utils/reportSubject'
import * as Toast from '#/components/Toast' import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {com, tools} from '#/lexicons' import {com, tools} from '#/lexicons'
import {toLex} from '#/types/bsky'
export function ChatDisabled({ export function ChatDisabled({
shape = 'pill', shape = 'pill',
@@ -102,14 +102,11 @@ function DialogInner() {
throw new Error('No current account, should be unreachable') throw new Error('No current account, should be unreachable')
await pdsClient.call( await pdsClient.call(
com.atproto.moderation.createReport, com.atproto.moderation.createReport,
toLex<com.atproto.moderation.createReport.$InputBody>({ {
reasonType: tools.ozone.report.defs.reasonAppeal.value, reasonType: tools.ozone.report.defs.reasonAppeal.value,
subject: { subject: accountReportSubject(currentAccount.did),
$type: 'com.atproto.admin.defs#repoRef',
did: currentAccount.did,
},
reason: details, reason: details,
}), },
{ {
service: api.moderation.service, service: api.moderation.service,
}, },
+8 -10
View File
@@ -1,6 +1,7 @@
import {useCallback, useEffect, useState} from 'react' import {useCallback, useEffect, useState} from 'react'
import {type ListRenderItemInfo, View} from 'react-native' import {type ListRenderItemInfo, View} from 'react-native'
import * as Contacts from 'expo-contacts' import * as Contacts from 'expo-contacts'
import {type DidString} from '@atproto/syntax'
import {type ModerationOpts} from '@bsky.app/sdk/moderation' import {type ModerationOpts} from '@bsky.app/sdk/moderation'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
@@ -28,12 +29,7 @@ import {
useContactsMatchesQuery, useContactsMatchesQuery,
useContactsSyncStatusQuery, useContactsSyncStatusQuery,
} from '#/state/queries/find-contacts' } from '#/state/queries/find-contacts'
import { import {useAppviewClient, usePdsClient, useSession} from '#/state/session'
useAgent,
useAppviewClient,
usePdsClient,
useSession,
} from '#/state/session'
import {ErrorScreen} from '#/view/com/util/error/ErrorScreen' import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
import {List} from '#/view/com/util/List' import {List} from '#/view/com/util/List'
import {atoms as a, tokens, useGutters, useTheme} from '#/alf' import {atoms as a, tokens, useGutters, useTheme} from '#/alf'
@@ -196,7 +192,7 @@ function SyncStatus({
refetchStatus: () => Promise<any> refetchStatus: () => Promise<any>
}) { }) {
const ax = useAnalytics() const ax = useAnalytics()
const agent = useAgent() const appviewClient = useAppviewClient()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const {_} = useLingui() const {_} = useLingui()
const moderationOpts = useModerationOpts() const moderationOpts = useModerationOpts()
@@ -221,7 +217,9 @@ function SyncStatus({
const {mutate: dismissMatch} = useMutation({ const {mutate: dismissMatch} = useMutation({
mutationFn: async (did: string) => { 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) => { onMutate: async (did: string) => {
ax.metric('contacts:settings:dismiss', {}) ax.metric('contacts:settings:dismiss', {})
@@ -493,12 +491,12 @@ function StatusFooter({syncedAt}: {syncedAt: string}) {
const {_, i18n} = useLingui() const {_, i18n} = useLingui()
const t = useTheme() const t = useTheme()
const ax = useAnalytics() const ax = useAnalytics()
const agent = useAgent() const appviewClient = useAppviewClient()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const {mutate: removeData, isPending} = useMutation({ const {mutate: removeData, isPending} = useMutation({
mutationFn: async () => { mutationFn: async () => {
await agent.app.bsky.contact.removeData({}) await appviewClient.call(app.bsky.contact.removeData, {})
}, },
onMutate: () => ax.metric('contacts:settings:removeData', {}), onMutate: () => ax.metric('contacts:settings:removeData', {}),
onSuccess: () => { onSuccess: () => {
+5 -4
View File
@@ -1,5 +1,6 @@
import {useMemo, useState} from 'react' import {useMemo, useState} from 'react'
import {type TextStyle, View, type ViewStyle} from 'react-native' import {type TextStyle, View, type ViewStyle} from 'react-native'
import {setInterestsPref} from '@bsky.app/sdk'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro' import {Trans} from '@lingui/react/macro'
@@ -23,7 +24,7 @@ import {createGetSuggestedUsersForDiscoverQueryKey} from '#/state/queries/trendi
import {createGetSuggestedUsersForExploreQueryKey} from '#/state/queries/trending/useGetSuggestedUsersForExploreQuery' import {createGetSuggestedUsersForExploreQueryKey} from '#/state/queries/trending/useGetSuggestedUsersForExploreQuery'
import {createGetSuggestedUsersForSeeMoreQueryKey} from '#/state/queries/trending/useGetSuggestedUsersForSeeMoreQuery' import {createGetSuggestedUsersForSeeMoreQueryKey} from '#/state/queries/trending/useGetSuggestedUsersForSeeMoreQuery'
import {createSuggestedStarterPacksQueryKey} from '#/state/queries/useSuggestedStarterPacksQuery' 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 {atoms as a, useGutters, useTheme} from '#/alf'
import {Admonition} from '#/components/Admonition' import {Admonition} from '#/components/Admonition'
import {Divider} from '#/components/Divider' import {Divider} from '#/components/Divider'
@@ -88,7 +89,7 @@ function Inner({
setIsSaving: (isSaving: boolean) => void setIsSaving: (isSaving: boolean) => void
}) { }) {
const {_} = useLingui() const {_} = useLingui()
const agent = useAgent() const pdsClient = usePdsClient()
const qc = useQueryClient() const qc = useQueryClient()
const interestsDisplayNames = useInterestsDisplayNames() const interestsDisplayNames = useInterestsDisplayNames()
const preselectedInterests = useMemo( const preselectedInterests = useMemo(
@@ -110,7 +111,7 @@ function Inner({
setIsSaving(true) setIsSaving(true)
try { try {
await agent.setInterestsPref({tags: interests}) await pdsClient.call(setInterestsPref, {tags: interests})
qc.setQueriesData( qc.setQueriesData(
{queryKey: preferencesQueryKey}, {queryKey: preferencesQueryKey},
(old?: UsePreferencesQueryResponse) => { (old?: UsePreferencesQueryResponse) => {
@@ -157,7 +158,7 @@ function Inner({
setIsSaving(false) setIsSaving(false)
} }
}, 1500) }, 1500)
}, [_, agent, setIsSaving, qc, preselectedInterests]) }, [_, pdsClient, setIsSaving, qc, preselectedInterests])
const onChangeInterests = async (interests: string[]) => { const onChangeInterests = async (interests: string[]) => {
setInterests(interests) setInterests(interests)
+9 -4
View File
@@ -1,6 +1,7 @@
import {useState} from 'react' import {useState} from 'react'
import {Alert, LayoutAnimation, Linking, Pressable, View} from 'react-native' import {Alert, LayoutAnimation, Linking, Pressable, View} from 'react-native'
import {useReducedMotion} from 'react-native-reanimated' import {useReducedMotion} from 'react-native-reanimated'
import {removeNuxs} from '@bsky.app/sdk'
import {moderateProfile} from '@bsky.app/sdk/moderation' import {moderateProfile} from '@bsky.app/sdk/moderation'
import {Trans, useLingui} from '@lingui/react/macro' import {Trans, useLingui} from '@lingui/react/macro'
import {useNavigation} from '@react-navigation/native' import {useNavigation} from '@react-navigation/native'
@@ -21,8 +22,12 @@ import {clearStorage} from '#/state/persisted'
import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useDeleteActorDeclaration} from '#/state/queries/messages/actor-declaration' import {useDeleteActorDeclaration} from '#/state/queries/messages/actor-declaration'
import {useProfileQuery, useProfilesQuery} from '#/state/queries/profile' import {useProfileQuery, useProfilesQuery} from '#/state/queries/profile'
import {useAgent} from '#/state/session' import {
import {type SessionAccount, useSession, useSessionApi} from '#/state/session' type SessionAccount,
usePdsClient,
useSession,
useSessionApi,
} from '#/state/session'
import {useOnboardingDispatch} from '#/state/shell' import {useOnboardingDispatch} from '#/state/shell'
import {useLoggedOutViewControls} from '#/state/shell/logged-out' import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {useCloseAllActiveElements} from '#/state/util' import {useCloseAllActiveElements} from '#/state/util'
@@ -386,7 +391,7 @@ function ProfilePreview({
function DevOptions() { function DevOptions() {
const {t: l} = useLingui() const {t: l} = useLingui()
const agent = useAgent() const pdsClient = usePdsClient()
const [override, setOverride] = useStorage(device, [ const [override, setOverride] = useStorage(device, [
'policyUpdateDebugOverride', 'policyUpdateDebugOverride',
]) ])
@@ -561,7 +566,7 @@ function DevOptions() {
<Button <Button
onPress={() => { onPress={() => {
device.set([PolicyUpdate202508], false) device.set([PolicyUpdate202508], false)
void agent.bskyAppRemoveNuxs([PolicyUpdate202508]) void pdsClient.call(removeNuxs, [PolicyUpdate202508])
Toast.show(`Done`, { Toast.show(`Done`, {
type: 'info', type: 'info',
}) })
@@ -26,7 +26,7 @@ import {useFetchDid, useUpdateHandleMutation} from '#/state/queries/handle'
import {RQKEY as RQKEY_PROFILE} from '#/state/queries/profile' import {RQKEY as RQKEY_PROFILE} from '#/state/queries/profile'
import {useServiceQuery} from '#/state/queries/service' import {useServiceQuery} from '#/state/queries/service'
import {useCurrentAccountProfile} from '#/state/queries/useCurrentAccountProfile' 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 {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
import {atoms as a, native, useBreakpoints, useTheme} from '#/alf' import {atoms as a, native, useBreakpoints, useTheme} from '#/alf'
import {Admonition} from '#/components/Admonition' import {Admonition} from '#/components/Admonition'
@@ -63,12 +63,12 @@ export function ChangeHandleDialog({
function ChangeHandleDialogInner() { function ChangeHandleDialogInner() {
const control = Dialog.useDialogContext() const control = Dialog.useDialogContext()
const {_} = useLingui() const {_} = useLingui()
const agent = useAgent() const {currentAccount} = useSession()
const { const {
data: serviceInfo, data: serviceInfo,
error: serviceInfoError, error: serviceInfoError,
refetch, refetch,
} = useServiceQuery(agent.serviceUrl.toString()) } = useServiceQuery(currentAccount?.service ?? '')
const [page, setPage] = useState<'provided-handle' | 'own-handle'>( const [page, setPage] = useState<'provided-handle' | 'own-handle'>(
'provided-handle', 'provided-handle',
@@ -152,7 +152,7 @@ function ProvidedHandlePage({
}) { }) {
const {_} = useLingui() const {_} = useLingui()
const [subdomain, setSubdomain] = useState('') const [subdomain, setSubdomain] = useState('')
const agent = useAgent() const {refreshSession} = useSessionApi()
const control = Dialog.useDialogContext() const control = Dialog.useDialogContext()
const {currentAccount} = useSession() const {currentAccount} = useSession()
const queryClient = useQueryClient() const queryClient = useQueryClient()
@@ -173,7 +173,7 @@ function ProvidedHandlePage({
queryKey: RQKEY_PROFILE(currentAccount.did), 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 {currentAccount} = useSession()
const [dnsPanel, setDNSPanel] = useState(true) const [dnsPanel, setDNSPanel] = useState(true)
const [domain, setDomain] = useState('') const [domain, setDomain] = useState('')
const agent = useAgent() const {refreshSession} = useSessionApi()
const control = Dialog.useDialogContext() const control = Dialog.useDialogContext()
const fetchDid = useFetchDid() const fetchDid = useFetchDid()
const queryClient = useQueryClient() const queryClient = useQueryClient()
@@ -328,7 +328,7 @@ function OwnHandlePage({goToServiceHandle}: {goToServiceHandle: () => void}) {
queryKey: RQKEY_PROFILE(currentAccount.did), 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 {cleanError, isNetworkError} from '#/lib/strings/errors'
import {checkAndFormatResetCode} from '#/lib/strings/password' import {checkAndFormatResetCode} from '#/lib/strings/password'
import {logger} from '#/logger' 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 {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
import {android, atoms as a, web} from '#/alf' import {android, atoms as a, web} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {Button, ButtonIcon, ButtonText} from '#/components/Button'
@@ -17,6 +17,7 @@ import * as TextField from '#/components/forms/TextField'
import {Loader} from '#/components/Loader' import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {IS_NATIVE} from '#/env' import {IS_NATIVE} from '#/env'
import {com} from '#/lexicons'
enum Stages { enum Stages {
RequestCode = 'RequestCode', RequestCode = 'RequestCode',
@@ -44,7 +45,7 @@ export function ChangePasswordDialog({
function Inner() { function Inner() {
const {_} = useLingui() const {_} = useLingui()
const {currentAccount} = useSession() const {currentAccount} = useSession()
const agent = useAgent() const pdsClient = usePdsClient()
const control = Dialog.useDialogContext() const control = Dialog.useDialogContext()
const [stage, setStage] = useState(Stages.RequestCode) const [stage, setStage] = useState(Stages.RequestCode)
@@ -85,7 +86,7 @@ function Inner() {
setError('') setError('')
setIsProcessing(true) setIsProcessing(true)
try { try {
await agent.com.atproto.server.requestPasswordReset({ await pdsClient.call(com.atproto.server.requestPasswordReset, {
email: currentAccount.email, email: currentAccount.email,
}) })
setStage(Stages.ChangePassword) setStage(Stages.ChangePassword)
@@ -129,7 +130,7 @@ function Inner() {
setError('') setError('')
setIsProcessing(true) setIsProcessing(true)
try { try {
await agent.com.atproto.server.resetPassword({ await pdsClient.call(com.atproto.server.resetPassword, {
token: formattedCode, token: formattedCode,
password: newPassword, password: newPassword,
}) })
@@ -5,7 +5,7 @@ import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro' import {Trans} from '@lingui/react/macro'
import {logger} from '#/logger' import {logger} from '#/logger'
import {useAgent, useSessionApi} from '#/state/session' import {usePdsClient, useSessionApi} from '#/state/session'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {type DialogOuterProps} from '#/components/Dialog' 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 {Loader} from '#/components/Loader'
import * as Prompt from '#/components/Prompt' import * as Prompt from '#/components/Prompt'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {com} from '#/lexicons'
export function DeactivateAccountDialog({ export function DeactivateAccountDialog({
control, control,
@@ -34,7 +35,7 @@ function DeactivateAccountDialogInner({
}) { }) {
const t = useTheme() const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
const agent = useAgent() const pdsClient = usePdsClient()
const {logoutCurrentAccount} = useSessionApi() const {logoutCurrentAccount} = useSessionApi()
const [pending, setPending] = useState(false) const [pending, setPending] = useState(false)
const [error, setError] = useState<string | undefined>() const [error, setError] = useState<string | undefined>()
@@ -42,7 +43,7 @@ function DeactivateAccountDialogInner({
const handleDeactivate = useCallback(async () => { const handleDeactivate = useCallback(async () => {
try { try {
setPending(true) setPending(true)
await agent.com.atproto.server.deactivateAccount({}) await pdsClient.call(com.atproto.server.deactivateAccount, {})
control.close(() => { control.close(() => {
logoutCurrentAccount('Deactivated') logoutCurrentAccount('Deactivated')
}) })
@@ -66,7 +67,7 @@ function DeactivateAccountDialogInner({
} finally { } finally {
setPending(false) setPending(false)
} }
}, [agent, control, logoutCurrentAccount, _, setPending]) }, [pdsClient, control, logoutCurrentAccount, _, setPending])
return ( return (
<> <>
@@ -1,5 +1,6 @@
import {useCallback, useRef, useState} from 'react' import {useCallback, useRef, useState} from 'react'
import {type TextInput, View} from 'react-native' import {type TextInput, View} from 'react-native'
import {type DidString} from '@atproto/syntax'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro' import {Trans} from '@lingui/react/macro'
@@ -8,8 +9,8 @@ import {useCleanError} from '#/lib/hooks/useCleanError'
import {sanitizeHandle} from '#/lib/strings/handles' import {sanitizeHandle} from '#/lib/strings/handles'
import {logger} from '#/logger' import {logger} from '#/logger'
import { import {
useAgent,
useChatClient, useChatClient,
usePdsClient,
useSession, useSession,
useSessionApi, useSessionApi,
} from '#/state/session' } from '#/state/session'
@@ -28,7 +29,7 @@ import {Loader} from '#/components/Loader'
import * as Prompt from '#/components/Prompt' import * as Prompt from '#/components/Prompt'
import * as toast from '#/components/Toast' import * as toast from '#/components/Toast'
import {Span, Text} from '#/components/Typography' import {Span, Text} from '#/components/Typography'
import {chat} from '#/lexicons' import {chat, com} from '#/lexicons'
import {resetToTab} from '#/Navigation' import {resetToTab} from '#/Navigation'
const WHITESPACE_RE = /\s/gu const WHITESPACE_RE = /\s/gu
@@ -77,7 +78,7 @@ function DeleteAccountDialogInner({
const t = useTheme() const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
const cleanError = useCleanError() const cleanError = useCleanError()
const agent = useAgent() const pdsClient = usePdsClient()
const chatClient = useChatClient() const chatClient = useChatClient()
const {currentAccount} = useSession() const {currentAccount} = useSession()
const {removeAccount} = useSessionApi() const {removeAccount} = useSessionApi()
@@ -95,7 +96,7 @@ function DeleteAccountDialogInner({
} }
try { try {
setEmailState(EmailState.PENDING) setEmailState(EmailState.PENDING)
await agent.com.atproto.server.requestAccountDelete() await pdsClient.call(com.atproto.server.requestAccountDelete)
setError('') setError('')
setEmailSentCount(prevCount => prevCount + 1) setEmailSentCount(prevCount => prevCount + 1)
setStep(Step.VERIFY_CODE) setStep(Step.VERIFY_CODE)
@@ -109,7 +110,7 @@ function DeleteAccountDialogInner({
} finally { } finally {
setEmailState(EmailState.DEFAULT) setEmailState(EmailState.DEFAULT)
} }
}, [agent, cleanError, emailState, setEmailState]) }, [pdsClient, cleanError, emailState, setEmailState])
const confirmDeletion = useCallback(async () => { const confirmDeletion = useCallback(async () => {
try { try {
@@ -121,8 +122,8 @@ function DeleteAccountDialogInner({
// Inform chat service of intent to delete account. The chat client is // Inform chat service of intent to delete account. The chat client is
// proxied to the chat service; a failure throws. // proxied to the chat service; a failure throws.
await chatClient.call(chat.bsky.actor.deleteAccount) await chatClient.call(chat.bsky.actor.deleteAccount)
await agent.com.atproto.server.deleteAccount({ await pdsClient.call(com.atproto.server.deleteAccount, {
did: currentAccount.did, did: currentAccount.did as DidString,
password, password,
token, token,
}) })
@@ -144,7 +145,8 @@ function DeleteAccountDialogInner({
} }
}, [ }, [
_, _,
agent, pdsClient,
chatClient,
cleanError, cleanError,
confirmCode, confirmCode,
control, control,
@@ -5,7 +5,7 @@ import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro' import {Trans} from '@lingui/react/macro'
import {cleanError} from '#/lib/strings/errors' 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 {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {Button, ButtonIcon, ButtonText} from '#/components/Button'
@@ -16,6 +16,7 @@ import {Loader} from '#/components/Loader'
import * as Toast from '#/components/Toast' import * as Toast from '#/components/Toast'
import {P, Text} from '#/components/Typography' import {P, Text} from '#/components/Typography'
import {IS_NATIVE} from '#/env' import {IS_NATIVE} from '#/env'
import {com} from '#/lexicons'
enum Stages { enum Stages {
Email, Email,
@@ -31,7 +32,8 @@ export function DisableEmail2FADialog({
const t = useTheme() const t = useTheme()
const {gtMobile} = useBreakpoints() const {gtMobile} = useBreakpoints()
const {currentAccount} = useSession() const {currentAccount} = useSession()
const agent = useAgent() const pdsClient = usePdsClient()
const {refreshSession} = useSessionApi()
const [stage, setStage] = useState<Stages>(Stages.Email) const [stage, setStage] = useState<Stages>(Stages.Email)
const [confirmationCode, setConfirmationCode] = useState<string>('') const [confirmationCode, setConfirmationCode] = useState<string>('')
@@ -42,7 +44,7 @@ export function DisableEmail2FADialog({
setError('') setError('')
setIsProcessing(true) setIsProcessing(true)
try { try {
await agent.com.atproto.server.requestEmailUpdate() await pdsClient.call(com.atproto.server.requestEmailUpdate)
setStage(Stages.ConfirmCode) setStage(Stages.ConfirmCode)
} catch (e) { } catch (e) {
setError(cleanError(String(e))) setError(cleanError(String(e)))
@@ -56,12 +58,12 @@ export function DisableEmail2FADialog({
setIsProcessing(true) setIsProcessing(true)
try { try {
if (currentAccount?.email) { if (currentAccount?.email) {
await agent.com.atproto.server.updateEmail({ await pdsClient.call(com.atproto.server.updateEmail, {
email: currentAccount.email, email: currentAccount.email,
token: confirmationCode.trim(), token: confirmationCode.trim(),
emailAuthFactor: false, emailAuthFactor: false,
}) })
await agent.resumeSession(agent.session!) await refreshSession()
Toast.show(_(msg({message: 'Email 2FA disabled', context: 'toast'}))) Toast.show(_(msg({message: 'Email 2FA disabled', context: 'toast'})))
} }
control.close() control.close()
@@ -1,10 +1,11 @@
import {useCallback, useState} from 'react' import {useCallback, useState} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {type DidString} from '@atproto/syntax'
import {Trans, useLingui} from '@lingui/react/macro' import {Trans, useLingui} from '@lingui/react/macro'
import {saveBytesToDisk} from '#/lib/media/manip' import {saveBytesToDisk} from '#/lib/media/manip'
import {logger} from '#/logger' import {logger} from '#/logger'
import {useAgent} from '#/state/session' import {useChatClient, usePdsClient, useSession} from '#/state/session'
import {atoms as a, useTheme, web} from '#/alf' import {atoms as a, useTheme, web} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog' import * as Dialog from '#/components/Dialog'
@@ -13,7 +14,7 @@ import {InlineLinkText} from '#/components/Link'
import {Loader} from '#/components/Loader' import {Loader} from '#/components/Loader'
import * as Toast from '#/components/Toast' import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {CHAT_PROXY_DID} from '#/env' import {com} from '#/lexicons'
export function ExportCarDialog({ export function ExportCarDialog({
control, control,
@@ -22,21 +23,28 @@ export function ExportCarDialog({
}) { }) {
const {t: l} = useLingui() const {t: l} = useLingui()
const t = useTheme() const t = useTheme()
const agent = useAgent() const {currentAccount} = useSession()
const pdsClient = usePdsClient()
const chatClient = useChatClient()
const [loading, setLoading] = useState<'repo' | 'chat' | false>(false) const [loading, setLoading] = useState<'repo' | 'chat' | false>(false)
const download = useCallback(async () => { const download = useCallback(async () => {
if (!agent.session) { if (!currentAccount) {
return // shouldn't ever happen return // shouldn't ever happen
} }
try { try {
setLoading('repo') setLoading('repo')
const did = agent.session.did const did = currentAccount.did as DidString
const downloadRes = await agent.com.atproto.sync.getRepo({did}) const data = await pdsClient.call(com.atproto.sync.getRepo, {did})
/*
* getRepo 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( const saveRes = await saveBytesToDisk(
'repo.car', 'repo.car',
downloadRes.data, data,
downloadRes.headers['content-type'] || 'application/vnd.ipld.car', 'application/vnd.ipld.car',
) )
if (saveRes) { if (saveRes) {
@@ -48,21 +56,24 @@ export function ExportCarDialog({
} finally { } finally {
setLoading(false) setLoading(false)
} }
}, [l, agent]) }, [l, currentAccount, pdsClient])
const downloadChatData = useCallback(async () => { const downloadChatData = useCallback(async () => {
if (!agent.session) { if (!currentAccount) {
return return
} }
try { try {
setLoading('chat') setLoading('chat')
// Using raw fetch because the XRPC client incorrectly tries to JSON-parse /*
// application/jsonl responses (substring match on application/json). * Using the client's low-level fetchHandler because the XRPC client
// The chat-service proxy header is inlined here (the endpoint is proxied * incorrectly tries to JSON-parse application/jsonl responses (substring
// to `did:web:api.bsky.chat`); this raw path bypasses the lex client. * match on application/json). The chat client already proxies to
const res = await agent.sessionManager.fetchHandler( * `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', '/xrpc/chat.bsky.actor.exportAccountData',
{headers: {'atproto-proxy': `${CHAT_PROXY_DID}#bsky_chat`}}, {},
) )
if (!res.ok) { if (!res.ok) {
throw new Error(`HTTP ${res.status}`) throw new Error(`HTTP ${res.status}`)
@@ -83,7 +94,7 @@ export function ExportCarDialog({
} finally { } finally {
setLoading(false) setLoading(false)
} }
}, [l, agent]) }, [l, currentAccount, chatClient])
return ( return (
<Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}> <Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}>
+13 -11
View File
@@ -7,7 +7,7 @@ import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro' import {Trans} from '@lingui/react/macro'
import {logger} from '#/logger' import {logger} from '#/logger'
import {isSignupQueued, useAgent, useSessionApi} from '#/state/session' import {isSignupQueued, usePdsClient, useSessionApi} from '#/state/session'
import {useOnboardingDispatch} from '#/state/shell' import {useOnboardingDispatch} from '#/state/shell'
import {Logo} from '#/view/icons/Logo' import {Logo} from '#/view/icons/Logo'
import {atoms as a, native, useBreakpoints, useTheme, web} from '#/alf' import {atoms as a, native, useBreakpoints, useTheme, web} from '#/alf'
@@ -15,6 +15,7 @@ import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {Loader} from '#/components/Loader' import {Loader} from '#/components/Loader'
import {P, Text} from '#/components/Typography' import {P, Text} from '#/components/Typography'
import {IS_IOS, IS_LIQUID_GLASS, IS_WEB} from '#/env' import {IS_IOS, IS_LIQUID_GLASS, IS_WEB} from '#/env'
import {com} from '#/lexicons'
const COL_WIDTH = 400 const COL_WIDTH = 400
@@ -24,8 +25,8 @@ export function SignupQueued() {
const insets = useSafeAreaInsets() const insets = useSafeAreaInsets()
const {gtMobile} = useBreakpoints() const {gtMobile} = useBreakpoints()
const onboardingDispatch = useOnboardingDispatch() const onboardingDispatch = useOnboardingDispatch()
const {logoutCurrentAccount} = useSessionApi() const {logoutCurrentAccount, refreshSession} = useSessionApi()
const agent = useAgent() const pdsClient = usePdsClient()
const [isProcessing, setProcessing] = useState(false) const [isProcessing, setProcessing] = useState(false)
const [estimatedTime, setEstimatedTime] = useState<string | undefined>( const [estimatedTime, setEstimatedTime] = useState<string | undefined>(
@@ -38,18 +39,18 @@ export function SignupQueued() {
const checkStatus = useCallback(async () => { const checkStatus = useCallback(async () => {
setProcessing(true) setProcessing(true)
try { try {
const res = await agent.com.atproto.temp.checkSignupQueue() const res = await pdsClient.call(com.atproto.temp.checkSignupQueue)
if (res.data.activated) { if (res.activated) {
// ready to go, exchange the access token for a usable one and kick off onboarding // ready to go, exchange the access token for a usable one and kick off onboarding
await agent.resumeSession() const refreshed = await refreshSession()
if (!isSignupQueued(agent.session?.accessJwt)) { if (!isSignupQueued(refreshed?.accessJwt)) {
onboardingDispatch({type: 'start'}) onboardingDispatch({type: 'start'})
} }
} else { } else {
// not ready, update UI // not ready, update UI
setEstimatedTime(msToString(res.data.estimatedTimeMs)) setEstimatedTime(msToString(res.estimatedTimeMs))
if (typeof res.data.placeInQueue !== 'undefined') { if (typeof res.placeInQueue !== 'undefined') {
setPlaceInQueue(Math.max(res.data.placeInQueue, 1)) setPlaceInQueue(Math.max(res.placeInQueue, 1))
} }
} }
} catch (e: any) { } catch (e: any) {
@@ -62,7 +63,8 @@ export function SignupQueued() {
setEstimatedTime, setEstimatedTime,
setPlaceInQueue, setPlaceInQueue,
onboardingDispatch, onboardingDispatch,
agent, pdsClient,
refreshSession,
]) ])
useEffect(() => { useEffect(() => {
+4 -7
View File
@@ -19,10 +19,10 @@ import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as TextField from '#/components/forms/TextField' import * as TextField from '#/components/forms/TextField'
import {SimpleInlineLinkText} from '#/components/Link' import {SimpleInlineLinkText} from '#/components/Link'
import {Loader} from '#/components/Loader' import {Loader} from '#/components/Loader'
import {accountReportSubject} from '#/components/moderation/ReportDialog/utils/reportSubject'
import {P, Text} from '#/components/Typography' import {P, Text} from '#/components/Typography'
import {IS_WEB} from '#/env' import {IS_WEB} from '#/env'
import {com, tools} from '#/lexicons' import {com, tools} from '#/lexicons'
import {toLex} from '#/types/bsky'
const COL_WIDTH = 400 const COL_WIDTH = 400
@@ -51,14 +51,11 @@ export function Takendown() {
if (!currentAccount) throw new Error('No session') if (!currentAccount) throw new Error('No session')
await pdsClient.call( await pdsClient.call(
com.atproto.moderation.createReport, com.atproto.moderation.createReport,
toLex<com.atproto.moderation.createReport.$InputBody>({ {
reasonType: tools.ozone.report.defs.reasonAppeal.value, reasonType: tools.ozone.report.defs.reasonAppeal.value,
subject: { subject: accountReportSubject(currentAccount.did),
$type: 'com.atproto.admin.defs#repoRef',
did: currentAccount.did,
},
reason: appealText, reason: appealText,
}), },
{ {
service: api.moderation.service, service: api.moderation.service,
}, },
+7 -4
View File
@@ -1,9 +1,10 @@
import {useMemo} from 'react' import {useMemo} from 'react'
import {setPersonalDetails} from '@bsky.app/sdk'
import {useMutation, useQueryClient} from '@tanstack/react-query' import {useMutation, useQueryClient} from '@tanstack/react-query'
import {restrictChatSettings} from '#/state/queries/messages/restrictChatSettings' import {restrictChatSettings} from '#/state/queries/messages/restrictChatSettings'
import {preferencesQueryKey} from '#/state/queries/preferences' import {preferencesQueryKey} from '#/state/queries/preferences'
import {useAgent, usePdsClient, useSession} from '#/state/session' import {usePdsClient, useSession} from '#/state/session'
import {usePatchAgeAssuranceOtherRequiredData} from '#/ageAssurance' import {usePatchAgeAssuranceOtherRequiredData} from '#/ageAssurance'
import {isUnderAge} from '#/ageAssurance/util' import {isUnderAge} from '#/ageAssurance/util'
import {IS_DEV} from '#/env' import {IS_DEV} from '#/env'
@@ -54,14 +55,14 @@ export function useIsBirthdateUpdateAllowed() {
export function useBirthdateMutation() { export function useBirthdateMutation() {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const agent = useAgent() const {currentAccount} = useSession()
const pdsClient = usePdsClient() const pdsClient = usePdsClient()
const patchOtherRequiredData = usePatchAgeAssuranceOtherRequiredData() const patchOtherRequiredData = usePatchAgeAssuranceOtherRequiredData()
return useMutation<void, unknown, {birthDate: Date}>({ return useMutation<void, unknown, {birthDate: Date}>({
mutationFn: async ({birthDate}: {birthDate: Date}) => { mutationFn: async ({birthDate}: {birthDate: Date}) => {
const bday = birthDate.toISOString() const bday = birthDate.toISOString()
await agent.setPersonalDetails({birthDate: bday}) await pdsClient.call(setPersonalDetails, {birthDate})
// triggers a refetch // triggers a refetch
await queryClient.invalidateQueries({ await queryClient.invalidateQueries({
queryKey: preferencesQueryKey, queryKey: preferencesQueryKey,
@@ -80,7 +81,9 @@ export function useBirthdateMutation() {
* birthdate, which may change the user's age assurance access level. * birthdate, which may change the user's age assurance access level.
*/ */
void patchOtherRequiredData({birthdate: bday}) void patchOtherRequiredData({birthdate: bday})
snoozeBirthdateUpdateAllowedForDid(agent.sessionManager.did!) if (currentAccount) {
snoozeBirthdateUpdateAllowedForDid(currentAccount.did)
}
}, },
}) })
} }
+7 -5
View File
@@ -5,9 +5,11 @@ import {
useEffect, useEffect,
useState, useState,
} from 'react' } from 'react'
import {type AtUriString} from '@atproto/syntax'
import * as persisted from '#/state/persisted' 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 StateContext = Map<string, boolean>
type SetStateContext = (uri: string, value: boolean) => void type SetStateContext = (uri: string, value: boolean) => void
@@ -56,7 +58,7 @@ export function useSetThreadMute() {
} }
function useMigrateMutes(setThreadMute: SetStateContext) { function useMigrateMutes(setThreadMute: SetStateContext) {
const agent = useAgent() const pdsClient = usePdsClient()
const {currentAccount} = useSession() const {currentAccount} = useSession()
useEffect(() => { useEffect(() => {
@@ -87,8 +89,8 @@ function useMigrateMutes(setThreadMute: SetStateContext) {
setThreadMute(root, true) setThreadMute(root, true)
await agent.api.app.bsky.graph await pdsClient
.muteThread({root}) .call(app.bsky.graph.muteThread, {root: root as AtUriString})
// not a big deal if this fails, since the post might have been deleted // not a big deal if this fails, since the post might have been deleted
.catch(console.error) .catch(console.error)
} }
@@ -100,5 +102,5 @@ function useMigrateMutes(setThreadMute: SetStateContext) {
cancelled = true cancelled = true
} }
} }
}, [agent, currentAccount, setThreadMute]) }, [pdsClient, currentAccount, setThreadMute])
} }
+18 -12
View File
@@ -7,6 +7,7 @@ import {
useRef, useRef,
} from 'react' } from 'react'
import {AppState, type AppStateStatus} from 'react-native' import {AppState, type AppStateStatus} from 'react-native'
import {type Service} from '@atproto/lex-client'
import {type AtUriString} from '@atproto/syntax' import {type AtUriString} from '@atproto/syntax'
import throttle from 'lodash.throttle' import throttle from 'lodash.throttle'
@@ -22,8 +23,8 @@ import {
} from '#/state/queries/post-feed' } from '#/state/queries/post-feed'
import {getItemsForFeedback} from '#/view/com/posts/PostFeed' import {getItemsForFeedback} from '#/view/com/posts/PostFeed'
import {useAnalytics} from '#/analytics' import {useAnalytics} from '#/analytics'
import {type app} from '#/lexicons' import {app} from '#/lexicons'
import {useAgent} from './session' import {useAppviewClient} from './session'
export const FEEDBACK_FEEDS = [...PROD_FEEDS, ...STAGING_FEEDS] export const FEEDBACK_FEEDS = [...PROD_FEEDS, ...STAGING_FEEDS]
@@ -66,7 +67,7 @@ export function useFeedFeedback(
) { ) {
const ax = useAnalytics() const ax = useAnalytics()
const logger = ax.logger.useChild(ax.logger.Context.FeedFeedback) const logger = ax.logger.useChild(ax.logger.Context.FeedFeedback)
const agent = useAgent() const appviewClient = useAppviewClient()
const feed = const feed =
!!feedSourceInfo && isFeedSourceFeedInfo(feedSourceInfo) !!feedSourceInfo && isFeedSourceFeedInfo(feedSourceInfo)
@@ -151,15 +152,20 @@ export function useFeedFeedback(
return return
} }
// Send to the feed /*
agent.app.bsky.feed * Send to the feed generator via a per-call `service` option, which
.sendInteractions( * lex-client turns into the `atproto-proxy` header. This overrides the
{interactions: interactionsToSend, feed: feed?.uri}, * appview client's own service target for this one call.
*/
appviewClient
.call(
app.bsky.feed.sendInteractions,
{ {
encoding: 'application/json', interactions: interactionsToSend,
headers: { feed: feed?.uri as AtUriString | undefined,
'atproto-proxy': `${proxyDid}#bsky_fg`, },
}, {
service: `${proxyDid}#bsky_fg` as Service,
}, },
) )
.catch(() => {}) // ignore upstream errors .catch(() => {}) // ignore upstream errors
@@ -173,7 +179,7 @@ export function useFeedFeedback(
) )
throttledFlushAggregatedStats() throttledFlushAggregatedStats()
logger.debug('flushed') logger.debug('flushed')
}, [agent, throttledFlushAggregatedStats, proxyDid, enabled, feed]) }, [appviewClient, throttledFlushAggregatedStats, proxyDid, enabled, feed])
const sendToFeed = useMemo( const sendToFeed = useMemo(
() => () =>
+7 -24
View File
@@ -595,42 +595,25 @@ export class Convo {
this.convo = parseConvoView(convo, this.senderUserDid) ?? this.convo this.convo = parseConvoView(convo, this.senderUserDid) ?? this.convo
if (this.convo) { if (this.convo) {
for (const member of this.convo.members) { for (const member of this.convo.members) {
// `this.convo` comes from `parseConvoView` in the still-old-typed this.relatedProfiles.set(member.did, member)
// `#/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.applyProfileShadows() this.applyProfileShadows()
} }
/* /*
* The partial merges into `this.convo.view` and is re-parsed by the * The partial merges into `this.convo.view` and is re-parsed by
* still-old-typed `parseConvoView` (`#/components/dms/util`, migrates in a * `parseConvoView`, and its callers build it from `this.convo.details` /
* later task), and its callers build it from old-typed `this.convo.details` / * members. The stored view is the lexicon `ConvoView`, so the partial is
* members. So this boundary stays in the old view world - typing the param * typed off it directly.
* 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.
*/ */
private updateConvo(convo: Partial<ConvoWithDetails['view']>) { private updateConvo(convo: Partial<chat.bsky.convo.defs.ConvoView>) {
if (this.convo) { if (this.convo) {
this.convo = this.convo =
parseConvoView({...this.convo.view, ...convo}, this.senderUserDid) ?? parseConvoView({...this.convo.view, ...convo}, this.senderUserDid) ??
this.convo this.convo
for (const member of this.convo.members) { for (const member of this.convo.members) {
// `this.convo` comes from `parseConvoView` in the still-old-typed this.relatedProfiles.set(member.did, member)
// `#/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.applyProfileShadows() 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 {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_ROOT = 'actor-starter-packs'
export const RQKEY_WITH_MEMBERSHIP_ROOT = 'actor-starter-packs-with-membership' export const RQKEY_WITH_MEMBERSHIP_ROOT = 'actor-starter-packs-with-membership'
@@ -17,17 +19,16 @@ export function useActorStarterPacksQuery({
did?: string did?: string
enabled?: boolean enabled?: boolean
}) { }) {
const agent = useAgent() const appviewClient = useAppviewClient()
return useInfiniteQuery({ return useInfiniteQuery({
queryKey: RQKEY(did), queryKey: RQKEY(did),
queryFn: async ({pageParam}: {pageParam?: string}) => { queryFn: async ({pageParam}: {pageParam?: string}) => {
const res = await agent.app.bsky.graph.getActorStarterPacks({ return await appviewClient.call(app.bsky.graph.getActorStarterPacks, {
actor: did!, actor: did! as AtIdentifierString,
limit: 10, limit: 10,
cursor: pageParam, cursor: pageParam,
}) })
return res.data
}, },
enabled: Boolean(did) && enabled, enabled: Boolean(did) && enabled,
initialPageParam: undefined, initialPageParam: undefined,
@@ -42,17 +43,19 @@ export function useActorStarterPacksWithMembershipsQuery({
did?: string did?: string
enabled?: boolean enabled?: boolean
}) { }) {
const agent = useAgent() const appviewClient = useAppviewClient()
return useInfiniteQuery({ return useInfiniteQuery({
queryKey: RQKEY_WITH_MEMBERSHIP(did), queryKey: RQKEY_WITH_MEMBERSHIP(did),
queryFn: async ({pageParam}: {pageParam?: string}) => { queryFn: async ({pageParam}: {pageParam?: string}) => {
const res = await agent.app.bsky.graph.getStarterPacksWithMembership({ return await appviewClient.call(
actor: did!, app.bsky.graph.getStarterPacksWithMembership,
limit: 10, {
cursor: pageParam, actor: did! as AtIdentifierString,
}) limit: 10,
return res.data cursor: pageParam,
},
)
}, },
enabled: Boolean(did) && enabled, enabled: Boolean(did) && enabled,
initialPageParam: undefined, 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 {useQuery} from '@tanstack/react-query'
import { import {
@@ -8,20 +10,8 @@ import {
import {useDebouncedValue} from '#/lib/hooks/useDebouncedValue' import {useDebouncedValue} from '#/lib/hooks/useDebouncedValue'
import {createFullHandle} from '#/lib/strings/handles' import {createFullHandle} from '#/lib/strings/handles'
import {useAnalytics} from '#/analytics' import {useAnalytics} from '#/analytics'
import {Agent} from '../session/agent' import {com} from '#/lexicons'
import * as bsky from '#/types/bsky'
/*
* `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}[]
}
export const RQKEY_handleAvailability = ( export const RQKEY_handleAvailability = (
handle: string, handle: string,
@@ -90,24 +80,28 @@ export async function checkHandleAvailability(
}, },
) { ) {
if (serviceDid === BSKY_SERVICE_DID) { if (serviceDid === BSKY_SERVICE_DID) {
const agent = new Agent(null, {service: BSKY_SERVICE})
// entryway has a special API for handle availability // entryway has a special API for handle availability
const {data} = await agent.com.atproto.temp.checkHandleAvailability({ const client = new Client({service: BSKY_SERVICE})
handle, const data = await client.call(com.atproto.temp.checkHandleAvailability, {
birthDate, handle: handle as HandleString,
birthDate: birthDate as DatetimeString | undefined,
email, email,
}) })
const result = data.result as CheckHandleAvailabilityResult const result = data.result
if ( if (
result.$type === bsky.isType(
'com.atproto.temp.checkHandleAvailability#resultAvailable' com.atproto.temp.checkHandleAvailability.resultAvailable,
result,
)
) { ) {
return {available: true} as const return {available: true} as const
} else if ( } else if (
result.$type === bsky.isType(
'com.atproto.temp.checkHandleAvailability#resultUnavailable' com.atproto.temp.checkHandleAvailability.resultUnavailable,
result,
)
) { ) {
return { return {
available: false, available: false,
@@ -120,13 +114,13 @@ export async function checkHandleAvailability(
} }
} else { } else {
// 3rd party PDSes won't have this API so just try and resolve the handle // 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 { try {
const res = await agent.resolveHandle({ const res = await client.call(com.atproto.identity.resolveHandle, {
handle, handle: handle as HandleString,
}) })
if (res.data.did) { if (res.did) {
return {available: false} as const return {available: false} as const
} }
} catch {} } catch {}
+21 -11
View File
@@ -1,8 +1,10 @@
import {useCallback} from 'react' import {useCallback} from 'react'
import {type AtIdentifierString, type HandleString} from '@atproto/syntax'
import {useMutation, useQueryClient} from '@tanstack/react-query' import {useMutation, useQueryClient} from '@tanstack/react-query'
import {STALE} from '#/state/queries' 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 handleQueryKeyRoot = 'handle'
const fetchHandleQueryKey = (handleOrDid: string) => [ const fetchHandleQueryKey = (handleOrDid: string) => [
@@ -14,7 +16,7 @@ const fetchDidQueryKey = (handleOrDid: string) => [didQueryKeyRoot, handleOrDid]
export function useFetchHandle() { export function useFetchHandle() {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const agent = useAgent() const appviewClient = useAppviewClient()
return useCallback( return useCallback(
async (handleOrDid: string) => { async (handleOrDid: string) => {
@@ -22,13 +24,16 @@ export function useFetchHandle() {
const res = await queryClient.fetchQuery({ const res = await queryClient.fetchQuery({
staleTime: STALE.MINUTES.FIVE, staleTime: STALE.MINUTES.FIVE,
queryKey: fetchHandleQueryKey(handleOrDid), 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 return handleOrDid
}, },
[queryClient, agent], [queryClient, appviewClient],
) )
} }
@@ -36,11 +41,13 @@ export function useUpdateHandleMutation(opts?: {
onSuccess?: (handle: string) => void onSuccess?: (handle: string) => void
}) { }) {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const agent = useAgent() const pdsClient = usePdsClient()
return useMutation({ return useMutation({
mutationFn: async ({handle}: {handle: string}) => { mutationFn: async ({handle}: {handle: string}) => {
await agent.updateHandle({handle}) await pdsClient.call(com.atproto.identity.updateHandle, {
handle: handle as HandleString,
})
}, },
onSuccess(_data, variables) { onSuccess(_data, variables) {
opts?.onSuccess?.(variables.handle) opts?.onSuccess?.(variables.handle)
@@ -53,7 +60,7 @@ export function useUpdateHandleMutation(opts?: {
export function useFetchDid() { export function useFetchDid() {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const agent = useAgent() const appviewClient = useAppviewClient()
return useCallback( return useCallback(
async (handleOrDid: string) => { async (handleOrDid: string) => {
@@ -63,13 +70,16 @@ export function useFetchDid() {
queryFn: async () => { queryFn: async () => {
let identifier = handleOrDid let identifier = handleOrDid
if (!identifier.startsWith('did:')) { if (!identifier.startsWith('did:')) {
const res = await agent.resolveHandle({handle: identifier}) const res = await appviewClient.call(
identifier = res.data.did com.atproto.identity.resolveHandle,
{handle: identifier as HandleString},
)
identifier = res.did
} }
return identifier 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 {useMutation} from '@tanstack/react-query'
import {useAgent} from '#/state/session' import {usePdsClient} from '#/state/session'
export function useLikeMutation() { export function useLikeMutation() {
const agent = useAgent() const pdsClient = usePdsClient()
return useMutation({ return useMutation({
mutationFn: async ({uri, cid}: {uri: string; cid: string}) => { 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} return {uri: res.uri}
}, },
}) })
} }
export function useUnlikeMutation() { export function useUnlikeMutation() {
const agent = useAgent() const pdsClient = usePdsClient()
return useMutation({ return useMutation({
mutationFn: async ({uri}: {uri: string}) => { 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.message` can also be a deleted-message view per the
* log union, which the strict MessageAndReactionView type * log union, which the strict MessageAndReactionView type
* rejects - the old types absorbed this via the open-union * rejects - the raw ConvoView cache type only admits a live
* catch-all. Keep the pre-migration runtime behavior (always * MessageView here. Keep the pre-migration runtime behavior
* store the view we got) and assert the cache type. * (always store the view we got) and assert into the cache
* type; the runtime value is unchanged.
*/ */
lastReaction: bsky.toLex< lastReaction: {
NonNullable<chat.bsky.convo.defs.ConvoView['lastReaction']>
>({
$type: 'chat.bsky.convo.defs#messageAndReactionView', $type: 'chat.bsky.convo.defs#messageAndReactionView',
reaction: log.reaction, reaction: log.reaction,
message: log.message, message: log.message,
}), } as NonNullable<
chat.bsky.convo.defs.ConvoView['lastReaction']
>,
rev: log.rev, 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 {useMutation, useQueryClient} from '@tanstack/react-query'
import {type AppNux, type Nux} from '#/state/queries/nuxs/definitions' import {type AppNux, type Nux} from '#/state/queries/nuxs/definitions'
@@ -6,7 +7,7 @@ import {
preferencesQueryKey, preferencesQueryKey,
usePreferencesQuery, usePreferencesQuery,
} from '#/state/queries/preferences' } from '#/state/queries/preferences'
import {useAgent} from '#/state/session' import {usePdsClient} from '#/state/session'
export {Nux} from '#/state/queries/nuxs/definitions' export {Nux} from '#/state/queries/nuxs/definitions'
@@ -42,11 +43,11 @@ export function useNuxs():
// if (__DEV__) { // if (__DEV__) {
// const queryClient = useQueryClient() // const queryClient = useQueryClient()
// const agent = useAgent() // const pdsClient = usePdsClient()
// // @ts-ignore // // @ts-ignore
// window.clearNux = async (ids: string[]) => { // window.clearNux = async (ids: string[]) => {
// await agent.bskyAppRemoveNuxs(ids) // await pdsClient.call(removeNuxs, ids)
// // triggers a refetch // // triggers a refetch
// await queryClient.invalidateQueries({ // await queryClient.invalidateQueries({
// queryKey: preferencesQueryKey, // queryKey: preferencesQueryKey,
@@ -97,12 +98,12 @@ export function useNux<T extends Nux>(
export function useSaveNux() { export function useSaveNux() {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const agent = useAgent() const pdsClient = usePdsClient()
return useMutation({ return useMutation({
retry: 3, retry: 3,
mutationFn: async (nux: AppNux) => { mutationFn: async (nux: AppNux) => {
await agent.bskyAppUpsertNux(serializeAppNux(nux)) await pdsClient.call(upsertNux, serializeAppNux(nux))
// triggers a refetch // triggers a refetch
await queryClient.invalidateQueries({ await queryClient.invalidateQueries({
queryKey: preferencesQueryKey, queryKey: preferencesQueryKey,
@@ -113,12 +114,12 @@ export function useSaveNux() {
export function useResetNuxs() { export function useResetNuxs() {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const agent = useAgent() const pdsClient = usePdsClient()
return useMutation({ return useMutation({
retry: 3, retry: 3,
mutationFn: async (ids: string[]) => { mutationFn: async (ids: string[]) => {
await agent.bskyAppRemoveNuxs(ids) await pdsClient.call(removeNuxs, ids)
// triggers a refetch // triggers a refetch
await queryClient.invalidateQueries({ await queryClient.invalidateQueries({
queryKey: preferencesQueryKey, queryKey: preferencesQueryKey,
+14 -4
View File
@@ -1,5 +1,7 @@
import {useState} from 'react' import {useState} from 'react'
import {type DidDocument, getPdsEndpoint} from '@atproto/common-web' 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 {useQuery, useQueryClient} from '@tanstack/react-query'
import {DEFAULT_SERVICE, PUBLIC_BSKY_SERVICE} from '#/lib/constants' 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 {isNetworkError} from '#/lib/strings/errors'
import {logger} from '#/logger' import {logger} from '#/logger'
import {STALE} from '#/state/queries' import {STALE} from '#/state/queries'
import {Agent} from '#/state/session/agent' import {com} from '#/lexicons'
const RQKEY_ROOT = 'pds-detection' const RQKEY_ROOT = 'pds-detection'
export const RQKEY = (identifier: string) => [RQKEY_ROOT, identifier] export const RQKEY = (identifier: string) => [RQKEY_ROOT, identifier]
@@ -147,16 +149,24 @@ export async function resolvePdsForIdentifier(
identifier: string, identifier: string,
): Promise<{did: string; pdsUrl: string | null} | null> { ): Promise<{did: string; pdsUrl: string | null} | null> {
const norm = normalizeIdentifier(identifier) 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 { try {
let did: string let did: string
if (norm.startsWith('did:')) { if (norm.startsWith('did:')) {
did = norm did = norm
} else { } else {
const res = await withResolveTimeout(signal => 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', { logger.debug('pds-detection: resolved identifier to DID', {
identifier: norm, identifier: norm,
+12 -5
View File
@@ -1,7 +1,8 @@
import {useQuery} from '@tanstack/react-query' import {useQuery} from '@tanstack/react-query'
import {STALE} from '#/state/queries' import {STALE} from '#/state/queries'
import {useAgent} from '#/state/session' import {useAppviewClient} from '#/state/session'
import {app} from '#/lexicons'
type ServiceConfig = { type ServiceConfig = {
checkEmailConfirmed: boolean checkEmailConfirmed: boolean
@@ -13,18 +14,24 @@ type ServiceConfig = {
} }
export function useServiceConfigQuery() { export function useServiceConfigQuery() {
const agent = useAgent() const appviewClient = useAppviewClient()
return useQuery<ServiceConfig>({ return useQuery<ServiceConfig>({
refetchOnWindowFocus: true, refetchOnWindowFocus: true,
staleTime: STALE.MINUTES.FIVE, staleTime: STALE.MINUTES.FIVE,
queryKey: ['service-config'], queryKey: ['service-config'],
queryFn: async () => { queryFn: async () => {
try { try {
const {data} = await agent.api.app.bsky.unspecced.getConfig() const data = await appviewClient.call(app.bsky.unspecced.getConfig)
return { return {
checkEmailConfirmed: Boolean(data.checkEmailConfirmed), 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 ?? [], liveNow: data.liveNow ?? [],
} }
} catch (e) { } catch (e) {
@@ -7,16 +7,15 @@ import {
import {getContentLanguages} from '#/state/preferences/languages' import {getContentLanguages} from '#/state/preferences/languages'
import {STALE} from '#/state/queries' import {STALE} from '#/state/queries'
import {usePreferencesQuery} from '#/state/queries/preferences' import {usePreferencesQuery} from '#/state/queries/preferences'
import {useAgent} from '#/state/session' import {useAppviewClient} from '#/state/session'
import {type app} from '#/lexicons' import {app} from '#/lexicons'
import {toLex} from '#/types/bsky'
export const DEFAULT_LIMIT = 15 export const DEFAULT_LIMIT = 15
export const createGetSuggestedFeedsQueryKey = () => ['suggested-feeds'] export const createGetSuggestedFeedsQueryKey = () => ['suggested-feeds']
export function useGetSuggestedFeedsQuery({enabled}: {enabled?: boolean}) { export function useGetSuggestedFeedsQuery({enabled}: {enabled?: boolean}) {
const agent = useAgent() const appviewClient = useAppviewClient()
const {data: preferences} = usePreferencesQuery() const {data: preferences} = usePreferencesQuery()
const savedFeeds = preferences?.savedFeeds const savedFeeds = preferences?.savedFeeds
@@ -26,7 +25,8 @@ export function useGetSuggestedFeedsQuery({enabled}: {enabled?: boolean}) {
queryKey: createGetSuggestedFeedsQueryKey(), queryKey: createGetSuggestedFeedsQueryKey(),
queryFn: async () => { queryFn: async () => {
const contentLangs = getContentLanguages().join(',') const contentLangs = getContentLanguages().join(',')
const {data} = await agent.app.bsky.unspecced.getSuggestedFeeds( const data = await appviewClient.call(
app.bsky.unspecced.getSuggestedFeeds,
{ {
limit: DEFAULT_LIMIT, 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 { return {
feeds: toLex<app.bsky.feed.defs.GeneratorView[]>( feeds: data.feeds.filter(feed => {
data.feeds.filter(feed => { const isSaved = !!savedFeeds?.find(s => s.value === feed.uri)
const isSaved = !!savedFeeds?.find(s => s.value === feed.uri) return !isSaved
return !isSaved }),
}),
),
} }
}, },
}) })
@@ -5,9 +5,8 @@ import {logger} from '#/logger'
import {getContentLanguages} from '#/state/preferences/languages' import {getContentLanguages} from '#/state/preferences/languages'
import {STALE} from '#/state/queries' import {STALE} from '#/state/queries'
import {usePreferencesQuery} from '#/state/queries/preferences' import {usePreferencesQuery} from '#/state/queries/preferences'
import {useAgent} from '#/state/session' import {useAppviewClient} from '#/state/session'
import {type app} from '#/lexicons' import {app} from '#/lexicons'
import {toLex} from '#/types/bsky'
export type QueryProps = { export type QueryProps = {
category?: string | null category?: string | null
@@ -28,7 +27,7 @@ export const createGetSuggestedOnboardingUsersQueryKey = (
] ]
export function useGetSuggestedOnboardingUsersQuery(props: QueryProps) { export function useGetSuggestedOnboardingUsersQuery(props: QueryProps) {
const agent = useAgent() const appviewClient = useAppviewClient()
const {data: preferences} = usePreferencesQuery() const {data: preferences} = usePreferencesQuery()
return useQuery({ return useQuery({
@@ -40,7 +39,8 @@ export function useGetSuggestedOnboardingUsersQuery(props: QueryProps) {
const overrideInterests = props.overrideInterests.join(',') 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, category: props.category ?? undefined,
limit: props.limit || 10, limit: props.limit || 10,
@@ -56,15 +56,7 @@ export function useGetSuggestedOnboardingUsersQuery(props: QueryProps) {
if (!data.recIdStr) { if (!data.recIdStr) {
logger.debug('getSuggestedOnboardingUsers response missing recIdStr') logger.debug('getSuggestedOnboardingUsers response missing recIdStr')
} }
/* return {...data, recId: data.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})
}, },
}) })
} }
@@ -8,9 +8,8 @@ import {logger} from '#/logger'
import {getContentLanguages} from '#/state/preferences/languages' import {getContentLanguages} from '#/state/preferences/languages'
import {STALE} from '#/state/queries' import {STALE} from '#/state/queries'
import {usePreferencesQuery} from '#/state/queries/preferences' import {usePreferencesQuery} from '#/state/queries/preferences'
import {useAgent} from '#/state/session' import {useAppviewClient} from '#/state/session'
import {type app} from '#/lexicons' import {app} from '#/lexicons'
import {toLex} from '#/types/bsky'
export type QueryProps = { export type QueryProps = {
limit?: number limit?: number
@@ -23,7 +22,7 @@ export const createGetSuggestedUsersForDiscoverQueryKey = (props: {
}) => [getSuggestedUsersForDiscoverQueryKeyRoot, props.limit] }) => [getSuggestedUsersForDiscoverQueryKeyRoot, props.limit]
export function useGetSuggestedUsersForDiscoverQuery(props: QueryProps = {}) { export function useGetSuggestedUsersForDiscoverQuery(props: QueryProps = {}) {
const agent = useAgent() const appviewClient = useAppviewClient()
const {data: preferences} = usePreferencesQuery() const {data: preferences} = usePreferencesQuery()
return useQuery({ return useQuery({
@@ -33,30 +32,22 @@ export function useGetSuggestedUsersForDiscoverQuery(props: QueryProps = {}) {
const contentLangs = getContentLanguages().join(',') const contentLangs = getContentLanguages().join(',')
const userInterests = aggregateUserInterests(preferences) const userInterests = aggregateUserInterests(preferences)
const {data} = const data = await appviewClient.call(
await agent.app.bsky.unspecced.getSuggestedUsersForDiscover( app.bsky.unspecced.getSuggestedUsersForDiscover,
{ {
limit: props.limit || 10, limit: props.limit || 10,
},
{
headers: {
...createBskyTopicsHeader(userInterests),
'Accept-Language': contentLangs,
}, },
{ },
headers: { )
...createBskyTopicsHeader(userInterests),
'Accept-Language': contentLangs,
},
},
)
if (!data.recIdStr) { if (!data.recIdStr) {
logger.debug('getSuggestedUsersForDiscover response missing recIdStr') logger.debug('getSuggestedUsersForDiscover response missing recIdStr')
} }
/* return {...data, recId: data.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})
}, },
}) })
} }
@@ -8,9 +8,8 @@ import {logger} from '#/logger'
import {getContentLanguages} from '#/state/preferences/languages' import {getContentLanguages} from '#/state/preferences/languages'
import {STALE} from '#/state/queries' import {STALE} from '#/state/queries'
import {usePreferencesQuery} from '#/state/queries/preferences' import {usePreferencesQuery} from '#/state/queries/preferences'
import {useAgent} from '#/state/session' import {useAppviewClient} from '#/state/session'
import {type app} from '#/lexicons' import {app} from '#/lexicons'
import {toLex} from '#/types/bsky'
export type QueryProps = { export type QueryProps = {
category?: string | null category?: string | null
@@ -24,7 +23,7 @@ export const createGetSuggestedUsersForExploreQueryKey = (
) => [getSuggestedUsersForExploreQueryKeyRoot, props.category, props.limit] ) => [getSuggestedUsersForExploreQueryKeyRoot, props.category, props.limit]
export function useGetSuggestedUsersForExploreQuery(props: QueryProps = {}) { export function useGetSuggestedUsersForExploreQuery(props: QueryProps = {}) {
const agent = useAgent() const appviewClient = useAppviewClient()
const {data: preferences} = usePreferencesQuery() const {data: preferences} = usePreferencesQuery()
return useQuery({ return useQuery({
@@ -34,7 +33,8 @@ export function useGetSuggestedUsersForExploreQuery(props: QueryProps = {}) {
const contentLangs = getContentLanguages().join(',') const contentLangs = getContentLanguages().join(',')
const userInterests = aggregateUserInterests(preferences) 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, category: props.category ?? undefined,
limit: props.limit || 10, limit: props.limit || 10,
@@ -50,14 +50,7 @@ export function useGetSuggestedUsersForExploreQuery(props: QueryProps = {}) {
if (!data.recIdStr) { if (!data.recIdStr) {
logger.debug('getSuggestedUsersForExplore response missing recIdStr') logger.debug('getSuggestedUsersForExplore response missing recIdStr')
} }
/* return {...data, recId: data.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})
}, },
}) })
} }
@@ -8,9 +8,8 @@ import {logger} from '#/logger'
import {getContentLanguages} from '#/state/preferences/languages' import {getContentLanguages} from '#/state/preferences/languages'
import {STALE} from '#/state/queries' import {STALE} from '#/state/queries'
import {usePreferencesQuery} from '#/state/queries/preferences' import {usePreferencesQuery} from '#/state/queries/preferences'
import {useAgent} from '#/state/session' import {useAppviewClient} from '#/state/session'
import {type app} from '#/lexicons' import {app} from '#/lexicons'
import {toLex} from '#/types/bsky'
export type QueryProps = { export type QueryProps = {
category?: string | null category?: string | null
@@ -26,7 +25,7 @@ export const createGetSuggestedUsersForSeeMoreQueryKey = (props: {
}) => [getSuggestedUsersForSeeMoreQueryKeyRoot, props.category, props.limit] }) => [getSuggestedUsersForSeeMoreQueryKeyRoot, props.category, props.limit]
export function useGetSuggestedUsersForSeeMoreQuery(props: QueryProps = {}) { export function useGetSuggestedUsersForSeeMoreQuery(props: QueryProps = {}) {
const agent = useAgent() const appviewClient = useAppviewClient()
const {data: preferences} = usePreferencesQuery() const {data: preferences} = usePreferencesQuery()
return useQuery({ return useQuery({
@@ -40,7 +39,8 @@ export function useGetSuggestedUsersForSeeMoreQuery(props: QueryProps = {}) {
const contentLangs = getContentLanguages().join(',') const contentLangs = getContentLanguages().join(',')
const userInterests = aggregateUserInterests(preferences) 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, category: props.category ?? undefined,
limit: props.limit || 50, limit: props.limit || 50,
@@ -56,14 +56,7 @@ export function useGetSuggestedUsersForSeeMoreQuery(props: QueryProps = {}) {
if (!data.recIdStr) { if (!data.recIdStr) {
logger.debug('getSuggestedUsersForSeeMore response missing recIdStr') logger.debug('getSuggestedUsersForSeeMore response missing recIdStr')
} }
/* return {...data, recId: data.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})
}, },
}) })
} }
@@ -7,9 +7,8 @@ import {
import {getContentLanguages} from '#/state/preferences/languages' import {getContentLanguages} from '#/state/preferences/languages'
import {STALE} from '#/state/queries' import {STALE} from '#/state/queries'
import {usePreferencesQuery} from '#/state/queries/preferences' import {usePreferencesQuery} from '#/state/queries/preferences'
import {useAgent} from '#/state/session' import {useAppviewClient} from '#/state/session'
import {type app} from '#/lexicons' import {app} from '#/lexicons'
import {toLex} from '#/types/bsky'
export const createOnboardingSuggestedStarterPacksQueryKey = ( export const createOnboardingSuggestedStarterPacksQueryKey = (
interests?: string[], interests?: string[],
@@ -22,7 +21,7 @@ export function useOnboardingSuggestedStarterPacksQuery({
enabled?: boolean enabled?: boolean
overrideInterests?: string[] overrideInterests?: string[]
}) { }) {
const agent = useAgent() const appviewClient = useAppviewClient()
const {data: preferences} = usePreferencesQuery() const {data: preferences} = usePreferencesQuery()
const contentLangs = getContentLanguages().join(',') const contentLangs = getContentLanguages().join(',')
@@ -31,27 +30,19 @@ export function useOnboardingSuggestedStarterPacksQuery({
staleTime: STALE.MINUTES.THREE, staleTime: STALE.MINUTES.THREE,
queryKey: createOnboardingSuggestedStarterPacksQueryKey(overrideInterests), queryKey: createOnboardingSuggestedStarterPacksQueryKey(overrideInterests),
queryFn: async () => { queryFn: async () => {
const {data} = return await appviewClient.call(
await agent.app.bsky.unspecced.getOnboardingSuggestedStarterPacks( app.bsky.unspecced.getOnboardingSuggestedStarterPacks,
{limit: 6}, {limit: 6},
{ {
headers: { headers: {
...createBskyTopicsHeader( ...createBskyTopicsHeader(
overrideInterests overrideInterests
? overrideInterests.join(',') ? overrideInterests.join(',')
: aggregateUserInterests(preferences), : aggregateUserInterests(preferences),
), ),
'Accept-Language': contentLangs, '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 {getContentLanguages} from '#/state/preferences/languages'
import {STALE} from '#/state/queries' import {STALE} from '#/state/queries'
import {usePreferencesQuery} from '#/state/queries/preferences' import {usePreferencesQuery} from '#/state/queries/preferences'
import {useAgent} from '#/state/session' import {useAppviewClient} from '#/state/session'
import {type app} from '#/lexicons' import {app} from '#/lexicons'
import {toLex} from '#/types/bsky'
export const createSuggestedStarterPacksQueryKey = (interests?: string[]) => [ export const createSuggestedStarterPacksQueryKey = (interests?: string[]) => [
'suggested-starter-packs', 'suggested-starter-packs',
@@ -23,7 +22,7 @@ export function useSuggestedStarterPacksQuery({
enabled?: boolean enabled?: boolean
overrideInterests?: string[] overrideInterests?: string[]
}) { }) {
const agent = useAgent() const appviewClient = useAppviewClient()
const {data: preferences} = usePreferencesQuery() const {data: preferences} = usePreferencesQuery()
const contentLangs = getContentLanguages().join(',') const contentLangs = getContentLanguages().join(',')
@@ -32,8 +31,9 @@ export function useSuggestedStarterPacksQuery({
staleTime: STALE.MINUTES.THREE, staleTime: STALE.MINUTES.THREE,
queryKey: createSuggestedStarterPacksQueryKey(overrideInterests), queryKey: createSuggestedStarterPacksQueryKey(overrideInterests),
queryFn: async () => { queryFn: async () => {
const {data} = await agent.app.bsky.unspecced.getSuggestedStarterPacks( return await appviewClient.call(
undefined, app.bsky.unspecced.getSuggestedStarterPacks,
{},
{ {
headers: { headers: {
...createBskyTopicsHeader( ...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 {PasswordSession} from '@atproto/lex-password-session'
import {isJwtExpired} from '#/lib/jwt' import {isJwtExpired} from '#/lib/jwt'
import {type TemporaryPushClient} from '#/lib/notifications/notifications'
import * as persisted from '#/state/persisted' import * as persisted from '#/state/persisted'
import { import {networkAwareFetch, sessionAccountToSessionData} from './session-core'
networkAwareFetch,
sessionAccountToSessionData,
SessionAgent,
} from './session-core'
import {type SessionAccount} from './types' import {type SessionAccount} from './types'
/* /*
@@ -33,22 +31,30 @@ export function isSessionExpired(account: SessionAccount) {
* Creates and resumes a throwaway session for every stored account. * Creates and resumes a throwaway session for every stored account.
* Intended to send push token revocations just before logout. * 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 * resumed over the network to obtain a valid access token. These sessions are
* deliberately hook-free (no `onUpdated`/`onDeleted`): they must NEVER persist * deliberately hook-free (no `onUpdated`/`onDeleted`): they must NEVER persist
* or race the active session. They are used once for the unregister call and * 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. * 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( export async function createTemporaryAgentsAndResume(
accounts: SessionAccount[], accounts: SessionAccount[],
): Promise<SessionAgent[]> { ): Promise<TemporaryPushClient[]> {
const settled = await Promise.allSettled( const settled = await Promise.allSettled(
accounts.map(async account => { accounts.map(async account => {
const session = await PasswordSession.resume( const session = await PasswordSession.resume(
sessionAccountToSessionData(account), sessionAccountToSessionData(account),
{fetch: networkAwareFetch}, {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 {useCallback, useEffect, useState} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {useSafeAreaInsets} from 'react-native-safe-area-context' import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {type AtIdentifierString} from '@atproto/syntax'
import {useLingui} from '@lingui/react/macro' import {useLingui} from '@lingui/react/macro'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
import {PressableScale} from '#/lib/custom-animations/PressableScale' import {PressableScale} from '#/lib/custom-animations/PressableScale'
import {STALE} from '#/state/queries' import {STALE} from '#/state/queries'
import {profilesQueryKey} from '#/state/queries/profile' 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 {useSetActiveLanding} from '#/state/shell/landing'
import { import {
useLoggedOutView, useLoggedOutView,
@@ -23,6 +24,7 @@ import {atoms as a, native, tokens, useTheme} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button' import {Button, ButtonIcon} from '#/components/Button'
import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times' import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
import {useAnalytics} from '#/analytics' import {useAnalytics} from '#/analytics'
import {app} from '#/lexicons'
import {SplashScreen} from './SplashScreen' import {SplashScreen} from './SplashScreen'
enum ScreenState { enum ScreenState {
@@ -65,7 +67,7 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const {accounts} = useSession() const {accounts} = useSession()
const agent = useAgent() const appviewClient = useAppviewClient()
useEffect(() => { useEffect(() => {
const actors = accounts.map(acc => acc.did) const actors = accounts.map(acc => acc.did)
if (actors.length === 0) return if (actors.length === 0) return
@@ -73,11 +75,12 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
queryKey: profilesQueryKey(actors), queryKey: profilesQueryKey(actors),
staleTime: STALE.MINUTES.FIVE, staleTime: STALE.MINUTES.FIVE,
queryFn: async () => { queryFn: async () => {
const res = await agent.getProfiles({actors}) return await appviewClient.call(app.bsky.actor.getProfiles, {
return res.data actors: actors as AtIdentifierString[],
})
}, },
}) })
}, [accounts, agent, queryClient]) }, [accounts, appviewClient, queryClient])
const onPressDismiss = useCallback(() => { const onPressDismiss = useCallback(() => {
if (onDismiss) { 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 {useQueryClient} from '@tanstack/react-query'
import {BLUESKY_PROXY_HEADER} from '#/lib/constants' 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 {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {useOnboardingDispatch} from '#/state/shell/onboarding' import {useOnboardingDispatch} from '#/state/shell/onboarding'
import {navigate} from '../../../Navigation' import {navigate} from '../../../Navigation'
@@ -31,7 +31,6 @@ const BTN = {height: 1, width: 1, backgroundColor: 'red'}
let hasConfiguredProxy = false let hasConfiguredProxy = false
export function TestCtrls() { export function TestCtrls() {
const agent = useAgent()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const {logoutEveryAccount, login} = useSessionApi() const {logoutEveryAccount, login} = useSessionApi()
const onboardingDispatch = useOnboardingDispatch() const onboardingDispatch = useOnboardingDispatch()
@@ -74,8 +73,12 @@ export function TestCtrls() {
autoCapitalize="none" autoCapitalize="none"
onSubmitEditing={() => { onSubmitEditing={() => {
const header = `${proxyHeader}#bsky_appview` 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) BLUESKY_PROXY_HEADER.set(header)
agent.configureProxy(header as any)
hasConfiguredProxy = true hasConfiguredProxy = true
setIsProxyConfigured(true) setIsProxyConfigured(true)
}} }}