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 solidzio/stable
parent
d614f6cb71
commit
05b55c1966
|
@ -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,6 +171,7 @@ export function Outer({
|
|||
return (
|
||||
isOpen && (
|
||||
<Portal>
|
||||
<FullWindowOverlay>
|
||||
<View
|
||||
// iOS
|
||||
accessibilityViewIsModal
|
||||
|
@ -211,6 +213,7 @@ export function Outer({
|
|||
</Context.Provider>
|
||||
</BottomSheet>
|
||||
</View>
|
||||
</FullWindowOverlay>
|
||||
</Portal>
|
||||
)
|
||||
)
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export {FullWindowOverlay} from 'react-native-screens'
|
|
@ -0,0 +1 @@
|
|||
export {Fragment as FullWindowOverlay} from 'react'
|
|
@ -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,15 +400,19 @@ 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,
|
||||
]}>
|
||||
<View style={styles.topbarInner}>
|
||||
<TouchableOpacity
|
||||
testID="composerDiscardButton"
|
||||
onPress={onPressCancel}
|
||||
|
@ -428,7 +427,7 @@ export const ComposePost = observer(function ComposePost({
|
|||
<Trans>Cancel</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={s.flex1} />
|
||||
<View style={a.flex_1} />
|
||||
{isProcessing ? (
|
||||
<>
|
||||
<Text style={pal.textLight}>{processingState}</Text>
|
||||
|
@ -478,7 +477,8 @@ export const ComposePost = observer(function ComposePost({
|
|||
)}
|
||||
</>
|
||||
)}
|
||||
</Animated.View>
|
||||
</View>
|
||||
|
||||
{isAltTextRequiredAndMissing && (
|
||||
<View style={[styles.reminderLine, pal.viewLight]}>
|
||||
<View style={styles.errorIcon}>
|
||||
|
@ -488,7 +488,7 @@ export const ComposePost = observer(function ComposePost({
|
|||
size={10}
|
||||
/>
|
||||
</View>
|
||||
<Text style={[pal.text, s.flex1]}>
|
||||
<Text style={[pal.text, a.flex_1]}>
|
||||
<Trans>One or more images is missing alt text.</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
|
@ -502,9 +502,10 @@ export const ComposePost = observer(function ComposePost({
|
|||
size={10}
|
||||
/>
|
||||
</View>
|
||||
<Text style={[s.red4, s.flex1]}>{error}</Text>
|
||||
<Text style={[s.red4, a.flex_1]}>{error}</Text>
|
||||
</View>
|
||||
)}
|
||||
</Animated.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,
|
||||
},
|
||||
|
|
|
@ -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={_(
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,7 +128,10 @@ export function ModalsContainer() {
|
|||
)
|
||||
}
|
||||
|
||||
const Container = activeModal ? FullWindowOverlay : Fragment
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<BottomSheet
|
||||
ref={bottomSheetRef}
|
||||
snapPoints={snapPoints}
|
||||
|
@ -145,6 +149,7 @@ export function ModalsContainer() {
|
|||
{element}
|
||||
<KeyboardPadding />
|
||||
</BottomSheet>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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 ? (
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
}))
|
||||
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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,8 +36,7 @@ export const Composer = observer(function ComposerImpl({}: {
|
|||
animationType="slide"
|
||||
onRequestClose={() => ref.current?.onPressCancel()}>
|
||||
<View style={[t.atoms.bg, a.flex_1]}>
|
||||
<LegacyModalProvider>
|
||||
<PortalProvider>
|
||||
<Providers open={open}>
|
||||
<ComposePost
|
||||
cancelRef={ref}
|
||||
replyTo={state?.replyTo}
|
||||
|
@ -45,16 +46,46 @@ export const Composer = observer(function ComposerImpl({}: {
|
|||
text={state?.text}
|
||||
imageUris={state?.imageUris}
|
||||
/>
|
||||
<LegacyModalsContainer />
|
||||
<PortalOutlet />
|
||||
</PortalProvider>
|
||||
</LegacyModalProvider>
|
||||
{isIOS && <IOSModalBackground active={open} />}
|
||||
</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}) {
|
||||
|
|
Loading…
Reference in New Issue