about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--modules/bottom-sheet/index.ts10
-rw-r--r--modules/bottom-sheet/src/BottomSheet.tsx114
-rw-r--r--modules/bottom-sheet/src/BottomSheetNativeComponent.tsx103
-rw-r--r--modules/bottom-sheet/src/BottomSheetPortal.tsx40
-rw-r--r--modules/bottom-sheet/src/lib/Portal.tsx67
-rw-r--r--src/App.native.tsx19
-rw-r--r--src/components/Dialog/index.tsx31
-rw-r--r--src/components/dialogs/GifSelect.tsx4
-rw-r--r--src/components/dialogs/MutedWords.tsx528
-rw-r--r--src/components/dialogs/PostInteractionSettingsDialog.tsx9
-rw-r--r--src/state/dialogs/index.tsx4
-rw-r--r--src/view/com/composer/Composer.tsx20
-rw-r--r--src/view/com/composer/GifAltText.tsx9
-rw-r--r--src/view/com/composer/photos/Gallery.tsx13
-rw-r--r--src/view/com/composer/photos/ImageAltTextDialog.tsx6
-rw-r--r--src/view/com/composer/photos/SelectGifBtn.tsx5
-rw-r--r--src/view/com/composer/threadgate/ThreadgateBtn.tsx5
-rw-r--r--src/view/com/composer/videos/SubtitleDialog.tsx4
-rw-r--r--src/view/screens/Storybook/Dialogs.tsx66
-rw-r--r--src/view/shell/index.tsx2
20 files changed, 550 insertions, 509 deletions
diff --git a/modules/bottom-sheet/index.ts b/modules/bottom-sheet/index.ts
index 1fe3dac0e..4009f2ab2 100644
--- a/modules/bottom-sheet/index.ts
+++ b/modules/bottom-sheet/index.ts
@@ -4,9 +4,19 @@ import {
   BottomSheetState,
   BottomSheetViewProps,
 } from './src/BottomSheet.types'
