about summary refs log tree commit diff
diff options
context:
space:
mode:
authorEric Bailey <git@esb.lol>2025-02-06 11:51:40 -0600
committerGitHub <noreply@github.com>2025-02-06 11:51:40 -0600
commit9cd4f92027774029234e38980fac3a12f136166f (patch)
tree52805dd7ba11a128bc0cf582c984d89d9a7c390d
parent1db2668a96208046ffe316114f65d432e57db994 (diff)
downloadvoidsky-9cd4f92027774029234e38980fac3a12f136166f.tar.zst
[APP-1013] Configure and apply default post interaction settings from user preferences (#7664)
* Add interaction settings screen

* Move header out of interaction settings form

* WIP hook it up

* Thread through default settings into composer

* Update copy pasta

* Handle edited state

* Copy feedback

* Sentence case

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* Update copy

* Bump SDK

* Fix new type error

* Less in your face

* Remove new dep

* Add slot

* Copy edit

---------

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
-rw-r--r--package.json2
-rw-r--r--src/Navigation.tsx9
-rw-r--r--src/components/dialogs/PostInteractionSettingsDialog.tsx82
-rw-r--r--src/lib/routes/types.ts1
-rw-r--r--src/routes.ts1
-rw-r--r--src/screens/Moderation/index.tsx16
-rw-r--r--src/screens/ModerationInteractionSettings/index.tsx127
-rw-r--r--src/screens/Settings/components/ExportCarDialog.tsx2
-rw-r--r--src/state/queries/post-interaction-settings.ts20
-rw-r--r--src/state/queries/preferences/const.ts4
-rw-r--r--src/view/com/composer/Composer.tsx10
-rw-r--r--src/view/com/composer/state/composer.ts25
-rw-r--r--yarn.lock99
13 files changed, 299 insertions, 99 deletions
diff --git a/package.json b/package.json
index 2c64fe78b..51289dd8d 100644
--- a/package.json
+++ b/package.json
@@ -54,7 +54,7 @@
     "icons:optimize": "svgo -f ./assets/icons"
   },
   "dependencies": {
-    "@atproto/api": "^0.13.31",
+    "@atproto/api": "^0.13.33",
     "@bitdrift/react-native": "^0.6.2",
     "@braintree/sanitize-url": "^6.0.2",
     "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
diff --git a/src/Navigation.tsx b/src/Navigation.tsx
index a6332c5d8..0dcce98bf 100644
--- a/src/Navigation.tsx
+++ b/src/Navigation.tsx
@@ -70,6 +70,7 @@ import {MessagesScreen} from '#/screens/Messages/ChatList'
 import {MessagesConversationScreen} from '#/screens/Messages/Conversation'
 import {MessagesSettingsScreen} from '#/screens/Messages/Settings'
 import {ModerationScreen} from '#/screens/Moderation'
+import {Screen as ModerationInteractionSettings} from '#/screens/ModerationInteractionSettings'
 import {PostLikedByScreen} from '#/screens/Post/PostLikedBy'
 import {PostQuotesScreen} from '#/screens/Post/PostQuotes'
 import {PostRepostedByScreen} from '#/screens/Post/PostRepostedBy'
@@ -156,6 +157,14 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
         options={{title: title(msg`Blocked Accounts`), requireAuth: true}}
       />
       <Stack.Screen
+        name="ModerationInteractionSettings"
+        getComponent={() => ModerationInteractionSettings}
+        options={{
+          title: title(msg`Post Interaction Settings`),
+          requireAuth: true,
+        }}
+      />
+      <Stack.Screen
         name="Settings"
         getComponent={() => SettingsScreen}
         options={{title: title(msg`Settings`), requireAuth: true}}
diff --git a/src/components/dialogs/PostInteractionSettingsDialog.tsx b/src/components/dialogs/PostInteractionSettingsDialog.tsx
index a698574a4..b443d59f2 100644
--- a/src/components/dialogs/PostInteractionSettingsDialog.tsx
+++ b/src/components/dialogs/PostInteractionSettingsDialog.tsx
@@ -40,6 +40,7 @@ import {Loader} from '#/components/Loader'
 import {Text} from '#/components/Typography'
 
 export type PostInteractionSettingsFormProps = {
+  canSave?: boolean
   onSave: () => void
   isSaving?: boolean
 
@@ -58,20 +59,53 @@ export function PostInteractionSettingsControlledDialog({
 }: PostInteractionSettingsFormProps & {
   control: Dialog.DialogControlProps
 }) {
+  const t = useTheme()
   const {_} = useLingui()
+
   return (
     <Dialog.Outer control={control}>
       <Dialog.Handle />
       <Dialog.ScrollableInner
         label={_(msg`Edit post interaction settings`)}
         style={[{maxWidth: 500}, a.w_full]}>
-        <PostInteractionSettingsForm {...rest} />
+        <View style={[a.gap_md]}>
+          <Header />
+          <PostInteractionSettingsForm {...rest} />
+          <Text
+            style={[
+              a.pt_sm,
+              a.text_sm,
+              a.leading_snug,
+              t.atoms.text_contrast_medium,
+            ]}>
+            <Trans>
+              You can set default interaction settings in{' '}
+              <Text style={[a.font_bold, t.atoms.text_contrast_medium]}>
+                Settings &rarr; Moderation &rarr; Interaction settings.
+              </Text>
+            </Trans>
+          </Text>
+        </View>
         <Dialog.Close />
       </Dialog.ScrollableInner>
     </Dialog.Outer>
   )
 }
 
+export function Header() {
+  return (
+    <View style={[a.gap_md, a.pb_sm]}>
+      <Text style={[a.text_2xl, a.font_bold]}>
+        <Trans>Post interaction settings</Trans>
+      </Text>
+      <Text style={[a.text_md, a.pb_xs]}>
+        <Trans>Customize who can interact with this post.</Trans>
+      </Text>
+      <Divider />
+    </View>
+  )
+}
+
 export type PostInteractionSettingsDialogProps = {
   control: Dialog.DialogControlProps
   /**
@@ -203,26 +237,31 @@ export function PostInteractionSettingsDialogControlledInner(
     <Dialog.ScrollableInner
       label={_(msg`Edit post interaction settings`)}
       style={[{maxWidth: 500}, a.w_full]}>
-      {isLoading ? (
-        <View style={[a.flex_1, a.py_4xl, a.align_center, a.justify_center]}>
-          <Loader size="xl" />
-        </View>
-      ) : (
-        <PostInteractionSettingsForm
-          replySettingsDisabled={!isThreadgateOwnedByViewer}
-          isSaving={isSaving}
-          onSave={onSave}
-          postgate={postgateValue}
-          onChangePostgate={setEditedPostgate}
-          threadgateAllowUISettings={allowUIValue}
-          onChangeThreadgateAllowUISettings={setEditedAllowUISettings}
-        />
-      )}
+      <View style={[a.gap_md]}>
+        <Header />
+
+        {isLoading ? (
+          <View style={[a.flex_1, a.py_4xl, a.align_center, a.justify_center]}>
+            <Loader size="xl" />
+          </View>
+        ) : (
+          <PostInteractionSettingsForm
+            replySettingsDisabled={!isThreadgateOwnedByViewer}
+            isSaving={isSaving}
+            onSave={onSave}
+            postgate={postgateValue}
+            onChangePostgate={setEditedPostgate}
+            threadgateAllowUISettings={allowUIValue}
+            onChangeThreadgateAllowUISettings={setEditedAllowUISettings}
+          />
+        )}
+      </View>
     </Dialog.ScrollableInner>
   )
 }
 
 export function PostInteractionSettingsForm({
+  canSave = true,
   onSave,
   isSaving,
   postgate,
@@ -283,17 +322,7 @@ export function PostInteractionSettingsForm({
   return (
     <View>
       <View style={[a.flex_1, a.gap_md]}>
-        <Text style={[a.text_2xl, a.font_bold]}>
-          <Trans>Post interaction settings</Trans>
-        </Text>
-
         <View style={[a.gap_lg]}>
-          <Text style={[a.text_md]}>
-            <Trans>Customize who can interact with this post.</Trans>
-          </Text>
-
-          <Divider />
-
           <View style={[a.gap_sm]}>
             <Text style={[a.font_bold, a.text_lg]}>
               <Trans>Quote settings</Trans>
@@ -435,6 +464,7 @@ export function PostInteractionSettingsForm({
       </View>
 
       <Button
+        disabled={!canSave || isSaving}
         label={_(msg`Save`)}
         onPress={onSave}
         color="primary"
diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts
index 66ee7bffa..8b69a66c4 100644
--- a/src/lib/routes/types.ts
+++ b/src/lib/routes/types.ts
@@ -12,6 +12,7 @@ export type CommonNavigatorParams = {
   ModerationModlists: undefined
   ModerationMutedAccounts: undefined
   ModerationBlockedAccounts: undefined
+  ModerationInteractionSettings: undefined
   Settings: undefined
   Profile: {name: string; hideBackButton?: boolean}
   ProfileFollowers: {name: string}
diff --git a/src/routes.ts b/src/routes.ts
index 8541d4254..576ac92d1 100644
--- a/src/routes.ts
+++ b/src/routes.ts
@@ -13,6 +13,7 @@ export const router = new Router({
   ModerationModlists: '/moderation/modlists',
   ModerationMutedAccounts: '/moderation/muted-accounts',
   ModerationBlockedAccounts: '/moderation/blocked-accounts',
+  ModerationInteractionSettings: '/moderation/interaction-settings',
   // profiles, threads, lists
   Profile: ['/profile/:name', '/profile/:name/rss'],
   ProfileFollowers: '/profile/:name/followers',
diff --git a/src/screens/Moderation/index.tsx b/src/screens/Moderation/index.tsx
index 6b4dd06bc..55cc67f8c 100644
--- a/src/screens/Moderation/index.tsx
+++ b/src/screens/Moderation/index.tsx
@@ -28,6 +28,7 @@ import * as Toggle from '#/components/forms/Toggle'
 import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
 import {CircleBanSign_Stroke2_Corner0_Rounded as CircleBanSign} from '#/components/icons/CircleBanSign'
 import {Props as SVGIconProps} from '#/components/icons/common'
+import {EditBig_Stroke2_Corner0_Rounded as EditBig} from '#/components/icons/EditBig'
 import {Filter_Stroke2_Corner0_Rounded as Filter} from '#/components/icons/Filter'
 import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group'
 import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person'
@@ -199,6 +200,21 @@ export function ModerationScreenInner({
           a.overflow_hidden,
           t.atoms.bg_contrast_25,
         ]}>
+        <Link
+          label={_(msg`View your default post interaction settings`)}
+          testID="interactionSettingsBtn"
+          to="/moderation/interaction-settings">
+          {state => (
+            <SubItem
+              title={_(msg`Interaction settings`)}
+              icon={EditBig}
+              style={[
+                (state.hovered || state.pressed) && [t.atoms.bg_contrast_50],
+              ]}
+            />
+          )}
+        </Link>
+        <Divider />
         <Button
           testID="mutedWordsBtn"
           label={_(msg`Open muted words and tags settings`)}
diff --git a/src/screens/ModerationInteractionSettings/index.tsx b/src/screens/ModerationInteractionSettings/index.tsx
new file mode 100644
index 000000000..99b29d950
--- /dev/null
+++ b/src/screens/ModerationInteractionSettings/index.tsx
@@ -0,0 +1,127 @@
+import React from 'react'
+import {View} from 'react-native'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import deepEqual from 'lodash.isequal'
+
+import {logger} from '#/logger'
+import {usePostInteractionSettingsMutation} from '#/state/queries/post-interaction-settings'
+import {createPostgateRecord} from '#/state/queries/postgate/util'
+import {
+  usePreferencesQuery,
+  UsePreferencesQueryResponse,
+} from '#/state/queries/preferences'
+import {
+  threadgateAllowUISettingToAllowRecordValue,
+  threadgateRecordToAllowUISetting,
+} from '#/state/queries/threadgate'
+import * as Toast from '#/view/com/util/Toast'
+import {atoms as a, useGutters} from '#/alf'
+import {Admonition} from '#/components/Admonition'
+import {PostInteractionSettingsForm} from '#/components/dialogs/PostInteractionSettingsDialog'
+import * as Layout from '#/components/Layout'
+import {Loader} from '#/components/Loader'
+
+export function Screen() {
+  const gutters = useGutters(['base'])
+  const {data: preferences} = usePreferencesQuery()
+  return (
+    <Layout.Screen testID="ModerationInteractionSettingsScreen">
+      <Layout.Header.Outer>
+        <Layout.Header.BackButton />
+        <Layout.Header.Content>
+          <Layout.Header.TitleText>
+            <Trans>Post Interaction Settings</Trans>
+          </Layout.Header.TitleText>
+        </Layout.Header.Content>
+        <Layout.Header.Slot />
+      </Layout.Header.Outer>
+      <Layout.Content>
+        <View style={[gutters, a.gap_xl]}>
+          <Admonition type="tip">
+            <Trans>
+              The following settings will be used as your defaults when creating
+              new posts. You can edit these for a specific post from the
+              composer.
+            </Trans>
+          </Admonition>
+          {preferences ? (
+            <Inner preferences={preferences} />
+          ) : (
+            <View style={[gutters, a.justify_center, a.align_center]}>
+              <Loader size="xl" />
+            </View>
+          )}
+        </View>
+      </Layout.Content>
+    </Layout.Screen>
+  )
+}
+
+function Inner({preferences}: {preferences: UsePreferencesQueryResponse}) {
+  const {_} = useLingui()
+  const {mutateAsync: setPostInteractionSettings, isPending} =
+    usePostInteractionSettingsMutation()
+  const [error, setError] = React.useState<string | undefined>(undefined)
+
+  const allowUI = React.useMemo(() => {
+    return threadgateRecordToAllowUISetting({
+      $type: 'app.bsky.feed.threadgate',
+      post: '',
+      createdAt: new Date().toString(),
+      allow: preferences.postInteractionSettings.threadgateAllowRules,
+    })
+  }, [preferences.postInteractionSettings.threadgateAllowRules])
+  const postgate = React.useMemo(() => {
+    return createPostgateRecord({
+      post: '',
+      embeddingRules:
+        preferences.postInteractionSettings.postgateEmbeddingRules,
+    })
+  }, [preferences.postInteractionSettings.postgateEmbeddingRules])
+
+  const [maybeEditedAllowUI, setAllowUI] = React.useState(allowUI)
+  const [maybeEditedPostgate, setEditedPostgate] = React.useState(postgate)
+
+  const wasEdited = React.useMemo(() => {
+    return (
+      !deepEqual(allowUI, maybeEditedAllowUI) ||
+      !deepEqual(postgate.embeddingRules, maybeEditedPostgate.embeddingRules)
+    )
+  }, [postgate, allowUI, maybeEditedAllowUI, maybeEditedPostgate])
+
+  const onSave = React.useCallback(async () => {
+    setError('')
+
+    try {
+      await setPostInteractionSettings({
+        threadgateAllowRules:
+          threadgateAllowUISettingToAllowRecordValue(maybeEditedAllowUI),
+        postgateEmbeddingRules: maybeEditedPostgate.embeddingRules ?? [],
+      })
+      Toast.show(_(msg`Settings saved`))
+    } catch (e: any) {
+      logger.error(`Failed to save post interaction settings`, {
+        context: 'ModerationInteractionSettingsScreen',
+        safeMessage: e.message,
+      })
+      setError(_(msg`Failed to save settings. Please try again.`))
+    }
+  }, [_, maybeEditedPostgate, maybeEditedAllowUI, setPostInteractionSettings])
+
+  return (
+    <>
+      <PostInteractionSettingsForm
+        canSave={wasEdited}
+        isSaving={isPending}
+        onSave={onSave}
+        postgate={maybeEditedPostgate}
+        onChangePostgate={setEditedPostgate}
+        threadgateAllowUISettings={maybeEditedAllowUI}
+        onChangeThreadgateAllowUISettings={setAllowUI}
+      />
+
+      {error && <Admonition type="error">{error}</Admonition>}
+    </>
+  )
+}
diff --git a/src/screens/Settings/components/ExportCarDialog.tsx b/src/screens/Settings/components/ExportCarDialog.tsx
index 2de3895d3..685707259 100644
--- a/src/screens/Settings/components/ExportCarDialog.tsx
+++ b/src/screens/Settings/components/ExportCarDialog.tsx
@@ -36,7 +36,7 @@ export function ExportCarDialog({
       const saveRes = await saveBytesToDisk(
         'repo.car',
         downloadRes.data,
-        downloadRes.headers['content-type'],
+        downloadRes.headers['content-type'] || 'application/vnd.ipld.car',
       )
 
       if (saveRes) {
diff --git a/src/state/queries/post-interaction-settings.ts b/src/state/queries/post-interaction-settings.ts
new file mode 100644
index 000000000..a256f2956
--- /dev/null
+++ b/src/state/queries/post-interaction-settings.ts
@@ -0,0 +1,20 @@
+import {AppBskyActorDefs} from '@atproto/api'
+import {useMutation, useQueryClient} from '@tanstack/react-query'
+
+import {preferencesQueryKey} from '#/state/queries/preferences'
+import {useAgent} from '#/state/session'
+
+export function usePostInteractionSettingsMutation() {
+  const qc = useQueryClient()
+  const agent = useAgent()
+  return useMutation({
+    async mutationFn(props: AppBskyActorDefs.PostInteractionSettingsPref) {
+      await agent.setPostInteractionSettings(props)
+    },
+    async onSuccess() {
+      await qc.invalidateQueries({
+        queryKey: preferencesQueryKey,
+      })
+    },
+  })
+}
diff --git a/src/state/queries/preferences/const.ts b/src/state/queries/preferences/const.ts
index 549f7ce29..3c1fead5e 100644
--- a/src/state/queries/preferences/const.ts
+++ b/src/state/queries/preferences/const.ts
@@ -39,4 +39,8 @@ export const DEFAULT_LOGGED_OUT_PREFERENCES: UsePreferencesQueryResponse = {
     activeProgressGuide: undefined,
     nuxs: [],
   },
+  postInteractionSettings: {
+    threadgateAllowRules: undefined,
+    postgateEmbeddingRules: [],
+  },
 }
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx
index 0e9b52ce0..78293f618 100644
--- a/src/view/com/composer/Composer.tsx
+++ b/src/view/com/composer/Composer.tsx
@@ -83,6 +83,7 @@ import {
   useLanguagePrefs,
   useLanguagePrefsApi,
 } from '#/state/preferences/languages'
+import {usePreferencesQuery} from '#/state/queries/preferences'
 import {useProfileQuery} from '#/state/queries/profile'
 import {Gif} from '#/state/queries/tenor'
 import {useAgent, useSession} from '#/state/session'
@@ -169,6 +170,7 @@ export const ComposePost = ({
   const discardPromptControl = Prompt.usePromptControl()
   const {closeAllDialogs} = useDialogStateControlContext()
   const {closeAllModals} = useModalControls()
+  const {data: preferences} = usePreferencesQuery()
 
   const [isKeyboardVisible] = useIsKeyboardVisible({iosUseWillEvents: true})
   const [isPublishing, setIsPublishing] = useState(false)
@@ -177,7 +179,13 @@ export const ComposePost = ({
 
   const [composerState, composerDispatch] = useReducer(
     composerReducer,
-    {initImageUris, initQuoteUri: initQuote?.uri, initText, initMention},
+    {
+      initImageUris,
+      initQuoteUri: initQuote?.uri,
+      initText,
+      initMention,
+      initInteractionSettings: preferences?.postInteractionSettings,
+    },
     createComposerState,
   )
 
diff --git a/src/view/com/composer/state/composer.ts b/src/view/com/composer/state/composer.ts
index 6d4f10297..f5a55f175 100644
--- a/src/view/com/composer/state/composer.ts
+++ b/src/view/com/composer/state/composer.ts
@@ -1,5 +1,10 @@
 import {ImagePickerAsset} from 'expo-image-picker'
-import {AppBskyFeedPostgate, AppBskyRichtextFacet, RichText} from '@atproto/api'
+import {
+  AppBskyFeedPostgate,
+  AppBskyRichtextFacet,
+  BskyPreferences,
+  RichText,
+} from '@atproto/api'
 import {nanoid} from 'nanoid/non-secure'
 
 import {SelfLabel} from '#/lib/moderation'
@@ -13,7 +18,7 @@ import {
 import {ComposerImage, createInitialImages} from '#/state/gallery'
 import {createPostgateRecord} from '#/state/queries/postgate/util'
 import {Gif} from '#/state/queries/tenor'
-import {threadgateViewToAllowUISetting} from '#/state/queries/threadgate'
+import {threadgateRecordToAllowUISetting} from '#/state/queries/threadgate'
 import {ThreadgateAllowUISetting} from '#/state/queries/threadgate'
 import {ComposerOpts} from '#/state/shell/composer'
 import {
@@ -477,11 +482,15 @@ export function createComposerState({
   initMention,
   initImageUris,
   initQuoteUri,
+  initInteractionSettings,
 }: {
   initText: string | undefined
   initMention: string | undefined
   initImageUris: ComposerOpts['imageUris']
   initQuoteUri: string | undefined
+  initInteractionSettings:
+    | BskyPreferences['postInteractionSettings']
+    | undefined
 }): ComposerState {
   let media: ImagesMedia | undefined
   if (initImageUris?.length) {
@@ -591,8 +600,16 @@ export function createComposerState({
           },
         },
       ],
-      postgate: createPostgateRecord({post: ''}),
-      threadgate: threadgateViewToAllowUISetting(undefined),
+      postgate: createPostgateRecord({
+        post: '',
+        embeddingRules: initInteractionSettings?.postgateEmbeddingRules || [],
+      }),
+      threadgate: threadgateRecordToAllowUISetting({
+        $type: 'app.bsky.feed.threadgate',
+        post: '',
+        createdAt: new Date().toString(),
+        allow: initInteractionSettings?.threadgateAllowRules,
+      }),
     },
   }
 }
diff --git a/yarn.lock b/yarn.lock
index 942656d21..f6a3781c0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -72,15 +72,15 @@
     tlds "^1.234.0"
     zod "^3.23.8"
 
-"@atproto/api@^0.13.31":
-  version "0.13.31"
-  resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.13.31.tgz#23ca2c9118eefddf6e0206f759e56b6726b68483"
-  integrity sha512-i2cUQuwe+3j8rgPJj4YWRjSQeJunGqJ3IzesnvbODjjZh3IS9jB80BZ/pTe/AvNg6JCBbqeWJjWDVKeFHaiZAw==
-  dependencies:
-    "@atproto/common-web" "^0.3.2"
-    "@atproto/lexicon" "^0.4.5"
-    "@atproto/syntax" "^0.3.1"
-    "@atproto/xrpc" "^0.6.6"
+"@atproto/api@^0.13.33":
+  version "0.13.33"
+  resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.13.33.tgz#01d31a1cfd1be311e11324b810b8a83fe4cbf9b0"
+  integrity sha512-d8AOvtxo2J2zrmcakJTUtLdz2ns+pAqywNXhPxPzHrHcw79D6MKBLHR0vr8oxkGwhDBQTsHiQWTk4gSo8PF7YA==
+  dependencies:
+    "@atproto/common-web" "^0.4.0"
+    "@atproto/lexicon" "^0.4.6"
+    "@atproto/syntax" "^0.3.2"
+    "@atproto/xrpc" "^0.6.8"
     await-lock "^2.2.2"
     multiformats "^9.9.0"
     tlds "^1.234.0"
@@ -169,10 +169,10 @@
     uint8arrays "3.0.0"
     zod "^3.23.8"
 
-"@atproto/common-web@^0.3.2":
-  version "0.3.2"
-  resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.3.2.tgz#4cf78ad4d24fed801882f3d35afc39bceccdff51"
-  integrity sha512-Vx0JtL1/CssJbFAb0UOdvTrkbUautsDfHNOXNTcX2vyPIxH9xOameSqLLunM1hZnOQbJwyjmQCt6TV+bhnanDg==
+"@atproto/common-web@^0.4.0":
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.4.0.tgz#b1407ae3f964f0ee23c2c3184f38041bac99d1f4"
+  integrity sha512-ZYL0P9myHybNgwh/hBY0HaBzqiLR1B5/ie5bJpLQAg0whRzNA28t8/nU2vh99tbsWcAF0LOD29M8++LyENJLNQ==
   dependencies:
     graphemer "^1.4.0"
     multiformats "^9.9.0"
@@ -293,13 +293,13 @@
     multiformats "^9.9.0"
     zod "^3.23.8"
 
-"@atproto/lexicon@^0.4.5":
-  version "0.4.5"
-  resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.4.5.tgz#4fcf3731193c674286e9e8d677bbab5dd530b817"
-  integrity sha512-fljWqMGKn+XWtTprBcS3F1hGBREnQYh6qYHv2sjENucc7REms1gtmZXSerB9N6pVeHVNOnXiILdukeAcic5OEw==
+"@atproto/lexicon@^0.4.6":
+  version "0.4.6"
+  resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.4.6.tgz#74b2a0f3e4c867b33f75430d4ccec70c47e41576"
+  integrity sha512-RbiwXcnTuLp9vQrNoQ7xly8HyifKkovqCYtbfXVwqdylWYKPhmRsYkRfcPNv/lILhT9Lm0GVnxNwGGwvvgIsfA==
   dependencies:
-    "@atproto/common-web" "^0.3.2"
-    "@atproto/syntax" "^0.3.1"
+    "@atproto/common-web" "^0.4.0"
+    "@atproto/syntax" "^0.3.2"
     iso-datestring-validator "^2.2.2"
     multiformats "^9.9.0"
     zod "^3.23.8"
@@ -447,6 +447,11 @@
   resolved "https://registry.yarnpkg.com/@atproto/syntax/-/syntax-0.3.1.tgz#4346418728f9643d783d2ffcf7c77e132e1f53d4"
   integrity sha512-fzW0Mg1QUOVCWUD3RgEsDt6d1OZ6DdFmbKcDdbzUfh0t4rhtRAC05KbZYmxuMPWDAiJ4BbbQ5dkAc/mNypMXkw==
 
+"@atproto/syntax@^0.3.2":
+  version "0.3.2"
+  resolved "https://registry.yarnpkg.com/@atproto/syntax/-/syntax-0.3.2.tgz#188f8dccba11e5ace1bf83cbff8ed9e1a3d2d66c"
+  integrity sha512-JLMhTbXER1Im98RrozfsLAZARGIAzKCZEm+Inh1IF00XU6tHcoGKS+HOw0Uy4R2r04yvxoFs8fswmwAhmMpMdw==
+
 "@atproto/xrpc-server@^0.7.4":
   version "0.7.4"
   resolved "https://registry.yarnpkg.com/@atproto/xrpc-server/-/xrpc-server-0.7.4.tgz#dfac8f7276c1c971a35eaba627eb6372088441c3"
@@ -473,12 +478,12 @@
     "@atproto/lexicon" "^0.4.4"
     zod "^3.23.8"
 
-"@atproto/xrpc@^0.6.6":
-  version "0.6.6"
-  resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.6.6.tgz#28f58270ef4a8056f7f718bd52512e74bcd3702f"
-  integrity sha512-umXEYVMo9/pyIBoKmIAIi64RXDW9tSXY+wqztlQ6I2GZtjLfNZqmAWU+wADk3SxUe54mvjxxGyA4TtyGtDMfhA==
+"@atproto/xrpc@^0.6.8":
+  version "0.6.8"
+  resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.6.8.tgz#cede54e17b6f8863f78e16f27f87c1966446eea6"
+  integrity sha512-+KW0NcwdFyLziccYimX6tPkORiwwxlJPqlkVL9bJyj8nJ0aB8cyqo9HXkziMI+R6ansB1BuWQ0tfdPlLLwrUcA==
   dependencies:
-    "@atproto/lexicon" "^0.4.5"
+    "@atproto/lexicon" "^0.4.6"
     zod "^3.23.8"
 
 "@aws-crypto/crc32@3.0.0":
@@ -3290,7 +3295,7 @@
     "@babel/parser" "^7.25.9"
     "@babel/types" "^7.25.9"
 
-"@babel/traverse--for-generate-function-map@npm:@babel/traverse@^7.25.3":
+"@babel/traverse--for-generate-function-map@npm:@babel/traverse@^7.25.3", "@babel/traverse@^7.25.3", "@babel/traverse@^7.25.9":
   version "7.25.9"
   resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.9.tgz#a50f8fe49e7f69f53de5bea7e413cd35c5e13c84"
   integrity sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==
@@ -3351,19 +3356,6 @@
     debug "^4.3.1"
     globals "^11.1.0"
 
-"@babel/traverse@^7.25.3", "@babel/traverse@^7.25.9":
-  version "7.25.9"
-  resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.9.tgz#a50f8fe49e7f69f53de5bea7e413cd35c5e13c84"
-  integrity sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==
-  dependencies:
-    "@babel/code-frame" "^7.25.9"
-    "@babel/generator" "^7.25.9"
-    "@babel/parser" "^7.25.9"
-    "@babel/template" "^7.25.9"
-    "@babel/types" "^7.25.9"
-    debug "^4.3.1"
-    globals "^11.1.0"
-
 "@babel/types@^7.0.0", "@babel/types@^7.20.0", "@babel/types@^7.20.7", "@babel/types@^7.22.10", "@babel/types@^7.22.5", "@babel/types@^7.3.3", "@babel/types@^7.4.4":
   version "7.22.10"
   resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.22.10.tgz#4a9e76446048f2c66982d1a989dd12b8a2d2dc03"
@@ -17652,16 +17644,7 @@ string-natural-compare@^3.0.1:
   resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
   integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
 
-"string-width-cjs@npm:string-width@^4.2.0":
-  version "4.2.3"
-  resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
-  integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
-  dependencies:
-    emoji-regex "^8.0.0"
-    is-fullwidth-code-point "^3.0.0"
-    strip-ansi "^6.0.1"
-
-string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
+"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
   version "4.2.3"
   resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
   integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -17761,7 +17744,7 @@ string_decoder@~1.1.1:
   dependencies:
     safe-buffer "~5.1.0"
 
-"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
+"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
   version "6.0.1"
   resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
   integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -17775,13 +17758,6 @@ strip-ansi@^5.2.0:
   dependencies:
     ansi-regex "^4.1.0"
 
-strip-ansi@^6.0.0, strip-ansi@^6.0.1:
-  version "6.0.1"
-  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
-  integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
-  dependencies:
-    ansi-regex "^5.0.1"
-
 strip-ansi@^7.0.1:
   version "7.1.0"
   resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
@@ -19056,7 +19032,7 @@ wordwrap@^1.0.0:
   resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
   integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==
 
-"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
+"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
   version "7.0.0"
   resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
   integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -19074,15 +19050,6 @@ wrap-ansi@^6.2.0:
     string-width "^4.1.0"
     strip-ansi "^6.0.0"
 
-wrap-ansi@^7.0.0:
-  version "7.0.0"
-  resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
-  integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
-  dependencies:
-    ansi-styles "^4.0.0"
-    string-width "^4.1.0"
-    strip-ansi "^6.0.0"
-
 wrap-ansi@^8.0.1, wrap-ansi@^8.1.0:
   version "8.1.0"
   resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"