about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorSamuel Newman <mozzius@protonmail.com>2024-06-11 04:10:57 +0100
committerGitHub <noreply@github.com>2024-06-11 05:10:57 +0200
commitd85c8a09760aa91c080465840144b61155dc7ddf (patch)
tree2ccd90963836c98acc276c3399bb7e8bc9859ee2 /src
parent14cddb7ec0a6088b5f2f89f61fc2a32656756f7a (diff)
downloadvoidsky-d85c8a09760aa91c080465840144b61155dc7ddf.tar.zst
Revert to old modal on android (#4458)
* revert to old modal on android

* close alf dialogs before closing composer

* Try to fix white area

* Use hook

* Fix Back button

* oops

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
Diffstat (limited to 'src')
-rw-r--r--src/state/modals/index.tsx6
-rw-r--r--src/state/util.ts7
-rw-r--r--src/view/com/composer/Composer.tsx67
-rw-r--r--src/view/shell/Composer.ios.tsx80
-rw-r--r--src/view/shell/Composer.tsx159
-rw-r--r--src/view/shell/Composer.web.tsx1
6 files changed, 173 insertions, 147 deletions
diff --git a/src/state/modals/index.tsx b/src/state/modals/index.tsx
index f8a64dc2d..ced14335b 100644
--- a/src/state/modals/index.tsx
+++ b/src/state/modals/index.tsx
@@ -169,11 +169,11 @@ const ModalContext = React.createContext<{
 const ModalControlContext = React.createContext<{
   openModal: (modal: Modal) => void
   closeModal: () => boolean
-  closeAllModals: () => void
+  closeAllModals: () => boolean
 }>({
   openModal: () => {},
   closeModal: () => false,
-  closeAllModals: () => {},
+  closeAllModals: () => false,
 })
 
 /**
@@ -206,7 +206,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
   })
 
   const closeAllModals = useNonReactiveCallback(() => {
+    let wasActive = activeModals.length > 0
     setActiveModals([])
+    return wasActive
   })
 
   unstable__openModal = openModal
diff --git a/src/state/util.ts b/src/state/util.ts
index f65d14a84..cdd8214a9 100644
--- a/src/state/util.ts
+++ b/src/state/util.ts
@@ -1,9 +1,10 @@
 import {useCallback} from 'react'
+
+import {useDialogStateControlContext} from '#/state/dialogs'
 import {useLightboxControls} from './lightbox'
 import {useModalControls} from './modals'
 import {useComposerControls} from './shell/composer'
 import {useSetDrawerOpen} from './shell/drawer-open'
-import {useDialogStateControlContext} from '#/state/dialogs'
 
 /**
  * returns true if something was closed
@@ -22,10 +23,10 @@ export function useCloseAnyActiveElement() {
     if (closeModal()) {
       return true
     }
-    if (closeComposer()) {
+    if (closeAllDialogs()) {
       return true
     }
-    if (closeAllDialogs()) {
+    if (closeComposer()) {
       return true
     }
     setDrawerOpen(false)
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx
index 5bcac2e67..e8ea5189f 100644
--- a/src/view/com/composer/Composer.tsx
+++ b/src/view/com/composer/Composer.tsx
@@ -8,6 +8,7 @@ import React, {
 } from 'react'
 import {
   ActivityIndicator,
+  BackHandler,
   Keyboard,
   LayoutChangeEvent,
   StyleSheet,
@@ -17,7 +18,7 @@ import {
 import {
   KeyboardAvoidingView,
   KeyboardStickyView,
-  useKeyboardContext,
+  useKeyboardController,
 } from 'react-native-keyboard-controller'
 import Animated, {
   interpolateColor,
@@ -42,6 +43,7 @@ import {LikelyType} from '#/lib/link-meta/link-meta'
 import {logEvent} from '#/lib/statsig/statsig'
 import {logger} from '#/logger'
 import {emitPostCreated} from '#/state/events'
+import {useModalControls} from '#/state/modals'
 import {useModals} from '#/state/modals'
 import {useRequireAltTextEnabled} from '#/state/preferences'
 import {
@@ -108,9 +110,7 @@ export const ComposePost = observer(function ComposePost({
   text: initText,
   imageUris: initImageUris,
   cancelRef,
-  isModalReady,
 }: Props & {
-  isModalReady: boolean
   cancelRef?: React.RefObject<CancelRef>
 }) {
   const {currentAccount} = useSession()
@@ -128,11 +128,12 @@ export const ComposePost = observer(function ComposePost({
   const textInput = useRef<TextInputRef>(null)
   const discardPromptControl = Prompt.usePromptControl()
   const {closeAllDialogs} = useDialogStateControlContext()
+  const {closeAllModals} = useModalControls()
   const t = useTheme()
 
   // Disable this in the composer to prevent any extra keyboard height being applied.
   // See https://github.com/bluesky-social/social-app/pull/4399
-  const {setEnabled} = useKeyboardContext()
+  const {setEnabled} = useKeyboardController()
   React.useEffect(() => {
     if (!isAndroid) return
     setEnabled(false)
@@ -180,6 +181,7 @@ export const ComposePost = observer(function ComposePost({
   const insets = useSafeAreaInsets()
   const viewStyles = useMemo(
     () => ({
+      paddingTop: isAndroid ? insets.top : 0,
       paddingBottom:
         isAndroid || (isIOS && !isKeyboardVisible) ? insets.bottom : 0,
     }),
@@ -205,6 +207,26 @@ export const ComposePost = observer(function ComposePost({
 
   useImperativeHandle(cancelRef, () => ({onPressCancel}))
 
+  // On Android, pressing Back should ask confirmation.
+  useEffect(() => {
+    if (!isAndroid) {
+      return
+    }
+    const backHandler = BackHandler.addEventListener(
+      'hardwareBackPress',
+      () => {
+        if (closeAllDialogs() || closeAllModals()) {
+          return true
+        }
+        onPressCancel()
+        return true
+      },
+    )
+    return () => {
+      backHandler.remove()
+    }
+  }, [onPressCancel, closeAllDialogs, closeAllModals])
+
   // listen to escape key on desktop web
   const onEscape = useCallback(
     (e: KeyboardEvent) => {
@@ -408,37 +430,6 @@ export const ComposePost = observer(function ComposePost({
     bottomBarAnimatedStyle,
   } = useAnimatedBorders()
 
-  // Backup focus on android, if the keyboard *still* refuses to show
-  useEffect(() => {
-    if (!isAndroid) return
-    if (!isModalReady) return
-
-    function tryFocus() {
-      if (!Keyboard.isVisible()) {
-        textInput.current?.blur()
-        textInput.current?.focus()
-      }
-    }
-
-    tryFocus()
-    // Retry with enough gap to avoid interrupting the previous attempt.
-    // Unfortunately we don't know which attempt will succeed.
-    const retryInterval = setInterval(tryFocus, 500)
-
-    function stopTrying() {
-      clearInterval(retryInterval)
-    }
-
-    // Deactivate this fallback as soon as anything happens.
-    const sub1 = Keyboard.addListener('keyboardDidShow', stopTrying)
-    const sub2 = Keyboard.addListener('keyboardDidHide', stopTrying)
-    return () => {
-      clearInterval(retryInterval)
-      sub1.remove()
-      sub2.remove()
-    }
-  }, [isModalReady])
-
   return (
     <>
       <KeyboardAvoidingView
@@ -567,11 +558,7 @@ export const ComposePost = observer(function ComposePost({
                 ref={textInput}
                 richtext={richtext}
                 placeholder={selectTextInputPlaceholder}
-                // fixes autofocus on android
-                key={
-                  isAndroid ? (isModalReady ? 'ready' : 'animating') : 'static'
-                }
-                autoFocus={isAndroid ? isModalReady : true}
+                autoFocus
                 setRichText={setRichText}
                 onPhotoPasted={onPhotoPasted}
                 onPressPublish={onPressPublish}
diff --git a/src/view/shell/Composer.ios.tsx b/src/view/shell/Composer.ios.tsx
new file mode 100644
index 000000000..a732e0cde
--- /dev/null
+++ b/src/view/shell/Composer.ios.tsx
@@ -0,0 +1,80 @@
+import React, {useLayoutEffect} from 'react'
+import {Modal, View} from 'react-native'
+import {StatusBar} from 'expo-status-bar'
+import * as SystemUI from 'expo-system-ui'
+import {observer} from 'mobx-react-lite'
+
+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 const Composer = observer(function ComposerImpl({}: {
+  winHeight: number
+}) {
+  const t = useTheme()
+  const state = useComposerState()
+  const ref = useComposerCancelRef()
+
+  const open = !!state
+
+  return (
+    <Modal
+      aria-modal
+      accessibilityViewIsModal
+      visible={open}
+      presentationStyle="pageSheet"
+      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}
+            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
+  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/Composer.tsx b/src/view/shell/Composer.tsx
index 1d656ca8f..b978d6b85 100644
--- a/src/view/shell/Composer.tsx
+++ b/src/view/shell/Composer.tsx
@@ -1,116 +1,73 @@
-import React, {useLayoutEffect, useState} 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 React, {useEffect} from 'react'
+import {Animated, Easing, StyleSheet, View} from 'react-native'
 import {observer} from 'mobx-react-lite'
 
-import {isIOS} from '#/platform/detection'
-import {Provider as LegacyModalProvider} from '#/state/modals'
-import {useComposerState} from '#/state/shell/composer'
-import {ModalsContainer as LegacyModalsContainer} from '#/view/com/modals/Modal'
-import {atoms as a, useTheme} from '#/alf'
-import {getBackgroundColor, useThemeName} from '#/alf/util/useColorModeTheme'
-import {
-  Outlet as PortalOutlet,
-  Provider as PortalProvider,
-} from '#/components/Portal'
-import {ComposePost, useComposerCancelRef} from '../com/composer/Composer'
+import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
+import {usePalette} from 'lib/hooks/usePalette'
+import {useComposerState} from 'state/shell/composer'
+import {ComposePost} from '../com/composer/Composer'
 
-export const Composer = observer(function ComposerImpl({}: {
+export const Composer = observer(function ComposerImpl({
+  winHeight,
+}: {
   winHeight: number
 }) {
-  const t = useTheme()
   const state = useComposerState()
-  const ref = useComposerCancelRef()
-  const [isModalReady, setIsModalReady] = useState(false)
+  const pal = usePalette('default')
+  const initInterp = useAnimatedValue(0)
 
-  const open = !!state
-  const [prevOpen, setPrevOpen] = useState(open)
-  if (open !== prevOpen) {
-    setPrevOpen(open)
-    if (!open) {
-      setIsModalReady(false)
+  useEffect(() => {
+    if (state) {
+      Animated.timing(initInterp, {
+        toValue: 1,
+        duration: 300,
+        easing: Easing.out(Easing.exp),
+        useNativeDriver: true,
+      }).start()
+    } else {
+      initInterp.setValue(0)
     }
+  }, [initInterp, state])
+  const wrapperAnimStyle = {
+    transform: [
+      {
+        translateY: initInterp.interpolate({
+          inputRange: [0, 1],
+          outputRange: [winHeight, 0],
+        }),
+      },
+    ],
+  }
+
+  // rendering
+  // =
+
+  if (!state) {
+    return <View />
   }
 
   return (
-    <Modal
+    <Animated.View
+      style={[styles.wrapper, pal.view, wrapperAnimStyle]}
       aria-modal
-      accessibilityViewIsModal
-      visible={open}
-      presentationStyle="formSheet"
-      animationType="slide"
-      onShow={() => setIsModalReady(true)}
-      onRequestClose={() => ref.current?.onPressCancel()}>
-      <View style={[t.atoms.bg, a.flex_1]}>
-        <Providers open={open}>
-          <ComposePost
-            isModalReady={isModalReady}
-            cancelRef={ref}
-            replyTo={state?.replyTo}
-            onPost={state?.onPost}
-            quote={state?.quote}
-            mention={state?.mention}
-            text={state?.text}
-            imageUris={state?.imageUris}
-          />
-        </Providers>
-      </View>
-    </Modal>
+      accessibilityViewIsModal>
+      <ComposePost
+        replyTo={state.replyTo}
+        onPost={state.onPost}
+        quote={state.quote}
+        mention={state.mention}
+        text={state.text}
+        imageUris={state.imageUris}
+      />
+    </Animated.View>
   )
 })
 
-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}) {
-  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
-}
+const styles = StyleSheet.create({
+  wrapper: {
+    position: 'absolute',
+    top: 0,
+    bottom: 0,
+    width: '100%',
+  },
+})
diff --git a/src/view/shell/Composer.web.tsx b/src/view/shell/Composer.web.tsx
index 47322d4ea..64353db23 100644
--- a/src/view/shell/Composer.web.tsx
+++ b/src/view/shell/Composer.web.tsx
@@ -56,7 +56,6 @@ export function Composer({}: {winHeight: number}) {
           t.atoms.border_contrast_medium,
         ]}>
         <ComposePost
-          isModalReady={true}
           replyTo={state.replyTo}
           quote={state.quote}
           onPost={state.onPost}