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