From 5bf1141b553a94906f0f9c26805f5bbfec38aad7 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 10 Oct 2025 10:03:47 -0500 Subject: [PATCH] Add validation to threadgate records, check for max hidden replies (#9178) --- .../PostControls/PostMenu/PostMenuItems.tsx | 35 ++++++++++++++++--- src/state/queries/threadgate/index.ts | 34 ++++++++++++++++++ 2 files changed, 64 insertions(+), 5 deletions(-) diff --git a/src/components/PostControls/PostMenu/PostMenuItems.tsx b/src/components/PostControls/PostMenu/PostMenuItems.tsx index 2ec0c6a4cd..63501c5952 100644 --- a/src/components/PostControls/PostMenu/PostMenuItems.tsx +++ b/src/components/PostControls/PostMenu/PostMenuItems.tsx @@ -46,7 +46,12 @@ import { useProfileBlockMutationQueue, useProfileMuteMutationQueue, } from '#/state/queries/profile' -import {useToggleReplyVisibilityMutation} from '#/state/queries/threadgate' +import { + InvalidInteractionSettingsError, + MAX_HIDDEN_REPLIES, + MaxHiddenRepliesError, + useToggleReplyVisibilityMutation, +} from '#/state/queries/threadgate' import {useRequireAuth, useSession} from '#/state/session' import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' import * as Toast from '#/view/com/util/Toast' @@ -339,10 +344,30 @@ let PostMenuItems = ({ : _(msg({message: 'Reply visibility updated', context: 'toast'})), ) } catch (e: any) { - Toast.show( - _(msg({message: 'Updating reply visibility failed', context: 'toast'})), - ) - logger.error(`Failed to ${action} reply`, {safeMessage: e.message}) + if (e instanceof MaxHiddenRepliesError) { + Toast.show( + _( + msg({ + message: `You can hide a maximum of ${MAX_HIDDEN_REPLIES} replies.`, + context: 'toast', + }), + ), + ) + } else if (e instanceof InvalidInteractionSettingsError) { + Toast.show( + _(msg({message: 'Invalid interaction settings.', context: 'toast'})), + ) + } else { + Toast.show( + _( + msg({ + message: 'Updating reply visibility failed', + context: 'toast', + }), + ), + ) + logger.error(`Failed to ${action} reply`, {safeMessage: e.message}) + } } } diff --git a/src/state/queries/threadgate/index.ts b/src/state/queries/threadgate/index.ts index eeb9f7035a..305acc5d05 100644 --- a/src/state/queries/threadgate/index.ts +++ b/src/state/queries/threadgate/index.ts @@ -25,6 +25,11 @@ import * as bsky from '#/types/bsky' export * from '#/state/queries/threadgate/types' export * from '#/state/queries/threadgate/util' +/** + * Must match the threadgate lexicon record definition. + */ +export const MAX_HIDDEN_REPLIES = 300 + export const threadgateRecordQueryKeyRoot = 'threadgate-record' export const createThreadgateRecordQueryKey = (uri: string) => [ threadgateRecordQueryKeyRoot, @@ -205,6 +210,7 @@ export async function upsertThreadgate( }) const next = await callback(prev) if (!next) return + validateThreadgateRecordOrThrow(next) await writeThreadgateRecord({ agent, postUri, @@ -358,3 +364,31 @@ export function useToggleReplyVisibilityMutation() { }, }) } + +export class MaxHiddenRepliesError extends Error { + constructor(message?: string) { + super(message || 'Maximum number of hidden replies reached') + this.name = 'MaxHiddenRepliesError' + } +} + +export class InvalidInteractionSettingsError extends Error { + constructor(message?: string) { + super(message || 'Invalid interaction settings') + this.name = 'InvalidInteractionSettingsError' + } +} + +export function validateThreadgateRecordOrThrow( + record: AppBskyFeedThreadgate.Record, +) { + const result = AppBskyFeedThreadgate.validateRecord(record) + + if (result.success) { + if ((result.value.hiddenReplies?.length ?? 0) > MAX_HIDDEN_REPLIES) { + throw new MaxHiddenRepliesError() + } + } else { + throw new InvalidInteractionSettingsError() + } +}