about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/App.native.tsx4
-rw-r--r--src/alf/util/useColorModeTheme.ts6
-rw-r--r--src/components/Button.tsx7
-rw-r--r--src/components/Dialog/context.ts5
-rw-r--r--src/components/Dialog/index.tsx380
-rw-r--r--src/components/Dialog/index.web.tsx12
-rw-r--r--src/components/Dialog/sheet-wrapper.ts20
-rw-r--r--src/components/Dialog/types.ts13
-rw-r--r--src/components/KeyboardControllerPadding.android.tsx31
-rw-r--r--src/components/KeyboardControllerPadding.tsx7
-rw-r--r--src/components/LikesDialog.tsx16
-rw-r--r--src/components/Link.tsx6
-rw-r--r--src/components/Menu/index.tsx21
-rw-r--r--src/components/NewskieDialog.tsx6
-rw-r--r--src/components/Portal.tsx2
-rw-r--r--src/components/Prompt.tsx18
-rw-r--r--src/components/ReportDialog/SelectLabelerView.tsx1
-rw-r--r--src/components/ReportDialog/SubmitView.tsx3
-rw-r--r--src/components/ReportDialog/index.tsx9
-rw-r--r--src/components/StarterPack/QrCodeDialog.tsx1
-rw-r--r--src/components/StarterPack/ShareDialog.tsx16
-rw-r--r--src/components/StarterPack/Wizard/WizardEditListDialog.tsx26
-rw-r--r--src/components/TagMenu/index.tsx1
-rw-r--r--src/components/dialogs/BirthDateSettings.tsx1
-rw-r--r--src/components/dialogs/EmbedConsent.tsx1
-rw-r--r--src/components/dialogs/GifSelect.ios.tsx255
-rw-r--r--src/components/dialogs/GifSelect.shared.tsx53
-rw-r--r--src/components/dialogs/GifSelect.tsx75
-rw-r--r--src/components/dialogs/MutedWords.tsx528
-rw-r--r--src/components/dialogs/PostInteractionSettingsDialog.tsx7
-rw-r--r--src/components/dialogs/SwitchAccount.tsx1
-rw-r--r--src/components/dialogs/nuxs/NeueTypography.tsx1
-rw-r--r--src/components/dms/ConvoMenu.tsx6
-rw-r--r--src/components/dms/MessageMenu.tsx10
-rw-r--r--src/components/dms/ReportDialog.tsx7
-rw-r--r--src/components/dms/dialogs/NewChatDialog.tsx8
-rw-r--r--src/components/dms/dialogs/SearchablePeopleList.tsx101
-rw-r--r--src/components/dms/dialogs/ShareViaChatDialog.tsx8
-rw-r--r--src/components/forms/Toggle.tsx2
-rw-r--r--src/components/moderation/LabelsOnMeDialog.tsx71
-rw-r--r--src/components/moderation/ModerationDetailsDialog.tsx33
-rw-r--r--src/lib/media/video/upload.ts2
-rw-r--r--src/lib/media/video/upload.web.ts2
-rw-r--r--src/screens/Onboarding/StepProfile/index.tsx37
-rw-r--r--src/screens/StarterPack/StarterPackScreen.tsx44
-rw-r--r--src/state/dialogs/index.tsx87
-rw-r--r--src/state/preferences/in-app-browser.tsx12
-rw-r--r--src/view/com/auth/server-input/index.tsx6
-rw-r--r--src/view/com/composer/Composer.tsx570
-rw-r--r--src/view/com/composer/GifAltText.tsx9
-rw-r--r--src/view/com/composer/photos/EditImageDialog.web.tsx1
-rw-r--r--src/view/com/composer/photos/Gallery.tsx13
-rw-r--r--src/view/com/composer/photos/ImageAltTextDialog.tsx9
-rw-r--r--src/view/com/composer/photos/SelectGifBtn.tsx5
-rw-r--r--src/view/com/composer/photos/SelectPhotoBtn.tsx14
-rw-r--r--src/view/com/composer/threadgate/ThreadgateBtn.tsx5
-rw-r--r--src/view/com/composer/videos/SubtitleDialog.tsx8
-rw-r--r--src/view/com/util/UserAvatar.tsx12
-rw-r--r--src/view/com/util/UserBanner.tsx6
-rw-r--r--src/view/com/util/forms/PostDropdownBtn.tsx16
-rw-r--r--src/view/com/util/post-ctrls/RepostButton.tsx5
-rw-r--r--src/view/screens/Settings/DisableEmail2FADialog.tsx1
-rw-r--r--src/view/screens/Settings/ExportCarDialog.tsx1
-rw-r--r--src/view/screens/Storybook/Dialogs.tsx14
-rw-r--r--src/view/shell/Composer.ios.tsx77
-rw-r--r--src/view/shell/index.tsx37
66 files changed, 1263 insertions, 1509 deletions
diff --git a/src/App.native.tsx b/src/App.native.tsx
index e2fcd6d2e..c6334379f 100644
--- a/src/App.native.tsx
+++ b/src/App.native.tsx
@@ -1,6 +1,6 @@
 import 'react-native-url-polyfill/auto'
-import 'lib/sentry' // must be near top
-import 'view/icons'
+import '#/lib/sentry' // must be near top
+import '#/view/icons'
 
 import React, {useEffect, useState} from 'react'
 import {GestureHandlerRootView} from 'react-native-gesture-handler'
diff --git a/src/alf/util/useColorModeTheme.ts b/src/alf/util/useColorModeTheme.ts
index 12840c706..561a504b2 100644
--- a/src/alf/util/useColorModeTheme.ts
+++ b/src/alf/util/useColorModeTheme.ts
@@ -1,9 +1,8 @@
 import React from 'react'
 import {ColorSchemeName, useColorScheme} from 'react-native'
-import * as SystemUI from 'expo-system-ui'
 
-import {isWeb} from 'platform/detection'
-import {useThemePrefs} from 'state/shell'
+import {isWeb} from '#/platform/detection'
+import {useThemePrefs} from '#/state/shell'
 import {dark, dim, light} from '#/alf/themes'
 import {ThemeName} from '#/alf/types'
 
@@ -12,7 +11,6 @@ export function useColorModeTheme(): ThemeName {
 
   React.useLayoutEffect(() => {
     updateDocument(theme)
-    SystemUI.setBackgroundColorAsync(getBackgroundColor(theme))
   }, [theme])
 
   return theme
diff --git a/src/components/Button.tsx b/src/components/Button.tsx
index 1c14b48c7..4acb4f1dc 100644
--- a/src/components/Button.tsx
+++ b/src/components/Button.tsx
@@ -87,6 +87,7 @@ export type ButtonProps = Pick<
     style?: StyleProp<ViewStyle>
     hoverStyle?: StyleProp<ViewStyle>
     children: NonTextElements | ((context: ButtonContext) => NonTextElements)
+    PressableComponent?: React.ComponentType<PressableProps>
   }
 
 export type ButtonTextProps = TextProps & VariantProps & {disabled?: boolean}
@@ -114,6 +115,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
       disabled = false,
       style,
       hoverStyle: hoverStyleProp,
+      PressableComponent = Pressable,
       ...rest
     },
     ref,
@@ -449,10 +451,11 @@ export const Button = React.forwardRef<View, ButtonProps>(
     const flattenedBaseStyles = flatten([baseStyles, style])
 
     return (
-      <Pressable
+      <PressableComponent
         role="button"
         accessibilityHint={undefined} // optional
         {...rest}
+        // @ts-ignore - this will always be a pressable
         ref={ref}
         aria-label={label}
         aria-pressed={state.pressed}
@@ -500,7 +503,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
         <Context.Provider value={context}>
           {typeof children === 'function' ? children(context) : children}
         </Context.Provider>
-      </Pressable>
+      </PressableComponent>
     )
   },
 )
diff --git a/src/components/Dialog/context.ts b/src/components/Dialog/context.ts
index 859f8edd7..b479bc7f0 100644
--- a/src/components/Dialog/context.ts
+++ b/src/components/Dialog/context.ts
@@ -6,9 +6,14 @@ import {
   DialogControlRefProps,
   DialogOuterProps,
 } from '#/components/Dialog/types'
