From 0c8cdba703fb3aeea3160c75deb7a0c3f5c4ffbb Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Mon, 3 Aug 2026 23:11:47 +0300 Subject: [PATCH] migrate the threadgate and postgate records to the pds client Co-Authored-By: Claude Fable 5 --- oxlint-suppressions.json | 16 +--- .../dialogs/PostInteractionSettingsDialog.tsx | 22 ++--- src/lib/api/index.ts | 3 +- .../ModerationInteractionSettings/index.tsx | 17 +++- src/state/queries/postgate/index.ts | 83 +++++++++-------- src/state/queries/postgate/util.ts | 34 +++++-- src/state/queries/threadgate/index.ts | 91 +++++++++---------- src/state/queries/threadgate/util.ts | 67 +++++++++----- src/view/com/composer/state/composer.ts | 38 +++++--- .../com/composer/threadgate/ThreadgateBtn.tsx | 23 +++-- 10 files changed, 214 insertions(+), 180 deletions(-) diff --git a/oxlint-suppressions.json b/oxlint-suppressions.json index 3b2304e8fe..5ffe12abbc 100644 --- a/oxlint-suppressions.json +++ b/oxlint-suppressions.json @@ -1400,16 +1400,13 @@ }, "src/state/queries/postgate/index.ts": { "typescript/no-explicit-any": { - "count": 2 + "count": 1 }, "typescript/no-floating-promises": { "count": 1 }, - "typescript/no-unsafe-call": { - "count": 2 - }, "typescript/no-unsafe-member-access": { - "count": 3 + "count": 1 }, "typescript/require-await": { "count": 2 @@ -1426,18 +1423,9 @@ } }, "src/state/queries/threadgate/index.ts": { - "typescript/no-explicit-any": { - "count": 1 - }, "typescript/no-floating-promises": { "count": 3 }, - "typescript/no-unsafe-call": { - "count": 2 - }, - "typescript/no-unsafe-member-access": { - "count": 2 - }, "typescript/require-await": { "count": 3 } diff --git a/src/components/dialogs/PostInteractionSettingsDialog.tsx b/src/components/dialogs/PostInteractionSettingsDialog.tsx index 8a5c7101cc..63684f4831 100644 --- a/src/components/dialogs/PostInteractionSettingsDialog.tsx +++ b/src/components/dialogs/PostInteractionSettingsDialog.tsx @@ -1,10 +1,7 @@ import {useCallback, useMemo, useState} from 'react' import {LayoutAnimation, Text as NestedText, View} from 'react-native' -import { - type AppBskyFeedDefs, - type AppBskyFeedPostgate, - AtUri, -} from '@atproto/api' +import {type AppBskyFeedDefs} from '@atproto/api' +import {AtUri} from '@atproto/syntax' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Plural, Trans} from '@lingui/react/macro' @@ -36,7 +33,7 @@ import { PostThreadContextProvider, usePostThreadContext, } from '#/state/queries/usePostThread' -import {useAgent, useSession} from '#/state/session' +import {usePdsClient, useSession} from '#/state/session' import {UserAvatar} from '#/view/com/util/UserAvatar' import {atoms as a, useTheme, web} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' @@ -53,6 +50,7 @@ import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' import {IS_IOS} from '#/env' +import {type app} from '#/lexicons' export type PostInteractionSettingsFormProps = { canSave?: boolean @@ -63,8 +61,8 @@ export type PostInteractionSettingsFormProps = { persist?: boolean onChangePersist?: (v: boolean) => void - postgate: AppBskyFeedPostgate.Record - onChangePostgate: (v: AppBskyFeedPostgate.Record) => void + postgate: app.bsky.feed.postgate.Main + onChangePostgate: (v: app.bsky.feed.postgate.Main) => void threadgateAllowUISettings: ThreadgateAllowUISetting[] onChangeThreadgateAllowUISettings: (v: ThreadgateAllowUISetting[]) => void @@ -178,7 +176,7 @@ export function PostInteractionSettingsDialogControlledInner( const {mutateAsync: setThreadgateAllow} = useSetThreadgateAllowMutation() const [editedPostgate, setEditedPostgate] = - useState() + useState() const [editedAllowUISettings, setEditedAllowUISettings] = useState() @@ -697,7 +695,7 @@ export function usePrefetchPostInteractionSettings({ }) { const ax = useAnalytics() const queryClient = useQueryClient() - const agent = useAgent() + const pdsClient = usePdsClient() const getPost = useGetPost() return useCallback(async () => { @@ -706,7 +704,7 @@ export function usePrefetchPostInteractionSettings({ queryClient.prefetchQuery({ queryKey: createPostgateQueryKey(postUri), queryFn: () => - getPostgateRecord({agent, postUri}).then(res => res ?? null), + getPostgateRecord({pdsClient, postUri}).then(res => res ?? null), staleTime: STALE.SECONDS.THIRTY, }), queryClient.prefetchQuery({ @@ -723,5 +721,5 @@ export function usePrefetchPostInteractionSettings({ safeMessage: e.message, }) } - }, [ax, queryClient, agent, postUri, rootPostUri, getPost]) + }, [ax, queryClient, pdsClient, postUri, rootPostUri, getPost]) } diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index 9f81abb77a..aee481669e 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -16,6 +16,7 @@ import { } from '@atproto/api' import {TID} from '@atproto/common-web' import {type Client} from '@atproto/lex' +import {toDatetimeString} from '@atproto/syntax' import {RichText} from '@bsky.app/sdk/richtext' import {t} from '@lingui/core/macro' import {type QueryClient} from '@tanstack/react-query' @@ -145,7 +146,7 @@ export async function post( collection: 'app.bsky.feed.threadgate', rkey: rkey, value: createThreadgateRecord({ - createdAt: now.toISOString(), + createdAt: toDatetimeString(now), post: uri, allow: threadgateAllowUISettingToAllowRecordValue(thread.threadgate), }), diff --git a/src/screens/ModerationInteractionSettings/index.tsx b/src/screens/ModerationInteractionSettings/index.tsx index 261e6b55ae..12d952c872 100644 --- a/src/screens/ModerationInteractionSettings/index.tsx +++ b/src/screens/ModerationInteractionSettings/index.tsx @@ -1,5 +1,6 @@ import {useCallback, useMemo, useState} from 'react' import {View} from 'react-native' +import {type AtUriString, toDatetimeString} from '@atproto/syntax' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' @@ -22,6 +23,7 @@ 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']) @@ -68,16 +70,21 @@ function Inner({preferences}: {preferences: UsePreferencesQueryResponse}) { const allowUI = useMemo(() => { return threadgateRecordToAllowUISetting({ $type: 'app.bsky.feed.threadgate', - post: '', - createdAt: new Date().toString(), - allow: preferences.postInteractionSettings.threadgateAllowRules, + post: '' as AtUriString, + createdAt: toDatetimeString(new Date()), + /* + * 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'], }) }, [preferences.postInteractionSettings.threadgateAllowRules]) const postgate = useMemo(() => { return createPostgateRecord({ post: '', - embeddingRules: - preferences.postInteractionSettings.postgateEmbeddingRules, + embeddingRules: preferences.postInteractionSettings + .postgateEmbeddingRules as app.bsky.feed.postgate.Main['embeddingRules'], }) }, [preferences.postInteractionSettings.postgateEmbeddingRules]) diff --git a/src/state/queries/postgate/index.ts b/src/state/queries/postgate/index.ts index 82a52730bc..e213fa73d5 100644 --- a/src/state/queries/postgate/index.ts +++ b/src/state/queries/postgate/index.ts @@ -3,13 +3,13 @@ import { AppBskyEmbedRecord, AppBskyEmbedRecordWithMedia, type AppBskyFeedDefs, - AppBskyFeedPostgate, - type AtpAgent, - AtUri, } from '@atproto/api' +import {type Client} from '@atproto/lex' +import {AtUri, type HandleString} from '@atproto/syntax' import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' import {networkRetry, retry} from '#/lib/async/retry' +import {isRecordNotFoundError} from '#/lib/xrpc-error' import {logger} from '#/logger' import {updatePostShadow} from '#/state/cache/post-shadow' import {STALE} from '#/state/queries' @@ -20,28 +20,28 @@ import { mergePostgateRecords, POSTGATE_COLLECTION, } from '#/state/queries/postgate/util' -import {useAgent} from '#/state/session' +import {usePdsClient} from '#/state/session' +import {app, com} from '#/lexicons' import * as bsky from '#/types/bsky' export async function getPostgateRecord({ - agent, + pdsClient, postUri, }: { - agent: AtpAgent + pdsClient: Client postUri: string -}): Promise { +}): Promise { const urip = new AtUri(postUri) if (!urip.host.startsWith('did:')) { - const res = await agent.resolveHandle({ - handle: urip.host, + const {did} = await pdsClient.call(com.atproto.identity.resolveHandle, { + handle: urip.host as HandleString, }) - // @ts-expect-error TODO new-sdk-migration - urip.host = res.data.did + urip.host = did } try { - const {data} = await retry( + const data = await retry( 2, e => { /* @@ -49,34 +49,31 @@ export async function getPostgateRecord({ * throwing an error. NB: This will also catch reference errors, such as * a typo in the URI. */ - if (e.message.includes(`Could not locate record:`)) { + if (isRecordNotFoundError(e)) { return false } return true }, () => - agent.api.com.atproto.repo.getRecord({ + pdsClient.call(com.atproto.repo.getRecord, { repo: urip.host, collection: POSTGATE_COLLECTION, - rkey: urip.rkey, + rkey: urip.rkeySafe, }), ) - if ( - data.value && - bsky.validate(data.value, AppBskyFeedPostgate.validateRecord) - ) { + if (data.value && bsky.matches(app.bsky.feed.postgate, data.value)) { return data.value } else { return undefined } - } catch (e: any) { + } catch (e) { /* * If the record doesn't exist, we want to return null instead of * throwing an error. NB: This will also catch reference errors, such as * a typo in the URI. */ - if (e.message.includes(`Could not locate record:`)) { + if (isRecordNotFoundError(e)) { return undefined } else { throw e @@ -85,21 +82,21 @@ export async function getPostgateRecord({ } export async function writePostgateRecord({ - agent, + pdsClient, postUri, postgate, }: { - agent: AtpAgent + pdsClient: Client postUri: string - postgate: AppBskyFeedPostgate.Record + postgate: app.bsky.feed.postgate.Main }) { const postUrip = new AtUri(postUri) await networkRetry(2, () => - agent.api.com.atproto.repo.putRecord({ - repo: agent.session!.did, + pdsClient.call(com.atproto.repo.putRecord, { + repo: pdsClient.assertDid, collection: POSTGATE_COLLECTION, - rkey: postUrip.rkey, + rkey: postUrip.rkeySafe, record: postgate, }), ) @@ -107,24 +104,24 @@ export async function writePostgateRecord({ export async function upsertPostgate( { - agent, + pdsClient, postUri, }: { - agent: AtpAgent + pdsClient: Client postUri: string }, callback: ( - postgate: AppBskyFeedPostgate.Record | undefined, - ) => Promise, + postgate: app.bsky.feed.postgate.Main | undefined, + ) => Promise, ) { const prev = await getPostgateRecord({ - agent, + pdsClient, postUri, }) const next = await callback(prev) if (!next) return await writePostgateRecord({ - agent, + pdsClient, postUri, postgate: next, }) @@ -135,18 +132,20 @@ export const createPostgateQueryKey = (postUri: string) => [ postUri, ] export function usePostgateQuery({postUri}: {postUri: string}) { - const agent = useAgent() + const pdsClient = usePdsClient() return useQuery({ staleTime: STALE.SECONDS.THIRTY, queryKey: createPostgateQueryKey(postUri), async queryFn() { - return await getPostgateRecord({agent, postUri}).then(res => res ?? null) + return await getPostgateRecord({pdsClient, postUri}).then( + res => res ?? null, + ) }, }) } export function useWritePostgateMutation() { - const agent = useAgent() + const pdsClient = usePdsClient() const queryClient = useQueryClient() return useMutation({ mutationFn: async ({ @@ -154,10 +153,10 @@ export function useWritePostgateMutation() { postgate, }: { postUri: string - postgate: AppBskyFeedPostgate.Record + postgate: app.bsky.feed.postgate.Main }) => { return writePostgateRecord({ - agent, + pdsClient, postUri, postgate, }) @@ -171,7 +170,7 @@ export function useWritePostgateMutation() { } export function useToggleQuoteDetachmentMutation() { - const agent = useAgent() + const pdsClient = usePdsClient() const queryClient = useQueryClient() const getPosts = useGetPosts() const prevEmbed = useRef(undefined) @@ -200,7 +199,7 @@ export function useToggleQuoteDetachmentMutation() { }) } - await upsertPostgate({agent, postUri: quoteUri}, async prev => { + await upsertPostgate({pdsClient, postUri: quoteUri}, async prev => { if (prev) { if (action === 'detach') { return mergePostgateRecords(prev, { @@ -264,7 +263,7 @@ export function useToggleQuoteDetachmentMutation() { } export function useToggleQuotepostEnabledMutation() { - const agent = useAgent() + const pdsClient = usePdsClient() return useMutation({ mutationFn: async ({ @@ -274,7 +273,7 @@ export function useToggleQuotepostEnabledMutation() { postUri: string action: 'enable' | 'disable' }) => { - await upsertPostgate({agent, postUri: postUri}, async prev => { + await upsertPostgate({pdsClient, postUri: postUri}, async prev => { if (prev) { if (action === 'disable') { return mergePostgateRecords(prev, { diff --git a/src/state/queries/postgate/util.ts b/src/state/queries/postgate/util.ts index 0952a1ad09..51c84419d0 100644 --- a/src/state/queries/postgate/util.ts +++ b/src/state/queries/postgate/util.ts @@ -3,29 +3,43 @@ import { AppBskyEmbedRecord, AppBskyEmbedRecordWithMedia, type AppBskyFeedDefs, - type AppBskyFeedPostgate, AtUri, } from '@atproto/api' +import {type AtUriString, toDatetimeString} from '@atproto/syntax' + +import {type app} from '#/lexicons' export const POSTGATE_COLLECTION = 'app.bsky.feed.postgate' +/** + * Create a new {@link app.bsky.feed.postgate.Main}. URIs are accepted as plain + * strings (callers hold raw AT-URIs, often from legacy-typed views) and + * asserted to the branded `AtUriString` here. + */ export function createPostgateRecord( - postgate: Partial & { - post: AppBskyFeedPostgate.Record['post'] + postgate: Omit< + Partial, + 'post' | 'detachedEmbeddingUris' + > & { + post: string + detachedEmbeddingUris?: string[] }, -): AppBskyFeedPostgate.Record { +): app.bsky.feed.postgate.Main { return { $type: POSTGATE_COLLECTION, - createdAt: new Date().toISOString(), - post: postgate.post, - detachedEmbeddingUris: postgate.detachedEmbeddingUris || [], + createdAt: toDatetimeString(new Date()), + post: postgate.post as AtUriString, + detachedEmbeddingUris: (postgate.detachedEmbeddingUris || + []) as AtUriString[], embeddingRules: postgate.embeddingRules || [], } } export function mergePostgateRecords( - prev: AppBskyFeedPostgate.Record, - next: Partial, + prev: app.bsky.feed.postgate.Main, + next: Omit, 'detachedEmbeddingUris'> & { + detachedEmbeddingUris?: string[] + }, ) { const detachedEmbeddingUris = Array.from( new Set([ @@ -199,5 +213,5 @@ export function getMaybeDetachedQuoteEmbed({ } export const embeddingRules = { - disableRule: {$type: 'app.bsky.feed.postgate#disableRule'}, + disableRule: {$type: 'app.bsky.feed.postgate#disableRule'} as const, } diff --git a/src/state/queries/threadgate/index.ts b/src/state/queries/threadgate/index.ts index 561275ca66..bd1a0f6b8c 100644 --- a/src/state/queries/threadgate/index.ts +++ b/src/state/queries/threadgate/index.ts @@ -1,12 +1,10 @@ -import { - type AppBskyFeedDefs, - AppBskyFeedThreadgate, - type AtpAgent, - AtUri, -} from '@atproto/api' +import {type AppBskyFeedDefs} from '@atproto/api' +import {type Client} from '@atproto/lex' +import {AtUri, type HandleString} from '@atproto/syntax' import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' import {networkRetry, retry} from '#/lib/async/retry' +import {isRecordNotFoundError} from '#/lib/xrpc-error' import {STALE} from '#/state/queries' import {useGetPost} from '#/state/queries/post' import {type ThreadgateAllowUISetting} from '#/state/queries/threadgate/types' @@ -17,8 +15,9 @@ import { threadgateViewToAllowUISetting, } from '#/state/queries/threadgate/util' import {useUpdatePostThreadThreadgateQueryCache} from '#/state/queries/usePostThread' -import {useAgent} from '#/state/session' +import {usePdsClient} from '#/state/session' import {useThreadgateHiddenReplyUrisAPI} from '#/state/threadgate-hidden-replies' +import {app, com} from '#/lexicons' import * as bsky from '#/types/bsky' export * from '#/state/queries/threadgate/types' @@ -40,9 +39,9 @@ export function useThreadgateRecordQuery({ initialData, }: { postUri?: string - initialData?: AppBskyFeedThreadgate.Record + initialData?: app.bsky.feed.threadgate.Main } = {}) { - const agent = useAgent() + const pdsClient = usePdsClient() return useQuery({ enabled: !!postUri, @@ -51,7 +50,7 @@ export function useThreadgateRecordQuery({ staleTime: STALE.MINUTES.ONE, async queryFn() { return getThreadgateRecord({ - agent, + pdsClient, postUri: postUri!, }) }, @@ -85,24 +84,23 @@ export function useThreadgateViewQuery({ } export async function getThreadgateRecord({ - agent, + pdsClient, postUri, }: { - agent: AtpAgent + pdsClient: Client postUri: string -}): Promise { +}): Promise { const urip = new AtUri(postUri) if (!urip.host.startsWith('did:')) { - const res = await agent.resolveHandle({ - handle: urip.host, + const {did} = await pdsClient.call(com.atproto.identity.resolveHandle, { + handle: urip.host as HandleString, }) - // @ts-expect-error TODO new-sdk-migration - urip.host = res.data.did + urip.host = did } try { - const {data} = await retry( + const data = await retry( 2, e => { /* @@ -110,34 +108,31 @@ export async function getThreadgateRecord({ * throwing an error. NB: This will also catch reference errors, such as * a typo in the URI. */ - if (e.message.includes(`Could not locate record:`)) { + if (isRecordNotFoundError(e)) { return false } return true }, () => - agent.api.com.atproto.repo.getRecord({ + pdsClient.call(com.atproto.repo.getRecord, { repo: urip.host, collection: 'app.bsky.feed.threadgate', - rkey: urip.rkey, + rkey: urip.rkeySafe, }), ) - if ( - data.value && - bsky.validate(data.value, AppBskyFeedThreadgate.validateRecord) - ) { + if (data.value && bsky.matches(app.bsky.feed.threadgate, data.value)) { return data.value } else { return null } - } catch (e: any) { + } catch (e) { /* * If the record doesn't exist, we want to return null instead of * throwing an error. NB: This will also catch reference errors, such as * a typo in the URI. */ - if (e.message.includes(`Could not locate record:`)) { + if (isRecordNotFoundError(e)) { return null } else { throw e @@ -146,13 +141,13 @@ export async function getThreadgateRecord({ } export async function writeThreadgateRecord({ - agent, + pdsClient, postUri, threadgate, }: { - agent: AtpAgent + pdsClient: Client postUri: string - threadgate: AppBskyFeedThreadgate.Record + threadgate: app.bsky.feed.threadgate.Main }) { const postUrip = new AtUri(postUri) const record = createThreadgateRecord({ @@ -162,10 +157,10 @@ export async function writeThreadgateRecord({ }) await networkRetry(2, () => - agent.api.com.atproto.repo.putRecord({ - repo: agent.session!.did, + pdsClient.call(com.atproto.repo.putRecord, { + repo: pdsClient.assertDid, collection: 'app.bsky.feed.threadgate', - rkey: postUrip.rkey, + rkey: postUrip.rkeySafe, record, }), ) @@ -173,25 +168,25 @@ export async function writeThreadgateRecord({ export async function upsertThreadgate( { - agent, + pdsClient, postUri, }: { - agent: AtpAgent + pdsClient: Client postUri: string }, callback: ( - threadgate: AppBskyFeedThreadgate.Record | null, - ) => Promise, + threadgate: app.bsky.feed.threadgate.Main | null, + ) => Promise, ) { const prev = await getThreadgateRecord({ - agent, + pdsClient, postUri, }) const next = await callback(prev) if (!next) return validateThreadgateRecordOrThrow(next) await writeThreadgateRecord({ - agent, + pdsClient, postUri, threadgate: next, }) @@ -201,15 +196,15 @@ export async function upsertThreadgate( * Update the allow list for a threadgate record. */ export async function updateThreadgateAllow({ - agent, + pdsClient, postUri, allow, }: { - agent: AtpAgent + pdsClient: Client postUri: string allow: ThreadgateAllowUISetting[] }) { - return upsertThreadgate({agent, postUri}, async prev => { + return upsertThreadgate({pdsClient, postUri}, async prev => { if (prev) { return { ...prev, @@ -225,7 +220,7 @@ export async function updateThreadgateAllow({ } export function useSetThreadgateAllowMutation() { - const agent = useAgent() + const pdsClient = usePdsClient() const queryClient = useQueryClient() const getPost = useGetPost() const updatePostThreadThreadgate = useUpdatePostThreadThreadgateQueryCache() @@ -238,7 +233,7 @@ export function useSetThreadgateAllowMutation() { postUri: string allow: ThreadgateAllowUISetting[] }) => { - return upsertThreadgate({agent, postUri}, async prev => { + return upsertThreadgate({pdsClient, postUri}, async prev => { if (prev) { return { ...prev, @@ -290,7 +285,7 @@ export function useSetThreadgateAllowMutation() { } export function useToggleReplyVisibilityMutation() { - const agent = useAgent() + const pdsClient = usePdsClient() const queryClient = useQueryClient() const hiddenReplies = useThreadgateHiddenReplyUrisAPI() @@ -310,7 +305,7 @@ export function useToggleReplyVisibilityMutation() { hiddenReplies.removeHiddenReplyUri(replyUri) } - await upsertThreadgate({agent, postUri}, async prev => { + await upsertThreadgate({pdsClient, postUri}, async prev => { if (prev) { if (action === 'hide') { return mergeThreadgateRecords(prev, { @@ -363,9 +358,9 @@ export class InvalidInteractionSettingsError extends Error { } export function validateThreadgateRecordOrThrow( - record: AppBskyFeedThreadgate.Record, + record: app.bsky.feed.threadgate.Main, ) { - const result = AppBskyFeedThreadgate.validateRecord(record) + const result = bsky.safeParse(app.bsky.feed.threadgate, record) if (result.success) { if ((result.value.hiddenReplies?.length ?? 0) > MAX_HIDDEN_REPLIES) { diff --git a/src/state/queries/threadgate/util.ts b/src/state/queries/threadgate/util.ts index 807afef76f..275611df1c 100644 --- a/src/state/queries/threadgate/util.ts +++ b/src/state/queries/threadgate/util.ts @@ -1,26 +1,34 @@ -import {type AppBskyFeedDefs, AppBskyFeedThreadgate} from '@atproto/api' +import {type AppBskyFeedDefs} from '@atproto/api' +import {type AtUriString, toDatetimeString} from '@atproto/syntax' import {type ThreadgateAllowUISetting} from '#/state/queries/threadgate/types' +import {app} from '#/lexicons' import * as bsky from '#/types/bsky' +/* + * Threadgate VIEWS stay on the legacy client types: they are read-only inputs + * from the appview, and branding them here would ripple through every post + * component. Only the threadgate RECORD is migrated, because it is written + * through `com.atproto.repo.putRecord`, whose body is typed as a lex `LexMap`. + */ export function threadgateViewToAllowUISetting( threadgateView: AppBskyFeedDefs.ThreadgateView | undefined, ): ThreadgateAllowUISetting[] { // Validate the record for clarity, since backwards compat code is a little confusing const threadgate = threadgateView && - bsky.validate(threadgateView.record, AppBskyFeedThreadgate.validateRecord) + bsky.matches(app.bsky.feed.threadgate, threadgateView.record) ? threadgateView.record : undefined return threadgateRecordToAllowUISetting(threadgate) } /** - * Converts a full {@link AppBskyFeedThreadgate.Record} to a list of + * Converts a full {@link app.bsky.feed.threadgate.Main} to a list of * {@link ThreadgateAllowUISetting}, for use by app UI. */ export function threadgateRecordToAllowUISetting( - threadgate: AppBskyFeedThreadgate.Record | undefined, + threadgate: app.bsky.feed.threadgate.Main | undefined, ): ThreadgateAllowUISetting[] { /* * If `threadgate` doesn't exist (default), or if `threadgate.allow === undefined`, it means @@ -40,13 +48,13 @@ export function threadgateRecordToAllowUISetting( const settings: ThreadgateAllowUISetting[] = threadgate.allow .map(allow => { let setting: ThreadgateAllowUISetting | undefined - if (AppBskyFeedThreadgate.isMentionRule(allow)) { + if (bsky.isType(app.bsky.feed.threadgate.mentionRule, allow)) { setting = {type: 'mention'} - } else if (AppBskyFeedThreadgate.isFollowingRule(allow)) { + } else if (bsky.isType(app.bsky.feed.threadgate.followingRule, allow)) { setting = {type: 'following'} - } else if (AppBskyFeedThreadgate.isListRule(allow)) { + } else if (bsky.isType(app.bsky.feed.threadgate.listRule, allow)) { setting = {type: 'list', list: allow.list} - } else if (AppBskyFeedThreadgate.isFollowerRule(allow)) { + } else if (bsky.isType(app.bsky.feed.threadgate.followerRule, allow)) { setting = {type: 'followers'} } return setting @@ -57,7 +65,7 @@ export function threadgateRecordToAllowUISetting( /** * Converts an array of {@link ThreadgateAllowUISetting} to the `allow` prop on - * {@link AppBskyFeedThreadgate.Record}. + * {@link app.bsky.feed.threadgate.Main}. * * If the `allow` property on the record is undefined, we infer that to mean * that everyone can reply. If it's an empty array, we infer that to mean that @@ -65,12 +73,12 @@ export function threadgateRecordToAllowUISetting( */ export function threadgateAllowUISettingToAllowRecordValue( threadgate: ThreadgateAllowUISetting[], -): AppBskyFeedThreadgate.Record['allow'] { +): app.bsky.feed.threadgate.Main['allow'] { if (threadgate.find(v => v.type === 'everybody')) { return undefined } - let allow: Exclude = [] + let allow: Exclude = [] if (!threadgate.find(v => v.type === 'nobody')) { for (const rule of threadgate) { @@ -83,7 +91,7 @@ export function threadgateAllowUISettingToAllowRecordValue( } else if (rule.type === 'list') { allow.push({ $type: 'app.bsky.feed.threadgate#listRule', - list: rule.list, + list: rule.list as AtUriString, }) } } @@ -93,18 +101,20 @@ export function threadgateAllowUISettingToAllowRecordValue( } /** - * Merges two {@link AppBskyFeedThreadgate.Record} objects, combining their + * Merges two {@link app.bsky.feed.threadgate.Main} objects, combining their * `allow` and `hiddenReplies` arrays and de-deduplicating them. * * Note: `allow` can be undefined here, be sure you don't accidentally set it * to an empty array. See other comments in this file. */ export function mergeThreadgateRecords( - prev: AppBskyFeedThreadgate.Record, - next: Partial, -): AppBskyFeedThreadgate.Record { + prev: app.bsky.feed.threadgate.Main, + next: Omit, 'hiddenReplies'> & { + hiddenReplies?: string[] + }, +): app.bsky.feed.threadgate.Main { // can be undefined if everyone can reply! - const allow: AppBskyFeedThreadgate.Record['allow'] | undefined = + const allow: app.bsky.feed.threadgate.Main['allow'] | undefined = prev.allow || next.allow ? [...(prev.allow || []), ...(next.allow || [])].filter( (v, i, a) => a.findIndex(t => t.$type === v.$type) === i, @@ -112,7 +122,7 @@ export function mergeThreadgateRecords( : undefined const hiddenReplies = Array.from( new Set([...(prev.hiddenReplies || []), ...(next.hiddenReplies || [])]), - ) + ) as AtUriString[] return createThreadgateRecord({ post: prev.post, @@ -122,21 +132,28 @@ export function mergeThreadgateRecords( } /** - * Create a new {@link AppBskyFeedThreadgate.Record} object with the given - * properties. + * Create a new {@link app.bsky.feed.threadgate.Main} object with the given + * properties. `post` is accepted as a plain string (callers hold raw AT-URIs) + * and asserted to the branded `AtUriString` here. */ export function createThreadgateRecord( - threadgate: Partial, -): AppBskyFeedThreadgate.Record { + threadgate: Omit< + Partial, + 'post' | 'hiddenReplies' + > & { + post?: string + hiddenReplies?: string[] + }, +): app.bsky.feed.threadgate.Main { if (!threadgate.post) { throw new Error('Cannot create a threadgate record without a post URI') } return { $type: 'app.bsky.feed.threadgate', - post: threadgate.post, - createdAt: new Date().toISOString(), + post: threadgate.post as AtUriString, + createdAt: toDatetimeString(new Date()), allow: threadgate.allow, // can be undefined! - hiddenReplies: threadgate.hiddenReplies || [], + hiddenReplies: (threadgate.hiddenReplies || []) as AtUriString[], } } diff --git a/src/view/com/composer/state/composer.ts b/src/view/com/composer/state/composer.ts index 3d2aa45992..34e6b69508 100644 --- a/src/view/com/composer/state/composer.ts +++ b/src/view/com/composer/state/composer.ts @@ -1,9 +1,6 @@ import {type ImagePickerAsset} from 'expo-image-picker' -import { - type AppBskyActorDefs, - type AppBskyDraftDefs, - type AppBskyFeedPostgate, -} from '@atproto/api' +import {type AppBskyActorDefs, type AppBskyDraftDefs} from '@atproto/api' +import {type AtUriString, toDatetimeString} from '@atproto/syntax' import {RichText} from '@bsky.app/sdk/richtext' import {nanoid} from 'nanoid/non-secure' @@ -103,7 +100,7 @@ export type PostAction = export type ThreadDraft = { posts: PostDraft[] - postgate: AppBskyFeedPostgate.Record + postgate: app.bsky.feed.postgate.Main threadgate: ThreadgateAllowUISetting[] } @@ -122,7 +119,7 @@ export type ComposerState = { } export type ComposerAction = - | {type: 'update_postgate'; postgate: AppBskyFeedPostgate.Record} + | {type: 'update_postgate'; postgate: app.bsky.feed.postgate.Main} | {type: 'update_threadgate'; threadgate: ThreadgateAllowUISetting[]} | { type: 'update_post' @@ -319,13 +316,18 @@ export function composerReducer( posts, postgate: createPostgateRecord({ post: '', - embeddingRules: postgateEmbeddingRules, + /* + * Draft records are still typed against the legacy client, so the + * stored rules arrive unbranded. Wave B migrates the draft types. + */ + embeddingRules: + postgateEmbeddingRules as app.bsky.feed.postgate.Main['embeddingRules'], }), threadgate: threadgateRecordToAllowUISetting({ $type: 'app.bsky.feed.threadgate', - post: '', - createdAt: new Date().toString(), - allow: threadgateAllow, + post: '' as AtUriString, + createdAt: toDatetimeString(new Date()), + allow: threadgateAllow as app.bsky.feed.threadgate.Main['allow'], }), }, } @@ -744,13 +746,19 @@ export function createComposerState({ ], postgate: createPostgateRecord({ post: '', - embeddingRules: initInteractionSettings?.postgateEmbeddingRules || [], + /* + * Preferences are still typed against the legacy client, so the stored + * rules arrive unbranded. Wave B migrates `getPreferences`. + */ + embeddingRules: (initInteractionSettings?.postgateEmbeddingRules || + []) as app.bsky.feed.postgate.Main['embeddingRules'], }), threadgate: threadgateRecordToAllowUISetting({ $type: 'app.bsky.feed.threadgate', - post: '', - createdAt: new Date().toString(), - allow: initInteractionSettings?.threadgateAllowRules, + post: '' as AtUriString, + createdAt: toDatetimeString(new Date()), + allow: + initInteractionSettings?.threadgateAllowRules as app.bsky.feed.threadgate.Main['allow'], }), }, } diff --git a/src/view/com/composer/threadgate/ThreadgateBtn.tsx b/src/view/com/composer/threadgate/ThreadgateBtn.tsx index 0f342b41c4..2602b9bc45 100644 --- a/src/view/com/composer/threadgate/ThreadgateBtn.tsx +++ b/src/view/com/composer/threadgate/ThreadgateBtn.tsx @@ -1,7 +1,7 @@ import {useEffect, useMemo, useState} from 'react' import {Keyboard, type StyleProp, type ViewStyle} from 'react-native' import {type AnimatedStyle} from 'react-native-reanimated' -import {type AppBskyFeedPostgate} from '@atproto/api' +import {type AtUriString, toDatetimeString} from '@atproto/syntax' import {Trans, useLingui} from '@lingui/react/macro' import deepEqual from 'fast-deep-equal' @@ -24,6 +24,7 @@ import {Group3_Stroke2_Corner0_Rounded as GroupIcon} from '#/components/icons/Gr import * as Tooltip from '#/components/Tooltip' import {useAnalytics} from '#/analytics' import {IS_NATIVE} from '#/env' +import {type app} from '#/lexicons' import {useThreadgateNudged} from '#/storage/hooks/threadgate-nudged' export function ThreadgateBtn({ @@ -32,8 +33,8 @@ export function ThreadgateBtn({ threadgateAllowUISettings, onChangeThreadgateAllowUISettings, }: { - postgate: AppBskyFeedPostgate.Record - onChangePostgate: (v: AppBskyFeedPostgate.Record) => void + postgate: app.bsky.feed.postgate.Main + onChangePostgate: (v: app.bsky.feed.postgate.Main) => void threadgateAllowUISettings: ThreadgateAllowUISetting[] onChangeThreadgateAllowUISettings: (v: ThreadgateAllowUISetting[]) => void @@ -83,14 +84,20 @@ export function ThreadgateBtn({ const prefThreadgateAllowUISettings = threadgateRecordToAllowUISetting({ $type: 'app.bsky.feed.threadgate', - post: '', - createdAt: new Date().toISOString(), - allow: preferences?.postInteractionSettings.threadgateAllowRules, + post: '' as AtUriString, + createdAt: toDatetimeString(new Date()), + /* + * 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'], }) const prefPostgate = createPostgateRecord({ post: '', - embeddingRules: - preferences?.postInteractionSettings?.postgateEmbeddingRules || [], + embeddingRules: (preferences?.postInteractionSettings + ?.postgateEmbeddingRules || + []) as app.bsky.feed.postgate.Main['embeddingRules'], }) const isDirty = useMemo(() => {