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
parent
14cddb7ec0
commit
d85c8a0976
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
|
@ -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}
|
||||||
|
|
Loading…
Reference in New Issue