[SDK] Migrate search, bookmarks, contacts and feed queries to the lex clients (#11360)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Samuel Newman
2026-08-13 22:26:16 +03:00
committed by GitHub
parent ea4f70fb1e
commit 4aae6b2c22
16 changed files with 350 additions and 287 deletions
@@ -6,13 +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 {app} from '#/lexicons'
import {useEmojiSearch} from './useEmojiSearch'
const DEFAULT_MOD_OPTS = {
@@ -31,7 +32,7 @@ export function useAutocomplete({
limit?: number
showSearchFallback?: boolean
}): AutocompleteApi {
const agent = useAgent()
const client = useAppviewClient()
const moderationOpts = useModerationOpts()
const emojiSearch = useEmojiSearch()
@@ -52,12 +53,12 @@ export function useAutocomplete({
// Going from "foo" to "foo." should not clear matches.
q = q.toLowerCase().trim().replace(/\.$/, '')
const res = await agent.searchActorsTypeahead({
const data = await client.call(app.bsky.actor.searchActorsTypeahead, {
q,
limit: limit || 8,
})
return (res?.data.actors || []).map(profile => ({
return (data?.actors || []).map(profile => ({
key: profile.did,
type: 'profile' as const,
value: '@' + profile.handle,
+20 -20
View File
@@ -2,7 +2,6 @@ import {useState} from 'react'
import {Keyboard, View} from 'react-native'
import {KeyboardAvoidingView} from 'react-native-keyboard-controller'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {AppBskyContactStartPhoneVerification} from '@atproto/api'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
@@ -14,8 +13,9 @@ import {
getDefaultCountry,
} from '#/lib/international-telephone-codes'
import {cleanError, isNetworkError} from '#/lib/strings/errors'
import {matchXrpcError} 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 client = 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 client.call(app.bsky.contact.startPhoneVerification, {
phone: constructFullPhoneNumber(phoneCountryCode, phoneNumber),
})
},
@@ -102,23 +103,22 @@ export function PhoneInput({
msg`A network error occurred. Please check your internet connection`,
),
)
} else if (
err instanceof
AppBskyContactStartPhoneVerification.RateLimitExceededError
) {
setError(_(msg`Rate limit exceeded. Please try again later.`))
} else if (
err instanceof AppBskyContactStartPhoneVerification.InvalidPhoneError
) {
setError(
_(
msg`The verification provider was unable to send a code to your phone number. Please check your phone number and try again.`,
),
)
} else {
logger.error('Verify phone number failed', {safeMessage: err})
setError(_(msg`An error occurred. ${cleanError(err)}`))
return
}
switch (matchXrpcError(err, app.bsky.contact.startPhoneVerification)) {
case 'RateLimitExceeded':
setError(_(msg`Rate limit exceeded. Please try again later.`))
return
case 'InvalidPhone':
setError(
_(
msg`The verification provider was unable to send a code to your phone number. Please check your phone number and try again.`,
),
)
return
}
logger.error('Verify phone number failed', {safeMessage: err})
setError(_(msg`An error occurred. ${cleanError(err)}`))
},
})
@@ -1,9 +1,5 @@
import {useEffect, useMemo, useState} from 'react'
import {Text as NestedText, View} from 'react-native'
import {
AppBskyContactStartPhoneVerification,
AppBskyContactVerifyPhone,
} from '@atproto/api'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
@@ -11,8 +7,9 @@ import {useMutation} from '@tanstack/react-query'
import {clamp} from '#/lib/numbers'
import {cleanError, isNetworkError} from '#/lib/strings/errors'
import {matchXrpcError} 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'
@@ -25,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'
@@ -43,7 +41,7 @@ export function VerifyNumber({
const t = useTheme()
const {_} = useLingui()
const ax = useAnalytics()
const agent = useAgent()
const client = useAppviewClient()
const gutters = useGutters([0, 'wide'])
const [otpCode, setOtpCode] = useState('')
@@ -72,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 data = await client.call(app.bsky.contact.verifyPhone, {
code,
phone,
})
return data.token
},
onSuccess: async token => {
// let the success state show for a moment
@@ -99,44 +100,47 @@ export function VerifyNumber({
msg`A network error occurred. Please check your internet connection.`,
),
})
} else if (err instanceof AppBskyContactVerifyPhone.InvalidCodeError) {
setError({
retryable: true,
isResendError: true,
message: _(msg`This code is invalid. Resend to get a new code.`),
})
} else if (err instanceof AppBskyContactVerifyPhone.InvalidPhoneError) {
setError({
retryable: false,
isResendError: false,
message: _(
msg`The verification provider was unable to send a code to your phone number. Please check your phone number and try again.`,
),
})
} else if (
err instanceof AppBskyContactVerifyPhone.RateLimitExceededError
) {
setError({
retryable: true,
isResendError: false,
message: _(
msg`Too many attempts. Please wait a few minutes and try again.`,
),
})
} else {
logger.error('Verify phone number failed', {safeMessage: err})
setError({
retryable: true,
isResendError: false,
message: _(msg`An error occurred. ${cleanError(err)}`),
})
return
}
switch (matchXrpcError(err, app.bsky.contact.verifyPhone)) {
case 'InvalidCode':
setError({
retryable: true,
isResendError: true,
message: _(msg`This code is invalid. Resend to get a new code.`),
})
return
case 'InvalidPhone':
setError({
retryable: false,
isResendError: false,
message: _(
msg`The verification provider was unable to send a code to your phone number. Please check your phone number and try again.`,
),
})
return
case 'RateLimitExceeded':
setError({
retryable: true,
isResendError: false,
message: _(
msg`Too many attempts. Please wait a few minutes and try again.`,
),
})
return
}
logger.error('Verify phone number failed', {safeMessage: err})
setError({
retryable: true,
isResendError: false,
message: _(msg`An error occurred. ${cleanError(err)}`),
})
},
})
const {mutate: resendCode, isPending: isResendingCode} = useMutation({
mutationFn: async () => {
await agent.app.bsky.contact.startPhoneVerification({phone: phone})
await client.call(app.bsky.contact.startPhoneVerification, {phone: phone})
},
onSuccess: () => {
dispatch({type: 'RESEND_VERIFICATION_CODE'})
@@ -155,35 +159,34 @@ export function VerifyNumber({
msg`A network error occurred. Please check your internet connection.`,
),
})
} else if (
err instanceof AppBskyContactStartPhoneVerification.InvalidPhoneError
) {
setError({
retryable: false,
isResendError: true,
message: _(
msg`The verification provider was unable to send a code to your phone number. Please check your phone number and try again.`,
),
})
} else if (
err instanceof
AppBskyContactStartPhoneVerification.RateLimitExceededError
) {
setError({
retryable: true,
isResendError: true,
message: _(
msg`Too many codes sent. Please wait a few minutes and try again.`,
),
})
} else {
logger.error('Resend failed', {safeMessage: err})
setError({
retryable: true,
isResendError: true,
message: _(msg`An error occurred. ${cleanError(err)}`),
})
return
}
switch (matchXrpcError(err, app.bsky.contact.startPhoneVerification)) {
case 'InvalidPhone':
setError({
retryable: false,
isResendError: true,
message: _(
msg`The verification provider was unable to send a code to your phone number. Please check your phone number and try again.`,
),
})
return
case 'RateLimitExceeded':
setError({
retryable: true,
isResendError: true,
message: _(
msg`Too many codes sent. Please wait a few minutes and try again.`,
),
})
return
}
logger.error('Resend failed', {safeMessage: err})
setError({
retryable: true,
isResendError: true,
message: _(msg`An error occurred. ${cleanError(err)}`),
})
},
})
@@ -3,6 +3,7 @@ import {View} from 'react-native'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import * as SMS from 'expo-sms'
import {type ModerationOpts} from '@atproto/api'
import {type DidString} from '@atproto/syntax'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {Plural, Trans} from '@lingui/react/macro'
@@ -20,7 +21,7 @@ import {
optimisticRemoveMatch,
useMatchesPassthroughQuery,
} from '#/state/queries/find-contacts'
import {useAgent, useSession} from '#/state/session'
import {useAgent, useAppviewClient, 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'
@@ -41,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'
@@ -90,6 +92,7 @@ export function ViewMatches({
const moderationOpts = useModerationOpts()
const queryClient = useQueryClient()
const agent = useAgent()
const client = useAppviewClient()
const insets = useSafeAreaInsets()
const listRef = useRef<ListMethods>(null)
@@ -218,7 +221,9 @@ export function ViewMatches({
const {mutate: dismissMatch} = useMutation({
mutationFn: async (did: string) => {
await agent.app.bsky.contact.dismissMatch({subject: did})
await client.call(app.bsky.contact.dismissMatch, {
subject: did as DidString,
})
},
onMutate: did => {
ax.metric('contacts:matches:dismiss', {entryPoint: context})
+15 -14
View File
@@ -1,11 +1,8 @@
import {useCallback, useEffect, useState} from 'react'
import {type ListRenderItemInfo, View} from 'react-native'
import * as Contacts from 'expo-contacts'
import {
type AppBskyContactDefs,
type AppBskyContactGetSyncStatus,
type ModerationOpts,
} from '@atproto/api'
import {type AppBskyContactDefs, type ModerationOpts} from '@atproto/api'
import {type DidString} from '@atproto/syntax'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {Plural, Trans} from '@lingui/react/macro'
@@ -32,7 +29,7 @@ import {
useContactsMatchesQuery,
useContactsSyncStatusQuery,
} from '#/state/queries/find-contacts'
import {useAgent, useSession} from '#/state/session'
import {useAgent, useAppviewClient, 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'
@@ -52,6 +49,7 @@ import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
import {IS_NATIVE} from '#/env'
import {InviteFriendsDialog} from '#/features/inviteFriends'
import {app} from '#/lexicons'
import type * as bsky from '#/types/bsky'
import {bulkWriteFollows} from '../Onboarding/util'
@@ -194,7 +192,7 @@ function SyncStatus({
refetchStatus: () => Promise<any>
}) {
const ax = useAnalytics()
const agent = useAgent()
const client = useAppviewClient()
const queryClient = useQueryClient()
const {_} = useLingui()
const moderationOpts = useModerationOpts()
@@ -219,7 +217,9 @@ function SyncStatus({
const {mutate: dismissMatch} = useMutation({
mutationFn: async (did: string) => {
await agent.app.bsky.contact.dismissMatch({subject: did})
await client.call(app.bsky.contact.dismissMatch, {
subject: did as DidString,
})
},
onMutate: async (did: string) => {
ax.metric('contacts:settings:dismiss', {})
@@ -371,6 +371,7 @@ function StatusHeader({
const {_} = useLingui()
const ax = useAnalytics()
const agent = useAgent()
const client = useAppviewClient()
const queryClient = useQueryClient()
const {currentAccount} = useSession()
@@ -384,12 +385,12 @@ function StatusHeader({
let cursor: string | undefined
do {
const page = await agent.app.bsky.contact.getMatches({
const page = await client.call(app.bsky.contact.getMatches, {
limit: 100,
cursor,
})
cursor = page.data.cursor
for (const profile of page.data.matches) {
cursor = page.cursor
for (const profile of page.matches) {
if (
profile.did !== currentAccount?.did &&
!isBlockedOrBlocking(profile) &&
@@ -487,17 +488,17 @@ function StatusFooter({syncedAt}: {syncedAt: string}) {
const {_, i18n} = useLingui()
const t = useTheme()
const ax = useAnalytics()
const agent = useAgent()
const client = useAppviewClient()
const queryClient = useQueryClient()
const {mutate: removeData, isPending} = useMutation({
mutationFn: async () => {
await agent.app.bsky.contact.removeData({})
await client.call(app.bsky.contact.removeData, {})
},
onMutate: () => ax.metric('contacts:settings:removeData', {}),
onSuccess: () => {
Toast.show(_(msg`Contacts removed`))
queryClient.setQueryData<AppBskyContactGetSyncStatus.OutputSchema>(
queryClient.setQueryData<app.bsky.contact.getSyncStatus.$OutputBody>(
findContactsStatusQueryKey,
{syncStatus: undefined},
)
+22 -13
View File
@@ -8,6 +8,7 @@ import {
} from 'react'
import {AppState, type AppStateStatus} from 'react-native'
import {type AppBskyFeedDefs} from '@atproto/api'
import {type AtUriString, type DidString} from '@atproto/syntax'
import throttle from 'lodash.throttle'
import {PROD_FEEDS, STAGING_FEEDS} from '#/lib/constants'
@@ -22,7 +23,8 @@ import {
} from '#/state/queries/post-feed'
import {getItemsForFeedback} from '#/view/com/posts/PostFeed'
import {useAnalytics} from '#/analytics'
import {useAgent} from './session'
import {app} from '#/lexicons'
import {useAppviewClient} from './session'
export const FEEDBACK_FEEDS = [...PROD_FEEDS, ...STAGING_FEEDS]
@@ -65,7 +67,7 @@ export function useFeedFeedback(
) {
const ax = useAnalytics()
const logger = ax.logger.useChild(ax.logger.Context.FeedFeedback)
const agent = useAgent()
const client = useAppviewClient()
const feed =
!!feedSourceInfo && isFeedSourceFeedInfo(feedSourceInfo)
@@ -150,16 +152,19 @@ export function useFeedFeedback(
return
}
// Send to the feed
agent.app.bsky.feed
.sendInteractions(
{interactions: interactionsToSend, feed: feed?.uri},
/*
* Send to the feed. Interactions go to the feed generator rather than the
* appview, which the agent did by setting `atproto-proxy` by hand; the
* client's per-call `service` option writes that same header.
*/
client
.call(
app.bsky.feed.sendInteractions,
{
encoding: 'application/json',
headers: {
'atproto-proxy': `${proxyDid}#bsky_fg`,
},
interactions: interactionsToSend,
feed: feed?.uri as AtUriString | undefined,
},
{service: `${proxyDid as DidString}#bsky_fg`},
)
.catch(() => {}) // ignore upstream errors
@@ -172,7 +177,7 @@ export function useFeedFeedback(
)
throttledFlushAggregatedStats()
logger.debug('flushed')
}, [agent, throttledFlushAggregatedStats, proxyDid, enabled, feed])
}, [client, throttledFlushAggregatedStats, proxyDid, enabled, feed])
const sendToFeed = useMemo(
() =>
@@ -283,9 +288,13 @@ function toString(interaction: AppBskyFeedDefs.Interaction): string {
}|${interaction.reqId || ''}`
}
function toInteraction(str: string): AppBskyFeedDefs.Interaction {
function toInteraction(str: string): app.bsky.feed.defs.Interaction {
const [item, event, feedContext, reqId] = str.split('|')
return {item, event, feedContext, reqId}
/*
* The fields come from splitting an internally-built key, so neither the
* at-uri nor the event token is narrowed by the compiler here.
*/
return {item, event, feedContext, reqId} as app.bsky.feed.defs.Interaction
}
type AggregatedStats = {
+10 -9
View File
@@ -9,7 +9,8 @@ import {keepPreviousData, useQuery, useQueryClient} from '@tanstack/react-query'
import {isJustAMute, moduiContainsHideableOffense} from '#/lib/moderation'
import {logger} from '#/logger'
import {STALE} from '#/state/queries'
import {useAgent} from '#/state/session'
import {useAppviewClient} from '#/state/session'
import {app} from '#/lexicons'
import {useModerationOpts} from '../preferences/moderation-opts'
import {DEFAULT_LOGGED_OUT_PREFERENCES} from './preferences'
@@ -27,7 +28,7 @@ export function useActorAutocompleteQuery(
limit?: number,
) {
const moderationOpts = useModerationOpts()
const agent = useAgent()
const client = useAppviewClient()
prefix = prefix.toLowerCase().trim()
if (prefix.endsWith('.')) {
@@ -39,13 +40,13 @@ export function useActorAutocompleteQuery(
staleTime: STALE.MINUTES.ONE,
queryKey: RQKEY(prefix || ''),
async queryFn() {
const res = prefix
? await agent.searchActorsTypeahead({
const data = prefix
? await client.call(app.bsky.actor.searchActorsTypeahead, {
q: prefix,
limit: limit || 8,
})
: undefined
return res?.data.actors || []
return data?.actors || []
},
select: useCallback(
(data: AppBskyActorDefs.ProfileViewBasic[]) => {
@@ -65,7 +66,7 @@ export type ActorAutocompleteFn = ReturnType<typeof useActorAutocompleteFn>
export function useActorAutocompleteFn() {
const queryClient = useQueryClient()
const moderationOpts = useModerationOpts()
const agent = useAgent()
const client = useAppviewClient()
return useCallback(
async ({query, limit = 8}: {query: string; limit?: number}) => {
@@ -77,7 +78,7 @@ export function useActorAutocompleteFn() {
staleTime: STALE.MINUTES.ONE,
queryKey: RQKEY(query || ''),
queryFn: () =>
agent.searchActorsTypeahead({
client.call(app.bsky.actor.searchActorsTypeahead, {
q: query,
limit,
}),
@@ -91,11 +92,11 @@ export function useActorAutocompleteFn() {
return computeSuggestions({
q: query,
searched: res?.data.actors,
searched: res?.actors,
moderationOpts: moderationOpts || DEFAULT_MOD_OPTS,
})
},
[queryClient, moderationOpts, agent],
[queryClient, moderationOpts, client],
)
}
+8 -9
View File
@@ -1,4 +1,3 @@
import {type AppBskyActorSearchActors} from '@atproto/api'
import {
type InfiniteData,
keepPreviousData,
@@ -8,7 +7,8 @@ import {
} from '@tanstack/react-query'
import {STALE} from '#/state/queries'
import {useAgent} from '#/state/session'
import {useAppviewClient} from '#/state/session'
import {app} from '#/lexicons'
export const RQKEY_ROOT = 'actor-search'
export const RQKEY = (query: string, limit?: number) => [
@@ -28,23 +28,22 @@ export function useActorSearch({
maintainData?: boolean
limit?: number
}) {
const agent = useAgent()
const client = useAppviewClient()
return useInfiniteQuery<
AppBskyActorSearchActors.OutputSchema,
app.bsky.actor.searchActors.$OutputBody,
Error,
InfiniteData<AppBskyActorSearchActors.OutputSchema>,
InfiniteData<app.bsky.actor.searchActors.$OutputBody>,
QueryKey,
string | undefined
>({
staleTime: STALE.MINUTES.FIVE,
queryKey: RQKEY(query, limit),
queryFn: async ({pageParam}) => {
const res = await agent.searchActors({
return await client.call(app.bsky.actor.searchActors, {
q: query,
limit,
cursor: pageParam,
})
return res.data
},
enabled: enabled && !!query,
initialPageParam: undefined,
@@ -54,7 +53,7 @@ export function useActorSearch({
})
}
function select(data: InfiniteData<AppBskyActorSearchActors.OutputSchema>) {
function select(data: InfiniteData<app.bsky.actor.searchActors.$OutputBody>) {
// enforce uniqueness
const dids = new Set()
@@ -77,7 +76,7 @@ export function* findAllProfilesInQueryData(
did: string,
) {
const queryDatas = queryClient.getQueriesData<
InfiniteData<AppBskyActorSearchActors.OutputSchema>
InfiniteData<app.bsky.actor.searchActors.$OutputBody>
>({
queryKey: [RQKEY_ROOT],
})
@@ -1,4 +1,5 @@
import {type AppBskyFeedDefs} from '@atproto/api'
import {type AtUriString} from '@atproto/syntax'
import {useMutation, useQueryClient} from '@tanstack/react-query'
import {isNetworkError} from '#/lib/strings/errors'
@@ -8,7 +9,8 @@ import {
optimisticallyDeleteBookmark,
optimisticallySaveBookmark,
} from '#/state/queries/bookmarks/useBookmarksQuery'
import {useAgent} from '#/state/session'
import {useAppviewClient} from '#/state/session'
import {app} from '#/lexicons'
type MutationArgs =
| {action: 'create'; post: AppBskyFeedDefs.PostView}
@@ -23,20 +25,20 @@ type MutationArgs =
export function useBookmarkMutation() {
const qc = useQueryClient()
const agent = useAgent()
const client = useAppviewClient()
return useMutation({
async mutationFn(args: MutationArgs) {
if (args.action === 'create') {
updatePostShadow(qc, args.post.uri, {bookmarked: true})
await agent.app.bsky.bookmark.createBookmark({
uri: args.post.uri,
await client.call(app.bsky.bookmark.createBookmark, {
uri: args.post.uri as AtUriString,
cid: args.post.cid,
})
} else if (args.action === 'delete') {
updatePostShadow(qc, args.uri, {bookmarked: false})
await agent.app.bsky.bookmark.deleteBookmark({
uri: args.uri,
await client.call(app.bsky.bookmark.deleteBookmark, {
uri: args.uri as AtUriString,
})
}
},
@@ -1,9 +1,4 @@
import {
type $Typed,
type AppBskyBookmarkGetBookmarks,
AppBskyFeedDefs,
AtUri,
} from '@atproto/api'
import {type $Typed, AppBskyFeedDefs, AtUri} from '@atproto/api'
import {
type InfiniteData,
type QueryClient,
@@ -16,28 +11,28 @@ import {
embedViewRecordToPostView,
getEmbeddedPost,
} from '#/state/queries/util'
import {useAgent} from '#/state/session'
import {useAppviewClient} from '#/state/session'
import {app} from '#/lexicons'
import * as bsky from '#/types/bsky'
export const bookmarksQueryKeyRoot = 'bookmarks'
export const createBookmarksQueryKey = () => [bookmarksQueryKeyRoot]
export function useBookmarksQuery() {
const agent = useAgent()
const client = useAppviewClient()
return useInfiniteQuery<
AppBskyBookmarkGetBookmarks.OutputSchema,
app.bsky.bookmark.getBookmarks.$OutputBody,
Error,
InfiniteData<AppBskyBookmarkGetBookmarks.OutputSchema>,
InfiniteData<app.bsky.bookmark.getBookmarks.$OutputBody>,
QueryKey,
string | undefined
>({
queryKey: createBookmarksQueryKey(),
async queryFn({pageParam}) {
const res = await agent.app.bsky.bookmark.getBookmarks({
return await client.call(app.bsky.bookmark.getBookmarks, {
cursor: pageParam,
})
return res.data
},
initialPageParam: undefined,
getNextPageParam: lastPage => lastPage.cursor,
@@ -45,7 +40,7 @@ export function useBookmarksQuery() {
}
export async function truncateAndInvalidate(qc: QueryClient) {
qc.setQueriesData<InfiniteData<AppBskyBookmarkGetBookmarks.OutputSchema>>(
qc.setQueriesData<InfiniteData<app.bsky.bookmark.getBookmarks.$OutputBody>>(
{queryKey: [bookmarksQueryKeyRoot]},
data => {
if (data) {
@@ -64,7 +59,7 @@ export async function optimisticallySaveBookmark(
qc: QueryClient,
post: AppBskyFeedDefs.PostView,
) {
qc.setQueriesData<InfiniteData<AppBskyBookmarkGetBookmarks.OutputSchema>>(
qc.setQueriesData<InfiniteData<app.bsky.bookmark.getBookmarks.$OutputBody>>(
{
queryKey: [bookmarksQueryKeyRoot],
},
@@ -75,19 +70,22 @@ export async function optimisticallySaveBookmark(
pages: data.pages.map((page, index) => {
if (index === 0) {
post.$type = 'app.bsky.feed.defs#postView'
/*
* The optimistic entry is synthesized from an `@atproto/api`
* `PostView`, whose string fields are unbranded, so it is asserted
* to the vendored view type the query data is now keyed on.
*/
const bookmark = {
createdAt: new Date().toISOString(),
subject: {
uri: post.uri,
cid: post.cid,
},
item: post as $Typed<AppBskyFeedDefs.PostView>,
} as unknown as app.bsky.bookmark.defs.BookmarkView
return {
...page,
bookmarks: [
{
createdAt: new Date().toISOString(),
subject: {
uri: post.uri,
cid: post.cid,
},
item: post as $Typed<AppBskyFeedDefs.PostView>,
},
...page.bookmarks,
],
bookmarks: [bookmark, ...page.bookmarks],
}
}
return page
@@ -101,7 +99,7 @@ export async function optimisticallyDeleteBookmark(
qc: QueryClient,
{uri}: {uri: string},
) {
qc.setQueriesData<InfiniteData<AppBskyBookmarkGetBookmarks.OutputSchema>>(
qc.setQueriesData<InfiniteData<app.bsky.bookmark.getBookmarks.$OutputBody>>(
{
queryKey: [bookmarksQueryKeyRoot],
},
@@ -125,7 +123,7 @@ export function* findAllPostsInQueryData(
uri: string,
): Generator<AppBskyFeedDefs.PostView, undefined> {
const queryDatas = queryClient.getQueriesData<
InfiniteData<AppBskyBookmarkGetBookmarks.OutputSchema>
InfiniteData<app.bsky.bookmark.getBookmarks.$OutputBody>
>({
queryKey: [bookmarksQueryKeyRoot],
})
+68 -56
View File
@@ -3,11 +3,11 @@ import {
type AppBskyActorDefs,
type AppBskyFeedDefs,
type AppBskyGraphDefs,
type AppBskyUnspeccedGetPopularFeedGenerators,
AtUri,
moderateFeedGenerator,
RichText,
} from '@atproto/api'
import {type AtUriString} from '@atproto/syntax'
import {t} from '@lingui/core/macro'
import {
type InfiniteData,
@@ -26,7 +26,8 @@ import {GCTIME, STALE} from '#/state/queries'
import {RQKEY as listQueryKey} from '#/state/queries/list'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {createQueryKey} from '#/state/queries/util'
import {useAgent, useSession} from '#/state/session'
import {useAppviewClient, useSession} from '#/state/session'
import {app} from '#/lexicons'
import {router} from '#/routes'
import {useModerationOpts} from '../preferences/moderation-opts'
import {type FeedDescriptor} from './post-feed'
@@ -184,7 +185,7 @@ export function getAvatarTypeFromUri(uri: string) {
export function useFeedSourceInfoQuery({uri}: {uri: string}) {
const type = getFeedTypeFromUri(uri)
const agent = useAgent()
const client = useAppviewClient()
return useQuery({
staleTime: STALE.INFINITY,
@@ -193,14 +194,16 @@ export function useFeedSourceInfoQuery({uri}: {uri: string}) {
let view: FeedSourceInfo
if (type === 'feed') {
const res = await agent.app.bsky.feed.getFeedGenerator({feed: uri})
view = hydrateFeedGenerator(res.data.view)
const data = await client.call(app.bsky.feed.getFeedGenerator, {
feed: uri as AtUriString,
})
view = hydrateFeedGenerator(data.view)
} else {
const res = await agent.app.bsky.graph.getList({
list: uri,
const data = await client.call(app.bsky.graph.getList, {
list: uri as AtUriString,
limit: 1,
})
view = hydrateList(res.data.list)
view = hydrateList(data.list)
}
return view
@@ -234,7 +237,7 @@ export function createGetPopularFeedsQueryKey(
export function useGetPopularFeedsQuery(options?: GetPopularFeedsOptions) {
const {hasSession} = useSession()
const agent = useAgent()
const client = useAppviewClient()
const limit = options?.limit || 10
const {data: preferences} = usePreferencesQuery()
const queryClient = useQueryClient()
@@ -255,24 +258,27 @@ export function useGetPopularFeedsQuery(options?: GetPopularFeedsOptions) {
enabled: Boolean(moderationOpts) && options?.enabled !== false,
queryKey: createGetPopularFeedsQueryKey(options),
queryFn: async ({pageParam}) => {
const res = await agent.app.bsky.unspecced.getPopularFeedGenerators({
limit,
cursor: pageParam,
})
const data = await client.call(
app.bsky.unspecced.getPopularFeedGenerators,
{
limit,
cursor: pageParam,
},
)
// precache feeds
for (const feed of res.data.feeds) {
for (const feed of data.feeds) {
const hydratedFeed = hydrateFeedGenerator(feed)
precacheFeed(queryClient, hydratedFeed)
}
return res.data
return data
},
initialPageParam: undefined as string | undefined,
getNextPageParam: lastPage => lastPage.cursor,
select: useCallback(
(
data: InfiniteData<AppBskyUnspeccedGetPopularFeedGenerators.OutputSchema>,
data: InfiniteData<app.bsky.unspecced.getPopularFeedGenerators.$OutputBody>,
) => {
const {
savedFeeds,
@@ -336,24 +342,27 @@ export function useGetPopularFeedsQuery(options?: GetPopularFeedsOptions) {
}
export function useSearchPopularFeedsMutation() {
const agent = useAgent()
const client = useAppviewClient()
const moderationOpts = useModerationOpts()
return useMutation({
mutationFn: async (query: string) => {
const res = await agent.app.bsky.unspecced.getPopularFeedGenerators({
limit: 10,
query: query,
})
const data = await client.call(
app.bsky.unspecced.getPopularFeedGenerators,
{
limit: 10,
query: query,
},
)
if (moderationOpts) {
return res.data.feeds.filter(feed => {
return data.feeds.filter(feed => {
const decision = moderateFeedGenerator(feed, moderationOpts)
return !decision.ui('contentMedia').blur
})
}
return res.data.feeds
return data.feeds
},
})
}
@@ -371,7 +380,7 @@ export function usePopularFeedsSearch({
query: string
enabled?: boolean
}) {
const agent = useAgent()
const client = useAppviewClient()
const moderationOpts = useModerationOpts()
const enabledInner = enabled ?? Boolean(moderationOpts)
@@ -379,12 +388,15 @@ export function usePopularFeedsSearch({
enabled: enabledInner,
queryKey: createPopularFeedsSearchQueryKey(query),
queryFn: async () => {
const res = await agent.app.bsky.unspecced.getPopularFeedGenerators({
limit: 15,
query: query,
})
const data = await client.call(
app.bsky.unspecced.getPopularFeedGenerators,
{
limit: 15,
query: query,
},
)
return res.data.feeds
return data.feeds
},
placeholderData: keepPreviousData,
select(data) {
@@ -444,7 +456,7 @@ const createPinnedFeedInfosQueryKey = (
export function usePinnedFeedsInfos() {
const {hasSession} = useSession()
const agent = useAgent()
const client = useAppviewClient()
const {data: preferences, isLoading: isLoadingPrefs} = usePreferencesQuery()
const pinnedItems = preferences?.savedFeeds.filter(feed => feed.pinned) ?? []
@@ -467,13 +479,13 @@ export function usePinnedFeedsInfos() {
const pinnedFeeds = pinnedItems.filter(feed => feed.type === 'feed')
let feedsPromise = Promise.resolve()
if (pinnedFeeds.length > 0) {
feedsPromise = agent.app.bsky.feed
.getFeedGenerators({
feeds: pinnedFeeds.map(f => f.value),
feedsPromise = client
.call(app.bsky.feed.getFeedGenerators, {
feeds: pinnedFeeds.map(f => f.value as AtUriString),
})
.then(res => {
for (let i = 0; i < res.data.feeds.length; i++) {
const feedView = res.data.feeds[i]
.then(data => {
for (let i = 0; i < data.feeds.length; i++) {
const feedView = data.feeds[i]
resolved.set(feedView.uri, hydrateFeedGenerator(feedView))
}
})
@@ -482,13 +494,13 @@ export function usePinnedFeedsInfos() {
// Get all lists. This currently has to be done individually.
const pinnedLists = pinnedItems.filter(feed => feed.type === 'list')
const listsPromises = pinnedLists.map(list =>
agent.app.bsky.graph
.getList({
list: list.value,
client
.call(app.bsky.graph.getList, {
list: list.value as AtUriString,
limit: 1,
})
.then(res => {
const listView = res.data.list
.then(data => {
const listView = data.list
resolved.set(listView.uri, hydrateList(listView))
}),
)
@@ -551,7 +563,7 @@ export type SavedFeedItem =
}
export function useSavedFeeds() {
const agent = useAgent()
const client = useAppviewClient()
const {data: preferences, isLoading: isLoadingPrefs} = usePreferencesQuery()
const savedItems = preferences?.savedFeeds ?? []
const queryClient = useQueryClient()
@@ -582,25 +594,25 @@ export function useSavedFeeds() {
let feedsPromise = Promise.resolve()
if (savedFeeds.length > 0) {
feedsPromise = agent.app.bsky.feed
.getFeedGenerators({
feeds: savedFeeds.map(f => f.value),
feedsPromise = client
.call(app.bsky.feed.getFeedGenerators, {
feeds: savedFeeds.map(f => f.value as AtUriString),
})
.then(res => {
res.data.feeds.forEach(f => {
.then(data => {
data.feeds.forEach(f => {
resolvedFeeds.set(f.uri, f)
})
})
}
const listsPromises = savedLists.map(list =>
agent.app.bsky.graph
.getList({
list: list.value,
client
.call(app.bsky.graph.getList, {
list: list.value as AtUriString,
limit: 1,
})
.then(res => {
const listView = res.data.list
.then(data => {
const listView = data.list
resolvedLists.set(listView.uri, listView)
}),
)
@@ -656,7 +668,7 @@ export function useSavedFeeds() {
const feedInfoQueryKeyRoot = 'feedInfo'
export function useFeedInfo(feedUri: string | undefined) {
const agent = useAgent()
const client = useAppviewClient()
return useQuery({
staleTime: STALE.INFINITY,
@@ -666,11 +678,11 @@ export function useFeedInfo(feedUri: string | undefined) {
return null
}
const res = await agent.app.bsky.feed.getFeedGenerator({
feed: feedUri,
const data = await client.call(app.bsky.feed.getFeedGenerator, {
feed: feedUri as AtUriString,
})
const feedSourceInfo = hydrateFeedGenerator(res.data.view)
const feedSourceInfo = hydrateFeedGenerator(data.view)
return feedSourceInfo
},
})
+19 -22
View File
@@ -1,4 +1,3 @@
import {type AppBskyContactGetMatches} from '@atproto/api'
import {
type InfiniteData,
type QueryClient,
@@ -6,8 +5,9 @@ import {
useQuery,
} from '@tanstack/react-query'
import {useAgent} from '#/state/session'
import {useAppviewClient} from '#/state/session'
import {type Match} from '#/components/contacts/state'
import {app} from '#/lexicons'
import type * as bsky from '#/types/bsky'
import {STALE} from '.'
@@ -15,13 +15,12 @@ const RQ_KEY_ROOT = 'find-contacts'
export const findContactsStatusQueryKey = [RQ_KEY_ROOT, 'sync-status']
export function useContactsSyncStatusQuery() {
const agent = useAgent()
const client = useAppviewClient()
return useQuery({
queryKey: findContactsStatusQueryKey,
queryFn: async () => {
const status = await agent.app.bsky.contact.getSyncStatus()
return status.data
return await client.call(app.bsky.contact.getSyncStatus, {})
},
staleTime: STALE.SECONDS.THIRTY,
})
@@ -30,15 +29,14 @@ export function useContactsSyncStatusQuery() {
export const findContactsGetMatchesQueryKey = [RQ_KEY_ROOT, 'matches']
export function useContactsMatchesQuery() {
const agent = useAgent()
const client = useAppviewClient()
return useInfiniteQuery({
queryKey: findContactsGetMatchesQueryKey,
queryFn: async ({pageParam}) => {
const matches = await agent.app.bsky.contact.getMatches({
return await client.call(app.bsky.contact.getMatches, {
cursor: pageParam,
})
return matches.data
},
initialPageParam: undefined as string | undefined,
getNextPageParam: lastPage => lastPage.cursor,
@@ -47,20 +45,19 @@ export function useContactsMatchesQuery() {
}
export function optimisticRemoveMatch(queryClient: QueryClient, did: string) {
queryClient.setQueryData<InfiniteData<AppBskyContactGetMatches.OutputSchema>>(
findContactsGetMatchesQueryKey,
old => {
if (!old) return old
queryClient.setQueryData<
InfiniteData<app.bsky.contact.getMatches.$OutputBody>
>(findContactsGetMatchesQueryKey, old => {
if (!old) return old
return {
...old,
pages: old.pages.map(page => ({
...page,
matches: page.matches.filter(match => match.did !== did),
})),
}
},
)
return {
...old,
pages: old.pages.map(page => ({
...page,
matches: page.matches.filter(match => match.did !== did),
})),
}
})
}
export const findContactsMatchesPassthroughQueryKey = (dids: string[]) => [
@@ -95,7 +92,7 @@ export function* findAllProfilesInQueryData(
did: string,
): Generator<bsky.profile.AnyProfileView, void> {
const queryDatas = queryClient.getQueriesData<
InfiniteData<AppBskyContactGetMatches.OutputSchema>
InfiniteData<app.bsky.contact.getMatches.$OutputBody>
>({
queryKey: findContactsGetMatchesQueryKey,
})
+17 -6
View File
@@ -1,13 +1,15 @@
import {useState} from 'react'
import {type DidDocument, getPdsEndpoint} from '@atproto/common-web'
import {type HandleString} from '@atproto/syntax'
import {useQuery, useQueryClient} from '@tanstack/react-query'
import {DEFAULT_SERVICE, PUBLIC_BSKY_SERVICE} from '#/lib/constants'
import {DEFAULT_SERVICE} from '#/lib/constants'
import {useDebouncedValue} from '#/lib/hooks/useDebouncedValue'
import {isNetworkError} from '#/lib/strings/errors'
import {logger} from '#/logger'
import {STALE} from '#/state/queries'
import {Agent} from '#/state/session/agent'
import {getPublicAppviewClient} from '#/state/session/clients'
import {com} from '#/lexicons'
const RQKEY_ROOT = 'pds-detection'
export const RQKEY = (identifier: string) => [RQKEY_ROOT, identifier]
@@ -147,16 +149,25 @@ export async function resolvePdsForIdentifier(
identifier: string,
): Promise<{did: string; pdsUrl: string | null} | null> {
const norm = normalizeIdentifier(identifier)
const agent = new Agent(null, {service: PUBLIC_BSKY_SERVICE})
/*
* Resolution runs without a session, so this uses the public appview client
* rather than a session-scoped one. It matches the unauthenticated,
* unproxied public agent this previously constructed by hand.
*/
const client = getPublicAppviewClient()
try {
let did: string
if (norm.startsWith('did:')) {
did = norm
} else {
const res = await withResolveTimeout(signal =>
agent.resolveHandle({handle: norm}, {signal}),
const data = await withResolveTimeout(signal =>
client.call(
com.atproto.identity.resolveHandle,
{handle: norm as HandleString},
{signal},
),
)
did = res.data.did
did = data.did
}
logger.debug('pds-detection: resolved identifier to DID', {
identifier: norm,
+19 -9
View File
@@ -1,15 +1,18 @@
import {type AtpAgent, AtUri} from '@atproto/api'
import {AtUri} from '@atproto/api'
import {type Client} from '@atproto/lex'
import {type HandleString} from '@atproto/syntax'
import {type QueryClient, queryOptions, useQuery} from '@tanstack/react-query'
import {STALE} from '#/state/queries'
import {useAgent} from '#/state/session'
import {useAppviewClient} from '#/state/session'
import {com} from '#/lexicons'
import {useUnstableProfileViewCache} from './profile'
const RQKEY_ROOT = 'resolved-did'
export const RQKEY = (didOrHandle: string) => [RQKEY_ROOT, didOrHandle]
const resolvedDidQueryOptions = (
agent: AtpAgent,
client: Client,
getUnstableProfile: (did: string) => {did: string} | undefined,
didOrHandle: string | undefined,
) =>
@@ -21,8 +24,15 @@ const resolvedDidQueryOptions = (
// Just return the did if it's already one
if (didOrHandle.startsWith('did:')) return didOrHandle
const res = await agent.resolveHandle({handle: didOrHandle})
return res.data.did
/*
* Resolution stays on the appview client: the old agent call was proxied
* to the appview, and the PDS implementation is not equivalent for
* handles hosted elsewhere.
*/
const data = await client.call(com.atproto.identity.resolveHandle, {
handle: didOrHandle as HandleString,
})
return data.did
},
initialData: () => {
// Return undefined if no did or handle
@@ -37,11 +47,11 @@ export function useResolveUriQuery(uri: string | undefined) {
const urip = new AtUri(uri || '')
const host = urip.host
const agent = useAgent()
const client = useAppviewClient()
const {getUnstableProfile} = useUnstableProfileViewCache()
return useQuery({
...resolvedDidQueryOptions(agent, getUnstableProfile, host),
...resolvedDidQueryOptions(client, getUnstableProfile, host),
select: did => ({
did,
uri: AtUri.make(did, urip.collection, urip.rkey).toString(),
@@ -50,11 +60,11 @@ export function useResolveUriQuery(uri: string | undefined) {
}
export function useResolveDidQuery(didOrHandle: string | undefined) {
const agent = useAgent()
const client = useAppviewClient()
const {getUnstableProfile} = useUnstableProfileViewCache()
return useQuery(
resolvedDidQueryOptions(agent, getUnstableProfile, didOrHandle),
resolvedDidQueryOptions(client, getUnstableProfile, didOrHandle),
)
}
+7 -6
View File
@@ -1,13 +1,14 @@
import {useCallback, useEffect, useState} from 'react'
import {View} from 'react-native'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {type DidString} from '@atproto/syntax'
import {useLingui} from '@lingui/react/macro'
import {useQueryClient} from '@tanstack/react-query'
import {PressableScale} from '#/lib/custom-animations/PressableScale'
import {STALE} from '#/state/queries'
import {profilesQueryKey} from '#/state/queries/profile'
import {useAgent, useSession} from '#/state/session'
import {useAppviewClient, useSession} from '#/state/session'
import {useSetActiveLanding} from '#/state/shell/landing'
import {
useLoggedOutView,
@@ -23,6 +24,7 @@ import {atoms as a, native, tokens, useTheme} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button'
import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
import {useAnalytics} from '#/analytics'
import {app} from '#/lexicons'
import {SplashScreen} from './SplashScreen'
enum ScreenState {
@@ -65,19 +67,18 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
const queryClient = useQueryClient()
const {accounts} = useSession()
const agent = useAgent()
const client = useAppviewClient()
useEffect(() => {
const actors = accounts.map(acc => acc.did)
const actors = accounts.map(acc => acc.did as DidString)
if (actors.length === 0) return
void queryClient.prefetchQuery({
queryKey: profilesQueryKey(actors),
staleTime: STALE.MINUTES.FIVE,
queryFn: async () => {
const res = await agent.getProfiles({actors})
return res.data
return await client.call(app.bsky.actor.getProfiles, {actors})
},
})
}, [accounts, agent, queryClient])
}, [accounts, client, queryClient])
const onPressDismiss = useCallback(() => {
if (onDismiss) {
+29 -16
View File
@@ -1,4 +1,4 @@
import {AppBskyDraftCreateDraft, AppBskyDraftDefs} from '@atproto/api'
import {AppBskyDraftDefs} from '@atproto/api'
import {
useInfiniteQuery,
useMutation,
@@ -6,10 +6,12 @@ import {
} from '@tanstack/react-query'
import {isNetworkError} from '#/lib/strings/errors'
import {useAgent} from '#/state/session'
import {matchXrpcError} from '#/lib/xrpc-error'
import {useAppviewClient} from '#/state/session'
import {type ComposerState} from '#/view/com/composer/state/composer'
import {useAnalytics} from '#/analytics'
import {getDeviceId} from '#/analytics/identifiers'
import {app} from '#/lexicons'
import {composerStateToDraft, draftViewToSummary} from './api'
import {logger} from './logger'
import * as storage from './storage'
@@ -20,7 +22,7 @@ const DRAFTS_QUERY_KEY = ['drafts']
* Hook to list all drafts for the current account
*/
export function useDraftsQuery() {
const agent = useAgent()
const client = useAppviewClient()
const ax = useAnalytics()
return useInfiniteQuery({
@@ -28,10 +30,12 @@ export function useDraftsQuery() {
queryFn: async ({pageParam}) => {
// Ensure media cache is populated before checking which media exists
await storage.ensureMediaCachePopulated()
const res = await agent.app.bsky.draft.getDrafts({cursor: pageParam})
const data = await client.call(app.bsky.draft.getDrafts, {
cursor: pageParam,
})
return {
cursor: res.data.cursor,
drafts: res.data.drafts.map(view =>
cursor: data.cursor,
drafts: data.drafts.map(view =>
draftViewToSummary({
view,
analytics: ax,
@@ -116,7 +120,7 @@ export async function loadDraftMedia(draft: AppBskyDraftDefs.Draft): Promise<{
* This ensures we don't lose data if the network request fails.
*/
export function useSaveDraftMutation() {
const agent = useAgent()
const client = useAppviewClient()
const queryClient = useQueryClient()
return useMutation({
@@ -132,7 +136,14 @@ export function useSaveDraftMutation() {
originalLocalRefs: Set<string> | undefined
}> => {
// Convert composer state to server draft format
const {draft, localRefPaths} = await composerStateToDraft(composerState)
const {draft: apiDraft, localRefPaths} =
await composerStateToDraft(composerState)
/*
* `composerStateToDraft` builds the draft against the `@atproto/api`
* types, whose string fields are unbranded, so it is asserted once here
* to the vendored input type.
*/
const draft = apiDraft as unknown as app.bsky.draft.defs.Draft
logger.debug('saving draft', {
existingDraftId,
@@ -147,7 +158,7 @@ export function useSaveDraftMutation() {
logger.debug('updating existing draft on server', {
draftId: existingDraftId,
})
await agent.app.bsky.draft.updateDraft({
await client.call(app.bsky.draft.updateDraft, {
draft: {
id: existingDraftId,
draft,
@@ -157,8 +168,8 @@ export function useSaveDraftMutation() {
} else {
// Create new draft
logger.debug('creating new draft on server')
const res = await agent.app.bsky.draft.createDraft({draft})
draftId = res.data.id
const data = await client.call(app.bsky.draft.createDraft, {draft})
draftId = data.id
logger.debug('created new draft', {draftId})
}
@@ -203,7 +214,7 @@ export function useSaveDraftMutation() {
},
onError: error => {
// Check for draft limit error
if (error instanceof AppBskyDraftCreateDraft.DraftLimitReachedError) {
if (matchXrpcError(error, app.bsky.draft.createDraft)) {
logger.error('Draft limit reached', {safeMessage: error.message})
// Error will be handled by caller
} else if (!isNetworkError(error)) {
@@ -220,7 +231,7 @@ export function useSaveDraftMutation() {
* Takes the full draft data to avoid re-fetching for media cleanup.
*/
export function useDeleteDraftMutation() {
const agent = useAgent()
const client = useAppviewClient()
const queryClient = useQueryClient()
return useMutation({
@@ -231,7 +242,7 @@ export function useDeleteDraftMutation() {
draft: AppBskyDraftDefs.Draft
}) => {
// Delete from server first - if this fails, we keep local media for retry
await agent.app.bsky.draft.deleteDraft({id: draftId})
await client.call(app.bsky.draft.deleteDraft, {id: draftId})
},
onSuccess: async (_, {draft}) => {
// Only delete local media after server deletion succeeds
@@ -264,7 +275,7 @@ export function useDeleteDraftMutation() {
* Takes draftId and originalLocalRefs from composer state.
*/
export function useCleanupPublishedDraftMutation() {
const agent = useAgent()
const client = useAppviewClient()
const queryClient = useQueryClient()
return useMutation({
@@ -280,7 +291,9 @@ export function useCleanupPublishedDraftMutation() {
mediaFileCount: originalLocalRefs.size,
})
// Delete from server first
await agent.app.bsky.draft.deleteDraft({id: draftId})
await client.call(app.bsky.draft.deleteDraft, {
id: draftId,
})
logger.debug('deleted draft from server', {draftId})
},
onSuccess: async (_, {originalLocalRefs}) => {