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<{
|
||||
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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 {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%',
|
||||
},
|
||||
})
|
||||
|
|
|
@ -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}
|
||||
|
|
Loading…
Reference in New Issue