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
Samuel Newman 2024-05-30 07:44:20 +03:00 committed by GitHub
parent fba4a9ca02
commit c4abaa1abc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 101 additions and 98 deletions

View File

@ -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()

View File

@ -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: {

View File

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

View File

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