import * as React from 'react'; import { createContext, useEffect, useMemo, useRef, useState } from 'react'; import { Alert, Platform, StyleSheet } from 'react-native'; import { Button, Dialog, Portal, Text } from 'react-native-paper'; import Modal from './Modal'; import { _ } from '@joplin/lib/locale'; import shim from '@joplin/lib/shim'; import makeShowMessageBox from '../utils/makeShowMessageBox'; export interface PromptButton { text: string; onPress?: ()=> void; style?: 'cancel'|'default'|'destructive'; } interface PromptOptions { cancelable?: boolean; } export interface DialogControl { prompt(title: string, message: string, buttons?: PromptButton[], options?: PromptOptions): void; } export const DialogContext = createContext<DialogControl>(null); interface Props { children: React.ReactNode; } interface PromptDialogData { key: string; title: string; message: string; buttons: PromptButton[]; onDismiss: (()=> void)|null; } const styles = StyleSheet.create({ dialogContainer: { maxWidth: 400, minWidth: '50%', alignSelf: 'center', }, modalContainer: { marginTop: 'auto', marginBottom: 'auto', }, }); const DialogManager: React.FC<Props> = props => { const [dialogModels, setPromptDialogs] = useState<PromptDialogData[]>([]); const nextDialogIdRef = useRef(0); const dialogControl: DialogControl = useMemo(() => { const defaultButtons = [{ text: _('OK') }]; return { prompt: (title: string, message: string, buttons: PromptButton[] = defaultButtons, options?: PromptOptions) => { if (Platform.OS !== 'web') { // Alert.alert provides a more native style on iOS. Alert.alert(title, message, buttons, options); // Alert.alert doesn't work on web. } else { const onDismiss = () => { setPromptDialogs(dialogs => dialogs.filter(d => d !== dialog)); }; const cancelable = options?.cancelable ?? true; const dialog: PromptDialogData = { key: `dialog-${nextDialogIdRef.current++}`, title, message, buttons: buttons.map(button => ({ ...button, onPress: () => { onDismiss(); button.onPress?.(); }, })), onDismiss: cancelable ? onDismiss : null, }; setPromptDialogs(dialogs => { return [ ...dialogs, dialog, ]; }); } }, }; }, []); const dialogControlRef = useRef(dialogControl); dialogControlRef.current = dialogControl; useEffect(() => { shim.showMessageBox = makeShowMessageBox(dialogControlRef); return () => { dialogControlRef.current = null; }; }, []); const dialogComponents: React.ReactNode[] = []; for (const dialog of dialogModels) { const buttons = dialog.buttons.map((button, index) => { return ( <Button key={`${index}-${button.text}`} onPress={button.onPress}>{button.text}</Button> ); }); dialogComponents.push( <Dialog testID={'prompt-dialog'} style={styles.dialogContainer} key={dialog.key} visible={true} onDismiss={dialog.onDismiss} > <Dialog.Title>{dialog.title}</Dialog.Title> <Dialog.Content> <Text variant='bodyMedium'>{dialog.message}</Text> </Dialog.Content> <Dialog.Actions> {buttons} </Dialog.Actions> </Dialog>, ); } // Web: Use a <Modal> wrapper for better keyboard focus handling. return <> <DialogContext.Provider value={dialogControl}> {props.children} </DialogContext.Provider> <Portal> <Modal visible={!!dialogComponents.length} containerStyle={styles.modalContainer} animationType='none' backgroundColor='rgba(0, 0, 0, 0.1)' transparent={true} onRequestClose={dialogModels[dialogComponents.length - 1]?.onDismiss} > {dialogComponents} </Modal> </Portal> </>; }; export default DialogManager;