diff --git a/src/components/Autocomplete/useAutocomplete/index.ts b/src/components/Autocomplete/useAutocomplete/index.ts index 86dc9e7cd1..8b1c6af41f 100644 --- a/src/components/Autocomplete/useAutocomplete/index.ts +++ b/src/components/Autocomplete/useAutocomplete/index.ts @@ -6,14 +6,14 @@ import {isJustAMute, moduiContainsHideableOffense} from '#/lib/moderation' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {STALE} from '#/state/queries' import {DEFAULT_LOGGED_OUT_PREFERENCES} from '#/state/queries/preferences' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' import { type AutocompleteApi, type AutocompleteItem, type AutocompleteItemType, type AutocompleteProfile, } from '#/components/Autocomplete/types' -import {toLex} from '#/types/bsky' +import {app} from '#/lexicons' import {useEmojiSearch} from './useEmojiSearch' const DEFAULT_MOD_OPTS = { @@ -32,7 +32,7 @@ export function useAutocomplete({ limit?: number showSearchFallback?: boolean }): AutocompleteApi { - const agent = useAgent() + const appviewClient = useAppviewClient() const moderationOpts = useModerationOpts() const emojiSearch = useEmojiSearch() @@ -53,17 +53,19 @@ export function useAutocomplete({ // Going from "foo" to "foo." should not clear matches. q = q.toLowerCase().trim().replace(/\.$/, '') - const res = await agent.searchActorsTypeahead({ - q, - limit: limit || 8, - }) + const res = await appviewClient.call( + app.bsky.actor.searchActorsTypeahead, + { + q, + limit: limit || 8, + }, + ) - return (res?.data.actors || []).map(profile => ({ + return (res?.actors || []).map(profile => ({ key: profile.did, type: 'profile' as const, value: '@' + profile.handle, - // emits #/lexicons views - profile: toLex(profile), + profile, })) } else if (type === 'emoji') { return emojiSearch(q, limit || 8) diff --git a/src/components/activity-notifications/SubscribeProfileDialog.tsx b/src/components/activity-notifications/SubscribeProfileDialog.tsx index aba2c338ee..32c3e595e3 100644 --- a/src/components/activity-notifications/SubscribeProfileDialog.tsx +++ b/src/components/activity-notifications/SubscribeProfileDialog.tsx @@ -16,7 +16,7 @@ import {cleanError} from '#/lib/strings/errors' import {sanitizeHandle} from '#/lib/strings/handles' import {updateProfileShadow} from '#/state/cache/profile-shadow' import {RQKEY_getActivitySubscriptions} from '#/state/queries/activity-subscriptions' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' import {atoms as a, platform, useTheme, web} from '#/alf' import {Admonition} from '#/components/Admonition' import { @@ -33,7 +33,7 @@ import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' import {IS_WEB} from '#/env' -import {type app} from '#/lexicons' +import {app} from '#/lexicons' import type * as bsky from '#/types/bsky' export function SubscribeProfileDialog({ @@ -71,7 +71,7 @@ function DialogInner({ const ax = useAnalytics() const {_} = useLingui() const t = useTheme() - const agent = useAgent() + const appviewClient = useAppviewClient() const control = Dialog.useDialogContext() const queryClient = useQueryClient() const initialState = parseActivitySubscription( @@ -119,7 +119,7 @@ function DialogInner({ mutationFn: async ( activitySubscription: Un$Typed, ) => { - await agent.app.bsky.notification.putActivitySubscription({ + await appviewClient.call(app.bsky.notification.putActivitySubscription, { subject: profile.did, activitySubscription, }) diff --git a/src/components/ageAssurance/AgeAssuranceAppealDialog.tsx b/src/components/ageAssurance/AgeAssuranceAppealDialog.tsx index 9a7db4cdfc..39d940863e 100644 --- a/src/components/ageAssurance/AgeAssuranceAppealDialog.tsx +++ b/src/components/ageAssurance/AgeAssuranceAppealDialog.tsx @@ -12,12 +12,12 @@ import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import {Loader} from '#/components/Loader' +import {accountReportSubject} from '#/components/moderation/ReportDialog/utils/reportSubject' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {logger} from '#/ageAssurance' import {useAnalytics} from '#/analytics' import {com, tools} from '#/lexicons' -import {toLex} from '#/types/bsky' export function AgeAssuranceAppealDialog({ control, @@ -54,14 +54,11 @@ function Inner({control}: {control: Dialog.DialogControlProps}) { await pdsClient.call( com.atproto.moderation.createReport, - toLex({ + { reasonType: tools.ozone.report.defs.reasonAppeal.value, - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: currentAccount?.did, - }, + subject: accountReportSubject(currentAccount?.did ?? ''), reason: `AGE_ASSURANCE_INQUIRY: ` + details, - }), + }, { service: api.moderation.service, }, diff --git a/src/components/contacts/screens/PhoneInput.tsx b/src/components/contacts/screens/PhoneInput.tsx index 8426ec04e0..4655817f6c 100644 --- a/src/components/contacts/screens/PhoneInput.tsx +++ b/src/components/contacts/screens/PhoneInput.tsx @@ -15,7 +15,7 @@ import { import {cleanError, isNetworkError} from '#/lib/strings/errors' import {getErrorName} from '#/lib/xrpc-error' import {logger} from '#/logger' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' import {OnboardingPosition} from '#/screens/Onboarding/Layout' import { android, @@ -34,6 +34,7 @@ import {Loader} from '#/components/Loader' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' import {useGeolocation} from '#/geolocation' +import {app} from '#/lexicons' import {isFindContactsFeatureEnabled} from '../country-allowlist' import { constructFullPhoneNumber, @@ -56,7 +57,7 @@ export function PhoneInput({ const {_} = useLingui() const ax = useAnalytics() const t = useTheme() - const agent = useAgent() + const appviewClient = useAppviewClient() const location = useGeolocation() const [countryCode, setCountryCode] = useState( () => state.phoneCountryCode ?? getDefaultCountry(location), @@ -78,7 +79,7 @@ export function PhoneInput({ phoneNumber: string }) => { // sends a onetime code to the user's phone number - await agent.app.bsky.contact.startPhoneVerification({ + await appviewClient.call(app.bsky.contact.startPhoneVerification, { phone: constructFullPhoneNumber(phoneCountryCode, phoneNumber), }) }, diff --git a/src/components/contacts/screens/VerifyNumber.tsx b/src/components/contacts/screens/VerifyNumber.tsx index 2ecf081671..0596808c53 100644 --- a/src/components/contacts/screens/VerifyNumber.tsx +++ b/src/components/contacts/screens/VerifyNumber.tsx @@ -9,7 +9,7 @@ import {clamp} from '#/lib/numbers' import {cleanError, isNetworkError} from '#/lib/strings/errors' import {getErrorName} from '#/lib/xrpc-error' import {logger} from '#/logger' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' import {OnboardingPosition} from '#/screens/Onboarding/Layout' import {atoms as a, useGutters, useTheme} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' @@ -22,6 +22,7 @@ import {Loader} from '#/components/Loader' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' +import {app} from '#/lexicons' import {OTPInput} from '../components/OTPInput' import {constructFullPhoneNumber, prettyPhoneNumber} from '../phone-number' import {type Action, type State, useOnPressBackButton} from '../state' @@ -40,7 +41,7 @@ export function VerifyNumber({ const t = useTheme() const {_} = useLingui() const ax = useAnalytics() - const agent = useAgent() + const appviewClient = useAppviewClient() const gutters = useGutters([0, 'wide']) const [otpCode, setOtpCode] = useState('') @@ -69,8 +70,11 @@ export function VerifyNumber({ isSuccess, } = useMutation({ mutationFn: async (code: string) => { - const res = await agent.app.bsky.contact.verifyPhone({code, phone}) - return res.data.token + const res = await appviewClient.call(app.bsky.contact.verifyPhone, { + code, + phone, + }) + return res.token }, onSuccess: async token => { // let the success state show for a moment @@ -131,7 +135,9 @@ export function VerifyNumber({ const {mutate: resendCode, isPending: isResendingCode} = useMutation({ mutationFn: async () => { - await agent.app.bsky.contact.startPhoneVerification({phone: phone}) + await appviewClient.call(app.bsky.contact.startPhoneVerification, { + phone: phone, + }) }, onSuccess: () => { dispatch({type: 'RESEND_VERIFICATION_CODE'}) diff --git a/src/components/contacts/screens/ViewMatches.tsx b/src/components/contacts/screens/ViewMatches.tsx index 1154b582b4..d77cef33d6 100644 --- a/src/components/contacts/screens/ViewMatches.tsx +++ b/src/components/contacts/screens/ViewMatches.tsx @@ -2,6 +2,7 @@ import {useCallback, useMemo, useRef, useState} from 'react' import {View} from 'react-native' import {useSafeAreaInsets} from 'react-native-safe-area-context' import * as SMS from 'expo-sms' +import {type DidString} from '@atproto/syntax' import {type ModerationOpts} from '@bsky.app/sdk/moderation' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' @@ -20,12 +21,7 @@ import { optimisticRemoveMatch, useMatchesPassthroughQuery, } from '#/state/queries/find-contacts' -import { - useAgent, - useAppviewClient, - usePdsClient, - useSession, -} from '#/state/session' +import {useAppviewClient, usePdsClient, useSession} from '#/state/session' import {List, type ListMethods} from '#/view/com/util/List' import {UserAvatar} from '#/view/com/util/UserAvatar' import {OnboardingPosition} from '#/screens/Onboarding/Layout' @@ -46,6 +42,7 @@ import * as ProfileCard from '#/components/ProfileCard' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' +import {app} from '#/lexicons' import type * as bsky from '#/types/bsky' import {InviteInfo} from '../components/InviteInfo' import {type Action, type Contact, type Match, type State} from '../state' @@ -94,7 +91,6 @@ export function ViewMatches({ const gutter = useGutters([0, 'wide']) const moderationOpts = useModerationOpts() const queryClient = useQueryClient() - const agent = useAgent() const pdsClient = usePdsClient() const appviewClient = useAppviewClient() const insets = useSafeAreaInsets() @@ -228,7 +224,9 @@ export function ViewMatches({ const {mutate: dismissMatch} = useMutation({ mutationFn: async (did: string) => { - await agent.app.bsky.contact.dismissMatch({subject: did}) + await appviewClient.call(app.bsky.contact.dismissMatch, { + subject: did as DidString, + }) }, onMutate: did => { ax.metric('contacts:matches:dismiss', {entryPoint: context}) diff --git a/src/components/dialogs/EmailDialog/data/useConfirmEmail.ts b/src/components/dialogs/EmailDialog/data/useConfirmEmail.ts index 67466be926..00472be52f 100644 --- a/src/components/dialogs/EmailDialog/data/useConfirmEmail.ts +++ b/src/components/dialogs/EmailDialog/data/useConfirmEmail.ts @@ -1,13 +1,15 @@ import {useMutation} from '@tanstack/react-query' -import {useAgent, useSession} from '#/state/session' +import {usePdsClient, useSession, useSessionApi} from '#/state/session' +import {com} from '#/lexicons' export function useConfirmEmail({ onSuccess, onError, }: {onSuccess?: () => void; onError?: () => void} = {}) { - const agent = useAgent() + const pdsClient = usePdsClient() const {currentAccount} = useSession() + const {refreshSession} = useSessionApi() return useMutation({ mutationFn: async ({token}: {token: string}) => { @@ -15,12 +17,12 @@ export function useConfirmEmail({ throw new Error('No email found for the current account') } - await agent.com.atproto.server.confirmEmail({ + await pdsClient.call(com.atproto.server.confirmEmail, { email: currentAccount.email.trim(), token: token.trim(), }) // will update session state at root of app - await agent.resumeSession(agent.session!) + await refreshSession() }, onSuccess, onError, diff --git a/src/components/dialogs/EmailDialog/data/useManageEmail2FA.ts b/src/components/dialogs/EmailDialog/data/useManageEmail2FA.ts index 358bf86544..d00e487474 100644 --- a/src/components/dialogs/EmailDialog/data/useManageEmail2FA.ts +++ b/src/components/dialogs/EmailDialog/data/useManageEmail2FA.ts @@ -1,10 +1,12 @@ import {useMutation} from '@tanstack/react-query' -import {useAgent, useSession} from '#/state/session' +import {usePdsClient, useSession, useSessionApi} from '#/state/session' +import {com} from '#/lexicons' export function useManageEmail2FA() { - const agent = useAgent() + const pdsClient = usePdsClient() const {currentAccount} = useSession() + const {refreshSession} = useSessionApi() return useMutation({ mutationFn: async ({ @@ -17,13 +19,13 @@ export function useManageEmail2FA() { throw new Error('No email found for the current account') } - await agent.com.atproto.server.updateEmail({ + await pdsClient.call(com.atproto.server.updateEmail, { email: currentAccount.email, emailAuthFactor: enabled, token, }) // will update session state at root of app - await agent.resumeSession(agent.session!) + await refreshSession() }, }) } diff --git a/src/components/dialogs/EmailDialog/data/useRequestEmailUpdate.ts b/src/components/dialogs/EmailDialog/data/useRequestEmailUpdate.ts index a442662fcf..a98f0c1097 100644 --- a/src/components/dialogs/EmailDialog/data/useRequestEmailUpdate.ts +++ b/src/components/dialogs/EmailDialog/data/useRequestEmailUpdate.ts @@ -1,13 +1,14 @@ import {useMutation} from '@tanstack/react-query' -import {useAgent} from '#/state/session' +import {usePdsClient} from '#/state/session' +import {com} from '#/lexicons' export function useRequestEmailUpdate() { - const agent = useAgent() + const pdsClient = usePdsClient() return useMutation({ mutationFn: async () => { - return (await agent.com.atproto.server.requestEmailUpdate()).data + return await pdsClient.call(com.atproto.server.requestEmailUpdate) }, }) } diff --git a/src/components/dialogs/EmailDialog/data/useRequestEmailVerification.ts b/src/components/dialogs/EmailDialog/data/useRequestEmailVerification.ts index ae308c7afc..894cd54a3b 100644 --- a/src/components/dialogs/EmailDialog/data/useRequestEmailVerification.ts +++ b/src/components/dialogs/EmailDialog/data/useRequestEmailVerification.ts @@ -1,13 +1,14 @@ import {useMutation} from '@tanstack/react-query' -import {useAgent} from '#/state/session' +import {usePdsClient} from '#/state/session' +import {com} from '#/lexicons' export function useRequestEmailVerification() { - const agent = useAgent() + const pdsClient = usePdsClient() return useMutation({ mutationFn: async () => { - await agent.com.atproto.server.requestEmailConfirmation() + await pdsClient.call(com.atproto.server.requestEmailConfirmation) }, }) } diff --git a/src/components/dialogs/EmailDialog/data/useUpdateEmail.ts b/src/components/dialogs/EmailDialog/data/useUpdateEmail.ts index 2ec1eb6dc2..167cebda3d 100644 --- a/src/components/dialogs/EmailDialog/data/useUpdateEmail.ts +++ b/src/components/dialogs/EmailDialog/data/useUpdateEmail.ts @@ -1,19 +1,26 @@ +import {type Client} from '@atproto/lex-client' import {useMutation} from '@tanstack/react-query' -import {useAgent} from '#/state/session' +import {usePdsClient, useSessionApi} from '#/state/session' import {useRequestEmailUpdate} from '#/components/dialogs/EmailDialog/data/useRequestEmailUpdate' +import {com} from '#/lexicons' async function updateEmailAndRefreshSession( - agent: ReturnType, + pdsClient: Client, + refreshSession: () => Promise, email: string, token?: string, ) { - await agent.com.atproto.server.updateEmail({email: email.trim(), token}) - await agent.resumeSession(agent.session!) + await pdsClient.call(com.atproto.server.updateEmail, { + email: email.trim(), + token, + }) + await refreshSession() } export function useUpdateEmail() { - const agent = useAgent() + const pdsClient = usePdsClient() + const {refreshSession} = useSessionApi() const {mutateAsync: requestEmailUpdate} = useRequestEmailUpdate() return useMutation< @@ -23,7 +30,12 @@ export function useUpdateEmail() { >({ mutationFn: async ({email, token}: {email: string; token?: string}) => { if (token) { - await updateEmailAndRefreshSession(agent, email, token) + await updateEmailAndRefreshSession( + pdsClient, + refreshSession, + email, + token, + ) return { status: 'success', } @@ -34,7 +46,12 @@ export function useUpdateEmail() { status: 'tokenRequired', } } else { - await updateEmailAndRefreshSession(agent, email, token) + await updateEmailAndRefreshSession( + pdsClient, + refreshSession, + email, + token, + ) return { status: 'success', } diff --git a/src/components/intents/VerifyEmailIntentDialog.tsx b/src/components/intents/VerifyEmailIntentDialog.tsx index b3504766f8..2a7746d012 100644 --- a/src/components/intents/VerifyEmailIntentDialog.tsx +++ b/src/components/intents/VerifyEmailIntentDialog.tsx @@ -4,7 +4,7 @@ import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' -import {useAgent, useSession} from '#/state/session' +import {usePdsClient, useSession} from '#/state/session' import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' @@ -16,6 +16,7 @@ import {useIntentDialogs} from '#/components/intents/IntentDialogs' import {Loader} from '#/components/Loader' import {Text} from '#/components/Typography' import {IS_NATIVE} from '#/env' +import {com} from '#/lexicons' export function VerifyEmailIntentDialog() { const {verifyEmailDialogControl: control} = useIntentDialogs() @@ -37,7 +38,7 @@ function Inner({}: {control: DialogControlProps}) { 'loading' | 'success' | 'failure' | 'resent' >('loading') const [sending, setSending] = useState(false) - const agent = useAgent() + const pdsClient = usePdsClient() const {currentAccount} = useSession() const {mutate: confirmEmail} = useConfirmEmail({ onSuccess: () => setStatus('success'), @@ -52,7 +53,7 @@ function Inner({}: {control: DialogControlProps}) { const onPressResendEmail = async () => { setSending(true) - await agent.com.atproto.server.requestEmailConfirmation() + await pdsClient.call(com.atproto.server.requestEmailConfirmation) setSending(false) setStatus('resent') } diff --git a/src/components/moderation/AppealForm.tsx b/src/components/moderation/AppealForm.tsx index ddf7080089..31a2c4d339 100644 --- a/src/components/moderation/AppealForm.tsx +++ b/src/components/moderation/AppealForm.tsx @@ -19,11 +19,14 @@ import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import {InlineLinkText} from '#/components/Link' import {Loader} from '#/components/Loader' +import { + accountReportSubject, + recordReportSubject, +} from '#/components/moderation/ReportDialog/utils/reportSubject' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {IS_ANDROID} from '#/env' import {com, tools} from '#/lexicons' -import {toLex} from '#/types/bsky' export function AppealForm({ label, @@ -39,7 +42,6 @@ export function AppealForm({ const {gtMobile} = useBreakpoints() const [details, setDetails] = useState('') const {subject} = useLabelSubject({label}) - const isAccountReport = 'did' in subject const pdsClient = usePdsClient() const sourceName = labeler ? sanitizeHandle(labeler.creator.handle, '@') @@ -48,19 +50,16 @@ export function AppealForm({ const {mutate, isPending} = useMutation({ mutationFn: async () => { - const $type = !isAccountReport - ? 'com.atproto.repo.strongRef' - : 'com.atproto.admin.defs#repoRef' await pdsClient.call( com.atproto.moderation.createReport, - toLex({ + { reasonType: tools.ozone.report.defs.reasonAppeal.value, - subject: { - $type, - ...subject, - }, + subject: + 'did' in subject + ? accountReportSubject(subject.did) + : recordReportSubject(subject.uri, subject.cid), reason: details, - }), + }, { service: `${label.src}#atproto_labeler` as Service, }, diff --git a/src/components/moderation/ReportDialog/action.ts b/src/components/moderation/ReportDialog/action.ts index 2e3177c47e..1a6e6f64e9 100644 --- a/src/components/moderation/ReportDialog/action.ts +++ b/src/components/moderation/ReportDialog/action.ts @@ -6,10 +6,14 @@ import {useMutation} from '@tanstack/react-query' import {logger} from '#/logger' import {usePdsClient} from '#/state/session' import {com} from '#/lexicons' -import {toLex} from '#/types/bsky' import {NEW_TO_OLD_REASONS_MAP} from './const' import {type ReportState} from './state' import {type ParsedReportSubject} from './types' +import { + accountReportSubject, + chatReportSubject, + recordReportSubject, +} from './utils/reportSubject' type CreateReportBody = com.atproto.moderation.createReport.$InputBody @@ -54,25 +58,20 @@ export function useSubmitReportMutation() { /* * The generated `createReport` subject union only declares repoRef and - * strongRef with branded did/uri strings; chat subjects (message/convo - * refs) are accepted on the wire but not in the lexicon, and the subject - * ids we hold here are plain strings. We build the body against a loose - * subject shape and `toLex` it to the schema body at the call boundary - * (matching the old widened-InputSchema shape). + * strongRef; chat subjects (message/convo refs) are accepted on the wire + * but not in the lexicon. The builders in `./utils/reportSubject` brand + * the plain-string ids into the schema `subject` slot, keeping the + * runtime values exact and confining the chat-subject assertion to one + * place. */ - let report: Omit & { - subject: {$type: string} & Record - } + let report: CreateReportBody switch (subject.type) { case 'account': { report = { reasonType, reason: state.details, - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: subject.did, - }, + subject: accountReportSubject(subject.did), } break } @@ -84,11 +83,7 @@ export function useSubmitReportMutation() { report = { reasonType, reason: state.details, - subject: { - $type: 'com.atproto.repo.strongRef', - uri: subject.uri, - cid: subject.cid, - }, + subject: recordReportSubject(subject.uri, subject.cid), } break } @@ -96,12 +91,12 @@ export function useSubmitReportMutation() { report = { reasonType, reason: state.details, - subject: { + subject: chatReportSubject({ $type: 'chat.bsky.convo.defs#messageRef', messageId: subject.message.id, convoId: subject.convoId, did: subject.message.sender.did, - }, + }), } break } @@ -109,11 +104,11 @@ export function useSubmitReportMutation() { report = { reasonType, reason: state.details, - subject: { + subject: chatReportSubject({ $type: 'chat.bsky.convo.defs#convoRef', convoId: subject.convoId, did: subject.did, - }, + }), } break } @@ -133,13 +128,9 @@ export function useSubmitReportMutation() { * per-call `service` option (previously an explicit header on the * bridge agent). */ - await pdsClient.call( - com.atproto.moderation.createReport, - toLex(report), - { - service: `${labeler.creator.did}#atproto_labeler` as Service, - }, - ) + await pdsClient.call(com.atproto.moderation.createReport, report, { + service: `${labeler.creator.did}#atproto_labeler` as Service, + }) } }, }) diff --git a/src/components/moderation/ReportDialog/utils/__tests__/reportSubject.test.ts b/src/components/moderation/ReportDialog/utils/__tests__/reportSubject.test.ts new file mode 100644 index 0000000000..0ac77bef51 --- /dev/null +++ b/src/components/moderation/ReportDialog/utils/__tests__/reportSubject.test.ts @@ -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', + }) + }) +}) diff --git a/src/components/moderation/ReportDialog/utils/reportSubject.ts b/src/components/moderation/ReportDialog/utils/reportSubject.ts new file mode 100644 index 0000000000..a639323b9b --- /dev/null +++ b/src/components/moderation/ReportDialog/utils/reportSubject.ts @@ -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 +} diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 89dff6145b..c16a8d3b4b 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -1,10 +1,17 @@ import {type Insets, Platform} from 'react-native' +import {type Service} from '@atproto/lex-client' import {api} from '@bsky.app/sdk' -import {type ProxyHeaderValue} from '#/state/session/agent' import {BLUESKY_PROXY_DID, IS_DEV} from '#/env' import {type app} from '#/lexicons' +/** + * The `atproto-proxy` header value: a DID plus a service fragment, e.g. + * `did:web:api.bsky.app#bsky_appview`. Kept local to this module (previously + * lived in the now-removed session `agent.ts` compat layer). + */ +type ProxyHeaderValue = `did:${string}:${string}#${string}` + export const LOCAL_DEV_SERVICE = Platform.OS === 'android' ? 'http://10.0.2.2:2583' : 'http://localhost:2583' export const STAGING_SERVICE = 'https://staging.bsky.dev' @@ -251,9 +258,12 @@ export const BLUESKY_MOD_SERVICE_HEADERS = { 'atproto-proxy': `${api.moderation.did}#atproto_labeler`, } -export const BLUESKY_NOTIF_SERVICE_HEADERS = { - 'atproto-proxy': `${BLUESKY_PROXY_DID}#bsky_notif`, -} +/** + * Service proxy identifier for the notification/entryway service. Passed as the + * per-call `service` option on the account client so lex-client emits the + * `atproto-proxy` header (replaces the old `BLUESKY_NOTIF_SERVICE_HEADERS`). + */ +export const NOTIF_SERVICE = `${BLUESKY_PROXY_DID}#bsky_notif` as Service export const webLinks = { tos: `https://bsky.social/about/support/tos`, diff --git a/src/lib/notifications/__tests__/notifications-proxy.test.ts b/src/lib/notifications/__tests__/notifications-proxy.test.ts new file mode 100644 index 0000000000..5c3a909723 --- /dev/null +++ b/src/lib/notifications/__tests__/notifications-proxy.test.ts @@ -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: 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) + }) +}) diff --git a/src/lib/notifications/notifications.ts b/src/lib/notifications/notifications.ts index 580b50d881..0e8e78cd0b 100644 --- a/src/lib/notifications/notifications.ts +++ b/src/lib/notifications/notifications.ts @@ -2,34 +2,47 @@ import {useCallback, useEffect} from 'react' import {Platform} from 'react-native' import * as Notifications from 'expo-notifications' import {getBadgeCountAsync, setBadgeCountAsync} from 'expo-notifications' +import {type Client} from '@atproto/lex-client' import debounce from 'lodash.debounce' import { - BLUESKY_NOTIF_SERVICE_HEADERS, + NOTIF_SERVICE, PUBLIC_APPVIEW_DID, PUBLIC_STAGING_APPVIEW_DID, } from '#/lib/constants' import {logger as notyLogger} from '#/lib/notifications/util' import {isNetworkError} from '#/lib/strings/errors' -import {type SessionAccount, useAgent, useSession} from '#/state/session' -import {type SessionAgent} from '#/state/session/session-core' +import {type SessionAccount, usePdsClient, useSession} from '#/state/session' import BackgroundNotificationHandler from '#/../modules/expo-background-notification-handler' import {useAgeAssurance} from '#/ageAssurance' import {useAnalytics} from '#/analytics' import {IS_DEV, IS_NATIVE} from '#/env' -import {type app} from '#/lexicons' +import {app} from '#/lexicons' + +/** + * A resumed throwaway account client paired with the account's service origin + * and handle. Produced by `createTemporaryClientsAndResume` (session util) and + * consumed by {@link unregisterPushToken}, which needs the service host to pick + * the correct appview DID and the handle for a debug log line without reaching + * into the session internals. + */ +export type TemporaryPushClient = { + client: Client + service: string + handle: string +} /** * @private * Registers the device's push notification token with the Bluesky server. */ async function _registerPushToken({ - agent, + client, currentAccount, token, extra = {}, }: { - agent: SessionAgent + client: Client currentAccount: SessionAccount token: Notifications.DevicePushToken extra?: { @@ -49,8 +62,8 @@ async function _registerPushToken({ notyLogger.debug(`registerPushToken: registering`, {...payload}) - await agent.app.bsky.notification.registerPush(payload, { - headers: BLUESKY_NOTIF_SERVICE_HEADERS, + await client.call(app.bsky.notification.registerPush, payload, { + service: NOTIF_SERVICE, }) notyLogger.debug(`registerPushToken: success`) @@ -75,7 +88,7 @@ const _registerPushTokenDebounced = debounce(_registerPushToken, 100) * `_registerPushTokenDebounced` directly. */ export function useRegisterPushToken() { - const agent = useAgent() + const client = usePdsClient() const {currentAccount} = useSession() return useCallback( @@ -88,7 +101,7 @@ export function useRegisterPushToken() { }) => { if (!currentAccount) return return _registerPushTokenDebounced({ - agent, + client, currentAccount, token, extra: { @@ -96,7 +109,7 @@ export function useRegisterPushToken() { }, }) }, - [agent, currentAccount], + [client, currentAccount], ) } @@ -327,16 +340,17 @@ export async function resetBadgeCount() { await setBadgeCountAsync(0) } -export async function unregisterPushToken(agents: SessionAgent[]) { +export async function unregisterPushToken(clients: TemporaryPushClient[]) { if (!IS_NATIVE) return try { const token = await getPushToken() if (token) { - for (const agent of agents) { - await agent.app.bsky.notification.unregisterPush( + for (const {client, service, handle} of clients) { + await client.call( + app.bsky.notification.unregisterPush, { - serviceDid: agent.serviceUrl.hostname.includes('staging') + serviceDid: service.includes('staging') ? PUBLIC_STAGING_APPVIEW_DID : PUBLIC_APPVIEW_DID, platform: Platform.OS, @@ -344,10 +358,10 @@ export async function unregisterPushToken(agents: SessionAgent[]) { appId: 'xyz.blueskyweb.app', }, { - headers: BLUESKY_NOTIF_SERVICE_HEADERS, + service: NOTIF_SERVICE, }, ) - notyLogger.debug(`Push token unregistered for ${agent.session?.handle}`) + notyLogger.debug(`Push token unregistered for ${handle}`) } } else { notyLogger.debug('Tried to unregister push token, but could not find one') diff --git a/src/screens/Deactivated.tsx b/src/screens/Deactivated.tsx index 2782c5ef0d..db4f60eada 100644 --- a/src/screens/Deactivated.tsx +++ b/src/screens/Deactivated.tsx @@ -10,7 +10,7 @@ import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher' import {logger} from '#/logger' import { type SessionAccount, - useAgent, + usePdsClient, useSession, useSessionApi, } from '#/state/session' @@ -25,6 +25,7 @@ import * as Layout from '#/components/Layout' import {Loader} from '#/components/Loader' import {Text} from '#/components/Typography' import {IS_WEB} from '#/env' +import {com} from '#/lexicons' const COL_WIDTH = 400 @@ -36,8 +37,8 @@ export function Deactivated() { const {onPressSwitchAccount, pendingDid} = useAccountSwitcher() const {setShowLoggedOut} = useLoggedOutViewControls() const hasOtherAccounts = accounts.length > 1 - const {logoutCurrentAccount} = useSessionApi() - const agent = useAgent() + const {logoutCurrentAccount, refreshSession} = useSessionApi() + const pdsClient = usePdsClient() const [pending, setPending] = useState(false) const [error, setError] = useState() const queryClient = useQueryClient() @@ -70,9 +71,9 @@ export function Deactivated() { const handleActivate = useCallback(async () => { try { setPending(true) - await agent.com.atproto.server.activateAccount() + await pdsClient.call(com.atproto.server.activateAccount) await queryClient.resetQueries() - await agent.resumeSession(agent.session!) + await refreshSession() } catch (e: any) { switch (e.message) { case 'Bad token scope': @@ -93,7 +94,7 @@ export function Deactivated() { } finally { setPending(false) } - }, [_, agent, setPending, setError, queryClient]) + }, [_, pdsClient, refreshSession, setPending, setError, queryClient]) return ( diff --git a/src/screens/Login/ForgotPasswordForm.tsx b/src/screens/Login/ForgotPasswordForm.tsx index 244f4f0bef..b6eff6f560 100644 --- a/src/screens/Login/ForgotPasswordForm.tsx +++ b/src/screens/Login/ForgotPasswordForm.tsx @@ -1,11 +1,11 @@ import {useCallback, useState} from 'react' import {Keyboard, View} from 'react-native' +import {Client} from '@atproto/lex-client' import {Trans, useLingui} from '@lingui/react/macro' import * as EmailValidator from 'email-validator' import {cleanError, isNetworkError} from '#/lib/strings/errors' import {logger} from '#/logger' -import {Agent} from '#/state/session/agent' import {atoms as a, useTheme, web} from '#/alf' import {Admonition} from '#/components/Admonition' import {Button, ButtonIcon, ButtonText} from '#/components/Button' @@ -15,7 +15,7 @@ import {At_Stroke2_Corner0_Rounded as At} from '#/components/icons/At' import {Loader} from '#/components/Loader' import {Text} from '#/components/Typography' import {IS_WEB} from '#/env' -import {type com} from '#/lexicons' +import {com} from '#/lexicons' import {FormContainer} from './FormContainer' type ServiceDescription = com.atproto.server.describeServer.$OutputBody @@ -55,8 +55,8 @@ export const ForgotPasswordForm = ({ setIsProcessing(true) try { - const agent = new Agent(null, {service: serviceUrl}) - await agent.com.atproto.server.requestPasswordReset({email}) + const client = new Client({service: serviceUrl}) + await client.call(com.atproto.server.requestPasswordReset, {email}) onEmailSent() } catch (err) { logger.warn('Failed to request password reset', {error: err}) diff --git a/src/screens/Login/SetNewPasswordForm.tsx b/src/screens/Login/SetNewPasswordForm.tsx index cdc8fe4e0f..5d57be2e6a 100644 --- a/src/screens/Login/SetNewPasswordForm.tsx +++ b/src/screens/Login/SetNewPasswordForm.tsx @@ -1,11 +1,11 @@ import {useState} from 'react' import {View} from 'react-native' +import {Client} from '@atproto/lex-client' import {Trans, useLingui} from '@lingui/react/macro' import {cleanError, isNetworkError} from '#/lib/strings/errors' import {checkAndFormatResetCode} from '#/lib/strings/password' import {logger} from '#/logger' -import {Agent} from '#/state/session/agent' import {atoms as a, web} from '#/alf' import {Admonition} from '#/components/Admonition' import {Button, ButtonIcon, ButtonText} from '#/components/Button' @@ -16,6 +16,7 @@ import {Loader} from '#/components/Loader' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' import {IS_WEB} from '#/env' +import {com} from '#/lexicons' import {FormContainer} from './FormContainer' export const SetNewPasswordForm = ({ @@ -61,8 +62,8 @@ export const SetNewPasswordForm = ({ setIsProcessing(true) try { - const agent = new Agent(null, {service: serviceUrl}) - await agent.com.atproto.server.resetPassword({ + const client = new Client({service: serviceUrl}) + await client.call(com.atproto.server.resetPassword, { token: formattedCode, password, }) diff --git a/src/screens/Messages/ConversationSettings/index.tsx b/src/screens/Messages/ConversationSettings/index.tsx index 74ce2df2f7..ebdbadb08d 100644 --- a/src/screens/Messages/ConversationSettings/index.tsx +++ b/src/screens/Messages/ConversationSettings/index.tsx @@ -169,16 +169,14 @@ function keyExtractor(item: Item) { return item.key } -/* - * The member list now comes from the migrated lexicon-typed query, but the - * narrowed `GroupConvoMember` target is still the old-typed shape from - * `#/components/dms/util` (migrates in a later task) - the guard doubles as - * the mixed-world bridge. TODO(phase4): retype to the lexicon member types - * once dms/util migrates. +/** + * Narrows a lexicon `ProfileViewBasic` member to a `GroupConvoMember` (a + * `ProfileViewBasic` whose `kind` is a group member, or absent when the + * account has been deleted). */ function isGroupMember( member: chat.bsky.actor.defs.ProfileViewBasic, -): member is chat.bsky.actor.defs.ProfileViewBasic & GroupConvoMember { +): member is GroupConvoMember { // Kind is missing when the account has been deleted. return ( member.kind === undefined || @@ -205,15 +203,7 @@ function GroupSettings({ const {data: memberListData = [], refetch} = useListConvoMembersQuery({ convoId: convo.view.id, - /* - * `convo.members` comes from the still-old-typed `#/components/dms/util` - * (migrates in a later task) while the member-list query is now typed on - * the lexicon ProfileViewBasic. TODO(phase4): drop toLex once dms/util - * migrates. - */ - placeholderData: bsky.toLex( - convo.members, - ), + placeholderData: convo.members, }) const {data: joinRequestsData, hasNextPage: hasMoreRequests} = diff --git a/src/screens/Messages/components/ChatDisabled.tsx b/src/screens/Messages/components/ChatDisabled.tsx index 04aa3ab60a..94a21d8e40 100644 --- a/src/screens/Messages/components/ChatDisabled.tsx +++ b/src/screens/Messages/components/ChatDisabled.tsx @@ -11,10 +11,10 @@ import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' import {Loader} from '#/components/Loader' +import {accountReportSubject} from '#/components/moderation/ReportDialog/utils/reportSubject' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {com, tools} from '#/lexicons' -import {toLex} from '#/types/bsky' export function ChatDisabled({ shape = 'pill', @@ -102,14 +102,11 @@ function DialogInner() { throw new Error('No current account, should be unreachable') await pdsClient.call( com.atproto.moderation.createReport, - toLex({ + { reasonType: tools.ozone.report.defs.reasonAppeal.value, - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: currentAccount.did, - }, + subject: accountReportSubject(currentAccount.did), reason: details, - }), + }, { service: api.moderation.service, }, diff --git a/src/screens/Settings/FindContactsSettings.tsx b/src/screens/Settings/FindContactsSettings.tsx index 2f0dcc5804..f2b1f11b41 100644 --- a/src/screens/Settings/FindContactsSettings.tsx +++ b/src/screens/Settings/FindContactsSettings.tsx @@ -1,6 +1,7 @@ import {useCallback, useEffect, useState} from 'react' import {type ListRenderItemInfo, View} from 'react-native' import * as Contacts from 'expo-contacts' +import {type DidString} from '@atproto/syntax' import {type ModerationOpts} from '@bsky.app/sdk/moderation' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' @@ -28,12 +29,7 @@ import { useContactsMatchesQuery, useContactsSyncStatusQuery, } from '#/state/queries/find-contacts' -import { - useAgent, - useAppviewClient, - usePdsClient, - useSession, -} from '#/state/session' +import {useAppviewClient, usePdsClient, useSession} from '#/state/session' import {ErrorScreen} from '#/view/com/util/error/ErrorScreen' import {List} from '#/view/com/util/List' import {atoms as a, tokens, useGutters, useTheme} from '#/alf' @@ -196,7 +192,7 @@ function SyncStatus({ refetchStatus: () => Promise }) { const ax = useAnalytics() - const agent = useAgent() + const appviewClient = useAppviewClient() const queryClient = useQueryClient() const {_} = useLingui() const moderationOpts = useModerationOpts() @@ -221,7 +217,9 @@ function SyncStatus({ const {mutate: dismissMatch} = useMutation({ mutationFn: async (did: string) => { - await agent.app.bsky.contact.dismissMatch({subject: did}) + await appviewClient.call(app.bsky.contact.dismissMatch, { + subject: did as DidString, + }) }, onMutate: async (did: string) => { ax.metric('contacts:settings:dismiss', {}) @@ -493,12 +491,12 @@ function StatusFooter({syncedAt}: {syncedAt: string}) { const {_, i18n} = useLingui() const t = useTheme() const ax = useAnalytics() - const agent = useAgent() + const appviewClient = useAppviewClient() const queryClient = useQueryClient() const {mutate: removeData, isPending} = useMutation({ mutationFn: async () => { - await agent.app.bsky.contact.removeData({}) + await appviewClient.call(app.bsky.contact.removeData, {}) }, onMutate: () => ax.metric('contacts:settings:removeData', {}), onSuccess: () => { diff --git a/src/screens/Settings/InterestsSettings.tsx b/src/screens/Settings/InterestsSettings.tsx index ced7148669..351ee60ffc 100644 --- a/src/screens/Settings/InterestsSettings.tsx +++ b/src/screens/Settings/InterestsSettings.tsx @@ -1,5 +1,6 @@ import {useMemo, useState} from 'react' import {type TextStyle, View, type ViewStyle} from 'react-native' +import {setInterestsPref} from '@bsky.app/sdk' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' @@ -23,7 +24,7 @@ import {createGetSuggestedUsersForDiscoverQueryKey} from '#/state/queries/trendi import {createGetSuggestedUsersForExploreQueryKey} from '#/state/queries/trending/useGetSuggestedUsersForExploreQuery' import {createGetSuggestedUsersForSeeMoreQueryKey} from '#/state/queries/trending/useGetSuggestedUsersForSeeMoreQuery' import {createSuggestedStarterPacksQueryKey} from '#/state/queries/useSuggestedStarterPacksQuery' -import {useAgent} from '#/state/session' +import {usePdsClient} from '#/state/session' import {atoms as a, useGutters, useTheme} from '#/alf' import {Admonition} from '#/components/Admonition' import {Divider} from '#/components/Divider' @@ -88,7 +89,7 @@ function Inner({ setIsSaving: (isSaving: boolean) => void }) { const {_} = useLingui() - const agent = useAgent() + const pdsClient = usePdsClient() const qc = useQueryClient() const interestsDisplayNames = useInterestsDisplayNames() const preselectedInterests = useMemo( @@ -110,7 +111,7 @@ function Inner({ setIsSaving(true) try { - await agent.setInterestsPref({tags: interests}) + await pdsClient.call(setInterestsPref, {tags: interests}) qc.setQueriesData( {queryKey: preferencesQueryKey}, (old?: UsePreferencesQueryResponse) => { @@ -157,7 +158,7 @@ function Inner({ setIsSaving(false) } }, 1500) - }, [_, agent, setIsSaving, qc, preselectedInterests]) + }, [_, pdsClient, setIsSaving, qc, preselectedInterests]) const onChangeInterests = async (interests: string[]) => { setInterests(interests) diff --git a/src/screens/Settings/Settings.tsx b/src/screens/Settings/Settings.tsx index 2a8e586289..d6635d59b1 100644 --- a/src/screens/Settings/Settings.tsx +++ b/src/screens/Settings/Settings.tsx @@ -1,6 +1,7 @@ import {useState} from 'react' import {Alert, LayoutAnimation, Linking, Pressable, View} from 'react-native' import {useReducedMotion} from 'react-native-reanimated' +import {removeNuxs} from '@bsky.app/sdk' import {moderateProfile} from '@bsky.app/sdk/moderation' import {Trans, useLingui} from '@lingui/react/macro' import {useNavigation} from '@react-navigation/native' @@ -21,8 +22,12 @@ import {clearStorage} from '#/state/persisted' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useDeleteActorDeclaration} from '#/state/queries/messages/actor-declaration' import {useProfileQuery, useProfilesQuery} from '#/state/queries/profile' -import {useAgent} from '#/state/session' -import {type SessionAccount, useSession, useSessionApi} from '#/state/session' +import { + type SessionAccount, + usePdsClient, + useSession, + useSessionApi, +} from '#/state/session' import {useOnboardingDispatch} from '#/state/shell' import {useLoggedOutViewControls} from '#/state/shell/logged-out' import {useCloseAllActiveElements} from '#/state/util' @@ -386,7 +391,7 @@ function ProfilePreview({ function DevOptions() { const {t: l} = useLingui() - const agent = useAgent() + const pdsClient = usePdsClient() const [override, setOverride] = useStorage(device, [ 'policyUpdateDebugOverride', ]) @@ -561,7 +566,7 @@ function DevOptions() {