diff --git a/app.config.js b/app.config.js
index fc70ca3332..57de4493de 100644
--- a/app.config.js
+++ b/app.config.js
@@ -99,7 +99,7 @@ module.exports = function (config) {
             'tr',
             'uk',
             'vi',
-            'yue-Hant',
+            'yue',
             'zh-Hans',
             'zh-Hant',
           ],
@@ -226,8 +226,12 @@ module.exports = function (config) {
           },
         ],
         'react-native-compressor',
-        // TODO: Reenable when the build issue is fixed.
-        // '@bitdrift/react-native',
+        [
+          '@bitdrift/react-native',
+          {
+            networkInstrumentation: true,
+          },
+        ],
         './plugins/starterPackAppClipExtension/withStarterPackAppClip.js',
         './plugins/withAndroidManifestPlugin.js',
         './plugins/withAndroidManifestFCMIconPlugin.js',
diff --git a/package.json b/package.json
index 991a08427f..88ef900238 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "bsky.app",
-  "version": "1.96.7",
+  "version": "1.97.0",
   "private": true,
   "engines": {
     "node": ">=20"
@@ -55,6 +55,7 @@
   },
   "dependencies": {
     "@atproto/api": "^0.13.28",
+    "@bitdrift/react-native": "^0.6.2",
     "@braintree/sanitize-url": "^6.0.2",
     "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
     "@emoji-mart/react": "^1.1.1",
diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx
index 8532cbbb49..489ebb2257 100644
--- a/src/components/Layout/index.tsx
+++ b/src/components/Layout/index.tsx
@@ -58,6 +58,7 @@ export const Content = React.memo(function Content({
   contentContainerStyle,
   ...props
 }: ContentProps) {
+  const t = useTheme()
   const {footerHeight} = useShellLayout()
   const animatedProps = useAnimatedProps(() => {
     return {
@@ -73,6 +74,7 @@ export const Content = React.memo(function Content({
     <Animated.ScrollView
       id="content"
       automaticallyAdjustsScrollIndicatorInsets={false}
+      indicatorStyle={t.scheme === 'dark' ? 'white' : 'black'}
       // sets the scroll inset to the height of the footer
       animatedProps={animatedProps}
       style={[scrollViewStyles.common, style]}
diff --git a/src/components/Link.tsx b/src/components/Link.tsx
index 3cd593a106..50e741ea7b 100644
--- a/src/components/Link.tsx
+++ b/src/components/Link.tsx
@@ -15,7 +15,6 @@ import {
   linkRequiresWarning,
 } from '#/lib/strings/url-helpers'
 import {isNative, isWeb} from '#/platform/detection'
-import {shouldClickOpenNewTab} from '#/platform/urls'
 import {useModalControls} from '#/state/modals'
 import {atoms as a, flatten, TextStyleProp, useTheme, web} from '#/alf'
 import {Button, ButtonProps} from '#/components/Button'
@@ -55,6 +54,12 @@ type BaseLinkProps = Pick<
    */
   onPress?: (e: GestureResponderEvent) => void | false
 
+  /**
+   * Callback for when the link is long pressed (on native). Prevent default
+   * and return `false` to exit early and prevent default long press hander.
+   */
+  onLongPress?: (e: GestureResponderEvent) => void | false
+
   /**
    * Web-only attribute. Sets `download` attr on web.
    */
@@ -72,6 +77,7 @@ export function useLink({
   action = 'push',
   disableMismatchWarning,
   onPress: outerOnPress,
+  onLongPress: outerOnLongPress,
   shareOnLongPress,
 }: BaseLinkProps & {
   displayText: string
@@ -175,8 +181,14 @@ export function useLink({
     }
   }, [disableMismatchWarning, displayText, href, isExternal, openModal])
 
-  const onLongPress =
-    isNative && isExternal && shareOnLongPress ? handleLongPress : undefined
+  const onLongPress = React.useCallback(
+    (e: GestureResponderEvent) => {
+      const exitEarlyIfFalse = outerOnLongPress?.(e)
+      if (exitEarlyIfFalse === false) return
+      return isNative && shareOnLongPress ? handleLongPress() : undefined
+    },
+    [outerOnLongPress, handleLongPress, shareOnLongPress],
+  )
 
   return {
     isExternal,
@@ -202,14 +214,16 @@ export function Link({
   to,
   action = 'push',
   onPress: outerOnPress,
+  onLongPress: outerOnLongPress,
   download,
   ...rest
 }: LinkProps) {
-  const {href, isExternal, onPress} = useLink({
+  const {href, isExternal, onPress, onLongPress} = useLink({
     to,
     displayText: typeof children === 'string' ? children : '',
     action,
     onPress: outerOnPress,
+    onLongPress: outerOnLongPress,
   })
 
   return (
@@ -220,6 +234,7 @@ export function Link({
       accessibilityRole="link"
       href={href}
       onPress={download ? undefined : onPress}
+      onLongPress={onLongPress}
       {...web({
         hrefAttrs: {
           target: download ? undefined : isExternal ? 'blank' : undefined,
@@ -241,7 +256,7 @@ export type InlineLinkProps = React.PropsWithChildren<
     TextStyleProp &
     Pick<TextProps, 'selectable' | 'numberOfLines'>
 > &
-  Pick<ButtonProps, 'label'> & {
+  Pick<ButtonProps, 'label' | 'accessibilityHint'> & {
     disableUnderline?: boolean
     title?: TextProps['title']
   }
@@ -253,6 +268,7 @@ export function InlineLinkText({
   disableMismatchWarning,
   style,
   onPress: outerOnPress,
+  onLongPress: outerOnLongPress,
   download,
   selectable,
   label,
@@ -268,6 +284,7 @@ export function InlineLinkText({
     action,
     disableMismatchWarning,
     onPress: outerOnPress,
+    onLongPress: outerOnLongPress,
     shareOnLongPress,
   })
   const {
@@ -319,6 +336,21 @@ export function InlineLinkText({
   )
 }
 
+export function WebOnlyInlineLinkText({
+  children,
+  to,
+  onPress,
+  ...props
+}: Omit<InlineLinkProps, 'onLongPress'>) {
+  return isWeb ? (
+    <InlineLinkText {...props} to={to} onPress={onPress}>
+      {children}
+    </InlineLinkText>
+  ) : (
+    <Text {...props}>{children}</Text>
+  )
+}
+
 /**
  * Utility to create a static `onPress` handler for a `Link` that would otherwise link to a URI
  *
@@ -327,7 +359,10 @@ export function InlineLinkText({
  */
 export function createStaticClick(
   onPressHandler: Exclude<BaseLinkProps['onPress'], undefined>,
-): Pick<BaseLinkProps, 'to' | 'onPress'> {
+): {
+  to: BaseLinkProps['to']
+  onPress: Exclude<BaseLinkProps['onPress'], undefined>
+} {
   return {
     to: '#',
     onPress(e: GestureResponderEvent) {
@@ -338,17 +373,72 @@ export function createStaticClick(
   }
 }
 
-export function WebOnlyInlineLinkText({
-  children,
-  to,
-  onPress,
-  ...props
-}: InlineLinkProps) {
-  return isWeb ? (
-    <InlineLinkText {...props} to={to} onPress={onPress}>
-      {children}
-    </InlineLinkText>
-  ) : (
-    <Text {...props}>{children}</Text>
+/**
+ * Utility to create a static `onPress` handler for a `Link`, but only if the
+ * click was not modified in some way e.g. `Cmd` or a middle click.
+ *
+ * On native, this behaves the same as `createStaticClick` because there are no
+ * options to "modify" the click in this sense.
+ *
+ * Example:
+ *   `<Link {...createStaticClick(e => {...})} />`
+ */
+export function createStaticClickIfUnmodified(
+  onPressHandler: Exclude<BaseLinkProps['onPress'], undefined>,
+): {onPress: Exclude<BaseLinkProps['onPress'], undefined>} {
+  return {
+    onPress(e: GestureResponderEvent) {
+      if (!isWeb || !isModifiedClickEvent(e)) {
+        e.preventDefault()
+        onPressHandler(e)
+        return false
+      }
+    },
+  }
+}
+
+/**
+ * Determines if the click event has a meta key pressed, indicating the user
+ * intends to deviate from default behavior.
+ */
+export function isClickEventWithMetaKey(e: GestureResponderEvent) {
+  if (!isWeb) return false
+  const event = e as unknown as MouseEvent
+  return event.metaKey || event.altKey || event.ctrlKey || event.shiftKey
+}
+
+/**
+ * Determines if the web click target is anything other than `_self`
+ */
+export function isClickTargetExternal(e: GestureResponderEvent) {
+  if (!isWeb) return false
+  const event = e as unknown as MouseEvent
+  const el = event.currentTarget as HTMLAnchorElement
+  return el && el.target && el.target !== '_self'
+}
+
+/**
+ * Determines if a click event has been modified in some way from its default
+ * behavior, e.g. `Cmd` or a middle click.
+ * {@link https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button}
+ */
+export function isModifiedClickEvent(e: GestureResponderEvent): boolean {
+  if (!isWeb) return false
+  const event = e as unknown as MouseEvent
+  const isPrimaryButton = event.button === 0
+  return (
+    isClickEventWithMetaKey(e) || isClickTargetExternal(e) || !isPrimaryButton
   )
 }
+
+/**
+ * Determines if a click event has been modified in a way that should indiciate
+ * that the user intends to open a new tab.
+ * {@link https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button}
+ */
+export function shouldClickOpenNewTab(e: GestureResponderEvent) {
+  if (!isWeb) return false
+  const event = e as unknown as MouseEvent
+  const isMiddleClick = isWeb && event.button === 1
+  return isClickEventWithMetaKey(e) || isClickTargetExternal(e) || isMiddleClick
+}
diff --git a/src/components/Menu/index.tsx b/src/components/Menu/index.tsx
index 99fb2d127e..9c970b051f 100644
--- a/src/components/Menu/index.tsx
+++ b/src/components/Menu/index.tsx
@@ -47,7 +47,12 @@ export function Root({
   return <Context.Provider value={context}>{children}</Context.Provider>
 }
 
-export function Trigger({children, label, role = 'button'}: TriggerProps) {
+export function Trigger({
+  children,
+  label,
+  role = 'button',
+  hint,
+}: TriggerProps) {
   const context = useMenuContext()
   const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
   const {
@@ -65,11 +70,13 @@ export function Trigger({children, label, role = 'button'}: TriggerProps) {
       pressed,
     },
     props: {
+      ref: null,
       onPress: context.control.open,
       onFocus,
       onBlur,
       onPressIn,
       onPressOut,
+      accessibilityHint: hint,
       accessibilityLabel: label,
       accessibilityRole: role,
     },
diff --git a/src/components/Menu/index.web.tsx b/src/components/Menu/index.web.tsx
index d1863e478e..dc91161682 100644
--- a/src/components/Menu/index.web.tsx
+++ b/src/components/Menu/index.web.tsx
@@ -110,7 +110,12 @@ const RadixTriggerPassThrough = React.forwardRef(
 )
 RadixTriggerPassThrough.displayName = 'RadixTriggerPassThrough'
 
-export function Trigger({children, label, role = 'button'}: TriggerProps) {
+export function Trigger({
+  children,
+  label,
+  role = 'button',
+  hint,
+}: TriggerProps) {
   const {control} = useMenuContext()
   const {
     state: hovered,
@@ -153,6 +158,7 @@ export function Trigger({children, label, role = 'button'}: TriggerProps) {
               onBlur: onBlur,
               onMouseEnter,
               onMouseLeave,
+              accessibilityHint: hint,
               accessibilityLabel: label,
               accessibilityRole: role,
             },
diff --git a/src/components/Menu/types.ts b/src/components/Menu/types.ts
index 44171d42cb..51baa24df5 100644
--- a/src/components/Menu/types.ts
+++ b/src/components/Menu/types.ts
@@ -19,6 +19,7 @@ export type ItemContextType = {
 }
 
 export type RadixPassThroughTriggerProps = {
+  ref: React.RefObject<any>
   id: string
   type: 'button'
   disabled: boolean
@@ -37,6 +38,7 @@ export type RadixPassThroughTriggerProps = {
 export type TriggerProps = {
   children(props: TriggerChildProps): React.ReactNode
   label: string
+  hint?: string
   role?: AccessibilityRole
 }
 export type TriggerChildProps =
@@ -59,11 +61,13 @@ export type TriggerChildProps =
        * object is empty.
        */
       props: {
+        ref: null
         onPress: () => void
         onFocus: () => void
         onBlur: () => void
         onPressIn: () => void
         onPressOut: () => void
+        accessibilityHint?: string
         accessibilityLabel: string
         accessibilityRole: AccessibilityRole
       }
@@ -85,6 +89,7 @@ export type TriggerChildProps =
         onBlur: () => void
         onMouseEnter: () => void
         onMouseLeave: () => void
+        accessibilityHint?: string
         accessibilityLabel: string
         accessibilityRole: AccessibilityRole
       }
diff --git a/src/components/RichText.tsx b/src/components/RichText.tsx
index 4edd9f88ee..7005d07428 100644
--- a/src/components/RichText.tsx
+++ b/src/components/RichText.tsx
@@ -1,19 +1,13 @@
 import React from 'react'
 import {TextStyle} from 'react-native'
 import {AppBskyRichtextFacet, RichText as RichTextAPI} from '@atproto/api'
-import {msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {useNavigation} from '@react-navigation/native'
 
-import {NavigationProp} from '#/lib/routes/types'
 import {toShortUrl} from '#/lib/strings/url-helpers'
-import {isNative} from '#/platform/detection'
-import {atoms as a, flatten, native, TextStyleProp, useTheme, web} from '#/alf'
+import {atoms as a, flatten, TextStyleProp} from '#/alf'
 import {isOnlyEmoji} from '#/alf/typography'
-import {useInteractionState} from '#/components/hooks/useInteractionState'
 import {InlineLinkText, LinkProps} from '#/components/Link'
 import {ProfileHoverCard} from '#/components/ProfileHoverCard'
-import {TagMenu, useTagMenuControl} from '#/components/TagMenu'
+import {RichTextTag} from '#/components/RichTextTag'
 import {Text, TextProps} from '#/components/Typography'
 
 const WORD_WRAP = {wordWrap: 1}
@@ -149,10 +143,9 @@ export function RichText({
       els.push(
         <RichTextTag
           key={key}
-          text={segment.text}
+          display={segment.text}
           tag={tag.tag}
-          style={interactiveStyles}
-          selectable={selectable}
+          textStyle={interactiveStyles}
           authorHandle={authorHandle}
         />,
       )
@@ -177,82 +170,3 @@ export function RichText({
     </Text>
   )
 }
-
-function RichTextTag({
-  text,
-  tag,
-  style,
-  selectable,
-  authorHandle,
-}: {
-  text: string
-  tag: string
-  selectable?: boolean
-  authorHandle?: string
-} & TextStyleProp) {
-  const t = useTheme()
-  const {_} = useLingui()
-  const control = useTagMenuControl()
-  const {
-    state: hovered,
-    onIn: onHoverIn,
-    onOut: onHoverOut,
-  } = useInteractionState()
-  const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
-  const navigation = useNavigation<NavigationProp>()
-
-  const navigateToPage = React.useCallback(() => {
-    navigation.push('Hashtag', {
-      tag: encodeURIComponent(tag),
-    })
-  }, [navigation, tag])
-
-  const openDialog = React.useCallback(() => {
-    control.open()
-  }, [control])
-
-  /*
-   * N.B. On web, this is wrapped in another pressable comopnent with a11y
-   * labels, etc. That's why only some of these props are applied here.
-   */
-
-  return (
-    <React.Fragment>
-      <TagMenu control={control} tag={tag} authorHandle={authorHandle}>
-        <Text
-          emoji
-          selectable={selectable}
-          {...native({
-            accessibilityLabel: _(msg`Hashtag: #${tag}`),
-            accessibilityHint: _(msg`Long press to open tag menu for #${tag}`),
-            accessibilityRole: isNative ? 'button' : undefined,
-            onPress: navigateToPage,
-            onLongPress: openDialog,
-          })}
-          {...web({
-            onMouseEnter: onHoverIn,
-            onMouseLeave: onHoverOut,
-          })}
-          // @ts-ignore
-          onFocus={onFocus}
-          onBlur={onBlur}
-          style={[
-            web({
-              cursor: 'pointer',
-            }),
-            {color: t.palette.primary_500},
-            (hovered || focused) && {
-              ...web({
-                outline: 0,
-                textDecorationLine: 'underline',
-                textDecorationColor: t.palette.primary_500,
-              }),
-            },
-            style,
-          ]}>
-          {text}
-        </Text>
-      </TagMenu>
-    </React.Fragment>
-  )
-}
diff --git a/src/components/RichTextTag.tsx b/src/components/RichTextTag.tsx
new file mode 100644
index 0000000000..562d44aa6f
--- /dev/null
+++ b/src/components/RichTextTag.tsx
@@ -0,0 +1,160 @@
+import React from 'react'
+import {StyleProp, Text as RNText, TextStyle} from 'react-native'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useNavigation} from '@react-navigation/native'
+
+import {NavigationProp} from '#/lib/routes/types'
+import {isInvalidHandle} from '#/lib/strings/handles'
+import {isNative, isWeb} from '#/platform/detection'
+import {
+  usePreferencesQuery,
+  useRemoveMutedWordsMutation,
+  useUpsertMutedWordsMutation,
+} from '#/state/queries/preferences'
+import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
+import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute'
+import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person'
+import {
+  createStaticClick,
+  createStaticClickIfUnmodified,
+  InlineLinkText,
+} from '#/components/Link'
+import {Loader} from '#/components/Loader'
+import * as Menu from '#/components/Menu'
+
+export function RichTextTag({
+  tag,
+  display,
+  authorHandle,
+  textStyle,
+}: {
+  tag: string
+  display: string
+  authorHandle?: string
+  textStyle: StyleProp<TextStyle>
+}) {
+  const {_} = useLingui()
+  const {isLoading: isPreferencesLoading, data: preferences} =
+    usePreferencesQuery()
+  const {
+    mutateAsync: upsertMutedWord,
+    variables: optimisticUpsert,
+    reset: resetUpsert,
+  } = useUpsertMutedWordsMutation()
+  const {
+    mutateAsync: removeMutedWords,
+    variables: optimisticRemove,
+    reset: resetRemove,
+  } = useRemoveMutedWordsMutation()
+  const navigation = useNavigation<NavigationProp>()
+  const label = _(msg`Hashtag ${tag}`)
+  const hint = isNative
+    ? _(msg`Long press to open tag menu for #${tag}`)
+    : _(msg`Click to open tag menu for ${tag}`)
+
+  const isMuted = Boolean(
+    (preferences?.moderationPrefs.mutedWords?.find(
+      m => m.value === tag && m.targets.includes('tag'),
+    ) ??
+      optimisticUpsert?.find(
+        m => m.value === tag && m.targets.includes('tag'),
+      )) &&
+      !optimisticRemove?.find(m => m?.value === tag),
+  )
+
+  /*
+   * Mute word records that exactly match the tag in question.
+   */
+  const removeableMuteWords = React.useMemo(() => {
+    return (
+      preferences?.moderationPrefs.mutedWords?.filter(word => {
+        return word.value === tag
+      }) || []
+    )
+  }, [tag, preferences?.moderationPrefs?.mutedWords])
+
+  return (
+    <Menu.Root>
+      <Menu.Trigger label={label} hint={hint}>
+        {({props: menuProps}) => (
+          <InlineLinkText
+            to={{
+              screen: 'Hashtag',
+              params: {tag: encodeURIComponent(tag)},
+            }}
+            {...menuProps}
+            onPress={e => {
+              if (isWeb) {
+                return createStaticClickIfUnmodified(() => {
+                  if (!isNative) {
+                    menuProps.onPress()
+                  }
+                }).onPress(e)
+              }
+            }}
+            onLongPress={createStaticClick(menuProps.onPress).onPress}
+            accessibilityHint={hint}
+            label={label}
+            style={textStyle}>
+            {isNative ? (
+              display
+            ) : (
+              <RNText ref={menuProps.ref}>{display}</RNText>
+            )}
+          </InlineLinkText>
+        )}
+      </Menu.Trigger>
+      <Menu.Outer>
+        <Menu.Group>
+          <Menu.Item
+            label={_(msg`See ${tag} posts`)}
+            onPress={() => {
+              navigation.push('Hashtag', {
+                tag: encodeURIComponent(tag),
+              })
+            }}>
+            <Menu.ItemText>
+              <Trans>See #{tag} posts</Trans>
+            </Menu.ItemText>
+            <Menu.ItemIcon icon={Search} />
+          </Menu.Item>
+          {authorHandle && !isInvalidHandle(authorHandle) && (
+            <Menu.Item
+              label={_(msg`See ${tag} posts by user`)}
+              onPress={() => {
+                navigation.push('Hashtag', {
+                  tag: encodeURIComponent(tag),
+                  author: authorHandle,
+                })
+              }}>
+              <Menu.ItemText>
+                <Trans>See #{tag} posts by user</Trans>
+              </Menu.ItemText>
+              <Menu.ItemIcon icon={Person} />
+            </Menu.Item>
+          )}
+        </Menu.Group>
+        <Menu.Divider />
+        <Menu.Item
+          label={isMuted ? _(msg`Unmute ${tag}`) : _(msg`Mute ${tag}`)}
+          onPress={() => {
+            if (isMuted) {
+              resetUpsert()
+              removeMutedWords(removeableMuteWords)
+            } else {
+              resetRemove()
+              upsertMutedWord([
+                {value: tag, targets: ['tag'], actorTarget: 'all'},
+              ])
+            }
+          }}>
+          <Menu.ItemText>
+            {isMuted ? _(msg`Unmute ${tag}`) : _(msg`Mute ${tag}`)}
+          </Menu.ItemText>
+          <Menu.ItemIcon icon={isPreferencesLoading ? Loader : Mute} />
+        </Menu.Item>
+      </Menu.Outer>
+    </Menu.Root>
+  )
+}
diff --git a/src/components/StarterPack/ProfileStarterPacks.tsx b/src/components/StarterPack/ProfileStarterPacks.tsx
index 64307070bf..d8925684b6 100644
--- a/src/components/StarterPack/ProfileStarterPacks.tsx
+++ b/src/components/StarterPack/ProfileStarterPacks.tsx
@@ -136,7 +136,6 @@ export const ProfileStarterPacks = React.forwardRef<
         headerOffset={headerOffset}
         progressViewOffset={ios(0)}
         contentContainerStyle={{paddingBottom: headerOffset + bottomBarOffset}}
-        indicatorStyle={t.name === 'light' ? 'black' : 'white'}
         removeClippedSubviews={true}
         desktopFixedHeight
         onEndReached={onEndReached}
diff --git a/src/components/TagMenu/index.tsx b/src/components/TagMenu/index.tsx
deleted file mode 100644
index 310ecc4c20..0000000000
--- a/src/components/TagMenu/index.tsx
+++ /dev/null
@@ -1,290 +0,0 @@
-import React from 'react'
-import {View} from 'react-native'
-import {msg, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {useNavigation} from '@react-navigation/native'
-
-import {NavigationProp} from '#/lib/routes/types'
-import {isInvalidHandle} from '#/lib/strings/handles'
-import {
-  usePreferencesQuery,
-  useRemoveMutedWordsMutation,
-  useUpsertMutedWordsMutation,
-} from '#/state/queries/preferences'
-import {atoms as a, native, useTheme} from '#/alf'
-import {Button, ButtonText} from '#/components/Button'
-import * as Dialog from '#/components/Dialog'
-import {Divider} from '#/components/Divider'
-import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
-import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute'
-import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person'
-import {createStaticClick, Link} from '#/components/Link'
-import {Loader} from '#/components/Loader'
-import {Text} from '#/components/Typography'
-
-export function useTagMenuControl() {
-  return Dialog.useDialogControl()
-}
-
-export function TagMenu({
-  children,
-  control,
-  tag,
-  authorHandle,
-}: React.PropsWithChildren<{
-  control: Dialog.DialogOuterProps['control']
-  /**
-   * This should be the sanitized tag value from the facet itself, not the
-   * "display" value with a leading `#`.
-   */
-  tag: string
-  authorHandle?: string
-}>) {
-  const navigation = useNavigation<NavigationProp>()
-  return (
-    <>
-      {children}
-      <Dialog.Outer control={control}>
-        <Dialog.Handle />
-        <TagMenuInner
-          control={control}
-          tag={tag}
-          authorHandle={authorHandle}
-          navigation={navigation}
-        />
-      </Dialog.Outer>
-    </>
-  )
-}
-
-function TagMenuInner({
-  control,
-  tag,
-  authorHandle,
-  navigation,
-}: {
-  control: Dialog.DialogOuterProps['control']
-  tag: string
-  authorHandle?: string
-  // Passed down because on native, we don't use real portals (and context would be wrong).
-  navigation: NavigationProp
-}) {
-  const {_} = useLingui()
-  const t = useTheme()
-  const {isLoading: isPreferencesLoading, data: preferences} =
-    usePreferencesQuery()
-  const {
-    mutateAsync: upsertMutedWord,
-    variables: optimisticUpsert,
-    reset: resetUpsert,
-  } = useUpsertMutedWordsMutation()
-  const {
-    mutateAsync: removeMutedWords,
-    variables: optimisticRemove,
-    reset: resetRemove,
-  } = useRemoveMutedWordsMutation()
-  const displayTag = '#' + tag
-
-  const isMuted = Boolean(
-    (preferences?.moderationPrefs.mutedWords?.find(
-      m => m.value === tag && m.targets.includes('tag'),
-    ) ??
-      optimisticUpsert?.find(
-        m => m.value === tag && m.targets.includes('tag'),
-      )) &&
-      !optimisticRemove?.find(m => m?.value === tag),
-  )
-
-  /*
-   * Mute word records that exactly match the tag in question.
-   */
-  const removeableMuteWords = React.useMemo(() => {
-    return (
-      preferences?.moderationPrefs.mutedWords?.filter(word => {
-        return word.value === tag
-      }) || []
-    )
-  }, [tag, preferences?.moderationPrefs?.mutedWords])
-
-  return (
-    <Dialog.Inner label={_(msg`Tag menu: ${displayTag}`)}>
-      {isPreferencesLoading ? (
-        <View style={[a.w_full, a.align_center]}>
-          <Loader size="lg" />
-        </View>
-      ) : (
-        <>
-          <View
-            style={[
-              a.rounded_md,
-              a.border,
-              a.mb_md,
-              t.atoms.border_contrast_low,
-              t.atoms.bg_contrast_25,
-            ]}>
-            <Link
-              label={_(msg`View all posts with tag ${displayTag}`)}
-              {...createStaticClick(() => {
-                control.close(() => {
-                  navigation.push('Hashtag', {
-                    tag: encodeURIComponent(tag),
-                  })
-                })
-              })}>
-              <View
-                style={[
-                  a.w_full,
-                  a.flex_row,
-                  a.align_center,
-                  a.justify_start,
-                  a.gap_md,
-                  a.px_lg,
-                  a.py_md,
-                ]}>
-                <Search size="lg" style={[t.atoms.text_contrast_medium]} />
-                <Text
-                  numberOfLines={1}
-                  ellipsizeMode="middle"
-                  style={[
-                    a.flex_1,
-                    a.text_md,
-                    a.font_bold,
-                    native({top: 2}),
-                    t.atoms.text_contrast_medium,
-                  ]}>
-                  <Trans>
-                    See{' '}
-                    <Text style={[a.text_md, a.font_bold, t.atoms.text]}>
-                      {displayTag}
-                    </Text>{' '}
-                    posts
-                  </Trans>
-                </Text>
-              </View>
-            </Link>
-
-            {authorHandle && !isInvalidHandle(authorHandle) && (
-              <>
-                <Divider />
-
-                <Link
-                  label={_(
-                    msg`View all posts by @${authorHandle} with tag ${displayTag}`,
-                  )}
-                  {...createStaticClick(() => {
-                    control.close(() => {
-                      navigation.push('Hashtag', {
-                        tag: encodeURIComponent(tag),
-                        author: authorHandle,
-                      })
-                    })
-                  })}>
-                  <View
-                    style={[
-                      a.w_full,
-                      a.flex_row,
-                      a.align_center,
-                      a.justify_start,
-                      a.gap_md,
-                      a.px_lg,
-                      a.py_md,
-                    ]}>
-                    <Person size="lg" style={[t.atoms.text_contrast_medium]} />
-                    <Text
-                      numberOfLines={1}
-                      ellipsizeMode="middle"
-                      style={[
-                        a.flex_1,
-                        a.text_md,
-                        a.font_bold,
-                        native({top: 2}),
-                        t.atoms.text_contrast_medium,
-                      ]}>
-                      <Trans>
-                        See{' '}
-                        <Text style={[a.text_md, a.font_bold, t.atoms.text]}>
-                          {displayTag}
-                        </Text>{' '}
-                        posts by this user
-                      </Trans>
-                    </Text>
-                  </View>
-                </Link>
-              </>
-            )}
-
-            {preferences ? (
-              <>
-                <Divider />
-
-                <Button
-                  label={
-                    isMuted
-                      ? _(msg`Unmute all ${displayTag} posts`)
-                      : _(msg`Mute all ${displayTag} posts`)
-                  }
-                  onPress={() => {
-                    control.close(() => {
-                      if (isMuted) {
-                        resetUpsert()
-                        removeMutedWords(removeableMuteWords)
-                      } else {
-                        resetRemove()
-                        upsertMutedWord([
-                          {
-                            value: tag,
-                            targets: ['tag'],
-                            actorTarget: 'all',
-                          },
-                        ])
-                      }
-                    })
-                  }}>
-                  <View
-                    style={[
-                      a.w_full,
-                      a.flex_row,
-                      a.align_center,
-                      a.justify_start,
-                      a.gap_md,
-                      a.px_lg,
-                      a.py_md,
-                    ]}>
-                    <Mute size="lg" style={[t.atoms.text_contrast_medium]} />
-                    <Text
-                      numberOfLines={1}
-                      ellipsizeMode="middle"
-                      style={[
-                        a.flex_1,
-                        a.text_md,
-                        a.font_bold,
-                        native({top: 2}),
-                        t.atoms.text_contrast_medium,
-                      ]}>
-                      {isMuted ? _(msg`Unmute`) : _(msg`Mute`)}{' '}
-                      <Text style={[a.text_md, a.font_bold, t.atoms.text]}>
-                        {displayTag}
-                      </Text>{' '}
-                      <Trans>posts</Trans>
-                    </Text>
-                  </View>
-                </Button>
-              </>
-            ) : null}
-          </View>
-
-          <Button
-            label={_(msg`Close this dialog`)}
-            size="small"
-            variant="ghost"
-            color="secondary"
-            onPress={() => control.close()}>
-            <ButtonText>
-              <Trans>Cancel</Trans>
-            </ButtonText>
-          </Button>
-        </>
-      )}
-    </Dialog.Inner>
-  )
-}
diff --git a/src/components/TagMenu/index.web.tsx b/src/components/TagMenu/index.web.tsx
deleted file mode 100644
index b6c306439a..0000000000
--- a/src/components/TagMenu/index.web.tsx
+++ /dev/null
@@ -1,163 +0,0 @@
-import React from 'react'
-import {msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {useNavigation} from '@react-navigation/native'
-
-import {NavigationProp} from '#/lib/routes/types'
-import {isInvalidHandle} from '#/lib/strings/handles'
-import {enforceLen} from '#/lib/strings/helpers'
-import {
-  usePreferencesQuery,
-  useRemoveMutedWordsMutation,
-  useUpsertMutedWordsMutation,
-} from '#/state/queries/preferences'
-import {EventStopper} from '#/view/com/util/EventStopper'
-import {NativeDropdown} from '#/view/com/util/forms/NativeDropdown'
-import {web} from '#/alf'
-import * as Dialog from '#/components/Dialog'
-
-export function useTagMenuControl(): Dialog.DialogControlProps {
-  return {
-    id: '',
-    // @ts-ignore
-    ref: null,
-    open: () => {
-      throw new Error(`TagMenu controls are only available on native platforms`)
-    },
-    close: () => {
-      throw new Error(`TagMenu controls are only available on native platforms`)
-    },
-  }
-}
-
-export function TagMenu({
-  children,
-  tag,
-  authorHandle,
-}: React.PropsWithChildren<{
-  /**
-   * This should be the sanitized tag value from the facet itself, not the
-   * "display" value with a leading `#`.
-   */
-  tag: string
-  authorHandle?: string
-}>) {
-  const {_} = useLingui()
-  const navigation = useNavigation<NavigationProp>()
-  const {data: preferences} = usePreferencesQuery()
-  const {mutateAsync: upsertMutedWord, variables: optimisticUpsert} =
-    useUpsertMutedWordsMutation()
-  const {mutateAsync: removeMutedWords, variables: optimisticRemove} =
-    useRemoveMutedWordsMutation()
-  const isMuted = Boolean(
-    (preferences?.moderationPrefs.mutedWords?.find(
-      m => m.value === tag && m.targets.includes('tag'),
-    ) ??
-      optimisticUpsert?.find(
-        m => m.value === tag && m.targets.includes('tag'),
-      )) &&
-      !optimisticRemove?.find(m => m?.value === tag),
-  )
-  const truncatedTag = '#' + enforceLen(tag, 15, true, 'middle')
-
-  /*
-   * Mute word records that exactly match the tag in question.
-   */
-  const removeableMuteWords = React.useMemo(() => {
-    return (
-      preferences?.moderationPrefs.mutedWords?.filter(word => {
-        return word.value === tag
-      }) || []
-    )
-  }, [tag, preferences?.moderationPrefs?.mutedWords])
-
-  const dropdownItems = React.useMemo(() => {
-    return [
-      {
-        label: _(msg`See ${truncatedTag} posts`),
-        onPress() {
-          navigation.push('Hashtag', {
-            tag: encodeURIComponent(tag),
-          })
-        },
-        testID: 'tagMenuSearch',
-        icon: {
-          ios: {
-            name: 'magnifyingglass',
-          },
-          android: '',
-          web: 'magnifying-glass',
-        },
-      },
-      authorHandle &&
-        !isInvalidHandle(authorHandle) && {
-          label: _(msg`See ${truncatedTag} posts by user`),
-          onPress() {
-            navigation.push('Hashtag', {
-              tag: encodeURIComponent(tag),
-              author: authorHandle,
-            })
-          },
-          testID: 'tagMenuSearchByUser',
-          icon: {
-            ios: {
-              name: 'magnifyingglass',
-            },
-            android: '',
-            web: ['far', 'user'],
-          },
-        },
-      preferences && {
-        label: 'separator',
-      },
-      preferences && {
-        label: isMuted
-          ? _(msg`Unmute ${truncatedTag}`)
-          : _(msg`Mute ${truncatedTag}`),
-        onPress() {
-          if (isMuted) {
-            removeMutedWords(removeableMuteWords)
-          } else {
-            upsertMutedWord([
-              {value: tag, targets: ['tag'], actorTarget: 'all'},
-            ])
-          }
-        },
-        testID: 'tagMenuMute',
-        icon: {
-          ios: {
-            name: 'speaker.slash',
-          },
-          android: 'ic_menu_sort_alphabetically',
-          web: isMuted ? 'eye' : ['far', 'eye-slash'],
-        },
-      },
-    ].filter(Boolean)
-  }, [
-    _,
-    authorHandle,
-    isMuted,
-    navigation,
-    preferences,
-    tag,
-    truncatedTag,
-    upsertMutedWord,
-    removeMutedWords,
-    removeableMuteWords,
-  ])
-
-  return (
-    <EventStopper>
-      <NativeDropdown
-        accessibilityLabel={_(msg`Click here to open tag menu for ${tag}`)}
-        accessibilityHint=""
-        // @ts-ignore
-        items={dropdownItems}
-        triggerStyle={web({
-          textAlign: 'left',
-        })}>
-        {children}
-      </NativeDropdown>
-    </EventStopper>
-  )
-}
diff --git a/src/lib/bitdrift.ts b/src/lib/bitdrift.ts
index f11da6f3b2..71493d0bcf 100644
--- a/src/lib/bitdrift.ts
+++ b/src/lib/bitdrift.ts
@@ -1,27 +1,23 @@
-// import {init} from '@bitdrift/react-native'
-// import {Statsig} from 'statsig-react-native-expo'
-// export {debug, error, info, warn} from '@bitdrift/react-native'
+import {init, SessionStrategy} from '@bitdrift/react-native'
+import {Statsig} from 'statsig-react-native-expo'
+export {debug, error, info, warn} from '@bitdrift/react-native'
 
-// import {initPromise} from './statsig/statsig'
+import {initPromise} from './statsig/statsig'
 
-// const BITDRIFT_API_KEY = process.env.BITDRIFT_API_KEY
+const BITDRIFT_API_KEY = process.env.BITDRIFT_API_KEY
 
-// initPromise.then(() => {
-//   let isEnabled = false
-//   try {
-//     if (Statsig.checkGate('enable_bitdrift')) {
-//       isEnabled = true
-//     }
-//   } catch (e) {
-//     // Statsig may complain about it being called too early.
-//   }
-//   if (isEnabled && BITDRIFT_API_KEY) {
-//     init(BITDRIFT_API_KEY, {url: 'https://api-bsky.bitdrift.io'})
-//   }
-// })
-
-// TODO: Reenable when the build issue is fixed.
-export function debug(_message: string) {}
-export function error(_message: string) {}
-export function info(_message: string) {}
-export function warn(_message: string) {}
+initPromise.then(() => {
+  let isEnabled = false
+  try {
+    if (Statsig.checkGate('enable_bitdrift')) {
+      isEnabled = true
+    }
+  } catch (e) {
+    // Statsig may complain about it being called too early.
+  }
+  if (isEnabled && BITDRIFT_API_KEY) {
+    init(BITDRIFT_API_KEY, SessionStrategy.Activity, {
+      url: 'https://api-bsky.bitdrift.io',
+    })
+  }
+})
diff --git a/src/lib/constants.ts b/src/lib/constants.ts
index 945e61c994..aa7ff29286 100644
--- a/src/lib/constants.ts
+++ b/src/lib/constants.ts
@@ -139,6 +139,11 @@ export const TIMELINE_SAVED_FEED = {
   value: 'following',
   pinned: true,
 }
+export const VIDEO_SAVED_FEED = {
+  type: 'feed',
+  value: VIDEO_FEED_URI,
+  pinned: true,
+}
 
 export const RECOMMENDED_SAVED_FEEDS: Pick<
   AppBskyActorDefs.SavedFeed,
diff --git a/src/lib/statsig/gates.ts b/src/lib/statsig/gates.ts
index d2caa47f2a..c0508c2dd0 100644
--- a/src/lib/statsig/gates.ts
+++ b/src/lib/statsig/gates.ts
@@ -3,6 +3,7 @@ export type Gate =
   | 'debug_show_feedcontext'
   | 'debug_subscriptions'
   | 'new_postonboarding'
+  | 'onboarding_add_video_feed'
   | 'remove_show_latest_button'
   | 'test_gate_1'
   | 'test_gate_2'
diff --git a/src/lib/statsig/statsig.tsx b/src/lib/statsig/statsig.tsx
index e59196f669..e0882806d5 100644
--- a/src/lib/statsig/statsig.tsx
+++ b/src/lib/statsig/statsig.tsx
@@ -5,8 +5,7 @@ import {sha256} from 'js-sha256'
 import {Statsig, StatsigProvider} from 'statsig-react-native-expo'
 
 import {BUNDLE_DATE, BUNDLE_IDENTIFIER, IS_TESTFLIGHT} from '#/lib/app-info'
-// TODO: Reenable when the build issue is fixed.
-// import * as bitdrift from '#/lib/bitdrift'
+import * as bitdrift from '#/lib/bitdrift'
 import {logger} from '#/logger'
 import {isWeb} from '#/platform/detection'
 import * as persisted from '#/state/persisted'
@@ -108,8 +107,7 @@ export function logEvent<E extends keyof LogEvents>(
     console.groupCollapsed(eventName)
     console.log(fullMetadata)
     console.groupEnd()
-    // TODO: Reenable when the build issue is fixed.
-    // bitdrift.info(eventName, fullMetadata)
+    bitdrift.info(eventName, fullMetadata)
   } catch (e) {
     // A log should never interrupt the calling code, whatever happens.
     logger.error('Failed to log an event', {message: e})
diff --git a/src/platform/urls.tsx b/src/platform/urls.tsx
index fd9d297aa2..514bde43e2 100644
--- a/src/platform/urls.tsx
+++ b/src/platform/urls.tsx
@@ -1,4 +1,4 @@
-import {GestureResponderEvent, Linking} from 'react-native'
+import {Linking} from 'react-native'
 
 import {isNative, isWeb} from './detection'
 
@@ -24,15 +24,3 @@ export function clearHash() {
     window.location.hash = ''
   }
 }
-
-export function shouldClickOpenNewTab(e: GestureResponderEvent) {
-  /**
-   * A `GestureResponderEvent`, but cast to `any` to avoid using a bunch
-   * of @ts-ignore below.
-   */
-  const event = e as any
-  const isMiddleClick = isWeb && event.button === 1
-  const isMetaKey =
-    isWeb && (event.metaKey || event.altKey || event.ctrlKey || event.shiftKey)
-  return isMetaKey || isMiddleClick
-}
diff --git a/src/screens/Onboarding/StepFinished.tsx b/src/screens/Onboarding/StepFinished.tsx
index fc0ea6a247..587ffa4f71 100644
--- a/src/screens/Onboarding/StepFinished.tsx
+++ b/src/screens/Onboarding/StepFinished.tsx
@@ -12,6 +12,7 @@ import {
   BSKY_APP_ACCOUNT_DID,
   DISCOVER_SAVED_FEED,
   TIMELINE_SAVED_FEED,
+  VIDEO_SAVED_FEED,
 } from '#/lib/constants'
 import {useRequestNotificationsPermission} from '#/lib/notifications/notifications'
 import {logEvent, useGate} from '#/lib/statsig/statsig'
@@ -111,6 +112,12 @@ export function StepFinished() {
               id: TID.nextStr(),
             },
           ]
+          if (gate('onboarding_add_video_feed')) {
+            feedsToSave.push({
+              ...VIDEO_SAVED_FEED,
+              id: TID.nextStr(),
+            })
+          }
 
           // Any starter pack feeds will be pinned _after_ the defaults
           if (starterPack && starterPack.feeds?.length) {
diff --git a/src/view/com/feeds/FeedSourceCard.tsx b/src/view/com/feeds/FeedSourceCard.tsx
index a591488891..b0b608f172 100644
--- a/src/view/com/feeds/FeedSourceCard.tsx
+++ b/src/view/com/feeds/FeedSourceCard.tsx
@@ -17,7 +17,6 @@ import {usePalette} from '#/lib/hooks/usePalette'
 import {sanitizeHandle} from '#/lib/strings/handles'
 import {s} from '#/lib/styles'
 import {logger} from '#/logger'
-import {shouldClickOpenNewTab} from '#/platform/urls'
 import {FeedSourceInfo, useFeedSourceInfoQuery} from '#/state/queries/feed'
 import {
   useAddSavedFeedsMutation,
@@ -29,6 +28,7 @@ import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
 import * as Toast from '#/view/com/util/Toast'
 import {useTheme} from '#/alf'
 import {atoms as a} from '#/alf'
+import {shouldClickOpenNewTab} from '#/components/Link'
 import * as Prompt from '#/components/Prompt'
 import {RichText} from '#/components/RichText'
 import {Text} from '../util/text/Text'
diff --git a/src/view/com/feeds/ProfileFeedgens.tsx b/src/view/com/feeds/ProfileFeedgens.tsx
index 64705ded8e..f5894f9ee7 100644
--- a/src/view/com/feeds/ProfileFeedgens.tsx
+++ b/src/view/com/feeds/ProfileFeedgens.tsx
@@ -205,9 +205,7 @@ export const ProfileFeedgens = React.forwardRef<
         headerOffset={headerOffset}
         progressViewOffset={ios(0)}
         contentContainerStyle={isMobile && {paddingBottom: headerOffset + 100}}
-        indicatorStyle={t.name === 'light' ? 'black' : 'white'}
         removeClippedSubviews={true}
-        // @ts-ignore our .web version only -prf
         desktopFixedHeight
         onEndReached={onEndReached}
       />
diff --git a/src/view/com/lists/ProfileLists.tsx b/src/view/com/lists/ProfileLists.tsx
index 2f63fd172b..d91a4fb669 100644
--- a/src/view/com/lists/ProfileLists.tsx
+++ b/src/view/com/lists/ProfileLists.tsx
@@ -203,9 +203,7 @@ export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>(
           contentContainerStyle={
             isMobile && {paddingBottom: headerOffset + 100}
           }
-          indicatorStyle={t.name === 'light' ? 'black' : 'white'}
           removeClippedSubviews={true}
-          // @ts-ignore our .web version only -prf
           desktopFixedHeight
           onEndReached={onEndReached}
         />
diff --git a/src/view/com/posts/PostFeed.tsx b/src/view/com/posts/PostFeed.tsx
index 888bba4bf4..4a68508354 100644
--- a/src/view/com/posts/PostFeed.tsx
+++ b/src/view/com/posts/PostFeed.tsx
@@ -19,7 +19,6 @@ import {isVideoView} from '#/lib/embeds'
 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
 import {logEvent} from '#/lib/statsig/statsig'
-import {useTheme} from '#/lib/ThemeContext'
 import {logger} from '#/logger'
 import {isIOS, isNative, isWeb} from '#/platform/detection'
 import {listenPostCreated} from '#/state/events'
@@ -188,7 +187,6 @@ let PostFeed = ({
   initialNumToRender?: number
   isVideoFeed?: boolean
 }): React.ReactNode => {
-  const theme = useTheme()
   const {_} = useLingui()
   const queryClient = useQueryClient()
   const {currentAccount, hasSession} = useSession()
@@ -716,7 +714,6 @@ let PostFeed = ({
           minHeight: Dimensions.get('window').height * 1.5,
         }}
         onScrolledDownChange={onScrolledDownChange}
-        indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'}
         onEndReached={onEndReached}
         onEndReachedThreshold={2} // number of posts left to trigger load more
         removeClippedSubviews={true}
diff --git a/src/view/com/util/List.tsx b/src/view/com/util/List.tsx
index 41ca5b5725..28f21d0392 100644
--- a/src/view/com/util/List.tsx
+++ b/src/view/com/util/List.tsx
@@ -164,6 +164,7 @@ let List = React.forwardRef<ListMethods, ListProps>(
           right: 1,
           ...props.scrollIndicatorInsets,
         }}
+        indicatorStyle={t.scheme === 'dark' ? 'white' : 'black'}
         contentOffset={contentOffset}
         refreshControl={refreshControl}
         onScroll={scrollHandler}
diff --git a/yarn.lock b/yarn.lock
index c212f8f07c..b462e66574 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3422,6 +3422,14 @@
   resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
   integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
 
+"@bitdrift/react-native@^0.6.2":
+  version "0.6.2"
+  resolved "https://registry.yarnpkg.com/@bitdrift/react-native/-/react-native-0.6.2.tgz#8e75d45a63fccad38b310fdea8069fa929cb97c3"
+  integrity sha512-4DIsZwAr9/Q1RI7lsnUphRoMuOuLWWESNXI759niSmU8XHTJISwwOQzUm7qWn7waBJGhxaq+jn+vlTV5Fai6zw==
+  dependencies:
+    "@expo/config-plugins" "^9.0.14"
+    fast-json-stringify "^6.0.0"
+
 "@braintree/sanitize-url@^6.0.2":
   version "6.0.4"
   resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz#923ca57e173c6b232bbbb07347b1be982f03e783"
@@ -3838,6 +3846,26 @@
     xcode "^3.0.1"
     xml2js "0.6.0"
 
+"@expo/config-plugins@^9.0.14":
+  version "9.0.14"
+  resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-9.0.14.tgz#c57cc86c238b276823ff66d96e4722366d57b12c"
+  integrity sha512-Lx1ebV95rTFKKQmbu4wMPLz65rKn7mqSpfANdCx+KwRxuLY2JQls8V4h3lQjG6dW8NWf9qV5QaEFAgNB6VMyOQ==
+  dependencies:
+    "@expo/config-types" "^52.0.3"
+    "@expo/json-file" "~9.0.1"
+    "@expo/plist" "^0.2.1"
+    "@expo/sdk-runtime-versions" "^1.0.0"
+    chalk "^4.1.2"
+    debug "^4.3.5"
+    getenv "^1.0.0"
+    glob "^10.4.2"
+    resolve-from "^5.0.0"
+    semver "^7.5.4"
+    slash "^3.0.0"
+    slugify "^1.6.6"
+    xcode "^3.0.1"
+    xml2js "0.6.0"
+
 "@expo/config-plugins@~9.0.12":
   version "9.0.12"
   resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-9.0.12.tgz#f122b2dca22e135eadf6e73442da3ced0ce8aa0a"
@@ -3863,6 +3891,11 @@
   resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-52.0.1.tgz#327af1b72a3a9d4556f41e083e0e284dd8198b96"
   integrity sha512-vD8ZetyKV7U29lR6+NJohYeoLYTH+eNYXJeNiSOrWCz0witJYY11meMmEnpEaVbN89EfC6uauSUOa6wihtbyPQ==
 
+"@expo/config-types@^52.0.3":
+  version "52.0.3"
+  resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-52.0.3.tgz#511f2f868172c93abeac7183beeb921dc72d6e1e"
+  integrity sha512-muxvuARmbysH5OGaiBRlh1Y6vfdmL56JtpXxB+y2Hfhu0ezG1U4FjZYBIacthckZPvnDCcP3xIu1R+eTo7/QFA==
+
 "@expo/config@~10.0.4":
   version "10.0.5"
   resolved "https://registry.yarnpkg.com/@expo/config/-/config-10.0.5.tgz#2de75e3f5d46a55f9f5140b73e0913265e6a41c6"
@@ -3986,6 +4019,15 @@
     json5 "^2.2.3"
     write-file-atomic "^2.3.0"
 
+"@expo/json-file@~9.0.1":
+  version "9.0.1"
+  resolved "https://registry.yarnpkg.com/@expo/json-file/-/json-file-9.0.1.tgz#ff60654caf1fa3c33f9b17dcd1e9691eb854a318"
+  integrity sha512-ZVPhbbEBEwafPCJ0+kI25O2Iivt3XKHEKAADCml1q2cmOIbQnKgLyn8DpOJXqWEyRQr/VWS+hflBh8DU2YFSqg==
+  dependencies:
+    "@babel/code-frame" "~7.10.4"
+    json5 "^2.2.3"
+    write-file-atomic "^2.3.0"
+
 "@expo/metro-config@0.19.8", "@expo/metro-config@~0.19.8":
   version "0.19.8"
   resolved "https://registry.yarnpkg.com/@expo/metro-config/-/metro-config-0.19.8.tgz#f1ea552b6fa5217093fe364ff5ca78a7e261a28b"
@@ -4045,6 +4087,15 @@
     base64-js "^1.2.3"
     xmlbuilder "^14.0.0"
 
+"@expo/plist@^0.2.1":
+  version "0.2.1"
+  resolved "https://registry.yarnpkg.com/@expo/plist/-/plist-0.2.1.tgz#a315e1964ee9eece5c56040d460db5de7af85889"
+  integrity sha512-9TaXGuNxa0LQwHQn4rYiU6YaERv6dPnQgsdKWq2rKKTr6LWOtGNQCi/yOk/HBLeZSxBm59APT5/6x60uRvr0Mg==
+  dependencies:
+    "@xmldom/xmldom" "~0.7.7"
+    base64-js "^1.2.3"
+    xmlbuilder "^14.0.0"
+
 "@expo/prebuild-config@^8.0.23":
   version "8.0.23"
   resolved "https://registry.yarnpkg.com/@expo/prebuild-config/-/prebuild-config-8.0.23.tgz#2ec6d5464f35d308bdb94ba75b7e6aba0ebb507d"
@@ -4140,6 +4191,13 @@
   resolved "https://registry.yarnpkg.com/@fastify/deepmerge/-/deepmerge-1.3.0.tgz#8116858108f0c7d9fd460d05a7d637a13fe3239a"
   integrity sha512-J8TOSBq3SoZbDhM9+R/u77hP93gz/rajSA+K2kGyijPpORPWUXHUpTaleoj+92As0S9uPRP7Oi8IqMf0u+ro6A==
 
+"@fastify/merge-json-schemas@^0.2.0":
+  version "0.2.1"
+  resolved "https://registry.yarnpkg.com/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz#3aa30d2f0c81a8ac5995b6d94ed4eaa2c3055824"
+  integrity sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==
+  dependencies:
+    dequal "^2.0.3"
+
 "@floating-ui/core@^0.7.3":
   version "0.7.3"
   resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-0.7.3.tgz#d274116678ffae87f6b60e90f88cc4083eefab86"
@@ -7542,6 +7600,13 @@ ajv-formats@^2.1.1:
   dependencies:
     ajv "^8.0.0"
 
+ajv-formats@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-3.0.1.tgz#3d5dc762bca17679c3c2ea7e90ad6b7532309578"
+  integrity sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==
+  dependencies:
+    ajv "^8.0.0"
+
 ajv-keywords@^3.5.2:
   version "3.5.2"
   resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d"
@@ -7584,6 +7649,16 @@ ajv@^8.0.0, ajv@^8.10.0, ajv@^8.11.0, ajv@^8.9.0:
     require-from-string "^2.0.2"
     uri-js "^4.2.2"
 
+ajv@^8.12.0:
+  version "8.17.1"
+  resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6"
+  integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==
+  dependencies:
+    fast-deep-equal "^3.1.3"
+    fast-uri "^3.0.1"
+    json-schema-traverse "^1.0.0"
+    require-from-string "^2.0.2"
+
 anser@^1.4.9:
   version "1.4.10"
   resolved "https://registry.yarnpkg.com/anser/-/anser-1.4.10.tgz#befa3eddf282684bd03b63dcda3927aef8c2e35b"
@@ -9542,6 +9617,11 @@ deprecated-react-native-prop-types@^5.0.0:
     invariant "^2.2.4"
     prop-types "^15.8.1"
 
+dequal@^2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
+  integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==
+
 destroy@1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015"
@@ -10856,6 +10936,18 @@ fast-json-stringify@^5.8.0:
     fast-uri "^2.1.0"
     rfdc "^1.2.0"
 
+fast-json-stringify@^6.0.0:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/fast-json-stringify/-/fast-json-stringify-6.0.1.tgz#82f1cb45fa96d0ca24b601f1738066976d6e2430"
+  integrity sha512-s7SJE83QKBZwg54dIbD5rCtzOBVD43V1ReWXXYqBgwCwHLYAAT0RQc/FmrQglXqWPpz6omtryJQOau5jI4Nrvg==
+  dependencies:
+    "@fastify/merge-json-schemas" "^0.2.0"
+    ajv "^8.12.0"
+    ajv-formats "^3.0.1"
+    fast-uri "^3.0.0"
+    json-schema-ref-resolver "^2.0.0"
+    rfdc "^1.2.0"
+
 fast-levenshtein@^2.0.6:
   version "2.0.6"
   resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
@@ -10888,6 +10980,11 @@ fast-uri@^2.1.0:
   resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-2.2.0.tgz#519a0f849bef714aad10e9753d69d8f758f7445a"
   integrity sha512-cIusKBIt/R/oI6z/1nyfe2FvGKVTohVRfvkOhvx0nCEW+xf5NoCXjAHcWp93uOUBchzYcsvPlrapAdX1uW+YGg==
 
+fast-uri@^3.0.0, fast-uri@^3.0.1:
+  version "3.0.6"
+  resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.6.tgz#88f130b77cfaea2378d56bf970dea21257a68748"
+  integrity sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==
+
 fast-xml-parser@4.2.5:
   version "4.2.5"
   resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.2.5.tgz#a6747a09296a6cb34f2ae634019bf1738f3b421f"
@@ -13189,6 +13286,13 @@ json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1:
   resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
   integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
 
+json-schema-ref-resolver@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/json-schema-ref-resolver/-/json-schema-ref-resolver-2.0.1.tgz#c92f16b452df069daac53e1984159e0f9af0598d"
+  integrity sha512-HG0SIB9X4J8bwbxCbnd5FfPEbcXAJYTi1pBJeP/QPON+w8ovSME8iRG+ElHNxZNX2Qh6eYn1GdzJFS4cDFfx0Q==
+  dependencies:
+    dequal "^2.0.3"
+
 json-schema-traverse@^0.4.1:
   version "0.4.1"
   resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"