From f1411df0a4f494da131d3c8dc1fea4dd8813422e Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 4 Aug 2026 02:35:51 +0300 Subject: [PATCH] flip the preferences and labeler reads to sdk types `UsePreferencesQueryResponse` now derives from the sdk's `BskyPreferences` rather than the legacy one, which removes the cast the previous slice put on the assembled response - the query already returns the sdk action's result, so the two now type structurally. `labeler.ts`'s three service reads move to the appview client and return `app.bsky.labeler.defs` views, because `interpretLabelValueDefinitions` takes the lexicon-typed view. `moderation-opts.tsx` reads `Client.appLabelers` instead of `AtpAgent.appLabelers`: the lex static is already branded, so the fallback labeler list satisfies `ModerationPrefsLabeler` without a cast. `MutedWords` follows the prefs types: `expiresAt` is a `DatetimeString` now, built with `toDatetimeString` rather than `Date.toISOString`, and `sanitizeMutedWordValue` comes from `@bsky.app/sdk/utils`. --- src/components/dialogs/MutedWords.tsx | 18 ++++--- .../ModerationInteractionSettings/index.tsx | 8 ++- src/state/preferences/label-defs.tsx | 8 ++- src/state/preferences/moderation-opts.tsx | 14 ++++-- src/state/queries/labeler.ts | 50 ++++++++++--------- src/state/queries/preferences/const.ts | 2 +- src/state/queries/preferences/index.ts | 39 +++++---------- src/state/queries/preferences/moderation.ts | 3 +- src/state/queries/preferences/types.ts | 2 +- 9 files changed, 69 insertions(+), 75 deletions(-) diff --git a/src/components/dialogs/MutedWords.tsx b/src/components/dialogs/MutedWords.tsx index a885f1bf5f..3263113a73 100644 --- a/src/components/dialogs/MutedWords.tsx +++ b/src/components/dialogs/MutedWords.tsx @@ -1,6 +1,7 @@ import {useCallback, useState} from 'react' import {View} from 'react-native' -import {type AppBskyActorDefs, sanitizeMutedWordValue} from '@atproto/api' +import {type DatetimeString, toDatetimeString} from '@atproto/syntax' +import {sanitizeMutedWordValue} from '@bsky.app/sdk/utils' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' @@ -35,6 +36,7 @@ import * as Menu from '#/components/Menu' import * as Prompt from '#/components/Prompt' import {Text} from '#/components/Typography' import {IS_NATIVE} from '#/env' +import {type app} from '#/lexicons' const ONE_DAY = 24 * 60 * 60 * 1000 @@ -68,20 +70,20 @@ function MutedWordsInner() { const sanitizedValue = sanitizeMutedWordValue(field) const surfaces = ['tag', targets.includes('content') && 'content'].filter( Boolean, - ) as AppBskyActorDefs.MutedWord['targets'] + ) as app.bsky.actor.defs.MutedWord['targets'] const actorTarget = excludeFollowing ? 'exclude-following' : 'all' const now = Date.now() const rawDuration = durations.at(0) // undefined evaluates to 'forever' - let duration: string | undefined + let duration: DatetimeString | undefined if (rawDuration === '24_hours') { - duration = new Date(now + ONE_DAY).toISOString() + duration = toDatetimeString(new Date(now + ONE_DAY)) } else if (rawDuration === '7_days') { - duration = new Date(now + 7 * ONE_DAY).toISOString() + duration = toDatetimeString(new Date(now + 7 * ONE_DAY)) } else if (rawDuration === '30_days') { - duration = new Date(now + 30 * ONE_DAY).toISOString() + duration = toDatetimeString(new Date(now + 30 * ONE_DAY)) } if (!sanitizedValue || !surfaces.length) { @@ -421,7 +423,7 @@ function MutedWordsInner() { function MutedWordRow({ style, word, -}: ViewStyleProp & {word: AppBskyActorDefs.MutedWord}) { +}: ViewStyleProp & {word: app.bsky.actor.defs.MutedWord}) { const t = useTheme() const {_} = useLingui() const {isPending, mutateAsync: removeMutedWord} = useRemoveMutedWordMutation() @@ -440,7 +442,7 @@ function MutedWordRow({ updateMutedWord({ ...word, expiresAt: days - ? new Date(Date.now() + days * ONE_DAY).toISOString() + ? toDatetimeString(new Date(Date.now() + days * ONE_DAY)) : undefined, }) } diff --git a/src/screens/ModerationInteractionSettings/index.tsx b/src/screens/ModerationInteractionSettings/index.tsx index 12d952c872..9ce833a8e5 100644 --- a/src/screens/ModerationInteractionSettings/index.tsx +++ b/src/screens/ModerationInteractionSettings/index.tsx @@ -23,7 +23,6 @@ import {PostInteractionSettingsForm} from '#/components/dialogs/PostInteractionS import * as Layout from '#/components/Layout' import {Loader} from '#/components/Loader' import * as Toast from '#/components/Toast' -import {type app} from '#/lexicons' export function Screen() { const gutters = useGutters(['base']) @@ -76,15 +75,14 @@ function Inner({preferences}: {preferences: UsePreferencesQueryResponse}) { * Preferences are still typed against the legacy client, so the stored * rules arrive unbranded. Wave B migrates `getPreferences`. */ - allow: preferences.postInteractionSettings - .threadgateAllowRules as app.bsky.feed.threadgate.Main['allow'], + allow: preferences.postInteractionSettings.threadgateAllowRules, }) }, [preferences.postInteractionSettings.threadgateAllowRules]) const postgate = useMemo(() => { return createPostgateRecord({ post: '', - embeddingRules: preferences.postInteractionSettings - .postgateEmbeddingRules as app.bsky.feed.postgate.Main['embeddingRules'], + embeddingRules: + preferences.postInteractionSettings.postgateEmbeddingRules, }) }, [preferences.postInteractionSettings.postgateEmbeddingRules]) diff --git a/src/state/preferences/label-defs.tsx b/src/state/preferences/label-defs.tsx index dd1859ecb6..8000bd231c 100644 --- a/src/state/preferences/label-defs.tsx +++ b/src/state/preferences/label-defs.tsx @@ -1,14 +1,12 @@ import {createContext, useContext} from 'react' -import { - type AppBskyLabelerDefs, - type InterpretedLabelValueDefinition, -} from '@atproto/api' +import {type InterpretedLabelValueDefinition} from '@bsky.app/sdk/moderation' +import {type app} from '#/lexicons' import {useLabelDefinitionsQuery} from '../queries/preferences' interface StateContext { labelDefs: Record - labelers: AppBskyLabelerDefs.LabelerViewDetailed[] + labelers: app.bsky.labeler.defs.LabelerViewDetailed[] } const stateContext = createContext({ diff --git a/src/state/preferences/moderation-opts.tsx b/src/state/preferences/moderation-opts.tsx index 119c9008db..77ef89f283 100644 --- a/src/state/preferences/moderation-opts.tsx +++ b/src/state/preferences/moderation-opts.tsx @@ -1,5 +1,6 @@ import {createContext, useContext, useMemo} from 'react' -import {AtpAgent, type ModerationOpts} from '@atproto/api' +import {Client} from '@atproto/lex' +import {type ModerationOpts} from '@bsky.app/sdk/moderation' import {useHiddenPosts, useLabelDefinitions} from '#/state/preferences' import {DEFAULT_LOGGED_OUT_LABEL_PREFERENCES} from '#/state/queries/preferences/const' @@ -38,16 +39,21 @@ export function Provider({children}: React.PropsWithChildren<{}>) { return undefined } return { - userDid, + /* + * `did`/`hiddenPosts` come from persisted storage typed as plain + * `string`, so brand them to the SDK's `DidString`/`AtUriString` slots. + */ + userDid: userDid as ModerationOpts['userDid'], prefs: { ...moderationPrefs, labelers: moderationPrefs.labelers.length ? moderationPrefs.labelers - : AtpAgent.appLabelers.map(did => ({ + : Client.appLabelers.map(did => ({ did, labels: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES, })), - hiddenPosts: hiddenPosts || [], + hiddenPosts: (hiddenPosts || + []) as ModerationOpts['prefs']['hiddenPosts'], }, labelDefs, } diff --git a/src/state/queries/labeler.ts b/src/state/queries/labeler.ts index b132b73ebe..3fd14cc5fc 100644 --- a/src/state/queries/labeler.ts +++ b/src/state/queries/labeler.ts @@ -1,4 +1,3 @@ -import {type AppBskyLabelerDefs} from '@atproto/api' import {type DidString} from '@atproto/syntax' import {addLabeler, removeLabeler} from '@bsky.app/sdk' import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' @@ -11,7 +10,8 @@ import { usePreferencesQuery, } from '#/state/queries/preferences' import {createQueryKey} from '#/state/queries/util' -import {useAgent, usePdsClient} from '#/state/session' +import {useAppviewClient, usePdsClient} from '#/state/session' +import {app} from '#/lexicons' const labelerInfoQueryKeyRoot = 'labeler-info' export const labelerInfoQueryKey = (did: string) => [ @@ -35,45 +35,47 @@ export function useLabelerInfoQuery({ did?: string enabled?: boolean }) { - const agent = useAgent() + const client = useAppviewClient() return useQuery({ enabled: !!did && enabled !== false, queryKey: labelerInfoQueryKey(did as string), queryFn: async () => { - const res = await agent.app.bsky.labeler.getServices({ - dids: [did!], + const res = await client.call(app.bsky.labeler.getServices, { + dids: [did! as DidString], detailed: true, }) - return res.data.views[0] as AppBskyLabelerDefs.LabelerViewDetailed + return res.views[0] as app.bsky.labeler.defs.LabelerViewDetailed }, }) } export function useLabelersInfoQuery({dids}: {dids: string[]}) { - const agent = useAgent() + const client = useAppviewClient() return useQuery({ enabled: !!dids.length, queryKey: labelersInfoQueryKey(dids), queryFn: async () => { - const res = await agent.app.bsky.labeler.getServices({dids}) - return res.data.views as AppBskyLabelerDefs.LabelerView[] + const res = await client.call(app.bsky.labeler.getServices, { + dids: dids as DidString[], + }) + return res.views as app.bsky.labeler.defs.LabelerView[] }, }) } export function useLabelersDetailedInfoQuery({dids}: {dids: string[]}) { - const agent = useAgent() + const client = useAppviewClient() return useQuery({ enabled: !!dids.length, queryKey: createLabelersDetailedInfoQueryKey(dids), gcTime: GCTIME.INFINITY, staleTime: STALE.MINUTES.ONE, queryFn: async () => { - const res = await agent.app.bsky.labeler.getServices({ - dids, + const res = await client.call(app.bsky.labeler.getServices, { + dids: dids as DidString[], detailed: true, }) - return res.data.views as AppBskyLabelerDefs.LabelerViewDetailed[] + return res.views as app.bsky.labeler.defs.LabelerViewDetailed[] }, }) } @@ -98,7 +100,7 @@ export function useRemoveLabelersMutation() { export function useLabelerSubscriptionMutation() { const queryClient = useQueryClient() - const agent = useAgent() + const appviewClient = useAppviewClient() const pdsClient = usePdsClient() const preferences = usePreferencesQuery() @@ -122,28 +124,30 @@ export function useLabelerSubscriptionMutation() { const labelerDids = ( preferences.data?.moderationPrefs?.labelers ?? [] ).map(l => l.did) - const invalidLabelers: string[] = [] + const invalidLabelers: DidString[] = [] if (labelerDids.length) { - const profiles = await agent.getProfiles({actors: labelerDids}) - if (profiles.data) { - for (const did of labelerDids) { - const exists = profiles.data.profiles.find(p => p.did === did) + const profiles = await appviewClient.call(app.bsky.actor.getProfiles, { + actors: labelerDids, + }) + if (profiles) { + for (const labelerDid of labelerDids) { + const exists = profiles.profiles.find(p => p.did === labelerDid) if (exists) { // profile came back but it's not a valid labeler if (exists.associated && !exists.associated.labeler) { - invalidLabelers.push(did) + invalidLabelers.push(labelerDid) } } else { // no response came back, might be deactivated or takendown - invalidLabelers.push(did) + invalidLabelers.push(labelerDid) } } } } if (invalidLabelers.length) { await Promise.all( - invalidLabelers.map(did => - pdsClient.call(removeLabeler, did as DidString), + invalidLabelers.map(labelerDid => + pdsClient.call(removeLabeler, labelerDid), ), ) } diff --git a/src/state/queries/preferences/const.ts b/src/state/queries/preferences/const.ts index 74c86d1020..81b50fffd0 100644 --- a/src/state/queries/preferences/const.ts +++ b/src/state/queries/preferences/const.ts @@ -1,4 +1,4 @@ -import {DEFAULT_LABEL_SETTINGS} from '@atproto/api' +import {DEFAULT_LABEL_SETTINGS} from '@bsky.app/sdk/moderation' import { type ThreadViewPreferences, diff --git a/src/state/queries/preferences/index.ts b/src/state/queries/preferences/index.ts index 6358de738c..74edee568f 100644 --- a/src/state/queries/preferences/index.ts +++ b/src/state/queries/preferences/index.ts @@ -89,13 +89,10 @@ export function usePreferencesQuery() { agent.configureLabelers(labelerDids) /* - * The sdk's `BskyPreferences` is field-for-field identical to the - * legacy one that `UsePreferencesQueryResponse` is still derived from; - * only its strings are branded (`AtUriString`, `DidString`). Cast at - * this one seam so every downstream consumer of the response - notably - * the `moderationPrefs` readers - keeps its current types. + * `BskyPreferences` is now the sdk's own type, so the assembled + * response types structurally with no cast at this seam. */ - const preferences = { + const preferences: UsePreferencesQueryResponse = { ...res, savedFeeds: res.savedFeeds.filter(f => f.type !== 'unknown'), /** @@ -111,7 +108,7 @@ export function usePreferencesQuery() { ...(res.threadViewPrefs ?? {}), }, userAge: res.birthDate ? getAge(res.birthDate) : undefined, - } as UsePreferencesQueryResponse + } return preferences } }, @@ -381,11 +378,8 @@ export function useUpsertMutedWordsMutation() { const client = usePdsClient() return useMutation({ - mutationFn: async (mutedWords: AppBskyActorDefs.MutedWord[]) => { - await client.call( - upsertMutedWords, - mutedWords as app.bsky.actor.defs.MutedWord[], - ) + mutationFn: async (mutedWords: app.bsky.actor.defs.MutedWord[]) => { + await client.call(upsertMutedWords, mutedWords) // triggers a refetch await queryClient.invalidateQueries({ queryKey: preferencesQueryKey, @@ -399,11 +393,8 @@ export function useUpdateMutedWordMutation() { const client = usePdsClient() return useMutation({ - mutationFn: async (mutedWord: AppBskyActorDefs.MutedWord) => { - await client.call( - updateMutedWord, - mutedWord as app.bsky.actor.defs.MutedWord, - ) + mutationFn: async (mutedWord: app.bsky.actor.defs.MutedWord) => { + await client.call(updateMutedWord, mutedWord) // triggers a refetch await queryClient.invalidateQueries({ queryKey: preferencesQueryKey, @@ -417,11 +408,8 @@ export function useRemoveMutedWordMutation() { const client = usePdsClient() return useMutation({ - mutationFn: async (mutedWord: AppBskyActorDefs.MutedWord) => { - await client.call( - removeMutedWord, - mutedWord as app.bsky.actor.defs.MutedWord, - ) + mutationFn: async (mutedWord: app.bsky.actor.defs.MutedWord) => { + await client.call(removeMutedWord, mutedWord) // triggers a refetch await queryClient.invalidateQueries({ queryKey: preferencesQueryKey, @@ -435,11 +423,8 @@ export function useRemoveMutedWordsMutation() { const client = usePdsClient() return useMutation({ - mutationFn: async (mutedWords: AppBskyActorDefs.MutedWord[]) => { - await client.call( - removeMutedWords, - mutedWords as app.bsky.actor.defs.MutedWord[], - ) + mutationFn: async (mutedWords: app.bsky.actor.defs.MutedWord[]) => { + await client.call(removeMutedWords, mutedWords) // triggers a refetch await queryClient.invalidateQueries({ queryKey: preferencesQueryKey, diff --git a/src/state/queries/preferences/moderation.ts b/src/state/queries/preferences/moderation.ts index 55c155dc5b..246e7596b5 100644 --- a/src/state/queries/preferences/moderation.ts +++ b/src/state/queries/preferences/moderation.ts @@ -1,5 +1,6 @@ import {useMemo} from 'react' -import {AtpAgent, interpretLabelValueDefinitions} from '@atproto/api' +import {AtpAgent} from '@atproto/api' +import {interpretLabelValueDefinitions} from '@bsky.app/sdk/moderation' import {isNonConfigurableModerationAuthority} from '#/state/session/additional-moderation-authorities' import {useLabelersDetailedInfoQuery} from '../labeler' diff --git a/src/state/queries/preferences/types.ts b/src/state/queries/preferences/types.ts index ab0a957522..f93ca955a4 100644 --- a/src/state/queries/preferences/types.ts +++ b/src/state/queries/preferences/types.ts @@ -1,4 +1,4 @@ -import {type BskyFeedViewPreference, type BskyPreferences} from '@atproto/api' +import {type BskyFeedViewPreference, type BskyPreferences} from '@bsky.app/sdk' export type UsePreferencesQueryResponse = Omit< BskyPreferences,