You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	Mobile: Accessibility: Improve dialog accessibility (#11395)
This commit is contained in:
		| @@ -575,7 +575,6 @@ packages/app-mobile/commands/openNote.js | |||||||
| packages/app-mobile/commands/scrollToHash.js | packages/app-mobile/commands/scrollToHash.js | ||||||
| packages/app-mobile/commands/util/goToNote.js | packages/app-mobile/commands/util/goToNote.js | ||||||
| packages/app-mobile/commands/util/showResource.js | packages/app-mobile/commands/util/showResource.js | ||||||
| packages/app-mobile/components/BackButtonDialogBox.js |  | ||||||
| packages/app-mobile/components/BetaChip.js | packages/app-mobile/components/BetaChip.js | ||||||
| packages/app-mobile/components/CameraView/ActionButtons.js | packages/app-mobile/components/CameraView/ActionButtons.js | ||||||
| packages/app-mobile/components/CameraView/Camera/index.jest.js | packages/app-mobile/components/CameraView/Camera/index.jest.js | ||||||
| @@ -588,7 +587,10 @@ packages/app-mobile/components/CameraView/types.js | |||||||
| packages/app-mobile/components/CameraView/utils/fitRectIntoBounds.js | packages/app-mobile/components/CameraView/utils/fitRectIntoBounds.js | ||||||
| packages/app-mobile/components/CameraView/utils/useBarcodeScanner.js | packages/app-mobile/components/CameraView/utils/useBarcodeScanner.js | ||||||
| packages/app-mobile/components/Checkbox.js | packages/app-mobile/components/Checkbox.js | ||||||
| packages/app-mobile/components/DialogManager.js | packages/app-mobile/components/DialogManager/PromptDialog.js | ||||||
|  | packages/app-mobile/components/DialogManager/hooks/useDialogControl.js | ||||||
|  | packages/app-mobile/components/DialogManager/index.js | ||||||
|  | packages/app-mobile/components/DialogManager/types.js | ||||||
| packages/app-mobile/components/DismissibleDialog.js | packages/app-mobile/components/DismissibleDialog.js | ||||||
| packages/app-mobile/components/Dropdown.test.js | packages/app-mobile/components/Dropdown.test.js | ||||||
| packages/app-mobile/components/Dropdown.js | packages/app-mobile/components/Dropdown.js | ||||||
| @@ -760,6 +762,7 @@ packages/app-mobile/components/screens/ShareManager/IncomingShareItem.js | |||||||
| packages/app-mobile/components/screens/ShareManager/index.test.js | packages/app-mobile/components/screens/ShareManager/index.test.js | ||||||
| packages/app-mobile/components/screens/ShareManager/index.js | packages/app-mobile/components/screens/ShareManager/index.js | ||||||
| packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js | packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js | ||||||
|  | packages/app-mobile/components/screens/dropbox-login.js | ||||||
| packages/app-mobile/components/screens/encryption-config.js | packages/app-mobile/components/screens/encryption-config.js | ||||||
| packages/app-mobile/components/screens/status.js | packages/app-mobile/components/screens/status.js | ||||||
| packages/app-mobile/components/side-menu-content.js | packages/app-mobile/components/side-menu-content.js | ||||||
|   | |||||||
							
								
								
									
										7
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -552,7 +552,6 @@ packages/app-mobile/commands/openNote.js | |||||||
| packages/app-mobile/commands/scrollToHash.js | packages/app-mobile/commands/scrollToHash.js | ||||||
| packages/app-mobile/commands/util/goToNote.js | packages/app-mobile/commands/util/goToNote.js | ||||||
| packages/app-mobile/commands/util/showResource.js | packages/app-mobile/commands/util/showResource.js | ||||||
| packages/app-mobile/components/BackButtonDialogBox.js |  | ||||||
| packages/app-mobile/components/BetaChip.js | packages/app-mobile/components/BetaChip.js | ||||||
| packages/app-mobile/components/CameraView/ActionButtons.js | packages/app-mobile/components/CameraView/ActionButtons.js | ||||||
| packages/app-mobile/components/CameraView/Camera/index.jest.js | packages/app-mobile/components/CameraView/Camera/index.jest.js | ||||||
| @@ -565,7 +564,10 @@ packages/app-mobile/components/CameraView/types.js | |||||||
| packages/app-mobile/components/CameraView/utils/fitRectIntoBounds.js | packages/app-mobile/components/CameraView/utils/fitRectIntoBounds.js | ||||||
| packages/app-mobile/components/CameraView/utils/useBarcodeScanner.js | packages/app-mobile/components/CameraView/utils/useBarcodeScanner.js | ||||||
| packages/app-mobile/components/Checkbox.js | packages/app-mobile/components/Checkbox.js | ||||||
| packages/app-mobile/components/DialogManager.js | packages/app-mobile/components/DialogManager/PromptDialog.js | ||||||
|  | packages/app-mobile/components/DialogManager/hooks/useDialogControl.js | ||||||
|  | packages/app-mobile/components/DialogManager/index.js | ||||||
|  | packages/app-mobile/components/DialogManager/types.js | ||||||
| packages/app-mobile/components/DismissibleDialog.js | packages/app-mobile/components/DismissibleDialog.js | ||||||
| packages/app-mobile/components/Dropdown.test.js | packages/app-mobile/components/Dropdown.test.js | ||||||
| packages/app-mobile/components/Dropdown.js | packages/app-mobile/components/Dropdown.js | ||||||
| @@ -737,6 +739,7 @@ packages/app-mobile/components/screens/ShareManager/IncomingShareItem.js | |||||||
| packages/app-mobile/components/screens/ShareManager/index.test.js | packages/app-mobile/components/screens/ShareManager/index.test.js | ||||||
| packages/app-mobile/components/screens/ShareManager/index.js | packages/app-mobile/components/screens/ShareManager/index.js | ||||||
| packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js | packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js | ||||||
|  | packages/app-mobile/components/screens/dropbox-login.js | ||||||
| packages/app-mobile/components/screens/encryption-config.js | packages/app-mobile/components/screens/encryption-config.js | ||||||
| packages/app-mobile/components/screens/status.js | packages/app-mobile/components/screens/status.js | ||||||
| packages/app-mobile/components/side-menu-content.js | packages/app-mobile/components/side-menu-content.js | ||||||
|   | |||||||
| @@ -42,7 +42,7 @@ export const runtime = (): CommandRuntime => { | |||||||
| 			} else { | 			} else { | ||||||
| 				const errorMessage = _('Unsupported link or message: %s', link); | 				const errorMessage = _('Unsupported link or message: %s', link); | ||||||
| 				logger.error(errorMessage); | 				logger.error(errorMessage); | ||||||
| 				await shim.showMessageBox(errorMessage); | 				await shim.showErrorDialog(errorMessage); | ||||||
| 			} | 			} | ||||||
| 		}, | 		}, | ||||||
| 	}; | 	}; | ||||||
|   | |||||||
| @@ -1,25 +0,0 @@ | |||||||
| import BackButtonService from '../services/BackButtonService'; |  | ||||||
| const DialogBox = require('react-native-dialogbox').default; |  | ||||||
|  |  | ||||||
| export default class BackButtonDialogBox extends DialogBox { |  | ||||||
| 	// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied |  | ||||||
| 	public constructor(props: any) { |  | ||||||
| 		super(props); |  | ||||||
|  |  | ||||||
| 		this.backHandler_ = () => { |  | ||||||
| 			if (this.state.isVisible) { |  | ||||||
| 				this.close(); |  | ||||||
| 				return true; |  | ||||||
| 			} |  | ||||||
| 			return false; |  | ||||||
| 		}; |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	public async componentDidUpdate() { |  | ||||||
| 		if (this.state.isVisible) { |  | ||||||
| 			BackButtonService.addHandler(this.backHandler_); |  | ||||||
| 		} else { |  | ||||||
| 			BackButtonService.removeHandler(this.backHandler_); |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| @@ -1,150 +0,0 @@ | |||||||
| 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; |  | ||||||
| @@ -0,0 +1,93 @@ | |||||||
|  | import * as React from 'react'; | ||||||
|  | import { Button, Dialog, Divider, Surface, Text } from 'react-native-paper'; | ||||||
|  | import { DialogType, PromptDialogData } from './types'; | ||||||
|  | import { StyleSheet } from 'react-native'; | ||||||
|  | import { useMemo } from 'react'; | ||||||
|  | import { themeStyle } from '../global-style'; | ||||||
|  |  | ||||||
|  | interface Props { | ||||||
|  | 	dialog: PromptDialogData; | ||||||
|  | 	themeId: number; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const useStyles = (themeId: number, isMenu: boolean) => { | ||||||
|  | 	return useMemo(() => { | ||||||
|  | 		const theme = themeStyle(themeId); | ||||||
|  |  | ||||||
|  | 		return StyleSheet.create({ | ||||||
|  | 			dialogContainer: { | ||||||
|  | 				backgroundColor: theme.backgroundColor, | ||||||
|  | 				borderRadius: 24, | ||||||
|  | 				paddingTop: 24, | ||||||
|  | 				marginLeft: 4, | ||||||
|  | 				marginRight: 4, | ||||||
|  | 			}, | ||||||
|  |  | ||||||
|  | 			buttonScrollerContent: { | ||||||
|  | 				flexDirection: 'row', | ||||||
|  | 				justifyContent: 'flex-end', | ||||||
|  | 				flexWrap: 'wrap', | ||||||
|  | 			}, | ||||||
|  |  | ||||||
|  | 			dialogContent: { | ||||||
|  | 				paddingBottom: 14, | ||||||
|  | 			}, | ||||||
|  | 			dialogActions: { | ||||||
|  | 				paddingBottom: 14, | ||||||
|  | 				paddingTop: 4, | ||||||
|  |  | ||||||
|  | 				...(isMenu ? { | ||||||
|  | 					flexDirection: 'column', | ||||||
|  | 					alignItems: 'stretch', | ||||||
|  | 				} : {}), | ||||||
|  | 			}, | ||||||
|  | 			dialogLabel: { | ||||||
|  | 				textAlign: isMenu ? 'center' : undefined, | ||||||
|  | 			}, | ||||||
|  | 		}); | ||||||
|  | 	}, [themeId, isMenu]); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const PromptDialog: React.FC<Props> = ({ dialog, themeId }) => { | ||||||
|  | 	const isMenu = dialog.type === DialogType.Menu; | ||||||
|  | 	const styles = useStyles(themeId, isMenu); | ||||||
|  |  | ||||||
|  | 	const buttons = dialog.buttons.map((button, index) => { | ||||||
|  | 		return ( | ||||||
|  | 			<Button | ||||||
|  | 				key={`${index}-${button.text}`} | ||||||
|  | 				onPress={button.onPress} | ||||||
|  | 			>{button.text}</Button> | ||||||
|  | 		); | ||||||
|  | 	}); | ||||||
|  | 	const titleComponent = <Text | ||||||
|  | 		variant='titleMedium' | ||||||
|  | 		accessibilityRole='header' | ||||||
|  | 		style={styles.dialogLabel} | ||||||
|  | 	>{dialog.title}</Text>; | ||||||
|  |  | ||||||
|  | 	return ( | ||||||
|  | 		<Surface | ||||||
|  | 			testID={'prompt-dialog'} | ||||||
|  | 			style={styles.dialogContainer} | ||||||
|  | 			key={dialog.key} | ||||||
|  | 			elevation={1} | ||||||
|  | 		> | ||||||
|  | 			<Dialog.Content style={styles.dialogContent}> | ||||||
|  | 				{dialog.title ? titleComponent : null} | ||||||
|  | 				<Text | ||||||
|  | 					variant='bodyMedium' | ||||||
|  | 					style={styles.dialogLabel} | ||||||
|  | 				>{dialog.message}</Text> | ||||||
|  | 			</Dialog.Content> | ||||||
|  | 			{isMenu ? <Divider/> : null} | ||||||
|  | 			<Dialog.Actions | ||||||
|  | 				style={styles.dialogActions} | ||||||
|  | 			> | ||||||
|  | 				{buttons} | ||||||
|  | 			</Dialog.Actions> | ||||||
|  | 		</Surface> | ||||||
|  | 	); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default PromptDialog; | ||||||
| @@ -0,0 +1,98 @@ | |||||||
|  | import * as React from 'react'; | ||||||
|  | import { Alert, Platform } from 'react-native'; | ||||||
|  | import { DialogControl, DialogType, MenuChoice, PromptButton, PromptDialogData, PromptOptions } from '../types'; | ||||||
|  | import { _ } from '@joplin/lib/locale'; | ||||||
|  | import { useMemo, useRef } from 'react'; | ||||||
|  |  | ||||||
|  | type SetPromptDialogs = React.Dispatch<React.SetStateAction<PromptDialogData[]>>; | ||||||
|  |  | ||||||
|  | const useDialogControl = (setPromptDialogs: SetPromptDialogs) => { | ||||||
|  | 	const nextDialogIdRef = useRef(0); | ||||||
|  |  | ||||||
|  | 	const dialogControl: DialogControl = useMemo(() => { | ||||||
|  | 		const onDismiss = (dialog: PromptDialogData) => { | ||||||
|  | 			setPromptDialogs(dialogs => dialogs.filter(d => d !== dialog)); | ||||||
|  | 		}; | ||||||
|  |  | ||||||
|  | 		const defaultButtons = [{ text: _('OK') }]; | ||||||
|  | 		const control: DialogControl = { | ||||||
|  | 			info: (message: string) => { | ||||||
|  | 				return new Promise<void>((resolve) => { | ||||||
|  | 					control.prompt(_('Info'), message, [{ | ||||||
|  | 						text: _('OK'), | ||||||
|  | 						onPress: () => resolve(), | ||||||
|  | 					}]); | ||||||
|  | 				}); | ||||||
|  | 			}, | ||||||
|  | 			error: (message: string) => { | ||||||
|  | 				return new Promise<void>((resolve) => { | ||||||
|  | 					control.prompt(_('Error'), message, [{ | ||||||
|  | 						text: _('OK'), | ||||||
|  | 						onPress: () => resolve(), | ||||||
|  | 					}]); | ||||||
|  | 				}); | ||||||
|  | 			}, | ||||||
|  | 			prompt: (title: string, message: string, buttons: PromptButton[] = defaultButtons, options?: PromptOptions) => { | ||||||
|  | 				// Alert.alert doesn't work on web. | ||||||
|  | 				if (Platform.OS !== 'web') { | ||||||
|  | 					// Note: Alert.alert provides a more native style on iOS. | ||||||
|  | 					Alert.alert(title, message, buttons, options); | ||||||
|  | 				} else { | ||||||
|  | 					const cancelable = options?.cancelable ?? true; | ||||||
|  | 					const dialog: PromptDialogData = { | ||||||
|  | 						type: DialogType.Prompt, | ||||||
|  | 						key: `dialog-${nextDialogIdRef.current++}`, | ||||||
|  | 						title, | ||||||
|  | 						message, | ||||||
|  | 						buttons: buttons.map(button => ({ | ||||||
|  | 							...button, | ||||||
|  | 							onPress: () => { | ||||||
|  | 								onDismiss(dialog); | ||||||
|  | 								button.onPress?.(); | ||||||
|  | 							}, | ||||||
|  | 						})), | ||||||
|  | 						onDismiss: cancelable ? () => onDismiss(dialog) : null, | ||||||
|  | 					}; | ||||||
|  |  | ||||||
|  | 					setPromptDialogs(dialogs => { | ||||||
|  | 						return [ | ||||||
|  | 							...dialogs, | ||||||
|  | 							dialog, | ||||||
|  | 						]; | ||||||
|  | 					}); | ||||||
|  | 				} | ||||||
|  | 			}, | ||||||
|  | 			showMenu: function<T>(title: string, choices: MenuChoice<T>[]) { | ||||||
|  | 				return new Promise<T>((resolve) => { | ||||||
|  | 					const dismiss = () => onDismiss(dialog); | ||||||
|  |  | ||||||
|  | 					const dialog: PromptDialogData = { | ||||||
|  | 						type: DialogType.Menu, | ||||||
|  | 						key: `menu-dialog-${nextDialogIdRef.current++}`, | ||||||
|  | 						title: '', | ||||||
|  | 						message: title, | ||||||
|  | 						buttons: choices.map(choice => ({ | ||||||
|  | 							text: choice.text, | ||||||
|  | 							onPress: () => { | ||||||
|  | 								dismiss(); | ||||||
|  | 								resolve(choice.id); | ||||||
|  | 							}, | ||||||
|  | 						})), | ||||||
|  | 						onDismiss: dismiss, | ||||||
|  | 					}; | ||||||
|  | 					setPromptDialogs(dialogs => { | ||||||
|  | 						return [ | ||||||
|  | 							...dialogs, | ||||||
|  | 							dialog, | ||||||
|  | 						]; | ||||||
|  | 					}); | ||||||
|  | 				}); | ||||||
|  | 			}, | ||||||
|  | 		}; | ||||||
|  |  | ||||||
|  | 		return control; | ||||||
|  | 	}, [setPromptDialogs]); | ||||||
|  | 	return dialogControl; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default useDialogControl; | ||||||
							
								
								
									
										86
									
								
								packages/app-mobile/components/DialogManager/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								packages/app-mobile/components/DialogManager/index.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,86 @@ | |||||||
|  | import * as React from 'react'; | ||||||
|  | import { createContext, useEffect, useMemo, useRef, useState } from 'react'; | ||||||
|  | import { StyleSheet, useWindowDimensions } from 'react-native'; | ||||||
|  | import { Portal } from 'react-native-paper'; | ||||||
|  | import Modal from '../Modal'; | ||||||
|  | import shim from '@joplin/lib/shim'; | ||||||
|  | import makeShowMessageBox from '../../utils/makeShowMessageBox'; | ||||||
|  | import { DialogControl, PromptDialogData } from './types'; | ||||||
|  | import useDialogControl from './hooks/useDialogControl'; | ||||||
|  | import PromptDialog from './PromptDialog'; | ||||||
|  |  | ||||||
|  | export type { DialogControl } from './types'; | ||||||
|  | export const DialogContext = createContext<DialogControl>(null); | ||||||
|  |  | ||||||
|  | interface Props { | ||||||
|  | 	themeId: number; | ||||||
|  | 	children: React.ReactNode; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const useStyles = () => { | ||||||
|  | 	const windowSize = useWindowDimensions(); | ||||||
|  |  | ||||||
|  | 	return useMemo(() => { | ||||||
|  | 		return StyleSheet.create({ | ||||||
|  | 			modalContainer: { | ||||||
|  | 				marginLeft: 'auto', | ||||||
|  | 				marginRight: 'auto', | ||||||
|  | 				marginTop: 'auto', | ||||||
|  | 				marginBottom: 'auto', | ||||||
|  | 				width: Math.max(windowSize.width / 2, 400), | ||||||
|  | 				maxWidth: '100%', | ||||||
|  | 			}, | ||||||
|  | 		}); | ||||||
|  | 	}, [windowSize.width]); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const DialogManager: React.FC<Props> = props => { | ||||||
|  | 	const [dialogModels, setPromptDialogs] = useState<PromptDialogData[]>([]); | ||||||
|  |  | ||||||
|  | 	const dialogControl = useDialogControl(setPromptDialogs); | ||||||
|  | 	const dialogControlRef = useRef(dialogControl); | ||||||
|  | 	dialogControlRef.current = dialogControl; | ||||||
|  |  | ||||||
|  | 	useEffect(() => { | ||||||
|  | 		shim.showMessageBox = makeShowMessageBox(dialogControlRef); | ||||||
|  |  | ||||||
|  | 		return () => { | ||||||
|  | 			dialogControlRef.current = null; | ||||||
|  | 		}; | ||||||
|  | 	}, []); | ||||||
|  |  | ||||||
|  | 	const styles = useStyles(); | ||||||
|  |  | ||||||
|  | 	const dialogComponents: React.ReactNode[] = []; | ||||||
|  | 	for (const dialog of dialogModels) { | ||||||
|  | 		dialogComponents.push( | ||||||
|  | 			<PromptDialog | ||||||
|  | 				key={dialog.key} | ||||||
|  | 				dialog={dialog} | ||||||
|  | 				themeId={props.themeId} | ||||||
|  | 			/>, | ||||||
|  | 		); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Web: Use a <Modal> wrapper for better keyboard focus handling. | ||||||
|  | 	return <> | ||||||
|  | 		<DialogContext.Provider value={dialogControl}> | ||||||
|  | 			{props.children} | ||||||
|  | 		</DialogContext.Provider> | ||||||
|  | 		<Portal> | ||||||
|  | 			<Modal | ||||||
|  | 				visible={!!dialogComponents.length} | ||||||
|  | 				scrollOverflow={true} | ||||||
|  | 				containerStyle={styles.modalContainer} | ||||||
|  | 				animationType='fade' | ||||||
|  | 				backgroundColor='rgba(0, 0, 0, 0.1)' | ||||||
|  | 				transparent={true} | ||||||
|  | 				onRequestClose={dialogModels[dialogComponents.length - 1]?.onDismiss} | ||||||
|  | 			> | ||||||
|  | 				{dialogComponents} | ||||||
|  | 			</Modal> | ||||||
|  | 		</Portal> | ||||||
|  | 	</>; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default DialogManager; | ||||||
							
								
								
									
										37
									
								
								packages/app-mobile/components/DialogManager/types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								packages/app-mobile/components/DialogManager/types.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | |||||||
|  |  | ||||||
|  | export interface PromptButton { | ||||||
|  | 	text: string; | ||||||
|  | 	onPress?: ()=> void; | ||||||
|  | 	style?: 'cancel'|'default'|'destructive'; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface PromptOptions { | ||||||
|  | 	cancelable?: boolean; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface MenuChoice<IdType> { | ||||||
|  | 	text: string; | ||||||
|  | 	id: IdType; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface DialogControl { | ||||||
|  | 	info(message: string): Promise<void>; | ||||||
|  | 	error(message: string): Promise<void>; | ||||||
|  | 	prompt(title: string, message: string, buttons?: PromptButton[], options?: PromptOptions): void; | ||||||
|  | 	showMenu<IdType>(title: string, choices: MenuChoice<IdType>[]): Promise<IdType>; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export enum DialogType { | ||||||
|  | 	Prompt, | ||||||
|  | 	Menu, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface PromptDialogData { | ||||||
|  | 	type: DialogType; | ||||||
|  | 	key: string; | ||||||
|  | 	title: string; | ||||||
|  | 	message: string; | ||||||
|  | 	buttons: PromptButton[]; | ||||||
|  | 	onDismiss: (()=> void)|null; | ||||||
|  | } | ||||||
|  |  | ||||||
| @@ -1,15 +1,20 @@ | |||||||
| import * as React from 'react'; | import * as React from 'react'; | ||||||
| import { RefObject, useCallback, useMemo, useRef } from 'react'; | import { RefObject, useCallback, useMemo, useRef } from 'react'; | ||||||
| import { GestureResponderEvent, Modal, ModalProps, StyleSheet, View, ViewStyle, useWindowDimensions } from 'react-native'; | import { GestureResponderEvent, Modal, ModalProps, ScrollView, StyleSheet, View, ViewStyle, useWindowDimensions } from 'react-native'; | ||||||
| import { hasNotch } from 'react-native-device-info'; | import { hasNotch } from 'react-native-device-info'; | ||||||
|  |  | ||||||
| interface ModalElementProps extends ModalProps { | interface ModalElementProps extends ModalProps { | ||||||
| 	children: React.ReactNode; | 	children: React.ReactNode; | ||||||
| 	containerStyle?: ViewStyle; | 	containerStyle?: ViewStyle; | ||||||
| 	backgroundColor?: string; | 	backgroundColor?: string; | ||||||
|  |  | ||||||
|  | 	// If scrollOverflow is provided, the modal is wrapped in a vertical | ||||||
|  | 	// ScrollView. This allows the user to scroll parts of dialogs into | ||||||
|  | 	// view that would otherwise be clipped by the screen edge. | ||||||
|  | 	scrollOverflow?: boolean; | ||||||
| } | } | ||||||
|  |  | ||||||
| const useStyles = (backgroundColor?: string) => { | const useStyles = (hasScrollView: boolean, backgroundColor: string|undefined) => { | ||||||
| 	const { width: windowWidth, height: windowHeight } = useWindowDimensions(); | 	const { width: windowWidth, height: windowHeight } = useWindowDimensions(); | ||||||
| 	const isLandscape = windowWidth > windowHeight; | 	const isLandscape = windowWidth > windowHeight; | ||||||
| 	return useMemo(() => { | 	return useMemo(() => { | ||||||
| @@ -25,12 +30,27 @@ const useStyles = (backgroundColor?: string) => { | |||||||
| 		return StyleSheet.create({ | 		return StyleSheet.create({ | ||||||
| 			modalBackground: { | 			modalBackground: { | ||||||
| 				...backgroundPadding, | 				...backgroundPadding, | ||||||
|  | 				flexGrow: 1, | ||||||
|  | 				flexShrink: 1, | ||||||
|  |  | ||||||
|  | 				// When hasScrollView, the modal background is wrapped in a ScrollView. In this case, it's | ||||||
|  | 				// possible to scroll content outside the background into view. To prevent the edge of the | ||||||
|  | 				// background from being visible, the background color is applied to the ScrollView container | ||||||
|  | 				// instead: | ||||||
|  | 				backgroundColor: hasScrollView ? null : backgroundColor, | ||||||
|  | 			}, | ||||||
|  | 			modalScrollView: { | ||||||
| 				backgroundColor, | 				backgroundColor, | ||||||
| 				flexGrow: 1, | 				flexGrow: 1, | ||||||
| 				flexShrink: 1, | 				flexShrink: 1, | ||||||
| 			}, | 			}, | ||||||
|  | 			modalScrollViewContent: { | ||||||
|  | 				// Make the scroll view's scrolling region at least as tall as its container. | ||||||
|  | 				// This makes it possible to vertically center the content of scrollable modals. | ||||||
|  | 				flexGrow: 1, | ||||||
|  | 			}, | ||||||
| 		}); | 		}); | ||||||
| 	}, [isLandscape, backgroundColor]); | 	}, [hasScrollView, isLandscape, backgroundColor]); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const useBackgroundTouchListeners = (onRequestClose: (event: GestureResponderEvent)=> void, backdropRef: RefObject<View>) => { | const useBackgroundTouchListeners = (onRequestClose: (event: GestureResponderEvent)=> void, backdropRef: RefObject<View>) => { | ||||||
| @@ -51,9 +71,10 @@ const ModalElement: React.FC<ModalElementProps> = ({ | |||||||
| 	children, | 	children, | ||||||
| 	containerStyle, | 	containerStyle, | ||||||
| 	backgroundColor, | 	backgroundColor, | ||||||
|  | 	scrollOverflow, | ||||||
| 	...modalProps | 	...modalProps | ||||||
| }) => { | }) => { | ||||||
| 	const styles = useStyles(backgroundColor); | 	const styles = useStyles(scrollOverflow, backgroundColor); | ||||||
|  |  | ||||||
| 	// contentWrapper adds padding. To allow styling the region outside of the modal | 	// contentWrapper adds padding. To allow styling the region outside of the modal | ||||||
| 	// (e.g. to add a background), the content is wrapped twice. | 	// (e.g. to add a background), the content is wrapped twice. | ||||||
| @@ -66,18 +87,25 @@ const ModalElement: React.FC<ModalElementProps> = ({ | |||||||
| 	const backgroundRef = useRef<View>(); | 	const backgroundRef = useRef<View>(); | ||||||
| 	const { onShouldBackgroundCaptureTouch, onBackgroundTouchFinished } = useBackgroundTouchListeners(modalProps.onRequestClose, backgroundRef); | 	const { onShouldBackgroundCaptureTouch, onBackgroundTouchFinished } = useBackgroundTouchListeners(modalProps.onRequestClose, backgroundRef); | ||||||
|  |  | ||||||
|  | 	const contentAndBackdrop = <View | ||||||
|  | 		ref={backgroundRef} | ||||||
|  | 		style={styles.modalBackground} | ||||||
|  | 		onStartShouldSetResponder={onShouldBackgroundCaptureTouch} | ||||||
|  | 		onResponderRelease={onBackgroundTouchFinished} | ||||||
|  | 	>{content}</View>; | ||||||
|  |  | ||||||
| 	// supportedOrientations: On iOS, this allows the dialog to be shown in non-portrait orientations. | 	// supportedOrientations: On iOS, this allows the dialog to be shown in non-portrait orientations. | ||||||
| 	return ( | 	return ( | ||||||
| 		<Modal | 		<Modal | ||||||
| 			supportedOrientations={['portrait', 'portrait-upside-down', 'landscape', 'landscape-left', 'landscape-right']} | 			supportedOrientations={['portrait', 'portrait-upside-down', 'landscape', 'landscape-left', 'landscape-right']} | ||||||
| 			{...modalProps} | 			{...modalProps} | ||||||
| 		> | 		> | ||||||
| 			<View | 			{scrollOverflow ? ( | ||||||
| 				ref={backgroundRef} | 				<ScrollView | ||||||
| 				style={styles.modalBackground} | 					style={styles.modalScrollView} | ||||||
| 				onStartShouldSetResponder={onShouldBackgroundCaptureTouch} | 					contentContainerStyle={styles.modalScrollViewContent} | ||||||
| 				onResponderRelease={onBackgroundTouchFinished} | 				>{contentAndBackdrop}</ScrollView> | ||||||
| 			>{content}</View> | 			) : contentAndBackdrop} | ||||||
| 		</Modal> | 		</Modal> | ||||||
| 	); | 	); | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -3,7 +3,6 @@ import * as React from 'react'; | |||||||
| import useOnMessage, { HandleMessageCallback, OnMarkForDownloadCallback } from './hooks/useOnMessage'; | import useOnMessage, { HandleMessageCallback, OnMarkForDownloadCallback } from './hooks/useOnMessage'; | ||||||
| import { useRef, useCallback, useState, useMemo } from 'react'; | import { useRef, useCallback, useState, useMemo } from 'react'; | ||||||
| import { View, ViewStyle } from 'react-native'; | import { View, ViewStyle } from 'react-native'; | ||||||
| import BackButtonDialogBox from '../BackButtonDialogBox'; |  | ||||||
| import ExtendedWebView from '../ExtendedWebView'; | import ExtendedWebView from '../ExtendedWebView'; | ||||||
| import { WebViewControl } from '../ExtendedWebView/types'; | import { WebViewControl } from '../ExtendedWebView/types'; | ||||||
| import useOnResourceLongPress from './hooks/useOnResourceLongPress'; | import useOnResourceLongPress from './hooks/useOnResourceLongPress'; | ||||||
| @@ -37,7 +36,6 @@ interface Props { | |||||||
| } | } | ||||||
|  |  | ||||||
| export default function NoteBodyViewer(props: Props) { | export default function NoteBodyViewer(props: Props) { | ||||||
| 	const dialogBoxRef = useRef(null); |  | ||||||
| 	const webviewRef = useRef<WebViewControl>(null); | 	const webviewRef = useRef<WebViewControl>(null); | ||||||
|  |  | ||||||
| 	const onScroll = useCallback(async (scrollTop: number) => { | 	const onScroll = useCallback(async (scrollTop: number) => { | ||||||
| @@ -49,7 +47,6 @@ export default function NoteBodyViewer(props: Props) { | |||||||
| 			onJoplinLinkClick: props.onJoplinLinkClick, | 			onJoplinLinkClick: props.onJoplinLinkClick, | ||||||
| 			onRequestEditResource: props.onRequestEditResource, | 			onRequestEditResource: props.onRequestEditResource, | ||||||
| 		}, | 		}, | ||||||
| 		dialogBoxRef, |  | ||||||
| 	); | 	); | ||||||
|  |  | ||||||
| 	const onPostMessage = useOnMessage(props.noteBody, { | 	const onPostMessage = useOnMessage(props.noteBody, { | ||||||
| @@ -101,9 +98,6 @@ export default function NoteBodyViewer(props: Props) { | |||||||
| 		if (props.onLoadEnd) props.onLoadEnd(); | 		if (props.onLoadEnd) props.onLoadEnd(); | ||||||
| 	}, [props.onLoadEnd]); | 	}, [props.onLoadEnd]); | ||||||
|  |  | ||||||
| 	// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied |  | ||||||
| 	const BackButtonDialogBox_ = BackButtonDialogBox as any; |  | ||||||
|  |  | ||||||
| 	const { html, injectedJs } = useSource(tempDir, props.themeId); | 	const { html, injectedJs } = useSource(tempDir, props.themeId); | ||||||
|  |  | ||||||
| 	return ( | 	return ( | ||||||
| @@ -119,7 +113,6 @@ export default function NoteBodyViewer(props: Props) { | |||||||
| 				onLoadEnd={onLoadEnd} | 				onLoadEnd={onLoadEnd} | ||||||
| 				onMessage={onWebViewMessage} | 				onMessage={onWebViewMessage} | ||||||
| 			/> | 			/> | ||||||
| 			<BackButtonDialogBox_ ref={dialogBoxRef}/> |  | ||||||
| 		</View> | 		</View> | ||||||
| 	); | 	); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,13 +1,13 @@ | |||||||
| import { useCallback } from 'react'; | import { useCallback, useContext } from 'react'; | ||||||
|  |  | ||||||
| const { _ } = require('@joplin/lib/locale.js'); | const { _ } = require('@joplin/lib/locale.js'); | ||||||
| const { dialogs } = require('../../../utils/dialogs.js'); |  | ||||||
| import Resource from '@joplin/lib/models/Resource'; | import Resource from '@joplin/lib/models/Resource'; | ||||||
| import { copyToCache } from '../../../utils/ShareUtils'; | import { copyToCache } from '../../../utils/ShareUtils'; | ||||||
| import isEditableResource from '../../NoteEditor/ImageEditor/isEditableResource'; | import isEditableResource from '../../NoteEditor/ImageEditor/isEditableResource'; | ||||||
| import shim from '@joplin/lib/shim'; | import shim from '@joplin/lib/shim'; | ||||||
| import shareFile from '../../../utils/shareFile'; | import shareFile from '../../../utils/shareFile'; | ||||||
| import Logger from '@joplin/utils/Logger'; | import Logger from '@joplin/utils/Logger'; | ||||||
|  | import { DialogContext } from '../../DialogManager'; | ||||||
|  |  | ||||||
| const logger = Logger.create('useOnResourceLongPress'); | const logger = Logger.create('useOnResourceLongPress'); | ||||||
|  |  | ||||||
| @@ -16,10 +16,11 @@ interface Callbacks { | |||||||
| 	onRequestEditResource: (message: string)=> void; | 	onRequestEditResource: (message: string)=> void; | ||||||
| } | } | ||||||
|  |  | ||||||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied | export default function useOnResourceLongPress(callbacks: Callbacks) { | ||||||
| export default function useOnResourceLongPress(callbacks: Callbacks, dialogBoxRef: any) { |  | ||||||
| 	const { onJoplinLinkClick, onRequestEditResource } = callbacks; | 	const { onJoplinLinkClick, onRequestEditResource } = callbacks; | ||||||
|  |  | ||||||
|  | 	const dialogManager = useContext(DialogContext); | ||||||
|  |  | ||||||
| 	return useCallback(async (msg: string) => { | 	return useCallback(async (msg: string) => { | ||||||
| 		try { | 		try { | ||||||
| 			const resourceId = msg.split(':')[1]; | 			const resourceId = msg.split(':')[1]; | ||||||
| @@ -42,7 +43,7 @@ export default function useOnResourceLongPress(callbacks: Callbacks, dialogBoxRe | |||||||
| 			} | 			} | ||||||
| 			actions.push({ text: _('Share'), id: 'share' }); | 			actions.push({ text: _('Share'), id: 'share' }); | ||||||
|  |  | ||||||
| 			const action = await dialogs.pop({ dialogbox: dialogBoxRef.current }, name, actions); | 			const action = await dialogManager.showMenu(name, actions); | ||||||
|  |  | ||||||
| 			if (action === 'open') { | 			if (action === 'open') { | ||||||
| 				onJoplinLinkClick(`joplin://${resourceId}`); | 				onJoplinLinkClick(`joplin://${resourceId}`); | ||||||
| @@ -54,7 +55,7 @@ export default function useOnResourceLongPress(callbacks: Callbacks, dialogBoxRe | |||||||
| 			} | 			} | ||||||
| 		} catch (e) { | 		} catch (e) { | ||||||
| 			logger.error('Could not handle link long press', e); | 			logger.error('Could not handle link long press', e); | ||||||
| 			void shim.showMessageBox(`An error occurred, check log for details: ${e}`); | 			void shim.showErrorDialog(`An error occurred, check log for details: ${e}`); | ||||||
| 		} | 		} | ||||||
| 	}, [onJoplinLinkClick, onRequestEditResource, dialogBoxRef]); | 	}, [onJoplinLinkClick, onRequestEditResource, dialogManager]); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -10,7 +10,6 @@ import Note from '@joplin/lib/models/Note'; | |||||||
| import Folder from '@joplin/lib/models/Folder'; | import Folder from '@joplin/lib/models/Folder'; | ||||||
| import { themeStyle } from '../global-style'; | import { themeStyle } from '../global-style'; | ||||||
| import { OnValueChangedListener } from '../Dropdown'; | import { OnValueChangedListener } from '../Dropdown'; | ||||||
| const DialogBox = require('react-native-dialogbox').default; |  | ||||||
| import { FolderEntity } from '@joplin/lib/services/database/types'; | import { FolderEntity } from '@joplin/lib/services/database/types'; | ||||||
| import { State } from '@joplin/lib/reducer'; | import { State } from '@joplin/lib/reducer'; | ||||||
| import IconButton from '../IconButton'; | import IconButton from '../IconButton'; | ||||||
| @@ -84,7 +83,6 @@ interface ScreenHeaderState { | |||||||
| class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeaderState> { | class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeaderState> { | ||||||
| 	// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied | 	// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied | ||||||
| 	private cachedStyles: any; | 	private cachedStyles: any; | ||||||
| 	public dialogbox?: typeof DialogBox; |  | ||||||
| 	public constructor(props: ScreenHeaderProps) { | 	public constructor(props: ScreenHeaderProps) { | ||||||
| 		super(props); | 		super(props); | ||||||
| 		this.cachedStyles = {}; | 		this.cachedStyles = {}; | ||||||
| @@ -645,11 +643,6 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade | |||||||
| 				<WarningBanner | 				<WarningBanner | ||||||
| 					showShouldUpgradeSyncTargetMessage={this.props.showShouldUpgradeSyncTargetMessage} | 					showShouldUpgradeSyncTargetMessage={this.props.showShouldUpgradeSyncTargetMessage} | ||||||
| 				/> | 				/> | ||||||
| 				<DialogBox |  | ||||||
| 					ref={(dialogbox: typeof DialogBox) => { |  | ||||||
| 						this.dialogbox = dialogbox; |  | ||||||
| 					}} |  | ||||||
| 				/> |  | ||||||
| 			</View> | 			</View> | ||||||
| 		); | 		); | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -105,7 +105,7 @@ class LogScreenComponent extends BaseScreenComponent<Props, State> { | |||||||
| 			logger.error('Unable to share log data:', e); | 			logger.error('Unable to share log data:', e); | ||||||
|  |  | ||||||
| 			// Display a message to the user (e.g. in the case where the user is out of disk space). | 			// Display a message to the user (e.g. in the case where the user is out of disk space). | ||||||
| 			void shim.showMessageBox(_('Error'), _('Unable to share log data. Reason: %s', e.toString())); | 			void shim.showErrorDialog(_('Unable to share log data. Reason: %s', e.toString())); | ||||||
| 		} finally { | 		} finally { | ||||||
| 			if (fileToShare) { | 			if (fileToShare) { | ||||||
| 				await shim.fsDriver().remove(fileToShare); | 				await shim.fsDriver().remove(fileToShare); | ||||||
|   | |||||||
| @@ -32,8 +32,6 @@ import { reg } from '@joplin/lib/registry'; | |||||||
| import ResourceFetcher from '@joplin/lib/services/ResourceFetcher'; | import ResourceFetcher from '@joplin/lib/services/ResourceFetcher'; | ||||||
| import { BaseScreenComponent } from '../base-screen'; | import { BaseScreenComponent } from '../base-screen'; | ||||||
| import { themeStyle, editorFont } from '../global-style'; | import { themeStyle, editorFont } from '../global-style'; | ||||||
| const { dialogs } = require('../../utils/dialogs.js'); |  | ||||||
| const DialogBox = require('react-native-dialogbox').default; |  | ||||||
| import shared, { BaseNoteScreenComponent, Props as BaseProps } from '@joplin/lib/components/shared/note-screen-shared'; | import shared, { BaseNoteScreenComponent, Props as BaseProps } from '@joplin/lib/components/shared/note-screen-shared'; | ||||||
| import { Asset, ImagePickerResponse, launchImageLibrary } from 'react-native-image-picker'; | import { Asset, ImagePickerResponse, launchImageLibrary } from 'react-native-image-picker'; | ||||||
| import SelectDateTimeDialog from '../SelectDateTimeDialog'; | import SelectDateTimeDialog from '../SelectDateTimeDialog'; | ||||||
| @@ -49,7 +47,7 @@ import { isSupportedLanguage } from '../../services/voiceTyping/vosk'; | |||||||
| import { ChangeEvent as EditorChangeEvent, SelectionRangeChangeEvent, UndoRedoDepthChangeEvent } from '@joplin/editor/events'; | import { ChangeEvent as EditorChangeEvent, SelectionRangeChangeEvent, UndoRedoDepthChangeEvent } from '@joplin/editor/events'; | ||||||
| import { join } from 'path'; | import { join } from 'path'; | ||||||
| import { Dispatch } from 'redux'; | import { Dispatch } from 'redux'; | ||||||
| import { RefObject } from 'react'; | import { RefObject, useContext } from 'react'; | ||||||
| import { SelectionRange } from '../NoteEditor/types'; | import { SelectionRange } from '../NoteEditor/types'; | ||||||
| import { getNoteCallbackUrl } from '@joplin/lib/callbackUrlUtils'; | import { getNoteCallbackUrl } from '@joplin/lib/callbackUrlUtils'; | ||||||
| import { AppState } from '../../utils/types'; | import { AppState } from '../../utils/types'; | ||||||
| @@ -64,6 +62,7 @@ import { ResourceInfo } from '../NoteBodyViewer/hooks/useRerenderHandler'; | |||||||
| import getImageDimensions from '../../utils/image/getImageDimensions'; | import getImageDimensions from '../../utils/image/getImageDimensions'; | ||||||
| import resizeImage from '../../utils/image/resizeImage'; | import resizeImage from '../../utils/image/resizeImage'; | ||||||
| import { CameraResult } from '../CameraView/types'; | import { CameraResult } from '../CameraView/types'; | ||||||
|  | import { DialogContext, DialogControl } from '../DialogManager'; | ||||||
|  |  | ||||||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied | // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied | ||||||
| const emptyArray: any[] = []; | const emptyArray: any[] = []; | ||||||
| @@ -87,6 +86,10 @@ interface Props extends BaseProps { | |||||||
| 	toolbarEnabled: boolean; | 	toolbarEnabled: boolean; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | interface ComponentProps extends Props { | ||||||
|  | 	dialogs: DialogControl; | ||||||
|  | } | ||||||
|  |  | ||||||
| interface State { | interface State { | ||||||
| 	note: NoteEntity; | 	note: NoteEntity; | ||||||
| 	mode: 'view'|'edit'; | 	mode: 'view'|'edit'; | ||||||
| @@ -117,7 +120,7 @@ interface State { | |||||||
| 	voiceTypingDialogShown: boolean; | 	voiceTypingDialogShown: boolean; | ||||||
| } | } | ||||||
|  |  | ||||||
| class NoteScreenComponent extends BaseScreenComponent<Props, State> implements BaseNoteScreenComponent { | class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> implements BaseNoteScreenComponent { | ||||||
| 	// This isn't in this.state because we don't want changing scroll to trigger | 	// This isn't in this.state because we don't want changing scroll to trigger | ||||||
| 	// a re-render. | 	// a re-render. | ||||||
| 	private lastBodyScroll: number|undefined = undefined; | 	private lastBodyScroll: number|undefined = undefined; | ||||||
| @@ -153,7 +156,7 @@ class NoteScreenComponent extends BaseScreenComponent<Props, State> implements B | |||||||
| 		return { header: null }; | 		return { header: null }; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	public constructor(props: Props) { | 	public constructor(props: ComponentProps) { | ||||||
| 		super(props); | 		super(props); | ||||||
|  |  | ||||||
| 		this.state = { | 		this.state = { | ||||||
| @@ -206,7 +209,10 @@ class NoteScreenComponent extends BaseScreenComponent<Props, State> implements B | |||||||
|  |  | ||||||
| 		const saveDialog = async () => { | 		const saveDialog = async () => { | ||||||
| 			if (this.isModified()) { | 			if (this.isModified()) { | ||||||
| 				const buttonId = await dialogs.pop(this, _('This note has been modified:'), [{ text: _('Save changes'), id: 'save' }, { text: _('Discard changes'), id: 'discard' }, { text: _('Cancel'), id: 'cancel' }]); | 				const buttonId = await this.props.dialogs.showMenu( | ||||||
|  | 					_('This note has been modified:'), | ||||||
|  | 					[{ text: _('Save changes'), id: 'save' }, { text: _('Discard changes'), id: 'discard' }, { text: _('Cancel'), id: 'cancel' }], | ||||||
|  | 				); | ||||||
|  |  | ||||||
| 				if (buttonId === 'cancel') return true; | 				if (buttonId === 'cancel') return true; | ||||||
| 				if (buttonId === 'save') await this.saveNoteButton_press(); | 				if (buttonId === 'save') await this.saveNoteButton_press(); | ||||||
| @@ -269,7 +275,7 @@ class NoteScreenComponent extends BaseScreenComponent<Props, State> implements B | |||||||
| 			try { | 			try { | ||||||
| 				await CommandService.instance().execute('openItem', msg); | 				await CommandService.instance().execute('openItem', msg); | ||||||
| 			} catch (error) { | 			} catch (error) { | ||||||
| 				dialogs.error(this, error.message); | 				await this.props.dialogs.error(error.message); | ||||||
| 			} | 			} | ||||||
| 		}; | 		}; | ||||||
|  |  | ||||||
| @@ -664,14 +670,15 @@ class NoteScreenComponent extends BaseScreenComponent<Props, State> implements B | |||||||
| 		if (canResize) { | 		if (canResize) { | ||||||
| 			const resizeLargeImages = Setting.value('imageResizing'); | 			const resizeLargeImages = Setting.value('imageResizing'); | ||||||
| 			if (resizeLargeImages === 'alwaysAsk') { | 			if (resizeLargeImages === 'alwaysAsk') { | ||||||
| 				const userAnswer = await dialogs.pop(this, `${_('You are about to attach a large image (%dx%d pixels). Would you like to resize it down to %d pixels before attaching it?', dimensions.width, dimensions.height, maxSize)}\n\n${_('(You may disable this prompt in the options)')}`, [ | 				const userAnswer = await this.props.dialogs.showMenu( | ||||||
| 					{ text: _('Yes'), id: 'yes' }, | 					`${_('You are about to attach a large image (%dx%d pixels). Would you like to resize it down to %d pixels before attaching it?', dimensions.width, dimensions.height, maxSize)}\n\n${_('(You may disable this prompt in the options)')}`, [ | ||||||
| 					{ text: _('No'), id: 'no' }, | 						{ text: _('Yes'), id: 'yes' }, | ||||||
| 					{ text: _('Cancel'), id: 'cancel' }, | 						{ text: _('No'), id: 'no' }, | ||||||
| 				]); | 						{ text: _('Cancel'), id: 'cancel' }, | ||||||
|  | 					]); | ||||||
| 				if (userAnswer === 'yes') return await saveResizedImage(); | 				if (userAnswer === 'yes') return await saveResizedImage(); | ||||||
| 				if (userAnswer === 'no') return await saveOriginalImage(); | 				if (userAnswer === 'no') return await saveOriginalImage(); | ||||||
| 				if (userAnswer === 'cancel') return false; | 				if (userAnswer === 'cancel' || !userAnswer) return false; | ||||||
| 			} else if (resizeLargeImages === 'alwaysResize') { | 			} else if (resizeLargeImages === 'alwaysResize') { | ||||||
| 				return await saveResizedImage(); | 				return await saveResizedImage(); | ||||||
| 			} | 			} | ||||||
| @@ -759,7 +766,7 @@ class NoteScreenComponent extends BaseScreenComponent<Props, State> implements B | |||||||
| 				if (!done) return null; | 				if (!done) return null; | ||||||
| 			} else { | 			} else { | ||||||
| 				if (fileType === 'image' && mimeType !== 'image/svg+xml') { | 				if (fileType === 'image' && mimeType !== 'image/svg+xml') { | ||||||
| 					dialogs.error(this, _('Unsupported image type: %s', mimeType)); | 					await this.props.dialogs.error(_('Unsupported image type: %s', mimeType)); | ||||||
| 					return null; | 					return null; | ||||||
| 				} else { | 				} else { | ||||||
| 					await shim.fsDriver().copy(localFilePath, targetPath); | 					await shim.fsDriver().copy(localFilePath, targetPath); | ||||||
| @@ -773,7 +780,7 @@ class NoteScreenComponent extends BaseScreenComponent<Props, State> implements B | |||||||
| 			} | 			} | ||||||
| 		} catch (error) { | 		} catch (error) { | ||||||
| 			reg.logger().warn('Could not attach file:', error); | 			reg.logger().warn('Could not attach file:', error); | ||||||
| 			await dialogs.error(this, error.message); | 			await this.props.dialogs.error(error.message); | ||||||
| 			return null; | 			return null; | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| @@ -996,7 +1003,7 @@ class NoteScreenComponent extends BaseScreenComponent<Props, State> implements B | |||||||
| 			await Linking.openURL(url); | 			await Linking.openURL(url); | ||||||
| 		} catch (error) { | 		} catch (error) { | ||||||
| 			this.props.dispatch({ type: 'SIDE_MENU_CLOSE' }); | 			this.props.dispatch({ type: 'SIDE_MENU_CLOSE' }); | ||||||
| 			await dialogs.error(this, error.message); | 			await this.props.dialogs.error(error.message); | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -1007,7 +1014,7 @@ class NoteScreenComponent extends BaseScreenComponent<Props, State> implements B | |||||||
| 		try { | 		try { | ||||||
| 			await Linking.openURL(note.source_url); | 			await Linking.openURL(note.source_url); | ||||||
| 		} catch (error) { | 		} catch (error) { | ||||||
| 			await dialogs.error(this, error.message); | 			await this.props.dialogs.error(error.message); | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -1070,7 +1077,7 @@ class NoteScreenComponent extends BaseScreenComponent<Props, State> implements B | |||||||
| 		if (Platform.OS === 'ios') buttons.push({ text: _('Attach photo'), id: 'attachPhoto' }); | 		if (Platform.OS === 'ios') buttons.push({ text: _('Attach photo'), id: 'attachPhoto' }); | ||||||
| 		buttons.push({ text: _('Take photo'), id: 'takePhoto' }); | 		buttons.push({ text: _('Take photo'), id: 'takePhoto' }); | ||||||
|  |  | ||||||
| 		const buttonId = await dialogs.pop(this, _('Choose an option'), buttons); | 		const buttonId = await this.props.dialogs.showMenu(_('Choose an option'), buttons); | ||||||
|  |  | ||||||
| 		if (buttonId === 'takePhoto') await this.takePhoto_onPress(); | 		if (buttonId === 'takePhoto') await this.takePhoto_onPress(); | ||||||
| 		if (buttonId === 'attachFile') await this.attachFile_onPress(); | 		if (buttonId === 'attachFile') await this.attachFile_onPress(); | ||||||
| @@ -1631,12 +1638,6 @@ class NoteScreenComponent extends BaseScreenComponent<Props, State> implements B | |||||||
|  |  | ||||||
| 				<SelectDateTimeDialog themeId={this.props.themeId} shown={this.state.alarmDialogShown} date={dueDate} onAccept={this.onAlarmDialogAccept} onReject={this.onAlarmDialogReject} /> | 				<SelectDateTimeDialog themeId={this.props.themeId} shown={this.state.alarmDialogShown} date={dueDate} onAccept={this.onAlarmDialogAccept} onReject={this.onAlarmDialogReject} /> | ||||||
|  |  | ||||||
| 				<DialogBox |  | ||||||
| 					// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied |  | ||||||
| 					ref={(dialogbox: any) => { |  | ||||||
| 						this.dialogbox = dialogbox; |  | ||||||
| 					}} |  | ||||||
| 				/> |  | ||||||
| 				{noteTagDialog} | 				{noteTagDialog} | ||||||
| 			</View> | 			</View> | ||||||
| 		); | 		); | ||||||
| @@ -1648,8 +1649,9 @@ class NoteScreenComponent extends BaseScreenComponent<Props, State> implements B | |||||||
| // which can cause some bugs where previously set state to another note would interfere | // which can cause some bugs where previously set state to another note would interfere | ||||||
| // how the new note should be rendered | // how the new note should be rendered | ||||||
| const NoteScreenWrapper = (props: Props) => { | const NoteScreenWrapper = (props: Props) => { | ||||||
|  | 	const dialogs = useContext(DialogContext); | ||||||
| 	return ( | 	return ( | ||||||
| 		<NoteScreenComponent key={props.noteId} {...props} /> | 		<NoteScreenComponent key={props.noteId} dialogs={dialogs} {...props} /> | ||||||
| 	); | 	); | ||||||
| }; | }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -11,15 +11,14 @@ import { themeStyle } from '../global-style'; | |||||||
| import { FolderPickerOptions, ScreenHeader } from '../ScreenHeader'; | import { FolderPickerOptions, ScreenHeader } from '../ScreenHeader'; | ||||||
| import { _ } from '@joplin/lib/locale'; | import { _ } from '@joplin/lib/locale'; | ||||||
| import ActionButton from '../buttons/FloatingActionButton'; | import ActionButton from '../buttons/FloatingActionButton'; | ||||||
| const { dialogs } = require('../../utils/dialogs.js'); |  | ||||||
| const DialogBox = require('react-native-dialogbox').default; |  | ||||||
| import BackButtonService from '../../services/BackButtonService'; |  | ||||||
| import { BaseScreenComponent } from '../base-screen'; | import { BaseScreenComponent } from '../base-screen'; | ||||||
| import { AppState } from '../../utils/types'; | import { AppState } from '../../utils/types'; | ||||||
| import { FolderEntity, NoteEntity, TagEntity } from '@joplin/lib/services/database/types'; | import { FolderEntity, NoteEntity, TagEntity } from '@joplin/lib/services/database/types'; | ||||||
| import { itemIsInTrash } from '@joplin/lib/services/trash'; | import { itemIsInTrash } from '@joplin/lib/services/trash'; | ||||||
| import AccessibleView from '../accessibility/AccessibleView'; | import AccessibleView from '../accessibility/AccessibleView'; | ||||||
| import { Dispatch } from 'redux'; | import { Dispatch } from 'redux'; | ||||||
|  | import { DialogContext, DialogControl } from '../DialogManager'; | ||||||
|  | import { useContext } from 'react'; | ||||||
|  |  | ||||||
| interface Props { | interface Props { | ||||||
| 	dispatch: Dispatch; | 	dispatch: Dispatch; | ||||||
| @@ -46,17 +45,18 @@ interface State { | |||||||
|  |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | interface ComponentProps extends Props { | ||||||
|  | 	dialogManager: DialogControl; | ||||||
|  | } | ||||||
|  |  | ||||||
| type Styles = Record<string, ViewStyle|TextStyle>; | type Styles = Record<string, ViewStyle|TextStyle>; | ||||||
|  |  | ||||||
| class NotesScreenComponent extends BaseScreenComponent<Props, State> { | class NotesScreenComponent extends BaseScreenComponent<ComponentProps, State> { | ||||||
| 	// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Partial refactor of old code from before rule was applied |  | ||||||
| 	private dialogbox: any; |  | ||||||
|  |  | ||||||
| 	private onAppStateChangeSub_: NativeEventSubscription = null; | 	private onAppStateChangeSub_: NativeEventSubscription = null; | ||||||
| 	private styles_: Record<number, Styles> = {}; | 	private styles_: Record<number, Styles> = {}; | ||||||
| 	private folderPickerOptions_: FolderPickerOptions; | 	private folderPickerOptions_: FolderPickerOptions; | ||||||
|  |  | ||||||
| 	public constructor(props: Props) { | 	public constructor(props: ComponentProps) { | ||||||
| 		super(props); | 		super(props); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -99,20 +99,12 @@ class NotesScreenComponent extends BaseScreenComponent<Props, State> { | |||||||
| 			id: { name: 'showCompletedTodos', value: !Setting.value('showCompletedTodos') }, | 			id: { name: 'showCompletedTodos', value: !Setting.value('showCompletedTodos') }, | ||||||
| 		}); | 		}); | ||||||
|  |  | ||||||
| 		const r = await dialogs.pop(this, Setting.settingMetadata('notes.sortOrder.field').label(), buttons); | 		const r = await this.props.dialogManager.showMenu(Setting.settingMetadata('notes.sortOrder.field').label(), buttons); | ||||||
| 		if (!r) return; | 		if (!r) return; | ||||||
|  |  | ||||||
| 		Setting.setValue(r.name, r.value); | 		Setting.setValue(r.name, r.value); | ||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
| 	private backHandler = () => { |  | ||||||
| 		if (this.dialogbox && this.dialogbox.state && this.dialogbox.state.isVisible) { |  | ||||||
| 			this.dialogbox.close(); |  | ||||||
| 			return true; |  | ||||||
| 		} |  | ||||||
| 		return false; |  | ||||||
| 	}; |  | ||||||
|  |  | ||||||
| 	public styles() { | 	public styles() { | ||||||
| 		if (!this.styles_) this.styles_ = {}; | 		if (!this.styles_) this.styles_ = {}; | ||||||
| 		const themeId = this.props.themeId; | 		const themeId = this.props.themeId; | ||||||
| @@ -132,14 +124,12 @@ class NotesScreenComponent extends BaseScreenComponent<Props, State> { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	public async componentDidMount() { | 	public async componentDidMount() { | ||||||
| 		BackButtonService.addHandler(this.backHandler); |  | ||||||
| 		await this.refreshNotes(); | 		await this.refreshNotes(); | ||||||
| 		this.onAppStateChangeSub_ = RNAppState.addEventListener('change', this.onAppStateChange_); | 		this.onAppStateChangeSub_ = RNAppState.addEventListener('change', this.onAppStateChange_); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	public async componentWillUnmount() { | 	public async componentWillUnmount() { | ||||||
| 		if (this.onAppStateChangeSub_) this.onAppStateChangeSub_.remove(); | 		if (this.onAppStateChangeSub_) this.onAppStateChangeSub_.remove(); | ||||||
| 		BackButtonService.removeHandler(this.backHandler); |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	public async componentDidUpdate(prevProps: Props) { | 	public async componentDidUpdate(prevProps: Props) { | ||||||
| @@ -298,17 +288,16 @@ class NotesScreenComponent extends BaseScreenComponent<Props, State> { | |||||||
| 				<ScreenHeader title={iconString + title} showBackButton={false} sortButton_press={this.sortButton_press} folderPickerOptions={this.folderPickerOptions()} showSearchButton={true} showSideMenuButton={true} /> | 				<ScreenHeader title={iconString + title} showBackButton={false} sortButton_press={this.sortButton_press} folderPickerOptions={this.folderPickerOptions()} showSearchButton={true} showSideMenuButton={true} /> | ||||||
| 				<NoteList /> | 				<NoteList /> | ||||||
| 				{actionButtonComp} | 				{actionButtonComp} | ||||||
| 				<DialogBox |  | ||||||
| 					// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied |  | ||||||
| 					ref={(dialogbox: any) => { |  | ||||||
| 						this.dialogbox = dialogbox; |  | ||||||
| 					}} |  | ||||||
| 				/> |  | ||||||
| 			</AccessibleView> | 			</AccessibleView> | ||||||
| 		); | 		); | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | const NotesScreenWrapper: React.FC<Props> = props => { | ||||||
|  | 	const dialogManager = useContext(DialogContext); | ||||||
|  | 	return <NotesScreenComponent {...props} dialogManager={dialogManager}/>; | ||||||
|  | }; | ||||||
|  |  | ||||||
| const NotesScreen = connect((state: AppState) => { | const NotesScreen = connect((state: AppState) => { | ||||||
| 	return { | 	return { | ||||||
| 		folders: state.folders, | 		folders: state.folders, | ||||||
| @@ -327,6 +316,6 @@ const NotesScreen = connect((state: AppState) => { | |||||||
| 		noteSelectionEnabled: state.noteSelectionEnabled, | 		noteSelectionEnabled: state.noteSelectionEnabled, | ||||||
| 		notesOrder: stateUtils.notesOrder(state.settings), | 		notesOrder: stateUtils.notesOrder(state.settings), | ||||||
| 	}; | 	}; | ||||||
| })(NotesScreenComponent); | })(NotesScreenWrapper); | ||||||
|  |  | ||||||
| export default NotesScreen; | export default NotesScreen; | ||||||
|   | |||||||
| @@ -1,29 +1,33 @@ | |||||||
| const React = require('react'); | import * as React from 'react'; | ||||||
| 
 | 
 | ||||||
| const { View, Button, Text, TextInput, TouchableOpacity, StyleSheet, ScrollView } = require('react-native'); | import { View, Button, Text, TextInput, TouchableOpacity, StyleSheet, ScrollView } from 'react-native'; | ||||||
|  | import { AppState } from '../../utils/types'; | ||||||
| const { connect } = require('react-redux'); | const { connect } = require('react-redux'); | ||||||
| const { ScreenHeader } = require('../ScreenHeader'); | import { ScreenHeader } from '../ScreenHeader'; | ||||||
| const { _ } = require('@joplin/lib/locale'); | import { _ } from '@joplin/lib/locale'; | ||||||
| const { BaseScreenComponent } = require('../base-screen'); | const { BaseScreenComponent } = require('../base-screen'); | ||||||
| const DialogBox = require('react-native-dialogbox').default; |  | ||||||
| const { dialogs } = require('../../utils/dialogs.js'); |  | ||||||
| const Shared = require('@joplin/lib/components/shared/dropbox-login-shared'); | const Shared = require('@joplin/lib/components/shared/dropbox-login-shared'); | ||||||
| const { themeStyle } = require('../global-style'); | import shim, { MessageBoxType } from '@joplin/lib/shim'; | ||||||
|  | import { themeStyle } from '../global-style'; | ||||||
| 
 | 
 | ||||||
| class DropboxLoginScreenComponent extends BaseScreenComponent { | class DropboxLoginScreenComponent extends BaseScreenComponent { | ||||||
| 	constructor() { | 	public constructor() { | ||||||
| 		super(); | 		super(); | ||||||
| 
 | 
 | ||||||
| 		this.styles_ = {}; | 		this.styles_ = {}; | ||||||
| 
 | 
 | ||||||
| 		this.shared_ = new Shared(this, msg => dialogs.info(this, msg), msg => dialogs.error(this, msg)); | 		this.shared_ = new Shared( | ||||||
|  | 			this, | ||||||
|  | 			(msg: string) => shim.showMessageBox(msg, { type: MessageBoxType.Info }), | ||||||
|  | 			(msg: string) => shim.showErrorDialog(msg), | ||||||
|  | 		); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	UNSAFE_componentWillMount() { | 	public UNSAFE_componentWillMount() { | ||||||
| 		this.shared_.refreshUrl(); | 		this.shared_.refreshUrl(); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	styles() { | 	private styles() { | ||||||
| 		const themeId = this.props.themeId; | 		const themeId = this.props.themeId; | ||||||
| 		const theme = themeStyle(themeId); | 		const theme = themeStyle(themeId); | ||||||
| 
 | 
 | ||||||
| @@ -47,7 +51,7 @@ class DropboxLoginScreenComponent extends BaseScreenComponent { | |||||||
| 		return this.styles_[themeId]; | 		return this.styles_[themeId]; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	render() { | 	public render() { | ||||||
| 		const theme = themeStyle(this.props.themeId); | 		const theme = themeStyle(this.props.themeId); | ||||||
| 
 | 
 | ||||||
| 		return ( | 		return ( | ||||||
| @@ -70,21 +74,15 @@ class DropboxLoginScreenComponent extends BaseScreenComponent { | |||||||
| 					{/* Add this extra padding to make sure the view is scrollable when the keyboard is visible on small screens (iPhone SE) */} | 					{/* Add this extra padding to make sure the view is scrollable when the keyboard is visible on small screens (iPhone SE) */} | ||||||
| 					<View style={{ height: 200 }}></View> | 					<View style={{ height: 200 }}></View> | ||||||
| 				</ScrollView> | 				</ScrollView> | ||||||
| 
 |  | ||||||
| 				<DialogBox |  | ||||||
| 					ref={dialogbox => { |  | ||||||
| 						this.dialogbox = dialogbox; |  | ||||||
| 					}} |  | ||||||
| 				/> |  | ||||||
| 			</View> | 			</View> | ||||||
| 		); | 		); | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| const DropboxLoginScreen = connect(state => { | const DropboxLoginScreen = connect((state: AppState) => { | ||||||
| 	return { | 	return { | ||||||
| 		themeId: state.settings.theme, | 		themeId: state.settings.theme, | ||||||
| 	}; | 	}; | ||||||
| })(DropboxLoginScreenComponent); | })(DropboxLoginScreenComponent); | ||||||
| 
 | 
 | ||||||
| module.exports = { DropboxLoginScreen }; | export default DropboxLoginScreen; | ||||||
| @@ -3,8 +3,6 @@ const { TextInput, TouchableOpacity, Linking, View, StyleSheet, Text, Button, Sc | |||||||
| const { connect } = require('react-redux'); | const { connect } = require('react-redux'); | ||||||
| import ScreenHeader from '../ScreenHeader'; | import ScreenHeader from '../ScreenHeader'; | ||||||
| import { themeStyle } from '../global-style'; | import { themeStyle } from '../global-style'; | ||||||
| const DialogBox = require('react-native-dialogbox').default; |  | ||||||
| const { dialogs } = require('../../utils/dialogs.js'); |  | ||||||
| import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService'; | import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService'; | ||||||
| import { _ } from '@joplin/lib/locale'; | import { _ } from '@joplin/lib/locale'; | ||||||
| import time from '@joplin/lib/time'; | import time from '@joplin/lib/time'; | ||||||
| @@ -13,7 +11,8 @@ import { MasterKeyEntity } from '@joplin/lib/services/e2ee/types'; | |||||||
| import { State } from '@joplin/lib/reducer'; | import { State } from '@joplin/lib/reducer'; | ||||||
| import { SyncInfo } from '@joplin/lib/services/synchronizer/syncInfoUtils'; | import { SyncInfo } from '@joplin/lib/services/synchronizer/syncInfoUtils'; | ||||||
| import { getDefaultMasterKey, setupAndDisableEncryption, toggleAndSetupEncryption } from '@joplin/lib/services/e2ee/utils'; | import { getDefaultMasterKey, setupAndDisableEncryption, toggleAndSetupEncryption } from '@joplin/lib/services/e2ee/utils'; | ||||||
| import { useMemo, useRef, useState } from 'react'; | import { useMemo, useState } from 'react'; | ||||||
|  | import shim from '@joplin/lib/shim'; | ||||||
|  |  | ||||||
| interface Props { | interface Props { | ||||||
| 	// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied | 	// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied | ||||||
| @@ -35,7 +34,6 @@ const EncryptionConfigScreen = (props: Props) => { | |||||||
| 	const { passwordChecks, masterPasswordKeys } = usePasswordChecker(props.masterKeys, props.activeMasterKeyId, props.masterPassword, props.passwords); | 	const { passwordChecks, masterPasswordKeys } = usePasswordChecker(props.masterKeys, props.activeMasterKeyId, props.masterPassword, props.passwords); | ||||||
| 	const { inputPasswords, onInputPasswordChange } = useInputPasswords(props.passwords); | 	const { inputPasswords, onInputPasswordChange } = useInputPasswords(props.passwords); | ||||||
| 	const { inputMasterPassword, onMasterPasswordSave, onMasterPasswordChange } = useInputMasterPassword(props.masterKeys, props.activeMasterKeyId); | 	const { inputMasterPassword, onMasterPasswordSave, onMasterPasswordChange } = useInputMasterPassword(props.masterKeys, props.activeMasterKeyId); | ||||||
| 	const dialogBoxRef = useRef(null); |  | ||||||
|  |  | ||||||
| 	const mkComps = []; | 	const mkComps = []; | ||||||
|  |  | ||||||
| @@ -240,7 +238,7 @@ const EncryptionConfigScreen = (props: Props) => { | |||||||
|  |  | ||||||
| 	const onToggleButtonClick = async () => { | 	const onToggleButtonClick = async () => { | ||||||
| 		if (props.encryptionEnabled) { | 		if (props.encryptionEnabled) { | ||||||
| 			const ok = await dialogs.confirmRef(dialogBoxRef.current, _('Disabling encryption means *all* your notes and attachments are going to be re-synchronised and sent unencrypted to the sync target. Do you wish to continue?')); | 			const ok = await shim.showConfirmationDialog(_('Disabling encryption means *all* your notes and attachments are going to be re-synchronised and sent unencrypted to the sync target. Do you wish to continue?')); | ||||||
| 			if (!ok) return; | 			if (!ok) return; | ||||||
|  |  | ||||||
| 			try { | 			try { | ||||||
| @@ -312,7 +310,6 @@ const EncryptionConfigScreen = (props: Props) => { | |||||||
| 				{nonExistingMasterKeySection} | 				{nonExistingMasterKeySection} | ||||||
| 				<View style={{ flex: 1, height: 20 }}></View> | 				<View style={{ flex: 1, height: 20 }}></View> | ||||||
| 			</ScrollView> | 			</ScrollView> | ||||||
| 			<DialogBox ref={dialogBoxRef}/> |  | ||||||
| 		</View> | 		</View> | ||||||
| 	); | 	); | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ const Folder = require('@joplin/lib/models/Folder').default; | |||||||
| const BaseModel = require('@joplin/lib/BaseModel').default; | const BaseModel = require('@joplin/lib/BaseModel').default; | ||||||
| const { ScreenHeader } = require('../ScreenHeader'); | const { ScreenHeader } = require('../ScreenHeader'); | ||||||
| const { BaseScreenComponent } = require('../base-screen'); | const { BaseScreenComponent } = require('../base-screen'); | ||||||
| const { dialogs } = require('../../utils/dialogs.js'); | const shim = require('@joplin/lib/shim').default; | ||||||
| const { _ } = require('@joplin/lib/locale'); | const { _ } = require('@joplin/lib/locale'); | ||||||
| const { default: FolderPicker } = require('../FolderPicker'); | const { default: FolderPicker } = require('../FolderPicker'); | ||||||
| const TextInput = require('../TextInput').default; | const TextInput = require('../TextInput').default; | ||||||
| @@ -73,7 +73,7 @@ class FolderScreenComponent extends BaseScreenComponent { | |||||||
| 			if (folder.id && !(await Folder.canNestUnder(folder.id, folder.parent_id))) throw new Error(_('Cannot move notebook to this location')); | 			if (folder.id && !(await Folder.canNestUnder(folder.id, folder.parent_id))) throw new Error(_('Cannot move notebook to this location')); | ||||||
| 			folder = await Folder.save(folder, { userSideValidation: true }); | 			folder = await Folder.save(folder, { userSideValidation: true }); | ||||||
| 		} catch (error) { | 		} catch (error) { | ||||||
| 			dialogs.error(this, _('The notebook could not be saved: %s', error.message)); | 			shim.showErrorDialog(_('The notebook could not be saved: %s', error.message)); | ||||||
| 			return; | 			return; | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| @@ -115,11 +115,6 @@ class FolderScreenComponent extends BaseScreenComponent { | |||||||
| 					/> | 					/> | ||||||
| 				</View> | 				</View> | ||||||
| 				<View style={{ flex: 1 }} /> | 				<View style={{ flex: 1 }} /> | ||||||
| 				<dialogs.DialogBox |  | ||||||
| 					ref={dialogbox => { |  | ||||||
| 						this.dialogbox = dialogbox; |  | ||||||
| 					}} |  | ||||||
| 				/> |  | ||||||
| 			</View> | 			</View> | ||||||
| 		); | 		); | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -49,7 +49,6 @@ | |||||||
|     "react": "18.3.1", |     "react": "18.3.1", | ||||||
|     "react-native": "0.74.1", |     "react-native": "0.74.1", | ||||||
|     "react-native-device-info": "10.14.0", |     "react-native-device-info": "10.14.0", | ||||||
|     "react-native-dialogbox": "0.6.10", |  | ||||||
|     "react-native-document-picker": "9.3.0", |     "react-native-document-picker": "9.3.0", | ||||||
|     "react-native-dropdownalert": "5.1.0", |     "react-native-dropdownalert": "5.1.0", | ||||||
|     "react-native-exit-app": "2.0.0", |     "react-native-exit-app": "2.0.0", | ||||||
|   | |||||||
| @@ -64,7 +64,7 @@ import StatusScreen from './components/screens/status'; | |||||||
| import SearchScreen from './components/screens/SearchScreen'; | import SearchScreen from './components/screens/SearchScreen'; | ||||||
| const { OneDriveLoginScreen } = require('./components/screens/onedrive-login.js'); | const { OneDriveLoginScreen } = require('./components/screens/onedrive-login.js'); | ||||||
| import EncryptionConfigScreen from './components/screens/encryption-config'; | import EncryptionConfigScreen from './components/screens/encryption-config'; | ||||||
| const { DropboxLoginScreen } = require('./components/screens/dropbox-login.js'); | import DropboxLoginScreen from './components/screens/dropbox-login.js'; | ||||||
| import { MenuProvider } from 'react-native-popup-menu'; | import { MenuProvider } from 'react-native-popup-menu'; | ||||||
| import SideMenu, { SideMenuPosition } from './components/SideMenu'; | import SideMenu, { SideMenuPosition } from './components/SideMenu'; | ||||||
| import SideMenuContent from './components/side-menu-content'; | import SideMenuContent from './components/side-menu-content'; | ||||||
| @@ -1343,7 +1343,7 @@ class AppComponent extends React.Component { | |||||||
| 					}, | 					}, | ||||||
| 				}, | 				}, | ||||||
| 			}}> | 			}}> | ||||||
| 				<DialogManager> | 				<DialogManager themeId={this.props.themeId}> | ||||||
| 					{mainContent} | 					{mainContent} | ||||||
| 				</DialogManager> | 				</DialogManager> | ||||||
| 			</PaperProvider> | 			</PaperProvider> | ||||||
|   | |||||||
| @@ -1,82 +0,0 @@ | |||||||
| const DialogBox = require('react-native-dialogbox').default; |  | ||||||
| const { Keyboard } = require('react-native'); |  | ||||||
|  |  | ||||||
| // Add this at the bottom of the component: |  | ||||||
| // |  | ||||||
| // <DialogBox ref={dialogbox => { this.dialogbox = dialogbox }}/> |  | ||||||
|  |  | ||||||
| const dialogs = {}; |  | ||||||
|  |  | ||||||
| dialogs.confirmRef = (ref, message) => { |  | ||||||
| 	if (!ref) throw new Error('ref is required'); |  | ||||||
|  |  | ||||||
| 	return new Promise((resolve) => { |  | ||||||
| 		Keyboard.dismiss(); |  | ||||||
|  |  | ||||||
| 		ref.confirm({ |  | ||||||
| 			content: message, |  | ||||||
|  |  | ||||||
| 			ok: { |  | ||||||
| 				callback: () => { |  | ||||||
| 					resolve(true); |  | ||||||
| 				}, |  | ||||||
| 			}, |  | ||||||
|  |  | ||||||
| 			cancel: { |  | ||||||
| 				callback: () => { |  | ||||||
| 					resolve(false); |  | ||||||
| 				}, |  | ||||||
| 			}, |  | ||||||
| 		}); |  | ||||||
| 	}); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| dialogs.confirm = (parentComponent, message) => { |  | ||||||
| 	if (!parentComponent) throw new Error('parentComponent is required'); |  | ||||||
| 	if (!('dialogbox' in parentComponent)) throw new Error('A "dialogbox" component must be defined on the parent component!'); |  | ||||||
|  |  | ||||||
| 	return dialogs.confirmRef(parentComponent.dialogbox, message); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| dialogs.pop = (parentComponent, message, buttons, options = null) => { |  | ||||||
| 	if (!parentComponent) throw new Error('parentComponent is required'); |  | ||||||
| 	if (!('dialogbox' in parentComponent)) throw new Error('A "dialogbox" component must be defined on the parent component!'); |  | ||||||
|  |  | ||||||
| 	if (!options) options = {}; |  | ||||||
| 	if (!('buttonFlow' in options)) options.buttonFlow = 'auto'; |  | ||||||
|  |  | ||||||
| 	return new Promise((resolve) => { |  | ||||||
| 		Keyboard.dismiss(); |  | ||||||
|  |  | ||||||
| 		const btns = []; |  | ||||||
| 		for (let i = 0; i < buttons.length; i++) { |  | ||||||
| 			btns.push({ |  | ||||||
| 				text: buttons[i].text, |  | ||||||
| 				callback: () => { |  | ||||||
| 					parentComponent.dialogbox.close(); |  | ||||||
| 					resolve(buttons[i].id); |  | ||||||
| 				}, |  | ||||||
| 			}); |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		parentComponent.dialogbox.pop({ |  | ||||||
| 			content: message, |  | ||||||
| 			btns: btns, |  | ||||||
| 			buttonFlow: options.buttonFlow, |  | ||||||
| 		}); |  | ||||||
| 	}); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| dialogs.error = (parentComponent, message) => { |  | ||||||
| 	Keyboard.dismiss(); |  | ||||||
| 	return parentComponent.dialogbox.alert(message); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| dialogs.info = (parentComponent, message) => { |  | ||||||
| 	Keyboard.dismiss(); |  | ||||||
| 	return parentComponent.dialogbox.alert(message); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| dialogs.DialogBox = DialogBox; |  | ||||||
|  |  | ||||||
| module.exports = { dialogs }; |  | ||||||
| @@ -1,28 +1,27 @@ | |||||||
| import { _ } from '@joplin/lib/locale'; | import { _ } from '@joplin/lib/locale'; | ||||||
| import { Alert } from 'react-native'; | import { Alert } from 'react-native'; | ||||||
| import { DialogControl, PromptButton } from '../components/DialogManager'; | import { DialogControl } from '../components/DialogManager'; | ||||||
| import { RefObject } from 'react'; | import { RefObject } from 'react'; | ||||||
|  | import { MessageBoxType, ShowMessageBoxOptions } from '@joplin/lib/shim'; | ||||||
|  | import { PromptButton } from '../components/DialogManager/types'; | ||||||
|  |  | ||||||
| interface Options { |  | ||||||
| 	title: string; |  | ||||||
| 	buttons: string[]; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| const makeShowMessageBox = (dialogControl: null|RefObject<DialogControl>) => (message: string, options: Options = null) => { | const makeShowMessageBox = (dialogControl: null|RefObject<DialogControl>) => (message: string, options: ShowMessageBoxOptions = null) => { | ||||||
| 	return new Promise<number>(resolve => { | 	return new Promise<number>(resolve => { | ||||||
| 		const defaultButtons: PromptButton[] = [ | 		const okButton: PromptButton = { | ||||||
| 			{ | 			text: _('OK'), | ||||||
| 				text: _('OK'), | 			onPress: () => resolve(0), | ||||||
| 				onPress: () => resolve(0), | 		}; | ||||||
| 			}, | 		const cancelButton: PromptButton = { | ||||||
| 			{ | 			text: _('Cancel'), | ||||||
| 				text: _('Cancel'), | 			onPress: () => resolve(1), | ||||||
| 				onPress: () => resolve(1), | 			style: 'cancel', | ||||||
| 				style: 'cancel', | 		}; | ||||||
| 			}, | 		const defaultConfirmButtons = [okButton, cancelButton]; | ||||||
| 		]; | 		const defaultAlertButtons = [okButton]; | ||||||
|  |  | ||||||
| 		let buttons = defaultButtons; | 		const dialogType = options.type ?? MessageBoxType.Confirm; | ||||||
|  | 		let buttons = dialogType === MessageBoxType.Confirm ? defaultConfirmButtons : defaultAlertButtons; | ||||||
| 		if (options?.buttons) { | 		if (options?.buttons) { | ||||||
| 			buttons = options.buttons.map((text, index) => { | 			buttons = options.buttons.map((text, index) => { | ||||||
| 				return { | 				return { | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| import { CommandRuntime, CommandDeclaration, CommandContext } from '../services/CommandService'; | import { CommandRuntime, CommandDeclaration, CommandContext } from '../services/CommandService'; | ||||||
| import { _ } from '../locale'; | import { _ } from '../locale'; | ||||||
| import Note from '../models/Note'; | import Note from '../models/Note'; | ||||||
| import shim from '../shim'; | import shim, { MessageBoxType } from '../shim'; | ||||||
|  |  | ||||||
| export const declaration: CommandDeclaration = { | export const declaration: CommandDeclaration = { | ||||||
| 	name: 'permanentlyDeleteNote', | 	name: 'permanentlyDeleteNote', | ||||||
| @@ -21,7 +21,7 @@ export const runtime = (): CommandRuntime => { | |||||||
| 				buttons: [_('Delete'), _('Cancel')], | 				buttons: [_('Delete'), _('Cancel')], | ||||||
| 				defaultId: 1, | 				defaultId: 1, | ||||||
| 				cancelId: 1, | 				cancelId: 1, | ||||||
| 				type: 'question', | 				type: MessageBoxType.Confirm, | ||||||
| 			}); | 			}); | ||||||
|  |  | ||||||
| 			if (result === deleteIndex) { | 			if (result === deleteIndex) { | ||||||
|   | |||||||
| @@ -40,6 +40,20 @@ interface AttachFileToNoteOptions { | |||||||
| 	markupLanguage?: MarkupLanguage; | 	markupLanguage?: MarkupLanguage; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | export enum MessageBoxType { | ||||||
|  | 	Confirm = 'question', | ||||||
|  | 	Error = 'error', | ||||||
|  | 	Info = 'info', | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface ShowMessageBoxOptions { | ||||||
|  | 	title?: string; | ||||||
|  | 	buttons?: string[]; | ||||||
|  | 	type?: MessageBoxType; | ||||||
|  | 	defaultId?: number; | ||||||
|  | 	cancelId?: number; | ||||||
|  | } | ||||||
|  |  | ||||||
| let isTestingEnv_ = false; | let isTestingEnv_ = false; | ||||||
|  |  | ||||||
| // We need to ensure that there's only one instance of React being used by all | // We need to ensure that there's only one instance of React being used by all | ||||||
| @@ -397,13 +411,16 @@ const shim = { | |||||||
| 	// Returns the index of the button that was clicked. By default, | 	// Returns the index of the button that was clicked. By default, | ||||||
| 	// 0 -> OK | 	// 0 -> OK | ||||||
| 	// 1 -> Cancel | 	// 1 -> Cancel | ||||||
| 	// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied | 	showMessageBox: (_message: string, _options: ShowMessageBoxOptions = null): Promise<number> => { | ||||||
| 	showMessageBox: (_message: string, _options: any = null): Promise<number> => { |  | ||||||
| 		throw new Error('Not implemented'); | 		throw new Error('Not implemented'); | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
|  | 	showErrorDialog: async (message: string): Promise<void> => { | ||||||
|  | 		await shim.showMessageBox(message, { type: MessageBoxType.Error }); | ||||||
|  | 	}, | ||||||
|  |  | ||||||
| 	showConfirmationDialog: async (message: string): Promise<boolean> => { | 	showConfirmationDialog: async (message: string): Promise<boolean> => { | ||||||
| 		return await shim.showMessageBox(message) === 0; | 		return await shim.showMessageBox(message, { type: MessageBoxType.Confirm }) === 0; | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
| 	// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied | 	// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied | ||||||
|   | |||||||
							
								
								
									
										13
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								yarn.lock
									
									
									
									
									
								
							| @@ -8402,7 +8402,6 @@ __metadata: | |||||||
|     react-dom: 18.3.1 |     react-dom: 18.3.1 | ||||||
|     react-native: 0.74.1 |     react-native: 0.74.1 | ||||||
|     react-native-device-info: 10.14.0 |     react-native-device-info: 10.14.0 | ||||||
|     react-native-dialogbox: 0.6.10 |  | ||||||
|     react-native-document-picker: 9.3.0 |     react-native-document-picker: 9.3.0 | ||||||
|     react-native-dropdownalert: 5.1.0 |     react-native-dropdownalert: 5.1.0 | ||||||
|     react-native-exit-app: 2.0.0 |     react-native-exit-app: 2.0.0 | ||||||
| @@ -39434,18 +39433,6 @@ __metadata: | |||||||
|   languageName: node |   languageName: node | ||||||
|   linkType: hard |   linkType: hard | ||||||
| 
 | 
 | ||||||
| "react-native-dialogbox@npm:0.6.10": |  | ||||||
|   version: 0.6.10 |  | ||||||
|   resolution: "react-native-dialogbox@npm:0.6.10" |  | ||||||
|   dependencies: |  | ||||||
|     prop-types: ^15.6.2 |  | ||||||
|   peerDependencies: |  | ||||||
|     react: "*" |  | ||||||
|     react-native: ">=0.30.0" |  | ||||||
|   checksum: 4163dbf7975551b905053b9df4cdbcb02116acb2aaf16e88546e0f48b82bc18af5f0fffec384dcb1f570cc35fc59804bdb7cba0a153e75cacd72201f159e7e58 |  | ||||||
|   languageName: node |  | ||||||
|   linkType: hard |  | ||||||
| 
 |  | ||||||
| "react-native-document-picker@npm:9.3.0": | "react-native-document-picker@npm:9.3.0": | ||||||
|   version: 9.3.0 |   version: 9.3.0 | ||||||
|   resolution: "react-native-document-picker@npm:9.3.0" |   resolution: "react-native-document-picker@npm:9.3.0" | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user