about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--src/components/Dialog/index.tsx85
-rw-r--r--src/components/FullWindowOverlay.ios.tsx1
-rw-r--r--src/components/FullWindowOverlay.tsx1
-rw-r--r--src/view/com/composer/Composer.tsx226
-rw-r--r--src/view/com/composer/ComposerReplyTo.tsx6
-rw-r--r--src/view/com/composer/threadgate/ThreadgateBtn.tsx5
-rw-r--r--src/view/com/modals/Modal.tsx41
-rw-r--r--src/view/com/modals/Threadgate.tsx21
-rw-r--r--src/view/com/util/BottomSheetCustomBackdrop.tsx4
-rw-r--r--src/view/com/util/forms/DropdownButton.tsx35
-rw-r--r--src/view/shell/Composer.tsx63
11 files changed, 268 insertions, 220 deletions
diff --git a/src/components/Dialog/index.tsx b/src/components/Dialog/index.tsx
index f32e0e79e..158244c8e 100644
--- a/src/components/Dialog/index.tsx
+++ b/src/components/Dialog/index.tsx
@@ -32,6 +32,7 @@ import {
   DialogOuterProps,
 } from '#/components/Dialog/types'
 import {createInput} from '#/components/forms/TextField'
+import {FullWindowOverlay} from '#/components/FullWindowOverlay'
 import {Portal} from '#/components/Portal'
 
 export {useDialogContext, useDialogControl} from '#/components/Dialog/context'
