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,
} from '#/components/Dialog/types'
import {createInput} from '#/components/forms/TextField'
import {FullWindowOverlay} from '#/components/FullWindowOverlay'
import {Portal} from '#/components/Portal'
export {useDialogContext, useDialogControl} from '#/components/Dialog/context'
@ -170,47 +171,49 @@ export function Outer({
return (
isOpen && (
<Portal>
<View
// iOS
accessibilityViewIsModal
// Android
importantForAccessibility="yes"
style={[a.absolute, a.inset_0]}
testID={testID}
onTouchMove={() => Keyboard.dismiss()}>
<BottomSheet
enableDynamicSizing={!hasSnapPoints}
enablePanDownToClose
keyboardBehavior="interactive"
android_keyboardInputMode="adjustResize"
keyboardBlurBehavior="restore"
topInset={insets.top}
{...sheetOptions}
snapPoints={sheetOptions.snapPoints || ['100%']}
ref={sheet}
index={openIndex}
backgroundStyle={{backgroundColor: 'transparent'}}
backdropComponent={Backdrop}
handleIndicatorStyle={{backgroundColor: t.palette.primary_500}}
handleStyle={{display: 'none'}}
onClose={onCloseAnimationComplete}>
<Context.Provider value={context}>
<View
style={[
a.absolute,
a.inset_0,
t.atoms.bg,
{
borderTopLeftRadius: 40,
borderTopRightRadius: 40,
height: Dimensions.get('window').height * 2,
},
]}
/>
{children}
</Context.Provider>
</BottomSheet>
</View>
<FullWindowOverlay>
<View
// iOS
accessibilityViewIsModal
// Android
importantForAccessibility="yes"
style={[a.absolute, a.inset_0]}
testID={testID}
onTouchMove={() => Keyboard.dismiss()}>
<BottomSheet
enableDynamicSizing={!hasSnapPoints}
enablePanDownToClose
keyboardBehavior="interactive"
android_keyboardInputMode="adjustResize"
keyboardBlurBehavior="restore"
topInset={insets.top}
{...sheetOptions}
snapPoints={sheetOptions.snapPoints || ['100%']}
ref={sheet}
index={openIndex}
backgroundStyle={{backgroundColor: 'transparent'}}
backdropComponent={Backdrop}
handleIndicatorStyle={{backgroundColor: t.palette.primary_500}}
handleStyle={{display: 'none'}}
onClose={onCloseAnimationComplete}>
<Context.Provider value={context}>
<View
style={[
a.absolute,
a.inset_0,
t.atoms.bg,
{
borderTopLeftRadius: 40,
borderTopRightRadius: 40,
height: Dimensions.get('window').height * 2,
},
]}
/>
{children}
</Context.Provider>
</BottomSheet>
</View>
</FullWindowOverlay>
</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(
hasScrolled.value,
[0, 1],
[
'transparent',
isWeb
? t.atoms.border_contrast_low.borderColor
: t.atoms.border_contrast_high.borderColor,
],
['transparent', t.atoms.border_contrast_medium.borderColor],
),
}
})
@ -405,106 +400,112 @@ export const ComposePost = observer(function ComposePost({
<KeyboardAvoidingView
testID="composePostView"
behavior="padding"
style={s.flex1}
keyboardVerticalOffset={replyTo ? 60 : isAndroid ? 120 : 100}>
<View style={[s.flex1, viewStyles]} aria-modal accessibilityViewIsModal>
style={a.flex_1}
keyboardVerticalOffset={replyTo ? 120 : isAndroid ? 180 : 150}>
<View
style={[a.flex_1, viewStyles]}
aria-modal
accessibilityViewIsModal>
<Animated.View
style={[
styles.topbar,
topBarAnimatedStyle,
isWeb && isTabletOrDesktop && styles.topbarDesktop,
]}>
<TouchableOpacity
testID="composerDiscardButton"
onPress={onPressCancel}
onAccessibilityEscape={onPressCancel}
accessibilityRole="button"
accessibilityLabel={_(msg`Cancel`)}
accessibilityHint={_(
msg`Closes post composer and discards post draft`,
)}
hitSlop={HITSLOP_10}>
<Text style={[pal.link, s.f18]}>
<Trans>Cancel</Trans>
</Text>
</TouchableOpacity>
<View style={s.flex1} />
{isProcessing ? (
<>
<Text style={pal.textLight}>{processingState}</Text>
<View style={styles.postBtn}>
<ActivityIndicator />
</View>
</>
) : (
<>
<LabelsBtn
labels={labels}
onChange={setLabels}
hasMedia={hasMedia}
/>
{canPost ? (
<TouchableOpacity
testID="composerPublishBtn"
onPress={onPressPublish}
accessibilityRole="button"
accessibilityLabel={
replyTo ? _(msg`Publish reply`) : _(msg`Publish post`)
}
accessibilityHint="">
<LinearGradient
colors={[
gradients.blueLight.start,
gradients.blueLight.end,
]}
start={{x: 0, y: 0}}
end={{x: 1, y: 1}}
style={styles.postBtn}>
<Text style={[s.white, s.f16, s.bold]}>
{replyTo ? (
<Trans context="action">Reply</Trans>
) : (
<Trans context="action">Post</Trans>
)}
</Text>
</LinearGradient>
</TouchableOpacity>
) : (
<View style={[styles.postBtn, pal.btn]}>
<Text style={[pal.textLight, s.f16, s.bold]}>
<Trans context="action">Post</Trans>
</Text>
</View>
<View style={styles.topbarInner}>
<TouchableOpacity
testID="composerDiscardButton"
onPress={onPressCancel}
onAccessibilityEscape={onPressCancel}
accessibilityRole="button"
accessibilityLabel={_(msg`Cancel`)}
accessibilityHint={_(
msg`Closes post composer and discards post draft`,
)}
</>
hitSlop={HITSLOP_10}>
<Text style={[pal.link, s.f18]}>
<Trans>Cancel</Trans>
</Text>
</TouchableOpacity>
<View style={a.flex_1} />
{isProcessing ? (
<>
<Text style={pal.textLight}>{processingState}</Text>
<View style={styles.postBtn}>
<ActivityIndicator />
</View>
</>
) : (
<>
<LabelsBtn
labels={labels}
onChange={setLabels}
hasMedia={hasMedia}
/>
{canPost ? (
<TouchableOpacity
testID="composerPublishBtn"
onPress={onPressPublish}
accessibilityRole="button"
accessibilityLabel={
replyTo ? _(msg`Publish reply`) : _(msg`Publish post`)
}
accessibilityHint="">
<LinearGradient
colors={[
gradients.blueLight.start,
gradients.blueLight.end,
]}
start={{x: 0, y: 0}}
end={{x: 1, y: 1}}
style={styles.postBtn}>
<Text style={[s.white, s.f16, s.bold]}>
{replyTo ? (
<Trans context="action">Reply</Trans>
) : (
<Trans context="action">Post</Trans>
)}
</Text>
</LinearGradient>
</TouchableOpacity>
) : (
<View style={[styles.postBtn, pal.btn]}>
<Text style={[pal.textLight, s.f16, s.bold]}>
<Trans context="action">Post</Trans>
</Text>
</View>
)}
</>
)}
</View>
{isAltTextRequiredAndMissing && (
<View style={[styles.reminderLine, pal.viewLight]}>
<View style={styles.errorIcon}>
<FontAwesomeIcon
icon="exclamation"
style={{color: colors.red4}}
size={10}
/>
</View>
<Text style={[pal.text, a.flex_1]}>
<Trans>One or more images is missing alt text.</Trans>
</Text>
</View>
)}
{error !== '' && (
<View style={styles.errorLine}>
<View style={styles.errorIcon}>
<FontAwesomeIcon
icon="exclamation"
style={{color: colors.red4}}
size={10}
/>
</View>
<Text style={[s.red4, a.flex_1]}>{error}</Text>
</View>
)}
</Animated.View>
{isAltTextRequiredAndMissing && (
<View style={[styles.reminderLine, pal.viewLight]}>
<View style={styles.errorIcon}>
<FontAwesomeIcon
icon="exclamation"
style={{color: colors.red4}}
size={10}
/>
</View>
<Text style={[pal.text, s.flex1]}>
<Trans>One or more images is missing alt text.</Trans>
</Text>
</View>
)}
{error !== '' && (
<View style={styles.errorLine}>
<View style={styles.errorIcon}>
<FontAwesomeIcon
icon="exclamation"
style={{color: colors.red4}}
size={10}
/>
</View>
<Text style={[s.red4, s.flex1]}>{error}</Text>
</View>
)}
<Animated.ScrollView
onScroll={scrollHandler}
style={styles.scrollView}
@ -576,7 +577,12 @@ export const ComposePost = observer(function ComposePost({
{replyTo ? null : (
<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]}>
<SelectPhotoBtn gallery={gallery} disabled={!canSelectImages} />
<OpenCameraBtn gallery={gallery} disabled={!canSelectImages} />
@ -598,7 +604,7 @@ export const ComposePost = observer(function ComposePost({
</Button>
) : null}
</View>
<View style={s.flex1} />
<View style={a.flex_1} />
<SelectLangBtn />
<CharProgress count={graphemeLength} />
</View>
@ -621,11 +627,6 @@ export function useComposerCancelRef() {
const styles = StyleSheet.create({
topbar: {
flexDirection: 'row',
alignItems: 'center',
marginHorizontal: 16,
height: 54,
gap: 4,
borderBottomWidth: StyleSheet.hairlineWidth,
},
topbarDesktop: {
@ -633,6 +634,13 @@ const styles = StyleSheet.create({
paddingBottom: 10,
height: 50,
},
topbarInner: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 16,
height: 54,
gap: 4,
},
postBtn: {
borderRadius: 20,
paddingHorizontal: 20,
@ -643,19 +651,19 @@ const styles = StyleSheet.create({
flexDirection: 'row',
backgroundColor: colors.red1,
borderRadius: 6,
marginHorizontal: 15,
marginHorizontal: 16,
paddingHorizontal: 8,
paddingVertical: 6,
marginVertical: 6,
marginBottom: 8,
},
reminderLine: {
flexDirection: 'row',
alignItems: 'center',
borderRadius: 6,
marginHorizontal: 15,
marginHorizontal: 16,
paddingHorizontal: 8,
paddingVertical: 6,
marginBottom: 6,
marginBottom: 8,
},
errorIcon: {
borderWidth: hairlineWidth,
@ -690,8 +698,8 @@ const styles = StyleSheet.create({
bottomBar: {
flexDirection: 'row',
paddingVertical: 4,
paddingLeft: 15,
paddingRight: 20,
paddingLeft: 8,
paddingRight: 16,
alignItems: 'center',
borderTopWidth: hairlineWidth,
},

View File

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

View File

@ -7,7 +7,7 @@ import {isNative} from '#/platform/detection'
import {useModalControls} from '#/state/modals'
import {ThreadgateSetting} from '#/state/queries/threadgate'
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 {CircleBanSign_Stroke2_Corner0_Rounded as CircleBanSign} from '#/components/icons/CircleBanSign'
import {Earth_Stroke2_Corner0_Rounded as Earth} from '#/components/icons/Globe'
@ -22,6 +22,7 @@ export function ThreadgateBtn({
}) {
const {track} = useAnalytics()
const {_} = useLingui()
const t = useTheme()
const {openModal} = useModalControls()
const onPress = () => {
@ -45,7 +46,7 @@ export function ThreadgateBtn({
: _(msg`Some people can reply`)
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
variant="solid"
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 {SafeAreaView} from 'react-native-safe-area-context'
import BottomSheet from '@discord/bottom-sheet/src'
import {useModalControls, useModals} from '#/state/modals'
import {usePalette} from 'lib/hooks/usePalette'
import {FullWindowOverlay} from '#/components/FullWindowOverlay'
import {KeyboardPadding} from '#/components/KeyboardPadding'
import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop'
import * as AddAppPassword from './AddAppPasswords'
@ -127,24 +128,28 @@ export function ModalsContainer() {
)
}
const Container = activeModal ? FullWindowOverlay : Fragment
return (
<BottomSheet
ref={bottomSheetRef}
snapPoints={snapPoints}
handleHeight={HANDLE_HEIGHT}
index={isModalActive ? 0 : -1}
enablePanDownToClose
android_keyboardInputMode="adjustResize"
keyboardBlurBehavior="restore"
backdropComponent={
isModalActive ? createCustomBackdrop(onClose) : undefined
}
handleIndicatorStyle={{backgroundColor: pal.text.color}}
handleStyle={[styles.handle, pal.view]}
onChange={onBottomSheetChange}>
{element}
<KeyboardPadding />
</BottomSheet>
<Container>
<BottomSheet
ref={bottomSheetRef}
snapPoints={snapPoints}
handleHeight={HANDLE_HEIGHT}
index={isModalActive ? 0 : -1}
enablePanDownToClose
android_keyboardInputMode="adjustResize"
keyboardBlurBehavior="restore"
backdropComponent={
isModalActive ? createCustomBackdrop(onClose) : undefined
}
handleIndicatorStyle={{backgroundColor: pal.text.color}}
handleStyle={[styles.handle, pal.view]}
onChange={onBottomSheetChange}>
{element}
<KeyboardPadding />
</BottomSheet>
</Container>
)
}

View File

@ -7,18 +7,19 @@ import {
View,
ViewStyle,
} from 'react-native'
import {Text} from '../util/text/Text'
import {s, colors} from 'lib/styles'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
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 {colors, s} from 'lib/styles'
import {isWeb} from 'platform/detection'
import {ScrollView} from 'view/com/modals/util'
import {Trans, msg} from '@lingui/macro'
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'
import {Text} from '../util/text/Text'
export const snapPoints = ['60%']
@ -155,7 +156,7 @@ function Selectable({
accessibilityLabel={label}
accessibilityHint=""
style={[styles.selectable, pal.border, pal.view, style]}>
<Text type="xl" style={[pal.text]}>
<Text type="lg" style={[pal.text]}>
{label}
</Text>
{isSelected ? (

View File

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

View File

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

View File

@ -1,5 +1,7 @@
import React, {useLayoutEffect} 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 {observer} from 'mobx-react-lite'
@ -34,27 +36,56 @@ export const Composer = observer(function ComposerImpl({}: {
animationType="slide"
onRequestClose={() => ref.current?.onPressCancel()}>
<View style={[t.atoms.bg, a.flex_1]}>
<LegacyModalProvider>
<PortalProvider>
<ComposePost
cancelRef={ref}
replyTo={state?.replyTo}
onPost={state?.onPost}
quote={state?.quote}
mention={state?.mention}
text={state?.text}
imageUris={state?.imageUris}
/>
<LegacyModalsContainer />
<PortalOutlet />
</PortalProvider>
</LegacyModalProvider>
{isIOS && <IOSModalBackground active={open} />}
<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
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}) {