about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/components/Button.tsx24
-rw-r--r--src/components/Dialog/context.ts6
-rw-r--r--src/components/Dialog/index.web.tsx26
-rw-r--r--src/components/Menu/index.web.tsx20
-rw-r--r--src/components/Prompt.tsx4
-rw-r--r--src/components/dialogs/Context.tsx54
-rw-r--r--src/components/dialogs/InAppBrowserConsent.tsx111
-rw-r--r--src/components/dms/EmojiReactionPicker.web.tsx4
-rw-r--r--src/components/icons/VerifiedCheck.tsx4
-rw-r--r--src/components/icons/VerifierCheck.tsx4
-rw-r--r--src/components/verification/VerificationCreatePrompt.tsx80
-rw-r--r--src/lib/hooks/useOpenLink.ts23
-rw-r--r--src/lib/media/manip.ts27
-rw-r--r--src/lib/media/manip.web.ts21
-rw-r--r--src/locale/locales/en/messages.po560
-rw-r--r--src/screens/Messages/components/MessageInput.tsx2
-rw-r--r--src/screens/Messages/components/MessageInput.web.tsx6
-rw-r--r--src/screens/Messages/components/MessagesList.tsx22
-rw-r--r--src/screens/Search/modules/ExploreTrendingTopics.tsx8
-rw-r--r--src/state/gallery.ts29
-rw-r--r--src/state/modals/index.tsx6
-rw-r--r--src/state/shell/composer/index.tsx8
-rw-r--r--src/view/com/composer/text-input/TextInput.web.tsx6
-rw-r--r--src/view/com/composer/text-input/web/EmojiPicker.tsx37
-rw-r--r--src/view/com/composer/text-input/web/EmojiPicker.web.tsx11
-rw-r--r--src/view/com/modals/InAppBrowserConsent.tsx99
-rw-r--r--src/view/com/modals/Modal.tsx4
-rw-r--r--src/view/com/util/PostMeta.tsx4
-rw-r--r--src/view/com/util/forms/NativeDropdown.web.tsx14
-rw-r--r--src/view/screens/ModerationBlockedAccounts.tsx186
-rw-r--r--src/view/screens/ModerationMutedAccounts.tsx182
-rw-r--r--src/view/shell/Composer.web.tsx22
-rw-r--r--src/view/shell/createNativeStackNavigatorWithAuth.tsx25
-rw-r--r--src/view/shell/desktop/LeftNav.tsx2
-rw-r--r--src/view/shell/index.tsx2
35 files changed, 876 insertions, 767 deletions
diff --git a/src/components/Button.tsx b/src/components/Button.tsx
index 123e6ee42..2d6ddc834 100644
--- a/src/components/Button.tsx
+++ b/src/components/Button.tsx
@@ -1,23 +1,23 @@
 import React from 'react'
 import {
-  AccessibilityProps,
-  GestureResponderEvent,
-  MouseEvent,
-  NativeSyntheticEvent,
+  type AccessibilityProps,
+  type GestureResponderEvent,
+  type MouseEvent,
+  type NativeSyntheticEvent,
   Pressable,
-  PressableProps,
-  StyleProp,
+  type PressableProps,
+  type StyleProp,
   StyleSheet,
-  TargetedEvent,
-  TextProps,
-  TextStyle,
+  type TargetedEvent,
+  type TextProps,
+  type TextStyle,
   View,
-  ViewStyle,
+  type ViewStyle,
 } from 'react-native'
 import {LinearGradient} from 'expo-linear-gradient'
 
 import {atoms as a, flatten, select, tokens, useTheme} from '#/alf'
-import {Props as SVGIconProps} from '#/components/icons/common'
+import {type Props as SVGIconProps} from '#/components/icons/common'
 import {Text} from '#/components/Typography'
 
 export type ButtonVariant = 'solid' | 'outline' | 'ghost' | 'gradient'