@@ -170,47 +171,49 @@ export function Outer({
   return (
     isOpen && (
       <Portal>
-        <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>
+          <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>
     )
   )
diff --git a/src/components/FullWindowOverlay.ios.tsx b/src/components/FullWindowOverlay.ios.tsx
new file mode 100644
index 000000000..db516c658
--- /dev/null
+++ b/src/components/FullWindowOverlay.ios.tsx
@@ -0,0 +1 @@
+export {FullWindowOverlay} from 'react-native-screens'
diff --git a/src/components/FullWindowOverlay.tsx b/src/components/FullWindowOverlay.tsx
new file mode 100644
index 000000000..c9bdb6e72
--- /dev/null
+++ b/src/components/FullWindowOverlay.tsx
@@ -0,0 +1 @@
+export {Fragment as FullWindowOverlay} from 'react'
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx
index c8bb8d726..b1c020a10 100644
--- a/src/view/com/composer/Composer.tsx
+++ b/src/view/com/composer/Composer.tsx
@@ -181,12 +181,7 @@ export const ComposePost = observer(function ComposePost({
       borderColor: interpolateColor(
         hasScrolled.value,
         [0, 1],
-        [
-          'transparent',
-          isWeb
-            ? t.atoms.border_contrast_low.borderColor
-            : t.atoms.border_contrast_high.borderColor,
-        ],
+        ['transparent', t.atoms.border_contrast_medium.borderColor],
       ),
     }
   })
@@ -405,106 +400,112 @@ export const ComposePost = observer(function ComposePost({
       <KeyboardAvoidingView
         testID="composePostView"
         behavior="padding"
-        style={s.flex1}
-        keyboardVerticalOffset={replyTo ? 60 : isAndroid ? 120 : 100}>
-        <View style={[s.flex1, viewStyles]} aria-modal accessibilityViewIsModal>
+        style={a.flex_1}
+        keyboardVerticalOffset={replyTo ? 120 : isAndroid ? 180 : 150}>
+        <View
+          style={[a.flex_1, viewStyles]}
+          aria-modal
+          accessibilityViewIsModal>
           <Animated.View
             style={[
               styles.topbar,
               topBarAnimatedStyle,
               isWeb && isTabletOrDesktop && styles.topbarDesktop,
             ]}>
-            <TouchableOpacity
-              testID="composerDiscardButton"
-              onPress={onPressCancel}
-              onAccessibilityEscape={onPressCancel}
-              accessibilityRole="button"
-              accessibilityLabel={_(msg`Cancel`)}
-              accessibilityHint={_(
-                msg`Closes post composer and discards post draft`,
+            <View style={styles.topbarInner}>
+              <TouchableOpacity
+                testID="composerDiscardButton"
+                onPress={onPressCancel}
+                onAccessibilityEscape={onPressCancel}
+                accessibilityRole="button"
+                accessibilityLabel={_(msg`Cancel`)}
+                accessibilityHint={_(
+                  msg`Closes post composer and discards post draft`,
+                )}
+                hitSlop={HITSLOP_10}>
+                <Text style={[pal.link, s.f18]}>
+                  <Trans>Cancel</Trans>
+                </Text>
+              </TouchableOpacity>
+              <View style={a.flex_1} />
+              {isProcessing ? (
+                <>
+                  <Text style={pal.textLight}>{processingState}</Text>
+                  <View style={styles.postBtn}>
+                    <ActivityIndicator />
+                  </View>
+                </>
+              ) : (
+                <>
+                  <LabelsBtn
+                    labels={labels}
+                    onChange={setLabels}
+                    hasMedia={hasMedia}
+                  />
+                  {canPost ? (
+                    <TouchableOpacity
+                      testID="composerPublishBtn"
+                      onPress={onPressPublish}
+                      accessibilityRole="button"
+                      accessibilityLabel={
+                        replyTo ? _(msg`Publish reply`) : _(msg`Publish post`)
+                      }
+                      accessibilityHint="">
+                      <LinearGradient
+                        colors={[
+                          gradients.blueLight.start,
+                          gradients.blueLight.end,
+                        ]}
+                        start={{x: 0, y: 0}}
+                        end={{x: 1, y: 1}}
+                        style={styles.postBtn}>
+                        <Text style={[s.white, s.f16, s.bold]}>
+                          {replyTo ? (
+                            <Trans context="action">Reply</Trans>
+                          ) : (
+                            <Trans context="action">Post</Trans>
+                          )}
+                        </Text>
+                      </LinearGradient>
+                    </TouchableOpacity>
+                  ) : (
+                    <View style={[styles.postBtn, pal.btn]}>
+                      <Text style={[pal.textLight, s.f16, s.bold]}>
+                        <Trans context="action">Post</Trans>
+                      </Text>
+                    </View>
+                  )}
+                </>
               )}
-              hitSlop={HITSLOP_10}>
-              <Text style={[pal.link, s.f18]}>
-                <Trans>Cancel</Trans>
-              </Text>
-            </TouchableOpacity>
-            <View style={s.flex1} />
-            {isProcessing ? (
-              <>
-                <Text style={pal.textLight}>{processingState}</Text>
-                <View style={styles.postBtn}>
-                  <ActivityIndicator />
+            </View>
+
+            {isAltTextRequiredAndMissing && (
+              <View style={[styles.reminderLine, pal.viewLight]}>
+                <View style={styles.errorIcon}>
+                  <FontAwesomeIcon
+                    icon="exclamation"
+                    style={{color: colors.red4}}
+                    size={10}
+                  />
                 </View>
-              </>
-            ) : (
-              <>
-                <LabelsBtn
-                  labels={labels}
-                  onChange={setLabels}
-                  hasMedia={hasMedia}
-                />
-                {canPost ? (
-                  <TouchableOpacity
-                    testID="composerPublishBtn"
-                    onPress={onPressPublish}
-                    accessibilityRole="button"
-                    accessibilityLabel={
-                      replyTo ? _(msg`Publish reply`) : _(msg`Publish post`)
-                    }
-                    accessibilityHint="">
-                    <LinearGradient
-                      colors={[
-                        gradients.blueLight.start,
-                        gradients.blueLight.end,
-                      ]}
-                      start={{x: 0, y: 0}}
-                      end={{x: 1, y: 1}}
-                      style={styles.postBtn}>
-                      <Text style={[s.white, s.f16, s.bold]}>
-                        {replyTo ? (
-                          <Trans context="action">Reply</Trans>
-                        ) : (
-                          <Trans context="action">Post</Trans>
-                        )}
-                      </Text>
-                    </LinearGradient>
-                  </TouchableOpacity>
-                ) : (
-                  <View style={[styles.postBtn, pal.btn]}>
-                    <Text style={[pal.textLight, s.f16, s.bold]}>
-                      <Trans context="action">Post</Trans>
-                    </Text>
-                  </View>
-                )}
-              </>
-            )}
-          </Animated.View>
-          {isAltTextRequiredAndMissing && (
-            <View style={[styles.reminderLine, pal.viewLight]}>
-              <View style={styles.errorIcon}>
-                <FontAwesomeIcon
-                  icon="exclamation"
-                  style={{color: colors.red4}}
-                  size={10}
-                />
+                <Text style={[pal.text, a.flex_1]}>
+                  <Trans>One or more images is missing alt text.</Trans>
+                </Text>
               </View>
-              <Text style={[pal.text, s.flex1]}>
-                <Trans>One or more images is missing alt text.</Trans>
-              </Text>
-            </View>
-          )}
-          {error !== '' && (
-            <View style={styles.errorLine}>
-              <View style={styles.errorIcon}>
-                <FontAwesomeIcon
-                  icon="exclamation"
-                  style={{color: colors.red4}}
-                  size={10}
-                />
+            )}
+            {error !== '' && (
+              <View style={styles.errorLine}>
+                <View style={styles.errorIcon}>
+                  <FontAwesomeIcon
+                    icon="exclamation"
+                    style={{color: colors.red4}}
+                    size={10}
+                  />
+                </View>
+                <Text style={[s.red4, a.flex_1]}>{error}</Text>
               </View>
-              <Text style={[s.red4, s.flex1]}>{error}</Text>
-            </View>
-          )}
+            )}
+          </Animated.View>
           <Animated.ScrollView
             onScroll={scrollHandler}
             style={styles.scrollView}
@@ -576,7 +577,12 @@ export const ComposePost = observer(function ComposePost({
         {replyTo ? null : (
           <ThreadgateBtn threadgate={threadgate} onChange={setThreadgate} />
         )}
-        <View style={[pal.border, styles.bottomBar]}>
+        <View
+          style={[
+            t.atoms.bg,
+            t.atoms.border_contrast_medium,
+            styles.bottomBar,
+          ]}>
           <View style={[a.flex_row, a.align_center, a.gap_xs]}>
             <SelectPhotoBtn gallery={gallery} disabled={!canSelectImages} />
             <OpenCameraBtn gallery={gallery} disabled={!canSelectImages} />
@@ -598,7 +604,7 @@ export const ComposePost = observer(function ComposePost({
               </Button>
             ) : null}
           </View>
-          <View style={s.flex1} />
+          <View style={a.flex_1} />
           <SelectLangBtn />
           <CharProgress count={graphemeLength} />
         </View>
@@ -621,11 +627,6 @@ export function useComposerCancelRef() {
 
 const styles = StyleSheet.create({
   topbar: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    marginHorizontal: 16,
-    height: 54,
-    gap: 4,
     borderBottomWidth: StyleSheet.hairlineWidth,
   },
   topbarDesktop: {
@@ -633,6 +634,13 @@ const styles = StyleSheet.create({
     paddingBottom: 10,
     height: 50,
   },
+  topbarInner: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    paddingHorizontal: 16,
+    height: 54,
+    gap: 4,
+  },
   postBtn: {
     borderRadius: 20,
     paddingHorizontal: 20,
@@ -643,19 +651,19 @@ const styles = StyleSheet.create({
     flexDirection: 'row',
     backgroundColor: colors.red1,
     borderRadius: 6,
-    marginHorizontal: 15,
+    marginHorizontal: 16,
     paddingHorizontal: 8,
     paddingVertical: 6,
-    marginVertical: 6,
+    marginBottom: 8,
   },
   reminderLine: {
     flexDirection: 'row',
     alignItems: 'center',
     borderRadius: 6,
-    marginHorizontal: 15,
+    marginHorizontal: 16,
     paddingHorizontal: 8,
     paddingVertical: 6,
-    marginBottom: 6,
+    marginBottom: 8,
   },
   errorIcon: {
     borderWidth: hairlineWidth,
@@ -690,8 +698,8 @@ const styles = StyleSheet.create({
   bottomBar: {
     flexDirection: 'row',
     paddingVertical: 4,
-    paddingLeft: 15,
-    paddingRight: 20,
+    paddingLeft: 8,
+    paddingRight: 16,
     alignItems: 'center',
     borderTopWidth: hairlineWidth,
   },
diff --git a/src/view/com/composer/ComposerReplyTo.tsx b/src/view/com/composer/ComposerReplyTo.tsx
index 902d60a46..6b38caff0 100644
--- a/src/view/com/composer/ComposerReplyTo.tsx
+++ b/src/view/com/composer/ComposerReplyTo.tsx
@@ -10,7 +10,6 @@ import {
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
-import {isWeb} from '#/platform/detection'
 import {sanitizeDisplayName} from 'lib/strings/display-names'
 import {sanitizeHandle} from 'lib/strings/handles'
 import {ComposerOptsPostRef} from 'state/shell/composer'
@@ -76,10 +75,7 @@ export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
 
   return (
     <Pressable
-      style={[
-        isWeb ? t.atoms.border_contrast_low : t.atoms.border_contrast_high,
-        styles.replyToLayout,
-      ]}
+      style={[t.atoms.border_contrast_medium, styles.replyToLayout]}
       onPress={onPress}
       accessibilityRole="button"
       accessibilityLabel={_(
diff --git a/src/view/com/composer/threadgate/ThreadgateBtn.tsx b/src/view/com/composer/threadgate/ThreadgateBtn.tsx
index df2a31e2b..afc9f5bfa 100644
--- a/src/view/com/composer/threadgate/ThreadgateBtn.tsx
+++ b/src/view/com/composer/threadgate/ThreadgateBtn.tsx
@@ -7,7 +7,7 @@ import {isNative} from '#/platform/detection'
 import {useModalControls} from '#/state/modals'
 import {ThreadgateSetting} from '#/state/queries/threadgate'
 import {useAnalytics} from 'lib/analytics/analytics'
-import {atoms as a} from '#/alf'
+import {atoms as a, useTheme} from '#/alf'
 import {Button, ButtonIcon, ButtonText} from '#/components/Button'
 import {CircleBanSign_Stroke2_Corner0_Rounded as CircleBanSign} from '#/components/icons/CircleBanSign'
 import {Earth_Stroke2_Corner0_Rounded as Earth} from '#/components/icons/Globe'
@@ -22,6 +22,7 @@ export function ThreadgateBtn({
 }) {
   const {track} = useAnalytics()
   const {_} = useLingui()
+  const t = useTheme()
   const {openModal} = useModalControls()
 
   const onPress = () => {
@@ -45,7 +46,7 @@ export function ThreadgateBtn({
     : _(msg`Some people can reply`)
 
   return (
-    <View style={[a.flex_row, a.pb_sm, a.px_md]}>
+    <View style={[a.flex_row, a.py_xs, a.px_sm, t.atoms.bg]}>
       <Button
         variant="solid"
         color="secondary"
diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx
index 3491b94e3..eb9666405 100644
--- a/src/view/com/modals/Modal.tsx
+++ b/src/view/com/modals/Modal.tsx
@@ -1,10 +1,11 @@
-import React, {useEffect, useRef} from 'react'
+import React, {Fragment, useEffect, useRef} from 'react'
 import {StyleSheet} from 'react-native'
 import {SafeAreaView} from 'react-native-safe-area-context'
 import BottomSheet from '@discord/bottom-sheet/src'
 
 import {useModalControls, useModals} from '#/state/modals'
 import {usePalette} from 'lib/hooks/usePalette'
+import {FullWindowOverlay} from '#/components/FullWindowOverlay'
 import {KeyboardPadding} from '#/components/KeyboardPadding'
 import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop'
 import * as AddAppPassword from './AddAppPasswords'
@@ -127,24 +128,28 @@ export function ModalsContainer() {
     )
   }
 
+  const Container = activeModal ? FullWindowOverlay : Fragment
+
   return (
-    <BottomSheet
-      ref={bottomSheetRef}
-      snapPoints={snapPoints}
-      handleHeight={HANDLE_HEIGHT}
-      index={isModalActive ? 0 : -1}
-      enablePanDownToClose
-      android_keyboardInputMode="adjustResize"
-      keyboardBlurBehavior="restore"
-      backdropComponent={
-        isModalActive ? createCustomBackdrop(onClose) : undefined
-      }
-      handleIndicatorStyle={{backgroundColor: pal.text.color}}
-      handleStyle={[styles.handle, pal.view]}
-      onChange={onBottomSheetChange}>
-      {element}
-      <KeyboardPadding />
-    </BottomSheet>
+    <Container>
+      <BottomSheet
+        ref={bottomSheetRef}
+        snapPoints={snapPoints}
+        handleHeight={HANDLE_HEIGHT}
+        index={isModalActive ? 0 : -1}
+        enablePanDownToClose
+        android_keyboardInputMode="adjustResize"
+        keyboardBlurBehavior="restore"
+        backdropComponent={
+          isModalActive ? createCustomBackdrop(onClose) : undefined
+        }
+        handleIndicatorStyle={{backgroundColor: pal.text.color}}
+        handleStyle={[styles.handle, pal.view]}
+        onChange={onBottomSheetChange}>
+        {element}
+        <KeyboardPadding />
+      </BottomSheet>
+    </Container>
   )
 }
 
diff --git a/src/view/com/modals/Threadgate.tsx b/src/view/com/modals/Threadgate.tsx
index 0e49fc2f3..a2e9f391c 100644
--- a/src/view/com/modals/Threadgate.tsx
+++ b/src/view/com/modals/Threadgate.tsx
@@ -7,18 +7,19 @@ import {
   View,
   ViewStyle,
 } from 'react-native'
-import {Text} from '../util/text/Text'
-import {s, colors} from 'lib/styles'
-import {usePalette} from 'lib/hooks/usePalette'
-import {isWeb} from 'platform/detection'
-import {ScrollView} from 'view/com/modals/util'
-import {Trans, msg} from '@lingui/macro'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
+import isEqual from 'lodash.isequal'
+
 import {useModalControls} from '#/state/modals'
-import {ThreadgateSetting} from '#/state/queries/threadgate'
 import {useMyListsQuery} from '#/state/queries/my-lists'
-import isEqual from 'lodash.isequal'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {ThreadgateSetting} from '#/state/queries/threadgate'
+import {usePalette} from 'lib/hooks/usePalette'
+import {colors, s} from 'lib/styles'
+import {isWeb} from 'platform/detection'
+import {ScrollView} from 'view/com/modals/util'
+import {Text} from '../util/text/Text'
 
 export const snapPoints = ['60%']
 
@@ -155,7 +156,7 @@ function Selectable({
       accessibilityLabel={label}
       accessibilityHint=""
       style={[styles.selectable, pal.border, pal.view, style]}>
-      <Text type="xl" style={[pal.text]}>
+      <Text type="lg" style={[pal.text]}>
         {label}
       </Text>
       {isSelected ? (
diff --git a/src/view/com/util/BottomSheetCustomBackdrop.tsx b/src/view/com/util/BottomSheetCustomBackdrop.tsx
index 0d15c5e55..25e882e87 100644
--- a/src/view/com/util/BottomSheetCustomBackdrop.tsx
+++ b/src/view/com/util/BottomSheetCustomBackdrop.tsx
@@ -1,7 +1,7 @@
 import React, {useMemo} from 'react'
 import {TouchableWithoutFeedback} from 'react-native'
 import Animated, {
-  Extrapolate,
+  Extrapolation,
   interpolate,
   useAnimatedStyle,
 } from 'react-native-reanimated'
@@ -21,7 +21,7 @@ export function createCustomBackdrop(
         animatedIndex.value, // current snap index
         [-1, 0], // input range
         [0, 0.5], // output range
-        Extrapolate.CLAMP,
+        Extrapolation.CLAMP,
       ),
     }))
 
diff --git a/src/view/com/util/forms/DropdownButton.tsx b/src/view/com/util/forms/DropdownButton.tsx
index 14b97161d..bfbafcad9 100644
--- a/src/view/com/util/forms/DropdownButton.tsx
+++ b/src/view/com/util/forms/DropdownButton.tsx
@@ -2,7 +2,6 @@ import React, {PropsWithChildren, useMemo, useRef} from 'react'
 import {
   Dimensions,
   GestureResponderEvent,
-  Platform,
   StyleProp,
   StyleSheet,
   TouchableOpacity,
@@ -11,8 +10,8 @@ import {
   View,
   ViewStyle,
 } from 'react-native'
+import Animated, {FadeIn, FadeInDown, FadeInUp} from 'react-native-reanimated'
 import RootSiblings from 'react-native-root-siblings'
-import {FullWindowOverlay} from 'react-native-screens'
 import {IconProp} from '@fortawesome/fontawesome-svg-core'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {msg} from '@lingui/macro'
@@ -23,6 +22,8 @@ import {usePalette} from 'lib/hooks/usePalette'
 import {colors} from 'lib/styles'
 import {useTheme} from 'lib/ThemeContext'
 import {isWeb} from 'platform/detection'
+import {native} from '#/alf'
+import {FullWindowOverlay} from '#/components/FullWindowOverlay'
 import {Text} from '../text/Text'
 import {Button, ButtonType} from './Button'
 
@@ -127,6 +128,7 @@ export function DropdownButton({
           pageY,
           menuWidth,
           items.filter(v => !!v) as DropdownItem[],
+          openUpwards,
         )
       },
     )
@@ -181,6 +183,7 @@ function createDropdownMenu(
   pageY: number,
   width: number,
   items: DropdownItem[],
+  opensUpwards = false,
 ): RootSiblings {
   const onPressItem = (index: number) => {
     sibling.destroy()
@@ -200,6 +203,7 @@ function createDropdownMenu(
         width={width}
         items={items}
         onPressItem={onPressItem}
+        openUpwards={opensUpwards}
       />
     ),
   )
@@ -214,6 +218,7 @@ type DropDownItemProps = {
   width: number
   items: DropdownItem[]
   onPressItem: (index: number) => void
+  openUpwards: boolean
 }
 
 const DropdownItems = ({
@@ -224,6 +229,7 @@ const DropdownItems = ({
   width,
   items,
   onPressItem,
+  openUpwards,
 }: DropDownItemProps) => {
   const pal = usePalette('default')
   const theme = useTheme()
@@ -242,13 +248,14 @@ const DropdownItems = ({
   // - (On mobile) be buttons by default, accept `label` and `nativeID`
   // props, and always have an explicit label
   return (
-    <Wrapper>
+    <FullWindowOverlay>
       {/* This TouchableWithoutFeedback renders the background so if the user clicks outside, the dropdown closes */}
       <TouchableWithoutFeedback
         onPress={onOuterPress}
         accessibilityLabel={_(msg`Toggle dropdown`)}
         accessibilityHint="">
-        <View
+        <Animated.View
+          entering={FadeIn}
           style={[
             styles.bg,
             // On web we need to adjust the top and bottom relative to the scroll position
@@ -264,7 +271,10 @@ const DropdownItems = ({
           ]}
         />
       </TouchableWithoutFeedback>
-      <View
+      <Animated.View
+        entering={native(
+          openUpwards ? FadeInDown.springify(1000) : FadeInUp.springify(1000),
+        )}
         style={[
           styles.menu,
           {left: x, top: y, width},
@@ -306,18 +316,11 @@ const DropdownItems = ({
           }
           return null
         })}
-      </View>
-    </Wrapper>
+      </Animated.View>
+    </FullWindowOverlay>
   )
 }
 
-// on iOS, due to formSheet presentation style, we need to render the overlay
-// as a full screen overlay
-const Wrapper = Platform.select({
-  ios: FullWindowOverlay,
-  default: ({children}) => <>{children}</>,
-})
-
 function isSep(item: DropdownItem): item is DropdownItemSeparator {
   return 'sep' in item && item.sep
 }
@@ -333,14 +336,12 @@ const styles = StyleSheet.create({
     position: 'absolute',
     left: 0,
     width: '100%',
-    backgroundColor: '#000',
-    opacity: 0.1,
+    backgroundColor: 'rgba(0, 0, 0, 0.1)',
   },
   menu: {
     position: 'absolute',
     backgroundColor: '#fff',
     borderRadius: 14,
-    opacity: 1,
     paddingVertical: 6,
   },
   menuItem: {
diff --git a/src/view/shell/Composer.tsx b/src/view/shell/Composer.tsx
index ce53ffc01..c80d7845b 100644
--- a/src/view/shell/Composer.tsx
+++ b/src/view/shell/Composer.tsx
@@ -1,5 +1,7 @@
 import React, {useLayoutEffect} from 'react'
 import {Modal, View} from 'react-native'
+import {GestureHandlerRootView} from 'react-native-gesture-handler'
+import {RootSiblingParent} from 'react-native-root-siblings'
 import {StatusBar} from 'expo-status-bar'
 import * as SystemUI from 'expo-system-ui'
 import {observer} from 'mobx-react-lite'
@@ -34,27 +36,56 @@ export const Composer = observer(function ComposerImpl({}: {
       animationType="slide"
       onRequestClose={() => ref.current?.onPressCancel()}>
       <View style={[t.atoms.bg, a.flex_1]}>
-        <LegacyModalProvider>
-          <PortalProvider>
-            <ComposePost
-              cancelRef={ref}
-              replyTo={state?.replyTo}
-              onPost={state?.onPost}
-              quote={state?.quote}
-              mention={state?.mention}
-              text={state?.text}
-              imageUris={state?.imageUris}
-            />
-            <LegacyModalsContainer />
-            <PortalOutlet />
-          </PortalProvider>
-        </LegacyModalProvider>
-        {isIOS && <IOSModalBackground active={open} />}
+        <Providers open={open}>
+          <ComposePost
+            cancelRef={ref}
+            replyTo={state?.replyTo}
+            onPost={state?.onPost}
+            quote={state?.quote}
+            mention={state?.mention}
+            text={state?.text}
+            imageUris={state?.imageUris}
+          />
+        </Providers>
       </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
+  if (isIOS) {
+    return (
+      <>
+        {children}
+        <IOSModalBackground active={open} />
+      </>
+    )
+  } else {
+    // on Android we just nest the dialogs within it
+    return (
+      <GestureHandlerRootView style={a.flex_1}>
+        <RootSiblingParent>
+          <LegacyModalProvider>
+            <PortalProvider>
+              {children}
+              <LegacyModalsContainer />
+              <PortalOutlet />
+            </PortalProvider>
+          </LegacyModalProvider>
+        </RootSiblingParent>
+      </GestureHandlerRootView>
+    )
+  }
+}
+
 // 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}) {