Use `<Modal>` for Composer (#3588)
* use <Modal> to display composer * trigger `onPressCancel` on modal cancel * remove android top padding * use light statusbar on ios * use KeyboardStickyView from r-n-keyboard-controller * make extra bottom padding ios-only * make cancelRef optional * scope legacy modals * don't change bg color on ios * use fullScreen instead of formSheet * adjust padding on keyboardaccessory to account for new buttons * Revert "use KeyboardStickyView from r-n-keyboard-controller" This reverts commit 426c812904f427bdd08107cffc32e4be1d9b83bc. * fix insets * tweaks and merge * revert 89f51c72 * nit * import keyboard provider --------- Co-authored-by: Hailey <me@haileyok.com> Co-authored-by: Dan Abramov <dan.abramov@gmail.com>zio/stable
parent
fba4a9ca02
commit
c4abaa1abc
|
@ -1,11 +1,11 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {ColorSchemeName, useColorScheme} from 'react-native'
|
import {ColorSchemeName, useColorScheme} from 'react-native'
|
||||||
|
|
||||||
import {useThemePrefs} from 'state/shell'
|
|
||||||
import {isWeb} from 'platform/detection'
|
|
||||||
import {ThemeName, light, dark, dim} from '#/alf/themes'
|
|
||||||
import * as SystemUI from 'expo-system-ui'
|
import * as SystemUI from 'expo-system-ui'
|
||||||
|
|
||||||
|
import {isWeb} from 'platform/detection'
|
||||||
|
import {useThemePrefs} from 'state/shell'
|
||||||
|
import {dark, dim, light, ThemeName} from '#/alf/themes'
|
||||||
|
|
||||||
export function useColorModeTheme(): ThemeName {
|
export function useColorModeTheme(): ThemeName {
|
||||||
const colorScheme = useColorScheme()
|
const colorScheme = useColorScheme()
|
||||||
const {colorMode, darkTheme} = useThemePrefs()
|
const {colorMode, darkTheme} = useThemePrefs()
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'
|
import React, {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useImperativeHandle,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react'
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
BackHandler,
|
|
||||||
Keyboard,
|
Keyboard,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
|
@ -79,6 +85,10 @@ import {TextInput, TextInputRef} from './text-input/TextInput'
|
||||||
import {ThreadgateBtn} from './threadgate/ThreadgateBtn'
|
import {ThreadgateBtn} from './threadgate/ThreadgateBtn'
|
||||||
import {useExternalLinkFetch} from './useExternalLinkFetch'
|
import {useExternalLinkFetch} from './useExternalLinkFetch'
|
||||||
|
|
||||||
|
type CancelRef = {
|
||||||
|
onPressCancel: () => void
|
||||||
|
}
|
||||||
|
|
||||||
type Props = ComposerOpts
|
type Props = ComposerOpts
|
||||||
export const ComposePost = observer(function ComposePost({
|
export const ComposePost = observer(function ComposePost({
|
||||||
replyTo,
|
replyTo,
|
||||||
|
@ -88,7 +98,10 @@ export const ComposePost = observer(function ComposePost({
|
||||||
openPicker,
|
openPicker,
|
||||||
text: initText,
|
text: initText,
|
||||||
imageUris: initImageUris,
|
imageUris: initImageUris,
|
||||||
}: Props) {
|
cancelRef,
|
||||||
|
}: Props & {
|
||||||
|
cancelRef?: React.RefObject<CancelRef>
|
||||||
|
}) {
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
const {data: currentProfile} = useProfileQuery({did: currentAccount!.did})
|
const {data: currentProfile} = useProfileQuery({did: currentAccount!.did})
|
||||||
|
@ -145,7 +158,7 @@ export const ComposePost = observer(function ComposePost({
|
||||||
() => ({
|
() => ({
|
||||||
paddingBottom:
|
paddingBottom:
|
||||||
isAndroid || (isIOS && !isKeyboardVisible) ? insets.bottom : 0,
|
isAndroid || (isIOS && !isKeyboardVisible) ? insets.bottom : 0,
|
||||||
paddingTop: isAndroid ? insets.top : isMobile ? 15 : 0,
|
paddingTop: isMobile && isWeb ? 15 : insets.top,
|
||||||
}),
|
}),
|
||||||
[insets, isKeyboardVisible, isMobile],
|
[insets, isKeyboardVisible, isMobile],
|
||||||
)
|
)
|
||||||
|
@ -167,23 +180,8 @@ export const ComposePost = observer(function ComposePost({
|
||||||
discardPromptControl,
|
discardPromptControl,
|
||||||
onClose,
|
onClose,
|
||||||
])
|
])
|
||||||
// android back button
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isAndroid) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const backHandler = BackHandler.addEventListener(
|
|
||||||
'hardwareBackPress',
|
|
||||||
() => {
|
|
||||||
onPressCancel()
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
return () => {
|
useImperativeHandle(cancelRef, () => ({onPressCancel}))
|
||||||
backHandler.remove()
|
|
||||||
}
|
|
||||||
}, [onPressCancel])
|
|
||||||
|
|
||||||
// listen to escape key on desktop web
|
// listen to escape key on desktop web
|
||||||
const onEscape = useCallback(
|
const onEscape = useCallback(
|
||||||
|
@ -583,19 +581,18 @@ export const ComposePost = observer(function ComposePost({
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export function useComposerCancelRef() {
|
||||||
|
return useRef<CancelRef>(null)
|
||||||
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
outer: {
|
|
||||||
flexDirection: 'column',
|
|
||||||
flex: 1,
|
|
||||||
height: '100%',
|
|
||||||
},
|
|
||||||
topbar: {
|
topbar: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingTop: 6,
|
marginTop: -14,
|
||||||
paddingBottom: 4,
|
paddingBottom: 4,
|
||||||
paddingHorizontal: 20,
|
paddingHorizontal: 20,
|
||||||
height: 55,
|
height: 50,
|
||||||
gap: 4,
|
gap: 4,
|
||||||
},
|
},
|
||||||
topbarDesktop: {
|
topbarDesktop: {
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
import {KeyboardStickyView} from 'react-native-keyboard-controller'
|
||||||
|
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||||
|
|
||||||
|
import {isWeb} from '#/platform/detection'
|
||||||
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
|
|
||||||
|
export function KeyboardAccessory({children}: {children: React.ReactNode}) {
|
||||||
|
const t = useTheme()
|
||||||
|
const {bottom} = useSafeAreaInsets()
|
||||||
|
|
||||||
|
const style = [
|
||||||
|
a.flex_row,
|
||||||
|
a.py_xs,
|
||||||
|
a.pl_sm,
|
||||||
|
a.pr_xl,
|
||||||
|
a.align_center,
|
||||||
|
a.border_t,
|
||||||
|
t.atoms.border_contrast_medium,
|
||||||
|
t.atoms.bg,
|
||||||
|
]
|
||||||
|
|
||||||
|
// todo: when iPad support is added, it should also not use the KeyboardStickyView
|
||||||
|
if (isWeb) {
|
||||||
|
return <View style={style}>{children}</View>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<KeyboardStickyView offset={{closed: -bottom}} style={style}>
|
||||||
|
{children}
|
||||||
|
</KeyboardStickyView>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,77 +1,49 @@
|
||||||
import React, {useEffect} from 'react'
|
import React from 'react'
|
||||||
|
import {Modal, View} from 'react-native'
|
||||||
import {observer} from 'mobx-react-lite'
|
import {observer} from 'mobx-react-lite'
|
||||||
import {Animated, Easing, Platform, StyleSheet, View} from 'react-native'
|
|
||||||
import {ComposePost} from '../com/composer/Composer'
|
|
||||||
import {useComposerState} from 'state/shell/composer'
|
|
||||||
import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
|
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
|
||||||
|
|
||||||
export const Composer = observer(function ComposerImpl({
|
import {Provider as LegacyModalProvider} from '#/state/modals'
|
||||||
winHeight,
|
import {useComposerState} from 'state/shell/composer'
|
||||||
}: {
|
import {ModalsContainer as LegacyModalsContainer} from '#/view/com/modals/Modal'
|
||||||
|
import {useTheme} from '#/alf'
|
||||||
|
import {
|
||||||
|
Outlet as PortalOutlet,
|
||||||
|
Provider as PortalProvider,
|
||||||
|
} from '#/components/Portal'
|
||||||
|
import {ComposePost, useComposerCancelRef} from '../com/composer/Composer'
|
||||||
|
|
||||||
|
export const Composer = observer(function ComposerImpl({}: {
|
||||||
winHeight: number
|
winHeight: number
|
||||||
}) {
|
}) {
|
||||||
|
const t = useTheme()
|
||||||
const state = useComposerState()
|
const state = useComposerState()
|
||||||
const pal = usePalette('default')
|
const ref = useComposerCancelRef()
|
||||||
const initInterp = useAnimatedValue(0)
|
|
||||||
|
|
||||||
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 (
|
return (
|
||||||
<Animated.View
|
<Modal
|
||||||
style={[styles.wrapper, pal.view, wrapperAnimStyle]}
|
|
||||||
aria-modal
|
aria-modal
|
||||||
accessibilityViewIsModal>
|
accessibilityViewIsModal
|
||||||
<ComposePost
|
visible={!!state}
|
||||||
replyTo={state.replyTo}
|
presentationStyle="overFullScreen"
|
||||||
onPost={state.onPost}
|
animationType="slide"
|
||||||
quote={state.quote}
|
onRequestClose={() => ref.current?.onPressCancel()}>
|
||||||
mention={state.mention}
|
<View style={[t.atoms.bg, {flex: 1}]}>
|
||||||
text={state.text}
|
<LegacyModalProvider>
|
||||||
imageUris={state.imageUris}
|
<PortalProvider>
|
||||||
/>
|
<ComposePost
|
||||||
</Animated.View>
|
cancelRef={ref}
|
||||||
|
replyTo={state?.replyTo}
|
||||||
|
onPost={state?.onPost}
|
||||||
|
quote={state?.quote}
|
||||||
|
mention={state?.mention}
|
||||||
|
text={state?.text}
|
||||||
|
imageUris={state?.imageUris}
|
||||||
|
/>
|
||||||
|
<LegacyModalsContainer />
|
||||||
|
<PortalOutlet />
|
||||||
|
</PortalProvider>
|
||||||
|
</LegacyModalProvider>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
wrapper: {
|
|
||||||
position: 'absolute',
|
|
||||||
top: 0,
|
|
||||||
bottom: 0,
|
|
||||||
width: '100%',
|
|
||||||
...Platform.select({
|
|
||||||
ios: {
|
|
||||||
paddingTop: 24,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
Loading…
Reference in New Issue