import React, {useImperativeHandle} from 'react' import { NativeScrollEvent, NativeSyntheticEvent, Pressable, ScrollView, StyleProp, TextInput, View, ViewStyle, } from 'react-native' import { KeyboardAwareScrollView, useKeyboardHandler, } from 'react-native-keyboard-controller' import {runOnJS} from 'react-native-reanimated' import {ReanimatedScrollEvent} from 'react-native-reanimated/lib/typescript/hook/commonTypes' import {useSafeAreaInsets} from 'react-native-safe-area-context' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useEnableKeyboardController} from '#/lib/hooks/useEnableKeyboardController' import {ScrollProvider} from '#/lib/ScrollContext' import {logger} from '#/logger' import {isAndroid, isIOS} from '#/platform/detection' import {useA11y} from '#/state/a11y' import {useDialogStateControlContext} from '#/state/dialogs' import {List, ListMethods, ListProps} from '#/view/com/util/List' import {atoms as a, tokens, useTheme} from '#/alf' import {useThemeName} from '#/alf/util/useColorModeTheme' import {Context, useDialogContext} from '#/components/Dialog/context' import { DialogControlProps, DialogInnerProps, DialogOuterProps, } from '#/components/Dialog/types' import {createInput} from '#/components/forms/TextField' import {BottomSheet, BottomSheetSnapPoint} from '../../../modules/bottom-sheet' import { BottomSheetSnapPointChangeEvent, BottomSheetStateChangeEvent, } from '../../../modules/bottom-sheet/src/BottomSheet.types' import {BottomSheetNativeComponent} from '../../../modules/bottom-sheet/src/BottomSheetNativeComponent' export {useDialogContext, useDialogControl} from '#/components/Dialog/context' export * from '#/components/Dialog/shared' export * from '#/components/Dialog/types' export * from '#/components/Dialog/utils' export const Input = createInput(TextInput) export function Outer({ children, control, onClose, nativeOptions, testID, }: React.PropsWithChildren) { const themeName = useThemeName() const t = useTheme(themeName) const ref = React.useRef(null) const closeCallbacks = React.useRef<(() => void)[]>([]) const {setDialogIsOpen, setFullyExpandedCount} = useDialogStateControlContext() const prevSnapPoint = React.useRef( BottomSheetSnapPoint.Hidden, ) const [disableDrag, setDisableDrag] = React.useState(false) const [snapPoint, setSnapPoint] = React.useState( BottomSheetSnapPoint.Partial, ) const callQueuedCallbacks = React.useCallback(() => { for (const cb of closeCallbacks.current) { try { cb() } catch (e: any) { logger.error(e || 'Error running close callback') } } closeCallbacks.current = [] }, []) const open = React.useCallback(() => { // Run any leftover callbacks that might have been queued up before calling `.open()` callQueuedCallbacks() setDialogIsOpen(control.id, true) ref.current?.present() }, [setDialogIsOpen, control.id, callQueuedCallbacks]) // This is the function that we call when we want to dismiss the dialog. const close = React.useCallback(cb => { if (typeof cb === 'function') { closeCallbacks.current.push(cb) } ref.current?.dismiss() }, []) // This is the actual thing we are doing once we "confirm" the dialog. We want the dialog's close animation to // happen before we run this. It is passed to the `BottomSheet` component. const onCloseAnimationComplete = React.useCallback(() => { // This removes the dialog from our list of stored dialogs. Not super necessary on iOS, but on Android this // tells us that we need to toggle the accessibility overlay setting setDialogIsOpen(control.id, false) callQueuedCallbacks() onClose?.() }, [callQueuedCallbacks, control.id, onClose, setDialogIsOpen]) const onSnapPointChange = (e: BottomSheetSnapPointChangeEvent) => { const {snapPoint} = e.nativeEvent setSnapPoint(snapPoint) if ( snapPoint === BottomSheetSnapPoint.Full && prevSnapPoint.current !== BottomSheetSnapPoint.Full ) { setFullyExpandedCount(c => c + 1) } else if ( snapPoint !== BottomSheetSnapPoint.Full && prevSnapPoint.current === BottomSheetSnapPoint.Full ) { setFullyExpandedCount(c => c - 1) } prevSnapPoint.current = snapPoint } const onStateChange = (e: BottomSheetStateChangeEvent) => { if (e.nativeEvent.state === 'closed') { onCloseAnimationComplete() if (prevSnapPoint.current === BottomSheetSnapPoint.Full) { setFullyExpandedCount(c => c - 1) } prevSnapPoint.current = BottomSheetSnapPoint.Hidden } } useImperativeHandle( control.ref, () => ({ open, close, }), [open, close], ) const context = React.useMemo( () => ({ close, isNativeDialog: true, nativeSnapPoint: snapPoint, disableDrag, setDisableDrag, }), [close, snapPoint, disableDrag, setDisableDrag], ) return ( {children} ) } export function Inner({children, style, header}: DialogInnerProps) { const insets = useSafeAreaInsets() return ( <> {header} {children} ) } export const ScrollableInner = React.forwardRef( function ScrollableInner( {children, contentContainerStyle, header, ...props}, ref, ) { const {nativeSnapPoint, disableDrag, setDisableDrag} = useDialogContext() const insets = useSafeAreaInsets() useEnableKeyboardController(isIOS) const [keyboardHeight, setKeyboardHeight] = React.useState(0) useKeyboardHandler( { onEnd: e => { 'worklet' runOnJS(setKeyboardHeight)(e.height) }, }, [], ) let paddingBottom = 0 if (isIOS) { paddingBottom += keyboardHeight / 4 if (nativeSnapPoint === BottomSheetSnapPoint.Full) { paddingBottom += insets.bottom + tokens.space.md } paddingBottom = Math.max(paddingBottom, tokens.space._2xl) } else { paddingBottom += keyboardHeight if (nativeSnapPoint === BottomSheetSnapPoint.Full) { paddingBottom += insets.top } paddingBottom += Math.max(insets.bottom, tokens.space._5xl) + tokens.space._2xl } const onScroll = (e: NativeSyntheticEvent) => { if (!isAndroid) { return } const {contentOffset} = e.nativeEvent if (contentOffset.y > 0 && !disableDrag) { setDisableDrag(true) } else if (contentOffset.y <= 1 && disableDrag) { setDisableDrag(false) } } return ( {header} {children} ) }, ) export const InnerFlatList = React.forwardRef< ListMethods, ListProps & { webInnerStyle?: StyleProp webInnerContentContainerStyle?: StyleProp } >(function InnerFlatList({style, ...props}, ref) { const insets = useSafeAreaInsets() const {nativeSnapPoint, disableDrag, setDisableDrag} = useDialogContext() const onScroll = (e: ReanimatedScrollEvent) => { 'worklet' if (!isAndroid) { return } const {contentOffset} = e if (contentOffset.y > 0 && !disableDrag) { runOnJS(setDisableDrag)(true) } else if (contentOffset.y <= 1 && disableDrag) { runOnJS(setDisableDrag)(false) } } return ( } ref={ref} {...props} style={[style]} /> ) }) export function Handle() { const t = useTheme() const {_} = useLingui() const {screenReaderEnabled} = useA11y() const {close} = useDialogContext() return ( close()} accessibilityLabel={_(msg`Dismiss`)} accessibilityHint={_(msg`Double tap to close the dialog`)}> ) } export function Close() { return null }