phase 4: migrate account flows, trending queries, notifications, and report subjects off the bridge

This commit is contained in:
Samuel Newman
2026-07-17 12:46:12 +03:00
parent 5a099a8db6
commit bab2b621a9
57 changed files with 724 additions and 494 deletions
+7 -4
View File
@@ -1,9 +1,10 @@
import {useMemo} from 'react'
import {setPersonalDetails} from '@bsky.app/sdk'
import {useMutation, useQueryClient} from '@tanstack/react-query'
import {restrictChatSettings} from '#/state/queries/messages/restrictChatSettings'
import {preferencesQueryKey} from '#/state/queries/preferences'
import {useAgent, usePdsClient, useSession} from '#/state/session'
import {usePdsClient, useSession} from '#/state/session'
import {usePatchAgeAssuranceOtherRequiredData} from '#/ageAssurance'
import {isUnderAge} from '#/ageAssurance/util'
import {IS_DEV} from '#/env'
@@ -54,14 +55,14 @@ export function useIsBirthdateUpdateAllowed() {
export function useBirthdateMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
const {currentAccount} = useSession()
const pdsClient = usePdsClient()
const patchOtherRequiredData = usePatchAgeAssuranceOtherRequiredData()
return useMutation<void, unknown, {birthDate: Date}>({
mutationFn: async ({birthDate}: {birthDate: Date}) => {
const bday = birthDate.toISOString()
await agent.setPersonalDetails({birthDate: bday})
await pdsClient.call(setPersonalDetails, {birthDate})
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
@@ -80,7 +81,9 @@ export function useBirthdateMutation() {
* birthdate, which may change the user's age assurance access level.
*/
void patchOtherRequiredData({birthdate: bday})
snoozeBirthdateUpdateAllowedForDid(agent.sessionManager.did!)
if (currentAccount) {
snoozeBirthdateUpdateAllowedForDid(currentAccount.did)
}
},
})
}
+7 -5
View File
@@ -5,9 +5,11 @@ import {
useEffect,
useState,
} from 'react'
import {type AtUriString} from '@atproto/syntax'
import * as persisted from '#/state/persisted'
import {useAgent, useSession} from '../session'
import {app} from '#/lexicons'
import {usePdsClient, useSession} from '../session'
type StateContext = Map<string, boolean>
type SetStateContext = (uri: string, value: boolean) => void
@@ -56,7 +58,7 @@ export function useSetThreadMute() {
}
function useMigrateMutes(setThreadMute: SetStateContext) {
const agent = useAgent()
const pdsClient = usePdsClient()
const {currentAccount} = useSession()
useEffect(() => {
@@ -87,8 +89,8 @@ function useMigrateMutes(setThreadMute: SetStateContext) {
setThreadMute(root, true)
await agent.api.app.bsky.graph
.muteThread({root})
await pdsClient
.call(app.bsky.graph.muteThread, {root: root as AtUriString})
// not a big deal if this fails, since the post might have been deleted
.catch(console.error)
}
@@ -100,5 +102,5 @@ function useMigrateMutes(setThreadMute: SetStateContext) {
cancelled = true
}
}
}, [agent, currentAccount, setThreadMute])
}, [pdsClient, currentAccount, setThreadMute])
}
+18 -12
View File
@@ -7,6 +7,7 @@ import {
useRef,
} from 'react'
import {AppState, type AppStateStatus} from 'react-native'
import {type Service} from '@atproto/lex-client'
import {type AtUriString} from '@atproto/syntax'
import throttle from 'lodash.throttle'
@@ -22,8 +23,8 @@ import {
} from '#/state/queries/post-feed'
import {getItemsForFeedback} from '#/view/com/posts/PostFeed'
import {useAnalytics} from '#/analytics'
import {type app} from '#/lexicons'
import {useAgent} from './session'
import {app} from '#/lexicons'
import {useAppviewClient} from './session'
export const FEEDBACK_FEEDS = [...PROD_FEEDS, ...STAGING_FEEDS]
@@ -66,7 +67,7 @@ export function useFeedFeedback(
) {
const ax = useAnalytics()
const logger = ax.logger.useChild(ax.logger.Context.FeedFeedback)
const agent = useAgent()
const appviewClient = useAppviewClient()
const feed =
!!feedSourceInfo && isFeedSourceFeedInfo(feedSourceInfo)
@@ -151,15 +152,20 @@ export function useFeedFeedback(
return
}
// Send to the feed
agent.app.bsky.feed
.sendInteractions(
{interactions: interactionsToSend, feed: feed?.uri},
/*
* Send to the feed generator via a per-call `service` option, which
* lex-client turns into the `atproto-proxy` header. This overrides the
* appview client's own service target for this one call.
*/
appviewClient
.call(
app.bsky.feed.sendInteractions,
{
encoding: 'application/json',
headers: {
'atproto-proxy': `${proxyDid}#bsky_fg`,
},
interactions: interactionsToSend,
feed: feed?.uri as AtUriString | undefined,
},
{
service: `${proxyDid}#bsky_fg` as Service,
},
)
.catch(() => {}) // ignore upstream errors
@@ -173,7 +179,7 @@ export function useFeedFeedback(
)
throttledFlushAggregatedStats()
logger.debug('flushed')
}, [agent, throttledFlushAggregatedStats, proxyDid, enabled, feed])
}, [appviewClient, throttledFlushAggregatedStats, proxyDid, enabled, feed])
const sendToFeed = useMemo(
() =>
+7 -24
View File
@@ -595,42 +595,25 @@ export class Convo {
this.convo = parseConvoView(convo, this.senderUserDid) ?? this.convo
if (this.convo) {
for (const member of this.convo.members) {
// `this.convo` comes from `parseConvoView` in the still-old-typed
// `#/components/dms/util` (migrates in a later task); bridge its member
// shape to the lexicon `ProfileViewBasic` we store. TODO(phase4): drop
// toLex once dms/util migrates.
this.relatedProfiles.set(
member.did,
bsky.toLex<chat.bsky.actor.defs.ProfileViewBasic>(member),
)
this.relatedProfiles.set(member.did, member)
}
}
this.applyProfileShadows()
}
/*
* The partial merges into `this.convo.view` and is re-parsed by the
* still-old-typed `parseConvoView` (`#/components/dms/util`, migrates in a
* later task), and its callers build it from old-typed `this.convo.details` /
* members. So this boundary stays in the old view world - typing the param
* off `ConvoWithDetails['view']` keeps it internally consistent without a
* per-call `toLex`. TODO(phase4): flip to `chat.bsky.convo.defs.ConvoView`
* once dms/util migrates.
* The partial merges into `this.convo.view` and is re-parsed by
* `parseConvoView`, and its callers build it from `this.convo.details` /
* members. The stored view is the lexicon `ConvoView`, so the partial is
* typed off it directly.
*/
private updateConvo(convo: Partial<ConvoWithDetails['view']>) {
private updateConvo(convo: Partial<chat.bsky.convo.defs.ConvoView>) {
if (this.convo) {
this.convo =
parseConvoView({...this.convo.view, ...convo}, this.senderUserDid) ??
this.convo
for (const member of this.convo.members) {
// `this.convo` comes from `parseConvoView` in the still-old-typed
// `#/components/dms/util` (migrates in a later task); bridge its member
// shape to the lexicon `ProfileViewBasic` we store. TODO(phase4): drop
// toLex once dms/util migrates.
this.relatedProfiles.set(
member.did,
bsky.toLex<chat.bsky.actor.defs.ProfileViewBasic>(member),
)
this.relatedProfiles.set(member.did, member)
}
this.applyProfileShadows()
}
+15 -12
View File
@@ -1,6 +1,8 @@
import {type AtIdentifierString} from '@atproto/syntax'
import {type QueryClient, useInfiniteQuery} from '@tanstack/react-query'
import {useAgent} from '#/state/session'
import {useAppviewClient} from '#/state/session'
import {app} from '#/lexicons'
export const RQKEY_ROOT = 'actor-starter-packs'
export const RQKEY_WITH_MEMBERSHIP_ROOT = 'actor-starter-packs-with-membership'
@@ -17,17 +19,16 @@ export function useActorStarterPacksQuery({
did?: string
enabled?: boolean
}) {
const agent = useAgent()
const appviewClient = useAppviewClient()
return useInfiniteQuery({
queryKey: RQKEY(did),
queryFn: async ({pageParam}: {pageParam?: string}) => {
const res = await agent.app.bsky.graph.getActorStarterPacks({
actor: did!,
return await appviewClient.call(app.bsky.graph.getActorStarterPacks, {
actor: did! as AtIdentifierString,
limit: 10,
cursor: pageParam,
})
return res.data
},
enabled: Boolean(did) && enabled,
initialPageParam: undefined,
@@ -42,17 +43,19 @@ export function useActorStarterPacksWithMembershipsQuery({
did?: string
enabled?: boolean
}) {
const agent = useAgent()
const appviewClient = useAppviewClient()
return useInfiniteQuery({
queryKey: RQKEY_WITH_MEMBERSHIP(did),
queryFn: async ({pageParam}: {pageParam?: string}) => {
const res = await agent.app.bsky.graph.getStarterPacksWithMembership({
actor: did!,
limit: 10,
cursor: pageParam,
})
return res.data
return await appviewClient.call(
app.bsky.graph.getStarterPacksWithMembership,
{
actor: did! as AtIdentifierString,
limit: 10,
cursor: pageParam,
},
)
},
enabled: Boolean(did) && enabled,
initialPageParam: undefined,
+21 -27
View File
@@ -1,3 +1,5 @@
import {Client} from '@atproto/lex-client'
import {type DatetimeString, type HandleString} from '@atproto/syntax'
import {useQuery} from '@tanstack/react-query'
import {
@@ -8,20 +10,8 @@ import {
import {useDebouncedValue} from '#/lib/hooks/useDebouncedValue'
import {createFullHandle} from '#/lib/strings/handles'
import {useAnalytics} from '#/analytics'
import {Agent} from '../session/agent'
/*
* `com.atproto.temp.checkHandleAvailability` is an entryway-only endpoint that
* isn't generated into `#/lexicons`, so we describe its result union locally.
* The response is discriminated by `$type`; we narrow against these shapes
* rather than a branded lexicon guard.
*/
type CheckHandleAvailabilityResult =
| {$type: 'com.atproto.temp.checkHandleAvailability#resultAvailable'}
| {
$type: 'com.atproto.temp.checkHandleAvailability#resultUnavailable'
suggestions: {handle: string; method: string}[]
}
import {com} from '#/lexicons'
import * as bsky from '#/types/bsky'
export const RQKEY_handleAvailability = (
handle: string,
@@ -90,24 +80,28 @@ export async function checkHandleAvailability(
},
) {
if (serviceDid === BSKY_SERVICE_DID) {
const agent = new Agent(null, {service: BSKY_SERVICE})
// entryway has a special API for handle availability
const {data} = await agent.com.atproto.temp.checkHandleAvailability({
handle,
birthDate,
const client = new Client({service: BSKY_SERVICE})
const data = await client.call(com.atproto.temp.checkHandleAvailability, {
handle: handle as HandleString,
birthDate: birthDate as DatetimeString | undefined,
email,
})
const result = data.result as CheckHandleAvailabilityResult
const result = data.result
if (
result.$type ===
'com.atproto.temp.checkHandleAvailability#resultAvailable'
bsky.isType(
com.atproto.temp.checkHandleAvailability.resultAvailable,
result,
)
) {
return {available: true} as const
} else if (
result.$type ===
'com.atproto.temp.checkHandleAvailability#resultUnavailable'
bsky.isType(
com.atproto.temp.checkHandleAvailability.resultUnavailable,
result,
)
) {
return {
available: false,
@@ -120,13 +114,13 @@ export async function checkHandleAvailability(
}
} else {
// 3rd party PDSes won't have this API so just try and resolve the handle
const agent = new Agent(null, {service: PUBLIC_BSKY_SERVICE})
const client = new Client({service: PUBLIC_BSKY_SERVICE})
try {
const res = await agent.resolveHandle({
handle,
const res = await client.call(com.atproto.identity.resolveHandle, {
handle: handle as HandleString,
})
if (res.data.did) {
if (res.did) {
return {available: false} as const
}
} catch {}
+21 -11
View File
@@ -1,8 +1,10 @@
import {useCallback} from 'react'
import {type AtIdentifierString, type HandleString} from '@atproto/syntax'
import {useMutation, useQueryClient} from '@tanstack/react-query'
import {STALE} from '#/state/queries'
import {useAgent} from '#/state/session'
import {useAppviewClient, usePdsClient} from '#/state/session'
import {app, com} from '#/lexicons'
const handleQueryKeyRoot = 'handle'
const fetchHandleQueryKey = (handleOrDid: string) => [
@@ -14,7 +16,7 @@ const fetchDidQueryKey = (handleOrDid: string) => [didQueryKeyRoot, handleOrDid]
export function useFetchHandle() {
const queryClient = useQueryClient()
const agent = useAgent()
const appviewClient = useAppviewClient()
return useCallback(
async (handleOrDid: string) => {
@@ -22,13 +24,16 @@ export function useFetchHandle() {
const res = await queryClient.fetchQuery({
staleTime: STALE.MINUTES.FIVE,
queryKey: fetchHandleQueryKey(handleOrDid),
queryFn: () => agent.getProfile({actor: handleOrDid}),
queryFn: () =>
appviewClient.call(app.bsky.actor.getProfile, {
actor: handleOrDid as AtIdentifierString,
}),
})
return res.data.handle
return res.handle
}
return handleOrDid
},
[queryClient, agent],
[queryClient, appviewClient],
)
}
@@ -36,11 +41,13 @@ export function useUpdateHandleMutation(opts?: {
onSuccess?: (handle: string) => void
}) {
const queryClient = useQueryClient()
const agent = useAgent()
const pdsClient = usePdsClient()
return useMutation({
mutationFn: async ({handle}: {handle: string}) => {
await agent.updateHandle({handle})
await pdsClient.call(com.atproto.identity.updateHandle, {
handle: handle as HandleString,
})
},
onSuccess(_data, variables) {
opts?.onSuccess?.(variables.handle)
@@ -53,7 +60,7 @@ export function useUpdateHandleMutation(opts?: {
export function useFetchDid() {
const queryClient = useQueryClient()
const agent = useAgent()
const appviewClient = useAppviewClient()
return useCallback(
async (handleOrDid: string) => {
@@ -63,13 +70,16 @@ export function useFetchDid() {
queryFn: async () => {
let identifier = handleOrDid
if (!identifier.startsWith('did:')) {
const res = await agent.resolveHandle({handle: identifier})
identifier = res.data.did
const res = await appviewClient.call(
com.atproto.identity.resolveHandle,
{handle: identifier as HandleString},
)
identifier = res.did
}
return identifier
},
})
},
[queryClient, agent],
[queryClient, appviewClient],
)
}
+10 -5
View File
@@ -1,22 +1,27 @@
import {type AtUriString} from '@atproto/syntax'
import {deleteLike, like} from '@bsky.app/sdk'
import {useMutation} from '@tanstack/react-query'
import {useAgent} from '#/state/session'
import {usePdsClient} from '#/state/session'
export function useLikeMutation() {
const agent = useAgent()
const pdsClient = usePdsClient()
return useMutation({
mutationFn: async ({uri, cid}: {uri: string; cid: string}) => {
const res = await agent.like(uri, cid)
const res = await pdsClient.call(like, {
uri: uri as AtUriString,
cid,
})
return {uri: res.uri}
},
})
}
export function useUnlikeMutation() {
const agent = useAgent()
const pdsClient = usePdsClient()
return useMutation({
mutationFn: async ({uri}: {uri: string}) => {
await agent.deleteLike(uri)
await pdsClient.call(deleteLike, uri as AtUriString)
},
})
}
@@ -745,17 +745,18 @@ export function ListConvosProviderInner({
/*
* `log.message` can also be a deleted-message view per the
* log union, which the strict MessageAndReactionView type
* rejects - the old types absorbed this via the open-union
* catch-all. Keep the pre-migration runtime behavior (always
* store the view we got) and assert the cache type.
* rejects - the raw ConvoView cache type only admits a live
* MessageView here. Keep the pre-migration runtime behavior
* (always store the view we got) and assert into the cache
* type; the runtime value is unchanged.
*/
lastReaction: bsky.toLex<
NonNullable<chat.bsky.convo.defs.ConvoView['lastReaction']>
>({
lastReaction: {
$type: 'chat.bsky.convo.defs#messageAndReactionView',
reaction: log.reaction,
message: log.message,
}),
} as NonNullable<
chat.bsky.convo.defs.ConvoView['lastReaction']
>,
rev: log.rev,
}
}),
+8 -7
View File
@@ -1,3 +1,4 @@
import {removeNuxs, upsertNux} from '@bsky.app/sdk'
import {useMutation, useQueryClient} from '@tanstack/react-query'
import {type AppNux, type Nux} from '#/state/queries/nuxs/definitions'
@@ -6,7 +7,7 @@ import {
preferencesQueryKey,
usePreferencesQuery,
} from '#/state/queries/preferences'
import {useAgent} from '#/state/session'
import {usePdsClient} from '#/state/session'
export {Nux} from '#/state/queries/nuxs/definitions'
@@ -42,11 +43,11 @@ export function useNuxs():
// if (__DEV__) {
// const queryClient = useQueryClient()
// const agent = useAgent()
// const pdsClient = usePdsClient()
// // @ts-ignore
// window.clearNux = async (ids: string[]) => {
// await agent.bskyAppRemoveNuxs(ids)
// await pdsClient.call(removeNuxs, ids)
// // triggers a refetch
// await queryClient.invalidateQueries({
// queryKey: preferencesQueryKey,
@@ -97,12 +98,12 @@ export function useNux<T extends Nux>(
export function useSaveNux() {
const queryClient = useQueryClient()
const agent = useAgent()
const pdsClient = usePdsClient()
return useMutation({
retry: 3,
mutationFn: async (nux: AppNux) => {
await agent.bskyAppUpsertNux(serializeAppNux(nux))
await pdsClient.call(upsertNux, serializeAppNux(nux))
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
@@ -113,12 +114,12 @@ export function useSaveNux() {
export function useResetNuxs() {
const queryClient = useQueryClient()
const agent = useAgent()
const pdsClient = usePdsClient()
return useMutation({
retry: 3,
mutationFn: async (ids: string[]) => {
await agent.bskyAppRemoveNuxs(ids)
await pdsClient.call(removeNuxs, ids)
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
+14 -4
View File
@@ -1,5 +1,7 @@
import {useState} from 'react'
import {type DidDocument, getPdsEndpoint} from '@atproto/common-web'
import {Client} from '@atproto/lex-client'
import {type HandleString} from '@atproto/syntax'
import {useQuery, useQueryClient} from '@tanstack/react-query'
import {DEFAULT_SERVICE, PUBLIC_BSKY_SERVICE} from '#/lib/constants'
@@ -7,7 +9,7 @@ import {useDebouncedValue} from '#/lib/hooks/useDebouncedValue'
import {isNetworkError} from '#/lib/strings/errors'
import {logger} from '#/logger'
import {STALE} from '#/state/queries'
import {Agent} from '#/state/session/agent'
import {com} from '#/lexicons'
const RQKEY_ROOT = 'pds-detection'
export const RQKEY = (identifier: string) => [RQKEY_ROOT, identifier]
@@ -147,16 +149,24 @@ 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})
/*
* Unauthenticated throwaway client pointed at the public appview -
* resolveHandle is a public read.
*/
const client = new Client({service: PUBLIC_BSKY_SERVICE})
try {
let did: string
if (norm.startsWith('did:')) {
did = norm
} else {
const res = await withResolveTimeout(signal =>
agent.resolveHandle({handle: norm}, {signal}),
client.call(
com.atproto.identity.resolveHandle,
{handle: norm as HandleString},
{signal},
),
)
did = res.data.did
did = res.did
}
logger.debug('pds-detection: resolved identifier to DID', {
identifier: norm,
+12 -5
View File
@@ -1,7 +1,8 @@
import {useQuery} from '@tanstack/react-query'
import {STALE} from '#/state/queries'
import {useAgent} from '#/state/session'
import {useAppviewClient} from '#/state/session'
import {app} from '#/lexicons'
type ServiceConfig = {
checkEmailConfirmed: boolean
@@ -13,18 +14,24 @@ type ServiceConfig = {
}
export function useServiceConfigQuery() {
const agent = useAgent()
const appviewClient = useAppviewClient()
return useQuery<ServiceConfig>({
refetchOnWindowFocus: true,
staleTime: STALE.MINUTES.FIVE,
queryKey: ['service-config'],
queryFn: async () => {
try {
const {data} = await agent.api.app.bsky.unspecced.getConfig()
const data = await appviewClient.call(app.bsky.unspecced.getConfig)
return {
checkEmailConfirmed: Boolean(data.checkEmailConfirmed),
// @ts-expect-error not included in types atm
topicsEnabled: Boolean(data.topicsEnabled),
/*
* `topicsEnabled` is served by the appview but is not (yet) part of
* the getConfig lexicon schema, so read it through a narrow local
* cast rather than the generated body type.
*/
topicsEnabled: Boolean(
(data as {topicsEnabled?: boolean}).topicsEnabled,
),
liveNow: data.liveNow ?? [],
}
} catch (e) {
@@ -7,16 +7,15 @@ import {
import {getContentLanguages} from '#/state/preferences/languages'
import {STALE} from '#/state/queries'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {useAgent} from '#/state/session'
import {type app} from '#/lexicons'
import {toLex} from '#/types/bsky'
import {useAppviewClient} from '#/state/session'
import {app} from '#/lexicons'
export const DEFAULT_LIMIT = 15
export const createGetSuggestedFeedsQueryKey = () => ['suggested-feeds']
export function useGetSuggestedFeedsQuery({enabled}: {enabled?: boolean}) {
const agent = useAgent()
const appviewClient = useAppviewClient()
const {data: preferences} = usePreferencesQuery()
const savedFeeds = preferences?.savedFeeds
@@ -26,7 +25,8 @@ export function useGetSuggestedFeedsQuery({enabled}: {enabled?: boolean}) {
queryKey: createGetSuggestedFeedsQueryKey(),
queryFn: async () => {
const contentLangs = getContentLanguages().join(',')
const {data} = await agent.app.bsky.unspecced.getSuggestedFeeds(
const data = await appviewClient.call(
app.bsky.unspecced.getSuggestedFeeds,
{
limit: DEFAULT_LIMIT,
},
@@ -38,17 +38,11 @@ export function useGetSuggestedFeedsQuery({enabled}: {enabled?: boolean}) {
},
)
/*
* TODO(phase4): drop toLex once getSuggestedFeeds migrates off the bridge
* agent (intentionally left on the bridge in Phase 3).
*/
return {
feeds: toLex<app.bsky.feed.defs.GeneratorView[]>(
data.feeds.filter(feed => {
const isSaved = !!savedFeeds?.find(s => s.value === feed.uri)
return !isSaved
}),
),
feeds: data.feeds.filter(feed => {
const isSaved = !!savedFeeds?.find(s => s.value === feed.uri)
return !isSaved
}),
}
},
})
@@ -5,9 +5,8 @@ import {logger} from '#/logger'
import {getContentLanguages} from '#/state/preferences/languages'
import {STALE} from '#/state/queries'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {useAgent} from '#/state/session'
import {type app} from '#/lexicons'
import {toLex} from '#/types/bsky'
import {useAppviewClient} from '#/state/session'
import {app} from '#/lexicons'
export type QueryProps = {
category?: string | null
@@ -28,7 +27,7 @@ export const createGetSuggestedOnboardingUsersQueryKey = (
]
export function useGetSuggestedOnboardingUsersQuery(props: QueryProps) {
const agent = useAgent()
const appviewClient = useAppviewClient()
const {data: preferences} = usePreferencesQuery()
return useQuery({
@@ -40,7 +39,8 @@ export function useGetSuggestedOnboardingUsersQuery(props: QueryProps) {
const overrideInterests = props.overrideInterests.join(',')
const {data} = await agent.app.bsky.unspecced.getSuggestedOnboardingUsers(
const data = await appviewClient.call(
app.bsky.unspecced.getSuggestedOnboardingUsers,
{
category: props.category ?? undefined,
limit: props.limit || 10,
@@ -56,15 +56,7 @@ export function useGetSuggestedOnboardingUsersQuery(props: QueryProps) {
if (!data.recIdStr) {
logger.debug('getSuggestedOnboardingUsers response missing recIdStr')
}
/*
* TODO(phase4): drop toLex once getSuggestedOnboardingUsers migrates off
* the bridge agent (this unspecced endpoint is intentionally left on the
* bridge in Phase 3, so it returns old `@atproto/api` view types).
*/
return toLex<{
actors: app.bsky.actor.defs.ProfileView[]
recId: string | undefined
}>({...data, recId: data.recIdStr})
return {...data, recId: data.recIdStr}
},
})
}
@@ -8,9 +8,8 @@ import {logger} from '#/logger'
import {getContentLanguages} from '#/state/preferences/languages'
import {STALE} from '#/state/queries'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {useAgent} from '#/state/session'
import {type app} from '#/lexicons'
import {toLex} from '#/types/bsky'
import {useAppviewClient} from '#/state/session'
import {app} from '#/lexicons'
export type QueryProps = {
limit?: number
@@ -23,7 +22,7 @@ export const createGetSuggestedUsersForDiscoverQueryKey = (props: {
}) => [getSuggestedUsersForDiscoverQueryKeyRoot, props.limit]
export function useGetSuggestedUsersForDiscoverQuery(props: QueryProps = {}) {
const agent = useAgent()
const appviewClient = useAppviewClient()
const {data: preferences} = usePreferencesQuery()
return useQuery({
@@ -33,30 +32,22 @@ export function useGetSuggestedUsersForDiscoverQuery(props: QueryProps = {}) {
const contentLangs = getContentLanguages().join(',')
const userInterests = aggregateUserInterests(preferences)
const {data} =
await agent.app.bsky.unspecced.getSuggestedUsersForDiscover(
{
limit: props.limit || 10,
const data = await appviewClient.call(
app.bsky.unspecced.getSuggestedUsersForDiscover,
{
limit: props.limit || 10,
},
{
headers: {
...createBskyTopicsHeader(userInterests),
'Accept-Language': contentLangs,
},
{
headers: {
...createBskyTopicsHeader(userInterests),
'Accept-Language': contentLangs,
},
},
)
},
)
if (!data.recIdStr) {
logger.debug('getSuggestedUsersForDiscover response missing recIdStr')
}
/*
* TODO(phase4): drop toLex once getSuggestedUsersForDiscover migrates off
* the bridge agent (this unspecced endpoint is intentionally left on the
* bridge in Phase 3, so it returns old `@atproto/api` view types).
*/
return toLex<{
actors: app.bsky.actor.defs.ProfileView[]
recId: string | undefined
}>({...data, recId: data.recIdStr})
return {...data, recId: data.recIdStr}
},
})
}
@@ -8,9 +8,8 @@ import {logger} from '#/logger'
import {getContentLanguages} from '#/state/preferences/languages'
import {STALE} from '#/state/queries'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {useAgent} from '#/state/session'
import {type app} from '#/lexicons'
import {toLex} from '#/types/bsky'
import {useAppviewClient} from '#/state/session'
import {app} from '#/lexicons'
export type QueryProps = {
category?: string | null
@@ -24,7 +23,7 @@ export const createGetSuggestedUsersForExploreQueryKey = (
) => [getSuggestedUsersForExploreQueryKeyRoot, props.category, props.limit]
export function useGetSuggestedUsersForExploreQuery(props: QueryProps = {}) {
const agent = useAgent()
const appviewClient = useAppviewClient()
const {data: preferences} = usePreferencesQuery()
return useQuery({
@@ -34,7 +33,8 @@ export function useGetSuggestedUsersForExploreQuery(props: QueryProps = {}) {
const contentLangs = getContentLanguages().join(',')
const userInterests = aggregateUserInterests(preferences)
const {data} = await agent.app.bsky.unspecced.getSuggestedUsersForExplore(
const data = await appviewClient.call(
app.bsky.unspecced.getSuggestedUsersForExplore,
{
category: props.category ?? undefined,
limit: props.limit || 10,
@@ -50,14 +50,7 @@ export function useGetSuggestedUsersForExploreQuery(props: QueryProps = {}) {
if (!data.recIdStr) {
logger.debug('getSuggestedUsersForExplore response missing recIdStr')
}
/*
* TODO(phase4): drop toLex once getSuggestedUsersForExplore migrates off
* the bridge agent (intentionally left on the bridge in Phase 3).
*/
return toLex<{
actors: app.bsky.actor.defs.ProfileView[]
recId: string | undefined
}>({...data, recId: data.recIdStr})
return {...data, recId: data.recIdStr}
},
})
}
@@ -8,9 +8,8 @@ import {logger} from '#/logger'
import {getContentLanguages} from '#/state/preferences/languages'
import {STALE} from '#/state/queries'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {useAgent} from '#/state/session'
import {type app} from '#/lexicons'
import {toLex} from '#/types/bsky'
import {useAppviewClient} from '#/state/session'
import {app} from '#/lexicons'
export type QueryProps = {
category?: string | null
@@ -26,7 +25,7 @@ export const createGetSuggestedUsersForSeeMoreQueryKey = (props: {
}) => [getSuggestedUsersForSeeMoreQueryKeyRoot, props.category, props.limit]
export function useGetSuggestedUsersForSeeMoreQuery(props: QueryProps = {}) {
const agent = useAgent()
const appviewClient = useAppviewClient()
const {data: preferences} = usePreferencesQuery()
return useQuery({
@@ -40,7 +39,8 @@ export function useGetSuggestedUsersForSeeMoreQuery(props: QueryProps = {}) {
const contentLangs = getContentLanguages().join(',')
const userInterests = aggregateUserInterests(preferences)
const {data} = await agent.app.bsky.unspecced.getSuggestedUsersForSeeMore(
const data = await appviewClient.call(
app.bsky.unspecced.getSuggestedUsersForSeeMore,
{
category: props.category ?? undefined,
limit: props.limit || 50,
@@ -56,14 +56,7 @@ export function useGetSuggestedUsersForSeeMoreQuery(props: QueryProps = {}) {
if (!data.recIdStr) {
logger.debug('getSuggestedUsersForSeeMore response missing recIdStr')
}
/*
* TODO(phase4): drop toLex once this unspecced endpoint migrates off the
* bridge agent (intentionally left on the bridge in Phase 3).
*/
return toLex<{
actors: app.bsky.actor.defs.ProfileView[]
recId: string | undefined
}>({...data, recId: data.recIdStr})
return {...data, recId: data.recIdStr}
},
})
}
@@ -7,9 +7,8 @@ import {
import {getContentLanguages} from '#/state/preferences/languages'
import {STALE} from '#/state/queries'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {useAgent} from '#/state/session'
import {type app} from '#/lexicons'
import {toLex} from '#/types/bsky'
import {useAppviewClient} from '#/state/session'
import {app} from '#/lexicons'
export const createOnboardingSuggestedStarterPacksQueryKey = (
interests?: string[],
@@ -22,7 +21,7 @@ export function useOnboardingSuggestedStarterPacksQuery({
enabled?: boolean
overrideInterests?: string[]
}) {
const agent = useAgent()
const appviewClient = useAppviewClient()
const {data: preferences} = usePreferencesQuery()
const contentLangs = getContentLanguages().join(',')
@@ -31,27 +30,19 @@ export function useOnboardingSuggestedStarterPacksQuery({
staleTime: STALE.MINUTES.THREE,
queryKey: createOnboardingSuggestedStarterPacksQueryKey(overrideInterests),
queryFn: async () => {
const {data} =
await agent.app.bsky.unspecced.getOnboardingSuggestedStarterPacks(
{limit: 6},
{
headers: {
...createBskyTopicsHeader(
overrideInterests
? overrideInterests.join(',')
: aggregateUserInterests(preferences),
),
'Accept-Language': contentLangs,
},
return await appviewClient.call(
app.bsky.unspecced.getOnboardingSuggestedStarterPacks,
{limit: 6},
{
headers: {
...createBskyTopicsHeader(
overrideInterests
? overrideInterests.join(',')
: aggregateUserInterests(preferences),
),
'Accept-Language': contentLangs,
},
)
/*
* TODO(phase4): drop toLex once getOnboardingSuggestedStarterPacks
* migrates off the bridge agent (intentionally left on the bridge in
* Phase 3).
*/
return toLex<app.bsky.unspecced.getOnboardingSuggestedStarterPacks.$OutputBody>(
data,
},
)
},
})
@@ -7,9 +7,8 @@ import {
import {getContentLanguages} from '#/state/preferences/languages'
import {STALE} from '#/state/queries'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {useAgent} from '#/state/session'
import {type app} from '#/lexicons'
import {toLex} from '#/types/bsky'
import {useAppviewClient} from '#/state/session'
import {app} from '#/lexicons'
export const createSuggestedStarterPacksQueryKey = (interests?: string[]) => [
'suggested-starter-packs',
@@ -23,7 +22,7 @@ export function useSuggestedStarterPacksQuery({
enabled?: boolean
overrideInterests?: string[]
}) {
const agent = useAgent()
const appviewClient = useAppviewClient()
const {data: preferences} = usePreferencesQuery()
const contentLangs = getContentLanguages().join(',')
@@ -32,8 +31,9 @@ export function useSuggestedStarterPacksQuery({
staleTime: STALE.MINUTES.THREE,
queryKey: createSuggestedStarterPacksQueryKey(overrideInterests),
queryFn: async () => {
const {data} = await agent.app.bsky.unspecced.getSuggestedStarterPacks(
undefined,
return await appviewClient.call(
app.bsky.unspecced.getSuggestedStarterPacks,
{},
{
headers: {
...createBskyTopicsHeader(
@@ -45,13 +45,6 @@ export function useSuggestedStarterPacksQuery({
},
},
)
/*
* TODO(phase4): drop toLex once getSuggestedStarterPacks migrates off the
* bridge agent (intentionally left on the bridge in Phase 3).
*/
return toLex<app.bsky.unspecced.getSuggestedStarterPacks.$OutputBody>(
data,
)
},
})
}
+14 -8
View File
@@ -1,12 +1,10 @@
import {Client} from '@atproto/lex-client'
import {PasswordSession} from '@atproto/lex-password-session'
import {isJwtExpired} from '#/lib/jwt'
import {type TemporaryPushClient} from '#/lib/notifications/notifications'
import * as persisted from '#/state/persisted'
import {
networkAwareFetch,
sessionAccountToSessionData,
SessionAgent,
} from './session-core'
import {networkAwareFetch, sessionAccountToSessionData} from './session-core'
import {type SessionAccount} from './types'
/*
@@ -33,22 +31,30 @@ export function isSessionExpired(account: SessionAccount) {
* Creates and resumes a throwaway session for every stored account.
* Intended to send push token revocations just before logout.
*
* Each returned {@link SessionAgent} wraps a temporary `PasswordSession`
* Each returned {@link TemporaryPushClient} wraps a temporary `PasswordSession`
* resumed over the network to obtain a valid access token. These sessions are
* deliberately hook-free (no `onUpdated`/`onDeleted`): they must NEVER persist
* or race the active session. They are used once for the unregister call and
* discarded (reclaimed by GC), so we never call `logout()` on them.
*
* Each session is wrapped in a plain account-shaped `Client` (no proxy header)
* paired with the account's service origin and handle, matching the contract
* {@link unregisterPushToken} consumes.
*/
export async function createTemporaryAgentsAndResume(
accounts: SessionAccount[],
): Promise<SessionAgent[]> {
): Promise<TemporaryPushClient[]> {
const settled = await Promise.allSettled(
accounts.map(async account => {
const session = await PasswordSession.resume(
sessionAccountToSessionData(account),
{fetch: networkAwareFetch},
)
return new SessionAgent(session)
return {
client: new Client(session),
service: session.session.service,
handle: session.session.handle,
} satisfies TemporaryPushClient
}),
)