Composer - fix modals, and other tweaks (#4298)

* fix depreciated import

* add animations to old dropdown

* wrap modals in fullwindowoverlay

* move errors inside header

* add background to bottom bar and stop overlap

* nest dialogs on android

* fix android (wrap in gesturehandlerrootview)

* make borders all the same color

* revert threadgate button back to solid
zio/stable
Samuel Newman 2024-05-31 14:55:51 +03:00 committed by GitHub
parent d614f6cb71
commit 05b55c1966
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 269 additions and 221 deletions

View File

@ -32,6 +32,7 @@ import {
DialogOuterProps, DialogOuterProps,
} from '#/components/Dialog/types' } from '#/components/Dialog/types'
import {createInput} from '#/components/forms/TextField' import {createInput} from '#/components/forms/TextField'
import {FullWindowOverlay} from '#/components/FullWindowOverlay'
import {Portal} from '#/components/Portal' import {Portal} from '#/components/Portal'
export {useDialogContext, useDialogControl} from '#/components/Dialog/context' export {useDialogContext, useDialogControl} from '#/components/Dialog/context'
@ -170,6 +171,7 @@ export function Outer({
return ( return (
isOpen && ( isOpen && (
<Portal> <Portal>
<FullWindowOverlay>
<View <View
// iOS // iOS
accessibilityViewIsModal accessibilityViewIsModal
@ -211,6 +213,7 @@ export function Outer({
</Context.Provider> </Context.Provider>
</BottomSheet> </BottomSheet>
</View> </View>
</FullWindowOverlay>
</Portal> </Portal>
) )
) )

View File

@ -0,0 +1 @@
export {FullWindowOverlay} from 'react-native-screens'

View File

@ -0,0 +1 @@
export {Fragment as FullWindowOverlay} from 'react'

View File

@ -181,12 +181,7 @@ export const ComposePost = observer(function ComposePost({
borderColor: interpolateColor( borderColor: interpolateColor(
hasScrolled.value, hasScrolled.value,
[0, 1], [0, 1],
[ ['transparent', t.atoms.border_contrast_medium.borderColor],
'transparent',
isWeb
? t.atoms.border_contrast_low.borderColor
: t.atoms.border_contrast_high.borderColor,
],
), ),
} }
}) })
@ -405,15 +400,19 @@ export const ComposePost = observer(function ComposePost({
<KeyboardAvoidingView <KeyboardAvoidingView
testID="composePostView" testID="composePostView"
behavior="padding" behavior="padding"
style={s.flex1} style={a.flex_1}
keyboardVerticalOffset={replyTo ? 60 : isAndroid ? 120 : 100}> keyboardVerticalOffset={replyTo ? 120 : isAndroid ? 180 : 150}>
<View style={[s.flex1, viewStyles]} aria-modal accessibilityViewIsModal> <View
style={[a.flex_1, viewStyles]}
aria-modal
accessibilityViewIsModal>
<Animated.View <Animated.View
style={[ style={[
styles.topbar, styles.topbar,
topBarAnimatedStyle, topBarAnimatedStyle,
isWeb && isTabletOrDesktop && styles.topbarDesktop, isWeb && isTabletOrDesktop && styles.topbarDesktop,
]}> ]}>
<View style={styles.topbarInner}>
<TouchableOpacity <TouchableOpacity
testID="composerDiscardButton" testID="composerDiscardButton"
onPress={onPressCancel} onPress={onPressCancel}
@ -428,7 +427,7 @@ export const ComposePost = observer(function ComposePost({
<Trans>Cancel</Trans> <Trans>Cancel</Trans>
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
<View style={s.flex1} /> <View style={a.flex_1} />
{isProcessing ? ( {isProcessing ? (
<> <>
<Text style={pal.textLight}>{processingState}</Text> <Text style={pal.textLight}>{processingState}</Text>
@ -478,7 +477,8 @@ export const ComposePost = observer(function ComposePost({
)} )}
</> </>
)} )}
</Animated.View> </View>
{isAltTextRequiredAndMissing && ( {isAltTextRequiredAndMissing && (
<View style={[styles.reminderLine, pal.viewLight]}> <View style={[styles.reminderLine, pal.viewLight]}>
<View style={styles.errorIcon}> <View style={styles.errorIcon}>
@ -488,7 +488,7 @@ export const ComposePost = observer(function ComposePost({
size={10} size={10}
/> />
</View> </View>
<Text style={[pal.text, s.flex1]}> <Text style={[pal.text, a.flex_1]}>
<Trans>One or more images is missing alt text.</Trans> <Trans>One or more images is missing alt text.</Trans>
</Text> </Text>
</View> </View>
@ -502,9 +502,10 @@ export const ComposePost = observer(function ComposePost({
size={10} size={10}
/> />
</View> </View>
<Text style={[s.red4, s.flex1]}>{error}</Text> <Text style={[s.red4, a.flex_1]}>{error}</Text>
</View> </View>
)} )}
</Animated.View>
<Animated.ScrollView <Animated.ScrollView
onScroll={scrollHandler} onScroll={scrollHandler}
style={styles.scrollView} style={styles.scrollView}
@ -576,7 +577,12 @@ export const ComposePost = observer(function ComposePost({
{replyTo ? null : ( {replyTo ? null : (
<ThreadgateBtn threadgate={threadgate} onChange={setThreadgate} /> <ThreadgateBtn threadgate={threadgate} onChange={setThreadgate} />
)} )}
<View style={[pal.border, styles.bottomBar]}> <View
style={[
t.atoms.bg,
t.atoms.border_contrast_medium,
styles.bottomBar,
]}>
<View style={[a.flex_row, a.align_center, a.gap_xs]}> <View style={[a.flex_row, a.align_center, a.gap_xs]}>
<SelectPhotoBtn gallery={gallery} disabled={!canSelectImages} /> <SelectPhotoBtn gallery={gallery} disabled={!canSelectImages} />
<OpenCameraBtn gallery={gallery} disabled={!canSelectImages} /> <OpenCameraBtn gallery={gallery} disabled={!canSelectImages} />
@ -598,7 +604,7 @@ export const ComposePost = observer(function ComposePost({
</Button> </Button>
) : null} ) : null}
</View> </View>
<View style={s.flex1} /> <View style={a.flex_1} />
<SelectLangBtn /> <SelectLangBtn />
<CharProgress count={graphemeLength} /> <CharProgress count={graphemeLength} />
</View> </View>
@ -621,11 +627,6 @@ export function useComposerCancelRef() {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
topbar: { topbar: {
flexDirection: 'row',
alignItems: 'center',
marginHorizontal: 16,
height: 54,
gap: 4,
borderBottomWidth: StyleSheet.hairlineWidth, borderBottomWidth: StyleSheet.hairlineWidth,
}, },
topbarDesktop: { topbarDesktop: {
@ -633,6 +634,13 @@ const styles = StyleSheet.create({
paddingBottom: 10, paddingBottom: 10,
height: 50, height: 50,
}, },
topbarInner: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 16,
height: 54,
gap: 4,
},
postBtn: { postBtn: {
borderRadius: 20, borderRadius: 20,
paddingHorizontal: 20, paddingHorizontal: 20,
@ -643,19 +651,19 @@ const styles = StyleSheet.create({
flexDirection: 'row', flexDirection: 'row',
backgroundColor: colors.red1, backgroundColor: colors.red1,
borderRadius: 6, borderRadius: 6,
marginHorizontal: 15, marginHorizontal: 16,
paddingHorizontal: 8, paddingHorizontal: 8,
paddingVertical: 6, paddingVertical: 6,
marginVertical: 6, marginBottom: 8,
}, },
reminderLine: { reminderLine: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
borderRadius: 6, borderRadius: 6,
marginHorizontal: 15, marginHorizontal: 16,
paddingHorizontal: 8, paddingHorizontal: 8,
paddingVertical: 6, paddingVertical: 6,
marginBottom: 6, marginBottom: 8,
}, },
errorIcon: { errorIcon: {
borderWidth: hairlineWidth, borderWidth: hairlineWidth,
@ -690,8 +698,8 @@ const styles = StyleSheet.create({
bottomBar: { bottomBar: {
flexDirection: 'row', flexDirection: 'row',
paddingVertical: 4, paddingVertical: 4,
paddingLeft: 15, paddingLeft: 8,
paddingRight: 20, paddingRight: 16,
alignItems: 'center', alignItems: 'center',
borderTopWidth: hairlineWidth, borderTopWidth: hairlineWidth,
}, },

View File

@ -10,7 +10,6 @@ import {
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {isWeb} from '#/platform/detection'
import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeDisplayName} from 'lib/strings/display-names'
import {sanitizeHandle} from 'lib/strings/handles' import {sanitizeHandle} from 'lib/strings/handles'
import {ComposerOptsPostRef} from 'state/shell/composer' import {ComposerOptsPostRef} from 'state/shell/composer'
@ -76,10 +75,7 @@ export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
return ( return (
<Pressable <Pressable
style={[ style={[t.atoms.border_contrast_medium, styles.replyToLayout]}
isWeb ? t.atoms.border_contrast_low : t.atoms.border_contrast_high,
styles.replyToLayout,
]}
onPress={onPress} onPress={onPress}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={_( accessibilityLabel={_(

View File

@ -7,7 +7,7 @@ import {isNative} from '#/platform/detection'
import {useModalControls} from '#/state/modals' import {useModalControls} from '#/state/modals'
import {ThreadgateSetting} from '#/state/queries/threadgate' import {ThreadgateSetting} from '#/state/queries/threadgate'
import {useAnalytics} from 'lib/analytics/analytics' import {useAnalytics} from 'lib/analytics/analytics'
import {atoms as a} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {CircleBanSign_Stroke2_Corner0_Rounded as CircleBanSign} from '#/components/icons/CircleBanSign' import {CircleBanSign_Stroke2_Corner0_Rounded as CircleBanSign} from '#/components/icons/CircleBanSign'
import {Earth_Stroke2_Corner0_Rounded as Earth} from '#/components/icons/Globe' import {Earth_Stroke2_Corner0_Rounded as Earth} from '#/components/icons/Globe'
@ -22,6 +22,7 @@ export function ThreadgateBtn({
}) { }) {
const {track} = useAnalytics() const {track} = useAnalytics()
const {_} = useLingui() const {_} = useLingui()
const t = useTheme()
const {openModal} = useModalControls() const {openModal} = useModalControls()
const onPress = () => { const onPress = () => {
@ -45,7 +46,7 @@ export function ThreadgateBtn({
: _(msg`Some people can reply`) : _(msg`Some people can reply`)
return ( return (
<View style={[a.flex_row, a.pb_sm, a.px_md]}> <View style={[a.flex_row, a.py_xs, a.px_sm, t.atoms.bg]}>
<Button <Button
variant="solid" variant="solid"
color="secondary" color="secondary"

View File

@ -1,10 +1,11 @@
import React, {useEffect, useRef} from 'react' import React, {Fragment, useEffect, useRef} from 'react'
import {StyleSheet} from 'react-native' import {StyleSheet} from 'react-native'
import {SafeAreaView} from 'react-native-safe-area-context' import {SafeAreaView} from 'react-native-safe-area-context'
import BottomSheet from '@discord/bottom-sheet/src' import BottomSheet from '@discord/bottom-sheet/src'
import {useModalControls, useModals} from '#/state/modals' import {useModalControls, useModals} from '#/state/modals'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {FullWindowOverlay} from '#/components/FullWindowOverlay'
import {KeyboardPadding} from '#/components/KeyboardPadding' import {KeyboardPadding} from '#/components/KeyboardPadding'
import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop' import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop'
import * as AddAppPassword from './AddAppPasswords' import * as AddAppPassword from './AddAppPasswords'
@ -127,7 +128,10 @@ export function ModalsContainer() {
) )
} }
const Container = activeModal ? FullWindowOverlay : Fragment
return ( return (
<Container>
<BottomSheet <BottomSheet
ref={bottomSheetRef} ref={bottomSheetRef}
snapPoints={snapPoints} snapPoints={snapPoints}
@ -145,6 +149,7 @@ export function ModalsContainer() {
{element} {element}
<KeyboardPadding /> <KeyboardPadding />
</BottomSheet> </BottomSheet>
</Container>
) )
} }

View File

@ -7,18 +7,19 @@ import {
View, View,
ViewStyle, ViewStyle,
} from 'react-native' } from 'react-native'
import {Text} from '../util/text/Text' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {s, colors} from 'lib/styles' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import isEqual from 'lodash.isequal'
import {useModalControls} from '#/state/modals'
import {useMyListsQuery} from '#/state/queries/my-lists'
import {ThreadgateSetting} from '#/state/queries/threadgate'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {colors, s} from 'lib/styles'
import {isWeb} from 'platform/detection' import {isWeb} from 'platform/detection'
import {ScrollView} from 'view/com/modals/util' import {ScrollView} from 'view/com/modals/util'
import {Trans, msg} from '@lingui/macro' import {Text} from '../util/text/Text'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
import {ThreadgateSetting} from '#/state/queries/threadgate'
import {useMyListsQuery} from '#/state/queries/my-lists'
import isEqual from 'lodash.isequal'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
export const snapPoints = ['60%'] export const snapPoints = ['60%']
@ -155,7 +156,7 @@ function Selectable({
accessibilityLabel={label} accessibilityLabel={label}
accessibilityHint="" accessibilityHint=""
style={[styles.selectable, pal.border, pal.view, style]}> style={[styles.selectable, pal.border, pal.view, style]}>
<Text type="xl" style={[pal.text]}> <Text type="lg" style={[pal.text]}>
{label} {label}
</Text> </Text>
{isSelected ? ( {isSelected ? (

View File

@ -1,7 +1,7 @@
import React, {useMemo} from 'react' import React, {useMemo} from 'react'
import {TouchableWithoutFeedback} from 'react-native' import {TouchableWithoutFeedback} from 'react-native'
import Animated, { import Animated, {
Extrapolate, Extrapolation,
interpolate, interpolate,
useAnimatedStyle, useAnimatedStyle,
} from 'react-native-reanimated' } from 'react-native-reanimated'
@ -21,7 +21,7 @@ export function createCustomBackdrop(
animatedIndex.value, // current snap index animatedIndex.value, // current snap index
[-1, 0], // input range [-1, 0], // input range
[0, 0.5], // output range [0, 0.5], // output range
Extrapolate.CLAMP, Extrapolation.CLAMP,
), ),
})) }))

View File

@ -2,7 +2,6 @@ import React, {PropsWithChildren, useMemo, useRef} from 'react'
import { import {
Dimensions, Dimensions,
GestureResponderEvent, GestureResponderEvent,
Platform,
StyleProp, StyleProp,
StyleSheet, StyleSheet,
TouchableOpacity, TouchableOpacity,
@ -11,8 +10,8 @@ import {
View, View,
ViewStyle, ViewStyle,
} from 'react-native' } from 'react-native'
import Animated, {FadeIn, FadeInDown, FadeInUp} from 'react-native-reanimated'
import RootSiblings from 'react-native-root-siblings' import RootSiblings from 'react-native-root-siblings'
import {FullWindowOverlay} from 'react-native-screens'
import {IconProp} from '@fortawesome/fontawesome-svg-core' import {IconProp} from '@fortawesome/fontawesome-svg-core'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
@ -23,6 +22,8 @@ import {usePalette} from 'lib/hooks/usePalette'
import {colors} from 'lib/styles' import {colors} from 'lib/styles'
import {useTheme} from 'lib/ThemeContext' import {useTheme} from 'lib/ThemeContext'
import {isWeb} from 'platform/detection' import {isWeb} from 'platform/detection'
import {native} from '#/alf'
import {FullWindowOverlay} from '#/components/FullWindowOverlay'
import {Text} from '../text/Text' import {Text} from '../text/Text'
import {Button, ButtonType} from './Button' import {Button, ButtonType} from './Button'
@ -127,6 +128,7 @@ export function DropdownButton({
pageY, pageY,
menuWidth, menuWidth,
items.filter(v => !!v) as DropdownItem[], items.filter(v => !!v) as DropdownItem[],
openUpwards,
) )
}, },
) )
@ -181,6 +183,7 @@ function createDropdownMenu(
pageY: number, pageY: number,
width: number, width: number,
items: DropdownItem[], items: DropdownItem[],
opensUpwards = false,
): RootSiblings { ): RootSiblings {
const onPressItem = (index: number) => { const onPressItem = (index: number) => {
sibling.destroy() sibling.destroy()
@ -200,6 +203,7 @@ function createDropdownMenu(
width={width} width={width}
items={items} items={items}
onPressItem={onPressItem} onPressItem={onPressItem}
openUpwards={opensUpwards}
/> />
), ),
) )
@ -214,6 +218,7 @@ type DropDownItemProps = {
width: number width: number
items: DropdownItem[] items: DropdownItem[]
onPressItem: (index: number) => void onPressItem: (index: number) => void
openUpwards: boolean
} }
const DropdownItems = ({ const DropdownItems = ({
@ -224,6 +229,7 @@ const DropdownItems = ({
width, width,
items, items,
onPressItem, onPressItem,
openUpwards,
}: DropDownItemProps) => { }: DropDownItemProps) => {
const pal = usePalette('default') const pal = usePalette('default')
const theme = useTheme() const theme = useTheme()
@ -242,13 +248,14 @@ const DropdownItems = ({
// - (On mobile) be buttons by default, accept `label` and `nativeID` // - (On mobile) be buttons by default, accept `label` and `nativeID`
// props, and always have an explicit label // props, and always have an explicit label
return ( return (
<Wrapper> <FullWindowOverlay>
{/* This TouchableWithoutFeedback renders the background so if the user clicks outside, the dropdown closes */} {/* This TouchableWithoutFeedback renders the background so if the user clicks outside, the dropdown closes */}
<TouchableWithoutFeedback <TouchableWithoutFeedback
onPress={onOuterPress} onPress={onOuterPress}
accessibilityLabel={_(msg`Toggle dropdown`)} accessibilityLabel={_(msg`Toggle dropdown`)}
accessibilityHint=""> accessibilityHint="">
<View <Animated.View
entering={FadeIn}
style={[ style={[
styles.bg, styles.bg,
// On web we need to adjust the top and bottom relative to the scroll position // On web we need to adjust the top and bottom relative to the scroll position
@ -264,7 +271,10 @@ const DropdownItems = ({
]} ]}
/> />
</TouchableWithoutFeedback> </TouchableWithoutFeedback>
<View <Animated.View
entering={native(
openUpwards ? FadeInDown.springify(1000) : FadeInUp.springify(1000),
)}
style={[ style={[
styles.menu, styles.menu,
{left: x, top: y, width}, {left: x, top: y, width},
@ -306,18 +316,11 @@ const DropdownItems = ({
} }
return null return null
})} })}
</View> </Animated.View>
</Wrapper> </FullWindowOverlay>
) )
} }
// on iOS, due to formSheet presentation style, we need to render the overlay
// as a full screen overlay
const Wrapper = Platform.select({
ios: FullWindowOverlay,
default: ({children}) => <>{children}</>,
})
function isSep(item: DropdownItem): item is DropdownItemSeparator { function isSep(item: DropdownItem): item is DropdownItemSeparator {
return 'sep' in item && item.sep return 'sep' in item && item.sep
} }
@ -333,14 +336,12 @@ const styles = StyleSheet.create({
position: 'absolute', position: 'absolute',
left: 0, left: 0,
width: '100%', width: '100%',
backgroundColor: '#000', backgroundColor: 'rgba(0, 0, 0, 0.1)',
opacity: 0.1,
}, },
menu: { menu: {
position: 'absolute', position: 'absolute',
backgroundColor: '#fff', backgroundColor: '#fff',
borderRadius: 14, borderRadius: 14,
opacity: 1,
paddingVertical: 6, paddingVertical: 6,
}, },
menuItem: { menuItem: {

View File

@ -1,5 +1,7 @@
import React, {useLayoutEffect} from 'react' import React, {useLayoutEffect} from 'react'
import {Modal, View} from 'react-native' 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 {StatusBar} from 'expo-status-bar'
import * as SystemUI from 'expo-system-ui' import * as SystemUI from 'expo-system-ui'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
@ -34,8 +36,7 @@ export const Composer = observer(function ComposerImpl({}: {
animationType="slide" animationType="slide"
onRequestClose={() => ref.current?.onPressCancel()}> onRequestClose={() => ref.current?.onPressCancel()}>
<View style={[t.atoms.bg, a.flex_1]}> <View style={[t.atoms.bg, a.flex_1]}>
<LegacyModalProvider> <Providers open={open}>
<PortalProvider>
<ComposePost <ComposePost
cancelRef={ref} cancelRef={ref}
replyTo={state?.replyTo} replyTo={state?.replyTo}
@ -45,16 +46,46 @@ export const Composer = observer(function ComposerImpl({}: {
text={state?.text} text={state?.text}
imageUris={state?.imageUris} imageUris={state?.imageUris}
/> />
<LegacyModalsContainer /> </Providers>
<PortalOutlet />
</PortalProvider>
</LegacyModalProvider>
{isIOS && <IOSModalBackground active={open} />}
</View> </View>
</Modal> </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
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 // 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. // we want it to be black due to the modal being a form sheet.
function IOSModalBackground({active}: {active: boolean}) { function IOSModalBackground({active}: {active: boolean}) {