phase 4 foundations: vendor missing lexicons, refreshSession api, toLex no-op purge, de-agent link-meta

This commit is contained in:
Samuel Newman
2026-07-17 11:55:07 +03:00
parent d13d1ca604
commit d052d14f2b
80 changed files with 649 additions and 450 deletions
+4 -15
View File
@@ -205,12 +205,8 @@ export function useFeedPreviews(
if (item.isFallbackMarker) continue
const moderations = item.items.map(item =>
// TODO(phase4): drop toLex once feed-manip is migrated off
// @atproto/api and yields lex-typed slice items.
moderatePost(
bsky.toLex<app.bsky.feed.defs.PostView>(item.post),
moderationOpts!,
),
moderatePost(item.post, moderationOpts!),
)
// apply moderation filters
@@ -236,19 +232,12 @@ export function useFeedPreviews(
const feedPostSliceItem: FeedPostSliceItem = {
_reactKey: `${item._reactKey}-${i}-${subItem.post.uri}`,
uri: subItem.post.uri,
// TODO(phase4): drop toLex once feed-manip is migrated
// off @atproto/api and yields lex-typed slice items.
post: bsky.toLex<app.bsky.feed.defs.PostView>(
subItem.post,
),
record: bsky.toLex<app.bsky.feed.post.Main>(
subItem.record,
),
post: subItem.post,
record: subItem.record,
moderation: moderations[i],
parentAuthor: subItem.parentAuthor
? bsky.toLex<app.bsky.actor.defs.ProfileViewBasic>(
subItem.parentAuthor,
)
? subItem.parentAuthor
: undefined,
isParentBlocked: subItem.isParentBlocked,
isParentNotFound: subItem.isParentNotFound,
+11 -20
View File
@@ -57,33 +57,24 @@ export function useAddGroupMembers(
chat.bsky.actor.defs.ProfileViewBasic[]
>(listConvoMembersQueryKey(convoId))
/*
* The profile views come from producers that still emit the old
* `@atproto/api` shapes (useProfileQuery migrates in a later task), while
* the chat caches are now typed on the lexicon views. Structurally
* identical modulo branded strings. TODO(phase4): drop toLex once those
* producers migrate.
*/
const addedBy: chat.bsky.actor.defs.ProfileViewBasic | undefined =
myProfile
? bsky.toLex<chat.bsky.actor.defs.ProfileViewBasic>({
? {
...myProfile,
$type: 'chat.bsky.actor.defs#profileViewBasic',
})
}
: undefined
const optimisticMembers: chat.bsky.actor.defs.ProfileViewBasic[] =
profiles.map(profile =>
bsky.toLex<chat.bsky.actor.defs.ProfileViewBasic>({
...profile,
$type: 'chat.bsky.actor.defs#profileViewBasic',
kind: {
$type: 'chat.bsky.actor.defs#groupConvoMember',
role: 'standard',
addedBy,
},
}),
)
profiles.map(profile => ({
...profile,
$type: 'chat.bsky.actor.defs#profileViewBasic',
kind: {
$type: 'chat.bsky.actor.defs#groupConvoMember',
role: 'standard',
addedBy,
},
}))
queryClient.setQueryData<chat.bsky.convo.defs.ConvoView>(
CONVO_KEY(convoId),
+1 -4
View File
@@ -297,11 +297,8 @@ export function* findAllPostsInQueryData(
if (bsky.isType(app.bsky.feed.defs.postView, item.subject)) {
const quotedPost = getEmbeddedPost(item.subject?.embed)
if (quotedPost && didOrHandleUriMatches(atUri, quotedPost)) {
// TODO(phase4): drop toLex once ../util is migrated and
// embedViewRecordToPostView returns the lexicon PostView.
yield bsky.toLex<app.bsky.feed.defs.PostView>(
embedViewRecordToPostView(quotedPost),
)
yield embedViewRecordToPostView(quotedPost)
}
}
}
+4 -12
View File
@@ -288,9 +288,8 @@ export function usePostFeedQuery(
.map(slice => {
const moderations = slice.items.map(item =>
moderatePost(
// TODO(phase4): drop toLex once feed-manip is migrated
// off @atproto/api and yields lex-typed slice items.
bsky.toLex<app.bsky.feed.defs.PostView>(item.post),
item.post,
moderationOpts!,
),
)
@@ -342,19 +341,12 @@ export function usePostFeedQuery(
const feedPostSliceItem: FeedPostSliceItem = {
_reactKey: `${slice._reactKey}-${i}-${item.post.uri}`,
uri: item.post.uri,
// TODO(phase4): drop toLex once feed-manip is migrated
// off @atproto/api and yields lex-typed slice items.
post: bsky.toLex<app.bsky.feed.defs.PostView>(
item.post,
),
record: bsky.toLex<app.bsky.feed.post.Main>(
item.record,
),
post: item.post,
record: item.record,
moderation: moderations[i],
parentAuthor: item.parentAuthor
? bsky.toLex<app.bsky.actor.defs.ProfileViewBasic>(
item.parentAuthor,
)
? item.parentAuthor
: undefined,
isParentBlocked: item.isParentBlocked,
isParentNotFound: item.isParentNotFound,
+2 -11
View File
@@ -44,7 +44,6 @@ import {useAgeAssurance} from '#/ageAssurance'
import {makeAgeRestrictedModerationPrefs} from '#/ageAssurance/util'
import {useAnalytics} from '#/analytics'
import {app} from '#/lexicons'
import {toLex} from '#/types/bsky'
export * from '#/state/queries/preferences/const'
export * from '#/state/queries/preferences/moderation'
@@ -110,16 +109,8 @@ export function usePreferencesQuery() {
) {
data = {
...data,
/*
* TODO(phase4): drop the toLex bridges once
* `#/ageAssurance/util` (makeAgeRestrictedModerationPrefs) sources
* `ModerationPrefs` from `@bsky.app/sdk/moderation` instead of
* `@atproto/api`. The two shapes differ only in scalar branding
* (e.g. MutedWord.actorTarget's UnknownString), so the values are
* structurally interchangeable at this boundary.
*/
moderationPrefs: toLex(
makeAgeRestrictedModerationPrefs(toLex(data.moderationPrefs)),
moderationPrefs: makeAgeRestrictedModerationPrefs(
data.moderationPrefs,
),
}
}
+4 -6
View File
@@ -7,7 +7,7 @@ import {
resolveLink,
} from '#/lib/api/resolve'
import {STALE} from '#/state/queries/index'
import {useAgent, useChatClient, useLexClient} from '#/state/session'
import {useChatClient, useLexClient} from '#/state/session'
import {type Gif} from '#/features/gifPicker/types'
export const RQKEY_LINK_ROOT = 'resolve-link'
@@ -26,15 +26,13 @@ export function resolveLinkQueryOptions(clients: ResolveClients, url: string) {
/**
* Bundle the clients the link resolver needs from the session hooks. The
* appview client serves the `app.bsky.*` reads and handle resolution, the chat
* client serves group join-link previews, and the bridge agent is still passed
* through to the not-yet-migrated `getLinkMeta`.
* appview client serves the `app.bsky.*` reads and handle resolution, and the
* chat client serves group join-link previews.
*/
export function useResolveClients(): ResolveClients {
const appview = useLexClient()
const chat = useChatClient()
const agent = useAgent()
return {appview, chat, agent}
return {appview, chat}
}
export function useResolveLinkQuery(url: string) {
+7 -19
View File
@@ -255,27 +255,23 @@ export function* getThreadPlaceholderCandidates(
for (let post of findAllPostsInNotifsQueryData(queryClient, uri)) {
yield postViewToThreadPlaceholder(post)
}
/*
* TODO(phase4): drop toLex once the feed/quote/search/bookmarks/explore
* `findAllPostsInQueryData` generators are migrated to `#/lexicons` types.
*/
for (let post of findAllPostsInFeedQueryData(queryClient, uri)) {
yield postViewToThreadPlaceholder(bsky.toLex(post))
yield postViewToThreadPlaceholder(post)
}
for (let post of findAllPostsInQuoteQueryData(queryClient, uri)) {
yield postViewToThreadPlaceholder(bsky.toLex(post))
yield postViewToThreadPlaceholder(post)
}
for (let post of findAllPostsInSearchQueryData(queryClient, uri)) {
yield postViewToThreadPlaceholder(bsky.toLex(post))
yield postViewToThreadPlaceholder(post)
}
for (let post of findAllPostsInBookmarksQueryData(queryClient, uri)) {
yield postViewToThreadPlaceholder(bsky.toLex(post))
yield postViewToThreadPlaceholder(post)
}
for (let post of findAllPostsInExploreFeedPreviewsQueryData(
queryClient,
uri,
)) {
yield postViewToThreadPlaceholder(bsky.toLex(post))
yield postViewToThreadPlaceholder(post)
}
}
@@ -302,11 +298,7 @@ export function* findAllPostsInQueryData(
const qp = getEmbeddedPost(item.value.post.embed)
if (qp && didOrHandleUriMatches(atUri, qp)) {
/*
* TODO(phase4): drop toLex once `embedViewRecordToPostView` in
* `#/state/queries/util` is migrated to `#/lexicons` types.
*/
yield bsky.toLex(embedViewRecordToPostView(qp))
yield embedViewRecordToPostView(qp)
}
}
}
@@ -335,11 +327,7 @@ export function* findAllProfilesInQueryData(
const qp = getEmbeddedPost(item.value.post.embed)
if (qp && qp.author.did === did) {
/*
* TODO(phase4): drop toLex once `getEmbeddedPost` in
* `#/state/queries/util` is migrated to `#/lexicons` types.
*/
yield bsky.toLex(qp.author)
yield qp.author
}
}
}
+25
View File
@@ -80,6 +80,7 @@ const ApiContext = createContext<SessionApiContext>({
resumeSession: async () => {},
removeAccount: () => {},
partialRefreshSession: async () => {},
refreshSession: () => Promise.resolve(undefined),
})
ApiContext.displayName = 'SessionApiContext'
@@ -344,6 +345,28 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
})
}, [store, state, cancelPendingTask])
const refreshSession = useCallback<
SessionApiContext['refreshSession']
>(async () => {
const bundle = store.getState().currentAgentState.agent as unknown as
| SessionBundle
| PublicSessionBundle
if (!bundle.session) return undefined // logged out: nothing to refresh
/*
* PasswordSession.refresh() re-runs com.atproto.server.refreshSession +
* getSession. On success the session's onUpdated hook fires, which the
* armed makeSessionHooks wiring maps to an 'update' event; the reducer
* snapshots the refreshed account. No explicit dispatch is needed here.
* The returned snapshot lets callers read post-refresh fields without
* waiting on the (async) reducer update.
*/
await bundle.session.refresh()
return sessionDataToSessionAccount(
bundle.session.session,
bundle.session.session.service,
)
}, [store])
const removeAccount = useCallback<SessionApiContext['removeAccount']>(
account => {
addSessionDebugLog({
@@ -443,6 +466,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
resumeSession,
removeAccount,
partialRefreshSession,
refreshSession,
}),
[
createAccount,
@@ -452,6 +476,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
resumeSession,
removeAccount,
partialRefreshSession,
refreshSession,
],
)
+11
View File
@@ -51,4 +51,15 @@ export type SessionApiContext = {
* `persistSessionHandler`.
*/
partialRefreshSession: () => Promise<void>
/**
* Force a full session refresh (re-runs `com.atproto.server.refreshSession`
* plus `getSession`) and return the refreshed account snapshot, or
* `undefined` when logged out.
*
* The refresh routes through the session's own `refresh()`, whose success
* hook propagates the updated account into state; the returned snapshot lets
* callers read post-refresh fields synchronously without waiting on the
* (async) reducer update. Rejections propagate to the caller.
*/
refreshSession: () => Promise<SessionAccount | undefined>
}