diff --git a/src/state/modals/index.tsx b/src/state/modals/index.tsx index f8a64dc2..ced14335 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 f65d14a8..cdd8214a 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 5bcac2e6..e8ea5189 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 }) { const {currentAccount} = useSession() @@ -128,11 +128,12 @@ export const ComposePost = observer(function ComposePost({ const textInput = useRef(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 ( <> ref.current?.onPressCancel()}> + + + + + + + ) +}) + +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} + + + ) +} + +// 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 ? : null +} diff --git a/src/view/shell/Composer.tsx b/src/view/shell/Composer.tsx index 1d656ca8..b978d6b8 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 } return ( - setIsModalReady(true)} - onRequestClose={() => ref.current?.onPressCancel()}> - - - - - - + accessibilityViewIsModal> + + ) }) -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} - - - ) - } else { - // on Android we just nest the dialogs within it - return ( - - - - - {children} - - - - - - - ) - } -} - -// 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 ? : 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 47322d4e..64353db2 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, ]}>