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>
zio/stable
Samuel Newman 2024-06-11 04:10:57 +01:00 committed by GitHub
parent 14cddb7ec0
commit d85c8a0976
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 173 additions and 147 deletions

View File

@ -169,11 +169,11 @@ const ModalContext = React.createContext<{
const ModalControlContext = React.createContext<{ const ModalControlContext = React.createContext<{
openModal: (modal: Modal) => void openModal: (modal: Modal) => void
closeModal: () => boolean closeModal: () => boolean
closeAllModals: () => void closeAllModals: () => boolean
}>({ }>({
openModal: () => {}, openModal: () => {},
closeModal: () => false, closeModal: () => false,
closeAllModals: () => {}, closeAllModals: () => false,
}) })
/** /**
@ -206,7 +206,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
}) })
const closeAllModals = useNonReactiveCallback(() => { const closeAllModals = useNonReactiveCallback(() => {
let wasActive = activeModals.length > 0
setActiveModals([]) setActiveModals([])
return wasActive
}) })
unstable__openModal = openModal unstable__openModal = openModal

View File

@ -1,9 +1,10 @@
import {useCallback} from 'react' import {useCallback} from 'react'
import {useDialogStateControlContext} from '#/state/dialogs'
import {useLightboxControls} from './lightbox' import {useLightboxControls} from './lightbox'
import {useModalControls} from './modals' import {useModalControls} from './modals'
import {useComposerControls} from './shell/composer' import {useComposerControls} from './shell/composer'
import {useSetDrawerOpen} from './shell/drawer-open' import {useSetDrawerOpen} from './shell/drawer-open'
import {useDialogStateControlContext} from '#/state/dialogs'
/** /**
* returns true if something was closed * returns true if something was closed
@ -22,10 +23,10 @@ export function useCloseAnyActiveElement() {
if (closeModal()) { if (closeModal()) {
return true return true
} }
if (closeComposer()) { if (closeAllDialogs()) {
return true return true
} }
if (closeAllDialogs()) { if (closeComposer()) {
return true return true
} }
setDrawerOpen(false) setDrawerOpen(false)

View File

@ -8,6 +8,7 @@ import React, {
} from 'react' } from 'react'
import { import {
ActivityIndicator, ActivityIndicator,
BackHandler,
Keyboard, Keyboard,
LayoutChangeEvent, LayoutChangeEvent,
StyleSheet, StyleSheet,
@ -17,7 +18,7 @@ import {
import { import {
KeyboardAvoidingView, KeyboardAvoidingView,
KeyboardStickyView, KeyboardStickyView,
useKeyboardContext, useKeyboardController,
} from 'react-native-keyboard-controller' } from 'react-native-keyboard-controller'
import Animated, { import Animated, {
interpolateColor, interpolateColor,
@ -42,6 +43,7 @@ import {LikelyType} from '#/lib/link-meta/link-meta'
import {logEvent} from '#/lib/statsig/statsig' import {logEvent} from '#/lib/statsig/statsig'
import {logger} from '#/logger' import {logger} from '#/logger'
import {emitPostCreated} from '#/state/events' import {emitPostCreated} from '#/state/events'
import {useModalControls} from '#/state/modals'
import {useModals} from '#/state/modals' import {useModals} from '#/state/modals'
import {useRequireAltTextEnabled} from '#/state/preferences' import {useRequireAltTextEnabled} from '#/state/preferences'
import { import {
@ -108,9 +110,7 @@ export const ComposePost = observer(function ComposePost({
text: initText, text: initText,
imageUris: initImageUris, imageUris: initImageUris,
cancelRef, cancelRef,
isModalReady,
}: Props & { }: Props & {
isModalReady: boolean
cancelRef?: React.RefObject<CancelRef> cancelRef?: React.RefObject<CancelRef>
}) { }) {
const {currentAccount} = useSession() const {currentAccount} = useSession()
@ -128,11 +128,12 @@ export const ComposePost = observer(function ComposePost({
const textInput = useRef<TextInputRef>(null) const textInput = useRef<TextInputRef>(null)
const discardPromptControl = Prompt.usePromptControl() const discardPromptControl = Prompt.usePromptControl()
const {closeAllDialogs} = useDialogStateControlContext() const {closeAllDialogs} = useDialogStateControlContext()
const {closeAllModals} = useModalControls()
const t = useTheme() const t = useTheme()
// Disable this in the composer to prevent any extra keyboard height being applied. // Disable this in the composer to prevent any extra keyboard height being applied.
// See https://github.com/bluesky-social/social-app/pull/4399 // See https://github.com/bluesky-social/social-app/pull/4399
const {setEnabled} = useKeyboardContext() const {setEnabled} = useKeyboardController()
React.useEffect(() => { React.useEffect(() => {
if (!isAndroid) return if (!isAndroid) return
setEnabled(false) setEnabled(false)
@ -180,6 +181,7 @@ export const ComposePost = observer(function ComposePost({
const insets = useSafeAreaInsets() const insets = useSafeAreaInsets()
const viewStyles = useMemo( const viewStyles = useMemo(
() => ({ () => ({
paddingTop: isAndroid ? insets.top : 0,
paddingBottom: paddingBottom:
isAndroid || (isIOS && !isKeyboardVisible) ? insets.bottom : 0, isAndroid || (isIOS && !isKeyboardVisible) ? insets.bottom : 0,
}), }),
@ -205,6 +207,26 @@ export const ComposePost = observer(function ComposePost({
useImperativeHandle(cancelRef, () => ({onPressCancel})) 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 // listen to escape key on desktop web
const onEscape = useCallback( const onEscape = useCallback(
(e: KeyboardEvent) => { (e: KeyboardEvent) => {
@ -408,37 +430,6 @@ export const ComposePost = observer(function ComposePost({
bottomBarAnimatedStyle, bottomBarAnimatedStyle,
} = useAnimatedBorders() } = 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 ( return (
<> <>
<KeyboardAvoidingView <KeyboardAvoidingView
@ -567,11 +558,7 @@ export const ComposePost = observer(function ComposePost({
ref={textInput} ref={textInput}
richtext={richtext} richtext={richtext}
placeholder={selectTextInputPlaceholder} placeholder={selectTextInputPlaceholder}
// fixes autofocus on android autoFocus
key={
isAndroid ? (isModalReady ? 'ready' : 'animating') : 'static'
}
autoFocus={isAndroid ? isModalReady : true}
setRichText={setRichText} setRichText={setRichText}
onPhotoPasted={onPhotoPasted} onPhotoPasted={onPhotoPasted}
onPressPublish={onPressPublish} onPressPublish={onPressPublish}

View File

@ -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
}

View File

@ -1,116 +1,73 @@
import React, {useLayoutEffect, useState} from 'react' import React, {useEffect} from 'react'
import {Modal, View} from 'react-native' import {Animated, Easing, StyleSheet, 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' import {observer} from 'mobx-react-lite'
import {isIOS} from '#/platform/detection' import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
import {Provider as LegacyModalProvider} from '#/state/modals' import {usePalette} from 'lib/hooks/usePalette'
import {useComposerState} from '#/state/shell/composer' import {useComposerState} from 'state/shell/composer'
import {ModalsContainer as LegacyModalsContainer} from '#/view/com/modals/Modal' import {ComposePost} from '../com/composer/Composer'
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'
export const Composer = observer(function ComposerImpl({}: { export const Composer = observer(function ComposerImpl({
winHeight,
}: {
winHeight: number winHeight: number
}) { }) {
const t = useTheme()
const state = useComposerState() const state = useComposerState()
const ref = useComposerCancelRef() const pal = usePalette('default')
const [isModalReady, setIsModalReady] = useState(false) const initInterp = useAnimatedValue(0)
const open = !!state useEffect(() => {
const [prevOpen, setPrevOpen] = useState(open) if (state) {
if (open !== prevOpen) { Animated.timing(initInterp, {
setPrevOpen(open) toValue: 1,
if (!open) { duration: 300,
setIsModalReady(false) 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 ( return (
<Modal <Animated.View
style={[styles.wrapper, pal.view, wrapperAnimStyle]}
aria-modal aria-modal
accessibilityViewIsModal 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 <ComposePost
isModalReady={isModalReady} replyTo={state.replyTo}
cancelRef={ref} onPost={state.onPost}
replyTo={state?.replyTo} quote={state.quote}
onPost={state?.onPost} mention={state.mention}
quote={state?.quote} text={state.text}
mention={state?.mention} imageUris={state.imageUris}
text={state?.text}
imageUris={state?.imageUris}
/> />
</Providers> </Animated.View>
</View>
</Modal>
) )
}) })
function Providers({ const styles = StyleSheet.create({
children, wrapper: {
open, position: 'absolute',
}: { top: 0,
children: React.ReactNode bottom: 0,
open: boolean width: '100%',
}) { },
// 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
}

View File

@ -56,7 +56,6 @@ export function Composer({}: {winHeight: number}) {
t.atoms.border_contrast_medium, t.atoms.border_contrast_medium,
]}> ]}>
<ComposePost <ComposePost
isModalReady={true}
replyTo={state.replyTo} replyTo={state.replyTo}
quote={state.quote} quote={state.quote}
onPost={state.onPost} onPost={state.onPost}