import { forwardRef, useCallback, useImperativeHandle, useMemo, useRef, useState, } from 'react' import { Keyboard, type KeyboardEventListener, type LayoutChangeEvent, Modal, type NativeScrollEvent, type NativeSyntheticEvent, Pressable, ScrollView, type StyleProp, TextInput, View, type ViewStyle, } from 'react-native' import {useReanimatedKeyboardAnimation} from 'react-native-keyboard-controller' import Animated, { runOnJS, type ScrollEvent, useAnimatedStyle, } from 'react-native-reanimated' import {useSafeAreaInsets} from 'react-native-safe-area-context' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {ScrollProvider} from '#/lib/ScrollContext' import {useA11y} from '#/state/a11y' import {List, type ListMethods, type ListProps} from '#/view/com/util/List' import {android, atoms as a, ios, platform, tokens, useTheme} from '#/alf' import {useThemeName} from '#/alf/util/useColorModeTheme' import {Context, useDialogContext} from '#/components/Dialog/context' import { type DialogControlProps, type DialogInnerProps, type DialogOuterProps, } from '#/components/Dialog/types' import {useDialogCallbackQueue} from '#/components/Dialog/utils' import {createInput} from '#/components/forms/TextField' import {useOnKeyboard} from '#/components/hooks/useOnKeyboard' import {IS_ANDROID, IS_IOS, IS_LIQUID_GLASS} from '#/env' import {BottomSheet, BottomSheetSnapPoint} from '../../../modules/bottom-sheet' import { type BottomSheetSnapPointChangeEvent, type BottomSheetStateChangeEvent, } from '../../../modules/bottom-sheet/src/BottomSheet.types' import {type 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(props: React.PropsWithChildren) { if (props.type === 'alert') { return } return } function OuterAlert({ children, control, onClose, testID, }: React.PropsWithChildren) { const t = useTheme() const [isOpen, setIsOpen] = useState(false) const {enqueueCallback, handleOpen, handleClose} = useDialogCallbackQueue( control, onClose, ) const open: DialogControlProps['open'] = () => { handleOpen() setIsOpen(true) } const close: DialogControlProps['close'] = cb => { enqueueCallback(cb) setIsOpen(false) } const onDismiss = () => { handleClose() } useImperativeHandle(control.ref, () => ({open, close}), [open, close]) const context = useMemo( () => ({ close, isNativeDialog: true, nativeSnapPoint: BottomSheetSnapPoint.Hidden, disableDrag: false, setDisableDrag: () => {}, isWithinDialog: true, type: 'alert' as const, }), [close], ) return ( close()} onDismiss={onDismiss}> close()} style={[ a.flex_1, a.justify_center, a.align_center, a.px_xl, {backgroundColor: 'rgba(0,0,0,0.5)'}, ]}> e.stopPropagation()} style={[ t.atoms.bg, a.w_full, {borderRadius: 48, maxWidth: 320, maxHeight: '90%'}, a.overflow_hidden, ]}> {children} ) } function OuterSheet({ children, control, onClose, nativeOptions, testID, }: React.PropsWithChildren) { const themeName = useThemeName() const t = useTheme(themeName) const ref = useRef(null) const {enqueueCallback, handleOpen, handleClose, setFullyExpandedCount} = useDialogCallbackQueue(control, onClose) const prevSnapPoint = useRef( BottomSheetSnapPoint.Hidden, ) const [disableDrag, setDisableDrag] = useState(false) const [snapPoint, setSnapPoint] = useState( BottomSheetSnapPoint.Partial, ) const open: DialogControlProps['open'] = () => { handleOpen() ref.current?.present() } const close: DialogControlProps['close'] = cb => { enqueueCallback(cb) ref.current?.dismiss() } // Runs after the bottom sheet close animation completes. const onCloseAnimationComplete = () => { handleClose() } 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 = useMemo( () => ({ close, isNativeDialog: true, nativeSnapPoint: snapPoint, disableDrag, setDisableDrag, isWithinDialog: true, type: 'sheet' as const, }), [close, snapPoint, disableDrag, setDisableDrag], ) return ( {children} ) } /** * @deprecated use `Dialog.ScrollableInner` instead */ export function Inner({children, style, header}: DialogInnerProps) { const insets = useSafeAreaInsets() return ( <> {header} {children} ) } export const ScrollableInner = forwardRef( function ScrollableInner( {children, contentContainerStyle, header, ...props}, ref, ) { const {nativeSnapPoint, disableDrag, setDisableDrag, type} = useDialogContext() if (type === 'alert') { return ( {header} {children} ) } const isAtMaxSnapPoint = nativeSnapPoint === BottomSheetSnapPoint.Full const insets = useSafeAreaInsets() const [keyboardHeight, setKeyboardHeight] = useState(() => IS_ANDROID ? (Keyboard.metrics()?.height ?? 0) : 0, ) const keyboardEventHandler = useCallback(e => { setKeyboardHeight(e.endCoordinates.height) }, []) useOnKeyboard('keyboardDidShow', keyboardEventHandler) useOnKeyboard('keyboardDidHide', keyboardEventHandler) const onScroll = (e: NativeSyntheticEvent) => { if (!IS_ANDROID) { 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 = forwardRef< ListMethods, ListProps & { webInnerStyle?: StyleProp webInnerContentContainerStyle?: StyleProp footer?: React.ReactNode } >(function InnerFlatList( {headerOffset, footer, style, contentContainerStyle, ...props}, ref, ) { const insets = useSafeAreaInsets() const {nativeSnapPoint, disableDrag, setDisableDrag} = useDialogContext() const isAtMaxSnapPoint = nativeSnapPoint === BottomSheetSnapPoint.Full const onScroll = (e: ScrollEvent) => { 'worklet' if (!IS_ANDROID) { return } const {contentOffset} = e if (contentOffset.y > 0 && !disableDrag) { runOnJS(setDisableDrag)(true) } else if (contentOffset.y <= 1 && disableDrag) { runOnJS(setDisableDrag)(false) } } return ( {footer} ) }) export function FlatListFooter({ children, onLayout, }: { children: React.ReactNode onLayout?: (event: LayoutChangeEvent) => void }) { const t = useTheme() const {bottom} = useSafeAreaInsets() const {height} = useReanimatedKeyboardAnimation() const animatedStyle = useAnimatedStyle(() => { if (!IS_IOS) return {} return { transform: [{translateY: Math.min(0, height.get() + bottom - 10)}], } }) return ( {children} ) } export function Handle({ difference = false, fill, }: { difference?: boolean fill?: string }) { 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 } export function Backdrop() { return null }