@@ -597,7 +597,7 @@ export function useSharedButtonTextStyles() {
       if (variant === 'solid' || variant === 'gradient') {
         if (!disabled) {
           baseStyles.push({
-            color: t.palette.contrast_100,
+            color: t.palette.contrast_50,
           })
         } else {
           baseStyles.push({
diff --git a/src/components/Dialog/context.ts b/src/components/Dialog/context.ts
index 331ff3f33..eb892403f 100644
--- a/src/components/Dialog/context.ts
+++ b/src/components/Dialog/context.ts
@@ -2,9 +2,9 @@ import React from 'react'
 
 import {useDialogStateContext} from '#/state/dialogs'
 import {
-  DialogContextProps,
-  DialogControlRefProps,
-  DialogOuterProps,
+  type DialogContextProps,
+  type DialogControlRefProps,
+  type DialogOuterProps,
 } from '#/components/Dialog/types'
 import {BottomSheetSnapPoint} from '../../../modules/bottom-sheet/src/BottomSheet.types'
 
diff --git a/src/components/Dialog/index.web.tsx b/src/components/Dialog/index.web.tsx
index 153954691..12bd8819b 100644
--- a/src/components/Dialog/index.web.tsx
+++ b/src/components/Dialog/index.web.tsx
@@ -1,17 +1,15 @@
 import React, {useImperativeHandle} from 'react'
 import {
   FlatList,
-  FlatListProps,
-  StyleProp,
+  type FlatListProps,
+  type StyleProp,
   TouchableWithoutFeedback,
   View,
-  ViewStyle,
+  type ViewStyle,
 } from 'react-native'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
-import {DismissableLayer} from '@radix-ui/react-dismissable-layer'
-import {useFocusGuards} from '@radix-ui/react-focus-guards'
-import {FocusScope} from '@radix-ui/react-focus-scope'
+import {DismissableLayer, FocusGuards, FocusScope} from 'radix-ui/internal'
 import {RemoveScrollBar} from 'react-remove-scroll-bar'
 
 import {logger} from '#/logger'
@@ -21,9 +19,9 @@ import {atoms as a, flatten, useBreakpoints, useTheme, web} from '#/alf'
 import {Button, ButtonIcon} from '#/components/Button'
 import {Context} from '#/components/Dialog/context'
 import {
-  DialogControlProps,
-  DialogInnerProps,
-  DialogOuterProps,
+  type DialogControlProps,
+  type DialogInnerProps,
+  type DialogOuterProps,
 } from '#/components/Dialog/types'
 import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
 import {Portal} from '#/components/Portal'
@@ -162,9 +160,9 @@ export function Inner({
   const {close} = React.useContext(Context)
   const {gtMobile} = useBreakpoints()
   const {reduceMotionEnabled} = useA11y()
-  useFocusGuards()
+  FocusGuards.useFocusGuards()
   return (
-    <FocusScope loop asChild trapped>
+    <FocusScope.FocusScope loop asChild trapped>
       <View
         role="dialog"
         aria-role="dialog"
@@ -191,7 +189,7 @@ export function Inner({
           !reduceMotionEnabled && a.zoom_fade_in,
           style,
         ])}>
-        <DismissableLayer
+        <DismissableLayer.DismissableLayer
           onInteractOutside={preventDefault}
           onFocusOutside={preventDefault}
           onDismiss={close}
@@ -200,9 +198,9 @@ export function Inner({
           <View style={[gtMobile ? a.p_2xl : a.p_xl, contentContainerStyle]}>
             {children}
           </View>
-        </DismissableLayer>
+        </DismissableLayer.DismissableLayer>
       </View>
-    </FocusScope>
+    </FocusScope.FocusScope>
   )
 }
 
diff --git a/src/components/Menu/index.web.tsx b/src/components/Menu/index.web.tsx
index ae021dcf1..27678bf2f 100644
--- a/src/components/Menu/index.web.tsx
+++ b/src/components/Menu/index.web.tsx
@@ -1,12 +1,12 @@
 import React from 'react'
-import {Pressable, StyleProp, View, ViewStyle} from 'react-native'
+import {Pressable, type StyleProp, View, type ViewStyle} from 'react-native'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
-import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
+import {DropdownMenu} from 'radix-ui'
 
 import {useA11y} from '#/state/a11y'
 import {atoms as a, flatten, useTheme, web} from '#/alf'
-import * as Dialog from '#/components/Dialog'
+import type * as Dialog from '#/components/Dialog'
 import {useInteractionState} from '#/components/hooks/useInteractionState'
 import {
   Context,
@@ -15,13 +15,13 @@ import {
   useMenuItemContext,
 } from '#/components/Menu/context'
 import {
-  ContextType,
-  GroupProps,
-  ItemIconProps,
-  ItemProps,
-  ItemTextProps,
-  RadixPassThroughTriggerProps,
-  TriggerProps,
+  type ContextType,
+  type GroupProps,
+  type ItemIconProps,
+  type ItemProps,
+  type ItemTextProps,
+  type RadixPassThroughTriggerProps,
+  type TriggerProps,
 } from '#/components/Menu/types'
 import {Portal} from '#/components/Portal'
 import {Text} from '#/components/Typography'
diff --git a/src/components/Prompt.tsx b/src/components/Prompt.tsx
index ed8c15f15..f21c98ff8 100644
--- a/src/components/Prompt.tsx
+++ b/src/components/Prompt.tsx
@@ -52,9 +52,7 @@ export function Outer({
         <Dialog.ScrollableInner
           accessibilityLabelledBy={titleId}
           accessibilityDescribedBy={descriptionId}
-          style={[
-            gtMobile ? {width: 'auto', maxWidth: 400, minWidth: 200} : a.w_full,
-          ]}>
+          style={[gtMobile ? {width: 400} : a.w_full]}>
           {children}
         </Dialog.ScrollableInner>
       </Context.Provider>
diff --git a/src/components/dialogs/Context.tsx b/src/components/dialogs/Context.tsx
index c9dff9a99..fda904b8b 100644
--- a/src/components/dialogs/Context.tsx
+++ b/src/components/dialogs/Context.tsx
@@ -1,32 +1,66 @@
-import React from 'react'
+import {createContext, useContext, useMemo, useState} from 'react'
 
 import * as Dialog from '#/components/Dialog'
 
-type Control = Dialog.DialogOuterProps['control']
+type Control = Dialog.DialogControlProps
+
+export type StatefulControl<T> = {
+  control: Control
+  open: (value: T) => void
+  clear: () => void
+  value: T | undefined
+}
 
 type ControlsContext = {
   mutedWordsDialogControl: Control
   signinDialogControl: Control
+  inAppBrowserConsentControl: StatefulControl<string>
 }
 
-const ControlsContext = React.createContext({
-  mutedWordsDialogControl: {} as Control,
-  signinDialogControl: {} as Control,
-})
+const ControlsContext = createContext<ControlsContext | null>(null)
 
 export function useGlobalDialogsControlContext() {
-  return React.useContext(ControlsContext)
+  const ctx = useContext(ControlsContext)
+  if (!ctx) {
+    throw new Error(
+      'useGlobalDialogsControlContext must be used within a Provider',
+    )
+  }
+  return ctx
 }
 
 export function Provider({children}: React.PropsWithChildren<{}>) {
   const mutedWordsDialogControl = Dialog.useDialogControl()
   const signinDialogControl = Dialog.useDialogControl()
-  const ctx = React.useMemo<ControlsContext>(
-    () => ({mutedWordsDialogControl, signinDialogControl}),
-    [mutedWordsDialogControl, signinDialogControl],
+  const inAppBrowserConsentControl = useStatefulDialogControl<string>()
+
+  const ctx = useMemo<ControlsContext>(
+    () => ({
+      mutedWordsDialogControl,
+      signinDialogControl,
+      inAppBrowserConsentControl,
+    }),
+    [mutedWordsDialogControl, signinDialogControl, inAppBrowserConsentControl],
   )
 
   return (
     <ControlsContext.Provider value={ctx}>{children}</ControlsContext.Provider>
   )
 }
+
+function useStatefulDialogControl<T>(initialValue?: T): StatefulControl<T> {
+  const [value, setValue] = useState(initialValue)
+  const control = Dialog.useDialogControl()
+  return useMemo(
+    () => ({
+      control,
+      open: (v: T) => {
+        setValue(v)
+        control.open()
+      },
+      clear: () => setValue(initialValue),
+      value,
+    }),
+    [control, value, initialValue],
+  )
+}
diff --git a/src/components/dialogs/InAppBrowserConsent.tsx b/src/components/dialogs/InAppBrowserConsent.tsx
new file mode 100644
index 000000000..4459c64db
--- /dev/null
+++ b/src/components/dialogs/InAppBrowserConsent.tsx
@@ -0,0 +1,111 @@
+import {useCallback} from 'react'
+import {View} from 'react-native'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {useOpenLink} from '#/lib/hooks/useOpenLink'
+import {isWeb} from '#/platform/detection'
+import {useSetInAppBrowser} from '#/state/preferences/in-app-browser'
+import {atoms as a, useTheme} from '#/alf'
+import {Button, ButtonIcon, ButtonText} from '#/components/Button'
+import * as Dialog from '#/components/Dialog'
+import {SquareArrowTopRight_Stroke2_Corner0_Rounded as External} from '#/components/icons/SquareArrowTopRight'
+import {Text} from '#/components/Typography'
+import {useGlobalDialogsControlContext} from './Context'
+
+export function InAppBrowserConsentDialog() {
+  const {inAppBrowserConsentControl} = useGlobalDialogsControlContext()
+
+  if (isWeb) return null
+
+  return (
+    <Dialog.Outer
+      control={inAppBrowserConsentControl.control}
+      nativeOptions={{preventExpansion: true}}
+      onClose={inAppBrowserConsentControl.clear}>
+      <Dialog.Handle />
+      <InAppBrowserConsentInner href={inAppBrowserConsentControl.value} />
+    </Dialog.Outer>
+  )
+}
+
+function InAppBrowserConsentInner({href}: {href?: string}) {
+  const control = Dialog.useDialogContext()
+  const {_} = useLingui()
+  const t = useTheme()
+  const setInAppBrowser = useSetInAppBrowser()
+  const openLink = useOpenLink()
+
+  const onUseIAB = useCallback(() => {
+    control.close(() => {
+      setInAppBrowser(true)
+      if (href) {
+        openLink(href, true)
+      }
+    })
+  }, [control, setInAppBrowser, href, openLink])
+
+  const onUseLinking = useCallback(() => {
+    control.close(() => {
+      setInAppBrowser(false)
+      if (href) {
+        openLink(href, false)
+      }
+    })
+  }, [control, setInAppBrowser, href, openLink])
+
+  const onCancel = useCallback(() => {
+    control.close()
+  }, [control])
+
+  return (
+    <Dialog.ScrollableInner label={_(msg`How should we open this link?`)}>
+      <View style={[a.gap_2xl]}>
+        <View style={[a.gap_sm]}>
+          <Text style={[a.font_heavy, a.text_2xl]}>
+            <Trans>How should we open this link?</Trans>
+          </Text>
+          <Text style={[t.atoms.text_contrast_high, a.leading_snug, a.text_md]}>
+            <Trans>
+              Your choice will be remembered for future links. You can change it
+              at any time in settings.
+            </Trans>
+          </Text>
+        </View>
+        <View style={[a.gap_sm]}>
+          <Button
+            label={_(msg`Use in-app browser`)}
+            onPress={onUseIAB}
+            size="large"
+            variant="solid"
+            color="primary">
+            <ButtonText>
+              <Trans>Use in-app browser</Trans>
+            </ButtonText>
+          </Button>
+          <Button
+            label={_(msg`Use my default browser`)}
+            onPress={onUseLinking}
+            size="large"
+            variant="solid"
+            color="secondary">
+            <ButtonText>
+              <Trans>Use my default browser</Trans>
+            </ButtonText>
+            <ButtonIcon position="right" icon={External} />
+          </Button>
+          <Button
+            label={_(msg`Cancel`)}
+            onPress={onCancel}
+            size="large"
+            variant="ghost"
+            color="secondary">
+            <ButtonText>
+              <Trans>Cancel</Trans>
+            </ButtonText>
+          </Button>
+        </View>
+      </View>
+    </Dialog.ScrollableInner>
+  )
+}
diff --git a/src/components/dms/EmojiReactionPicker.web.tsx b/src/components/dms/EmojiReactionPicker.web.tsx
index d9e1c87f7..cdd3ce414 100644
--- a/src/components/dms/EmojiReactionPicker.web.tsx
+++ b/src/components/dms/EmojiReactionPicker.web.tsx
@@ -4,10 +4,10 @@ import {type ChatBskyConvoDefs} from '@atproto/api'
 import EmojiPicker from '@emoji-mart/react'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
-import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
+import {DropdownMenu} from 'radix-ui'
 
 import {useSession} from '#/state/session'
-import {type Emoji} from '#/view/com/composer/text-input/web/EmojiPicker.web'
+import {type Emoji} from '#/view/com/composer/text-input/web/EmojiPicker'
 import {useWebPreloadEmoji} from '#/view/com/composer/text-input/web/useWebPreloadEmoji'
 import {atoms as a, flatten, useTheme} from '#/alf'
 import {DotGrid_Stroke2_Corner0_Rounded as DotGridIcon} from '#/components/icons/DotGrid'
diff --git a/src/components/icons/VerifiedCheck.tsx b/src/components/icons/VerifiedCheck.tsx
index 9299eb6e3..9d0aa9158 100644
--- a/src/components/icons/VerifiedCheck.tsx
+++ b/src/components/icons/VerifiedCheck.tsx
@@ -18,12 +18,12 @@ export const VerifiedCheck = React.forwardRef<Svg, Props>(function LogoImpl(
       width={size}
       height={size}
       style={[style]}>
-      <Circle cx="12" cy="12" r="12" fill={fill} />
+      <Circle cx="12" cy="12" r="11.5" fill={fill} />
       <Path
         fill="#fff"
         fillRule="evenodd"
         clipRule="evenodd"
-        d="M18.311 7.421a1.437 1.437 0 0 1 0 2.033l-6.571 6.571a1.437 1.437 0 0 1-2.033 0L6.42 12.74a1.438 1.438 0 0 1 2.033-2.033l2.27 2.269 5.554-5.555a1.437 1.437 0 0 1 2.033 0Z"
+        d="M17.659 8.175a1.361 1.361 0 0 1 0 1.925l-6.224 6.223a1.361 1.361 0 0 1-1.925 0L6.4 13.212a1.361 1.361 0 0 1 1.925-1.925l2.149 2.148 5.26-5.26a1.361 1.361 0 0 1 1.925 0Z"
       />
     </Svg>
   )
diff --git a/src/components/icons/VerifierCheck.tsx b/src/components/icons/VerifierCheck.tsx
index 7c3a0149d..143c24b97 100644
--- a/src/components/icons/VerifierCheck.tsx
+++ b/src/components/icons/VerifierCheck.tsx
@@ -22,13 +22,13 @@ export const VerifierCheck = React.forwardRef<Svg, Props>(function LogoImpl(
         fill={fill}
         fillRule="evenodd"
         clipRule="evenodd"
-        d="M8.792 1.54a4.11 4.11 0 0 1 6.416 0 4.128 4.128 0 0 0 3.146 1.54c2.616.04 4.544 2.5 4 5.1a4.277 4.277 0 0 0 .777 3.462c1.6 2.104.912 5.17-1.427 6.36a4.21 4.21 0 0 0-2.177 2.774c-.62 2.584-3.408 3.948-5.781 2.83a4.092 4.092 0 0 0-3.492 0c-2.373 1.118-5.16-.246-5.78-2.83a4.21 4.21 0 0 0-2.178-2.775c-2.34-1.19-3.028-4.256-1.427-6.36a4.277 4.277 0 0 0 .776-3.46c-.543-2.602 1.385-5.06 4.001-5.1a4.128 4.128 0 0 0 3.146-1.54Z"
+        d="M8.792 1.615a4.154 4.154 0 0 1 6.416 0 4.154 4.154 0 0 0 3.146 1.515 4.154 4.154 0 0 1 4 5.017 4.154 4.154 0 0 0 .777 3.404 4.154 4.154 0 0 1-1.427 6.255 4.153 4.153 0 0 0-2.177 2.73 4.154 4.154 0 0 1-5.781 2.784 4.154 4.154 0 0 0-3.492 0 4.154 4.154 0 0 1-5.78-2.784 4.154 4.154 0 0 0-2.178-2.73A4.154 4.154 0 0 1 .87 11.551a4.154 4.154 0 0 0 .776-3.404A4.154 4.154 0 0 1 5.646 3.13a4.154 4.154 0 0 0 3.146-1.515Z"
       />
       <Path
         fill="#fff"
         fillRule="evenodd"
         clipRule="evenodd"
-        d="M17.659 8.399a1.361 1.361 0 0 1 0 1.925l-6.224 6.223a1.361 1.361 0 0 1-1.925 0L6.4 13.435a1.361 1.361 0 1 1 1.925-1.925l2.149 2.15 5.26-5.261a1.361 1.361 0 0 1 1.925 0Z"
+        d="M17.861 8.26a1.438 1.438 0 0 1 0 2.033l-6.571 6.571a1.437 1.437 0 0 1-2.033 0L5.97 13.58a1.438 1.438 0 0 1 2.033-2.033l2.27 2.269 5.554-5.555a1.437 1.437 0 0 1 2.033 0Z"
       />
     </Svg>
   )
diff --git a/src/components/verification/VerificationCreatePrompt.tsx b/src/components/verification/VerificationCreatePrompt.tsx
index 39ac6dbf6..eb57d7c36 100644
--- a/src/components/verification/VerificationCreatePrompt.tsx
+++ b/src/components/verification/VerificationCreatePrompt.tsx
@@ -1,15 +1,19 @@
-import {useCallback} from 'react'
+import {useCallback, useState} from 'react'
 import {View} from 'react-native'
-import {msg} from '@lingui/macro'
+import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
 import {logger} from '#/logger'
 import {useModerationOpts} from '#/state/preferences/moderation-opts'
 import {useVerificationCreateMutation} from '#/state/queries/verification/useVerificationCreateMutation'
 import * as Toast from '#/view/com/util/Toast'
-import {atoms as a} from '#/alf'
+import {atoms as a, useBreakpoints} from '#/alf'
+import {Admonition} from '#/components/Admonition'
+import {Button, ButtonIcon, ButtonText} from '#/components/Button'
 import {type DialogControlProps} from '#/components/Dialog'
+import * as Dialog from '#/components/Dialog'
 import {VerifiedCheck} from '#/components/icons/VerifiedCheck'
+import {Loader} from '#/components/Loader'
 import * as ProfileCard from '#/components/ProfileCard'
 import * as Prompt from '#/components/Prompt'
 import type * as bsky from '#/types/bsky'
@@ -22,19 +26,22 @@ export function VerificationCreatePrompt({
   profile: bsky.profile.AnyProfileView
 }) {
   const {_} = useLingui()
+  const {gtMobile} = useBreakpoints()
   const moderationOpts = useModerationOpts()
-  const {mutateAsync: create} = useVerificationCreateMutation()
+  const {mutateAsync: create, isPending} = useVerificationCreateMutation()
+  const [error, setError] = useState(``)
   const onConfirm = useCallback(async () => {
     try {
       await create({profile})
       Toast.show(_(msg`Successfully verified`))
+      control.close()
     } catch (e) {
-      Toast.show(_(msg`Failed to create a verification`), 'xmark')
+      setError(_(msg`Verification failed, please try again.`))
       logger.error('Failed to create a verification', {
         safeMessage: e,
       })
     }
-  }, [_, profile, create])
+  }, [_, profile, create, control])
 
   return (
     <Prompt.Outer control={control}>
@@ -47,24 +54,51 @@ export function VerificationCreatePrompt({
       <Prompt.DescriptionText>
         {_(msg`This action can be undone at any time.`)}
       </Prompt.DescriptionText>
-      <View style={[a.pb_xl]}>
-        {moderationOpts ? (
-          <ProfileCard.Header>
-            <ProfileCard.Avatar
-              profile={profile}
-              moderationOpts={moderationOpts}
-            />
-            <ProfileCard.NameAndHandle
-              profile={profile}
-              moderationOpts={moderationOpts}
-            />
-          </ProfileCard.Header>
-        ) : null}
+
+      {moderationOpts ? (
+        <ProfileCard.Header>
+          <ProfileCard.Avatar
+            profile={profile}
+            moderationOpts={moderationOpts}
+          />
+          <ProfileCard.NameAndHandle
+            profile={profile}
+            moderationOpts={moderationOpts}
+          />
+        </ProfileCard.Header>
+      ) : null}
+
+      {error && (
+        <View style={[a.pt_lg]}>
+          <Admonition type="error">{error}</Admonition>
+        </View>
+      )}
+
+      <View style={[a.pt_xl]}>
+        {profile.displayName ? (
+          <Prompt.Actions>
+            <Button
+              variant="solid"
+              color="primary"
+              size={gtMobile ? 'small' : 'large'}
+              label={_(msg`Verify account`)}
+              onPress={onConfirm}>
+              <ButtonText>{_(msg`Verify account`)}</ButtonText>
+              {isPending && <ButtonIcon icon={Loader} />}
+            </Button>
+            <Prompt.Cancel />
+          </Prompt.Actions>
+        ) : (
+          <Admonition type="warning">
+            <Trans>
+              This user does not have a display name, and therefore cannot be
+              verified.
+            </Trans>
+          </Admonition>
+        )}
       </View>
-      <Prompt.Actions>
-        <Prompt.Action cta={_(msg`Verify account`)} onPress={onConfirm} />
-        <Prompt.Cancel />
-      </Prompt.Actions>
+
+      <Dialog.Close />
     </Prompt.Outer>
   )
 }
diff --git a/src/lib/hooks/useOpenLink.ts b/src/lib/hooks/useOpenLink.ts
index a949dacc6..28c1bca3d 100644
--- a/src/lib/hooks/useOpenLink.ts
+++ b/src/lib/hooks/useOpenLink.ts
@@ -12,16 +12,18 @@ import {
   toNiceDomain,
 } from '#/lib/strings/url-helpers'
 import {isNative} from '#/platform/detection'
-import {useModalControls} from '#/state/modals'
 import {useInAppBrowser} from '#/state/preferences/in-app-browser'
 import {useTheme} from '#/alf'
+import {useDialogContext} from '#/components/Dialog'
 import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper'
+import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
 
 export function useOpenLink() {
-  const {openModal} = useModalControls()
   const enabled = useInAppBrowser()
   const t = useTheme()
   const sheetWrapper = useSheetWrapper()
+  const dialogContext = useDialogContext()
+  const {inAppBrowserConsentControl} = useGlobalDialogsControlContext()
 
   const openLink = useCallback(
     async (url: string, override?: boolean, shouldProxy?: boolean) => {
@@ -42,10 +44,17 @@ export function useOpenLink() {
 
       if (isNative && !url.startsWith('mailto:')) {
         if (override === undefined && enabled === undefined) {
-          openModal({
-            name: 'in-app-browser-consent',
-            href: url,
-          })
+          // consent dialog is a global dialog, and while it's possible to nest dialogs,
+          // the actual components need to be nested. sibling dialogs on iOS are not supported.
+          // thus, check if we're in a dialog, and if so, close the existing dialog before opening the
+          // consent dialog -sfn
+          if (dialogContext.isWithinDialog) {
+            dialogContext.close(() => {
+              inAppBrowserConsentControl.open(url)
+            })
+          } else {
+            inAppBrowserConsentControl.open(url)
+          }
           return
         } else if (override ?? enabled) {
           await sheetWrapper(
@@ -62,7 +71,7 @@ export function useOpenLink() {
       }
       Linking.openURL(url)
     },
-    [enabled, openModal, t, sheetWrapper],
+    [enabled, inAppBrowserConsentControl, t, sheetWrapper, dialogContext],
   )
 
   return openLink
diff --git a/src/lib/media/manip.ts b/src/lib/media/manip.ts
index 7f052068d..f6ef8347d 100644
--- a/src/lib/media/manip.ts
+++ b/src/lib/media/manip.ts
@@ -178,15 +178,20 @@ async function doResize(localUri: string, opts: DoResizeOpts): Promise<Image> {
     height: imageRes.height,
   })
 
-  for (let i = 0; i < 9; i++) {
-    // nearest 10th
-    const quality = Math.round((1 - 0.1 * i) * 10) / 10
+  let minQualityPercentage = 0
+  let maxQualityPercentage = 101 // exclusive
+  let newDataUri
+
+  while (maxQualityPercentage - minQualityPercentage > 1) {
+    const qualityPercentage = Math.round(
+      (maxQualityPercentage + minQualityPercentage) / 2,
+    )
     const resizeRes = await manipulateAsync(
       localUri,
       [{resize: newDimensions}],
       {
         format: SaveFormat.JPEG,
-        compress: quality,
+        compress: qualityPercentage / 100,
       },
     )
 
@@ -198,8 +203,8 @@ async function doResize(localUri: string, opts: DoResizeOpts): Promise<Image> {
     }
 
     if (fileInfo.size < opts.maxSize) {
-      safeDeleteAsync(imageRes.uri)
-      return {
+      minQualityPercentage = qualityPercentage
+      newDataUri = {
         path: normalizePath(resizeRes.uri),
         mime: 'image/jpeg',
         size: fileInfo.size,
@@ -207,9 +212,17 @@ async function doResize(localUri: string, opts: DoResizeOpts): Promise<Image> {
         height: resizeRes.height,
       }
     } else {
-      safeDeleteAsync(resizeRes.uri)
+      maxQualityPercentage = qualityPercentage
     }
+
+    safeDeleteAsync(resizeRes.uri)
   }
+
+  if (newDataUri) {
+    safeDeleteAsync(imageRes.uri)
+    return newDataUri
+  }
+
   throw new Error(
     `This image is too big! We couldn't compress it down to ${opts.maxSize} bytes`,
   )
diff --git a/src/lib/media/manip.web.ts b/src/lib/media/manip.web.ts
index 4761f2fe0..ffef7314d 100644
--- a/src/lib/media/manip.web.ts
+++ b/src/lib/media/manip.web.ts
@@ -72,17 +72,28 @@ interface DoResizeOpts {
 async function doResize(dataUri: string, opts: DoResizeOpts): Promise<RNImage> {
   let newDataUri
 
-  for (let i = 0; i <= 10; i++) {
-    newDataUri = await createResizedImage(dataUri, {
+  let minQualityPercentage = 0
+  let maxQualityPercentage = 101 //exclusive
+
+  while (maxQualityPercentage - minQualityPercentage > 1) {
+    const qualityPercentage = Math.round(
+      (maxQualityPercentage + minQualityPercentage) / 2,
+    )
+    const tempDataUri = await createResizedImage(dataUri, {
       width: opts.width,
       height: opts.height,
-      quality: 1 - i * 0.1,
+      quality: qualityPercentage / 100,
       mode: opts.mode,
     })
-    if (getDataUriSize(newDataUri) < opts.maxSize) {
-      break
+
+    if (getDataUriSize(tempDataUri) < opts.maxSize) {
+      minQualityPercentage = qualityPercentage
+      newDataUri = tempDataUri
+    } else {
+      maxQualityPercentage = qualityPercentage
     }
   }
+
   if (!newDataUri) {
     throw new Error('Failed to compress image')
   }
diff --git a/src/locale/locales/en/messages.po b/src/locale/locales/en/messages.po
index d5c02a4cd..6ad7d50f3 100644
--- a/src/locale/locales/en/messages.po
+++ b/src/locale/locales/en/messages.po
@@ -22,8 +22,7 @@ msgstr ""
 msgid "(contains embedded content)"
 msgstr ""
 
-#: src/screens/Settings/AccountSettings.tsx:65
-#: src/view/com/modals/VerifyEmail.tsx:150
+#: src/screens/Settings/AccountSettings.tsx:67
 msgid "(no email)"
 msgstr ""
 
@@ -417,7 +416,7 @@ msgstr ""
 msgid "<0>You</0> and<1> </1><2>{0} </2>are included in your starter pack"
 msgstr ""
 
-#: src/screens/Profile/Header/Handle.tsx:52
+#: src/screens/Profile/Header/Handle.tsx:53
 msgid "⚠Invalid Handle"
 msgstr ""
 
@@ -446,10 +445,10 @@ msgstr ""
 msgid "A new form of verification"
 msgstr ""
 
-#: src/Navigation.tsx:400
+#: src/Navigation.tsx:401
 #: src/screens/Settings/AboutSettings.tsx:72
-#: src/screens/Settings/Settings.tsx:223
-#: src/screens/Settings/Settings.tsx:226
+#: src/screens/Settings/Settings.tsx:224
+#: src/screens/Settings/Settings.tsx:227
 msgid "About"
 msgstr ""
 
@@ -468,20 +467,20 @@ msgid "Accept Request"
 msgstr ""
 
 #: src/screens/Settings/AccessibilitySettings.tsx:46
-#: src/screens/Settings/Settings.tsx:199
-#: src/screens/Settings/Settings.tsx:202
+#: src/screens/Settings/Settings.tsx:200
+#: src/screens/Settings/Settings.tsx:203
 msgid "Accessibility"
 msgstr ""
 
-#: src/Navigation.tsx:352
+#: src/Navigation.tsx:353
 msgid "Accessibility Settings"
 msgstr ""
 
-#: src/Navigation.tsx:368
+#: src/Navigation.tsx:369
 #: src/screens/Login/LoginForm.tsx:194
-#: src/screens/Settings/AccountSettings.tsx:45
-#: src/screens/Settings/Settings.tsx:161
-#: src/screens/Settings/Settings.tsx:164
+#: src/screens/Settings/AccountSettings.tsx:47
+#: src/screens/Settings/Settings.tsx:162
+#: src/screens/Settings/Settings.tsx:165
 msgid "Account"
 msgstr ""
 
@@ -512,11 +511,11 @@ msgstr ""
 msgid "Account Muted by List"
 msgstr ""
 
-#: src/screens/Settings/Settings.tsx:480
+#: src/screens/Settings/Settings.tsx:499
 msgid "Account options"
 msgstr ""
 
-#: src/screens/Settings/Settings.tsx:516
+#: src/screens/Settings/Settings.tsx:535
 msgid "Account removed from quick access"
 msgstr ""
 
@@ -584,8 +583,8 @@ msgstr ""
 msgid "Add alt text (optional)"
 msgstr ""
 
-#: src/screens/Settings/Settings.tsx:423
-#: src/screens/Settings/Settings.tsx:426
+#: src/screens/Settings/Settings.tsx:442
+#: src/screens/Settings/Settings.tsx:445
 #: src/view/shell/desktop/LeftNav.tsx:281
 #: src/view/shell/desktop/LeftNav.tsx:285
 msgid "Add another account"
@@ -713,6 +712,10 @@ msgstr ""
 msgid "Advanced"
 msgstr ""
 
+#: src/components/dialogs/ChangeEmailDialog.tsx:143
+msgid "alice@example.com"
+msgstr ""
+
 #: src/view/screens/Notifications.tsx:86
 msgid "All"
 msgstr ""
@@ -792,15 +795,14 @@ msgid "Alt text will be truncated. {MAX_ALT_TEXT, plural, other {Limit: {0} char
 msgstr ""
 
 #: src/screens/Settings/components/DisableEmail2FADialog.tsx:93
-#: src/view/com/modals/VerifyEmail.tsx:132
 msgid "An email has been sent to {0}. It includes a confirmation code which you can enter below."
 msgstr ""
 
-#: src/view/com/modals/ChangeEmail.tsx:114
-msgid "An email has been sent to your previous address, {0}. It includes a confirmation code which you can enter below."
+#: src/components/dialogs/ChangeEmailDialog.tsx:59
+msgid "An email has been sent to your previous address, {currentEmail}. It includes a confirmation code which you can enter below."
 msgstr ""
 
-#: src/components/dialogs/VerifyEmailDialog.tsx:91
+#: src/components/dialogs/VerifyEmailDialog.tsx:120
 msgid "An email has been sent! Please enter the confirmation code included in the email below."
 msgstr ""
 
@@ -916,7 +918,7 @@ msgstr ""
 msgid "Anybody can interact"
 msgstr ""
 
-#: src/Navigation.tsx:408
+#: src/Navigation.tsx:409
 #: src/screens/Settings/AppIconSettings/index.tsx:67
 #: src/screens/Settings/AppIconSettings/SettingsListItem.tsx:18
 #: src/screens/Settings/AppIconSettings/SettingsListItem.tsx:23
@@ -953,7 +955,7 @@ msgstr ""
 msgid "App passwords"
 msgstr ""
 
-#: src/Navigation.tsx:320
+#: src/Navigation.tsx:321
 #: src/screens/Settings/AppPasswords.tsx:51
 msgid "App Passwords"
 msgstr ""
@@ -989,10 +991,10 @@ msgstr ""
 msgid "Appeal this decision"
 msgstr ""
 
-#: src/Navigation.tsx:360
+#: src/Navigation.tsx:361
 #: src/screens/Settings/AppearanceSettings.tsx:85
-#: src/screens/Settings/Settings.tsx:191
-#: src/screens/Settings/Settings.tsx:194
+#: src/screens/Settings/Settings.tsx:192
+#: src/screens/Settings/Settings.tsx:195
 msgid "Appearance"
 msgstr ""
 
@@ -1128,7 +1130,7 @@ msgid "Before you may message another user, you must first verify your email."
 msgstr ""
 
 #: src/components/dialogs/BirthDateSettings.tsx:103
-#: src/screens/Settings/AccountSettings.tsx:109
+#: src/screens/Settings/AccountSettings.tsx:111
 msgid "Birthday"
 msgstr ""
 
@@ -1195,8 +1197,8 @@ msgstr ""
 msgid "Blocked accounts"
 msgstr ""
 
-#: src/Navigation.tsx:161
-#: src/view/screens/ModerationBlockedAccounts.tsx:110
+#: src/Navigation.tsx:162
+#: src/view/screens/ModerationBlockedAccounts.tsx:104
 msgid "Blocked Accounts"
 msgstr ""
 
@@ -1205,7 +1207,7 @@ msgstr ""
 msgid "Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you."
 msgstr ""
 
-#: src/view/screens/ModerationBlockedAccounts.tsx:121
+#: src/view/screens/ModerationBlockedAccounts.tsx:190
 msgid "Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you. You will not see their content and they will be prevented from seeing yours."
 msgstr ""
 
@@ -1373,9 +1375,11 @@ msgstr ""
 msgid "Camera"
 msgstr ""
 
+#: src/components/dialogs/InAppBrowserConsent.tsx:98
+#: src/components/dialogs/InAppBrowserConsent.tsx:104
 #: src/components/Menu/index.tsx:306
+#: src/components/Prompt.tsx:138
 #: src/components/Prompt.tsx:140
-#: src/components/Prompt.tsx:142
 #: src/screens/Deactivated.tsx:158
 #: src/screens/Profile/Header/EditProfileDialog.tsx:227
 #: src/screens/Profile/Header/EditProfileDialog.tsx:235
@@ -1384,24 +1388,18 @@ msgstr ""
 #: src/screens/Settings/AppIconSettings/index.tsx:225
 #: src/screens/Settings/components/ChangeHandleDialog.tsx:78
 #: src/screens/Settings/components/ChangeHandleDialog.tsx:85
-#: src/screens/Settings/Settings.tsx:268
+#: src/screens/Settings/Settings.tsx:269
 #: src/screens/Takendown.tsx:99
 #: src/screens/Takendown.tsx:102
 #: src/view/com/composer/Composer.tsx:937
 #: src/view/com/composer/Composer.tsx:948
-#: src/view/com/modals/ChangeEmail.tsx:213
-#: src/view/com/modals/ChangeEmail.tsx:215
 #: src/view/com/modals/ChangePassword.tsx:279
 #: src/view/com/modals/ChangePassword.tsx:282
 #: src/view/com/modals/CreateOrEditList.tsx:335
 #: src/view/com/modals/CropImage.web.tsx:97
 #: src/view/com/modals/EditProfile.tsx:269
-#: src/view/com/modals/InAppBrowserConsent.tsx:75
-#: src/view/com/modals/InAppBrowserConsent.tsx:77
 #: src/view/com/modals/LinkWarning.tsx:105
 #: src/view/com/modals/LinkWarning.tsx:107
-#: src/view/com/modals/VerifyEmail.tsx:255
-#: src/view/com/modals/VerifyEmail.tsx:261
 #: src/view/com/util/post-ctrls/RepostButton.tsx:213
 #: src/view/shell/desktop/LeftNav.tsx:209
 msgid "Cancel"
@@ -1459,8 +1457,7 @@ msgstr ""
 msgid "Captions & alt text"
 msgstr ""
 
-#: src/screens/Settings/components/Email2FAToggle.tsx:60
-#: src/view/com/modals/VerifyEmail.tsx:160
+#: src/screens/Settings/components/Email2FAToggle.tsx:76
 msgid "Change"
 msgstr ""
 
@@ -1473,13 +1470,13 @@ msgstr ""
 msgid "Change app icon to \"{0}\""
 msgstr ""
 
-#: src/screens/Settings/AccountSettings.tsx:97
-#: src/screens/Settings/AccountSettings.tsx:101
+#: src/screens/Settings/AccountSettings.tsx:99
+#: src/screens/Settings/AccountSettings.tsx:103
 msgid "Change email"
 msgstr ""
 
-#: src/components/dialogs/VerifyEmailDialog.tsx:162
-#: src/components/dialogs/VerifyEmailDialog.tsx:187
+#: src/components/dialogs/VerifyEmailDialog.tsx:200
+#: src/components/dialogs/VerifyEmailDialog.tsx:225
 msgid "Change email address"
 msgstr ""
 
@@ -1492,10 +1489,6 @@ msgstr ""
 msgid "Change moderation service"
 msgstr ""
 
-#: src/view/com/modals/VerifyEmail.tsx:155
-msgid "Change my email"
-msgstr ""
-
 #: src/view/com/modals/ChangePassword.tsx:153
 msgid "Change Password"
 msgstr ""
@@ -1508,11 +1501,11 @@ msgstr ""
 msgid "Change report reason"
 msgstr ""
 
-#: src/view/com/modals/ChangeEmail.tsx:104
+#: src/components/dialogs/ChangeEmailDialog.tsx:53
 msgid "Change Your Email"
 msgstr ""
 
-#: src/components/dialogs/VerifyEmailDialog.tsx:171
+#: src/components/dialogs/VerifyEmailDialog.tsx:209
 msgid "Change your email address"
 msgstr ""
 
@@ -1525,7 +1518,7 @@ msgstr ""
 msgid "Changes hosting provider"
 msgstr ""
 
-#: src/Navigation.tsx:425
+#: src/Navigation.tsx:426
 #: src/view/shell/bottom-bar/BottomBar.tsx:205
 #: src/view/shell/desktop/LeftNav.tsx:535
 #: src/view/shell/Drawer.tsx:438
@@ -1543,7 +1536,7 @@ msgctxt "toast"
 msgid "Chat muted"
 msgstr ""
 
-#: src/Navigation.tsx:435
+#: src/Navigation.tsx:436
 #: src/screens/Messages/components/InboxPreview.tsx:24
 msgid "Chat request inbox"
 msgstr ""
@@ -1554,7 +1547,7 @@ msgid "Chat requests"
 msgstr ""
 
 #: src/components/dms/ConvoMenu.tsx:75
-#: src/Navigation.tsx:430
+#: src/Navigation.tsx:431
 #: src/screens/Messages/ChatList.tsx:328
 msgid "Chat settings"
 msgstr ""
@@ -1626,11 +1619,11 @@ msgstr ""
 msgid "Choose your username"
 msgstr ""
 
-#: src/screens/Settings/Settings.tsx:401
+#: src/screens/Settings/Settings.tsx:420
 msgid "Clear all storage data"
 msgstr ""
 
-#: src/screens/Settings/Settings.tsx:403
+#: src/screens/Settings/Settings.tsx:422
 msgid "Clear all storage data (restart after this)"
 msgstr ""
 
@@ -1671,11 +1664,14 @@ msgstr ""
 msgid "Clip 🐴 clop 🐴"
 msgstr ""
 
+#: src/components/dialogs/ChangeEmailDialog.tsx:244
+#: src/components/dialogs/ChangeEmailDialog.tsx:250
 #: src/components/dialogs/GifSelect.tsx:281
 #: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:174
 #: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:183
 #: src/components/dialogs/SearchablePeopleList.tsx:295
-#: src/components/dialogs/VerifyEmailDialog.tsx:289
+#: src/components/dialogs/VerifyEmailDialog.tsx:346
+#: src/components/dialogs/VerifyEmailDialog.tsx:352
 #: src/components/dms/EmojiPopup.android.tsx:58
 #: src/components/dms/ReportDialog.tsx:381
 #: src/components/dms/ReportDialog.tsx:390
@@ -1692,8 +1688,8 @@ msgstr ""
 msgid "Close"
 msgstr ""
 
-#: src/components/Dialog/index.web.tsx:113
-#: src/components/Dialog/index.web.tsx:261
+#: src/components/Dialog/index.web.tsx:111
+#: src/components/Dialog/index.web.tsx:259
 msgid "Close active dialog"
 msgstr ""
 
@@ -1715,8 +1711,8 @@ msgstr ""
 msgid "Close drawer menu"
 msgstr ""
 
-#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:137
-#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:173
+#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:136
+#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:172
 msgid "Close emoji picker"
 msgstr ""
 
@@ -1750,8 +1746,8 @@ msgstr ""
 msgid "Closes post composer and discards post draft"
 msgstr ""
 
-#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:138
-#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:174
+#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:137
+#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:173
 msgid "Closes the emoji picker"
 msgstr ""
 
@@ -1786,7 +1782,7 @@ msgstr ""
 msgid "Comics"
 msgstr ""
 
-#: src/Navigation.tsx:310
+#: src/Navigation.tsx:311
 #: src/view/screens/CommunityGuidelines.tsx:34
 msgid "Community Guidelines"
 msgstr ""
@@ -1823,23 +1819,17 @@ msgstr ""
 msgid "Configured in <0>moderation settings</0>."
 msgstr ""
 
-#: src/components/dialogs/VerifyEmailDialog.tsx:253
-#: src/components/dialogs/VerifyEmailDialog.tsx:260
-#: src/components/dialogs/VerifyEmailDialog.tsx:283
-#: src/components/Prompt.tsx:183
-#: src/components/Prompt.tsx:186
+#: src/components/dialogs/ChangeEmailDialog.tsx:203
+#: src/components/dialogs/ChangeEmailDialog.tsx:210
+#: src/components/dialogs/VerifyEmailDialog.tsx:316
+#: src/components/dialogs/VerifyEmailDialog.tsx:323
+#: src/components/Prompt.tsx:181
+#: src/components/Prompt.tsx:184
 #: src/screens/Settings/components/DisableEmail2FADialog.tsx:185
 #: src/screens/Settings/components/DisableEmail2FADialog.tsx:188
-#: src/view/com/modals/VerifyEmail.tsx:239
-#: src/view/com/modals/VerifyEmail.tsx:241
 msgid "Confirm"
 msgstr ""
 
-#: src/view/com/modals/ChangeEmail.tsx:188
-#: src/view/com/modals/ChangeEmail.tsx:190
-msgid "Confirm Change"
-msgstr ""
-
 #: src/view/com/modals/lang-settings/ConfirmLanguagesButton.tsx:34
 msgid "Confirm content language settings"
 msgstr ""
@@ -1856,18 +1846,18 @@ msgstr ""
 msgid "Confirm your birthdate"
 msgstr ""
 
-#: src/components/dialogs/VerifyEmailDialog.tsx:214
+#: src/components/dialogs/ChangeEmailDialog.tsx:160
+#: src/components/dialogs/ChangeEmailDialog.tsx:164
+#: src/components/dialogs/VerifyEmailDialog.tsx:252
 #: src/screens/Login/LoginForm.tsx:274
 #: src/screens/Settings/components/DisableEmail2FADialog.tsx:144
 #: src/screens/Settings/components/DisableEmail2FADialog.tsx:150
-#: src/view/com/modals/ChangeEmail.tsx:152
 #: src/view/com/modals/DeleteAccount.tsx:220
 #: src/view/com/modals/DeleteAccount.tsx:226
-#: src/view/com/modals/VerifyEmail.tsx:173
 msgid "Confirmation code"
 msgstr ""
 
-#: src/components/dialogs/VerifyEmailDialog.tsx:210
+#: src/components/dialogs/VerifyEmailDialog.tsx:248
 msgid "Confirmation Code"
 msgstr ""
 
@@ -1885,12 +1875,12 @@ msgid "Content & Media"
 msgstr ""
 
 #: src/screens/Settings/AccessibilitySettings.tsx:109
-#: src/screens/Settings/Settings.tsx:183
-#: src/screens/Settings/Settings.tsx:186
+#: src/screens/Settings/Settings.tsx:184
+#: src/screens/Settings/Settings.tsx:187
 msgid "Content and media"
 msgstr ""
 
-#: src/Navigation.tsx:384
+#: src/Navigation.tsx:385
 msgid "Content and Media"
 msgstr ""
 
@@ -2063,7 +2053,7 @@ msgstr ""
 msgid "Copy TXT record value"
 msgstr ""
 
-#: src/Navigation.tsx:315
+#: src/Navigation.tsx:316
 #: src/view/screens/CopyrightPolicy.tsx:31
 msgid "Copyright Policy"
 msgstr ""
@@ -2099,7 +2089,7 @@ msgstr ""
 
 #: src/components/StarterPack/ProfileStarterPacks.tsx:178
 #: src/components/StarterPack/ProfileStarterPacks.tsx:274
-#: src/Navigation.tsx:460
+#: src/Navigation.tsx:461
 msgid "Create a starter pack"
 msgstr ""
 
@@ -2204,13 +2194,13 @@ msgstr ""
 msgid "Date of birth"
 msgstr ""
 
-#: src/screens/Settings/AccountSettings.tsx:146
-#: src/screens/Settings/AccountSettings.tsx:151
+#: src/screens/Settings/AccountSettings.tsx:148
+#: src/screens/Settings/AccountSettings.tsx:153
 #: src/screens/Settings/components/DeactivateAccountDialog.tsx:73
 msgid "Deactivate account"
 msgstr ""
 
-#: src/screens/Settings/Settings.tsx:382
+#: src/screens/Settings/Settings.tsx:394
 msgid "Debug Moderation"
 msgstr ""
 
@@ -2237,8 +2227,8 @@ msgstr ""
 msgid "Delete"
 msgstr ""
 
-#: src/screens/Settings/AccountSettings.tsx:156
-#: src/screens/Settings/AccountSettings.tsx:161
+#: src/screens/Settings/AccountSettings.tsx:158
+#: src/screens/Settings/AccountSettings.tsx:163
 msgid "Delete account"
 msgstr ""
 
@@ -2259,7 +2249,7 @@ msgstr ""
 msgid "Delete chat"
 msgstr ""
 
-#: src/screens/Settings/Settings.tsx:389
+#: src/screens/Settings/Settings.tsx:401
 msgid "Delete chat declaration record"
 msgstr ""
 
@@ -2370,8 +2360,8 @@ msgctxt "toast"
 msgid "Developer mode enabled"
 msgstr ""
 
-#: src/screens/Settings/Settings.tsx:250
-#: src/screens/Settings/Settings.tsx:253
+#: src/screens/Settings/Settings.tsx:251
+#: src/screens/Settings/Settings.tsx:254
 msgid "Developer options"
 msgstr ""
 
@@ -2610,7 +2600,7 @@ msgstr ""
 msgid "Each code works once. You'll receive more invite codes periodically."
 msgstr ""
 
-#: src/screens/Settings/AccountSettings.tsx:112
+#: src/screens/Settings/AccountSettings.tsx:114
 #: src/screens/StarterPack/StarterPackScreen.tsx:583
 #: src/screens/StarterPack/Wizard/index.tsx:534
 #: src/screens/StarterPack/Wizard/index.tsx:541
@@ -2656,7 +2646,7 @@ msgstr ""
 msgid "Edit Moderation List"
 msgstr ""
 
-#: src/Navigation.tsx:325
+#: src/Navigation.tsx:326
 #: src/view/screens/Feeds.tsx:515
 msgid "Edit My Feeds"
 msgstr ""
@@ -2706,7 +2696,7 @@ msgstr ""
 msgid "Edit your profile description"
 msgstr ""
 
-#: src/Navigation.tsx:465
+#: src/Navigation.tsx:466
 msgid "Edit your starter pack"
 msgstr ""
 
@@ -2719,9 +2709,8 @@ msgstr ""
 msgid "Either the creator of this list has blocked you or you have blocked the creator."
 msgstr ""
 
-#: src/screens/Settings/AccountSettings.tsx:60
+#: src/screens/Settings/AccountSettings.tsx:62
 #: src/screens/Signup/StepInfo/index.tsx:193
-#: src/view/com/modals/ChangeEmail.tsx:136
 msgid "Email"
 msgstr ""
 
@@ -2742,19 +2731,8 @@ msgstr ""
 msgid "Email Resent"
 msgstr ""
 
-#: src/view/com/modals/ChangeEmail.tsx:54
-#: src/view/com/modals/ChangeEmail.tsx:83
-msgctxt "toast"
-msgid "Email updated"
-msgstr ""
-
-#: src/view/com/modals/ChangeEmail.tsx:106
-msgid "Email Updated"
-msgstr ""
-
-#: src/view/com/modals/VerifyEmail.tsx:85
-msgctxt "toast"
-msgid "Email verified"
+#: src/components/dialogs/ChangeEmailDialog.tsx:63
+msgid "Email Updated!"
 msgstr ""
 
 #: src/components/intents/VerifyEmailIntentDialog.tsx:79
@@ -2780,8 +2758,8 @@ msgstr ""
 msgid "Embedded video player"
 msgstr ""
 
-#: src/screens/Settings/components/Email2FAToggle.tsx:56
 #: src/screens/Settings/components/Email2FAToggle.tsx:60
+#: src/screens/Settings/components/Email2FAToggle.tsx:76
 msgid "Enable"
 msgstr ""
 
@@ -2793,7 +2771,7 @@ msgstr ""
 msgid "Enable adult content"
 msgstr ""
 
-#: src/screens/Settings/components/Email2FAToggle.tsx:53
+#: src/screens/Settings/components/Email2FAToggle.tsx:57
 msgid "Enable Email 2FA"
 msgstr ""
 
@@ -2855,14 +2833,10 @@ msgstr ""
 msgid "Enter a word or tag"
 msgstr ""
 
-#: src/components/dialogs/VerifyEmailDialog.tsx:89
+#: src/components/dialogs/VerifyEmailDialog.tsx:118
 msgid "Enter Code"
 msgstr ""
 
-#: src/view/com/modals/VerifyEmail.tsx:113
-msgid "Enter Confirmation Code"
-msgstr ""
-
 #: src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx:405
 msgid "Enter fullscreen"
 msgstr ""
@@ -2892,11 +2866,7 @@ msgstr ""
 msgid "Enter your email address"
 msgstr ""
 
-#: src/view/com/modals/ChangeEmail.tsx:42
-msgid "Enter your new email above"
-msgstr ""
-
-#: src/view/com/modals/ChangeEmail.tsx:112
+#: src/components/dialogs/ChangeEmailDialog.tsx:138
 msgid "Enter your new email address below."
 msgstr ""
 
@@ -3035,15 +3005,15 @@ msgstr ""
 msgid "Explicit sexual images."
 msgstr ""
 
-#: src/Navigation.tsx:649
+#: src/Navigation.tsx:650
 #: src/screens/Search/Shell.tsx:307
 #: src/view/shell/desktop/LeftNav.tsx:617
 #: src/view/shell/Drawer.tsx:386
 msgid "Explore"
 msgstr ""
 
-#: src/screens/Settings/AccountSettings.tsx:137
-#: src/screens/Settings/AccountSettings.tsx:141
+#: src/screens/Settings/AccountSettings.tsx:139
+#: src/screens/Settings/AccountSettings.tsx:143
 msgid "Export my data"
 msgstr ""
 
@@ -3066,7 +3036,7 @@ msgstr ""
 msgid "External media may allow websites to collect information about you and your device. No information is sent or requested until you press the \"play\" button."
 msgstr ""
 
-#: src/Navigation.tsx:344
+#: src/Navigation.tsx:345
 #: src/screens/Settings/ExternalMediaPreferences.tsx:31
 msgid "External Media Preferences"
 msgstr ""
@@ -3085,10 +3055,6 @@ msgstr ""
 msgid "Failed to change handle. Please try again."
 msgstr ""
 
-#: src/components/verification/VerificationCreatePrompt.tsx:32
-msgid "Failed to create a verification"
-msgstr ""
-
 #: src/screens/Settings/components/AddAppPasswordDialog.tsx:173
 msgid "Failed to create app password. Please try again."
 msgstr ""
@@ -3218,7 +3184,7 @@ msgstr ""
 msgid "Failed to verify handle. Please try again."
 msgstr ""
 
-#: src/Navigation.tsx:260
+#: src/Navigation.tsx:261
 msgid "Feed"
 msgstr ""
 
@@ -3247,7 +3213,7 @@ msgctxt "toast"
 msgid "Feedback sent!"
 msgstr ""
 
-#: src/Navigation.tsx:445
+#: src/Navigation.tsx:446
 #: src/screens/Search/SearchResults.tsx:68
 #: src/screens/StarterPack/StarterPackScreen.tsx:185
 #: src/view/screens/Feeds.tsx:508
@@ -3412,7 +3378,7 @@ msgstr ""
 msgid "Followed by <0>{0}</0>, <1>{1}</1>, and {2, plural, one {# other} other {# others}}"
 msgstr ""
 
-#: src/Navigation.tsx:214
+#: src/Navigation.tsx:215
 msgid "Followers of @{0} that you know"
 msgstr ""
 
@@ -3451,7 +3417,7 @@ msgstr ""
 msgid "Following feed preferences"
 msgstr ""
 
-#: src/Navigation.tsx:331
+#: src/Navigation.tsx:332
 #: src/screens/Settings/FollowingFeedPreferences.tsx:53
 msgid "Following Feed Preferences"
 msgstr ""
@@ -3536,9 +3502,9 @@ msgstr ""
 msgid "Get help"
 msgstr ""
 
-#: src/view/com/modals/VerifyEmail.tsx:197
-#: src/view/com/modals/VerifyEmail.tsx:199
-msgid "Get Started"
+#: src/components/dialogs/VerifyEmailDialog.tsx:263
+#: src/components/dialogs/VerifyEmailDialog.tsx:269
+msgid "Get started"
 msgstr ""
 
 #: src/components/ProgressGuide/List.tsx:35
@@ -3633,8 +3599,8 @@ msgstr ""
 msgid "Half way there!"
 msgstr ""
 
-#: src/screens/Settings/AccountSettings.tsx:126
-#: src/screens/Settings/AccountSettings.tsx:131
+#: src/screens/Settings/AccountSettings.tsx:128
+#: src/screens/Settings/AccountSettings.tsx:133
 msgid "Handle"
 msgstr ""
 
@@ -3660,7 +3626,7 @@ msgstr ""
 msgid "Harassment, trolling, or intolerance"
 msgstr ""
 
-#: src/Navigation.tsx:415
+#: src/Navigation.tsx:416
 msgid "Hashtag"
 msgstr ""
 
@@ -3672,8 +3638,8 @@ msgstr ""
 msgid "Having trouble?"
 msgstr ""
 
-#: src/screens/Settings/Settings.tsx:215
-#: src/screens/Settings/Settings.tsx:219
+#: src/screens/Settings/Settings.tsx:216
+#: src/screens/Settings/Settings.tsx:220
 #: src/view/shell/desktop/RightNav.tsx:120
 #: src/view/shell/desktop/RightNav.tsx:121
 #: src/view/shell/Drawer.tsx:353
@@ -3812,8 +3778,8 @@ msgstr ""
 msgid "Hold up! We’re gradually giving access to video, and you’re still waiting in line. Check back soon!"
 msgstr ""
 
-#: src/Navigation.tsx:644
-#: src/Navigation.tsx:664
+#: src/Navigation.tsx:645
+#: src/Navigation.tsx:665
 #: src/view/shell/bottom-bar/BottomBar.tsx:162
 #: src/view/shell/desktop/LeftNav.tsx:599
 #: src/view/shell/Drawer.tsx:412
@@ -3840,25 +3806,20 @@ msgstr ""
 msgid "Hot replies first"
 msgstr ""
 
-#: src/view/com/modals/InAppBrowserConsent.tsx:41
+#: src/components/dialogs/InAppBrowserConsent.tsx:62
+#: src/components/dialogs/InAppBrowserConsent.tsx:66
 msgid "How should we open this link?"
 msgstr ""
 
+#: src/components/dialogs/ChangeEmailDialog.tsx:189
+#: src/components/dialogs/ChangeEmailDialog.tsx:196
+#: src/components/dialogs/VerifyEmailDialog.tsx:302
+#: src/components/dialogs/VerifyEmailDialog.tsx:309
 #: src/screens/Settings/components/DisableEmail2FADialog.tsx:133
 #: src/screens/Settings/components/DisableEmail2FADialog.tsx:136
-#: src/view/com/modals/VerifyEmail.tsx:222
 msgid "I have a code"
 msgstr ""
 
-#: src/components/dialogs/VerifyEmailDialog.tsx:239
-#: src/components/dialogs/VerifyEmailDialog.tsx:246
-msgid "I Have a Code"
-msgstr ""
-
-#: src/view/com/modals/VerifyEmail.tsx:224
-msgid "I have a confirmation code"
-msgstr ""
-
 #: src/screens/Settings/components/ChangeHandleDialog.tsx:280
 #: src/screens/Settings/components/ChangeHandleDialog.tsx:286
 msgid "I have my own domain"
@@ -4095,13 +4056,13 @@ msgstr ""
 msgid "Language selection"
 msgstr ""
 
-#: src/Navigation.tsx:187
+#: src/Navigation.tsx:188
 msgid "Language Settings"
 msgstr ""
 
 #: src/screens/Settings/LanguageSettings.tsx:71
-#: src/screens/Settings/Settings.tsx:207
-#: src/screens/Settings/Settings.tsx:210
+#: src/screens/Settings/Settings.tsx:208
+#: src/screens/Settings/Settings.tsx:211
 msgid "Languages"
 msgstr ""
 
@@ -4246,8 +4207,8 @@ msgstr ""
 msgid "Like this labeler"
 msgstr ""
 
-#: src/Navigation.tsx:265
-#: src/Navigation.tsx:270
+#: src/Navigation.tsx:266
+#: src/Navigation.tsx:271
 msgid "Liked by"
 msgstr ""
 
@@ -4282,7 +4243,7 @@ msgstr ""
 msgid "Linear"
 msgstr ""
 
-#: src/Navigation.tsx:220
+#: src/Navigation.tsx:221
 msgid "List"
 msgstr ""
 
@@ -4340,7 +4301,7 @@ msgctxt "toast"
 msgid "List unmuted"
 msgstr ""
 
-#: src/Navigation.tsx:141
+#: src/Navigation.tsx:142
 #: src/view/screens/Lists.tsx:62
 #: src/view/screens/Profile.tsx:221
 #: src/view/screens/Profile.tsx:229
@@ -4376,7 +4337,7 @@ msgstr ""
 msgid "Loading..."
 msgstr ""
 
-#: src/Navigation.tsx:290
+#: src/Navigation.tsx:291
 msgid "Log"
 msgstr ""
 
@@ -4450,6 +4411,11 @@ msgstr ""
 msgid "Marked all as read"
 msgstr ""
 
+#: src/components/dialogs/VerifyEmailDialog.tsx:273
+#: src/components/dialogs/VerifyEmailDialog.tsx:281
+msgid "Maybe later"
+msgstr ""
+
 #: src/view/screens/Profile.tsx:224
 msgid "Media"
 msgstr ""
@@ -4508,7 +4474,7 @@ msgstr ""
 msgid "Message options"
 msgstr ""
 
-#: src/Navigation.tsx:659
+#: src/Navigation.tsx:660
 msgid "Messages"
 msgstr ""
 
@@ -4527,10 +4493,10 @@ msgstr ""
 msgid "Misleading Post"
 msgstr ""
 
-#: src/Navigation.tsx:146
+#: src/Navigation.tsx:147
 #: src/screens/Moderation/index.tsx:93
-#: src/screens/Settings/Settings.tsx:175
-#: src/screens/Settings/Settings.tsx:178
+#: src/screens/Settings/Settings.tsx:176
+#: src/screens/Settings/Settings.tsx:179
 msgid "Moderation"
 msgstr ""
 
@@ -4566,7 +4532,7 @@ msgstr ""
 msgid "Moderation lists"
 msgstr ""
 
-#: src/Navigation.tsx:151
+#: src/Navigation.tsx:152
 #: src/view/screens/ModerationModlists.tsx:62
 msgid "Moderation Lists"
 msgstr ""
@@ -4575,7 +4541,7 @@ msgstr ""
 msgid "moderation settings"
 msgstr ""
 
-#: src/Navigation.tsx:280
+#: src/Navigation.tsx:281
 msgid "Moderation states"
 msgstr ""
 
@@ -4698,12 +4664,12 @@ msgstr ""
 msgid "Muted accounts"
 msgstr ""
 
-#: src/Navigation.tsx:156
-#: src/view/screens/ModerationMutedAccounts.tsx:123
+#: src/Navigation.tsx:157
+#: src/view/screens/ModerationMutedAccounts.tsx:118
 msgid "Muted Accounts"
 msgstr ""
 
-#: src/view/screens/ModerationMutedAccounts.tsx:135
+#: src/view/screens/ModerationMutedAccounts.tsx:204
 msgid "Muted accounts have their posts removed from your feed and from your notifications. Mutes are completely private."
 msgstr ""
 
@@ -4771,7 +4737,7 @@ msgstr ""
 msgid "Navigates to your profile"
 msgstr ""
 
-#: src/components/dialogs/VerifyEmailDialog.tsx:196
+#: src/components/dialogs/VerifyEmailDialog.tsx:234
 msgid "Need to change it?"
 msgstr ""
 
@@ -4804,6 +4770,10 @@ msgstr ""
 msgid "New chat"
 msgstr ""
 
+#: src/components/dialogs/ChangeEmailDialog.tsx:142
+msgid "New email address"
+msgstr ""
+
 #: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:63
 msgid "New Feature"
 msgstr ""
@@ -5027,16 +4997,11 @@ msgstr ""
 msgid "Not followed by anyone you're following"
 msgstr ""
 
-#: src/Navigation.tsx:136
+#: src/Navigation.tsx:137
 #: src/view/screens/Profile.tsx:122
 msgid "Not Found"
 msgstr ""
 
-#: src/view/com/modals/VerifyEmail.tsx:254
-#: src/view/com/modals/VerifyEmail.tsx:260
-msgid "Not right now"
-msgstr ""
-
 #: src/view/com/profile/ProfileMenu.tsx:442
 #: src/view/com/util/forms/PostDropdownBtnMenuItems.tsx:791
 #: src/view/com/util/post-ctrls/PostCtrls.tsx:361
@@ -5055,7 +5020,7 @@ msgstr ""
 msgid "Notification filters"
 msgstr ""
 
-#: src/Navigation.tsx:440
+#: src/Navigation.tsx:441
 #: src/view/screens/Notifications.tsx:134
 msgid "Notification settings"
 msgstr ""
@@ -5072,7 +5037,7 @@ msgstr ""
 msgid "Notification Sounds"
 msgstr ""
 
-#: src/Navigation.tsx:654
+#: src/Navigation.tsx:655
 #: src/view/screens/Notifications.tsx:128
 #: src/view/shell/bottom-bar/BottomBar.tsx:236
 #: src/view/shell/desktop/LeftNav.tsx:636
@@ -5135,7 +5100,7 @@ msgstr ""
 msgid "on<0><1/><2><3/></2></0>"
 msgstr ""
 
-#: src/screens/Settings/Settings.tsx:354
+#: src/screens/Settings/Settings.tsx:355
 msgid "Onboarding reset"
 msgstr ""
 
@@ -5232,7 +5197,7 @@ msgstr ""
 msgid "Open message options"
 msgstr ""
 
-#: src/screens/Settings/Settings.tsx:380
+#: src/screens/Settings/Settings.tsx:392
 msgid "Open moderation debug page"
 msgstr ""
 
@@ -5252,12 +5217,12 @@ msgstr ""
 msgid "Open starter pack menu"
 msgstr ""
 
-#: src/screens/Settings/Settings.tsx:373
-#: src/screens/Settings/Settings.tsx:387
+#: src/screens/Settings/Settings.tsx:385
+#: src/screens/Settings/Settings.tsx:399
 msgid "Open storybook page"
 msgstr ""
 
-#: src/screens/Settings/Settings.tsx:366
+#: src/screens/Settings/Settings.tsx:378
 msgid "Open system log"
 msgstr ""
 
@@ -5289,7 +5254,7 @@ msgstr ""
 msgid "Opens captions and alt text dialog"
 msgstr ""
 
-#: src/screens/Settings/AccountSettings.tsx:127
+#: src/screens/Settings/AccountSettings.tsx:129
 msgid "Opens change handle dialog"
 msgstr ""
 
@@ -5319,7 +5284,7 @@ msgstr ""
 msgid "Opens GIF select dialog"
 msgstr ""
 
-#: src/screens/Settings/Settings.tsx:216
+#: src/screens/Settings/Settings.tsx:217
 msgid "Opens helpdesk in browser"
 msgstr ""
 
@@ -5413,8 +5378,8 @@ msgid "Page Not Found"
 msgstr ""
 
 #: src/screens/Login/LoginForm.tsx:228
-#: src/screens/Settings/AccountSettings.tsx:117
-#: src/screens/Settings/AccountSettings.tsx:121
+#: src/screens/Settings/AccountSettings.tsx:119
+#: src/screens/Settings/AccountSettings.tsx:123
 #: src/screens/Signup/StepInfo/index.tsx:228
 #: src/view/com/modals/DeleteAccount.tsx:239
 #: src/view/com/modals/DeleteAccount.tsx:246
@@ -5453,11 +5418,11 @@ msgstr ""
 msgid "People"
 msgstr ""
 
-#: src/Navigation.tsx:207
+#: src/Navigation.tsx:208
 msgid "People followed by @{0}"
 msgstr ""
 
-#: src/Navigation.tsx:200
+#: src/Navigation.tsx:201
 msgid "People following @{0}"
 msgstr ""
 
@@ -5577,10 +5542,6 @@ msgstr ""
 msgid "Please complete the verification captcha."
 msgstr ""
 
-#: src/view/com/modals/ChangeEmail.tsx:65
-msgid "Please confirm your email before changing it. This is a temporary requirement while email-updating tools are added, and it will soon be removed."
-msgstr ""
-
 #: src/screens/Signup/StepInfo/index.tsx:109
 msgid "Please double-check that you have entered your email address correctly."
 msgstr ""
@@ -5643,7 +5604,7 @@ msgstr ""
 msgid "Please sign in as @{0}"
 msgstr ""
 
-#: src/view/com/modals/VerifyEmail.tsx:109
+#: src/components/dialogs/VerifyEmailDialog.tsx:108
 msgid "Please Verify Your Email"
 msgstr ""
 
@@ -5676,10 +5637,10 @@ msgstr ""
 msgid "Post by {0}"
 msgstr ""
 
-#: src/Navigation.tsx:233
-#: src/Navigation.tsx:240
-#: src/Navigation.tsx:247
-#: src/Navigation.tsx:254
+#: src/Navigation.tsx:234
+#: src/Navigation.tsx:241
+#: src/Navigation.tsx:248
+#: src/Navigation.tsx:255
 msgid "Post by @{0}"
 msgstr ""
 
@@ -5714,7 +5675,7 @@ msgstr ""
 msgid "Post interaction settings"
 msgstr ""
 
-#: src/Navigation.tsx:167
+#: src/Navigation.tsx:168
 #: src/screens/ModerationInteractionSettings/index.tsx:34
 msgid "Post Interaction Settings"
 msgstr ""
@@ -5802,17 +5763,17 @@ msgstr ""
 msgid "Privacy"
 msgstr ""
 
-#: src/screens/Settings/Settings.tsx:169
-#: src/screens/Settings/Settings.tsx:172
+#: src/screens/Settings/Settings.tsx:170
+#: src/screens/Settings/Settings.tsx:173
 msgid "Privacy and security"
 msgstr ""
 
-#: src/Navigation.tsx:376
+#: src/Navigation.tsx:377
 #: src/screens/Settings/PrivacyAndSecuritySettings.tsx:36
 msgid "Privacy and Security"
 msgstr ""
 
-#: src/Navigation.tsx:300
+#: src/Navigation.tsx:301
 #: src/screens/Settings/AboutSettings.tsx:89
 #: src/screens/Settings/AboutSettings.tsx:92
 #: src/view/screens/PrivacyPolicy.tsx:31
@@ -6017,7 +5978,7 @@ msgstr ""
 #: src/components/FeedCard.tsx:343
 #: src/components/StarterPack/Wizard/WizardListCard.tsx:102
 #: src/components/StarterPack/Wizard/WizardListCard.tsx:109
-#: src/screens/Settings/Settings.tsx:518
+#: src/screens/Settings/Settings.tsx:537
 #: src/view/com/feeds/FeedSourceCard.tsx:322
 #: src/view/com/modals/UserAddRemoveLists.tsx:235
 #: src/view/com/posts/PostFeedErrorMessage.tsx:213
@@ -6032,8 +5993,8 @@ msgstr ""
 msgid "Remove {historyItem}"
 msgstr ""
 
-#: src/screens/Settings/Settings.tsx:497
-#: src/screens/Settings/Settings.tsx:500
+#: src/screens/Settings/Settings.tsx:516
+#: src/screens/Settings/Settings.tsx:519
 msgid "Remove account"
 msgstr ""
 
@@ -6074,7 +6035,7 @@ msgstr ""
 msgid "Remove from my feeds"
 msgstr ""
 
-#: src/screens/Settings/Settings.tsx:510
+#: src/screens/Settings/Settings.tsx:529
 msgid "Remove from quick access?"
 msgstr ""
 
@@ -6377,9 +6338,9 @@ msgstr ""
 msgid "Reposts of this post"
 msgstr ""
 
-#: src/view/com/modals/ChangeEmail.tsx:176
-#: src/view/com/modals/ChangeEmail.tsx:178
-msgid "Request Change"
+#: src/components/dialogs/ChangeEmailDialog.tsx:175
+#: src/components/dialogs/ChangeEmailDialog.tsx:182
+msgid "Request change"
 msgstr ""
 
 #: src/view/com/modals/ChangePassword.tsx:253
@@ -6392,7 +6353,7 @@ msgstr ""
 msgid "Require alt text before posting"
 msgstr ""
 
-#: src/screens/Settings/components/Email2FAToggle.tsx:54
+#: src/screens/Settings/components/Email2FAToggle.tsx:58
 msgid "Require an email code to sign in to your account."
 msgstr ""
 
@@ -6404,13 +6365,15 @@ msgstr ""
 msgid "Required in your region"
 msgstr ""
 
+#: src/components/dialogs/ChangeEmailDialog.tsx:217
+#: src/components/dialogs/ChangeEmailDialog.tsx:227
+#: src/components/dialogs/VerifyEmailDialog.tsx:330
+#: src/components/dialogs/VerifyEmailDialog.tsx:340
 #: src/screens/Settings/components/DisableEmail2FADialog.tsx:173
 #: src/screens/Settings/components/DisableEmail2FADialog.tsx:176
 msgid "Resend email"
 msgstr ""
 
-#: src/components/dialogs/VerifyEmailDialog.tsx:267
-#: src/components/dialogs/VerifyEmailDialog.tsx:277
 #: src/components/intents/VerifyEmailIntentDialog.tsx:130
 msgid "Resend Email"
 msgstr ""
@@ -6427,8 +6390,8 @@ msgstr ""
 msgid "Reset Code"
 msgstr ""
 
-#: src/screens/Settings/Settings.tsx:394
-#: src/screens/Settings/Settings.tsx:396
+#: src/screens/Settings/Settings.tsx:406
+#: src/screens/Settings/Settings.tsx:408
 msgid "Reset onboarding state"
 msgstr ""
 
@@ -6597,7 +6560,7 @@ msgstr ""
 msgid "Search"
 msgstr ""
 
-#: src/Navigation.tsx:226
+#: src/Navigation.tsx:227
 #: src/screens/Profile/ProfileSearch.tsx:37
 msgid "Search @{0}'s posts"
 msgstr ""
@@ -6670,7 +6633,7 @@ msgstr ""
 msgid "Searches for profiles"
 msgstr ""
 
-#: src/view/com/modals/ChangeEmail.tsx:105
+#: src/components/dialogs/ChangeEmailDialog.tsx:57
 msgid "Security Step Required"
 msgstr ""
 
@@ -6804,19 +6767,14 @@ msgstr ""
 msgid "Send a neat website!"
 msgstr ""
 
-#: src/components/dialogs/VerifyEmailDialog.tsx:232
-msgid "Send Confirmation"
+#: src/components/dialogs/VerifyEmailDialog.tsx:295
+msgid "Send confirmation"
 msgstr ""
 
-#: src/components/dialogs/VerifyEmailDialog.tsx:225
+#: src/components/dialogs/VerifyEmailDialog.tsx:288
 msgid "Send confirmation email"
 msgstr ""
 
-#: src/view/com/modals/VerifyEmail.tsx:210
-#: src/view/com/modals/VerifyEmail.tsx:212
-msgid "Send Confirmation Email"
-msgstr ""
-
 #: src/view/com/modals/DeleteAccount.tsx:145
 msgid "Send email"
 msgstr ""
@@ -6892,8 +6850,8 @@ msgstr ""
 msgid "Sets email for password reset"
 msgstr ""
 
-#: src/Navigation.tsx:182
-#: src/screens/Settings/Settings.tsx:88
+#: src/Navigation.tsx:183
+#: src/screens/Settings/Settings.tsx:89
 #: src/view/shell/desktop/LeftNav.tsx:709
 #: src/view/shell/Drawer.tsx:555
 msgid "Settings"
@@ -6981,7 +6939,7 @@ msgstr ""
 msgid "Share your favorite feed!"
 msgstr ""
 
-#: src/Navigation.tsx:285
+#: src/Navigation.tsx:286
 msgid "Shared Preferences Tester"
 msgstr ""
 
@@ -7097,7 +7055,7 @@ msgstr ""
 msgid "Shows information about when this post was created"
 msgstr ""
 
-#: src/screens/Settings/Settings.tsx:112
+#: src/screens/Settings/Settings.tsx:113
 msgid "Shows other accounts you can switch to"
 msgstr ""
 
@@ -7149,9 +7107,9 @@ msgstr ""
 msgid "Sign in to Bluesky or create a new account"
 msgstr ""
 
-#: src/screens/Settings/Settings.tsx:233
-#: src/screens/Settings/Settings.tsx:235
-#: src/screens/Settings/Settings.tsx:267
+#: src/screens/Settings/Settings.tsx:234
+#: src/screens/Settings/Settings.tsx:236
+#: src/screens/Settings/Settings.tsx:268
 #: src/screens/SignupQueued.tsx:93
 #: src/screens/SignupQueued.tsx:96
 #: src/screens/Takendown.tsx:85
@@ -7165,7 +7123,7 @@ msgstr ""
 msgid "Sign Out"
 msgstr ""
 
-#: src/screens/Settings/Settings.tsx:264
+#: src/screens/Settings/Settings.tsx:265
 #: src/view/shell/desktop/LeftNav.tsx:205
 msgid "Sign out?"
 msgstr ""
@@ -7197,6 +7155,10 @@ msgstr ""
 msgid "Smaller"
 msgstr ""
 
+#: src/components/dialogs/VerifyEmailDialog.tsx:274
+msgid "Snoozes the reminder"
+msgstr ""
+
 #: src/screens/Onboarding/index.tsx:37
 #: src/screens/Onboarding/state.ts:100
 msgid "Software Dev"
@@ -7313,8 +7275,8 @@ msgstr ""
 msgid "Start chat with {displayName}"
 msgstr ""
 
-#: src/Navigation.tsx:450
-#: src/Navigation.tsx:455
+#: src/Navigation.tsx:451
+#: src/Navigation.tsx:456
 #: src/screens/StarterPack/Wizard/index.tsx:186
 msgid "Starter Pack"
 msgstr ""
@@ -7354,12 +7316,12 @@ msgstr ""
 msgid "Step {0} of {1}"
 msgstr ""
 
-#: src/screens/Settings/Settings.tsx:359
+#: src/screens/Settings/Settings.tsx:360
 msgid "Storage cleared, you need to restart the app now."
 msgstr ""
 
-#: src/Navigation.tsx:275
-#: src/screens/Settings/Settings.tsx:375
+#: src/Navigation.tsx:276
+#: src/screens/Settings/Settings.tsx:387
 msgid "Storybook"
 msgstr ""
 
@@ -7404,11 +7366,11 @@ msgstr ""
 msgid "Subscribe to this list"
 msgstr ""
 
-#: src/components/dialogs/VerifyEmailDialog.tsx:95
+#: src/components/dialogs/VerifyEmailDialog.tsx:124
 msgid "Success!"
 msgstr ""
 
-#: src/components/verification/VerificationCreatePrompt.tsx:30
+#: src/components/verification/VerificationCreatePrompt.tsx:36
 msgid "Successfully verified"
 msgstr ""
 
@@ -7435,15 +7397,15 @@ msgctxt "Name of app icon variant"
 msgid "Sunset"
 msgstr ""
 
-#: src/Navigation.tsx:295
+#: src/Navigation.tsx:296
 #: src/view/screens/Support.tsx:31
 #: src/view/screens/Support.tsx:34
 msgid "Support"
 msgstr ""
 
-#: src/screens/Settings/Settings.tsx:110
-#: src/screens/Settings/Settings.tsx:124
-#: src/screens/Settings/Settings.tsx:462
+#: src/screens/Settings/Settings.tsx:111
+#: src/screens/Settings/Settings.tsx:125
+#: src/screens/Settings/Settings.tsx:481
 #: src/view/shell/desktop/LeftNav.tsx:243
 msgid "Switch account"
 msgstr ""
@@ -7470,7 +7432,7 @@ msgstr ""
 
 #: src/screens/Settings/AboutSettings.tsx:104
 #: src/screens/Settings/AboutSettings.tsx:107
-#: src/screens/Settings/Settings.tsx:368
+#: src/screens/Settings/Settings.tsx:380
 msgid "System log"
 msgstr ""
 
@@ -7522,7 +7484,7 @@ msgstr ""
 msgid "Terms"
 msgstr ""
 
-#: src/Navigation.tsx:305
+#: src/Navigation.tsx:306
 #: src/screens/Settings/AboutSettings.tsx:81
 #: src/screens/Settings/AboutSettings.tsx:84
 #: src/view/screens/TermsOfService.tsx:31
@@ -7555,7 +7517,7 @@ msgstr ""
 msgid "Text input field"
 msgstr ""
 
-#: src/components/dialogs/VerifyEmailDialog.tsx:96
+#: src/components/dialogs/VerifyEmailDialog.tsx:125
 msgid "Thank you! Your email has been successfully verified."
 msgstr ""
 
@@ -7627,6 +7589,10 @@ msgstr ""
 msgid "The Discover feed now knows what you like"
 msgstr ""
 
+#: src/components/dialogs/ChangeEmailDialog.tsx:74
+msgid "The email address you entered is the same as your current email address."
+msgstr ""
+
 #: src/screens/StarterPack/StarterPackLandingScreen.tsx:324
 msgid "The experience is better in the app. Download Bluesky now and we'll pick back up where you left off."
 msgstr ""
@@ -7815,7 +7781,7 @@ msgstr ""
 msgid "This account is blocked by one or more of your moderation lists. To unblock, please visit the lists directly and remove this user."
 msgstr ""
 
-#: src/components/verification/VerificationCreatePrompt.tsx:48
+#: src/components/verification/VerificationCreatePrompt.tsx:55
 msgid "This action can be undone at any time."
 msgstr ""
 
@@ -7890,10 +7856,6 @@ msgstr ""
 msgid "This information is not shared with other users."
 msgstr ""
 
-#: src/view/com/modals/VerifyEmail.tsx:127
-msgid "This is important in case you ever need to change your email or reset your password."
-msgstr ""
-
 #: src/components/moderation/ModerationDetailsDialog.tsx:168
 msgid "This label was applied by the author."
 msgstr ""
@@ -7967,6 +7929,10 @@ msgstr ""
 msgid "This should create a domain record at:"
 msgstr ""
 
+#: src/components/verification/VerificationCreatePrompt.tsx:93
+msgid "This user does not have a display name, and therefore cannot be verified."
+msgstr ""
+
 #: src/view/com/profile/ProfileFollowers.tsx:95
 msgid "This user doesn't have any followers."
 msgstr ""
@@ -8004,7 +7970,7 @@ msgstr ""
 msgid "This will delete \"{0}\" from your muted words. You can always add it back later."
 msgstr ""
 
-#: src/screens/Settings/Settings.tsx:512
+#: src/screens/Settings/Settings.tsx:531
 msgid "This will remove @{0} from the quick access list."
 msgstr ""
 
@@ -8035,7 +8001,7 @@ msgstr ""
 msgid "Threaded mode"
 msgstr ""
 
-#: src/Navigation.tsx:338
+#: src/Navigation.tsx:339
 msgid "Threads Preferences"
 msgstr ""
 
@@ -8081,7 +8047,7 @@ msgstr ""
 msgid "Top"
 msgstr ""
 
-#: src/Navigation.tsx:420
+#: src/Navigation.tsx:421
 msgid "Topic"
 msgstr ""
 
@@ -8302,6 +8268,11 @@ msgstr ""
 msgid "Unpinned from your feeds"
 msgstr ""
 
+#: src/screens/Settings/Settings.tsx:413
+#: src/screens/Settings/Settings.tsx:415
+msgid "Unsnooze email reminder"
+msgstr ""
+
 #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:246
 msgid "Unsubscribe"
 msgstr ""
@@ -8410,8 +8381,8 @@ msgstr ""
 msgid "Use default provider"
 msgstr ""
 
-#: src/view/com/modals/InAppBrowserConsent.tsx:53
-#: src/view/com/modals/InAppBrowserConsent.tsx:55
+#: src/components/dialogs/InAppBrowserConsent.tsx:77
+#: src/components/dialogs/InAppBrowserConsent.tsx:83
 msgid "Use in-app browser"
 msgstr ""
 
@@ -8420,8 +8391,8 @@ msgstr ""
 msgid "Use in-app browser to open links"
 msgstr ""
 
-#: src/view/com/modals/InAppBrowserConsent.tsx:63
-#: src/view/com/modals/InAppBrowserConsent.tsx:65
+#: src/components/dialogs/InAppBrowserConsent.tsx:87
+#: src/components/dialogs/InAppBrowserConsent.tsx:93
 msgid "Use my default browser"
 msgstr ""
 
@@ -8514,11 +8485,15 @@ msgstr ""
 msgid "Value:"
 msgstr ""
 
+#: src/components/verification/VerificationCreatePrompt.tsx:39
+msgid "Verification failed, please try again."
+msgstr ""
+
 #: src/screens/Moderation/index.tsx:288
 msgid "Verification settings"
 msgstr ""
 
-#: src/Navigation.tsx:175
+#: src/Navigation.tsx:176
 #: src/screens/Moderation/VerificationSettings.tsx:32
 msgid "Verification Settings"
 msgstr ""
@@ -8535,7 +8510,8 @@ msgstr ""
 msgid "Verified email required"
 msgstr ""
 
-#: src/components/verification/VerificationCreatePrompt.tsx:65
+#: src/components/verification/VerificationCreatePrompt.tsx:84
+#: src/components/verification/VerificationCreatePrompt.tsx:86
 #: src/view/com/profile/ProfileMenu.tsx:308
 #: src/view/com/profile/ProfileMenu.tsx:311
 msgid "Verify account"
@@ -8546,14 +8522,15 @@ msgstr ""
 msgid "Verify DNS Record"
 msgstr ""
 
-#: src/components/dialogs/VerifyEmailDialog.tsx:134
-#: src/components/intents/VerifyEmailIntentDialog.tsx:67
-msgid "Verify email dialog"
+#: src/components/dialogs/ChangeEmailDialog.tsx:234
+#: src/components/dialogs/ChangeEmailDialog.tsx:240
+msgid "Verify email"
 msgstr ""
 
-#: src/view/com/modals/ChangeEmail.tsx:200
-#: src/view/com/modals/ChangeEmail.tsx:202
-msgid "Verify New Email"
+#: src/components/dialogs/ChangeEmailDialog.tsx:122
+#: src/components/dialogs/VerifyEmailDialog.tsx:163
+#: src/components/intents/VerifyEmailIntentDialog.tsx:67
+msgid "Verify email dialog"
 msgstr ""
 
 #: src/view/com/composer/videos/SelectVideoBtn.tsx:131
@@ -8565,17 +8542,16 @@ msgstr ""
 msgid "Verify Text File"
 msgstr ""
 
-#: src/components/verification/VerificationCreatePrompt.tsx:44
+#: src/components/verification/VerificationCreatePrompt.tsx:51
 msgid "Verify this account?"
 msgstr ""
 
-#: src/screens/Settings/AccountSettings.tsx:75
-#: src/screens/Settings/AccountSettings.tsx:91
+#: src/screens/Settings/AccountSettings.tsx:77
+#: src/screens/Settings/AccountSettings.tsx:93
 msgid "Verify your email"
 msgstr ""
 
-#: src/components/dialogs/VerifyEmailDialog.tsx:85
-#: src/view/com/modals/VerifyEmail.tsx:111
+#: src/components/dialogs/VerifyEmailDialog.tsx:114
 msgid "Verify Your Email"
 msgstr ""
 
@@ -8593,7 +8569,7 @@ msgstr ""
 msgid "Video failed to process"
 msgstr ""
 
-#: src/Navigation.tsx:471
+#: src/Navigation.tsx:472
 msgid "Video Feed"
 msgstr ""
 
@@ -9170,11 +9146,11 @@ msgstr ""
 msgid "You have no lists."
 msgstr ""
 
-#: src/view/screens/ModerationBlockedAccounts.tsx:138
+#: src/view/screens/ModerationBlockedAccounts.tsx:164
 msgid "You have not blocked any accounts yet. To block an account, go to their profile and select \"Block account\" from the menu on their account."
 msgstr ""
 
-#: src/view/screens/ModerationMutedAccounts.tsx:151
+#: src/view/screens/ModerationMutedAccounts.tsx:179
 msgid "You have not muted any accounts yet. To mute an account, go to their profile and select \"Mute account\" from the menu on their account."
 msgstr ""
 
@@ -9239,10 +9215,18 @@ msgstr ""
 msgid "You must select at least one labeler for a report"
 msgstr ""
 
+#: src/screens/Settings/components/Email2FAToggle.tsx:67
+msgid "You need to verify your email address before you can enable email 2FA."
+msgstr ""
+
 #: src/screens/Deactivated.tsx:128
 msgid "You previously deactivated @{0}."
 msgstr ""
 
+#: src/screens/Settings/Settings.tsx:371
+msgid "You probably want to restart the app now."
+msgstr ""
+
 #: src/components/dms/MessageItem.tsx:135
 msgid "You reacted {0}"
 msgstr ""
@@ -9251,7 +9235,7 @@ msgstr ""
 msgid "You reacted {0} to {1}"
 msgstr ""
 
-#: src/screens/Settings/Settings.tsx:265
+#: src/screens/Settings/Settings.tsx:266
 #: src/view/shell/desktop/LeftNav.tsx:206
 msgid "You will be signed out of all your accounts."
 msgstr ""
@@ -9296,7 +9280,7 @@ msgstr ""
 msgid "You'll follow these people right away"
 msgstr ""
 
-#: src/components/dialogs/VerifyEmailDialog.tsx:178
+#: src/components/dialogs/VerifyEmailDialog.tsx:216
 msgid "You'll receive an email at <0>{0}</0> to verify it's you."
 msgstr ""
 
@@ -9382,14 +9366,18 @@ msgstr ""
 msgid "Your chats have been disabled"
 msgstr ""
 
-#: src/view/com/modals/InAppBrowserConsent.tsx:44
-msgid "Your choice will be saved, but can be changed later in settings."
+#: src/components/dialogs/InAppBrowserConsent.tsx:69
+msgid "Your choice will be remembered for future links. You can change it at any time in settings."
 msgstr ""
 
 #: src/screens/Settings/components/ChangeHandleDialog.tsx:513
 msgid "Your current handle <0>{0}</0> will automatically remain reserved for you. You can switch back to it at any time from this account."
 msgstr ""
 
+#: src/components/dialogs/ChangeEmailDialog.tsx:65
+msgid "Your email address has been updated but it is not yet verified. As a next step, please verify your new email."
+msgstr ""
+
 #: src/screens/Login/ForgotPasswordForm.tsx:51
 #: src/screens/Signup/state.ts:270
 #: src/screens/Signup/StepInfo/index.tsx:98
@@ -9397,11 +9385,7 @@ msgstr ""
 msgid "Your email appears to be invalid."
 msgstr ""
 
-#: src/view/com/modals/ChangeEmail.tsx:120
-msgid "Your email has been updated but not verified. As a next step, please verify your new email."
-msgstr ""
-
-#: src/view/com/modals/VerifyEmail.tsx:122
+#: src/components/dialogs/VerifyEmailDialog.tsx:110
 msgid "Your email has not yet been verified. This is an important security step which we recommend."
 msgstr ""
 
@@ -9425,7 +9409,7 @@ msgstr ""
 msgid "Your full username will be <0>@{0}</0>"
 msgstr ""
 
-#: src/Navigation.tsx:392
+#: src/Navigation.tsx:393
 #: src/screens/Search/modules/ExploreInterestsCard.tsx:67
 #: src/screens/Settings/ContentAndMediaSettings.tsx:92
 #: src/screens/Settings/ContentAndMediaSettings.tsx:95
diff --git a/src/screens/Messages/components/MessageInput.tsx b/src/screens/Messages/components/MessageInput.tsx
index 69cba07f7..6cde1d4fe 100644
--- a/src/screens/Messages/components/MessageInput.tsx
+++ b/src/screens/Messages/components/MessageInput.tsx
@@ -24,7 +24,7 @@ import {
   useMessageDraft,
   useSaveMessageDraft,
 } from '#/state/messages/message-drafts'
-import {type EmojiPickerPosition} from '#/view/com/composer/text-input/web/EmojiPicker.web'
+import {type EmojiPickerPosition} from '#/view/com/composer/text-input/web/EmojiPicker'
 import * as Toast from '#/view/com/util/Toast'
 import {android, atoms as a, useTheme} from '#/alf'
 import {useSharedInputStyles} from '#/components/forms/TextField'
diff --git a/src/screens/Messages/components/MessageInput.web.tsx b/src/screens/Messages/components/MessageInput.web.tsx
index bac163685..0dce99ad6 100644
--- a/src/screens/Messages/components/MessageInput.web.tsx
+++ b/src/screens/Messages/components/MessageInput.web.tsx
@@ -15,9 +15,9 @@ import {
 } from '#/state/messages/message-drafts'
 import {textInputWebEmitter} from '#/view/com/composer/text-input/textInputWebEmitter'
 import {
-  Emoji,
-  EmojiPickerPosition,
-} from '#/view/com/composer/text-input/web/EmojiPicker.web'
+  type Emoji,
+  type EmojiPickerPosition,
+} from '#/view/com/composer/text-input/web/EmojiPicker'
 import * as Toast from '#/view/com/util/Toast'
 import {atoms as a, useTheme} from '#/alf'
 import {Button} from '#/components/Button'
diff --git a/src/screens/Messages/components/MessagesList.tsx b/src/screens/Messages/components/MessagesList.tsx
index b400bc578..ce33ca3aa 100644
--- a/src/screens/Messages/components/MessagesList.tsx
+++ b/src/screens/Messages/components/MessagesList.tsx
@@ -1,5 +1,5 @@
 import {useCallback, useEffect, useRef, useState} from 'react'
-import {LayoutChangeEvent, View} from 'react-native'
+import {type LayoutChangeEvent, View} from 'react-native'
 import {useKeyboardHandler} from 'react-native-keyboard-controller'
 import Animated, {
   runOnJS,
@@ -8,10 +8,10 @@ import Animated, {
   useAnimatedStyle,
   useSharedValue,
 } from 'react-native-reanimated'
-import {ReanimatedScrollEvent} from 'react-native-reanimated/lib/typescript/hook/commonTypes'
+import {type ReanimatedScrollEvent} from 'react-native-reanimated/lib/typescript/hook/commonTypes'
 import {
-  $Typed,
-  AppBskyEmbedRecord,
+  type $Typed,
+  type AppBskyEmbedRecord,
   AppBskyRichtextFacet,
   RichText,
 } from '@atproto/api'
@@ -26,19 +26,23 @@ import {logger} from '#/logger'
 import {isNative} from '#/platform/detection'
 import {isWeb} from '#/platform/detection'
 import {
-  ActiveConvoStates,
+  type ActiveConvoStates,
   isConvoActive,
   useConvoActive,
 } from '#/state/messages/convo'
-import {ConvoItem, ConvoState, ConvoStatus} from '#/state/messages/convo/types'
+import {
+  type ConvoItem,
+  type ConvoState,
+  ConvoStatus,
+} from '#/state/messages/convo/types'
 import {useGetPost} from '#/state/queries/post'
 import {useAgent} from '#/state/session'
 import {useShellLayout} from '#/state/shell/shell-layout'
 import {
   EmojiPicker,
-  EmojiPickerState,
-} from '#/view/com/composer/text-input/web/EmojiPicker.web'
-import {List, ListMethods} from '#/view/com/util/List'
+  type EmojiPickerState,
+} from '#/view/com/composer/text-input/web/EmojiPicker'
+import {List, type ListMethods} from '#/view/com/util/List'
 import {ChatDisabled} from '#/screens/Messages/components/ChatDisabled'
 import {MessageInput} from '#/screens/Messages/components/MessageInput'
 import {MessageListError} from '#/screens/Messages/components/MessageListError'
diff --git a/src/screens/Search/modules/ExploreTrendingTopics.tsx b/src/screens/Search/modules/ExploreTrendingTopics.tsx
index 167f6d193..1d3bc2d86 100644
--- a/src/screens/Search/modules/ExploreTrendingTopics.tsx
+++ b/src/screens/Search/modules/ExploreTrendingTopics.tsx
@@ -265,12 +265,12 @@ export function TrendingTopicRowSkeleton({}: {withPosts: boolean}) {
               style={[a.rounded_full]}
             />
           </View>
-          <LoadingPlaceholder width={90} height={18} />
+          <LoadingPlaceholder width={90} height={17} />
         </View>
         <View style={[a.flex_row, a.gap_sm, a.align_center, {paddingLeft: 20}]}>
-          <LoadingPlaceholder width={70} height={18} />
-          <LoadingPlaceholder width={40} height={18} />
-          <LoadingPlaceholder width={60} height={18} />
+          <LoadingPlaceholder width={70} height={16} />
+          <LoadingPlaceholder width={40} height={16} />
+          <LoadingPlaceholder width={60} height={16} />
         </View>
       </View>
       <View style={[a.flex_shrink_0]}>
diff --git a/src/state/gallery.ts b/src/state/gallery.ts
index f4c8b712e..f03ed2afe 100644
--- a/src/state/gallery.ts
+++ b/src/state/gallery.ts
@@ -5,8 +5,8 @@ import {
   moveAsync,
 } from 'expo-file-system'
 import {
-  Action,
-  ActionCrop,
+  type Action,
+  type ActionCrop,
   manipulateAsync,
   SaveFormat,
 } from 'expo-image-manipulator'
@@ -210,17 +210,21 @@ export async function compressImage(img: ComposerImage): Promise<ImageMeta> {
   const source = img.transformed || img.source
 
   const [w, h] = containImageRes(source.width, source.height, POST_IMG_MAX)
-  const cacheDir = isNative && getImageCacheDirectory()
 
-  for (let i = 10; i > 0; i--) {
-    // Float precision
-    const factor = i / 10
+  let minQualityPercentage = 0
+  let maxQualityPercentage = 101 // exclusive
+  let newDataUri
+
+  while (maxQualityPercentage - minQualityPercentage > 1) {
+    const qualityPercentage = Math.round(
+      (maxQualityPercentage + minQualityPercentage) / 2,
+    )
 
     const res = await manipulateAsync(
       source.path,
       [{resize: {width: w, height: h}}],
       {
-        compress: factor,
+        compress: qualityPercentage / 100,
         format: SaveFormat.JPEG,
         base64: true,
       },
@@ -229,17 +233,20 @@ export async function compressImage(img: ComposerImage): Promise<ImageMeta> {
     const base64 = res.base64
 
     if (base64 !== undefined && getDataUriSize(base64) <= POST_IMG_MAX.size) {
-      return {
+      minQualityPercentage = qualityPercentage
+      newDataUri = {
         path: await moveIfNecessary(res.uri),
         width: res.width,
         height: res.height,
         mime: 'image/jpeg',
       }
+    } else {
+      maxQualityPercentage = qualityPercentage
     }
+  }
 
-    if (cacheDir) {
-      await deleteAsync(res.uri)
-    }
+  if (newDataUri) {
+    return newDataUri
   }
 
   throw new Error(`Unable to compress image`)
diff --git a/src/state/modals/index.tsx b/src/state/modals/index.tsx
index 45c4fb467..f79f6213f 100644
--- a/src/state/modals/index.tsx
+++ b/src/state/modals/index.tsx
@@ -66,11 +66,6 @@ export interface LinkWarningModal {
   share?: boolean
 }
 
-export interface InAppBrowserConsentModal {
-  name: 'in-app-browser-consent'
-  href: string
-}
-
 export type Modal =
   // Account
   | DeleteAccountModal
@@ -96,7 +91,6 @@ export type Modal =
 
   // Generic
   | LinkWarningModal
-  | InAppBrowserConsentModal
 
 const ModalContext = React.createContext<{
   isModalActive: boolean
diff --git a/src/state/shell/composer/index.tsx b/src/state/shell/composer/index.tsx
index 33634c047..b425873fc 100644
--- a/src/state/shell/composer/index.tsx
+++ b/src/state/shell/composer/index.tsx
@@ -1,8 +1,8 @@
 import React from 'react'
 import {
-  AppBskyActorDefs,
-  AppBskyFeedDefs,
-  ModerationDecision,
+  type AppBskyActorDefs,
+  type AppBskyFeedDefs,
+  type ModerationDecision,
 } from '@atproto/api'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
@@ -12,7 +12,7 @@ import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
 import {postUriToRelativePath, toBskyAppUrl} from '#/lib/strings/url-helpers'
 import {purgeTemporaryImageFiles} from '#/state/gallery'
 import {precacheResolveLinkQuery} from '#/state/queries/resolve-link'
-import type {EmojiPickerPosition} from '#/view/com/composer/text-input/web/EmojiPicker.web'
+import {type EmojiPickerPosition} from '#/view/com/composer/text-input/web/EmojiPicker'
 import * as Toast from '#/view/com/util/Toast'
 
 export interface ComposerOptsPostRef {
diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx
index 8ec4fefa8..06ff9836c 100644
--- a/src/view/com/composer/text-input/TextInput.web.tsx
+++ b/src/view/com/composer/text-input/TextInput.web.tsx
@@ -12,14 +12,14 @@ import {Placeholder} from '@tiptap/extension-placeholder'
 import {Text as TiptapText} from '@tiptap/extension-text'
 import {generateJSON} from '@tiptap/html'
 import {Fragment, Node, Slice} from '@tiptap/pm/model'
-import {EditorContent, JSONContent, useEditor} from '@tiptap/react'
+import {EditorContent, type JSONContent, useEditor} from '@tiptap/react'
 
 import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle'
 import {usePalette} from '#/lib/hooks/usePalette'
 import {blobToDataUri, isUriImage} from '#/lib/media/util'
 import {useActorAutocompleteFn} from '#/state/queries/actor-autocomplete'
 import {
-  LinkFacetMatch,
+  type LinkFacetMatch,
   suggestLinkCardUri,
 } from '#/view/com/composer/text-input/text-input-util'
 import {textInputWebEmitter} from '#/view/com/composer/text-input/textInputWebEmitter'
@@ -28,7 +28,7 @@ import {normalizeTextStyles} from '#/alf/typography'
 import {Portal} from '#/components/Portal'
 import {Text} from '../../util/text/Text'
 import {createSuggestion} from './web/Autocomplete'
-import {Emoji} from './web/EmojiPicker.web'
+import {type Emoji} from './web/EmojiPicker'
 import {LinkDecorator} from './web/LinkDecorator'
 import {TagDecorator} from './web/TagDecorator'
 
diff --git a/src/view/com/composer/text-input/web/EmojiPicker.tsx b/src/view/com/composer/text-input/web/EmojiPicker.tsx
new file mode 100644
index 000000000..5001753a5
--- /dev/null
+++ b/src/view/com/composer/text-input/web/EmojiPicker.tsx
@@ -0,0 +1,37 @@
+export type Emoji = {
+  aliases?: string[]
+  emoticons: string[]
+  id: string
+  keywords: string[]
+  name: string
+  native: string
+  shortcodes?: string
+  unified: string
+}
+
+export interface EmojiPickerPosition {
+  top: number
+  left: number
+  right: number
+  bottom: number
+  nextFocusRef: React.MutableRefObject<HTMLElement> | null
+}
+
+export interface EmojiPickerState {
+  isOpen: boolean
+  pos: EmojiPickerPosition
+}
+
+interface IProps {
+  state: EmojiPickerState
+  close: () => void
+  /**
+   * If `true`, overrides position and ensures picker is pinned to the top of
+   * the target element.
+   */
+  pinToTop?: boolean
+}
+
+export function EmojiPicker(_opts: IProps) {
+  return null
+}
diff --git a/src/view/com/composer/text-input/web/EmojiPicker.web.tsx b/src/view/com/composer/text-input/web/EmojiPicker.web.tsx
index b3659f22d..c0cae620f 100644
--- a/src/view/com/composer/text-input/web/EmojiPicker.web.tsx
+++ b/src/view/com/composer/text-input/web/EmojiPicker.web.tsx
@@ -3,8 +3,7 @@ import {Pressable, useWindowDimensions, View} from 'react-native'
 import Picker from '@emoji-mart/react'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
-import {DismissableLayer} from '@radix-ui/react-dismissable-layer'
-import {FocusScope} from '@radix-ui/react-focus-scope'
+import {DismissableLayer, FocusScope} from 'radix-ui/internal'
 
 import {textInputWebEmitter} from '#/view/com/composer/text-input/textInputWebEmitter'
 import {atoms as a, flatten} from '#/alf'
@@ -121,7 +120,7 @@ export function EmojiPicker({state, close, pinToTop}: IProps) {
 
   return (
     <Portal>
-      <FocusScope
+      <FocusScope.FocusScope
         loop
         trapped
         onUnmountAutoFocus={e => {
@@ -154,7 +153,7 @@ export function EmojiPicker({state, close, pinToTop}: IProps) {
             },
           ])}>
           <View style={[{position: 'absolute'}, position]}>
-            <DismissableLayer
+            <DismissableLayer.DismissableLayer
               onFocusOutside={evt => evt.preventDefault()}
               onDismiss={close}>
               <Picker
@@ -164,7 +163,7 @@ export function EmojiPicker({state, close, pinToTop}: IProps) {
                 onEmojiSelect={onInsert}
                 autoFocus={true}
               />
-            </DismissableLayer>
+            </DismissableLayer.DismissableLayer>
           </View>
         </View>
 
@@ -175,7 +174,7 @@ export function EmojiPicker({state, close, pinToTop}: IProps) {
           onPress={close}
           style={[a.fixed, a.inset_0]}
         />
-      </FocusScope>
+      </FocusScope.FocusScope>
     </Portal>
   )
 }
diff --git a/src/view/com/modals/InAppBrowserConsent.tsx b/src/view/com/modals/InAppBrowserConsent.tsx
deleted file mode 100644
index 105edfbc6..000000000
--- a/src/view/com/modals/InAppBrowserConsent.tsx
+++ /dev/null
@@ -1,99 +0,0 @@
-import React from 'react'
-import {StyleSheet, View} from 'react-native'
-import {msg, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-
-import {useOpenLink} from '#/lib/hooks/useOpenLink'
-import {usePalette} from '#/lib/hooks/usePalette'
-import {s} from '#/lib/styles'
-import {useModalControls} from '#/state/modals'
-import {useSetInAppBrowser} from '#/state/preferences/in-app-browser'
-import {ScrollView} from '#/view/com/modals/util'
-import {Button} from '#/view/com/util/forms/Button'
-import {Text} from '#/view/com/util/text/Text'
-
-export const snapPoints = [350]
-
-export function Component({href}: {href: string}) {
-  const pal = usePalette('default')
-  const {closeModal} = useModalControls()
-  const {_} = useLingui()
-  const setInAppBrowser = useSetInAppBrowser()
-  const openLink = useOpenLink()
-
-  const onUseIAB = React.useCallback(() => {
-    setInAppBrowser(true)
-    closeModal()
-    openLink(href, true)
-  }, [closeModal, setInAppBrowser, href, openLink])
-
-  const onUseLinking = React.useCallback(() => {
-    setInAppBrowser(false)
-    closeModal()
-    openLink(href, false)
-  }, [closeModal, setInAppBrowser, href, openLink])
-
-  return (
-    <ScrollView
-      testID="inAppBrowserConsentModal"
-      style={[s.flex1, pal.view, {paddingHorizontal: 20, paddingTop: 10}]}>
-      <Text style={[pal.text, styles.title]}>
-        <Trans>How should we open this link?</Trans>
-      </Text>
-      <Text style={pal.text}>
-        <Trans>
-          Your choice will be saved, but can be changed later in settings.
-        </Trans>
-      </Text>
-      <View style={[styles.btnContainer]}>
-        <Button
-          testID="confirmBtn"
-          type="inverted"
-          onPress={onUseIAB}
-          accessibilityLabel={_(msg`Use in-app browser`)}
-          accessibilityHint=""
-          label={_(msg`Use in-app browser`)}
-          labelContainerStyle={{justifyContent: 'center', padding: 8}}
-          labelStyle={[s.f18]}
-        />
-        <Button
-          testID="confirmBtn"
-          type="inverted"
-          onPress={onUseLinking}
-          accessibilityLabel={_(msg`Use my default browser`)}
-          accessibilityHint=""
-          label={_(msg`Use my default browser`)}
-          labelContainerStyle={{justifyContent: 'center', padding: 8}}
-          labelStyle={[s.f18]}
-        />
-        <Button
-          testID="cancelBtn"
-          type="default"
-          onPress={() => {
-            closeModal()
-          }}
-          accessibilityLabel={_(msg`Cancel`)}
-          accessibilityHint=""
-          label={_(msg`Cancel`)}
-          labelContainerStyle={{justifyContent: 'center', padding: 8}}
-          labelStyle={[s.f18]}
-        />
-      </View>
-    </ScrollView>
-  )
-}
-
-const styles = StyleSheet.create({
-  title: {
-    textAlign: 'center',
-    fontWeight: '600',
-    fontSize: 24,
-    marginBottom: 12,
-  },
-  btnContainer: {
-    marginTop: 20,
-    flexDirection: 'column',
-    justifyContent: 'center',
-    rowGap: 10,
-  },
-})
diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx
index d0b50c857..8fd927f16 100644
--- a/src/view/com/modals/Modal.tsx
+++ b/src/view/com/modals/Modal.tsx
@@ -11,7 +11,6 @@ import * as ChangePasswordModal from './ChangePassword'
 import * as CreateOrEditListModal from './CreateOrEditList'
 import * as DeleteAccountModal from './DeleteAccount'
 import * as EditProfileModal from './EditProfile'
-import * as InAppBrowserConsentModal from './InAppBrowserConsent'
 import * as InviteCodesModal from './InviteCodes'
 import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
 import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
@@ -76,9 +75,6 @@ export function ModalsContainer() {
   } else if (activeModal?.name === 'link-warning') {
     snapPoints = LinkWarningModal.snapPoints
     element = <LinkWarningModal.Component {...activeModal} />
-  } else if (activeModal?.name === 'in-app-browser-consent') {
-    snapPoints = InAppBrowserConsentModal.snapPoints
-    element = <InAppBrowserConsentModal.Component {...activeModal} />
   } else {
     return null
   }
diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx
index d5af32236..fd8e3a38b 100644
--- a/src/view/com/util/PostMeta.tsx
+++ b/src/view/com/util/PostMeta.tsx
@@ -107,11 +107,11 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
                   a.pl_2xs,
                   a.self_center,
                   {
-                    marginTop: platform({web: -1, ios: -1, android: -2}),
+                    marginTop: platform({web: 0, ios: 0, android: -1}),
                   },
                 ]}>
                 <VerificationCheck
-                  width={14}
+                  width={platform({android: 13, default: 12})}
                   verifier={verification.role === 'verifier'}
                 />
               </View>
diff --git a/src/view/com/util/forms/NativeDropdown.web.tsx b/src/view/com/util/forms/NativeDropdown.web.tsx
index b3ec319e3..9b4a84e05 100644
--- a/src/view/com/util/forms/NativeDropdown.web.tsx
+++ b/src/view/com/util/forms/NativeDropdown.web.tsx
@@ -1,9 +1,15 @@
 import React from 'react'
-import {Pressable, StyleSheet, Text, View, ViewStyle} from 'react-native'
-import {IconProp} from '@fortawesome/fontawesome-svg-core'
+import {
+  Pressable,
+  StyleSheet,
+  Text,
+  type View,
+  type ViewStyle,
+} from 'react-native'
+import {type IconProp} from '@fortawesome/fontawesome-svg-core'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
-import {MenuItemCommonProps} from 'zeego/lib/typescript/menu'
+import {DropdownMenu} from 'radix-ui'
+import {type MenuItemCommonProps} from 'zeego/lib/typescript/menu'
 
 import {HITSLOP_10} from '#/lib/constants'
 import {usePalette} from '#/lib/hooks/usePalette'
diff --git a/src/view/screens/ModerationBlockedAccounts.tsx b/src/view/screens/ModerationBlockedAccounts.tsx
index cefa29f6c..bb94f8083 100644
--- a/src/view/screens/ModerationBlockedAccounts.tsx
+++ b/src/view/screens/ModerationBlockedAccounts.tsx
@@ -1,19 +1,10 @@
-import React from 'react'
-import {
-  ActivityIndicator,
-  FlatList,
-  RefreshControl,
-  StyleSheet,
-  View,
-} from 'react-native'
+import {useCallback, useMemo, useState} from 'react'
+import {type StyleProp, View, type ViewStyle} from 'react-native'
 import {type AppBskyActorDefs as ActorDefs} from '@atproto/api'
-import {msg, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
+import {Trans} from '@lingui/macro'
 import {useFocusEffect} from '@react-navigation/native'
 import {type NativeStackScreenProps} from '@react-navigation/native-stack'
 
-import {usePalette} from '#/lib/hooks/usePalette'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
 import {type CommonNavigatorParams} from '#/lib/routes/types'
 import {cleanError} from '#/lib/strings/errors'
 import {logger} from '#/logger'
@@ -21,11 +12,12 @@ import {useModerationOpts} from '#/state/preferences/moderation-opts'
 import {useMyBlockedAccountsQuery} from '#/state/queries/my-blocked-accounts'
 import {useSetMinimalShellMode} from '#/state/shell'
 import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
-import {Text} from '#/view/com/util/text/Text'
-import {ViewHeader} from '#/view/com/util/ViewHeader'
+import {List} from '#/view/com/util/List'
 import {atoms as a, useTheme} from '#/alf'
 import * as Layout from '#/components/Layout'
+import {ListFooter} from '#/components/Lists'
 import * as ProfileCard from '#/components/ProfileCard'
+import {Text} from '#/components/Typography'
 
 type Props = NativeStackScreenProps<
   CommonNavigatorParams,
@@ -33,13 +25,10 @@ type Props = NativeStackScreenProps<
 >
 export function ModerationBlockedAccounts({}: Props) {
   const t = useTheme()
-  const pal = usePalette('default')
-  const {_} = useLingui()
   const setMinimalShellMode = useSetMinimalShellMode()
-  const {isTabletOrDesktop} = useWebMediaQueries()
   const moderationOpts = useModerationOpts()
 
-  const [isPTRing, setIsPTRing] = React.useState(false)
+  const [isPTRing, setIsPTRing] = useState(false)
   const {
     data,
     isFetching,
@@ -51,7 +40,7 @@ export function ModerationBlockedAccounts({}: Props) {
     isFetchingNextPage,
   } = useMyBlockedAccountsQuery()
   const isEmpty = !isFetching && !data?.pages[0]?.blocks.length
-  const profiles = React.useMemo(() => {
+  const profiles = useMemo(() => {
     if (data?.pages) {
       return data.pages.flatMap(page => page.blocks)
     }
@@ -59,12 +48,12 @@ export function ModerationBlockedAccounts({}: Props) {
   }, [data])
 
   useFocusEffect(
-    React.useCallback(() => {
+    useCallback(() => {
       setMinimalShellMode(false)
     }, [setMinimalShellMode]),
   )
 
-  const onRefresh = React.useCallback(async () => {
+  const onRefresh = useCallback(async () => {
     setIsPTRing(true)
     try {
       await refetch()
@@ -74,7 +63,7 @@ export function ModerationBlockedAccounts({}: Props) {
     setIsPTRing(false)
   }, [refetch, setIsPTRing])
 
-  const onEndReached = React.useCallback(async () => {
+  const onEndReached = useCallback(async () => {
     if (isFetching || !hasNextPage || isError) return
 
     try {
@@ -104,28 +93,22 @@ export function ModerationBlockedAccounts({}: Props) {
       </View>
     )
   }
+
   return (
     <Layout.Screen testID="blockedAccountsScreen">
-      <Layout.Center style={[a.flex_1, {paddingBottom: 100}]}>
-        <ViewHeader title={_(msg`Blocked Accounts`)} showOnDesktop />
-        <Text
-          type="sm"
-          style={[
-            styles.description,
-            pal.text,
-            isTabletOrDesktop && styles.descriptionDesktop,
-            {
-              marginTop: 20,
-            },
-          ]}>
-          <Trans>
-            Blocked accounts cannot reply in your threads, mention you, or
-            otherwise interact with you. You will not see their content and they
-            will be prevented from seeing yours.
-          </Trans>
-        </Text>
+      <Layout.Center>
+        <Layout.Header.Outer>
+          <Layout.Header.BackButton />
+          <Layout.Header.Content>
+            <Layout.Header.TitleText>
+              <Trans>Blocked Accounts</Trans>
+            </Layout.Header.TitleText>
+          </Layout.Header.Content>
+          <Layout.Header.Slot />
+        </Layout.Header.Outer>
         {isEmpty ? (
-          <View style={[pal.border]}>
+          <View>
+            <Info style={[a.border_b]} />
             {isError ? (
               <ErrorScreen
                 title="Oops!"
@@ -133,42 +116,29 @@ export function ModerationBlockedAccounts({}: Props) {
                 onPressTryAgain={refetch}
               />
             ) : (
-              <View style={[styles.empty, pal.viewLight]}>
-                <Text type="lg" style={[pal.text, styles.emptyText]}>
-                  <Trans>
-                    You have not blocked any accounts yet. To block an account,
-                    go to their profile and select "Block account" from the menu
-                    on their account.
-                  </Trans>
-                </Text>
-              </View>
+              <Empty />
             )}
           </View>
         ) : (
-          <FlatList
-            style={[!isTabletOrDesktop && styles.flex1]}
+          <List
             data={profiles}
             keyExtractor={(item: ActorDefs.ProfileView) => item.did}
-            refreshControl={
-              <RefreshControl
-                refreshing={isPTRing}
-                onRefresh={onRefresh}
-                tintColor={pal.colors.text}
-                titleColor={pal.colors.text}
-              />
-            }
+            refreshing={isPTRing}
+            onRefresh={onRefresh}
             onEndReached={onEndReached}
             renderItem={renderItem}
             initialNumToRender={15}
             // FIXME(dan)
 
-            ListFooterComponent={() => (
-              <View style={styles.footer}>
-                {(isFetching || isFetchingNextPage) && <ActivityIndicator />}
-              </View>
-            )}
-            // @ts-ignore our .web version only -prf
-            desktopFixedHeight
+            ListHeaderComponent={Info}
+            ListFooterComponent={
+              <ListFooter
+                isFetchingNextPage={isFetchingNextPage}
+                hasNextPage={hasNextPage}
+                error={cleanError(error)}
+                onRetry={fetchNextPage}
+              />
+            }
           />
         )}
       </Layout.Center>
@@ -176,37 +146,53 @@ export function ModerationBlockedAccounts({}: Props) {
   )
 }
 
-const styles = StyleSheet.create({
-  title: {
-    textAlign: 'center',
-    marginTop: 12,
-    marginBottom: 12,
-  },
-  description: {
-    textAlign: 'center',
-    paddingHorizontal: 30,
-    marginBottom: 14,
-  },
-  descriptionDesktop: {
-    marginTop: 14,
-  },
-
-  flex1: {
-    flex: 1,
-  },
-  empty: {
-    paddingHorizontal: 20,
-    paddingVertical: 20,
-    borderRadius: 16,
-    marginHorizontal: 24,
-    marginTop: 10,
-  },
-  emptyText: {
-    textAlign: 'center',
-  },
+function Empty() {
+  const t = useTheme()
+  return (
+    <View style={[a.pt_2xl, a.px_xl, a.align_center]}>
+      <View
+        style={[
+          a.py_md,
+          a.px_lg,
+          a.rounded_sm,
+          t.atoms.bg_contrast_25,
+          a.border,
+          t.atoms.border_contrast_low,
+          {maxWidth: 400},
+        ]}>
+        <Text style={[a.text_sm, a.text_center, t.atoms.text_contrast_high]}>
+          <Trans>
+            You have not blocked any accounts yet. To block an account, go to
+            their profile and select "Block account" from the menu on their
+            account.
+          </Trans>
+        </Text>
+      </View>
+    </View>
+  )
+}
 
-  footer: {
-    height: 200,
-    paddingTop: 20,
-  },
-})
+function Info({style}: {style?: StyleProp<ViewStyle>}) {
+  const t = useTheme()
+  return (
+    <View
+      style={[
+        a.w_full,
+        t.atoms.bg_contrast_25,
+        a.py_md,
+        a.px_xl,
+        a.border_t,
+        {marginTop: a.border.borderWidth * -1},
+        t.atoms.border_contrast_low,
+        style,
+      ]}>
+      <Text style={[a.text_center, a.text_sm, t.atoms.text_contrast_high]}>
+        <Trans>
+          Blocked accounts cannot reply in your threads, mention you, or
+          otherwise interact with you. You will not see their content and they
+          will be prevented from seeing yours.
+        </Trans>
+      </Text>
+    </View>
+  )
+}
diff --git a/src/view/screens/ModerationMutedAccounts.tsx b/src/view/screens/ModerationMutedAccounts.tsx
index f49337b7c..11d787ca1 100644
--- a/src/view/screens/ModerationMutedAccounts.tsx
+++ b/src/view/screens/ModerationMutedAccounts.tsx
@@ -1,19 +1,11 @@
-import React from 'react'
-import {
-  ActivityIndicator,
-  FlatList,
-  RefreshControl,
-  StyleSheet,
-  View,
-} from 'react-native'
+import {useCallback, useMemo, useState} from 'react'
+import {type StyleProp, View, type ViewStyle} from 'react-native'
 import {type AppBskyActorDefs as ActorDefs} from '@atproto/api'
-import {msg, Trans} from '@lingui/macro'
+import {Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useFocusEffect} from '@react-navigation/native'
 import {type NativeStackScreenProps} from '@react-navigation/native-stack'
 
-import {usePalette} from '#/lib/hooks/usePalette'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
 import {type CommonNavigatorParams} from '#/lib/routes/types'
 import {cleanError} from '#/lib/strings/errors'
 import {logger} from '#/logger'
@@ -21,11 +13,12 @@ import {useModerationOpts} from '#/state/preferences/moderation-opts'
 import {useMyMutedAccountsQuery} from '#/state/queries/my-muted-accounts'
 import {useSetMinimalShellMode} from '#/state/shell'
 import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
-import {Text} from '#/view/com/util/text/Text'
-import {ViewHeader} from '#/view/com/util/ViewHeader'
+import {List} from '#/view/com/util/List'
 import {atoms as a, useTheme} from '#/alf'
 import * as Layout from '#/components/Layout'
+import {ListFooter} from '#/components/Lists'
 import * as ProfileCard from '#/components/ProfileCard'
+import {Text} from '#/components/Typography'
 
 type Props = NativeStackScreenProps<
   CommonNavigatorParams,
@@ -33,13 +26,11 @@ type Props = NativeStackScreenProps<
 >
 export function ModerationMutedAccounts({}: Props) {
   const t = useTheme()
-  const pal = usePalette('default')
+  const moderationOpts = useModerationOpts()
   const {_} = useLingui()
   const setMinimalShellMode = useSetMinimalShellMode()
-  const {isTabletOrDesktop} = useWebMediaQueries()
-  const moderationOpts = useModerationOpts()
 
-  const [isPTRing, setIsPTRing] = React.useState(false)
+  const [isPTRing, setIsPTRing] = useState(false)
   const {
     data,
     isFetching,
@@ -51,7 +42,7 @@ export function ModerationMutedAccounts({}: Props) {
     isFetchingNextPage,
   } = useMyMutedAccountsQuery()
   const isEmpty = !isFetching && !data?.pages[0]?.mutes.length
-  const profiles = React.useMemo(() => {
+  const profiles = useMemo(() => {
     if (data?.pages) {
       return data.pages.flatMap(page => page.mutes)
     }
@@ -59,12 +50,12 @@ export function ModerationMutedAccounts({}: Props) {
   }, [data])
 
   useFocusEffect(
-    React.useCallback(() => {
+    useCallback(() => {
       setMinimalShellMode(false)
     }, [setMinimalShellMode]),
   )
 
-  const onRefresh = React.useCallback(async () => {
+  const onRefresh = useCallback(async () => {
     setIsPTRing(true)
     try {
       await refetch()
@@ -74,7 +65,7 @@ export function ModerationMutedAccounts({}: Props) {
     setIsPTRing(false)
   }, [refetch, setIsPTRing])
 
-  const onEndReached = React.useCallback(async () => {
+  const onEndReached = useCallback(async () => {
     if (isFetching || !hasNextPage || isError) return
 
     try {
@@ -120,25 +111,19 @@ export function ModerationMutedAccounts({}: Props) {
   }
   return (
     <Layout.Screen testID="mutedAccountsScreen">
-      <ViewHeader title={_(msg`Muted Accounts`)} showOnDesktop />
-      <Layout.Center style={[a.flex_1, {paddingBottom: 100}]}>
-        <Text
-          type="sm"
-          style={[
-            styles.description,
-            pal.text,
-            isTabletOrDesktop && styles.descriptionDesktop,
-            {
-              marginTop: 20,
-            },
-          ]}>
-          <Trans>
-            Muted accounts have their posts removed from your feed and from your
-            notifications. Mutes are completely private.
-          </Trans>
-        </Text>
+      <Layout.Header.Outer>
+        <Layout.Header.BackButton />
+        <Layout.Header.Content>
+          <Layout.Header.TitleText>
+            <Trans>Muted Accounts</Trans>
+          </Layout.Header.TitleText>
+        </Layout.Header.Content>
+        <Layout.Header.Slot />
+      </Layout.Header.Outer>
+      <Layout.Center>
         {isEmpty ? (
-          <View style={[pal.border]}>
+          <View>
+            <Info style={[a.border_b]} />
             {isError ? (
               <ErrorScreen
                 title="Oops!"
@@ -146,42 +131,29 @@ export function ModerationMutedAccounts({}: Props) {
                 onPressTryAgain={refetch}
               />
             ) : (
-              <View style={[styles.empty, pal.viewLight]}>
-                <Text type="lg" style={[pal.text, styles.emptyText]}>
-                  <Trans>
-                    You have not muted any accounts yet. To mute an account, go
-                    to their profile and select "Mute account" from the menu on
-                    their account.
-                  </Trans>
-                </Text>
-              </View>
+              <Empty />
             )}
           </View>
         ) : (
-          <FlatList
-            style={[!isTabletOrDesktop && styles.flex1]}
+          <List
             data={profiles}
             keyExtractor={item => item.did}
-            refreshControl={
-              <RefreshControl
-                refreshing={isPTRing}
-                onRefresh={onRefresh}
-                tintColor={pal.colors.text}
-                titleColor={pal.colors.text}
-              />
-            }
+            refreshing={isPTRing}
+            onRefresh={onRefresh}
             onEndReached={onEndReached}
             renderItem={renderItem}
             initialNumToRender={15}
             // FIXME(dan)
 
-            ListFooterComponent={() => (
-              <View style={styles.footer}>
-                {(isFetching || isFetchingNextPage) && <ActivityIndicator />}
-              </View>
-            )}
-            // @ts-ignore our .web version only -prf
-            desktopFixedHeight
+            ListHeaderComponent={Info}
+            ListFooterComponent={
+              <ListFooter
+                isFetchingNextPage={isFetchingNextPage}
+                hasNextPage={hasNextPage}
+                error={cleanError(error)}
+                onRetry={fetchNextPage}
+              />
+            }
           />
         )}
       </Layout.Center>
@@ -189,37 +161,51 @@ export function ModerationMutedAccounts({}: Props) {
   )
 }
 
-const styles = StyleSheet.create({
-  title: {
-    textAlign: 'center',
-    marginTop: 12,
-    marginBottom: 12,
-  },
-  description: {
-    textAlign: 'center',
-    paddingHorizontal: 30,
-    marginBottom: 14,
-  },
-  descriptionDesktop: {
-    marginTop: 14,
-  },
-
-  flex1: {
-    flex: 1,
-  },
-  empty: {
-    paddingHorizontal: 20,
-    paddingVertical: 20,
-    borderRadius: 16,
-    marginHorizontal: 24,
-    marginTop: 10,
-  },
-  emptyText: {
-    textAlign: 'center',
-  },
+function Empty() {
+  const t = useTheme()
+  return (
+    <View style={[a.pt_2xl, a.px_xl, a.align_center]}>
+      <View
+        style={[
+          a.py_md,
+          a.px_lg,
+          a.rounded_sm,
+          t.atoms.bg_contrast_25,
+          a.border,
+          t.atoms.border_contrast_low,
+          {maxWidth: 400},
+        ]}>
+        <Text style={[a.text_sm, a.text_center, t.atoms.text_contrast_high]}>
+          <Trans>
+            You have not muted any accounts yet. To mute an account, go to their
+            profile and select "Mute account" from the menu on their account.
+          </Trans>
+        </Text>
+      </View>
+    </View>
+  )
+}
 
-  footer: {
-    height: 200,
-    paddingTop: 20,
-  },
-})
+function Info({style}: {style?: StyleProp<ViewStyle>}) {
+  const t = useTheme()
+  return (
+    <View
+      style={[
+        a.w_full,
+        t.atoms.bg_contrast_25,
+        a.py_md,
+        a.px_xl,
+        a.border_t,
+        {marginTop: a.border.borderWidth * -1},
+        t.atoms.border_contrast_low,
+        style,
+      ]}>
+      <Text style={[a.text_center, a.text_sm, t.atoms.text_contrast_high]}>
+        <Trans>
+          Muted accounts have their posts removed from your feed and from your
+          notifications. Mutes are completely private.
+        </Trans>
+      </Text>
+    </View>
+  )
+}
diff --git a/src/view/shell/Composer.web.tsx b/src/view/shell/Composer.web.tsx
index b76e88372..ce3695212 100644
--- a/src/view/shell/Composer.web.tsx
+++ b/src/view/shell/Composer.web.tsx
@@ -1,18 +1,16 @@
 import React from 'react'
 import {StyleSheet, View} from 'react-native'
-import {DismissableLayer} from '@radix-ui/react-dismissable-layer'
-import {useFocusGuards} from '@radix-ui/react-focus-guards'
-import {FocusScope} from '@radix-ui/react-focus-scope'
+import {DismissableLayer, FocusGuards, FocusScope} from 'radix-ui/internal'
 import {RemoveScrollBar} from 'react-remove-scroll-bar'
 
 import {useA11y} from '#/state/a11y'
 import {useModals} from '#/state/modals'
-import {ComposerOpts, useComposerState} from '#/state/shell/composer'
+import {type ComposerOpts, useComposerState} from '#/state/shell/composer'
 import {
   EmojiPicker,
-  EmojiPickerPosition,
-  EmojiPickerState,
-} from '#/view/com/composer/text-input/web/EmojiPicker.web'
+  type EmojiPickerPosition,
+  type EmojiPickerState,
+} from '#/view/com/composer/text-input/web/EmojiPicker'
 import {atoms as a, flatten, useBreakpoints, useTheme} from '#/alf'
 import {ComposePost, useComposerCancelRef} from '../com/composer/Composer'
 
@@ -66,11 +64,11 @@ function Inner({state}: {state: ComposerOpts}) {
     }))
   }, [])
 
-  useFocusGuards()
+  FocusGuards.useFocusGuards()
 
   return (
-    <FocusScope loop trapped asChild>
-      <DismissableLayer
+    <FocusScope.FocusScope loop trapped asChild>
+      <DismissableLayer.DismissableLayer
         role="dialog"
         aria-modal
         style={flatten([
@@ -114,8 +112,8 @@ function Inner({state}: {state: ComposerOpts}) {
           />
         </View>
         <EmojiPicker state={pickerState} close={onClosePicker} />
-      </DismissableLayer>
-    </FocusScope>
+      </DismissableLayer.DismissableLayer>
+    </FocusScope.FocusScope>
   )
 }
 
diff --git a/src/view/shell/createNativeStackNavigatorWithAuth.tsx b/src/view/shell/createNativeStackNavigatorWithAuth.tsx
index 11beaa2e9..868bba5b0 100644
--- a/src/view/shell/createNativeStackNavigatorWithAuth.tsx
+++ b/src/view/shell/createNativeStackNavigatorWithAuth.tsx
@@ -5,21 +5,21 @@ import {View} from 'react-native'
 // Copyright (c) 2017 React Navigation Contributors
 import {
   createNavigatorFactory,
-  EventArg,
-  ParamListBase,
-  StackActionHelpers,
+  type EventArg,
+  type ParamListBase,
+  type StackActionHelpers,
   StackActions,
-  StackNavigationState,
+  type StackNavigationState,
   StackRouter,
-  StackRouterOptions,
+  type StackRouterOptions,
   useNavigationBuilder,
 } from '@react-navigation/native'
-import type {
-  NativeStackNavigationEventMap,
-  NativeStackNavigationOptions,
+import {
+  type NativeStackNavigationEventMap,
+  type NativeStackNavigationOptions,
 } from '@react-navigation/native-stack'
 import {NativeStackView} from '@react-navigation/native-stack'
-import type {NativeStackNavigatorProps} from '@react-navigation/native-stack/src/types'
+import {type NativeStackNavigatorProps} from '@react-navigation/native-stack/src/types'
 
 import {PWI_ENABLED} from '#/lib/build-flags'
 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
@@ -35,7 +35,7 @@ import {Deactivated} from '#/screens/Deactivated'
 import {Onboarding} from '#/screens/Onboarding'
 import {SignupQueued} from '#/screens/SignupQueued'
 import {Takendown} from '#/screens/Takendown'
-import {atoms as a} from '#/alf'
+import {atoms as a, useLayoutBreakpoints} from '#/alf'
 import {BottomBarWeb} from './bottom-bar/BottomBarWeb'
 import {DesktopLeftNav} from './desktop/LeftNav'
 import {DesktopRightNav} from './desktop/RightNav'
@@ -101,7 +101,8 @@ function NativeStackNavigator({
   const onboardingState = useOnboardingState()
   const {showLoggedOut} = useLoggedOutView()
   const {setShowLoggedOut} = useLoggedOutViewControls()
-  const {isMobile, isTabletOrMobile} = useWebMediaQueries()
+  const {isMobile} = useWebMediaQueries()
+  const {leftNavMinimal} = useLayoutBreakpoints()
   if (!hasSession && (!PWI_ENABLED || activeRouteRequiresAuth || isNative)) {
     return <LoggedOut />
   }
@@ -138,7 +139,7 @@ function NativeStackNavigator({
 
   // Show the bottom bar if we have a session only on mobile web. If we don't have a session, we want to show it
   // on both tablet and mobile web so that we see the create account CTA.
-  const showBottomBar = hasSession ? isMobile : isTabletOrMobile
+  const showBottomBar = hasSession ? isMobile : leftNavMinimal
 
   return (
     <NavigationContent>
diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx
index 5cef18ebf..7d7c0ac8d 100644
--- a/src/view/shell/desktop/LeftNav.tsx
+++ b/src/view/shell/desktop/LeftNav.tsx
@@ -571,7 +571,7 @@ export function DesktopLeftNav() {
       ]}>
       {hasSession ? (
         <ProfileCard />
-      ) : isDesktop ? (
+      ) : !leftNavMinimal ? (
         <View style={[a.pt_xl]}>
           <NavSignupCard />
         </View>
diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx
index 3d3a5520c..1e34f6da5 100644
--- a/src/view/shell/index.tsx
+++ b/src/view/shell/index.tsx
@@ -25,6 +25,7 @@ import {ModalsContainer} from '#/view/com/modals/Modal'
 import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
 import {atoms as a, select, useTheme} from '#/alf'
 import {setSystemUITheme} from '#/alf/util/systemUI'
+import {InAppBrowserConsentDialog} from '#/components/dialogs/InAppBrowserConsent'
 import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
 import {SigninDialog} from '#/components/dialogs/Signin'
 import {Outlet as PortalOutlet} from '#/components/Portal'
@@ -151,6 +152,7 @@ function ShellInner() {
       <ModalsContainer />
       <MutedWordsDialog />
       <SigninDialog />
+      <InAppBrowserConsentDialog />
       <Lightbox />
       <PortalOutlet />
       <BottomSheetOutlet />