+import {BottomSheetSnapPoint} from '../../../modules/bottom-sheet/src/BottomSheet.types'
 
 export const Context = React.createContext<DialogContextProps>({
   close: () => {},
+  isNativeDialog: false,
+  nativeSnapPoint: BottomSheetSnapPoint.Hidden,
+  disableDrag: false,
+  setDisableDrag: () => {},
 })
 
 export function useDialogContext() {
diff --git a/src/components/Dialog/index.tsx b/src/components/Dialog/index.tsx
index d5d92048a..49b5e10b2 100644
--- a/src/components/Dialog/index.tsx
+++ b/src/components/Dialog/index.tsx
@@ -1,86 +1,48 @@
 import React, {useImperativeHandle} from 'react'
 import {
-  Dimensions,
-  Keyboard,
+  NativeScrollEvent,
+  NativeSyntheticEvent,
   Pressable,
+  ScrollView,
   StyleProp,
+  TextInput,
   View,
   ViewStyle,
 } from 'react-native'
-import Animated, {useAnimatedStyle} from 'react-native-reanimated'
+import {
+  KeyboardAwareScrollView,
+  useKeyboardHandler,
+} from 'react-native-keyboard-controller'
+import {runOnJS} from 'react-native-reanimated'
 import {useSafeAreaInsets} from 'react-native-safe-area-context'
-import BottomSheet, {
-  BottomSheetBackdropProps,
-  BottomSheetFlatList,
-  BottomSheetFlatListMethods,
-  BottomSheetScrollView,
-  BottomSheetScrollViewMethods,
-  BottomSheetTextInput,
-  BottomSheetView,
-  useBottomSheet,
-  WINDOW_HEIGHT,
-} from '@discord/bottom-sheet/src'
-import {BottomSheetFlatListProps} from '@discord/bottom-sheet/src/components/bottomSheetScrollable/types'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
 
 import {logger} from '#/logger'
+import {isAndroid, isIOS} from '#/platform/detection'
+import {useA11y} from '#/state/a11y'
 import {useDialogStateControlContext} from '#/state/dialogs'
-import {atoms as a, flatten, useTheme} from '#/alf'
-import {Context} from '#/components/Dialog/context'
+import {List, ListMethods, ListProps} from '#/view/com/util/List'
+import {atoms as a, useTheme} from '#/alf'
+import {Context, useDialogContext} from '#/components/Dialog/context'
 import {
   DialogControlProps,
   DialogInnerProps,
   DialogOuterProps,
 } from '#/components/Dialog/types'
 import {createInput} from '#/components/forms/TextField'
-import {FullWindowOverlay} from '#/components/FullWindowOverlay'
-import {Portal} from '#/components/Portal'
+import {Portal as DefaultPortal} from '#/components/Portal'
+import {BottomSheet, BottomSheetSnapPoint} from '../../../modules/bottom-sheet'
+import {
+  BottomSheetSnapPointChangeEvent,
+  BottomSheetStateChangeEvent,
+} from '../../../modules/bottom-sheet/src/BottomSheet.types'
 
 export {useDialogContext, useDialogControl} from '#/components/Dialog/context'
 export * from '#/components/Dialog/types'
 export * from '#/components/Dialog/utils'
 // @ts-ignore
-export const Input = createInput(BottomSheetTextInput)
-
-function Backdrop(props: BottomSheetBackdropProps) {
-  const t = useTheme()
-  const bottomSheet = useBottomSheet()
-
-  const animatedStyle = useAnimatedStyle(() => {
-    const opacity =
-      (Math.abs(WINDOW_HEIGHT - props.animatedPosition.value) - 50) / 1000
-
-    return {
-      opacity: Math.min(Math.max(opacity, 0), 0.55),
-    }
-  })
-
-  const onPress = React.useCallback(() => {
-    bottomSheet.close()
-  }, [bottomSheet])
-
-  return (
-    <Animated.View
-      style={[
-        t.atoms.bg_contrast_300,
-        {
-          top: 0,
-          left: 0,
-          right: 0,
-          bottom: 0,
-          position: 'absolute',
-        },
-        animatedStyle,
-      ]}>
-      <Pressable
-        accessibilityRole="button"
-        accessibilityLabel="Dialog backdrop"
-        accessibilityHint="Press the backdrop to close the dialog"
-        style={{flex: 1}}
-        onPress={onPress}
-      />
-    </Animated.View>
-  )
-}
+export const Input = createInput(TextInput)
 
 export function Outer({
   children,
@@ -88,24 +50,22 @@ export function Outer({
   onClose,
   nativeOptions,
   testID,
+  Portal = DefaultPortal,
 }: React.PropsWithChildren<DialogOuterProps>) {
   const t = useTheme()
-  const sheet = React.useRef<BottomSheet>(null)
-  const sheetOptions = nativeOptions?.sheet || {}
-  const hasSnapPoints = !!sheetOptions.snapPoints
-  const insets = useSafeAreaInsets()
+  const ref = React.useRef<BottomSheet>(null)
   const closeCallbacks = React.useRef<(() => void)[]>([])
-  const {setDialogIsOpen} = useDialogStateControlContext()
+  const {setDialogIsOpen, setFullyExpandedCount} =
+    useDialogStateControlContext()
 
-  /*
-   * Used to manage open/closed, but index is otherwise handled internally by `BottomSheet`
-   */
-  const [openIndex, setOpenIndex] = React.useState(-1)
+  const prevSnapPoint = React.useRef<BottomSheetSnapPoint>(
+    BottomSheetSnapPoint.Hidden,
+  )
 
-  /*
-   * `openIndex` is the index of the snap point to open the bottom sheet to. If >0, the bottom sheet is open.
-   */
-  const isOpen = openIndex > -1
+  const [disableDrag, setDisableDrag] = React.useState(false)
+  const [snapPoint, setSnapPoint] = React.useState<BottomSheetSnapPoint>(
+    BottomSheetSnapPoint.Partial,
+  )
 
   const callQueuedCallbacks = React.useCallback(() => {
     for (const cb of closeCallbacks.current) {
@@ -119,25 +79,19 @@ export function Outer({
     closeCallbacks.current = []
   }, [])
 
-  const open = React.useCallback<DialogControlProps['open']>(
-    ({index} = {}) => {
-      // Run any leftover callbacks that might have been queued up before calling `.open()`
-      callQueuedCallbacks()
-
-      setDialogIsOpen(control.id, true)
-      // can be set to any index of `snapPoints`, but `0` is the first i.e. "open"
-      setOpenIndex(index || 0)
-      sheet.current?.snapToIndex(index || 0)
-    },
-    [setDialogIsOpen, control.id, callQueuedCallbacks],
-  )
+  const open = React.useCallback<DialogControlProps['open']>(() => {
+    // Run any leftover callbacks that might have been queued up before calling `.open()`
+    callQueuedCallbacks()
+    setDialogIsOpen(control.id, true)
+    ref.current?.present()
+  }, [setDialogIsOpen, control.id, callQueuedCallbacks])
 
   // This is the function that we call when we want to dismiss the dialog.
   const close = React.useCallback<DialogControlProps['close']>(cb => {
     if (typeof cb === 'function') {
       closeCallbacks.current.push(cb)
     }
-    sheet.current?.close()
+    ref.current?.dismiss()
   }, [])
 
   // This is the actual thing we are doing once we "confirm" the dialog. We want the dialog's close animation to
@@ -146,12 +100,39 @@ export function Outer({
     // This removes the dialog from our list of stored dialogs. Not super necessary on iOS, but on Android this
     // tells us that we need to toggle the accessibility overlay setting
     setDialogIsOpen(control.id, false)
-    setOpenIndex(-1)
-
     callQueuedCallbacks()
     onClose?.()
   }, [callQueuedCallbacks, control.id, onClose, setDialogIsOpen])
 
+  const onSnapPointChange = (e: BottomSheetSnapPointChangeEvent) => {
+    const {snapPoint} = e.nativeEvent
+    setSnapPoint(snapPoint)
+
+    if (
+      snapPoint === BottomSheetSnapPoint.Full &&
+      prevSnapPoint.current !== BottomSheetSnapPoint.Full
+    ) {
+      setFullyExpandedCount(c => c + 1)
+    } else if (
+      snapPoint !== BottomSheetSnapPoint.Full &&
+      prevSnapPoint.current === BottomSheetSnapPoint.Full
+    ) {
+      setFullyExpandedCount(c => c - 1)
+    }
+    prevSnapPoint.current = snapPoint
+  }
+
+  const onStateChange = (e: BottomSheetStateChangeEvent) => {
+    if (e.nativeEvent.state === 'closed') {
+      onCloseAnimationComplete()
+
+      if (prevSnapPoint.current === BottomSheetSnapPoint.Full) {
+        setFullyExpandedCount(c => c - 1)
+      }
+      prevSnapPoint.current = BottomSheetSnapPoint.Hidden
+    }
+  }
+
   useImperativeHandle(
     control.ref,
     () => ({
@@ -161,159 +142,144 @@ export function Outer({
     [open, close],
   )
 
-  React.useEffect(() => {
-    return () => {
-      setDialogIsOpen(control.id, false)
-    }
-  }, [control.id, setDialogIsOpen])
-
-  const context = React.useMemo(() => ({close}), [close])
+  const context = React.useMemo(
+    () => ({
+      close,
+      isNativeDialog: true,
+      nativeSnapPoint: snapPoint,
+      disableDrag,
+      setDisableDrag,
+    }),
+    [close, snapPoint, disableDrag, setDisableDrag],
+  )
 
   return (
-    isOpen && (
-      <Portal>
-        <FullWindowOverlay>
-          <View
-            // iOS
-            accessibilityViewIsModal
-            // Android
-            importantForAccessibility="yes"
-            style={[a.absolute, a.inset_0]}
-            testID={testID}
-            onTouchMove={() => Keyboard.dismiss()}>
-            <BottomSheet
-              enableDynamicSizing={!hasSnapPoints}
-              enablePanDownToClose
-              keyboardBehavior="interactive"
-              android_keyboardInputMode="adjustResize"
-              keyboardBlurBehavior="restore"
-              topInset={insets.top}
-              {...sheetOptions}
-              snapPoints={sheetOptions.snapPoints || ['100%']}
-              ref={sheet}
-              index={openIndex}
-              backgroundStyle={{backgroundColor: 'transparent'}}
-              backdropComponent={Backdrop}
-              handleIndicatorStyle={{backgroundColor: t.palette.primary_500}}
-              handleStyle={{display: 'none'}}
-              onClose={onCloseAnimationComplete}>
-              <Context.Provider value={context}>
-                <View
-                  style={[
-                    a.absolute,
-                    a.inset_0,
-                    t.atoms.bg,
-                    {
-                      borderTopLeftRadius: 40,
-                      borderTopRightRadius: 40,
-                      height: Dimensions.get('window').height * 2,
-                    },
-                  ]}
-                />
-                {children}
-              </Context.Provider>
-            </BottomSheet>
-          </View>
-        </FullWindowOverlay>
-      </Portal>
-    )
+    <Portal>
+      <Context.Provider value={context}>
+        <BottomSheet
+          ref={ref}
+          cornerRadius={20}
+          backgroundColor={t.atoms.bg.backgroundColor}
+          {...nativeOptions}
+          onSnapPointChange={onSnapPointChange}
+          onStateChange={onStateChange}
+          disableDrag={disableDrag}>
+          <View testID={testID}>{children}</View>
+        </BottomSheet>
+      </Context.Provider>
+    </Portal>
   )
 }
 
 export function Inner({children, style}: DialogInnerProps) {
   const insets = useSafeAreaInsets()
   return (
-    <BottomSheetView
+    <View
       style={[
-        a.py_xl,
+        a.pt_2xl,
         a.px_xl,
         {
-          paddingTop: 40,
-          borderTopLeftRadius: 40,
-          borderTopRightRadius: 40,
-          paddingBottom: insets.bottom + a.pb_5xl.paddingBottom,
+          paddingBottom: insets.bottom + insets.top,
         },
-        flatten(style),
+        style,
       ]}>
       {children}
-    </BottomSheetView>
+    </View>
   )
 }
 
-export const ScrollableInner = React.forwardRef<
-  BottomSheetScrollViewMethods,
-  DialogInnerProps
->(function ScrollableInner({children, style}, ref) {
-  const insets = useSafeAreaInsets()
-  return (
-    <BottomSheetScrollView
-      keyboardShouldPersistTaps="handled"
-      style={[
-        a.flex_1, // main diff is this
-        a.p_xl,
-        a.h_full,
-        {
-          paddingTop: 40,
-          borderTopLeftRadius: 40,
-          borderTopRightRadius: 40,
-        },
-        style,
-      ]}
-      contentContainerStyle={a.pb_4xl}
-      ref={ref}>
-      {children}
-      <View style={{height: insets.bottom + a.pt_5xl.paddingTop}} />
-    </BottomSheetScrollView>
-  )
-})
+export const ScrollableInner = React.forwardRef<ScrollView, DialogInnerProps>(
+  function ScrollableInner({children, style, ...props}, ref) {
+    const {nativeSnapPoint, disableDrag, setDisableDrag} = useDialogContext()
+    const insets = useSafeAreaInsets()
+    const [keyboardHeight, setKeyboardHeight] = React.useState(0)
+    useKeyboardHandler({
+      onEnd: e => {
+        'worklet'
+        runOnJS(setKeyboardHeight)(e.height)
+      },
+    })
+
+    const basePading =
+      (isIOS ? 30 : 50) + (isIOS ? keyboardHeight / 4 : keyboardHeight)
+    const fullPaddingBase = insets.bottom + insets.top + basePading
+    const fullPadding = isIOS ? fullPaddingBase : fullPaddingBase + 50
+
+    const paddingBottom =
+      nativeSnapPoint === BottomSheetSnapPoint.Full ? fullPadding : basePading
+
+    const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
+      const {contentOffset} = e.nativeEvent
+      if (contentOffset.y > 0 && !disableDrag) {
+        setDisableDrag(true)
+      } else if (contentOffset.y <= 1 && disableDrag) {
+        setDisableDrag(false)
+      }
+    }
+
+    return (
+      <KeyboardAwareScrollView
+        style={[style]}
+        contentContainerStyle={[a.pt_2xl, a.px_xl, {paddingBottom}]}
+        ref={ref}
+        {...props}
+        bounces={nativeSnapPoint === BottomSheetSnapPoint.Full}
+        bottomOffset={30}
+        scrollEventThrottle={50}
+        onScroll={isAndroid ? onScroll : undefined}>
+        {children}
+      </KeyboardAwareScrollView>
+    )
+  },
+)
 
 export const InnerFlatList = React.forwardRef<
-  BottomSheetFlatListMethods,
-  BottomSheetFlatListProps<any> & {webInnerStyle?: StyleProp<ViewStyle>}
->(function InnerFlatList({style, contentContainerStyle, ...props}, ref) {
+  ListMethods,
+  ListProps<any> & {webInnerStyle?: StyleProp<ViewStyle>}
+>(function InnerFlatList({style, ...props}, ref) {
   const insets = useSafeAreaInsets()
-
+  const {nativeSnapPoint} = useDialogContext()
   return (
-    <BottomSheetFlatList
+    <List
       keyboardShouldPersistTaps="handled"
-      contentContainerStyle={[a.pb_4xl, flatten(contentContainerStyle)]}
+      bounces={nativeSnapPoint === BottomSheetSnapPoint.Full}
       ListFooterComponent={
         <View style={{height: insets.bottom + a.pt_5xl.paddingTop}} />
       }
       ref={ref}
       {...props}
-      style={[
-        a.flex_1,
-        a.p_xl,
-        a.pt_0,
-        a.h_full,
-        {
-          marginTop: 40,
-        },
-        flatten(style),
-      ]}
+      style={[style]}
     />
   )
 })
 
 export function Handle() {
   const t = useTheme()
+  const {_} = useLingui()
+  const {screenReaderEnabled} = useA11y()
+  const {close} = useDialogContext()
 
   return (
-    <View style={[a.absolute, a.w_full, a.align_center, a.z_10, {height: 40}]}>
-      <View
-        style={[
-          a.rounded_sm,
-          {
-            top: a.pt_lg.paddingTop,
-            width: 35,
-            height: 4,
-            alignSelf: 'center',
-            backgroundColor: t.palette.contrast_900,
-            opacity: 0.5,
-          },
-        ]}
-      />
+    <View style={[a.absolute, a.w_full, a.align_center, a.z_10, {height: 20}]}>
+      <Pressable
+        accessible={screenReaderEnabled}
+        onPress={() => close()}
+        accessibilityLabel={_(msg`Dismiss`)}
+        accessibilityHint={_(msg`Double tap to close the dialog`)}>
+        <View
+          style={[
+            a.rounded_sm,
+            {
+              top: 10,
+              width: 35,
+              height: 5,
+              alignSelf: 'center',
+              backgroundColor: t.palette.contrast_975,
+              opacity: 0.5,
+            },
+          ]}
+        />
+      </Pressable>
     </View>
   )
 }
diff --git a/src/components/Dialog/index.web.tsx b/src/components/Dialog/index.web.tsx
index bf20bd295..7b9cfb693 100644
--- a/src/components/Dialog/index.web.tsx
+++ b/src/components/Dialog/index.web.tsx
@@ -103,6 +103,10 @@ export function Outer({
   const context = React.useMemo(
     () => ({
       close,
+      isNativeDialog: false,
+      nativeSnapPoint: 0,
+      disableDrag: false,
+      setDisableDrag: () => {},
     }),
     [close],
   )
@@ -229,10 +233,6 @@ export const InnerFlatList = React.forwardRef<
   )
 })
 
-export function Handle() {
-  return null
-}
-
 export function Close() {
   const {_} = useLingui()
   const {close} = React.useContext(Context)
@@ -258,3 +258,7 @@ export function Close() {
     </View>
   )
 }
+
+export function Handle() {
+  return null
+}
diff --git a/src/components/Dialog/sheet-wrapper.ts b/src/components/Dialog/sheet-wrapper.ts
new file mode 100644
index 000000000..37c663383
--- /dev/null
+++ b/src/components/Dialog/sheet-wrapper.ts
@@ -0,0 +1,20 @@
+import {useCallback} from 'react'
+
+import {useDialogStateControlContext} from '#/state/dialogs'
+
+/**
+ * If we're calling a system API like the image picker that opens a sheet
+ * wrap it in this function to make sure the status bar is the correct color.
+ */
+export function useSheetWrapper() {
+  const {setFullyExpandedCount} = useDialogStateControlContext()
+  return useCallback(
+    async <T>(promise: Promise<T>): Promise<T> => {
+      setFullyExpandedCount(c => c + 1)
+      const res = await promise
+      setFullyExpandedCount(c => c - 1)
+      return res
+    },
+    [setFullyExpandedCount],
+  )
+}
diff --git a/src/components/Dialog/types.ts b/src/components/Dialog/types.ts
index 1ddab02ee..caa787535 100644
--- a/src/components/Dialog/types.ts
+++ b/src/components/Dialog/types.ts
@@ -4,9 +4,11 @@ import type {
   GestureResponderEvent,
   ScrollViewProps,
 } from 'react-native'
-import {BottomSheetProps} from '@discord/bottom-sheet/src'
 
 import {ViewStyleProp} from '#/alf'
+import {PortalComponent} from '#/components/Portal'
+import {BottomSheetViewProps} from '../../../modules/bottom-sheet'
+import {BottomSheetSnapPoint} from '../../../modules/bottom-sheet/src/BottomSheet.types'
 
 type A11yProps = Required<AccessibilityProps>
 
@@ -37,6 +39,10 @@ export type DialogControlProps = DialogControlRefProps & {
 
 export type DialogContextProps = {
   close: DialogControlProps['close']
+  isNativeDialog: boolean
+  nativeSnapPoint: BottomSheetSnapPoint
+  disableDrag: boolean
+  setDisableDrag: React.Dispatch<React.SetStateAction<boolean>>
 }
 
 export type DialogControlOpenOptions = {
@@ -52,11 +58,10 @@ export type DialogControlOpenOptions = {
 export type DialogOuterProps = {
   control: DialogControlProps
   onClose?: () => void
-  nativeOptions?: {
-    sheet?: Omit<BottomSheetProps, 'children'>
-  }
+  nativeOptions?: Omit<BottomSheetViewProps, 'children'>
   webOptions?: {}
   testID?: string
+  Portal?: PortalComponent
 }
 
 type DialogInnerPropsBase<T> = React.PropsWithChildren<ViewStyleProp> & T
diff --git a/src/components/KeyboardControllerPadding.android.tsx b/src/components/KeyboardControllerPadding.android.tsx
deleted file mode 100644
index 92ef1b0b0..000000000
--- a/src/components/KeyboardControllerPadding.android.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import React from 'react'
-import {useKeyboardHandler} from 'react-native-keyboard-controller'
-import Animated, {
-  useAnimatedStyle,
-  useSharedValue,
-} from 'react-native-reanimated'
-
-export function KeyboardControllerPadding({maxHeight}: {maxHeight?: number}) {
-  const keyboardHeight = useSharedValue(0)
-
-  useKeyboardHandler(
-    {
-      onMove: e => {
-        'worklet'
-
-        if (maxHeight && e.height > maxHeight) {
-          keyboardHeight.value = maxHeight
-        } else {
-          keyboardHeight.value = e.height
-        }
-      },
-    },
-    [maxHeight],
-  )
-
-  const animatedStyle = useAnimatedStyle(() => ({
-    height: keyboardHeight.value,
-  }))
-
-  return <Animated.View style={animatedStyle} />
-}
diff --git a/src/components/KeyboardControllerPadding.tsx b/src/components/KeyboardControllerPadding.tsx
deleted file mode 100644
index f3163d87c..000000000
--- a/src/components/KeyboardControllerPadding.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-export function KeyboardControllerPadding({
-  maxHeight: _,
-}: {
-  maxHeight?: number
-}) {
-  return null
-}
diff --git a/src/components/LikesDialog.tsx b/src/components/LikesDialog.tsx
index 94a3f27e2..4c68596f7 100644
--- a/src/components/LikesDialog.tsx
+++ b/src/components/LikesDialog.tsx
@@ -1,20 +1,19 @@
-import React, {useMemo, useCallback} from 'react'
+import React, {useCallback, useMemo} from 'react'
 import {ActivityIndicator, FlatList, View} from 'react-native'
+import {AppBskyFeedGetLikes as GetLikes} from '@atproto/api'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
-import {AppBskyFeedGetLikes as GetLikes} from '@atproto/api'
 
-import {useResolveUriQuery} from '#/state/queries/resolve-uri'
-import {useLikedByQuery} from '#/state/queries/post-liked-by'
 import {cleanError} from '#/lib/strings/errors'
 import {logger} from '#/logger'
-
+import {useLikedByQuery} from '#/state/queries/post-liked-by'
+import {useResolveUriQuery} from '#/state/queries/resolve-uri'
+import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
+import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
 import {atoms as a, useTheme} from '#/alf'
-import {Text} from '#/components/Typography'
 import * as Dialog from '#/components/Dialog'
-import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
-import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
 import {Loader} from '#/components/Loader'
+import {Text} from '#/components/Typography'
 
 interface LikesDialogProps {
   control: Dialog.DialogOuterProps['control']
@@ -25,7 +24,6 @@ export function LikesDialog(props: LikesDialogProps) {
   return (
     <Dialog.Outer control={props.control}>
       <Dialog.Handle />
-
       <LikesDialogInner {...props} />
     </Dialog.Outer>
   )
diff --git a/src/components/Link.tsx b/src/components/Link.tsx
index c80b9f370..447833a23 100644
--- a/src/components/Link.tsx
+++ b/src/components/Link.tsx
@@ -103,17 +103,17 @@ export function useLink({
           linkRequiresWarning(href, displayText),
       )
 
-      if (requiresWarning) {
+      if (isWeb) {
         e.preventDefault()
+      }
 
+      if (requiresWarning) {
         openModal({
           name: 'link-warning',
           text: displayText,
           href: href,
         })
       } else {
-        e.preventDefault()
-
         if (isExternal) {
           openLink(href)
         } else {
diff --git a/src/components/Menu/index.tsx b/src/components/Menu/index.tsx
index a0a21a50f..a22f43cf8 100644
--- a/src/components/Menu/index.tsx
+++ b/src/components/Menu/index.tsx
@@ -4,7 +4,7 @@ import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import flattenReactChildren from 'react-keyed-flatten-children'
 
-import {isNative} from 'platform/detection'
+import {isNative} from '#/platform/detection'
 import {atoms as a, useTheme} from '#/alf'
 import {Button, ButtonText} from '#/components/Button'
 import * as Dialog from '#/components/Dialog'
@@ -82,19 +82,21 @@ export function Outer({
   style?: StyleProp<ViewStyle>
 }>) {
   const context = React.useContext(Context)
+  const {_} = useLingui()
 
   return (
-    <Dialog.Outer control={context.control}>
+    <Dialog.Outer
+      control={context.control}
+      nativeOptions={{preventExpansion: true}}>
       <Dialog.Handle />
-
       {/* Re-wrap with context since Dialogs are portal-ed to root */}
       <Context.Provider value={context}>
-        <Dialog.ScrollableInner label="Menu TODO">
+        <Dialog.ScrollableInner label={_(msg`Menu`)} style={[a.pt_sm]}>
           <View style={[a.gap_lg]}>
             {children}
             {isNative && showCancel && <Cancel />}
+            <View style={[{height: a.pb_lg.paddingBottom}]} />
           </View>
-          <View style={{height: a.gap_lg.gap}} />
         </Dialog.ScrollableInner>
       </Context.Provider>
     </Dialog.Outer>
@@ -116,15 +118,14 @@ export function Item({children, label, style, onPress, ...rest}: ItemProps) {
       {...rest}
       accessibilityHint=""
       accessibilityLabel={label}
-      onPress={e => {
-        onPress(e)
-
+      onFocus={onFocus}
+      onBlur={onBlur}
+      onPress={async e => {
+        await onPress(e)
         if (!e.defaultPrevented) {
           control?.close()
         }
       }}
-      onFocus={onFocus}
-      onBlur={onBlur}
       onPressIn={e => {
         onPressIn()
         rest.onPressIn?.(e)
diff --git a/src/components/NewskieDialog.tsx b/src/components/NewskieDialog.tsx
index 1a523a839..0e3520658 100644
--- a/src/components/NewskieDialog.tsx
+++ b/src/components/NewskieDialog.tsx
@@ -5,12 +5,12 @@ import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {differenceInSeconds} from 'date-fns'
 
+import {HITSLOP_10} from '#/lib/constants'
 import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
+import {sanitizeDisplayName} from '#/lib/strings/display-names'
 import {isNative} from '#/platform/detection'
 import {useModerationOpts} from '#/state/preferences/moderation-opts'
-import {HITSLOP_10} from 'lib/constants'
-import {sanitizeDisplayName} from 'lib/strings/display-names'
-import {useSession} from 'state/session'
+import {useSession} from '#/state/session'
 import {atoms as a, useTheme} from '#/alf'
 import {Button, ButtonText} from '#/components/Button'
 import * as Dialog from '#/components/Dialog'
diff --git a/src/components/Portal.tsx b/src/components/Portal.tsx
index 03b397b2b..7441df005 100644
--- a/src/components/Portal.tsx
+++ b/src/components/Portal.tsx
@@ -12,6 +12,8 @@ type ComponentMap = {
   [id: string]: Component
 }
 
+export type PortalComponent = ({children}: {children?: React.ReactNode}) => null
+
 export function createPortalGroup() {
   const Context = React.createContext<ContextType>({
     outlet: null,
diff --git a/src/components/Prompt.tsx b/src/components/Prompt.tsx
index 8765cdee3..fc6919af8 100644
--- a/src/components/Prompt.tsx
+++ b/src/components/Prompt.tsx
@@ -4,8 +4,9 @@ import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
 import {atoms as a, useBreakpoints, useTheme} from '#/alf'
-import {Button, ButtonColor, ButtonProps, ButtonText} from '#/components/Button'
+import {Button, ButtonColor, ButtonText} from '#/components/Button'
 import * as Dialog from '#/components/Dialog'
+import {PortalComponent} from '#/components/Portal'
 import {Text} from '#/components/Typography'
 
 export {
@@ -25,9 +26,11 @@ export function Outer({
   children,
   control,
   testID,
+  Portal,
 }: React.PropsWithChildren<{
   control: Dialog.DialogControlProps
   testID?: string
+  Portal?: PortalComponent
 }>) {
   const {gtMobile} = useBreakpoints()
   const titleId = React.useId()
@@ -39,10 +42,9 @@ export function Outer({
   )
 
   return (
-    <Dialog.Outer control={control} testID={testID}>
+    <Dialog.Outer control={control} testID={testID} Portal={Portal}>
+      <Dialog.Handle />
       <Context.Provider value={context}>
-        <Dialog.Handle />
-
         <Dialog.ScrollableInner
           accessibilityLabelledBy={titleId}
           accessibilityDescribedBy={descriptionId}
@@ -141,7 +143,7 @@ export function Action({
    * Note: The dialog will close automatically when the action is pressed, you
    * should NOT close the dialog as a side effect of this method.
    */
-  onPress: ButtonProps['onPress']
+  onPress: (e: GestureResponderEvent) => void
   color?: ButtonColor
   /**
    * Optional i18n string. If undefined, it will default to "Confirm".
@@ -181,6 +183,7 @@ export function Basic({
   onConfirm,
   confirmButtonColor,
   showCancel = true,
+  Portal,
 }: React.PropsWithChildren<{
   control: Dialog.DialogOuterProps['control']
   title: string
@@ -194,12 +197,13 @@ export function Basic({
    * Note: The dialog will close automatically when the action is pressed, you
    * should NOT close the dialog as a side effect of this method.
    */
-  onConfirm: ButtonProps['onPress']
+  onConfirm: (e: GestureResponderEvent) => void
   confirmButtonColor?: ButtonColor
   showCancel?: boolean
+  Portal?: PortalComponent
 }>) {
   return (
-    <Outer control={control} testID="confirmModal">
+    <Outer control={control} testID="confirmModal" Portal={Portal}>
       <TitleText>{title}</TitleText>
       <DescriptionText>{description}</DescriptionText>
       <Actions>
diff --git a/src/components/ReportDialog/SelectLabelerView.tsx b/src/components/ReportDialog/SelectLabelerView.tsx
index f7a8139ea..039bbf123 100644
--- a/src/components/ReportDialog/SelectLabelerView.tsx
+++ b/src/components/ReportDialog/SelectLabelerView.tsx
@@ -4,7 +4,6 @@ import {AppBskyLabelerDefs} from '@atproto/api'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
-export {useDialogControl as useReportDialogControl} from '#/components/Dialog'
 import {getLabelingServiceTitle} from '#/lib/moderation'
 import {atoms as a, useBreakpoints, useTheme} from '#/alf'
 import {Button, useButtonContext} from '#/components/Button'
diff --git a/src/components/ReportDialog/SubmitView.tsx b/src/components/ReportDialog/SubmitView.tsx
index e323d1504..ef4a9b7fb 100644
--- a/src/components/ReportDialog/SubmitView.tsx
+++ b/src/components/ReportDialog/SubmitView.tsx
@@ -6,6 +6,7 @@ import {useLingui} from '@lingui/react'
 
 import {getLabelingServiceTitle} from '#/lib/moderation'
 import {ReportOption} from '#/lib/moderation/useReportOptions'
+import {isAndroid} from '#/platform/detection'
 import {useAgent} from '#/state/session'
 import {CharProgress} from '#/view/com/composer/char-progress/CharProgress'
 import * as Toast from '#/view/com/util/Toast'
@@ -225,6 +226,8 @@ export function SubmitView({
           {submitting && <ButtonIcon icon={Loader} />}
         </Button>
       </View>
+      {/* Maybe fix this later -h */}
+      {isAndroid ? <View style={{height: 300}} /> : null}
     </View>
   )
 }
diff --git a/src/components/ReportDialog/index.tsx b/src/components/ReportDialog/index.tsx
index c87d32f9e..5bf8aa5b4 100644
--- a/src/components/ReportDialog/index.tsx
+++ b/src/components/ReportDialog/index.tsx
@@ -1,5 +1,6 @@
 import React from 'react'
 import {Pressable, View} from 'react-native'
+import {ScrollView} from 'react-native-gesture-handler'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
@@ -8,12 +9,10 @@ import {useMyLabelersQuery} from '#/state/queries/preferences'
 export {useDialogControl as useReportDialogControl} from '#/components/Dialog'
 
 import {AppBskyLabelerDefs} from '@atproto/api'
-import {BottomSheetScrollViewMethods} from '@discord/bottom-sheet/src'
 
 import {atoms as a} from '#/alf'
 import * as Dialog from '#/components/Dialog'
 import {useDelayedLoading} from '#/components/hooks/useDelayedLoading'
-import {useOnKeyboardDidShow} from '#/components/hooks/useOnKeyboard'
 import {Loader} from '#/components/Loader'
 import {Text} from '#/components/Typography'
 import {SelectLabelerView} from './SelectLabelerView'
@@ -25,7 +24,6 @@ export function ReportDialog(props: ReportDialogProps) {
   return (
     <Dialog.Outer control={props.control}>
       <Dialog.Handle />
-
       <ReportDialogInner {...props} />
     </Dialog.Outer>
   )
@@ -40,10 +38,7 @@ function ReportDialogInner(props: ReportDialogProps) {
   } = useMyLabelersQuery()
   const isLoading = useDelayedLoading(500, isLabelerLoading)
 
-  const ref = React.useRef<BottomSheetScrollViewMethods>(null)
-  useOnKeyboardDidShow(() => {
-    ref.current?.scrollToEnd({animated: true})
-  })
+  const ref = React.useRef<ScrollView>(null)
 
   return (
     <Dialog.ScrollableInner label={_(msg`Report dialog`)} ref={ref}>
diff --git a/src/components/StarterPack/QrCodeDialog.tsx b/src/components/StarterPack/QrCodeDialog.tsx
index b2af8ff73..2feea0973 100644
--- a/src/components/StarterPack/QrCodeDialog.tsx
+++ b/src/components/StarterPack/QrCodeDialog.tsx
@@ -149,7 +149,6 @@ export function QrCodeDialog({
 
   return (
     <Dialog.Outer control={control}>
-      <Dialog.Handle />
       <Dialog.ScrollableInner
         label={_(msg`Create a QR code for a starter pack`)}>
         <View style={[a.flex_1, a.align_center, a.gap_5xl]}>
diff --git a/src/components/StarterPack/ShareDialog.tsx b/src/components/StarterPack/ShareDialog.tsx
index 9851b0856..997c6479c 100644
--- a/src/components/StarterPack/ShareDialog.tsx
+++ b/src/components/StarterPack/ShareDialog.tsx
@@ -6,14 +6,14 @@ import {AppBskyGraphDefs} from '@atproto/api'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
+import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
+import {saveImageToMediaLibrary} from '#/lib/media/manip'
+import {shareUrl} from '#/lib/sharing'
+import {logEvent} from '#/lib/statsig/statsig'
+import {getStarterPackOgCard} from '#/lib/strings/starter-pack'
 import {logger} from '#/logger'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {saveImageToMediaLibrary} from 'lib/media/manip'
-import {shareUrl} from 'lib/sharing'
-import {logEvent} from 'lib/statsig/statsig'
-import {getStarterPackOgCard} from 'lib/strings/starter-pack'
-import {isNative, isWeb} from 'platform/detection'
-import * as Toast from 'view/com/util/Toast'
+import {isNative, isWeb} from '#/platform/detection'
+import * as Toast from '#/view/com/util/Toast'
 import {atoms as a, useTheme} from '#/alf'
 import {Button, ButtonText} from '#/components/Button'
 import {DialogControlProps} from '#/components/Dialog'
@@ -32,6 +32,7 @@ interface Props {
 export function ShareDialog(props: Props) {
   return (
     <Dialog.Outer control={props.control}>
+      <Dialog.Handle />
       <ShareDialogInner {...props} />
     </Dialog.Outer>
   )
@@ -84,7 +85,6 @@ function ShareDialogInner({
 
   return (
     <>
-      <Dialog.Handle />
       <Dialog.ScrollableInner label={_(msg`Share link dialog`)}>
         {!imageLoaded || !link ? (
           <View style={[a.p_xl, a.align_center]}>
diff --git a/src/components/StarterPack/Wizard/WizardEditListDialog.tsx b/src/components/StarterPack/Wizard/WizardEditListDialog.tsx
index f7b0aba34..1e9f1c52d 100644
--- a/src/components/StarterPack/Wizard/WizardEditListDialog.tsx
+++ b/src/components/StarterPack/Wizard/WizardEditListDialog.tsx
@@ -3,13 +3,13 @@ import type {ListRenderItemInfo} from 'react-native'
 import {View} from 'react-native'
 import {AppBskyActorDefs, ModerationOpts} from '@atproto/api'
 import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs'
-import {BottomSheetFlatListMethods} from '@discord/bottom-sheet'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
-import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
-import {isWeb} from 'platform/detection'
-import {useSession} from 'state/session'
+import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
+import {isWeb} from '#/platform/detection'
+import {useSession} from '#/state/session'
+import {ListMethods} from '#/view/com/util/List'
 import {WizardAction, WizardState} from '#/screens/StarterPack/Wizard/State'
 import {atoms as a, native, useTheme, web} from '#/alf'
 import {Button, ButtonText} from '#/components/Button'
@@ -45,7 +45,7 @@ export function WizardEditListDialog({
   const {currentAccount} = useSession()
   const initialNumToRender = useInitialNumToRender()
 
-  const listRef = useRef<BottomSheetFlatListMethods>(null)
+  const listRef = useRef<ListMethods>(null)
 
   const getData = () => {
     if (state.currentStep === 'Feeds') return state.feeds
@@ -76,10 +76,7 @@ export function WizardEditListDialog({
     )
 
   return (
-    <Dialog.Outer
-      control={control}
-      testID="newChatDialog"
-      nativeOptions={{sheet: {snapPoints: ['95%']}}}>
+    <Dialog.Outer control={control} testID="newChatDialog">
       <Dialog.Handle />
       <Dialog.InnerFlatList
         ref={listRef}
@@ -89,6 +86,7 @@ export function WizardEditListDialog({
         ListHeaderComponent={
           <View
             style={[
+              native(a.pt_4xl),
               a.flex_row,
               a.justify_between,
               a.border_b,
@@ -103,13 +101,7 @@ export function WizardEditListDialog({
                       height: 48,
                     },
                   ]
-                : [
-                    a.pb_sm,
-                    a.align_end,
-                    {
-                      height: 68,
-                    },
-                  ],
+                : [a.pb_sm, a.align_end],
             ]}>
             <View style={{width: 60}} />
             <Text style={[a.font_bold, a.text_xl]}>
@@ -143,8 +135,6 @@ export function WizardEditListDialog({
             paddingHorizontal: 0,
             marginTop: 0,
             paddingTop: 0,
-            borderTopLeftRadius: 40,
-            borderTopRightRadius: 40,
           }),
         ]}
         webInnerStyle={[a.py_0, {maxWidth: 500, minWidth: 200}]}
diff --git a/src/components/TagMenu/index.tsx b/src/components/TagMenu/index.tsx
index 2c6a0b674..917624a03 100644
--- a/src/components/TagMenu/index.tsx
+++ b/src/components/TagMenu/index.tsx
@@ -85,7 +85,6 @@ export function TagMenu({
 
       <Dialog.Outer control={control}>
         <Dialog.Handle />
-
         <Dialog.Inner label={_(msg`Tag menu: ${displayTag}`)}>
           {isPreferencesLoading ? (
             <View style={[a.w_full, a.align_center]}>
diff --git a/src/components/dialogs/BirthDateSettings.tsx b/src/components/dialogs/BirthDateSettings.tsx
index 08608f9d8..81d0c6740 100644
--- a/src/components/dialogs/BirthDateSettings.tsx
+++ b/src/components/dialogs/BirthDateSettings.tsx
@@ -31,7 +31,6 @@ export function BirthDateSettingsDialog({
   return (
     <Dialog.Outer control={control}>
       <Dialog.Handle />
-
       <Dialog.ScrollableInner label={_(msg`My Birthday`)}>
         <View style={[a.gap_sm, a.pb_lg]}>
           <Text style={[a.text_2xl, a.font_bold]}>
diff --git a/src/components/dialogs/EmbedConsent.tsx b/src/components/dialogs/EmbedConsent.tsx
index 765b8adc7..824155d8b 100644
--- a/src/components/dialogs/EmbedConsent.tsx
+++ b/src/components/dialogs/EmbedConsent.tsx
@@ -50,7 +50,6 @@ export function EmbedConsentDialog({
   return (
     <Dialog.Outer control={control}>
       <Dialog.Handle />
-
       <Dialog.ScrollableInner
         label={_(msg`External Media`)}
         style={[gtMobile ? {width: 'auto', maxWidth: 400} : a.w_full]}>
diff --git a/src/components/dialogs/GifSelect.ios.tsx b/src/components/dialogs/GifSelect.ios.tsx
deleted file mode 100644
index 2f867e865..000000000
--- a/src/components/dialogs/GifSelect.ios.tsx
+++ /dev/null
@@ -1,255 +0,0 @@
-import React, {
-  useCallback,
-  useImperativeHandle,
-  useMemo,
-  useRef,
-  useState,
-} from 'react'
-import {Modal, ScrollView, TextInput, View} from 'react-native'
-import {msg, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-
-import {cleanError} from '#/lib/strings/errors'
-import {
-  Gif,
-  useFeaturedGifsQuery,
-  useGifSearchQuery,
-} from '#/state/queries/tenor'
-import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
-import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
-import {FlatList_INTERNAL} from '#/view/com/util/Views'
-import {atoms as a, useBreakpoints, useTheme} from '#/alf'
-import * as TextField from '#/components/forms/TextField'
-import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
-import {Button, ButtonText} from '../Button'
-import {Handle} from '../Dialog'
-import {useThrottledValue} from '../hooks/useThrottledValue'
-import {ListFooter, ListMaybePlaceholder} from '../Lists'
-import {GifPreview} from './GifSelect.shared'
-
-export function GifSelectDialog({
-  controlRef,
-  onClose,
-  onSelectGif: onSelectGifProp,
-}: {
-  controlRef: React.RefObject<{open: () => void}>
-  onClose: () => void
-  onSelectGif: (gif: Gif) => void
-}) {
-  const t = useTheme()
-  const [open, setOpen] = useState(false)
-
-  useImperativeHandle(controlRef, () => ({
-    open: () => setOpen(true),
-  }))
-
-  const close = useCallback(() => {
-    setOpen(false)
-    onClose()
-  }, [onClose])
-
-  const onSelectGif = useCallback(
-    (gif: Gif) => {
-      onSelectGifProp(gif)
-      close()
-    },
-    [onSelectGifProp, close],
-  )
-
-  const renderErrorBoundary = useCallback(
-    (error: any) => <ModalError details={String(error)} close={close} />,
-    [close],
-  )
-
-  return (
-    <Modal
-      visible={open}
-      animationType="slide"
-      presentationStyle="formSheet"
-      onRequestClose={close}
-      aria-modal
-      accessibilityViewIsModal>
-      <View style={[a.flex_1, t.atoms.bg]}>
-        <Handle />
-        <ErrorBoundary renderError={renderErrorBoundary}>
-          <GifList onSelectGif={onSelectGif} close={close} />
-        </ErrorBoundary>
-      </View>
-    </Modal>
-  )
-}
-
-function GifList({
-  onSelectGif,
-}: {
-  close: () => void
-  onSelectGif: (gif: Gif) => void
-}) {
-  const {_} = useLingui()
-  const t = useTheme()
-  const {gtMobile} = useBreakpoints()
-  const textInputRef = useRef<TextInput>(null)
-  const listRef = useRef<FlatList_INTERNAL>(null)
-  const [undeferredSearch, setSearch] = useState('')
-  const search = useThrottledValue(undeferredSearch, 500)
-
-  const isSearching = search.length > 0
-
-  const trendingQuery = useFeaturedGifsQuery()
-  const searchQuery = useGifSearchQuery(search)
-
-  const {
-    data,
-    fetchNextPage,
-    isFetchingNextPage,
-    hasNextPage,
-    error,
-    isLoading,
-    isError,
-    refetch,
-  } = isSearching ? searchQuery : trendingQuery
-
-  const flattenedData = useMemo(() => {
-    return data?.pages.flatMap(page => page.results) || []
-  }, [data])
-
-  const renderItem = useCallback(
-    ({item}: {item: Gif}) => {
-      return <GifPreview gif={item} onSelectGif={onSelectGif} />
-    },
-    [onSelectGif],
-  )
-
-  const onEndReached = React.useCallback(() => {
-    if (isFetchingNextPage || !hasNextPage || error) return
-    fetchNextPage()
-  }, [isFetchingNextPage, hasNextPage, error, fetchNextPage])
-
-  const hasData = flattenedData.length > 0
-
-  const onGoBack = useCallback(() => {
-    if (isSearching) {
-      // clear the input and reset the state
-      textInputRef.current?.clear()
-      setSearch('')
-    } else {
-      close()
-    }
-  }, [isSearching])
-
-  const listHeader = useMemo(() => {
-    return (
-      <View style={[a.relative, a.mb_lg, a.pt_4xl, a.flex_row, a.align_center]}>
-        {/* cover top corners */}
-        <View
-          style={[
-            a.absolute,
-            a.inset_0,
-            {
-              borderBottomLeftRadius: 8,
-              borderBottomRightRadius: 8,
-            },
-            t.atoms.bg,
-          ]}
-        />
-
-        <TextField.Root>
-          <TextField.Icon icon={Search} />
-          <TextField.Input
-            label={_(msg`Search GIFs`)}
-            placeholder={_(msg`Search Tenor`)}
-            onChangeText={text => {
-              setSearch(text)
-              listRef.current?.scrollToOffset({offset: 0, animated: false})
-            }}
-            returnKeyType="search"
-            clearButtonMode="while-editing"
-            inputRef={textInputRef}
-            maxLength={50}
-          />
-        </TextField.Root>
-      </View>
-    )
-  }, [t.atoms.bg, _])
-
-  return (
-    <FlatList_INTERNAL
-      ref={listRef}
-      key={gtMobile ? '3 cols' : '2 cols'}
-      data={flattenedData}
-      renderItem={renderItem}
-      numColumns={gtMobile ? 3 : 2}
-      columnWrapperStyle={a.gap_sm}
-      contentContainerStyle={a.px_lg}
-      ListHeaderComponent={
-        <>
-          {listHeader}
-          {!hasData && (
-            <ListMaybePlaceholder
-              isLoading={isLoading}
-              isError={isError}
-              onRetry={refetch}
-              onGoBack={onGoBack}
-              emptyType="results"
-              sideBorders={false}
-              topBorder={false}
-              errorTitle={_(msg`Failed to load GIFs`)}
-              errorMessage={_(msg`There was an issue connecting to Tenor.`)}
-              emptyMessage={
-                isSearching
-                  ? _(msg`No search results found for "${search}".`)
-                  : _(
-                      msg`No featured GIFs found. There may be an issue with Tenor.`,
-                    )
-              }
-            />
-          )}
-        </>
-      }
-      stickyHeaderIndices={[0]}
-      onEndReached={onEndReached}
-      onEndReachedThreshold={4}
-      keyExtractor={(item: Gif) => item.id}
-      keyboardDismissMode="on-drag"
-      ListFooterComponent={
-        hasData ? (
-          <ListFooter
-            isFetchingNextPage={isFetchingNextPage}
-            error={cleanError(error)}
-            onRetry={fetchNextPage}
-            style={{borderTopWidth: 0}}
-          />
-        ) : null
-      }
-    />
-  )
-}
-
-function ModalError({details, close}: {details?: string; close: () => void}) {
-  const {_} = useLingui()
-
-  return (
-    <ScrollView
-      style={[a.flex_1, a.gap_md]}
-      centerContent
-      contentContainerStyle={a.px_lg}>
-      <ErrorScreen
-        title={_(msg`Oh no!`)}
-        message={_(
-          msg`There was an unexpected issue in the application. Please let us know if this happened to you!`,
-        )}
-        details={details}
-      />
-      <Button
-        label={_(msg`Close dialog`)}
-        onPress={close}
-        color="primary"
-        size="large"
-        variant="solid">
-        <ButtonText>
-          <Trans>Close</Trans>
-        </ButtonText>
-      </Button>
-    </ScrollView>
-  )
-}
diff --git a/src/components/dialogs/GifSelect.shared.tsx b/src/components/dialogs/GifSelect.shared.tsx
deleted file mode 100644
index 90b2abaa8..000000000
--- a/src/components/dialogs/GifSelect.shared.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import React, {useCallback} from 'react'
-import {Image} from 'expo-image'
-import {msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-
-import {logEvent} from '#/lib/statsig/statsig'
-import {Gif} from '#/state/queries/tenor'
-import {atoms as a, useBreakpoints, useTheme} from '#/alf'
-import {Button} from '../Button'
-
-export function GifPreview({
-  gif,
-  onSelectGif,
-}: {
-  gif: Gif
-  onSelectGif: (gif: Gif) => void
-}) {
-  const {gtTablet} = useBreakpoints()
-  const {_} = useLingui()
-  const t = useTheme()
-
-  const onPress = useCallback(() => {
-    logEvent('composer:gif:select', {})
-    onSelectGif(gif)
-  }, [onSelectGif, gif])
-
-  return (
-    <Button
-      label={_(msg`Select GIF "${gif.title}"`)}
-      style={[a.flex_1, gtTablet ? {maxWidth: '33%'} : {maxWidth: '50%'}]}
-      onPress={onPress}>
-      {({pressed}) => (
-        <Image
-          style={[
-            a.flex_1,
-            a.mb_sm,
-            a.rounded_sm,
-            {aspectRatio: 1, opacity: pressed ? 0.8 : 1},
-            t.atoms.bg_contrast_25,
-          ]}
-          source={{
-            uri: gif.media_formats.tinygif.url,
-          }}
-          contentFit="cover"
-          accessibilityLabel={gif.title}
-          accessibilityHint=""
-          cachePolicy="none"
-          accessibilityIgnoresInvertColors
-        />
-      )}
-    </Button>
-  )
-}
diff --git a/src/components/dialogs/GifSelect.tsx b/src/components/dialogs/GifSelect.tsx
index 1afc588da..6023b5808 100644
--- a/src/components/dialogs/GifSelect.tsx
+++ b/src/components/dialogs/GifSelect.tsx
@@ -6,10 +6,12 @@ import React, {
   useState,
 } from 'react'
 import {TextInput, View} from 'react-native'
-import {BottomSheetFlatListMethods} from '@discord/bottom-sheet'
+import {useWindowDimensions} from 'react-native'
+import {Image} from 'expo-image'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
+import {logEvent} from '#/lib/statsig/statsig'
 import {cleanError} from '#/lib/strings/errors'
 import {isWeb} from '#/platform/detection'
 import {
@@ -19,7 +21,8 @@ import {
 } from '#/state/queries/tenor'
 import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
 import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
-import {atoms as a, useBreakpoints, useTheme} from '#/alf'
+import {ListMethods} from '#/view/com/util/List'
+import {atoms as a, ios, native, useBreakpoints, useTheme} from '#/alf'
 import * as Dialog from '#/components/Dialog'
 import * as TextField from '#/components/forms/TextField'
 import {useThrottledValue} from '#/components/hooks/useThrottledValue'
@@ -27,16 +30,18 @@ import {ArrowLeft_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arr
 import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
 import {Button, ButtonIcon, ButtonText} from '../Button'
 import {ListFooter, ListMaybePlaceholder} from '../Lists'
-import {GifPreview} from './GifSelect.shared'
+import {PortalComponent} from '../Portal'
 
 export function GifSelectDialog({
   controlRef,
   onClose,
   onSelectGif: onSelectGifProp,
+  Portal,
 }: {
   controlRef: React.RefObject<{open: () => void}>
   onClose: () => void
   onSelectGif: (gif: Gif) => void
+  Portal?: PortalComponent
 }) {
   const control = Dialog.useDialogControl()
 
@@ -59,8 +64,13 @@ export function GifSelectDialog({
   return (
     <Dialog.Outer
       control={control}
-      nativeOptions={{sheet: {snapPoints: ['100%']}}}
-      onClose={onClose}>
+      onClose={onClose}
+      Portal={Portal}
+      nativeOptions={{
+        bottomInset: 0,
+        // use system corner radius on iOS
+        ...ios({cornerRadius: undefined}),
+      }}>
       <Dialog.Handle />
       <ErrorBoundary renderError={renderErrorBoundary}>
         <GifList control={control} onSelectGif={onSelectGif} />
@@ -80,9 +90,10 @@ function GifList({
   const t = useTheme()
   const {gtMobile} = useBreakpoints()
   const textInputRef = useRef<TextInput>(null)
-  const listRef = useRef<BottomSheetFlatListMethods>(null)
+  const listRef = useRef<ListMethods>(null)
   const [undeferredSearch, setSearch] = useState('')
   const search = useThrottledValue(undeferredSearch, 500)
+  const {height} = useWindowDimensions()
 
   const isSearching = search.length > 0
 
@@ -95,7 +106,7 @@ function GifList({
     isFetchingNextPage,
     hasNextPage,
     error,
-    isLoading,
+    isPending,
     isError,
     refetch,
   } = isSearching ? searchQuery : trendingQuery
@@ -132,6 +143,7 @@ function GifList({
     return (
       <View
         style={[
+          native(a.pt_4xl),
           a.relative,
           a.mb_lg,
           a.flex_row,
@@ -196,13 +208,14 @@ function GifList({
         data={flattenedData}
         renderItem={renderItem}
         numColumns={gtMobile ? 3 : 2}
-        columnWrapperStyle={a.gap_sm}
+        columnWrapperStyle={[a.gap_sm]}
+        contentContainerStyle={[native([a.px_xl, {minHeight: height}])]}
         ListHeaderComponent={
           <>
             {listHeader}
             {!hasData && (
               <ListMaybePlaceholder
-                isLoading={isLoading}
+                isLoading={isPending}
                 isError={isError}
                 onRetry={refetch}
                 onGoBack={onGoBack}
@@ -273,3 +286,47 @@ function DialogError({details}: {details?: string}) {
     </Dialog.ScrollableInner>
   )
 }
+
+export function GifPreview({
+  gif,
+  onSelectGif,
+}: {
+  gif: Gif
+  onSelectGif: (gif: Gif) => void
+}) {
+  const {gtTablet} = useBreakpoints()
+  const {_} = useLingui()
+  const t = useTheme()
+
+  const onPress = useCallback(() => {
+    logEvent('composer:gif:select', {})
+    onSelectGif(gif)
+  }, [onSelectGif, gif])
+
+  return (
+    <Button
+      label={_(msg`Select GIF "${gif.title}"`)}
+      style={[a.flex_1, gtTablet ? {maxWidth: '33%'} : {maxWidth: '50%'}]}
+      onPress={onPress}>
+      {({pressed}) => (
+        <Image
+          style={[
+            a.flex_1,
+            a.mb_sm,
+            a.rounded_sm,
+            {aspectRatio: 1, opacity: pressed ? 0.8 : 1},
+            t.atoms.bg_contrast_25,
+          ]}
+          source={{
+            uri: gif.media_formats.tinygif.url,
+          }}
+          contentFit="cover"
+          accessibilityLabel={gif.title}
+          accessibilityHint=""
+          cachePolicy="none"
+          accessibilityIgnoresInvertColors
+        />
+      )}
+    </Button>
+  )
+}
diff --git a/src/components/dialogs/MutedWords.tsx b/src/components/dialogs/MutedWords.tsx
index 81a614103..c3aae8f0d 100644
--- a/src/components/dialogs/MutedWords.tsx
+++ b/src/components/dialogs/MutedWords.tsx
@@ -30,11 +30,14 @@ import {PageText_Stroke2_Corner0_Rounded as PageText} from '#/components/icons/P
 import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
 import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
 import {Loader} from '#/components/Loader'
+import {createPortalGroup} from '#/components/Portal'
 import * as Prompt from '#/components/Prompt'
 import {Text} from '#/components/Typography'
 
 const ONE_DAY = 24 * 60 * 60 * 1000
 
+const Portal = createPortalGroup()
+
 export function MutedWordsDialog() {
   const {mutedWordsDialogControl: control} = useGlobalDialogsControlContext()
   return (
@@ -105,307 +108,349 @@ function MutedWordsInner() {
   }, [_, field, targets, addMutedWord, setField, durations, excludeFollowing])
 
   return (
-    <Dialog.ScrollableInner label={_(msg`Manage your muted words and tags`)}>
-      <View>
-        <Text
-          style={[a.text_md, a.font_bold, a.pb_sm, t.atoms.text_contrast_high]}>
-          <Trans>Add muted words and tags</Trans>
-        </Text>
-        <Text style={[a.pb_lg, a.leading_snug, t.atoms.text_contrast_medium]}>
-          <Trans>
-            Posts can be muted based on their text, their tags, or both. We
-            recommend avoiding common words that appear in many posts, since it
-            can result in no posts being shown.
-          </Trans>
-        </Text>
-
-        <View style={[a.pb_sm]}>
-          <Dialog.Input
-            autoCorrect={false}
-            autoCapitalize="none"
-            autoComplete="off"
-            label={_(msg`Enter a word or tag`)}
-            placeholder={_(msg`Enter a word or tag`)}
-            value={field}
-            onChangeText={value => {
-              if (error) {
-                setError('')
-              }
-              setField(value)
-            }}
-            onSubmitEditing={submit}
-          />
-        </View>
+    <Portal.Provider>
+      <Dialog.ScrollableInner label={_(msg`Manage your muted words and tags`)}>
+        <View>
+          <Text
+            style={[
+              a.text_md,
+              a.font_bold,
+              a.pb_sm,
+              t.atoms.text_contrast_high,
+            ]}>
+            <Trans>Add muted words and tags</Trans>
+          </Text>
+          <Text style={[a.pb_lg, a.leading_snug, t.atoms.text_contrast_medium]}>
+            <Trans>
+              Posts can be muted based on their text, their tags, or both. We
+              recommend avoiding common words that appear in many posts, since
+              it can result in no posts being shown.
+            </Trans>
+          </Text>
 
-        <View style={[a.pb_xl, a.gap_sm]}>
-          <Toggle.Group
-            label={_(msg`Select how long to mute this word for.`)}
-            type="radio"
-            values={durations}
-            onChange={setDurations}>
-            <Text
-              style={[
-                a.pb_xs,
-                a.text_sm,
-                a.font_bold,
-                t.atoms.text_contrast_medium,
-              ]}>
-              <Trans>Duration:</Trans>
-            </Text>
+          <View style={[a.pb_sm]}>
+            <Dialog.Input
+              autoCorrect={false}
+              autoCapitalize="none"
+              autoComplete="off"
+              label={_(msg`Enter a word or tag`)}
+              placeholder={_(msg`Enter a word or tag`)}
+              value={field}
+              onChangeText={value => {
+                if (error) {
+                  setError('')
+                }
+                setField(value)
+              }}
+              onSubmitEditing={submit}
+            />
+          </View>
+
+          <View style={[a.pb_xl, a.gap_sm]}>
+            <Toggle.Group
+              label={_(msg`Select how long to mute this word for.`)}
+              type="radio"
+              values={durations}
+              onChange={setDurations}>
+              <Text
+                style={[
+                  a.pb_xs,
+                  a.text_sm,
+                  a.font_bold,
+                  t.atoms.text_contrast_medium,
+                ]}>
+                <Trans>Duration:</Trans>
+              </Text>
 
-            <View
-              style={[
-                gtMobile && [a.flex_row, a.align_center, a.justify_start],
-                a.gap_sm,
-              ]}>
               <View
                 style={[
-                  a.flex_1,
-                  a.flex_row,
-                  a.justify_start,
-                  a.align_center,
+                  gtMobile && [a.flex_row, a.align_center, a.justify_start],
                   a.gap_sm,
                 ]}>
-                <Toggle.Item
-                  label={_(msg`Mute this word until you unmute it`)}
-                  name="forever"
-                  style={[a.flex_1]}>
-                  <TargetToggle>
-                    <View
-                      style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
-                      <Toggle.Radio />
-                      <Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
-                        <Trans>Forever</Trans>
-                      </Toggle.LabelText>
-                    </View>
-                  </TargetToggle>
-                </Toggle.Item>
+                <View
+                  style={[
+                    a.flex_1,
+                    a.flex_row,
+                    a.justify_start,
+                    a.align_center,
+                    a.gap_sm,
+                  ]}>
+                  <Toggle.Item
+                    label={_(msg`Mute this word until you unmute it`)}
+                    name="forever"
+                    style={[a.flex_1]}>
+                    <TargetToggle>
+                      <View
+                        style={[
+                          a.flex_1,
+                          a.flex_row,
+                          a.align_center,
+                          a.gap_sm,
+                        ]}>
+                        <Toggle.Radio />
+                        <Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
+                          <Trans>Forever</Trans>
+                        </Toggle.LabelText>
+                      </View>
+                    </TargetToggle>
+                  </Toggle.Item>
+
+                  <Toggle.Item
+                    label={_(msg`Mute this word for 24 hours`)}
+                    name="24_hours"
+                    style={[a.flex_1]}>
+                    <TargetToggle>
+                      <View
+                        style={[
+                          a.flex_1,
+                          a.flex_row,
+                          a.align_center,
+                          a.gap_sm,
+                        ]}>
+                        <Toggle.Radio />
+                        <Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
+                          <Trans>24 hours</Trans>
+                        </Toggle.LabelText>
+                      </View>
+                    </TargetToggle>
+                  </Toggle.Item>
+                </View>
 
-                <Toggle.Item
-                  label={_(msg`Mute this word for 24 hours`)}
-                  name="24_hours"
-                  style={[a.flex_1]}>
-                  <TargetToggle>
-                    <View
-                      style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
-                      <Toggle.Radio />
-                      <Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
-                        <Trans>24 hours</Trans>
-                      </Toggle.LabelText>
-                    </View>
-                  </TargetToggle>
-                </Toggle.Item>
+                <View
+                  style={[
+                    a.flex_1,
+                    a.flex_row,
+                    a.justify_start,
+                    a.align_center,
+                    a.gap_sm,
+                  ]}>
+                  <Toggle.Item
+                    label={_(msg`Mute this word for 7 days`)}
+                    name="7_days"
+                    style={[a.flex_1]}>
+                    <TargetToggle>
+                      <View
+                        style={[
+                          a.flex_1,
+                          a.flex_row,
+                          a.align_center,
+                          a.gap_sm,
+                        ]}>
+                        <Toggle.Radio />
+                        <Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
+                          <Trans>7 days</Trans>
+                        </Toggle.LabelText>
+                      </View>
+                    </TargetToggle>
+                  </Toggle.Item>
+
+                  <Toggle.Item
+                    label={_(msg`Mute this word for 30 days`)}
+                    name="30_days"
+                    style={[a.flex_1]}>
+                    <TargetToggle>
+                      <View
+                        style={[
+                          a.flex_1,
+                          a.flex_row,
+                          a.align_center,
+                          a.gap_sm,
+                        ]}>
+                        <Toggle.Radio />
+                        <Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
+                          <Trans>30 days</Trans>
+                        </Toggle.LabelText>
+                      </View>
+                    </TargetToggle>
+                  </Toggle.Item>
+                </View>
               </View>
+            </Toggle.Group>
 
-              <View
+            <Toggle.Group
+              label={_(
+                msg`Select what content this mute word should apply to.`,
+              )}
+              type="radio"
+              values={targets}
+              onChange={setTargets}>
+              <Text
                 style={[
-                  a.flex_1,
-                  a.flex_row,
-                  a.justify_start,
-                  a.align_center,
-                  a.gap_sm,
+                  a.pb_xs,
+                  a.text_sm,
+                  a.font_bold,
+                  t.atoms.text_contrast_medium,
                 ]}>
+                <Trans>Mute in:</Trans>
+              </Text>
+
+              <View style={[a.flex_row, a.align_center, a.gap_sm, a.flex_wrap]}>
                 <Toggle.Item
-                  label={_(msg`Mute this word for 7 days`)}
-                  name="7_days"
+                  label={_(msg`Mute this word in post text and tags`)}
+                  name="content"
                   style={[a.flex_1]}>
                   <TargetToggle>
                     <View
                       style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
                       <Toggle.Radio />
                       <Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
-                        <Trans>7 days</Trans>
+                        <Trans>Text & tags</Trans>
                       </Toggle.LabelText>
                     </View>
+                    <PageText size="sm" />
                   </TargetToggle>
                 </Toggle.Item>
 
                 <Toggle.Item
-                  label={_(msg`Mute this word for 30 days`)}
-                  name="30_days"
+                  label={_(msg`Mute this word in tags only`)}
+                  name="tag"
                   style={[a.flex_1]}>
                   <TargetToggle>
                     <View
                       style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
                       <Toggle.Radio />
                       <Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
-                        <Trans>30 days</Trans>
+                        <Trans>Tags only</Trans>
                       </Toggle.LabelText>
                     </View>
+                    <Hashtag size="sm" />
                   </TargetToggle>
                 </Toggle.Item>
               </View>
-            </View>
-          </Toggle.Group>
+            </Toggle.Group>
 
-          <Toggle.Group
-            label={_(msg`Select what content this mute word should apply to.`)}
-            type="radio"
-            values={targets}
-            onChange={setTargets}>
-            <Text
-              style={[
-                a.pb_xs,
-                a.text_sm,
-                a.font_bold,
-                t.atoms.text_contrast_medium,
-              ]}>
-              <Trans>Mute in:</Trans>
-            </Text>
-
-            <View style={[a.flex_row, a.align_center, a.gap_sm, a.flex_wrap]}>
+            <View>
+              <Text
+                style={[
+                  a.pb_xs,
+                  a.text_sm,
+                  a.font_bold,
+                  t.atoms.text_contrast_medium,
+                ]}>
+                <Trans>Options:</Trans>
+              </Text>
               <Toggle.Item
-                label={_(msg`Mute this word in post text and tags`)}
-                name="content"
-                style={[a.flex_1]}>
+                label={_(msg`Do not apply this mute word to users you follow`)}
+                name="exclude_following"
+                style={[a.flex_row, a.justify_between]}
+                value={excludeFollowing}
+                onChange={setExcludeFollowing}>
                 <TargetToggle>
                   <View
                     style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
-                    <Toggle.Radio />
+                    <Toggle.Checkbox />
                     <Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
-                      <Trans>Text & tags</Trans>
+                      <Trans>Exclude users you follow</Trans>
                     </Toggle.LabelText>
                   </View>
-                  <PageText size="sm" />
                 </TargetToggle>
               </Toggle.Item>
+            </View>
 
-              <Toggle.Item
-                label={_(msg`Mute this word in tags only`)}
-                name="tag"
-                style={[a.flex_1]}>
-                <TargetToggle>
-                  <View
-                    style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
-                    <Toggle.Radio />
-                    <Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
-                      <Trans>Tags only</Trans>
-                    </Toggle.LabelText>
-                  </View>
-                  <Hashtag size="sm" />
-                </TargetToggle>
-              </Toggle.Item>
+            <View style={[a.pt_xs]}>
+              <Button
+                disabled={isPending || !field}
+                label={_(msg`Add mute word for configured settings`)}
+                size="large"
+                color="primary"
+                variant="solid"
+                style={[]}
+                onPress={submit}>
+                <ButtonText>
+                  <Trans>Add</Trans>
+                </ButtonText>
+                <ButtonIcon icon={isPending ? Loader : Plus} position="right" />
+              </Button>
             </View>
-          </Toggle.Group>
 
-          <View>
+            {error && (
+              <View
+                style={[
+                  a.mb_lg,
+                  a.flex_row,
+                  a.rounded_sm,
+                  a.p_md,
+                  a.mb_xs,
+                  t.atoms.bg_contrast_25,
+                  {
+                    backgroundColor: t.palette.negative_400,
+                  },
+                ]}>
+                <Text
+                  style={[
+                    a.italic,
+                    {color: t.palette.white},
+                    native({marginTop: 2}),
+                  ]}>
+                  {error}
+                </Text>
+              </View>
+            )}
+          </View>
+
+          <Divider />
+
+          <View style={[a.pt_2xl]}>
             <Text
               style={[
-                a.pb_xs,
-                a.text_sm,
+                a.text_md,
                 a.font_bold,
-                t.atoms.text_contrast_medium,
+                a.pb_md,
+                t.atoms.text_contrast_high,
               ]}>
-              <Trans>Options:</Trans>
+              <Trans>Your muted words</Trans>
             </Text>
-            <Toggle.Item
-              label={_(msg`Do not apply this mute word to users you follow`)}
-              name="exclude_following"
-              style={[a.flex_row, a.justify_between]}
-              value={excludeFollowing}
-              onChange={setExcludeFollowing}>
-              <TargetToggle>
-                <View style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
-                  <Toggle.Checkbox />
-                  <Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
-                    <Trans>Exclude users you follow</Trans>
-                  </Toggle.LabelText>
-                </View>
-              </TargetToggle>
-            </Toggle.Item>
-          </View>
-
-          <View style={[a.pt_xs]}>
-            <Button
-              disabled={isPending || !field}
-              label={_(msg`Add mute word for configured settings`)}
-              size="large"
-              color="primary"
-              variant="solid"
-              style={[]}
-              onPress={submit}>
-              <ButtonText>
-                <Trans>Add</Trans>
-              </ButtonText>
-              <ButtonIcon icon={isPending ? Loader : Plus} position="right" />
-            </Button>
-          </View>
 
-          {error && (
-            <View
-              style={[
-                a.mb_lg,
-                a.flex_row,
-                a.rounded_sm,
-                a.p_md,
-                a.mb_xs,
-                t.atoms.bg_contrast_25,
-                {
-                  backgroundColor: t.palette.negative_400,
-                },
-              ]}>
-              <Text
+            {isPreferencesLoading ? (
+              <Loader />
+            ) : preferencesError || !preferences ? (
+              <View
                 style={[
-                  a.italic,
-                  {color: t.palette.white},
-                  native({marginTop: 2}),
+                  a.py_md,
+                  a.px_lg,
+                  a.rounded_md,
+                  t.atoms.bg_contrast_25,
                 ]}>
-                {error}
-              </Text>
-            </View>
-          )}
-        </View>
-
-        <Divider />
-
-        <View style={[a.pt_2xl]}>
-          <Text
-            style={[
-              a.text_md,
-              a.font_bold,
-              a.pb_md,
-              t.atoms.text_contrast_high,
-            ]}>
-            <Trans>Your muted words</Trans>
-          </Text>
+                <Text style={[a.italic, t.atoms.text_contrast_high]}>
+                  <Trans>
+                    We're sorry, but we weren't able to load your muted words at
+                    this time. Please try again.
+                  </Trans>
+                </Text>
+              </View>
+            ) : preferences.moderationPrefs.mutedWords.length ? (
+              [...preferences.moderationPrefs.mutedWords]
+                .reverse()
+                .map((word, i) => (
+                  <MutedWordRow
+                    key={word.value + i}
+                    word={word}
+                    style={[i % 2 === 0 && t.atoms.bg_contrast_25]}
+                  />
+                ))
+            ) : (
+              <View
+                style={[
+                  a.py_md,
+                  a.px_lg,
+                  a.rounded_md,
+                  t.atoms.bg_contrast_25,
+                ]}>
+                <Text style={[a.italic, t.atoms.text_contrast_high]}>
+                  <Trans>You haven't muted any words or tags yet</Trans>
+                </Text>
+              </View>
+            )}
+          </View>
 
-          {isPreferencesLoading ? (
-            <Loader />
-          ) : preferencesError || !preferences ? (
-            <View
-              style={[a.py_md, a.px_lg, a.rounded_md, t.atoms.bg_contrast_25]}>
-              <Text style={[a.italic, t.atoms.text_contrast_high]}>
-                <Trans>
-                  We're sorry, but we weren't able to load your muted words at
-                  this time. Please try again.
-                </Trans>
-              </Text>
-            </View>
-          ) : preferences.moderationPrefs.mutedWords.length ? (
-            [...preferences.moderationPrefs.mutedWords]
-              .reverse()
-              .map((word, i) => (
-                <MutedWordRow
-                  key={word.value + i}
-                  word={word}
-                  style={[i % 2 === 0 && t.atoms.bg_contrast_25]}
-                />
-              ))
-          ) : (
-            <View
-              style={[a.py_md, a.px_lg, a.rounded_md, t.atoms.bg_contrast_25]}>
-              <Text style={[a.italic, t.atoms.text_contrast_high]}>
-                <Trans>You haven't muted any words or tags yet</Trans>
-              </Text>
-            </View>
-          )}
+          {isNative && <View style={{height: 20}} />}
         </View>
 
-        {isNative && <View style={{height: 20}} />}
-      </View>
+        <Dialog.Close />
+      </Dialog.ScrollableInner>
 
-      <Dialog.Close />
-    </Dialog.ScrollableInner>
+      <Portal.Outlet />
+    </Portal.Provider>
   )
 }
 
@@ -437,6 +482,7 @@ function MutedWordRow({
         onConfirm={remove}
         confirmButtonCta={_(msg`Remove`)}
         confirmButtonColor="negative"
+        Portal={Portal.Portal}
       />
 
       <View
diff --git a/src/components/dialogs/PostInteractionSettingsDialog.tsx b/src/components/dialogs/PostInteractionSettingsDialog.tsx
index 47eefae6f..bddc49968 100644
--- a/src/components/dialogs/PostInteractionSettingsDialog.tsx
+++ b/src/components/dialogs/PostInteractionSettingsDialog.tsx
@@ -37,6 +37,7 @@ import * as Toggle from '#/components/forms/Toggle'
 import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
 import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
 import {Loader} from '#/components/Loader'
+import {PortalComponent} from '#/components/Portal'
 import {Text} from '#/components/Typography'
 
 export type PostInteractionSettingsFormProps = {
@@ -54,13 +55,15 @@ export type PostInteractionSettingsFormProps = {
 
 export function PostInteractionSettingsControlledDialog({
   control,
+  Portal,
   ...rest
 }: PostInteractionSettingsFormProps & {
   control: Dialog.DialogControlProps
+  Portal?: PortalComponent
 }) {
   const {_} = useLingui()
   return (
-    <Dialog.Outer control={control}>
+    <Dialog.Outer control={control} Portal={Portal}>
       <Dialog.Handle />
       <Dialog.ScrollableInner
         label={_(msg`Edit post interaction settings`)}
@@ -231,7 +234,6 @@ export function PostInteractionSettingsForm({
 }: PostInteractionSettingsFormProps) {
   const t = useTheme()
   const {_} = useLingui()
-  const control = Dialog.useDialogContext()
   const {data: lists} = useMyListsQuery('curate')
   const [quotesEnabled, setQuotesEnabled] = React.useState(
     !(
@@ -437,7 +439,6 @@ export function PostInteractionSettingsForm({
       <Button
         label={_(msg`Save`)}
         onPress={onSave}
-        onAccessibilityEscape={control.close}
         color="primary"
         size="large"
         variant="solid"
diff --git a/src/components/dialogs/SwitchAccount.tsx b/src/components/dialogs/SwitchAccount.tsx
index 0bd4bcb8c..ea870e2da 100644
--- a/src/components/dialogs/SwitchAccount.tsx
+++ b/src/components/dialogs/SwitchAccount.tsx
@@ -43,7 +43,6 @@ export function SwitchAccountDialog({
   return (
     <Dialog.Outer control={control}>
       <Dialog.Handle />
-
       <Dialog.ScrollableInner label={_(msg`Switch Account`)}>
         <View style={[a.gap_lg]}>
           <Text style={[a.text_2xl, a.font_bold]}>
diff --git a/src/components/dialogs/nuxs/NeueTypography.tsx b/src/components/dialogs/nuxs/NeueTypography.tsx
index f160c8774..f29dc356d 100644
--- a/src/components/dialogs/nuxs/NeueTypography.tsx
+++ b/src/components/dialogs/nuxs/NeueTypography.tsx
@@ -44,7 +44,6 @@ export function NeueTypography() {
   return (
     <Dialog.Outer control={control} onClose={onClose}>
       <Dialog.Handle />
-
       <Dialog.ScrollableInner label={_(msg`Introducing new font settings`)}>
         <View style={[a.gap_xl]}>
           <View style={[a.gap_md]}>
diff --git a/src/components/dms/ConvoMenu.tsx b/src/components/dms/ConvoMenu.tsx
index a4fa625fa..affc292c1 100644
--- a/src/components/dms/ConvoMenu.tsx
+++ b/src/components/dms/ConvoMenu.tsx
@@ -136,7 +136,7 @@ let ConvoMenu = ({
           <Menu.Outer>
             <Menu.Item
               label={_(msg`Leave conversation`)}
-              onPress={leaveConvoControl.open}>
+              onPress={() => leaveConvoControl.open()}>
               <Menu.ItemText>
                 <Trans>Leave conversation</Trans>
               </Menu.ItemText>
@@ -195,7 +195,7 @@ let ConvoMenu = ({
               </Menu.Item>
               <Menu.Item
                 label={_(msg`Report conversation`)}
-                onPress={reportControl.open}>
+                onPress={() => reportControl.open()}>
                 <Menu.ItemText>
                   <Trans>Report conversation</Trans>
                 </Menu.ItemText>
@@ -206,7 +206,7 @@ let ConvoMenu = ({
             <Menu.Group>
               <Menu.Item
                 label={_(msg`Leave conversation`)}
-                onPress={leaveConvoControl.open}>
+                onPress={() => leaveConvoControl.open()}>
                 <Menu.ItemText>
                   <Trans>Leave conversation</Trans>
                 </Menu.ItemText>
diff --git a/src/components/dms/MessageMenu.tsx b/src/components/dms/MessageMenu.tsx
index 2978d2b22..8680a68bf 100644
--- a/src/components/dms/MessageMenu.tsx
+++ b/src/components/dms/MessageMenu.tsx
@@ -7,11 +7,11 @@ import {useLingui} from '@lingui/react'
 
 import {richTextToString} from '#/lib/strings/rich-text-helpers'
 import {getTranslatorLink} from '#/locale/helpers'
+import {isWeb} from '#/platform/detection'
+import {useConvoActive} from '#/state/messages/convo'
 import {useLanguagePrefs} from '#/state/preferences'
 import {useOpenLink} from '#/state/preferences/in-app-browser'
-import {isWeb} from 'platform/detection'
-import {useConvoActive} from 'state/messages/convo'
-import {useSession} from 'state/session'
+import {useSession} from '#/state/session'
 import * as Toast from '#/view/com/util/Toast'
 import {atoms as a, useTheme} from '#/alf'
 import {ReportDialog} from '#/components/dms/ReportDialog'
@@ -120,7 +120,7 @@ export let MessageMenu = ({
             <Menu.Item
               testID="messageDropdownDeleteBtn"
               label={_(msg`Delete message for me`)}
-              onPress={deleteControl.open}>
+              onPress={() => deleteControl.open()}>
               <Menu.ItemText>{_(msg`Delete for me`)}</Menu.ItemText>
               <Menu.ItemIcon icon={Trash} position="right" />
             </Menu.Item>
@@ -128,7 +128,7 @@ export let MessageMenu = ({
               <Menu.Item
                 testID="messageDropdownReportBtn"
                 label={_(msg`Report message`)}
-                onPress={reportControl.open}>
+                onPress={() => reportControl.open()}>
                 <Menu.ItemText>{_(msg`Report`)}</Menu.ItemText>
                 <Menu.ItemIcon icon={Warning} position="right" />
               </Menu.Item>
diff --git a/src/components/dms/ReportDialog.tsx b/src/components/dms/ReportDialog.tsx
index 2dcd77854..06d69ff4b 100644
--- a/src/components/dms/ReportDialog.tsx
+++ b/src/components/dms/ReportDialog.tsx
@@ -10,13 +10,11 @@ import {useLingui} from '@lingui/react'
 import {useMutation} from '@tanstack/react-query'
 
 import {ReportOption} from '#/lib/moderation/useReportOptions'
-import {isAndroid} from '#/platform/detection'
 import {useAgent} from '#/state/session'
 import {CharProgress} from '#/view/com/composer/char-progress/CharProgress'
 import * as Toast from '#/view/com/util/Toast'
 import {atoms as a, useBreakpoints, useTheme} from '#/alf'
 import * as Dialog from '#/components/Dialog'
-import {KeyboardControllerPadding} from '#/components/KeyboardControllerPadding'
 import {Button, ButtonIcon, ButtonText} from '../Button'
 import {Divider} from '../Divider'
 import {ChevronLeft_Stroke2_Corner0_Rounded as Chevron} from '../icons/Chevron'
@@ -41,14 +39,11 @@ let ReportDialog = ({
 }): React.ReactNode => {
   const {_} = useLingui()
   return (
-    <Dialog.Outer
-      control={control}
-      nativeOptions={isAndroid ? {sheet: {snapPoints: ['100%']}} : {}}>
+    <Dialog.Outer control={control}>
       <Dialog.Handle />
       <Dialog.ScrollableInner label={_(msg`Report this message`)}>
         <DialogInner params={params} />
         <Dialog.Close />
-        <KeyboardControllerPadding />
       </Dialog.ScrollableInner>
     </Dialog.Outer>
   )
diff --git a/src/components/dms/dialogs/NewChatDialog.tsx b/src/components/dms/dialogs/NewChatDialog.tsx
index 19f6eb6df..e80fef2d7 100644
--- a/src/components/dms/dialogs/NewChatDialog.tsx
+++ b/src/components/dms/dialogs/NewChatDialog.tsx
@@ -2,9 +2,9 @@ import React, {useCallback} from 'react'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
+import {logEvent} from '#/lib/statsig/statsig'
 import {logger} from '#/logger'
 import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
-import {logEvent} from 'lib/statsig/statsig'
 import {FAB} from '#/view/com/util/fab/FAB'
 import * as Toast from '#/view/com/util/Toast'
 import {useTheme} from '#/alf'
@@ -55,10 +55,8 @@ export function NewChat({
         accessibilityHint=""
       />
 
-      <Dialog.Outer
-        control={control}
-        testID="newChatDialog"
-        nativeOptions={{sheet: {snapPoints: ['100%']}}}>
+      <Dialog.Outer control={control} testID="newChatDialog">
+        <Dialog.Handle />
         <SearchablePeopleList
           title={_(msg`Start a new chat`)}
           onSelectChat={onCreateChat}
diff --git a/src/components/dms/dialogs/SearchablePeopleList.tsx b/src/components/dms/dialogs/SearchablePeopleList.tsx
index a13dfe509..a5687a096 100644
--- a/src/components/dms/dialogs/SearchablePeopleList.tsx
+++ b/src/components/dms/dialogs/SearchablePeopleList.tsx
@@ -5,10 +5,8 @@ import React, {
   useRef,
   useState,
 } from 'react'
-import type {TextInput as TextInputType} from 'react-native'
-import {View} from 'react-native'
+import {TextInput, View} from 'react-native'
 import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api'
-import {BottomSheetFlatListMethods} from '@discord/bottom-sheet'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
@@ -16,18 +14,17 @@ import {sanitizeDisplayName} from '#/lib/strings/display-names'
 import {sanitizeHandle} from '#/lib/strings/handles'
 import {isWeb} from '#/platform/detection'
 import {useModerationOpts} from '#/state/preferences/moderation-opts'
+import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
 import {useListConvosQuery} from '#/state/queries/messages/list-converations'
 import {useProfileFollowsQuery} from '#/state/queries/profile-follows'
 import {useSession} from '#/state/session'
-import {useActorAutocompleteQuery} from 'state/queries/actor-autocomplete'
+import {ListMethods} from '#/view/com/util/List'
 import {UserAvatar} from '#/view/com/util/UserAvatar'
 import {atoms as a, native, useTheme, web} from '#/alf'
-import {Button} from '#/components/Button'
+import {Button, ButtonIcon} from '#/components/Button'
 import * as Dialog from '#/components/Dialog'
-import {TextInput} from '#/components/dms/dialogs/TextInput'
 import {canBeMessaged} from '#/components/dms/util'
 import {useInteractionState} from '#/components/hooks/useInteractionState'
-import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
 import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
 import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
 import {Text} from '#/components/Typography'
@@ -66,9 +63,9 @@ export function SearchablePeopleList({
   const {_} = useLingui()
   const moderationOpts = useModerationOpts()
   const control = Dialog.useDialogContext()
-  const listRef = useRef<BottomSheetFlatListMethods>(null)
+  const listRef = useRef<ListMethods>(null)
   const {currentAccount} = useSession()
-  const inputRef = useRef<TextInputType>(null)
+  const inputRef = useRef<TextInput>(null)
 
   const [searchText, setSearchText] = useState('')
 
@@ -101,15 +98,15 @@ export function SearchablePeopleList({
           })
         }
 
-        _items = _items.sort(a => {
+        _items = _items.sort(item => {
           // @ts-ignore
-          return a.enabled ? -1 : 1
+          return item.enabled ? -1 : 1
         })
       }
     } else {
       const placeholders: Item[] = Array(10)
         .fill(0)
-        .map((_, i) => ({
+        .map((__, i) => ({
           type: 'placeholder',
           key: i + '',
         }))
@@ -155,9 +152,9 @@ export function SearchablePeopleList({
           }
 
           // only sort follows
-          followsItems = followsItems.sort(a => {
+          followsItems = followsItems.sort(item => {
             // @ts-ignore
-            return a.enabled ? -1 : 1
+            return item.enabled ? -1 : 1
           })
 
           // then append
@@ -177,9 +174,9 @@ export function SearchablePeopleList({
           }
         }
 
-        _items = _items.sort(a => {
+        _items = _items.sort(item => {
           // @ts-ignore
-          return a.enabled ? -1 : 1
+          return item.enabled ? -1 : 1
         })
       } else {
         _items.push(...placeholders)
@@ -242,57 +239,46 @@ export function SearchablePeopleList({
       <View
         style={[
           a.relative,
-          a.pt_md,
+          web(a.pt_lg),
+          native(a.pt_4xl),
           a.pb_xs,
           a.px_lg,
           a.border_b,
           t.atoms.border_contrast_low,
           t.atoms.bg,
-          native([a.pt_lg]),
         ]}>
-        <View
-          style={[
-            a.relative,
-            native(a.align_center),
-            a.justify_center,
-            {height: 32},
-          ]}>
-          <Button
-            label={_(msg`Close`)}
-            size="small"
-            shape="round"
-            variant="ghost"
-            color="secondary"
-            style={[
-              a.absolute,
-              a.z_20,
-              native({
-                left: -7,
-              }),
-              web({
-                right: -4,
-              }),
-            ]}
-            onPress={() => control.close()}>
-            {isWeb ? (
-              <X size="md" fill={t.palette.contrast_500} />
-            ) : (
-              <ChevronLeft size="md" fill={t.palette.contrast_500} />
-            )}
-          </Button>
+        <View style={[a.relative, native(a.align_center), a.justify_center]}>
           <Text
             style={[
               a.z_10,
               a.text_lg,
-              a.font_bold,
+              a.font_heavy,
               a.leading_tight,
               t.atoms.text_contrast_high,
             ]}>
             {title}
           </Text>
+          {isWeb ? (
+            <Button
+              label={_(msg`Close`)}
+              size="small"
+              shape="round"
+              variant={isWeb ? 'ghost' : 'solid'}
+              color="secondary"
+              style={[
+                a.absolute,
+                a.z_20,
+                web({right: -4}),
+                native({right: 0}),
+                native({height: 32, width: 32, borderRadius: 16}),
+              ]}
+              onPress={() => control.close()}>
+              <ButtonIcon icon={X} size="md" />
+            </Button>
+          ) : null}
         </View>
 
-        <View style={[native([a.pt_sm]), web([a.pt_xs])]}>
+        <View style={[, web([a.pt_xs])]}>
           <SearchInput
             inputRef={inputRef}
             value={searchText}
@@ -309,7 +295,6 @@ export function SearchablePeopleList({
     t.atoms.border_contrast_low,
     t.atoms.bg,
     t.atoms.text_contrast_high,
-    t.palette.contrast_500,
     _,
     title,
     searchText,
@@ -326,14 +311,7 @@ export function SearchablePeopleList({
       keyExtractor={(item: Item) => item.key}
       style={[
         web([a.py_0, {height: '100vh', maxHeight: 600}, a.px_0]),
-        native({
-          height: '100%',
-          paddingHorizontal: 0,
-          marginTop: 0,
-          paddingTop: 0,
-          borderTopLeftRadius: 40,
-          borderTopRightRadius: 40,
-        }),
+        native({height: '100%'}),
       ]}
       webInnerStyle={[a.py_0, {maxWidth: 500, minWidth: 200}]}
       keyboardDismissMode="on-drag"
@@ -396,7 +374,8 @@ function ProfileCard({
           <View style={[a.flex_1, a.gap_2xs]}>
             <Text
               style={[t.atoms.text, a.font_bold, a.leading_tight, a.self_start]}
-              numberOfLines={1}>
+              numberOfLines={1}
+              emoji>
               {displayName}
             </Text>
             <Text
@@ -474,7 +453,7 @@ function SearchInput({
   value: string
   onChangeText: (text: string) => void
   onEscape: () => void
-  inputRef: React.RefObject<TextInputType>
+  inputRef: React.RefObject<TextInput>
 }) {
   const t = useTheme()
   const {_} = useLingui()
diff --git a/src/components/dms/dialogs/ShareViaChatDialog.tsx b/src/components/dms/dialogs/ShareViaChatDialog.tsx
index 01906a430..38b558343 100644
--- a/src/components/dms/dialogs/ShareViaChatDialog.tsx
+++ b/src/components/dms/dialogs/ShareViaChatDialog.tsx
@@ -2,9 +2,9 @@ import React, {useCallback} from 'react'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
+import {logEvent} from '#/lib/statsig/statsig'
 import {logger} from '#/logger'
 import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
-import {logEvent} from 'lib/statsig/statsig'
 import * as Toast from '#/view/com/util/Toast'
 import * as Dialog from '#/components/Dialog'
 import {SearchablePeopleList} from './SearchablePeopleList'
@@ -17,10 +17,8 @@ export function SendViaChatDialog({
   onSelectChat: (chatId: string) => void
 }) {
   return (
-    <Dialog.Outer
-      control={control}
-      testID="sendViaChatChatDialog"
-      nativeOptions={{sheet: {snapPoints: ['100%']}}}>
+    <Dialog.Outer control={control} testID="sendViaChatChatDialog">
+      <Dialog.Handle />
       <SendViaChatDialogInner control={control} onSelectChat={onSelectChat} />
     </Dialog.Outer>
   )
diff --git a/src/components/forms/Toggle.tsx b/src/components/forms/Toggle.tsx
index 6dc387b23..4e3695bbf 100644
--- a/src/components/forms/Toggle.tsx
+++ b/src/components/forms/Toggle.tsx
@@ -2,8 +2,8 @@ import React from 'react'
 import {Pressable, View, ViewStyle} from 'react-native'
 import Animated, {LinearTransition} from 'react-native-reanimated'
 
+import {HITSLOP_10} from '#/lib/constants'
 import {isNative} from '#/platform/detection'
-import {HITSLOP_10} from 'lib/constants'
 import {
   atoms as a,
   flatten,
diff --git a/src/components/moderation/LabelsOnMeDialog.tsx b/src/components/moderation/LabelsOnMeDialog.tsx
index e63cea93b..bf0d1905e 100644
--- a/src/components/moderation/LabelsOnMeDialog.tsx
+++ b/src/components/moderation/LabelsOnMeDialog.tsx
@@ -32,7 +32,6 @@ export function LabelsOnMeDialog(props: LabelsOnMeDialogProps) {
   return (
     <Dialog.Outer control={props.control}>
       <Dialog.Handle />
-
       <LabelsOnMeDialogInner {...props} />
     </Dialog.Outer>
   )
@@ -158,23 +157,25 @@ function Label({
       <Divider />
 
       <View style={[a.px_md, a.py_sm, t.atoms.bg_contrast_25]}>
-        <Text style={[t.atoms.text_contrast_medium]}>
-          {isSelfLabel ? (
+        {isSelfLabel ? (
+          <Text style={[t.atoms.text_contrast_medium]}>
             <Trans>This label was applied by you.</Trans>
-          ) : (
-            <Trans>
-              Source:{' '}
-              <InlineLinkText
-                label={sourceName}
-                to={makeProfileLink(
-                  labeler ? labeler.creator : {did: label.src, handle: ''},
-                )}
-                onPress={() => control.close()}>
-                {sourceName}
-              </InlineLinkText>
-            </Trans>
-          )}
-        </Text>
+          </Text>
+        ) : (
+          <View style={{flexDirection: 'row'}}>
+            <Text style={[t.atoms.text_contrast_medium]}>
+              <Trans>Source: </Trans>{' '}
+            </Text>
+            <InlineLinkText
+              label={sourceName}
+              to={makeProfileLink(
+                labeler ? labeler.creator : {did: label.src, handle: ''},
+              )}
+              onPress={() => control.close()}>
+              {sourceName}
+            </InlineLinkText>
+          </View>
+        )}
       </View>
     </View>
   )
@@ -236,24 +237,24 @@ function AppealForm({
 
   return (
     <>
-      <Text style={[a.text_2xl, a.font_bold, a.pb_xs, a.leading_tight]}>
-        <Trans>Appeal "{strings.name}" label</Trans>
-      </Text>
-      <Text style={[a.text_md, a.leading_snug]}>
-        <Trans>
-          This appeal will be sent to{' '}
-          <InlineLinkText
-            label={sourceName}
-            to={makeProfileLink(
-              labeler ? labeler.creator : {did: label.src, handle: ''},
-            )}
-            onPress={() => control.close()}
-            style={[a.text_md, a.leading_snug]}>
-            {sourceName}
-          </InlineLinkText>
-          .
-        </Trans>
-      </Text>
+      <View style={{flexWrap: 'wrap', flexDirection: 'row'}}>
+        <Text style={[a.text_2xl, a.font_bold, a.pb_xs, a.leading_tight]}>
+          <Trans>Appeal "{strings.name}" label</Trans>
+        </Text>
+        <Text style={[a.text_md, a.leading_snug]}>
+          <Trans>This appeal will be sent to</Trans>{' '}
+        </Text>
+        <InlineLinkText
+          label={sourceName}
+          to={makeProfileLink(
+            labeler ? labeler.creator : {did: label.src, handle: ''},
+          )}
+          onPress={() => control.close()}
+          style={[a.text_md, a.leading_snug]}>
+          {sourceName}
+        </InlineLinkText>
+        <Text style={[a.text_md, a.leading_snug]}>.</Text>
+      </View>
       <View style={[a.my_md]}>
         <Dialog.Input
           label={_(msg`Text input field`)}
diff --git a/src/components/moderation/ModerationDetailsDialog.tsx b/src/components/moderation/ModerationDetailsDialog.tsx
index 225917853..0a1fae67e 100644
--- a/src/components/moderation/ModerationDetailsDialog.tsx
+++ b/src/components/moderation/ModerationDetailsDialog.tsx
@@ -141,23 +141,24 @@ function ModerationDetailsDialogInner({
       {modcause?.type === 'label' && (
         <View style={[a.pt_lg]}>
           <Divider />
-          <Text style={[t.atoms.text, a.text_md, a.leading_snug, a.mt_lg]}>
-            {modcause.source.type === 'user' ? (
+          {modcause.source.type === 'user' ? (
+            <Text style={[t.atoms.text, a.text_md, a.leading_snug, a.mt_lg]}>
               <Trans>This label was applied by the author.</Trans>
-            ) : (
-              <Trans>
-                This label was applied by{' '}
-                <InlineLinkText
-                  label={desc.source || _(msg`an unknown labeler`)}
-                  to={makeProfileLink({did: modcause.label.src, handle: ''})}
-                  onPress={() => control.close()}
-                  style={a.text_md}>
-                  {desc.source || _(msg`an unknown labeler`)}
-                </InlineLinkText>
-                .
-              </Trans>
-            )}
-          </Text>
+            </Text>
+          ) : (
+            <>
+              <Text style={[t.atoms.text, a.text_md, a.leading_snug, a.mt_lg]}>
+                <Trans>This label was applied by </Trans>
+              </Text>
+              <InlineLinkText
+                label={desc.source || _(msg`an unknown labeler`)}
+                to={makeProfileLink({did: modcause.label.src, handle: ''})}
+                onPress={() => control.close()}
+                style={a.text_md}>
+                {desc.source || _(msg`an unknown labeler`)}
+              </InlineLinkText>
+            </>
+          )}
         </View>
       )}
 
diff --git a/src/lib/media/video/upload.ts b/src/lib/media/video/upload.ts
index 3330370b3..720283a8d 100644
--- a/src/lib/media/video/upload.ts
+++ b/src/lib/media/video/upload.ts
@@ -7,8 +7,8 @@ import {nanoid} from 'nanoid/non-secure'
 import {AbortError} from '#/lib/async/cancelable'
 import {ServerError} from '#/lib/media/video/errors'
 import {CompressedVideo} from '#/lib/media/video/types'
-import {createVideoEndpointUrl, mimeToExt} from './util'
 import {getServiceAuthToken, getVideoUploadLimits} from './upload.shared'
+import {createVideoEndpointUrl, mimeToExt} from './util'
 
 export async function uploadVideo({
   video,
diff --git a/src/lib/media/video/upload.web.ts b/src/lib/media/video/upload.web.ts
index ec65f96c9..d1b441a36 100644
--- a/src/lib/media/video/upload.web.ts
+++ b/src/lib/media/video/upload.web.ts
@@ -7,8 +7,8 @@ import {nanoid} from 'nanoid/non-secure'
 import {AbortError} from '#/lib/async/cancelable'
 import {ServerError} from '#/lib/media/video/errors'
 import {CompressedVideo} from '#/lib/media/video/types'
-import {createVideoEndpointUrl, mimeToExt} from './util'
 import {getServiceAuthToken, getVideoUploadLimits} from './upload.shared'
+import {createVideoEndpointUrl, mimeToExt} from './util'
 
 export async function uploadVideo({
   video,
diff --git a/src/screens/Onboarding/StepProfile/index.tsx b/src/screens/Onboarding/StepProfile/index.tsx
index 663418f22..73472ec33 100644
--- a/src/screens/Onboarding/StepProfile/index.tsx
+++ b/src/screens/Onboarding/StepProfile/index.tsx
@@ -32,6 +32,7 @@ import {
 import {atoms as a, useBreakpoints, useTheme} from '#/alf'
 import {Button, ButtonIcon, ButtonText} from '#/components/Button'
 import * as Dialog from '#/components/Dialog'
+import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper'
 import {IconCircle} from '#/components/IconCircle'
 import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
 import {CircleInfo_Stroke2_Corner0_Rounded} from '#/components/icons/CircleInfo'
@@ -89,15 +90,18 @@ export function StepProfile() {
     requestNotificationsPermission('StartOnboarding')
   }, [gate, requestNotificationsPermission])
 
+  const sheetWrapper = useSheetWrapper()
   const openPicker = React.useCallback(
     async (opts?: ImagePickerOptions) => {
-      const response = await launchImageLibraryAsync({
-        exif: false,
-        mediaTypes: MediaTypeOptions.Images,
-        quality: 1,
-        ...opts,
-        legacy: true,
-      })
+      const response = await sheetWrapper(
+        launchImageLibraryAsync({
+          exif: false,
+          mediaTypes: MediaTypeOptions.Images,
+          quality: 1,
+          ...opts,
+          legacy: true,
+        }),
+      )
 
       return (response.assets ?? [])
         .slice(0, 1)
@@ -121,7 +125,7 @@ export function StepProfile() {
           size: getDataUriSize(image.uri),
         }))
     },
-    [_, setError],
+    [_, setError, sheetWrapper],
   )
 
   const onContinue = React.useCallback(async () => {
@@ -168,9 +172,11 @@ export function StepProfile() {
 
     setError('')
 
-    const items = await openPicker({
-      aspect: [1, 1],
-    })
+    const items = await sheetWrapper(
+      openPicker({
+        aspect: [1, 1],
+      }),
+    )
     let image = items[0]
     if (!image) return
 
@@ -196,7 +202,13 @@ export function StepProfile() {
       image,
       useCreatedAvatar: false,
     }))
-  }, [requestPhotoAccessIfNeeded, setAvatar, openPicker, setError])
+  }, [
+    requestPhotoAccessIfNeeded,
+    setAvatar,
+    openPicker,
+    setError,
+    sheetWrapper,
+  ])
 
   const onSecondaryPress = React.useCallback(() => {
     if (avatar.useCreatedAvatar) {
@@ -286,7 +298,6 @@ export function StepProfile() {
       </View>
 
       <Dialog.Outer control={creatorControl}>
-        <Dialog.Handle />
         <Dialog.Inner
           label="Avatar creator"
           style={[
diff --git a/src/screens/StarterPack/StarterPackScreen.tsx b/src/screens/StarterPack/StarterPackScreen.tsx
index 0aa863f7b..68803ac00 100644
--- a/src/screens/StarterPack/StarterPackScreen.tsx
+++ b/src/screens/StarterPack/StarterPackScreen.tsx
@@ -15,35 +15,35 @@ import {useNavigation} from '@react-navigation/native'
 import {NativeStackScreenProps} from '@react-navigation/native-stack'
 import {useQueryClient} from '@tanstack/react-query'
 
+import {batchedUpdates} from '#/lib/batchedUpdates'
+import {HITSLOP_20} from '#/lib/constants'
+import {isBlockedOrBlocking, isMuted} from '#/lib/moderation/blocked-and-muted'
+import {makeProfileLink, makeStarterPackLink} from '#/lib/routes/links'
+import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types'
+import {logEvent} from '#/lib/statsig/statsig'
 import {cleanError} from '#/lib/strings/errors'
+import {getStarterPackOgCard} from '#/lib/strings/starter-pack'
 import {logger} from '#/logger'
+import {isWeb} from '#/platform/detection'
+import {updateProfileShadow} from '#/state/cache/profile-shadow'
+import {useModerationOpts} from '#/state/preferences/moderation-opts'
+import {getAllListMembers} from '#/state/queries/list-members'
+import {useResolvedStarterPackShortLink} from '#/state/queries/resolve-short-link'
+import {useResolveDidQuery} from '#/state/queries/resolve-uri'
+import {useShortenLink} from '#/state/queries/shorten-link'
 import {useDeleteStarterPackMutation} from '#/state/queries/starter-packs'
+import {useStarterPackQuery} from '#/state/queries/starter-packs'
+import {useAgent, useSession} from '#/state/session'
+import {useLoggedOutViewControls} from '#/state/shell/logged-out'
 import {
   ProgressGuideAction,
   useProgressGuideControls,
 } from '#/state/shell/progress-guide'
-import {batchedUpdates} from 'lib/batchedUpdates'
-import {HITSLOP_20} from 'lib/constants'
-import {isBlockedOrBlocking, isMuted} from 'lib/moderation/blocked-and-muted'
-import {makeProfileLink, makeStarterPackLink} from 'lib/routes/links'
-import {CommonNavigatorParams, NavigationProp} from 'lib/routes/types'
-import {logEvent} from 'lib/statsig/statsig'
-import {getStarterPackOgCard} from 'lib/strings/starter-pack'
-import {isWeb} from 'platform/detection'
-import {updateProfileShadow} from 'state/cache/profile-shadow'
-import {useModerationOpts} from 'state/preferences/moderation-opts'
-import {getAllListMembers} from 'state/queries/list-members'
-import {useResolvedStarterPackShortLink} from 'state/queries/resolve-short-link'
-import {useResolveDidQuery} from 'state/queries/resolve-uri'
-import {useShortenLink} from 'state/queries/shorten-link'
-import {useStarterPackQuery} from 'state/queries/starter-packs'
-import {useAgent, useSession} from 'state/session'
-import {useLoggedOutViewControls} from 'state/shell/logged-out'
-import {useSetActiveStarterPack} from 'state/shell/starter-pack'
+import {useSetActiveStarterPack} from '#/state/shell/starter-pack'
+import {PagerWithHeader} from '#/view/com/pager/PagerWithHeader'
+import {ProfileSubpageHeader} from '#/view/com/profile/ProfileSubpageHeader'
 import * as Toast from '#/view/com/util/Toast'
-import {PagerWithHeader} from 'view/com/pager/PagerWithHeader'
-import {ProfileSubpageHeader} from 'view/com/profile/ProfileSubpageHeader'
-import {CenteredView} from 'view/com/util/Views'
+import {CenteredView} from '#/view/com/util/Views'
 import {bulkWriteFollows} from '#/screens/Onboarding/util'
 import {atoms as a, useBreakpoints, useTheme} from '#/alf'
 import {Button, ButtonIcon, ButtonText} from '#/components/Button'
@@ -591,7 +591,7 @@ function OverflowMenu({
 
               <Menu.Item
                 label={_(msg`Report starter pack`)}
-                onPress={reportDialogControl.open}>
+                onPress={() => reportDialogControl.open()}>
                 <Menu.ItemText>
                   <Trans>Report starter pack</Trans>
                 </Menu.ItemText>
diff --git a/src/state/dialogs/index.tsx b/src/state/dialogs/index.tsx
index 26bb6792f..80893190f 100644
--- a/src/state/dialogs/index.tsx
+++ b/src/state/dialogs/index.tsx
@@ -1,8 +1,9 @@
 import React from 'react'
-import {SharedValue, useSharedValue} from 'react-native-reanimated'
 
+import {isWeb} from '#/platform/detection'
 import {DialogControlRefProps} from '#/components/Dialog'
 import {Provider as GlobalDialogsProvider} from '#/components/dialogs/Context'
+import {BottomSheet} from '../../../modules/bottom-sheet'
 
 interface IDialogContext {
   /**
@@ -16,25 +17,24 @@ interface IDialogContext {
    * `useId`.
    */
   openDialogs: React.MutableRefObject<Set<string>>
+}
+
+interface IDialogControlContext {
+  closeAllDialogs(): boolean
+  setDialogIsOpen(id: string, isOpen: boolean): void
   /**
-   * The counterpart to `accessibilityViewIsModal` for Android. This property
-   * applies to the parent of all non-modal views, and prevents TalkBack from
-   * navigating within content beneath an open dialog.
-   *
-   * @see https://reactnative.dev/docs/accessibility#importantforaccessibility-android
+   * The number of dialogs that are fully expanded. This is used to determine the backgground color of the status bar
+   * on iOS.
    */
-  importantForAccessibility: SharedValue<'auto' | 'no-hide-descendants'>
+  fullyExpandedCount: number
+  setFullyExpandedCount: React.Dispatch<React.SetStateAction<number>>
 }
 
 const DialogContext = React.createContext<IDialogContext>({} as IDialogContext)
 
-const DialogControlContext = React.createContext<{
-  closeAllDialogs(): boolean
-  setDialogIsOpen(id: string, isOpen: boolean): void
-}>({
-  closeAllDialogs: () => false,
-  setDialogIsOpen: () => {},
-})
+const DialogControlContext = React.createContext<IDialogControlContext>(
+  {} as IDialogControlContext,
+)
 
 export function useDialogStateContext() {
   return React.useContext(DialogContext)
@@ -45,48 +45,55 @@ export function useDialogStateControlContext() {
 }
 
 export function Provider({children}: React.PropsWithChildren<{}>) {
+  const [fullyExpandedCount, setFullyExpandedCount] = React.useState(0)
+
   const activeDialogs = React.useRef<
     Map<string, React.MutableRefObject<DialogControlRefProps>>
   >(new Map())
   const openDialogs = React.useRef<Set<string>>(new Set())
-  const importantForAccessibility = useSharedValue<
-    'auto' | 'no-hide-descendants'
-  >('auto')
 
   const closeAllDialogs = React.useCallback(() => {
-    openDialogs.current.forEach(id => {
-      const dialog = activeDialogs.current.get(id)
-      if (dialog) dialog.current.close()
-    })
-    return openDialogs.current.size > 0
+    if (isWeb) {
+      openDialogs.current.forEach(id => {
+        const dialog = activeDialogs.current.get(id)
+        if (dialog) dialog.current.close()
+      })
+
+      return openDialogs.current.size > 0
+    } else {
+      BottomSheet.dismissAll()
+      return false
+    }
   }, [])
 
-  const setDialogIsOpen = React.useCallback(
-    (id: string, isOpen: boolean) => {
-      if (isOpen) {
-        openDialogs.current.add(id)
-        importantForAccessibility.value = 'no-hide-descendants'
-      } else {
-        openDialogs.current.delete(id)
-        if (openDialogs.current.size < 1) {
-          importantForAccessibility.value = 'auto'
-        }
-      }
-    },
-    [importantForAccessibility],
-  )
+  const setDialogIsOpen = React.useCallback((id: string, isOpen: boolean) => {
+    if (isOpen) {
+      openDialogs.current.add(id)
+    } else {
+      openDialogs.current.delete(id)
+    }
+  }, [])
 
   const context = React.useMemo<IDialogContext>(
     () => ({
       activeDialogs,
       openDialogs,
-      importantForAccessibility,
     }),
-    [importantForAccessibility, activeDialogs, openDialogs],
+    [activeDialogs, openDialogs],
   )
   const controls = React.useMemo(
-    () => ({closeAllDialogs, setDialogIsOpen}),
-    [closeAllDialogs, setDialogIsOpen],
+    () => ({
+      closeAllDialogs,
+      setDialogIsOpen,
+      fullyExpandedCount,
+      setFullyExpandedCount,
+    }),
+    [
+      closeAllDialogs,
+      setDialogIsOpen,
+      fullyExpandedCount,
+      setFullyExpandedCount,
+    ],
   )
 
   return (
diff --git a/src/state/preferences/in-app-browser.tsx b/src/state/preferences/in-app-browser.tsx
index 76c854105..1494fa4e8 100644
--- a/src/state/preferences/in-app-browser.tsx
+++ b/src/state/preferences/in-app-browser.tsx
@@ -2,14 +2,14 @@ import React from 'react'
 import {Linking} from 'react-native'
 import * as WebBrowser from 'expo-web-browser'
 
-import {isNative} from '#/platform/detection'
-import * as persisted from '#/state/persisted'
-import {usePalette} from 'lib/hooks/usePalette'
+import {usePalette} from '#/lib/hooks/usePalette'
 import {
   createBskyAppAbsoluteUrl,
   isBskyRSSUrl,
   isRelativeUrl,
-} from 'lib/strings/url-helpers'
+} from '#/lib/strings/url-helpers'
+import {isNative} from '#/platform/detection'
+import * as persisted from '#/state/persisted'
 import {useModalControls} from '../modals'
 
 type StateContext = persisted.Schema['useInAppBrowser']
@@ -62,7 +62,7 @@ export function useOpenLink() {
   const pal = usePalette('default')
 
   const openLink = React.useCallback(
-    (url: string, override?: boolean) => {
+    async (url: string, override?: boolean) => {
       if (isBskyRSSUrl(url) && isRelativeUrl(url)) {
         url = createBskyAppAbsoluteUrl(url)
       }
@@ -75,7 +75,7 @@ export function useOpenLink() {
           })
           return
         } else if (override ?? enabled) {
-          WebBrowser.openBrowserAsync(url, {
+          await WebBrowser.openBrowserAsync(url, {
             presentationStyle:
               WebBrowser.WebBrowserPresentationStyle.FULL_SCREEN,
             toolbarColor: pal.colors.backgroundLight,
diff --git a/src/view/com/auth/server-input/index.tsx b/src/view/com/auth/server-input/index.tsx
index fb69e1d9c..74b0d2315 100644
--- a/src/view/com/auth/server-input/index.tsx
+++ b/src/view/com/auth/server-input/index.tsx
@@ -66,12 +66,8 @@ export function ServerInputDialog({
   ])
 
   return (
-    <Dialog.Outer
-      control={control}
-      nativeOptions={{sheet: {snapPoints: ['100%']}}}
-      onClose={onClose}>
+    <Dialog.Outer control={control} onClose={onClose}>
       <Dialog.Handle />
-
       <Dialog.ScrollableInner
         accessibilityDescribedBy="dialog-description"
         accessibilityLabelledBy="dialog-title">
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx
index 227964907..e03c64a42 100644
--- a/src/view/com/composer/Composer.tsx
+++ b/src/view/com/composer/Composer.tsx
@@ -114,11 +114,14 @@ import {Button, ButtonIcon, ButtonText} from '#/components/Button'
 import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
 import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons/Emoji'
 import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
+import {createPortalGroup} from '#/components/Portal'
 import * as Prompt from '#/components/Prompt'
 import {Text as NewText} from '#/components/Typography'
 import {composerReducer, createComposerState} from './state/composer'
 import {NO_VIDEO, NoVideoState, processVideo, VideoState} from './state/video'
 
+const Portal = createPortalGroup()
+
 const MAX_IMAGES = 4
 
 type CancelRef = {
@@ -613,296 +616,313 @@ export const ComposePost = ({
   const keyboardVerticalOffset = useKeyboardVerticalOffset()
 
   return (
-    <KeyboardAvoidingView
-      testID="composePostView"
-      behavior={isIOS ? 'padding' : 'height'}
-      keyboardVerticalOffset={keyboardVerticalOffset}
-      style={a.flex_1}>
-      <View style={[a.flex_1, viewStyles]} aria-modal accessibilityViewIsModal>
-        <Animated.View
-          style={topBarAnimatedStyle}
-          layout={native(LinearTransition)}>
-          <View style={styles.topbarInner}>
-            <Button
-              label={_(msg`Cancel`)}
-              variant="ghost"
-              color="primary"
-              shape="default"
-              size="small"
-              style={[
-                a.rounded_full,
-                a.py_sm,
-                {paddingLeft: 7, paddingRight: 7},
-              ]}
-              onPress={onPressCancel}
-              accessibilityHint={_(
-                msg`Closes post composer and discards post draft`,
-              )}>
-              <ButtonText style={[a.text_md]}>
-                <Trans>Cancel</Trans>
-              </ButtonText>
-            </Button>
-            <View style={a.flex_1} />
-            {isProcessing ? (
-              <>
-                <Text style={pal.textLight}>{processingState}</Text>
-                <View style={styles.postBtn}>
-                  <ActivityIndicator />
-                </View>
-              </>
-            ) : (
-              <View style={[styles.postBtnWrapper]}>
-                <LabelsBtn
-                  labels={labels}
-                  onChange={setLabels}
-                  hasMedia={hasMedia}
-                />
-                {canPost ? (
-                  <Button
-                    testID="composerPublishBtn"
-                    label={
-                      replyTo ? _(msg`Publish reply`) : _(msg`Publish post`)
-                    }
-                    variant="solid"
-                    color="primary"
-                    shape="default"
-                    size="small"
-                    style={[a.rounded_full, a.py_sm]}
-                    onPress={() => onPressPublish()}
-                    disabled={videoState.status !== 'idle' && publishOnUpload}>
-                    <ButtonText style={[a.text_md]}>
-                      {replyTo ? (
-                        <Trans context="action">Reply</Trans>
-                      ) : (
-                        <Trans context="action">Post</Trans>
-                      )}
-                    </ButtonText>
-                  </Button>
-                ) : (
-                  <View style={[styles.postBtn, pal.btn]}>
-                    <Text style={[pal.textLight, s.f16, s.bold]}>
-                      <Trans context="action">Post</Trans>
-                    </Text>
+    <Portal.Provider>
+      <KeyboardAvoidingView
+        testID="composePostView"
+        behavior={isIOS ? 'padding' : 'height'}
+        keyboardVerticalOffset={keyboardVerticalOffset}
+        style={a.flex_1}>
+        <View
+          style={[a.flex_1, viewStyles]}
+          aria-modal
+          accessibilityViewIsModal>
+          <Animated.View
+            style={topBarAnimatedStyle}
+            layout={native(LinearTransition)}>
+            <View style={styles.topbarInner}>
+              <Button
+                label={_(msg`Cancel`)}
+                variant="ghost"
+                color="primary"
+                shape="default"
+                size="small"
+                style={[
+                  a.rounded_full,
+                  a.py_sm,
+                  {paddingLeft: 7, paddingRight: 7},
+                ]}
+                onPress={onPressCancel}
+                accessibilityHint={_(
+                  msg`Closes post composer and discards post draft`,
+                )}>
+                <ButtonText style={[a.text_md]}>
+                  <Trans>Cancel</Trans>
+                </ButtonText>
+              </Button>
+              <View style={a.flex_1} />
+              {isProcessing ? (
+                <>
+                  <Text style={pal.textLight}>{processingState}</Text>
+                  <View style={styles.postBtn}>
+                    <ActivityIndicator />
                   </View>
-                )}
-              </View>
-            )}
-          </View>
-
-          {isAltTextRequiredAndMissing && (
-            <View style={[styles.reminderLine, pal.viewLight]}>
-              <View style={styles.errorIcon}>
-                <FontAwesomeIcon
-                  icon="exclamation"
-                  style={{color: colors.red4}}
-                  size={10}
-                />
-              </View>
-              <Text style={[pal.text, a.flex_1]}>
-                <Trans>One or more images is missing alt text.</Trans>
-              </Text>
+                </>
+              ) : (
+                <View style={[styles.postBtnWrapper]}>
+                  <LabelsBtn
+                    labels={labels}
+                    onChange={setLabels}
+                    hasMedia={hasMedia}
+                  />
+                  {canPost ? (
+                    <Button
+                      testID="composerPublishBtn"
+                      label={
+                        replyTo ? _(msg`Publish reply`) : _(msg`Publish post`)
+                      }
+                      variant="solid"
+                      color="primary"
+                      shape="default"
+                      size="small"
+                      style={[a.rounded_full, a.py_sm]}
+                      onPress={() => onPressPublish()}
+                      disabled={
+                        videoState.status !== 'idle' && publishOnUpload
+                      }>
+                      <ButtonText style={[a.text_md]}>
+                        {replyTo ? (
+                          <Trans context="action">Reply</Trans>
+                        ) : (
+                          <Trans context="action">Post</Trans>
+                        )}
+                      </ButtonText>
+                    </Button>
+                  ) : (
+                    <View style={[styles.postBtn, pal.btn]}>
+                      <Text style={[pal.textLight, s.f16, s.bold]}>
+                        <Trans context="action">Post</Trans>
+                      </Text>
+                    </View>
+                  )}
+                </View>
+              )}
             </View>
-          )}
-          <ErrorBanner
-            error={error}
-            videoState={videoState}
-            clearError={() => setError('')}
-            clearVideo={clearVideo}
-          />
-        </Animated.View>
-        <Animated.ScrollView
-          layout={native(LinearTransition)}
-          onScroll={scrollHandler}
-          style={styles.scrollView}
-          keyboardShouldPersistTaps="always"
-          onContentSizeChange={onScrollViewContentSizeChange}
-          onLayout={onScrollViewLayout}>
-          {replyTo ? <ComposerReplyTo replyTo={replyTo} /> : undefined}
 
-          <View
-            style={[
-              styles.textInputLayout,
-              isNative && styles.textInputLayoutMobile,
-            ]}>
-            <UserAvatar
-              avatar={currentProfile?.avatar}
-              size={50}
-              type={currentProfile?.associated?.labeler ? 'labeler' : 'user'}
-            />
-            <TextInput
-              ref={textInput}
-              richtext={richtext}
-              placeholder={selectTextInputPlaceholder}
-              autoFocus
-              setRichText={setRichText}
-              onPhotoPasted={onPhotoPasted}
-              onPressPublish={() => onPressPublish()}
-              onNewLink={onNewLink}
-              onError={setError}
-              accessible={true}
-              accessibilityLabel={_(msg`Write post`)}
-              accessibilityHint={_(
-                msg`Compose posts up to ${MAX_GRAPHEME_LENGTH} characters in length`,
-              )}
+            {isAltTextRequiredAndMissing && (
+              <View style={[styles.reminderLine, pal.viewLight]}>
+                <View style={styles.errorIcon}>
+                  <FontAwesomeIcon
+                    icon="exclamation"
+                    style={{color: colors.red4}}
+                    size={10}
+                  />
+                </View>
+                <Text style={[pal.text, a.flex_1]}>
+                  <Trans>One or more images is missing alt text.</Trans>
+                </Text>
+              </View>
+            )}
+            <ErrorBanner
+              error={error}
+              videoState={videoState}
+              clearError={() => setError('')}
+              clearVideo={clearVideo}
             />
-          </View>
-
-          <Gallery images={images} dispatch={dispatch} />
-          {images.length === 0 && extLink && (
-            <View style={a.relative}>
-              <ExternalEmbed
-                link={extLink}
-                gif={extGif}
-                onRemove={() => {
-                  if (extGif) {
-                    dispatch({type: 'embed_remove_gif'})
-                  } else {
-                    dispatch({type: 'embed_remove_link'})
-                  }
-                  setExtLink(undefined)
-                  setExtGif(undefined)
-                }}
+          </Animated.View>
+          <Animated.ScrollView
+            layout={native(LinearTransition)}
+            onScroll={scrollHandler}
+            style={styles.scrollView}
+            keyboardShouldPersistTaps="always"
+            onContentSizeChange={onScrollViewContentSizeChange}
+            onLayout={onScrollViewLayout}>
+            {replyTo ? <ComposerReplyTo replyTo={replyTo} /> : undefined}
+
+            <View
+              style={[
+                styles.textInputLayout,
+                isNative && styles.textInputLayoutMobile,
+              ]}>
+              <UserAvatar
+                avatar={currentProfile?.avatar}
+                size={50}
+                type={currentProfile?.associated?.labeler ? 'labeler' : 'user'}
               />
-              <GifAltText
-                link={extLink}
-                gif={extGif}
-                onSubmit={handleChangeGifAltText}
+              <TextInput
+                ref={textInput}
+                richtext={richtext}
+                placeholder={selectTextInputPlaceholder}
+                autoFocus
+                setRichText={setRichText}
+                onPhotoPasted={onPhotoPasted}
+                onPressPublish={() => onPressPublish()}
+                onNewLink={onNewLink}
+                onError={setError}
+                accessible={true}
+                accessibilityLabel={_(msg`Write post`)}
+                accessibilityHint={_(
+                  msg`Compose posts up to ${MAX_GRAPHEME_LENGTH} characters in length`,
+                )}
               />
             </View>
-          )}
-          <LayoutAnimationConfig skipExiting>
-            {hasVideo && (
-              <Animated.View
-                style={[a.w_full, a.mt_lg]}
-                entering={native(ZoomIn)}
-                exiting={native(ZoomOut)}>
-                {videoState.asset &&
-                  (videoState.status === 'compressing' ? (
-                    <VideoTranscodeProgress
-                      asset={videoState.asset}
-                      progress={videoState.progress}
-                      clear={clearVideo}
-                    />
-                  ) : videoState.video ? (
-                    <VideoPreview
-                      asset={videoState.asset}
-                      video={videoState.video}
-                      setDimensions={updateVideoDimensions}
-                      clear={clearVideo}
-                    />
-                  ) : null)}
-                <SubtitleDialogBtn
-                  defaultAltText={videoState.altText}
-                  saveAltText={altText =>
-                    dispatch({
-                      type: 'embed_update_video',
-                      videoAction: {
-                        type: 'update_alt_text',
-                        altText,
-                        signal: videoState.abortController.signal,
-                      },
-                    })
-                  }
-                  captions={videoState.captions}
-                  setCaptions={updater => {
-                    dispatch({
-                      type: 'embed_update_video',
-                      videoAction: {
-                        type: 'update_captions',
-                        updater,
-                        signal: videoState.abortController.signal,
-                      },
-                    })
+
+            <Gallery
+              images={images}
+              dispatch={dispatch}
+              Portal={Portal.Portal}
+            />
+            {images.length === 0 && extLink && (
+              <View style={a.relative}>
+                <ExternalEmbed
+                  link={extLink}
+                  gif={extGif}
+                  onRemove={() => {
+                    if (extGif) {
+                      dispatch({type: 'embed_remove_gif'})
+                    } else {
+                      dispatch({type: 'embed_remove_link'})
+                    }
+                    setExtLink(undefined)
+                    setExtGif(undefined)
                   }}
                 />
-              </Animated.View>
+                <GifAltText
+                  link={extLink}
+                  gif={extGif}
+                  onSubmit={handleChangeGifAltText}
+                  Portal={Portal.Portal}
+                />
+              </View>
             )}
-          </LayoutAnimationConfig>
-          <View style={!hasVideo ? [a.mt_md] : []}>
-            {quote ? (
-              <View style={[s.mt5, s.mb2, isWeb && s.mb10]}>
-                <View style={{pointerEvents: 'none'}}>
-                  <QuoteEmbed quote={quote} />
-                </View>
-                {quote.uri !== initQuote?.uri && (
-                  <QuoteX
-                    onRemove={() => {
-                      dispatch({type: 'embed_remove_quote'})
-                      setQuote(undefined)
+            <LayoutAnimationConfig skipExiting>
+              {hasVideo && (
+                <Animated.View
+                  style={[a.w_full, a.mt_lg]}
+                  entering={native(ZoomIn)}
+                  exiting={native(ZoomOut)}>
+                  {videoState.asset &&
+                    (videoState.status === 'compressing' ? (
+                      <VideoTranscodeProgress
+                        asset={videoState.asset}
+                        progress={videoState.progress}
+                        clear={clearVideo}
+                      />
+                    ) : videoState.video ? (
+                      <VideoPreview
+                        asset={videoState.asset}
+                        video={videoState.video}
+                        setDimensions={updateVideoDimensions}
+                        clear={clearVideo}
+                      />
+                    ) : null)}
+                  <SubtitleDialogBtn
+                    defaultAltText={videoState.altText}
+                    saveAltText={altText =>
+                      dispatch({
+                        type: 'embed_update_video',
+                        videoAction: {
+                          type: 'update_alt_text',
+                          altText,
+                          signal: videoState.abortController.signal,
+                        },
+                      })
+                    }
+                    captions={videoState.captions}
+                    setCaptions={updater => {
+                      dispatch({
+                        type: 'embed_update_video',
+                        videoAction: {
+                          type: 'update_captions',
+                          updater,
+                          signal: videoState.abortController.signal,
+                        },
+                      })
                     }}
+                    Portal={Portal.Portal}
                   />
-                )}
-              </View>
-            ) : null}
-          </View>
-        </Animated.ScrollView>
-        <SuggestedLanguage text={richtext.text} />
-
-        {replyTo ? null : (
-          <ThreadgateBtn
-            postgate={postgate}
-            onChangePostgate={setPostgate}
-            threadgateAllowUISettings={threadgateAllowUISettings}
-            onChangeThreadgateAllowUISettings={
-              onChangeThreadgateAllowUISettings
-            }
-            style={bottomBarAnimatedStyle}
-          />
-        )}
-        <View
-          style={[
-            t.atoms.bg,
-            t.atoms.border_contrast_medium,
-            styles.bottomBar,
-          ]}>
-          {videoState.status !== 'idle' && videoState.status !== 'done' ? (
-            <VideoUploadToolbar state={videoState} />
-          ) : (
-            <ToolbarWrapper style={[a.flex_row, a.align_center, a.gap_xs]}>
-              <SelectPhotoBtn
-                size={images.length}
-                disabled={!canSelectImages}
-                onAdd={onImageAdd}
-              />
-              <SelectVideoBtn
-                onSelectVideo={selectVideo}
-                disabled={!canSelectImages || images?.length > 0}
-                setError={setError}
-              />
-              <OpenCameraBtn disabled={!canSelectImages} onAdd={onImageAdd} />
-              <SelectGifBtn
-                onClose={focusTextInput}
-                onSelectGif={onSelectGif}
-                disabled={hasMedia}
-              />
-              {!isMobile ? (
-                <Button
-                  onPress={onEmojiButtonPress}
-                  style={a.p_sm}
-                  label={_(msg`Open emoji picker`)}
-                  accessibilityHint={_(msg`Open emoji picker`)}
-                  variant="ghost"
-                  shape="round"
-                  color="primary">
-                  <EmojiSmile size="lg" />
-                </Button>
+                </Animated.View>
+              )}
+            </LayoutAnimationConfig>
+            <View style={!hasVideo ? [a.mt_md] : []}>
+              {quote ? (
+                <View style={[s.mt5, s.mb2, isWeb && s.mb10]}>
+                  <View style={{pointerEvents: 'none'}}>
+                    <QuoteEmbed quote={quote} />
+                  </View>
+                  {quote.uri !== initQuote?.uri && (
+                    <QuoteX
+                      onRemove={() => {
+                        dispatch({type: 'embed_remove_quote'})
+                        setQuote(undefined)
+                      }}
+                    />
+                  )}
+                </View>
               ) : null}
-            </ToolbarWrapper>
+            </View>
+          </Animated.ScrollView>
+          <SuggestedLanguage text={richtext.text} />
+
+          {replyTo ? null : (
+            <ThreadgateBtn
+              postgate={postgate}
+              onChangePostgate={setPostgate}
+              threadgateAllowUISettings={threadgateAllowUISettings}
+              onChangeThreadgateAllowUISettings={
+                onChangeThreadgateAllowUISettings
+              }
+              style={bottomBarAnimatedStyle}
+              Portal={Portal.Portal}
+            />
           )}
-          <View style={a.flex_1} />
-          <SelectLangBtn />
-          <CharProgress count={graphemeLength} />
+          <View
+            style={[
+              t.atoms.bg,
+              t.atoms.border_contrast_medium,
+              styles.bottomBar,
+            ]}>
+            {videoState.status !== 'idle' && videoState.status !== 'done' ? (
+              <VideoUploadToolbar state={videoState} />
+            ) : (
+              <ToolbarWrapper style={[a.flex_row, a.align_center, a.gap_xs]}>
+                <SelectPhotoBtn
+                  size={images.length}
+                  disabled={!canSelectImages}
+                  onAdd={onImageAdd}
+                />
+                <SelectVideoBtn
+                  onSelectVideo={selectVideo}
+                  disabled={!canSelectImages || images?.length > 0}
+                  setError={setError}
+                />
+                <OpenCameraBtn disabled={!canSelectImages} onAdd={onImageAdd} />
+                <SelectGifBtn
+                  onClose={focusTextInput}
+                  onSelectGif={onSelectGif}
+                  disabled={hasMedia}
+                  Portal={Portal.Portal}
+                />
+                {!isMobile ? (
+                  <Button
+                    onPress={onEmojiButtonPress}
+                    style={a.p_sm}
+                    label={_(msg`Open emoji picker`)}
+                    accessibilityHint={_(msg`Open emoji picker`)}
+                    variant="ghost"
+                    shape="round"
+                    color="primary">
+                    <EmojiSmile size="lg" />
+                  </Button>
+                ) : null}
+              </ToolbarWrapper>
+            )}
+            <View style={a.flex_1} />
+            <SelectLangBtn />
+            <CharProgress count={graphemeLength} />
+          </View>
         </View>
-      </View>
-      <Prompt.Basic
-        control={discardPromptControl}
-        title={_(msg`Discard draft?`)}
-        description={_(msg`Are you sure you'd like to discard this draft?`)}
-        onConfirm={onClose}
-        confirmButtonCta={_(msg`Discard`)}
-        confirmButtonColor="negative"
-      />
-    </KeyboardAvoidingView>
+        <Prompt.Basic
+          control={discardPromptControl}
+          title={_(msg`Discard draft?`)}
+          description={_(msg`Are you sure you'd like to discard this draft?`)}
+          onConfirm={onClose}
+          confirmButtonCta={_(msg`Discard`)}
+          confirmButtonColor="negative"
+          Portal={Portal.Portal}
+        />
+      </KeyboardAvoidingView>
+      <Portal.Outlet />
+    </Portal.Provider>
   )
 }
 
diff --git a/src/view/com/composer/GifAltText.tsx b/src/view/com/composer/GifAltText.tsx
index a05607c76..3479fb973 100644
--- a/src/view/com/composer/GifAltText.tsx
+++ b/src/view/com/composer/GifAltText.tsx
@@ -20,6 +20,7 @@ import * as Dialog from '#/components/Dialog'
 import * as TextField from '#/components/forms/TextField'
 import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
 import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
+import {PortalComponent} from '#/components/Portal'
 import {Text} from '#/components/Typography'
 import {GifEmbed} from '../util/post-embeds/GifEmbed'
 import {AltTextReminder} from './photos/Gallery'
@@ -28,10 +29,12 @@ export function GifAltText({
   link: linkProp,
   gif,
   onSubmit,
+  Portal,
 }: {
   link: ExternalEmbedDraft
   gif?: Gif
   onSubmit: (alt: string) => void
+  Portal: PortalComponent
 }) {
   const control = Dialog.useDialogControl()
   const {_} = useLingui()
@@ -96,9 +99,7 @@ export function GifAltText({
 
       <AltTextReminder />
 
-      <Dialog.Outer
-        control={control}
-        nativeOptions={isAndroid ? {sheet: {snapPoints: ['100%']}} : {}}>
+      <Dialog.Outer control={control} Portal={Portal}>
         <Dialog.Handle />
         <AltTextInner
           onSubmit={onPressSubmit}
@@ -185,6 +186,8 @@ function AltTextInner({
         </View>
       </View>
       <Dialog.Close />
+      {/* Maybe fix this later -h */}
+      {isAndroid ? <View style={{height: 300}} /> : null}
     </Dialog.ScrollableInner>
   )
 }
diff --git a/src/view/com/composer/photos/EditImageDialog.web.tsx b/src/view/com/composer/photos/EditImageDialog.web.tsx
index 0afb83ed9..ebe528abc 100644
--- a/src/view/com/composer/photos/EditImageDialog.web.tsx
+++ b/src/view/com/composer/photos/EditImageDialog.web.tsx
@@ -20,6 +20,7 @@ import {EditImageDialogProps} from './EditImageDialog'
 export const EditImageDialog = (props: EditImageDialogProps) => {
   return (
     <Dialog.Outer control={props.control}>
+      <Dialog.Handle />
       <EditImageInner key={props.image.source.id} {...props} />
     </Dialog.Outer>
   )
diff --git a/src/view/com/composer/photos/Gallery.tsx b/src/view/com/composer/photos/Gallery.tsx
index 5ff7042bc..3958a85c0 100644
--- a/src/view/com/composer/photos/Gallery.tsx
+++ b/src/view/com/composer/photos/Gallery.tsx
@@ -21,6 +21,7 @@ import {ComposerImage, cropImage} from '#/state/gallery'
 import {Text} from '#/view/com/util/text/Text'
 import {useTheme} from '#/alf'
 import * as Dialog from '#/components/Dialog'
+import {PortalComponent} from '#/components/Portal'
 import {ComposerAction} from '../state/composer'
 import {EditImageDialog} from './EditImageDialog'
 import {ImageAltTextDialog} from './ImageAltTextDialog'
@@ -30,6 +31,7 @@ const IMAGE_GAP = 8
 interface GalleryProps {
   images: ComposerImage[]
   dispatch: (action: ComposerAction) => void
+  Portal: PortalComponent
 }
 
 export let Gallery = (props: GalleryProps): React.ReactNode => {
@@ -57,7 +59,12 @@ interface GalleryInnerProps extends GalleryProps {
   containerInfo: Dimensions
 }
 
-const GalleryInner = ({images, containerInfo, dispatch}: GalleryInnerProps) => {
+const GalleryInner = ({
+  images,
+  containerInfo,
+  dispatch,
+  Portal,
+}: GalleryInnerProps) => {
   const {isMobile} = useWebMediaQueries()
 
   const {altTextControlStyle, imageControlsStyle, imageStyle} =
@@ -111,6 +118,7 @@ const GalleryInner = ({images, containerInfo, dispatch}: GalleryInnerProps) => {
               onRemove={() => {
                 dispatch({type: 'embed_remove_image', image})
               }}
+              Portal={Portal}
             />
           )
         })}
@@ -127,6 +135,7 @@ type GalleryItemProps = {
   imageStyle?: ViewStyle
   onChange: (next: ComposerImage) => void
   onRemove: () => void
+  Portal: PortalComponent
 }
 
 const GalleryItem = ({
@@ -136,6 +145,7 @@ const GalleryItem = ({
   imageStyle,
   onChange,
   onRemove,
+  Portal,
 }: GalleryItemProps): React.ReactNode => {
   const {_} = useLingui()
   const t = useTheme()
@@ -230,6 +240,7 @@ const GalleryItem = ({
         control={altTextControl}
         image={image}
         onChange={onChange}
+        Portal={Portal}
       />
 
       <EditImageDialog
diff --git a/src/view/com/composer/photos/ImageAltTextDialog.tsx b/src/view/com/composer/photos/ImageAltTextDialog.tsx
index 123e1066a..16ce4351a 100644
--- a/src/view/com/composer/photos/ImageAltTextDialog.tsx
+++ b/src/view/com/composer/photos/ImageAltTextDialog.tsx
@@ -5,25 +5,26 @@ import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
 import {MAX_ALT_TEXT} from '#/lib/constants'
-import {isWeb} from '#/platform/detection'
+import {isAndroid, isWeb} from '#/platform/detection'
 import {ComposerImage} from '#/state/gallery'
 import {atoms as a, useTheme} from '#/alf'
 import {Button, ButtonText} from '#/components/Button'
 import * as Dialog from '#/components/Dialog'
 import * as TextField from '#/components/forms/TextField'
+import {PortalComponent} from '#/components/Portal'
 import {Text} from '#/components/Typography'
 
 type Props = {
   control: Dialog.DialogOuterProps['control']
   image: ComposerImage
   onChange: (next: ComposerImage) => void
+  Portal: PortalComponent
 }
 
 export const ImageAltTextDialog = (props: Props): React.ReactNode => {
   return (
-    <Dialog.Outer control={props.control}>
+    <Dialog.Outer control={props.control} Portal={props.Portal}>
       <Dialog.Handle />
-
       <ImageAltTextInner {...props} />
     </Dialog.Outer>
   )
@@ -116,6 +117,8 @@ const ImageAltTextInner = ({
           </ButtonText>
         </Button>
       </View>
+      {/* Maybe fix this later -h */}
+      {isAndroid ? <View style={{height: 300}} /> : null}
     </Dialog.ScrollableInner>
   )
 }
diff --git a/src/view/com/composer/photos/SelectGifBtn.tsx b/src/view/com/composer/photos/SelectGifBtn.tsx
index d13df0a11..d482e0783 100644
--- a/src/view/com/composer/photos/SelectGifBtn.tsx
+++ b/src/view/com/composer/photos/SelectGifBtn.tsx
@@ -9,14 +9,16 @@ import {atoms as a, useTheme} from '#/alf'
 import {Button} from '#/components/Button'
 import {GifSelectDialog} from '#/components/dialogs/GifSelect'
 import {GifSquare_Stroke2_Corner0_Rounded as GifIcon} from '#/components/icons/Gif'
+import {PortalComponent} from '#/components/Portal'
 
 type Props = {
   onClose: () => void
   onSelectGif: (gif: Gif) => void
   disabled?: boolean
+  Portal?: PortalComponent
 }
 
-export function SelectGifBtn({onClose, onSelectGif, disabled}: Props) {
+export function SelectGifBtn({onClose, onSelectGif, disabled, Portal}: Props) {
   const {_} = useLingui()
   const ref = useRef<{open: () => void}>(null)
   const t = useTheme()
@@ -46,6 +48,7 @@ export function SelectGifBtn({onClose, onSelectGif, disabled}: Props) {
         controlRef={ref}
         onClose={onClose}
         onSelectGif={onSelectGif}
+        Portal={Portal}
       />
     </>
   )
diff --git a/src/view/com/composer/photos/SelectPhotoBtn.tsx b/src/view/com/composer/photos/SelectPhotoBtn.tsx
index 34ead3d9a..37bfbafe6 100644
--- a/src/view/com/composer/photos/SelectPhotoBtn.tsx
+++ b/src/view/com/composer/photos/SelectPhotoBtn.tsx
@@ -9,6 +9,7 @@ import {isNative} from '#/platform/detection'
 import {ComposerImage, createComposerImage} from '#/state/gallery'
 import {atoms as a, useTheme} from '#/alf'
 import {Button} from '#/components/Button'
+import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper'
 import {Image_Stroke2_Corner0_Rounded as Image} from '#/components/icons/Image'
 
 type Props = {
@@ -21,23 +22,26 @@ export function SelectPhotoBtn({size, disabled, onAdd}: Props) {
   const {_} = useLingui()
   const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
   const t = useTheme()
+  const sheetWrapper = useSheetWrapper()
 
   const onPressSelectPhotos = useCallback(async () => {
     if (isNative && !(await requestPhotoAccessIfNeeded())) {
       return
     }
 
-    const images = await openPicker({
-      selectionLimit: 4 - size,
-      allowsMultipleSelection: true,
-    })
+    const images = await sheetWrapper(
+      openPicker({
+        selectionLimit: 4 - size,
+        allowsMultipleSelection: true,
+      }),
+    )
 
     const results = await Promise.all(
       images.map(img => createComposerImage(img)),
     )
 
     onAdd(results)
-  }, [requestPhotoAccessIfNeeded, size, onAdd])
+  }, [requestPhotoAccessIfNeeded, size, onAdd, sheetWrapper])
 
   return (
     <Button
diff --git a/src/view/com/composer/threadgate/ThreadgateBtn.tsx b/src/view/com/composer/threadgate/ThreadgateBtn.tsx
index b0806180c..7e57a57d4 100644
--- a/src/view/com/composer/threadgate/ThreadgateBtn.tsx
+++ b/src/view/com/composer/threadgate/ThreadgateBtn.tsx
@@ -13,6 +13,7 @@ import * as Dialog from '#/components/Dialog'
 import {PostInteractionSettingsControlledDialog} from '#/components/dialogs/PostInteractionSettingsDialog'
 import {Earth_Stroke2_Corner0_Rounded as Earth} from '#/components/icons/Globe'
 import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group'
+import {PortalComponent} from '#/components/Portal'
 
 export function ThreadgateBtn({
   postgate,
@@ -20,6 +21,7 @@ export function ThreadgateBtn({
   threadgateAllowUISettings,
   onChangeThreadgateAllowUISettings,
   style,
+  Portal,
 }: {
   postgate: AppBskyFeedPostgate.Record
   onChangePostgate: (v: AppBskyFeedPostgate.Record) => void
@@ -28,6 +30,8 @@ export function ThreadgateBtn({
   onChangeThreadgateAllowUISettings: (v: ThreadgateAllowUISetting[]) => void
 
   style?: StyleProp<AnimatedStyle<ViewStyle>>
+
+  Portal: PortalComponent
 }) {
   const {_} = useLingui()
   const t = useTheme()
@@ -77,6 +81,7 @@ export function ThreadgateBtn({
         onChangePostgate={onChangePostgate}
         threadgateAllowUISettings={threadgateAllowUISettings}
         onChangeThreadgateAllowUISettings={onChangeThreadgateAllowUISettings}
+        Portal={Portal}
       />
     </>
   )
diff --git a/src/view/com/composer/videos/SubtitleDialog.tsx b/src/view/com/composer/videos/SubtitleDialog.tsx
index f66684d4e..04522ee1d 100644
--- a/src/view/com/composer/videos/SubtitleDialog.tsx
+++ b/src/view/com/composer/videos/SubtitleDialog.tsx
@@ -7,7 +7,7 @@ import {useLingui} from '@lingui/react'
 import {MAX_ALT_TEXT} from '#/lib/constants'
 import {useEnforceMaxGraphemeCount} from '#/lib/strings/helpers'
 import {LANGUAGES} from '#/locale/languages'
-import {isAndroid, isWeb} from '#/platform/detection'
+import {isWeb} from '#/platform/detection'
 import {useLanguagePrefs} from '#/state/preferences'
 import {atoms as a, useTheme, web} from '#/alf'
 import {Button, ButtonIcon, ButtonText} from '#/components/Button'
@@ -17,6 +17,7 @@ import {CC_Stroke2_Corner0_Rounded as CCIcon} from '#/components/icons/CC'
 import {PageText_Stroke2_Corner0_Rounded as PageTextIcon} from '#/components/icons/PageText'
 import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
 import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
+import {PortalComponent} from '#/components/Portal'
 import {Text} from '#/components/Typography'
 import {SubtitleFilePicker} from './SubtitleFilePicker'
 
@@ -29,6 +30,7 @@ interface Props {
   captions: CaptionsTrack[]
   saveAltText: (altText: string) => void
   setCaptions: (updater: (prev: CaptionsTrack[]) => CaptionsTrack[]) => void
+  Portal: PortalComponent
 }
 
 export function SubtitleDialogBtn(props: Props) {
@@ -56,9 +58,7 @@ export function SubtitleDialogBtn(props: Props) {
           {isWeb ? <Trans>Captions & alt text</Trans> : <Trans>Alt text</Trans>}
         </ButtonText>
       </Button>
-      <Dialog.Outer
-        control={control}
-        nativeOptions={isAndroid ? {sheet: {snapPoints: ['60%']}} : {}}>
+      <Dialog.Outer control={control} Portal={props.Portal}>
         <Dialog.Handle />
         <SubtitleDialogInner {...props} />
       </Dialog.Outer>
diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx
index 2b4376b69..43555ccb4 100644
--- a/src/view/com/util/UserAvatar.tsx
+++ b/src/view/com/util/UserAvatar.tsx
@@ -20,6 +20,7 @@ import {isAndroid, isNative, isWeb} from '#/platform/detection'
 import {precacheProfile} from '#/state/queries/profile'
 import {HighPriorityImage} from '#/view/com/util/images/Image'
 import {tokens, useTheme} from '#/alf'
+import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper'
 import {
   Camera_Filled_Stroke2_Corner0_Rounded as CameraFilled,
   Camera_Stroke2_Corner0_Rounded as Camera,
@@ -271,6 +272,7 @@ let EditableUserAvatar = ({
   const {_} = useLingui()
   const {requestCameraAccessIfNeeded} = useCameraPermission()
   const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
+  const sheetWrapper = useSheetWrapper()
 
   const aviStyle = useMemo(() => {
     if (type === 'algo' || type === 'list') {
@@ -306,9 +308,11 @@ let EditableUserAvatar = ({
       return
     }
 
-    const items = await openPicker({
-      aspect: [1, 1],
-    })
+    const items = await sheetWrapper(
+      openPicker({
+        aspect: [1, 1],
+      }),
+    )
     const item = items[0]
     if (!item) {
       return
@@ -332,7 +336,7 @@ let EditableUserAvatar = ({
         logger.error('Failed to crop banner', {error: e})
       }
     }
-  }, [onSelectNewAvatar, requestPhotoAccessIfNeeded])
+  }, [onSelectNewAvatar, requestPhotoAccessIfNeeded, sheetWrapper])
 
   const onRemoveAvatar = React.useCallback(() => {
     onSelectNewAvatar(null)
diff --git a/src/view/com/util/UserBanner.tsx b/src/view/com/util/UserBanner.tsx
index 13f4081fc..622cb2129 100644
--- a/src/view/com/util/UserBanner.tsx
+++ b/src/view/com/util/UserBanner.tsx
@@ -17,6 +17,7 @@ import {logger} from '#/logger'
 import {isAndroid, isNative} from '#/platform/detection'
 import {EventStopper} from '#/view/com/util/EventStopper'
 import {tokens, useTheme as useAlfTheme} from '#/alf'
+import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper'
 import {
   Camera_Filled_Stroke2_Corner0_Rounded as CameraFilled,
   Camera_Stroke2_Corner0_Rounded as Camera,
@@ -43,6 +44,7 @@ export function UserBanner({
   const {_} = useLingui()
   const {requestCameraAccessIfNeeded} = useCameraPermission()
   const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
+  const sheetWrapper = useSheetWrapper()
 
   const onOpenCamera = React.useCallback(async () => {
     if (!(await requestCameraAccessIfNeeded())) {
@@ -60,7 +62,7 @@ export function UserBanner({
     if (!(await requestPhotoAccessIfNeeded())) {
       return
     }
-    const items = await openPicker()
+    const items = await sheetWrapper(openPicker())
     if (!items[0]) {
       return
     }
@@ -80,7 +82,7 @@ export function UserBanner({
         logger.error('Failed to crop banner', {error: e})
       }
     }
-  }, [onSelectNewBanner, requestPhotoAccessIfNeeded])
+  }, [onSelectNewBanner, requestPhotoAccessIfNeeded, sheetWrapper])
 
   const onRemoveBanner = React.useCallback(() => {
     onSelectNewBanner?.(null)
diff --git a/src/view/com/util/forms/PostDropdownBtn.tsx b/src/view/com/util/forms/PostDropdownBtn.tsx
index 33287564a..cd1f2d3de 100644
--- a/src/view/com/util/forms/PostDropdownBtn.tsx
+++ b/src/view/com/util/forms/PostDropdownBtn.tsx
@@ -240,8 +240,8 @@ let PostDropdownBtn = ({
     Toast.show(_(msg`Copied to clipboard`), 'clipboard-check')
   }, [_, richText])
 
-  const onPressTranslate = React.useCallback(() => {
-    openLink(translatorUrl)
+  const onPressTranslate = React.useCallback(async () => {
+    await openLink(translatorUrl)
   }, [openLink, translatorUrl])
 
   const onHidePost = React.useCallback(() => {
@@ -439,7 +439,7 @@ let PostDropdownBtn = ({
               <Menu.Item
                 testID="postDropdownSendViaDMBtn"
                 label={_(msg`Send via direct message`)}
-                onPress={sendViaChatControl.open}>
+                onPress={() => sendViaChatControl.open()}>
                 <Menu.ItemText>
                   <Trans>Send via direct message</Trans>
                 </Menu.ItemText>
@@ -467,7 +467,7 @@ let PostDropdownBtn = ({
               <Menu.Item
                 testID="postDropdownEmbedBtn"
                 label={_(msg`Embed post`)}
-                onPress={embedPostControl.open}>
+                onPress={() => embedPostControl.open()}>
                 <Menu.ItemText>{_(msg`Embed post`)}</Menu.ItemText>
                 <Menu.ItemIcon icon={CodeBrackets} position="right" />
               </Menu.Item>
@@ -542,7 +542,7 @@ let PostDropdownBtn = ({
                           ? _(msg`Hide reply for me`)
                           : _(msg`Hide post for me`)
                       }
-                      onPress={hidePromptControl.open}>
+                      onPress={() => hidePromptControl.open()}>
                       <Menu.ItemText>
                         {isReply
                           ? _(msg`Hide reply for me`)
@@ -630,7 +630,9 @@ let PostDropdownBtn = ({
                     <Menu.Item
                       testID="postDropdownEditPostInteractions"
                       label={_(msg`Edit interaction settings`)}
-                      onPress={postInteractionSettingsDialogControl.open}
+                      onPress={() =>
+                        postInteractionSettingsDialogControl.open()
+                      }
                       {...(isAuthor
                         ? Platform.select({
                             web: {
@@ -649,7 +651,7 @@ let PostDropdownBtn = ({
                     <Menu.Item
                       testID="postDropdownDeleteBtn"
                       label={_(msg`Delete post`)}
-                      onPress={deletePromptControl.open}>
+                      onPress={() => deletePromptControl.open()}>
                       <Menu.ItemText>{_(msg`Delete post`)}</Menu.ItemText>
                       <Menu.ItemIcon icon={Trash} position="right" />
                     </Menu.Item>
diff --git a/src/view/com/util/post-ctrls/RepostButton.tsx b/src/view/com/util/post-ctrls/RepostButton.tsx
index 0ecdf25b9..9be72ae23 100644
--- a/src/view/com/util/post-ctrls/RepostButton.tsx
+++ b/src/view/com/util/post-ctrls/RepostButton.tsx
@@ -86,7 +86,9 @@ let RepostButton = ({
           </Text>
         ) : undefined}
       </Button>
-      <Dialog.Outer control={dialogControl}>
+      <Dialog.Outer
+        control={dialogControl}
+        nativeOptions={{preventExpansion: true}}>
         <Dialog.Handle />
         <Dialog.Inner label={_(msg`Repost or quote post`)}>
           <View style={a.gap_xl}>
@@ -155,7 +157,6 @@ let RepostButton = ({
             </View>
             <Button
               label={_(msg`Cancel quote post`)}
-              onAccessibilityEscape={close}
               onPress={close}
               size="large"
               variant="solid"
diff --git a/src/view/screens/Settings/DisableEmail2FADialog.tsx b/src/view/screens/Settings/DisableEmail2FADialog.tsx
index a27cff9a3..e4341fcd2 100644
--- a/src/view/screens/Settings/DisableEmail2FADialog.tsx
+++ b/src/view/screens/Settings/DisableEmail2FADialog.tsx
@@ -79,7 +79,6 @@ export function DisableEmail2FADialog({
   return (
     <Dialog.Outer control={control}>
       <Dialog.Handle />
-
       <Dialog.ScrollableInner
         accessibilityDescribedBy="dialog-description"
         accessibilityLabelledBy="dialog-title">
diff --git a/src/view/screens/Settings/ExportCarDialog.tsx b/src/view/screens/Settings/ExportCarDialog.tsx
index a6ddb3820..1d8d26471 100644
--- a/src/view/screens/Settings/ExportCarDialog.tsx
+++ b/src/view/screens/Settings/ExportCarDialog.tsx
@@ -53,7 +53,6 @@ export function ExportCarDialog({
   return (
     <Dialog.Outer control={control}>
       <Dialog.Handle />
-
       <Dialog.ScrollableInner
         accessibilityDescribedBy="dialog-description"
         accessibilityLabelledBy="dialog-title">
diff --git a/src/view/screens/Storybook/Dialogs.tsx b/src/view/screens/Storybook/Dialogs.tsx
index 3a9f67de8..a0a2a2755 100644
--- a/src/view/screens/Storybook/Dialogs.tsx
+++ b/src/view/screens/Storybook/Dialogs.tsx
@@ -2,8 +2,8 @@ import React from 'react'
 import {View} from 'react-native'
 import {useNavigation} from '@react-navigation/native'
 
+import {NavigationProp} from '#/lib/routes/types'
 import {useDialogStateControlContext} from '#/state/dialogs'
-import {NavigationProp} from 'lib/routes/types'
 import {atoms as a} from '#/alf'
 import {Button, ButtonText} from '#/components/Button'
 import * as Dialog from '#/components/Dialog'
@@ -179,19 +179,13 @@ export function Dialogs() {
       </Prompt.Outer>
 
       <Dialog.Outer control={basic}>
-        <Dialog.Handle />
-
         <Dialog.Inner label="test">
           <H3 nativeID="dialog-title">Dialog</H3>
           <P nativeID="dialog-description">A basic dialog</P>
         </Dialog.Inner>
       </Dialog.Outer>
 
-      <Dialog.Outer
-        control={scrollable}
-        nativeOptions={{sheet: {snapPoints: ['100%']}}}>
-        <Dialog.Handle />
-
+      <Dialog.Outer control={scrollable}>
         <Dialog.ScrollableInner
           accessibilityDescribedBy="dialog-description"
           accessibilityLabelledBy="dialog-title">
@@ -230,8 +224,6 @@ export function Dialogs() {
       </Dialog.Outer>
 
       <Dialog.Outer control={testDialog}>
-        <Dialog.Handle />
-
         <Dialog.ScrollableInner
           accessibilityDescribedBy="dialog-description"
           accessibilityLabelledBy="dialog-title">
@@ -356,8 +348,6 @@ export function Dialogs() {
 
       {shouldRenderUnmountTest && (
         <Dialog.Outer control={unmountTestDialog}>
-          <Dialog.Handle />
-
           <Dialog.Inner label="test">
             <H3 nativeID="dialog-title">Unmount Test Dialog</H3>
             <P nativeID="dialog-description">Will unmount in about 5 seconds</P>
diff --git a/src/view/shell/Composer.ios.tsx b/src/view/shell/Composer.ios.tsx
index 18410bf39..02efad878 100644
--- a/src/view/shell/Composer.ios.tsx
+++ b/src/view/shell/Composer.ios.tsx
@@ -1,19 +1,28 @@
-import React, {useLayoutEffect} from 'react'
+import React from 'react'
 import {Modal, View} from 'react-native'
-import {StatusBar} from 'expo-status-bar'
-import * as SystemUI from 'expo-system-ui'
 
+import {useDialogStateControlContext} from '#/state/dialogs'
 import {useComposerState} from '#/state/shell/composer'
 import {atoms as a, useTheme} from '#/alf'
-import {getBackgroundColor, useThemeName} from '#/alf/util/useColorModeTheme'
 import {ComposePost, useComposerCancelRef} from '../com/composer/Composer'
 
 export function Composer({}: {winHeight: number}) {
+  const {setFullyExpandedCount} = useDialogStateControlContext()
   const t = useTheme()
   const state = useComposerState()
   const ref = useComposerCancelRef()
 
   const open = !!state
+  const prevOpen = React.useRef(open)
+
+  React.useEffect(() => {
+    if (open && !prevOpen.current) {
+      setFullyExpandedCount(c => c + 1)
+    } else if (!open && prevOpen.current) {
+      setFullyExpandedCount(c => c - 1)
+    }
+    prevOpen.current = open
+  }, [open, setFullyExpandedCount])
 
   return (
     <Modal
@@ -24,56 +33,18 @@ export function Composer({}: {winHeight: number}) {
       animationType="slide"
       onRequestClose={() => ref.current?.onPressCancel()}>
       <View style={[t.atoms.bg, a.flex_1]}>
-        <Providers open={open}>
-          <ComposePost
-            cancelRef={ref}
-            replyTo={state?.replyTo}
-            onPost={state?.onPost}
-            quote={state?.quote}
-            quoteCount={state?.quoteCount}
-            mention={state?.mention}
-            text={state?.text}
-            imageUris={state?.imageUris}
-            videoUri={state?.videoUri}
-          />
-        </Providers>
+        <ComposePost
+          cancelRef={ref}
+          replyTo={state?.replyTo}
+          onPost={state?.onPost}
+          quote={state?.quote}
+          quoteCount={state?.quoteCount}
+          mention={state?.mention}
+          text={state?.text}
+          imageUris={state?.imageUris}
+          videoUri={state?.videoUri}
+        />
       </View>
     </Modal>
   )
 }
-
-function Providers({
-  children,
-  open,
-}: {
-  children: React.ReactNode
-  open: boolean
-}) {
-  // on iOS, it's a native formSheet. We use FullWindowOverlay to make
-  // the dialogs appear over it
-  return (
-    <>
-      {children}
-      <IOSModalBackground active={open} />
-    </>
-  )
-}
-
-// Generally, the backdrop of the app is the theme color, but when this is open
-// we want it to be black due to the modal being a form sheet.
-function IOSModalBackground({active}: {active: boolean}) {
-  const theme = useThemeName()
-
-  useLayoutEffect(() => {
-    SystemUI.setBackgroundColorAsync('black')
-
-    return () => {
-      SystemUI.setBackgroundColorAsync(getBackgroundColor(theme))
-    }
-  }, [theme])
-
-  // Set the status bar to light - however, only if the modal is active
-  // If we rely on this component being mounted to set this,
-  // there'll be a delay before it switches back to default.
-  return active ? <StatusBar style="light" animated /> : null
-}
diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx
index aed92cbb7..8bc3de24d 100644
--- a/src/view/shell/index.tsx
+++ b/src/view/shell/index.tsx
@@ -13,6 +13,14 @@ import * as NavigationBar from 'expo-navigation-bar'
 import {StatusBar} from 'expo-status-bar'
 import {useNavigation, useNavigationState} from '@react-navigation/native'
 
+import {useDedupe} from '#/lib/hooks/useDedupe'
+import {useNotificationsHandler} from '#/lib/hooks/useNotificationHandler'
+import {usePalette} from '#/lib/hooks/usePalette'
+import {useNotificationsRegistration} from '#/lib/notifications/notifications'
+import {isStateAtTabRoot} from '#/lib/routes/helpers'
+import {useTheme} from '#/lib/ThemeContext'
+import {isAndroid, isIOS} from '#/platform/detection'
+import {useDialogStateControlContext} from '#/state/dialogs'
 import {useSession} from '#/state/session'
 import {
   useIsDrawerOpen,
@@ -20,17 +28,9 @@ import {
   useSetDrawerOpen,
 } from '#/state/shell'
 import {useCloseAnyActiveElement} from '#/state/util'
-import {useDedupe} from 'lib/hooks/useDedupe'
-import {useNotificationsHandler} from 'lib/hooks/useNotificationHandler'
-import {usePalette} from 'lib/hooks/usePalette'
-import {useNotificationsRegistration} from 'lib/notifications/notifications'
-import {isStateAtTabRoot} from 'lib/routes/helpers'
-import {useTheme} from 'lib/ThemeContext'
-import {isAndroid} from 'platform/detection'
-import {useDialogStateContext} from 'state/dialogs'
-import {Lightbox} from 'view/com/lightbox/Lightbox'
-import {ModalsContainer} from 'view/com/modals/Modal'
-import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
+import {Lightbox} from '#/view/com/lightbox/Lightbox'
+import {ModalsContainer} from '#/view/com/modals/Modal'
+import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
 import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
 import {SigninDialog} from '#/components/dialogs/Signin'
 import {Outlet as PortalOutlet} from '#/components/Portal'
@@ -61,7 +61,6 @@ function ShellInner() {
   const canGoBack = useNavigationState(state => !isStateAtTabRoot(state))
   const {hasSession} = useSession()
   const closeAnyActiveElement = useCloseAnyActiveElement()
-  const {importantForAccessibility} = useDialogStateContext()
 
   useNotificationsRegistration()
   useNotificationsHandler()
@@ -101,9 +100,7 @@ function ShellInner() {
 
   return (
     <>
-      <Animated.View
-        style={containerPadding}
-        importantForAccessibility={importantForAccessibility}>
+      <Animated.View style={containerPadding}>
         <ErrorBoundary>
           <Drawer
             renderDrawerContent={renderDrawerContent}
@@ -127,6 +124,7 @@ function ShellInner() {
 }
 
 export const Shell: React.FC = function ShellImpl() {
+  const {fullyExpandedCount} = useDialogStateControlContext()
   const pal = usePalette('default')
   const theme = useTheme()
   React.useEffect(() => {
@@ -140,7 +138,14 @@ export const Shell: React.FC = function ShellImpl() {
   }, [theme])
   return (
     <View testID="mobileShellView" style={[styles.outerContainer, pal.view]}>
-      <StatusBar style={theme.colorScheme === 'dark' ? 'light' : 'dark'} />
+      <StatusBar
+        style={
+          theme.colorScheme === 'dark' || (isIOS && fullyExpandedCount > 0)
+            ? 'light'
+            : 'dark'
+        }
+        animated
+      />
       <RoutesContainer>
         <ShellInner />
       </RoutesContainer>