+import {BottomSheetNativeComponent} from './src/BottomSheetNativeComponent'
+import {
+  BottomSheetOutlet,
+  BottomSheetPortalProvider,
+  BottomSheetProvider,
+} from './src/BottomSheetPortal'
 
 export {
   BottomSheet,
+  BottomSheetNativeComponent,
+  BottomSheetOutlet,
+  BottomSheetPortalProvider,
+  BottomSheetProvider,
   BottomSheetSnapPoint,
   type BottomSheetState,
   type BottomSheetViewProps,
diff --git a/modules/bottom-sheet/src/BottomSheet.tsx b/modules/bottom-sheet/src/BottomSheet.tsx
index 9e7d0c209..bcc2c42ad 100644
--- a/modules/bottom-sheet/src/BottomSheet.tsx
+++ b/modules/bottom-sheet/src/BottomSheet.tsx
@@ -1,100 +1,24 @@
-import * as React from 'react'
-import {
-  Dimensions,
-  NativeSyntheticEvent,
-  Platform,
-  StyleProp,
-  View,
-  ViewStyle,
-} from 'react-native'
-import {requireNativeModule, requireNativeViewManager} from 'expo-modules-core'
+import React from 'react'
 
-import {BottomSheetState, BottomSheetViewProps} from './BottomSheet.types'
+import {BottomSheetViewProps} from './BottomSheet.types'
+import {BottomSheetNativeComponent} from './BottomSheetNativeComponent'
+import {useBottomSheetPortal_INTERNAL} from './BottomSheetPortal'
 
-const screenHeight = Dimensions.get('screen').height
+export const BottomSheet = React.forwardRef<
+  BottomSheetNativeComponent,
+  BottomSheetViewProps
+>(function BottomSheet(props, ref) {
+  const Portal = useBottomSheetPortal_INTERNAL()
 
-const NativeView: React.ComponentType<
-  BottomSheetViewProps & {
-    ref: React.RefObject<any>
-    style: StyleProp<ViewStyle>
-  }
-> = requireNativeViewManager('BottomSheet')
-
-const NativeModule = requireNativeModule('BottomSheet')
-
-export class BottomSheet extends React.Component<
-  BottomSheetViewProps,
-  {
-    open: boolean
-  }
-> {
-  ref = React.createRef<any>()
-
-  constructor(props: BottomSheetViewProps) {
-    super(props)
-    this.state = {
-      open: false,
-    }
-  }
-
-  present() {
-    this.setState({open: true})
-  }
-
-  dismiss() {
-    this.ref.current?.dismiss()
-  }
-
-  private onStateChange = (
-    event: NativeSyntheticEvent<{state: BottomSheetState}>,
-  ) => {
-    const {state} = event.nativeEvent
-    const isOpen = state !== 'closed'
-    this.setState({open: isOpen})
-    this.props.onStateChange?.(event)
-  }
-
-  private updateLayout = () => {
-    this.ref.current?.updateLayout()
-  }
-
-  static dismissAll = async () => {
-    await NativeModule.dismissAll()
-  }
-
-  render() {
-    const {children, backgroundColor, ...rest} = this.props
-    const cornerRadius = rest.cornerRadius ?? 0
-
-    if (!this.state.open) {
-      return null
-    }
-
-    return (
-      <NativeView
-        {...rest}
-        onStateChange={this.onStateChange}
-        ref={this.ref}
-        style={{
-          position: 'absolute',
-          height: screenHeight,
-          width: '100%',
-        }}
-        containerBackgroundColor={backgroundColor}>
-        <View
-          style={[
-            {
-              flex: 1,
-              backgroundColor,
-            },
-            Platform.OS === 'android' && {
-              borderTopLeftRadius: cornerRadius,
-              borderTopRightRadius: cornerRadius,
-            },
-          ]}>
-          <View onLayout={this.updateLayout}>{children}</View>
-        </View>
-      </NativeView>
+  if (__DEV__ && !Portal) {
+    throw new Error(
+      'BottomSheet: You need to wrap your component tree with a <BottomSheetPortalProvider> to use the bottom sheet.',
     )
   }
-}
+
+  return (
+    <Portal>
+      <BottomSheetNativeComponent {...props} ref={ref} />
+    </Portal>
+  )
+})
diff --git a/modules/bottom-sheet/src/BottomSheetNativeComponent.tsx b/modules/bottom-sheet/src/BottomSheetNativeComponent.tsx
new file mode 100644
index 000000000..eadd9b4a1
--- /dev/null
+++ b/modules/bottom-sheet/src/BottomSheetNativeComponent.tsx
@@ -0,0 +1,103 @@
+import * as React from 'react'
+import {
+  Dimensions,
+  NativeSyntheticEvent,
+  Platform,
+  StyleProp,
+  View,
+  ViewStyle,
+} from 'react-native'
+import {requireNativeModule, requireNativeViewManager} from 'expo-modules-core'
+
+import {BottomSheetState, BottomSheetViewProps} from './BottomSheet.types'
+import {BottomSheetPortalProvider} from './BottomSheetPortal'
+
+const screenHeight = Dimensions.get('screen').height
+
+const NativeView: React.ComponentType<
+  BottomSheetViewProps & {
+    ref: React.RefObject<any>
+    style: StyleProp<ViewStyle>
+  }
+> = requireNativeViewManager('BottomSheet')
+
+const NativeModule = requireNativeModule('BottomSheet')
+
+export class BottomSheetNativeComponent extends React.Component<
+  BottomSheetViewProps,
+  {
+    open: boolean
+  }
+> {
+  ref = React.createRef<any>()
+
+  constructor(props: BottomSheetViewProps) {
+    super(props)
+    this.state = {
+      open: false,
+    }
+  }
+
+  present() {
+    this.setState({open: true})
+  }
+
+  dismiss() {
+    this.ref.current?.dismiss()
+  }
+
+  private onStateChange = (
+    event: NativeSyntheticEvent<{state: BottomSheetState}>,
+  ) => {
+    const {state} = event.nativeEvent
+    const isOpen = state !== 'closed'
+    this.setState({open: isOpen})
+    this.props.onStateChange?.(event)
+  }
+
+  private updateLayout = () => {
+    this.ref.current?.updateLayout()
+  }
+
+  static dismissAll = async () => {
+    await NativeModule.dismissAll()
+  }
+
+  render() {
+    const {children, backgroundColor, ...rest} = this.props
+    const cornerRadius = rest.cornerRadius ?? 0
+
+    if (!this.state.open) {
+      return null
+    }
+
+    return (
+      <NativeView
+        {...rest}
+        onStateChange={this.onStateChange}
+        ref={this.ref}
+        style={{
+          position: 'absolute',
+          height: screenHeight,
+          width: '100%',
+        }}
+        containerBackgroundColor={backgroundColor}>
+        <View
+          style={[
+            {
+              flex: 1,
+              backgroundColor,
+            },
+            Platform.OS === 'android' && {
+              borderTopLeftRadius: cornerRadius,
+              borderTopRightRadius: cornerRadius,
+            },
+          ]}>
+          <View onLayout={this.updateLayout}>
+            <BottomSheetPortalProvider>{children}</BottomSheetPortalProvider>
+          </View>
+        </View>
+      </NativeView>
+    )
+  }
+}
diff --git a/modules/bottom-sheet/src/BottomSheetPortal.tsx b/modules/bottom-sheet/src/BottomSheetPortal.tsx
new file mode 100644
index 000000000..da14cfa77
--- /dev/null
+++ b/modules/bottom-sheet/src/BottomSheetPortal.tsx
@@ -0,0 +1,40 @@
+import React from 'react'
+
+import {createPortalGroup_INTERNAL} from './lib/Portal'
+
+type PortalContext = React.ElementType<{children: React.ReactNode}>
+
+const Context = React.createContext({} as PortalContext)
+
+export const useBottomSheetPortal_INTERNAL = () => React.useContext(Context)
+
+export function BottomSheetPortalProvider({
+  children,
+}: {
+  children: React.ReactNode
+}) {
+  const portal = React.useMemo(() => {
+    return createPortalGroup_INTERNAL()
+  }, [])
+
+  return (
+    <Context.Provider value={portal.Portal}>
+      <portal.Provider>
+        {children}
+        <portal.Outlet />
+      </portal.Provider>
+    </Context.Provider>
+  )
+}
+
+const defaultPortal = createPortalGroup_INTERNAL()
+
+export const BottomSheetOutlet = defaultPortal.Outlet
+
+export function BottomSheetProvider({children}: {children: React.ReactNode}) {
+  return (
+    <Context.Provider value={defaultPortal.Portal}>
+      <defaultPortal.Provider>{children}</defaultPortal.Provider>
+    </Context.Provider>
+  )
+}
diff --git a/modules/bottom-sheet/src/lib/Portal.tsx b/modules/bottom-sheet/src/lib/Portal.tsx
new file mode 100644
index 000000000..dd1bc4c13
--- /dev/null
+++ b/modules/bottom-sheet/src/lib/Portal.tsx
@@ -0,0 +1,67 @@
+import React from 'react'
+
+type Component = React.ReactElement
+
+type ContextType = {
+  outlet: Component | null
+  append(id: string, component: Component): void
+  remove(id: string): void
+}
+
+type ComponentMap = {
+  [id: string]: Component
+}
+
+export function createPortalGroup_INTERNAL() {
+  const Context = React.createContext<ContextType>({
+    outlet: null,
+    append: () => {},
+    remove: () => {},
+  })
+
+  function Provider(props: React.PropsWithChildren<{}>) {
+    const map = React.useRef<ComponentMap>({})
+    const [outlet, setOutlet] = React.useState<ContextType['outlet']>(null)
+
+    const append = React.useCallback<ContextType['append']>((id, component) => {
+      if (map.current[id]) return
+      map.current[id] = <React.Fragment key={id}>{component}</React.Fragment>
+      setOutlet(<>{Object.values(map.current)}</>)
+    }, [])
+
+    const remove = React.useCallback<ContextType['remove']>(id => {
+      delete map.current[id]
+      setOutlet(<>{Object.values(map.current)}</>)
+    }, [])
+
+    const contextValue = React.useMemo(
+      () => ({
+        outlet,
+        append,
+        remove,
+      }),
+      [outlet, append, remove],
+    )
+
+    return (
+      <Context.Provider value={contextValue}>{props.children}</Context.Provider>
+    )
+  }
+
+  function Outlet() {
+    const ctx = React.useContext(Context)
+    return ctx.outlet
+  }
+
+  function Portal({children}: React.PropsWithChildren<{}>) {
+    const {append, remove} = React.useContext(Context)
+    const id = React.useId()
+    React.useEffect(() => {
+      append(id, children as Component)
+      return () => remove(id)
+    }, [id, children, append, remove])
+    return null
+  }
+
+  return {Provider, Outlet, Portal}
+}
diff --git a/src/App.native.tsx b/src/App.native.tsx
index 96b493af4..0b9f112ee 100644
--- a/src/App.native.tsx
+++ b/src/App.native.tsx
@@ -68,6 +68,7 @@ import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
 import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs'
 import {Provider as PortalProvider} from '#/components/Portal'
 import {Splash} from '#/Splash'
+import {BottomSheetProvider} from '../modules/bottom-sheet'
 import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
 
 SplashScreen.preventAutoHideAsync()
@@ -197,14 +198,16 @@ function App() {
                       <DialogStateProvider>
                         <LightboxStateProvider>
                           <PortalProvider>
-                            <StarterPackProvider>
-                              <SafeAreaProvider
-                                initialMetrics={initialWindowMetrics}>
-                                <IntentDialogProvider>
-                                  <InnerApp />
-                                </IntentDialogProvider>
-                              </SafeAreaProvider>
-                            </StarterPackProvider>
+                            <BottomSheetProvider>
+                              <StarterPackProvider>
+                                <SafeAreaProvider
+                                  initialMetrics={initialWindowMetrics}>
+                                  <IntentDialogProvider>
+                                    <InnerApp />
+                                  </IntentDialogProvider>
+                                </SafeAreaProvider>
+                              </StarterPackProvider>
+                            </BottomSheetProvider>
                           </PortalProvider>
                         </LightboxStateProvider>
                       </DialogStateProvider>
diff --git a/src/components/Dialog/index.tsx b/src/components/Dialog/index.tsx
index 49b5e10b2..6d859aeb0 100644
--- a/src/components/Dialog/index.tsx
+++ b/src/components/Dialog/index.tsx
@@ -31,12 +31,12 @@ import {
   DialogOuterProps,
 } from '#/components/Dialog/types'
 import {createInput} from '#/components/forms/TextField'
-import {Portal as DefaultPortal} from '#/components/Portal'
 import {BottomSheet, BottomSheetSnapPoint} from '../../../modules/bottom-sheet'
 import {
   BottomSheetSnapPointChangeEvent,
   BottomSheetStateChangeEvent,
 } from '../../../modules/bottom-sheet/src/BottomSheet.types'
+import {BottomSheetNativeComponent} from '../../../modules/bottom-sheet/src/BottomSheetNativeComponent'
 
 export {useDialogContext, useDialogControl} from '#/components/Dialog/context'
 export * from '#/components/Dialog/types'
@@ -50,10 +50,9 @@ export function Outer({
   onClose,
   nativeOptions,
   testID,
-  Portal = DefaultPortal,
 }: React.PropsWithChildren<DialogOuterProps>) {
   const t = useTheme()
-  const ref = React.useRef<BottomSheet>(null)
+  const ref = React.useRef<BottomSheetNativeComponent>(null)
   const closeCallbacks = React.useRef<(() => void)[]>([])
   const {setDialogIsOpen, setFullyExpandedCount} =
     useDialogStateControlContext()
@@ -154,20 +153,18 @@ export function Outer({
   )
 
   return (
-    <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>
+    <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>
   )
 }
 
diff --git a/src/components/dialogs/GifSelect.tsx b/src/components/dialogs/GifSelect.tsx
index c0ed202da..765e9415d 100644
--- a/src/components/dialogs/GifSelect.tsx
+++ b/src/components/dialogs/GifSelect.tsx
@@ -30,18 +30,15 @@ import {useThrottledValue} from '#/components/hooks/useThrottledValue'
 import {ArrowLeft_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow'
 import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
 import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
-import {PortalComponent} from '#/components/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()
 
@@ -65,7 +62,6 @@ export function GifSelectDialog({
     <Dialog.Outer
       control={control}
       onClose={onClose}
-      Portal={Portal}
       nativeOptions={{
         bottomInset: 0,
         // use system corner radius on iOS
diff --git a/src/components/dialogs/MutedWords.tsx b/src/components/dialogs/MutedWords.tsx
index c3aae8f0d..81a614103 100644
--- a/src/components/dialogs/MutedWords.tsx
+++ b/src/components/dialogs/MutedWords.tsx
@@ -30,14 +30,11 @@ 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 (
@@ -108,349 +105,307 @@ function MutedWordsInner() {
   }, [_, field, targets, addMutedWord, setField, durations, excludeFollowing])
 
   return (
-    <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_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>
+    <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>
 
-          <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_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={[
-                  gtMobile && [a.flex_row, a.align_center, a.justify_start],
+                  a.flex_1,
+                  a.flex_row,
+                  a.justify_start,
+                  a.align_center,
                   a.gap_sm,
                 ]}>
-                <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 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 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>
+                <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.Group>
 
-            <Toggle.Group
-              label={_(
-                msg`Select what content this mute word should apply to.`,
-              )}
-              type="radio"
-              values={targets}
-              onChange={setTargets}>
-              <Text
+              <View
                 style={[
-                  a.pb_xs,
-                  a.text_sm,
-                  a.font_bold,
-                  t.atoms.text_contrast_medium,
+                  a.flex_1,
+                  a.flex_row,
+                  a.justify_start,
+                  a.align_center,
+                  a.gap_sm,
                 ]}>
-                <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 in post text and tags`)}
-                  name="content"
+                  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>Text & tags</Trans>
+                        <Trans>7 days</Trans>
                       </Toggle.LabelText>
                     </View>
-                    <PageText size="sm" />
                   </TargetToggle>
                 </Toggle.Item>
 
                 <Toggle.Item
-                  label={_(msg`Mute this word in tags only`)}
-                  name="tag"
+                  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>Tags only</Trans>
+                        <Trans>30 days</Trans>
                       </Toggle.LabelText>
                     </View>
-                    <Hashtag size="sm" />
                   </TargetToggle>
                 </Toggle.Item>
               </View>
-            </Toggle.Group>
+            </View>
+          </Toggle.Group>
 
-            <View>
-              <Text
-                style={[
-                  a.pb_xs,
-                  a.text_sm,
-                  a.font_bold,
-                  t.atoms.text_contrast_medium,
-                ]}>
-                <Trans>Options:</Trans>
-              </Text>
+          <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]}>
               <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}>
+                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.Checkbox />
+                    <Toggle.Radio />
                     <Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
-                      <Trans>Exclude users you follow</Trans>
+                      <Trans>Text & tags</Trans>
                     </Toggle.LabelText>
                   </View>
+                  <PageText size="sm" />
                 </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>
+              <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>
+          </Toggle.Group>
 
-            {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]}>
+          <View>
             <Text
               style={[
-                a.text_md,
+                a.pb_xs,
+                a.text_sm,
                 a.font_bold,
-                a.pb_md,
-                t.atoms.text_contrast_high,
+                t.atoms.text_contrast_medium,
               ]}>
-              <Trans>Your muted words</Trans>
+              <Trans>Options:</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>
 
-            {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
+          <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
                 style={[
-                  a.py_md,
-                  a.px_lg,
-                  a.rounded_md,
-                  t.atoms.bg_contrast_25,
+                  a.italic,
+                  {color: t.palette.white},
+                  native({marginTop: 2}),
                 ]}>
-                <Text style={[a.italic, t.atoms.text_contrast_high]}>
-                  <Trans>You haven't muted any words or tags yet</Trans>
-                </Text>
-              </View>
-            )}
-          </View>
+                {error}
+              </Text>
+            </View>
+          )}
+        </View>
+
+        <Divider />
 
-          {isNative && <View style={{height: 20}} />}
+        <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>
+
+          {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>
+          )}
         </View>
 
-        <Dialog.Close />
-      </Dialog.ScrollableInner>
+        {isNative && <View style={{height: 20}} />}
+      </View>
 
-      <Portal.Outlet />
-    </Portal.Provider>
+      <Dialog.Close />
+    </Dialog.ScrollableInner>
   )
 }
 
@@ -482,7 +437,6 @@ 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 bddc49968..0b8b386d3 100644
--- a/src/components/dialogs/PostInteractionSettingsDialog.tsx
+++ b/src/components/dialogs/PostInteractionSettingsDialog.tsx
@@ -37,7 +37,6 @@ 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 = {
@@ -55,15 +54,13 @@ export type PostInteractionSettingsFormProps = {
 
 export function PostInteractionSettingsControlledDialog({
   control,
-  Portal,
   ...rest
 }: PostInteractionSettingsFormProps & {
   control: Dialog.DialogControlProps
-  Portal?: PortalComponent
 }) {
   const {_} = useLingui()
   return (
-    <Dialog.Outer control={control} Portal={Portal}>
+    <Dialog.Outer control={control}>
       <Dialog.Handle />
       <Dialog.ScrollableInner
         label={_(msg`Edit post interaction settings`)}
@@ -207,7 +204,9 @@ export function PostInteractionSettingsDialogControlledInner(
       label={_(msg`Edit post interaction settings`)}
       style={[{maxWidth: 500}, a.w_full]}>
       {isLoading ? (
-        <Loader size="xl" />
+        <View style={[a.flex_1, a.py_4xl, a.align_center, a.justify_center]}>
+          <Loader size="xl" />
+        </View>
       ) : (
         <PostInteractionSettingsForm
           replySettingsDisabled={!isThreadgateOwnedByViewer}
diff --git a/src/state/dialogs/index.tsx b/src/state/dialogs/index.tsx
index 80893190f..f770e9c16 100644
--- a/src/state/dialogs/index.tsx
+++ b/src/state/dialogs/index.tsx
@@ -3,7 +3,7 @@ import React from 'react'
 import {isWeb} from '#/platform/detection'
 import {DialogControlRefProps} from '#/components/Dialog'
 import {Provider as GlobalDialogsProvider} from '#/components/dialogs/Context'
-import {BottomSheet} from '../../../modules/bottom-sheet'
+import {BottomSheetNativeComponent} from '../../../modules/bottom-sheet'
 
 interface IDialogContext {
   /**
@@ -61,7 +61,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
 
       return openDialogs.current.size > 0
     } else {
-      BottomSheet.dismissAll()
+      BottomSheetNativeComponent.dismissAll()
       return false
     }
   }, [])
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx
index 8cc8fba0d..49a498cce 100644
--- a/src/view/com/composer/Composer.tsx
+++ b/src/view/com/composer/Composer.tsx
@@ -107,9 +107,9 @@ 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 {BottomSheetPortalProvider} from '../../../../modules/bottom-sheet'
 import {
   composerReducer,
   createComposerState,
@@ -117,8 +117,6 @@ import {
 } from './state/composer'
 import {NO_VIDEO, NoVideoState, processVideo, VideoState} from './state/video'
 
-const Portal = createPortalGroup()
-
 type CancelRef = {
   onPressCancel: () => void
 }
@@ -522,7 +520,7 @@ export const ComposePost = ({
   const keyboardVerticalOffset = useKeyboardVerticalOffset()
 
   return (
-    <Portal.Provider>
+    <BottomSheetPortalProvider>
       <KeyboardAvoidingView
         testID="composePostView"
         behavior={isIOS ? 'padding' : 'height'}
@@ -666,11 +664,7 @@ export const ComposePost = ({
               />
             </View>
 
-            <Gallery
-              images={images}
-              dispatch={dispatch}
-              Portal={Portal.Portal}
-            />
+            <Gallery images={images} dispatch={dispatch} />
 
             {extGif && (
               <View style={a.relative} key={extGif.url}>
@@ -684,7 +678,6 @@ export const ComposePost = ({
                   gif={extGif}
                   altText={extGifAlt ?? ''}
                   onSubmit={handleChangeGifAltText}
-                  Portal={Portal.Portal}
                 />
               </View>
             )}
@@ -744,7 +737,6 @@ export const ComposePost = ({
                         },
                       })
                     }}
-                    Portal={Portal.Portal}
                   />
                 </Animated.View>
               )}
@@ -782,7 +774,6 @@ export const ComposePost = ({
                 })
               }}
               style={bottomBarAnimatedStyle}
-              Portal={Portal.Portal}
             />
           )}
           <View
@@ -819,7 +810,6 @@ export const ComposePost = ({
                     onClose={focusTextInput}
                     onSelectGif={onSelectGif}
                     disabled={hasMedia}
-                    Portal={Portal.Portal}
                   />
                   {!isMobile ? (
                     <Button
@@ -849,11 +839,9 @@ export const ComposePost = ({
           onConfirm={onClose}
           confirmButtonCta={_(msg`Discard`)}
           confirmButtonColor="negative"
-          Portal={Portal.Portal}
         />
       </KeyboardAvoidingView>
-      <Portal.Outlet />
-    </Portal.Provider>
+    </BottomSheetPortalProvider>
   )
 }
 
diff --git a/src/view/com/composer/GifAltText.tsx b/src/view/com/composer/GifAltText.tsx
index 01778c381..732bd4bd6 100644
--- a/src/view/com/composer/GifAltText.tsx
+++ b/src/view/com/composer/GifAltText.tsx
@@ -21,7 +21,6 @@ import * as TextField from '#/components/forms/TextField'
 import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
 import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
 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'
@@ -30,12 +29,10 @@ export function GifAltTextDialog({
   gif,
   altText,
   onSubmit,
-  Portal,
 }: {
   gif: Gif
   altText: string
   onSubmit: (alt: string) => void
-  Portal: PortalComponent
 }) {
   const {data} = useResolveGifQuery(gif)
   const vendorAltText = parseAltFromGIFDescription(data?.description ?? '').alt
@@ -50,7 +47,6 @@ export function GifAltTextDialog({
       thumb={data.thumb?.source.path}
       params={params}
       onSubmit={onSubmit}
-      Portal={Portal}
     />
   )
 }
@@ -61,14 +57,12 @@ export function GifAltTextDialogLoaded({
   onSubmit,
   params,
   thumb,
-  Portal,
 }: {
   vendorAltText: string
   altText: string
   onSubmit: (alt: string) => void
   params: EmbedPlayerParams
   thumb: string | undefined
-  Portal: PortalComponent
 }) {
   const control = Dialog.useDialogControl()
   const {_} = useLingui()
@@ -113,8 +107,7 @@ export function GifAltTextDialogLoaded({
         control={control}
         onClose={() => {
           onSubmit(altTextDraft)
-        }}
-        Portal={Portal}>
+        }}>
         <Dialog.Handle />
         <AltTextInner
           vendorAltText={vendorAltText}
diff --git a/src/view/com/composer/photos/Gallery.tsx b/src/view/com/composer/photos/Gallery.tsx
index 3958a85c0..5ff7042bc 100644
--- a/src/view/com/composer/photos/Gallery.tsx
+++ b/src/view/com/composer/photos/Gallery.tsx
@@ -21,7 +21,6 @@ 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'
@@ -31,7 +30,6 @@ const IMAGE_GAP = 8
 interface GalleryProps {
   images: ComposerImage[]
   dispatch: (action: ComposerAction) => void
-  Portal: PortalComponent
 }
 
 export let Gallery = (props: GalleryProps): React.ReactNode => {
@@ -59,12 +57,7 @@ interface GalleryInnerProps extends GalleryProps {
   containerInfo: Dimensions
 }
 
-const GalleryInner = ({
-  images,
-  containerInfo,
-  dispatch,
-  Portal,
-}: GalleryInnerProps) => {
+const GalleryInner = ({images, containerInfo, dispatch}: GalleryInnerProps) => {
   const {isMobile} = useWebMediaQueries()
 
   const {altTextControlStyle, imageControlsStyle, imageStyle} =
@@ -118,7 +111,6 @@ const GalleryInner = ({
               onRemove={() => {
                 dispatch({type: 'embed_remove_image', image})
               }}
-              Portal={Portal}
             />
           )
         })}
@@ -135,7 +127,6 @@ type GalleryItemProps = {
   imageStyle?: ViewStyle
   onChange: (next: ComposerImage) => void
   onRemove: () => void
-  Portal: PortalComponent
 }
 
 const GalleryItem = ({
@@ -145,7 +136,6 @@ const GalleryItem = ({
   imageStyle,
   onChange,
   onRemove,
-  Portal,
 }: GalleryItemProps): React.ReactNode => {
   const {_} = useLingui()
   const t = useTheme()
@@ -240,7 +230,6 @@ 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 e9e8d4222..aa0b0987a 100644
--- a/src/view/com/composer/photos/ImageAltTextDialog.tsx
+++ b/src/view/com/composer/photos/ImageAltTextDialog.tsx
@@ -15,21 +15,18 @@ import * as Dialog from '#/components/Dialog'
 import {DialogControlProps} from '#/components/Dialog'
 import * as TextField from '#/components/forms/TextField'
 import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
-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 = ({
   control,
   image,
   onChange,
-  Portal,
 }: Props): React.ReactNode => {
   const [altText, setAltText] = React.useState(image.alt)
 
@@ -41,8 +38,7 @@ export const ImageAltTextDialog = ({
           ...image,
           alt: enforceLen(altText, MAX_ALT_TEXT, true),
         })
-      }}
-      Portal={Portal}>
+      }}>
       <Dialog.Handle />
       <ImageAltTextInner
         control={control}
diff --git a/src/view/com/composer/photos/SelectGifBtn.tsx b/src/view/com/composer/photos/SelectGifBtn.tsx
index d482e0783..d13df0a11 100644
--- a/src/view/com/composer/photos/SelectGifBtn.tsx
+++ b/src/view/com/composer/photos/SelectGifBtn.tsx
@@ -9,16 +9,14 @@ 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, Portal}: Props) {
+export function SelectGifBtn({onClose, onSelectGif, disabled}: Props) {
   const {_} = useLingui()
   const ref = useRef<{open: () => void}>(null)
   const t = useTheme()
@@ -48,7 +46,6 @@ export function SelectGifBtn({onClose, onSelectGif, disabled, Portal}: Props) {
         controlRef={ref}
         onClose={onClose}
         onSelectGif={onSelectGif}
-        Portal={Portal}
       />
     </>
   )
diff --git a/src/view/com/composer/threadgate/ThreadgateBtn.tsx b/src/view/com/composer/threadgate/ThreadgateBtn.tsx
index 7e57a57d4..b0806180c 100644
--- a/src/view/com/composer/threadgate/ThreadgateBtn.tsx
+++ b/src/view/com/composer/threadgate/ThreadgateBtn.tsx
@@ -13,7 +13,6 @@ 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,
@@ -21,7 +20,6 @@ export function ThreadgateBtn({
   threadgateAllowUISettings,
   onChangeThreadgateAllowUISettings,
   style,
-  Portal,
 }: {
   postgate: AppBskyFeedPostgate.Record
   onChangePostgate: (v: AppBskyFeedPostgate.Record) => void
@@ -30,8 +28,6 @@ export function ThreadgateBtn({
   onChangeThreadgateAllowUISettings: (v: ThreadgateAllowUISetting[]) => void
 
   style?: StyleProp<AnimatedStyle<ViewStyle>>
-
-  Portal: PortalComponent
 }) {
   const {_} = useLingui()
   const t = useTheme()
@@ -81,7 +77,6 @@ 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 04522ee1d..27c3de02b 100644
--- a/src/view/com/composer/videos/SubtitleDialog.tsx
+++ b/src/view/com/composer/videos/SubtitleDialog.tsx
@@ -17,7 +17,6 @@ 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'
 
@@ -30,7 +29,6 @@ interface Props {
   captions: CaptionsTrack[]
   saveAltText: (altText: string) => void
   setCaptions: (updater: (prev: CaptionsTrack[]) => CaptionsTrack[]) => void
-  Portal: PortalComponent
 }
 
 export function SubtitleDialogBtn(props: Props) {
@@ -58,7 +56,7 @@ export function SubtitleDialogBtn(props: Props) {
           {isWeb ? <Trans>Captions & alt text</Trans> : <Trans>Alt text</Trans>}
         </ButtonText>
       </Button>
-      <Dialog.Outer control={control} Portal={props.Portal}>
+      <Dialog.Outer control={control}>
         <Dialog.Handle />
         <SubtitleDialogInner {...props} />
       </Dialog.Outer>
diff --git a/src/view/screens/Storybook/Dialogs.tsx b/src/view/screens/Storybook/Dialogs.tsx
index e6fcef555..343d7f07b 100644
--- a/src/view/screens/Storybook/Dialogs.tsx
+++ b/src/view/screens/Storybook/Dialogs.tsx
@@ -8,13 +8,10 @@ import {atoms as a} from '#/alf'
 import {Button, ButtonText} from '#/components/Button'
 import * as Dialog from '#/components/Dialog'
 import * as Menu from '#/components/Menu'
-import {createPortalGroup} from '#/components/Portal'
 import * as Prompt from '#/components/Prompt'
 import {H3, P, Text} from '#/components/Typography'
 import {PlatformInfo} from '../../../../modules/expo-bluesky-swiss-army'
 
-const Portal = createPortalGroup()
-
 export function Dialogs() {
   const scrollable = Dialog.useDialogControl()
   const basic = Dialog.useDialogControl()
@@ -201,41 +198,34 @@ export function Dialogs() {
       </Dialog.Outer>
 
       <Dialog.Outer control={withMenu}>
-        <Portal.Provider>
-          <Dialog.Inner label="test">
-            <H3 nativeID="dialog-title">Dialog with Menu</H3>
-            <Menu.Root>
-              <Menu.Trigger label="Open menu">
-                {({props}) => (
-                  <Button
-                    style={a.mt_2xl}
-                    label="Open menu"
-                    color="primary"
-                    variant="solid"
-                    size="large"
-                    {...props}>
-                    <ButtonText>Open Menu</ButtonText>
-                  </Button>
-                )}
-              </Menu.Trigger>
-              <Menu.Outer Portal={Portal.Portal}>
-                <Menu.Group>
-                  <Menu.Item
-                    label="Item 1"
-                    onPress={() => console.log('item 1')}>
-                    <Menu.ItemText>Item 1</Menu.ItemText>
-                  </Menu.Item>
-                  <Menu.Item
-                    label="Item 2"
-                    onPress={() => console.log('item 2')}>
-                    <Menu.ItemText>Item 2</Menu.ItemText>
-                  </Menu.Item>
-                </Menu.Group>
-              </Menu.Outer>
-            </Menu.Root>
-          </Dialog.Inner>
-          <Portal.Outlet />
-        </Portal.Provider>
+        <Dialog.Inner label="test">
+          <H3 nativeID="dialog-title">Dialog with Menu</H3>
+          <Menu.Root>
+            <Menu.Trigger label="Open menu">
+              {({props}) => (
+                <Button
+                  style={a.mt_2xl}
+                  label="Open menu"
+                  color="primary"
+                  variant="solid"
+                  size="large"
+                  {...props}>
+                  <ButtonText>Open Menu</ButtonText>
+                </Button>
+              )}
+            </Menu.Trigger>
+            <Menu.Outer>
+              <Menu.Group>
+                <Menu.Item label="Item 1" onPress={() => console.log('item 1')}>
+                  <Menu.ItemText>Item 1</Menu.ItemText>
+                </Menu.Item>
+                <Menu.Item label="Item 2" onPress={() => console.log('item 2')}>
+                  <Menu.ItemText>Item 2</Menu.ItemText>
+                </Menu.Item>
+              </Menu.Group>
+            </Menu.Outer>
+          </Menu.Root>
+        </Dialog.Inner>
       </Dialog.Outer>
 
       <Dialog.Outer control={scrollable}>
diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx
index 8bc3de24d..9f7569beb 100644
--- a/src/view/shell/index.tsx
+++ b/src/view/shell/index.tsx
@@ -34,6 +34,7 @@ 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'
+import {BottomSheetOutlet} from '../../../modules/bottom-sheet'
 import {updateActiveViewAsync} from '../../../modules/expo-bluesky-swiss-army/src/VisibilityView'
 import {RoutesContainer, TabsNavigator} from '../../Navigation'
 import {Composer} from './Composer'
@@ -119,6 +120,7 @@ function ShellInner() {
       <SigninDialog />
       <Lightbox />
       <PortalOutlet />
+      <BottomSheetOutlet />
     </>
   )
 }