diff --git a/src/features/liveNow/index.tsx b/src/features/liveNow/index.tsx index 23f33a5ffc..3b60b688bb 100644 --- a/src/features/liveNow/index.tsx +++ b/src/features/liveNow/index.tsx @@ -2,13 +2,12 @@ import {useMemo} from 'react' import { type $Typed, type AppBskyActorDefs, - type AppBskyActorStatus, AppBskyEmbedExternal, - AtUri, - ComAtprotoRepoPutRecord, moderateStatus, } from '@atproto/api' import {retry} from '@atproto/common-web' +import {type l} from '@atproto/lex' +import {type AtIdentifierString, AtUri, toDatetimeString} from '@atproto/syntax' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' @@ -17,18 +16,20 @@ import {isAfter, parseISO} from 'date-fns' import {uploadBlob} from '#/lib/api' import {imageToThumb} from '#/lib/api/resolve' import {getLinkMeta, type LinkMeta} from '#/lib/link-meta/link-meta' +import {matchXrpcError} from '#/lib/xrpc-error' import {useAppConfig} from '#/state/appConfig' import { updateProfileShadow, useMaybeProfileShadow, } from '#/state/cache/profile-shadow' import {useModerationOpts} from '#/state/preferences/moderation-opts' -import {useAgent, useSession} from '#/state/session' +import {useAgent, usePdsClient, useSession} from '#/state/session' import {useTickEveryMinute} from '#/state/shell' import {useDialogContext} from '#/components/Dialog' import * as Toast from '#/components/Toast' import {useAnalytics} from '#/analytics' import {getLiveNowHost, getLiveServiceNames} from '#/features/liveNow/utils' +import {app, com} from '#/lexicons' import type * as bsky from '#/types/bsky' export * from '#/features/liveNow/utils' @@ -224,6 +225,7 @@ export function useUpsertLiveStatusMutation( const ax = useAnalytics() const {currentAccount} = useSession() const agent = useAgent() + const pdsClient = usePdsClient() const queryClient = useQueryClient() const control = useDialogContext() const {_} = useLingui() @@ -232,10 +234,10 @@ export function useUpsertLiveStatusMutation( mutationFn: async () => { if (!currentAccount) throw new Error('Not logged in') - let embed: $Typed | undefined + let embed: $Typed | undefined if (linkMeta) { - let thumb + let thumb: l.BlobRef | undefined if (linkMeta.image) { try { @@ -246,7 +248,11 @@ export function useUpsertLiveStatusMutation( img.source.path, img.source.mime, ) - thumb = blob.data.blob + /* + * `uploadBlob` still returns the legacy `BlobRef` class + * instance; it moves to the client with the blob pipeline. + */ + thumb = blob.data.blob as unknown as l.BlobRef } } catch (e: any) { ax.logger.error(`Failed to upload thumbnail for live status`, { @@ -263,7 +269,8 @@ export function useUpsertLiveStatusMutation( $type: 'app.bsky.embed.external#external', title: linkMeta.title ?? '', description: linkMeta.description ?? '', - uri: linkMeta.url, + // `getLinkMeta` returns a plain url string + uri: linkMeta.url as l.UriString, thumb, }, } @@ -271,32 +278,41 @@ export function useUpsertLiveStatusMutation( const record = { $type: 'app.bsky.actor.status', - createdAt: createdAt ?? new Date().toISOString(), + createdAt: toDatetimeString( + createdAt ? new Date(createdAt) : new Date(), + ), status: 'app.bsky.actor.status#live', durationMinutes: duration, embed, - } satisfies AppBskyActorStatus.Record + } satisfies app.bsky.actor.status.Main const upsert = async () => { - const repo = currentAccount.did + // the session account is still legacy-typed, so its did is unbranded + const repo = currentAccount.did as AtIdentifierString const collection = 'app.bsky.actor.status' - const existing = await agent.com.atproto.repo - .getRecord({repo, collection, rkey: 'self'}) + const existing = await pdsClient + .call(com.atproto.repo.getRecord, {repo, collection, rkey: 'self'}) .catch(_e => undefined) - await agent.com.atproto.repo.putRecord({ + /* + * Stays on the raw `putRecord`, not `pdsClient.put`: the lexicon lets + * `swapRecord` be null (meaning "must not already exist"), while the + * record-helper option type is `string | undefined`. + */ + await pdsClient.call(com.atproto.repo.putRecord, { repo, collection, rkey: 'self', record, - swapRecord: existing?.data.cid || null, + swapRecord: existing?.cid || null, }) } await retry(upsert, { maxRetries: 5, - retryable: e => e instanceof ComAtprotoRepoPutRecord.InvalidSwapError, + retryable: e => + matchXrpcError(e, com.atproto.repo.putRecord) === 'InvalidSwap', }) return { @@ -353,7 +369,7 @@ export function useUpsertLiveStatusMutation( export function useRemoveLiveStatusMutation() { const ax = useAnalytics() const {currentAccount} = useSession() - const agent = useAgent() + const pdsClient = usePdsClient() const queryClient = useQueryClient() const control = useDialogContext() const {_} = useLingui() @@ -362,8 +378,8 @@ export function useRemoveLiveStatusMutation() { mutationFn: async () => { if (!currentAccount) throw new Error('Not logged in') - await agent.app.bsky.actor.status.delete({ - repo: currentAccount.did, + await pdsClient.delete(app.bsky.actor.status, { + repo: currentAccount.did as AtIdentifierString, rkey: 'self', }) }, diff --git a/src/screens/Profile/components/GermButton.tsx b/src/screens/Profile/components/GermButton.tsx index 7e32d60bcc..5a69ec42c9 100644 --- a/src/screens/Profile/components/GermButton.tsx +++ b/src/screens/Profile/components/GermButton.tsx @@ -1,10 +1,8 @@ import {Platform, View} from 'react-native' import {Image} from 'expo-image' -import { - type AppBskyActorDefs, - type AppBskyActorGetProfile, - type AtpAgent, -} from '@atproto/api' +import {type AppBskyActorDefs} from '@atproto/api' +import {type Client} from '@atproto/lex' +import {type DidString} from '@atproto/syntax' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' @@ -13,7 +11,7 @@ import {useMutation, useQueryClient} from '@tanstack/react-query' import {until} from '#/lib/async/until' import {isNetworkError} from '#/lib/strings/errors' import {RQKEY} from '#/state/queries/profile' -import {useAgent, useSession} from '#/state/session' +import {useAppviewClient, usePdsClient, useSession} from '#/state/session' import {atoms as a, useTheme, web} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' @@ -24,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, com} from '#/lexicons' import type * as bsky from '#/types/bsky' export function GermButton({ @@ -119,25 +118,27 @@ function GermSelfButton({did}: {did: string}) { const ax = useAnalytics() const {_} = useLingui() const selfExplanationDialogControl = Dialog.useDialogControl() - const agent = useAgent() + const appviewClient = useAppviewClient() + const pdsClient = usePdsClient() const queryClient = useQueryClient() const {mutate: deleteDeclaration, isPending} = useMutation({ mutationFn: async () => { - const previousRecord = await agent.com.germnetwork.declaration - .get({ - repo: did, + const previousRecord = await pdsClient + // the profile view is still legacy-typed, so its did is unbranded + .get(com.germnetwork.declaration, { + repo: did as DidString, rkey: 'self', }) .then(res => res.value) .catch(() => null) - await agent.com.germnetwork.declaration.delete({ - repo: did, + await pdsClient.delete(com.germnetwork.declaration, { + repo: did as DidString, rkey: 'self', }) - await whenAppViewReady(agent, did, res => !res.data.associated?.germ) + await whenAppViewReady(appviewClient, did, res => !res.associated?.germ) return previousRecord }, @@ -147,14 +148,15 @@ function GermSelfButton({did}: {did: string}) { async function undo() { if (!previousRecord) return try { - await agent.com.germnetwork.declaration.put( - { - repo: did, - rkey: 'self', - }, - previousRecord, + await pdsClient.put(com.germnetwork.declaration, previousRecord, { + repo: did as DidString, + rkey: 'self', + }) + await whenAppViewReady( + appviewClient, + did, + res => !!res.associated?.germ, ) - await whenAppViewReady(agent, did, res => !!res.data.associated?.germ) await queryClient.refetchQueries({queryKey: RQKEY(did)}) Toast.show(_(msg`Germ DM reconnected`)) @@ -323,14 +325,17 @@ function platform() { } async function whenAppViewReady( - agent: AtpAgent, + appviewClient: Client, actor: string, - fn: (res: AppBskyActorGetProfile.Response) => boolean, + fn: (res: app.bsky.actor.getProfile.$OutputBody) => boolean, ) { await until( 5, // 5 tries 1e3, // 1s delay between tries fn, - () => agent.app.bsky.actor.getProfile({actor}), + () => + appviewClient.call(app.bsky.actor.getProfile, { + actor: actor as DidString, + }), ) } diff --git a/src/state/queries/profile.ts b/src/state/queries/profile.ts index 23fd4340ac..dea8d35eea 100644 --- a/src/state/queries/profile.ts +++ b/src/state/queries/profile.ts @@ -10,6 +10,11 @@ import { type ComAtprotoRepoUploadBlob, type Un$Typed, } from '@atproto/api' +import { + type AtIdentifierString, + type DidString, + toDatetimeString, +} from '@atproto/syntax' import { type InfiniteData, keepPreviousData, @@ -33,10 +38,11 @@ import { useUnstableProfileViewCache, } from '#/state/queries/unstable-profile-cache' import {useUpdateProfileVerificationCache} from '#/state/queries/verification/useUpdateProfileVerificationCache' -import {useAgent, useSession} from '#/state/session' +import {useAgent, usePdsClient, useSession} from '#/state/session' import * as userActionHistory from '#/state/userActionHistory' import {useAnalytics} from '#/analytics' import {type Metrics, toClout} from '#/analytics/metrics' +import {app} from '#/lexicons' import type * as bsky from '#/types/bsky' import { ProgressGuideAction, @@ -634,17 +640,18 @@ export function useProfileBlockMutationQueue( function useProfileBlockMutation() { const {currentAccount} = useSession() - const agent = useAgent() + const pdsClient = usePdsClient() const queryClient = useQueryClient() return useMutation<{uri: string; cid: string}, Error, {did: string}>({ mutationFn: async ({did}) => { if (!currentAccount) { throw new Error('Not signed in') } - return await agent.app.bsky.graph.block.create( - {repo: currentAccount.did}, - {subject: did, createdAt: new Date().toISOString()}, - ) + return await pdsClient.create(app.bsky.graph.block, { + // the profile view is still legacy-typed, so its did is unbranded + subject: did as DidString, + createdAt: toDatetimeString(new Date()), + }) }, onSuccess(_, {did}) { void queryClient.invalidateQueries({queryKey: RQKEY_MY_BLOCKED()}) @@ -655,16 +662,16 @@ function useProfileBlockMutation() { function useProfileUnblockMutation() { const {currentAccount} = useSession() - const agent = useAgent() + const pdsClient = usePdsClient() const queryClient = useQueryClient() return useMutation({ mutationFn: async ({blockUri}) => { if (!currentAccount) { throw new Error('Not signed in') } - const {rkey} = new AtUri(blockUri) - await agent.app.bsky.graph.block.delete({ - repo: currentAccount.did, + const {rkeySafe: rkey} = new AtUri(blockUri) + await pdsClient.delete(app.bsky.graph.block, { + repo: currentAccount.did as AtIdentifierString, rkey, }) },