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:
		| @@ -42,7 +42,7 @@ export const runtime = (): CommandRuntime => { | ||||
| 			} else { | ||||
| 				const errorMessage = _('Unsupported link or message: %s', link); | ||||
| 				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 { 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'; | ||||
|  | ||||
| interface ModalElementProps extends ModalProps { | ||||
| 	children: React.ReactNode; | ||||
| 	containerStyle?: ViewStyle; | ||||
| 	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 isLandscape = windowWidth > windowHeight; | ||||
| 	return useMemo(() => { | ||||
| @@ -25,12 +30,27 @@ const useStyles = (backgroundColor?: string) => { | ||||
| 		return StyleSheet.create({ | ||||
| 			modalBackground: { | ||||
| 				...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, | ||||
| 				flexGrow: 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>) => { | ||||
| @@ -51,9 +71,10 @@ const ModalElement: React.FC<ModalElementProps> = ({ | ||||
| 	children, | ||||
| 	containerStyle, | ||||
| 	backgroundColor, | ||||
| 	scrollOverflow, | ||||
| 	...modalProps | ||||
| }) => { | ||||
| 	const styles = useStyles(backgroundColor); | ||||
| 	const styles = useStyles(scrollOverflow, backgroundColor); | ||||
|  | ||||
| 	// contentWrapper adds padding. To allow styling the region outside of the modal | ||||
| 	// (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 { 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. | ||||
| 	return ( | ||||
| 		<Modal | ||||
| 			supportedOrientations={['portrait', 'portrait-upside-down', 'landscape', 'landscape-left', 'landscape-right']} | ||||
| 			{...modalProps} | ||||
| 		> | ||||
| 			<View | ||||
| 				ref={backgroundRef} | ||||
| 				style={styles.modalBackground} | ||||
| 				onStartShouldSetResponder={onShouldBackgroundCaptureTouch} | ||||
| 				onResponderRelease={onBackgroundTouchFinished} | ||||
| 			>{content}</View> | ||||
| 			{scrollOverflow ? ( | ||||
| 				<ScrollView | ||||
| 					style={styles.modalScrollView} | ||||
| 					contentContainerStyle={styles.modalScrollViewContent} | ||||
| 				>{contentAndBackdrop}</ScrollView> | ||||
| 			) : contentAndBackdrop} | ||||
| 		</Modal> | ||||
| 	); | ||||
| }; | ||||
|   | ||||
| @@ -3,7 +3,6 @@ import * as React from 'react'; | ||||
| import useOnMessage, { HandleMessageCallback, OnMarkForDownloadCallback } from './hooks/useOnMessage'; | ||||
| import { useRef, useCallback, useState, useMemo } from 'react'; | ||||
| import { View, ViewStyle } from 'react-native'; | ||||
| import BackButtonDialogBox from '../BackButtonDialogBox'; | ||||
| import ExtendedWebView from '../ExtendedWebView'; | ||||
| import { WebViewControl } from '../ExtendedWebView/types'; | ||||
| import useOnResourceLongPress from './hooks/useOnResourceLongPress'; | ||||
| @@ -37,7 +36,6 @@ interface Props { | ||||
| } | ||||
|  | ||||
| export default function NoteBodyViewer(props: Props) { | ||||
| 	const dialogBoxRef = useRef(null); | ||||
| 	const webviewRef = useRef<WebViewControl>(null); | ||||
|  | ||||
| 	const onScroll = useCallback(async (scrollTop: number) => { | ||||
| @@ -49,7 +47,6 @@ export default function NoteBodyViewer(props: Props) { | ||||
| 			onJoplinLinkClick: props.onJoplinLinkClick, | ||||
| 			onRequestEditResource: props.onRequestEditResource, | ||||
| 		}, | ||||
| 		dialogBoxRef, | ||||
| 	); | ||||
|  | ||||
| 	const onPostMessage = useOnMessage(props.noteBody, { | ||||
| @@ -101,9 +98,6 @@ export default function NoteBodyViewer(props: Props) { | ||||
| 		if (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); | ||||
|  | ||||
| 	return ( | ||||
| @@ -119,7 +113,6 @@ export default function NoteBodyViewer(props: Props) { | ||||
| 				onLoadEnd={onLoadEnd} | ||||
| 				onMessage={onWebViewMessage} | ||||
| 			/> | ||||
| 			<BackButtonDialogBox_ ref={dialogBoxRef}/> | ||||
| 		</View> | ||||
| 	); | ||||
| } | ||||
|   | ||||
| @@ -1,13 +1,13 @@ | ||||
| import { useCallback } from 'react'; | ||||
| import { useCallback, useContext } from 'react'; | ||||
|  | ||||
| const { _ } = require('@joplin/lib/locale.js'); | ||||
| const { dialogs } = require('../../../utils/dialogs.js'); | ||||
| import Resource from '@joplin/lib/models/Resource'; | ||||
| import { copyToCache } from '../../../utils/ShareUtils'; | ||||
| import isEditableResource from '../../NoteEditor/ImageEditor/isEditableResource'; | ||||
| import shim from '@joplin/lib/shim'; | ||||
| import shareFile from '../../../utils/shareFile'; | ||||
| import Logger from '@joplin/utils/Logger'; | ||||
| import { DialogContext } from '../../DialogManager'; | ||||
|  | ||||
| const logger = Logger.create('useOnResourceLongPress'); | ||||
|  | ||||
| @@ -16,10 +16,11 @@ interface Callbacks { | ||||
| 	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, dialogBoxRef: any) { | ||||
| export default function useOnResourceLongPress(callbacks: Callbacks) { | ||||
| 	const { onJoplinLinkClick, onRequestEditResource } = callbacks; | ||||
|  | ||||
| 	const dialogManager = useContext(DialogContext); | ||||
|  | ||||
| 	return useCallback(async (msg: string) => { | ||||
| 		try { | ||||
| 			const resourceId = msg.split(':')[1]; | ||||
| @@ -42,7 +43,7 @@ export default function useOnResourceLongPress(callbacks: Callbacks, dialogBoxRe | ||||
| 			} | ||||
| 			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') { | ||||
| 				onJoplinLinkClick(`joplin://${resourceId}`); | ||||
| @@ -54,7 +55,7 @@ export default function useOnResourceLongPress(callbacks: Callbacks, dialogBoxRe | ||||
| 			} | ||||
| 		} catch (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 { themeStyle } from '../global-style'; | ||||
| import { OnValueChangedListener } from '../Dropdown'; | ||||
| const DialogBox = require('react-native-dialogbox').default; | ||||
| import { FolderEntity } from '@joplin/lib/services/database/types'; | ||||
| import { State } from '@joplin/lib/reducer'; | ||||
| import IconButton from '../IconButton'; | ||||
| @@ -84,7 +83,6 @@ interface ScreenHeaderState { | ||||
| class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeaderState> { | ||||
| 	// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied | ||||
| 	private cachedStyles: any; | ||||
| 	public dialogbox?: typeof DialogBox; | ||||
| 	public constructor(props: ScreenHeaderProps) { | ||||
| 		super(props); | ||||
| 		this.cachedStyles = {}; | ||||
| @@ -645,11 +643,6 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade | ||||
| 				<WarningBanner | ||||
| 					showShouldUpgradeSyncTargetMessage={this.props.showShouldUpgradeSyncTargetMessage} | ||||
| 				/> | ||||
| 				<DialogBox | ||||
| 					ref={(dialogbox: typeof DialogBox) => { | ||||
| 						this.dialogbox = dialogbox; | ||||
| 					}} | ||||
| 				/> | ||||
| 			</View> | ||||
| 		); | ||||
| 	} | ||||
|   | ||||
| @@ -105,7 +105,7 @@ class LogScreenComponent extends BaseScreenComponent<Props, State> { | ||||
| 			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). | ||||
| 			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 { | ||||
| 			if (fileToShare) { | ||||
| 				await shim.fsDriver().remove(fileToShare); | ||||
|   | ||||
| @@ -32,8 +32,6 @@ import { reg } from '@joplin/lib/registry'; | ||||
| import ResourceFetcher from '@joplin/lib/services/ResourceFetcher'; | ||||
| import { BaseScreenComponent } from '../base-screen'; | ||||
| 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 { Asset, ImagePickerResponse, launchImageLibrary } from 'react-native-image-picker'; | ||||
| 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 { join } from 'path'; | ||||
| import { Dispatch } from 'redux'; | ||||
| import { RefObject } from 'react'; | ||||
| import { RefObject, useContext } from 'react'; | ||||
| import { SelectionRange } from '../NoteEditor/types'; | ||||
| import { getNoteCallbackUrl } from '@joplin/lib/callbackUrlUtils'; | ||||
| import { AppState } from '../../utils/types'; | ||||
| @@ -64,6 +62,7 @@ import { ResourceInfo } from '../NoteBodyViewer/hooks/useRerenderHandler'; | ||||
| import getImageDimensions from '../../utils/image/getImageDimensions'; | ||||
| import resizeImage from '../../utils/image/resizeImage'; | ||||
| 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 | ||||
| const emptyArray: any[] = []; | ||||
| @@ -87,6 +86,10 @@ interface Props extends BaseProps { | ||||
| 	toolbarEnabled: boolean; | ||||
| } | ||||
|  | ||||
| interface ComponentProps extends Props { | ||||
| 	dialogs: DialogControl; | ||||
| } | ||||
|  | ||||
| interface State { | ||||
| 	note: NoteEntity; | ||||
| 	mode: 'view'|'edit'; | ||||
| @@ -117,7 +120,7 @@ interface State { | ||||
| 	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 | ||||
| 	// a re-render. | ||||
| 	private lastBodyScroll: number|undefined = undefined; | ||||
| @@ -153,7 +156,7 @@ class NoteScreenComponent extends BaseScreenComponent<Props, State> implements B | ||||
| 		return { header: null }; | ||||
| 	} | ||||
|  | ||||
| 	public constructor(props: Props) { | ||||
| 	public constructor(props: ComponentProps) { | ||||
| 		super(props); | ||||
|  | ||||
| 		this.state = { | ||||
| @@ -206,7 +209,10 @@ class NoteScreenComponent extends BaseScreenComponent<Props, State> implements B | ||||
|  | ||||
| 		const saveDialog = async () => { | ||||
| 			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 === 'save') await this.saveNoteButton_press(); | ||||
| @@ -269,7 +275,7 @@ class NoteScreenComponent extends BaseScreenComponent<Props, State> implements B | ||||
| 			try { | ||||
| 				await CommandService.instance().execute('openItem', msg); | ||||
| 			} 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) { | ||||
| 			const resizeLargeImages = Setting.value('imageResizing'); | ||||
| 			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)')}`, [ | ||||
| 					{ text: _('Yes'), id: 'yes' }, | ||||
| 					{ text: _('No'), id: 'no' }, | ||||
| 					{ text: _('Cancel'), id: 'cancel' }, | ||||
| 				]); | ||||
| 				const userAnswer = await this.props.dialogs.showMenu( | ||||
| 					`${_('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: _('Yes'), id: 'yes' }, | ||||
| 						{ text: _('No'), id: 'no' }, | ||||
| 						{ text: _('Cancel'), id: 'cancel' }, | ||||
| 					]); | ||||
| 				if (userAnswer === 'yes') return await saveResizedImage(); | ||||
| 				if (userAnswer === 'no') return await saveOriginalImage(); | ||||
| 				if (userAnswer === 'cancel') return false; | ||||
| 				if (userAnswer === 'cancel' || !userAnswer) return false; | ||||
| 			} else if (resizeLargeImages === 'alwaysResize') { | ||||
| 				return await saveResizedImage(); | ||||
| 			} | ||||
| @@ -759,7 +766,7 @@ class NoteScreenComponent extends BaseScreenComponent<Props, State> implements B | ||||
| 				if (!done) return null; | ||||
| 			} else { | ||||
| 				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; | ||||
| 				} else { | ||||
| 					await shim.fsDriver().copy(localFilePath, targetPath); | ||||
| @@ -773,7 +780,7 @@ class NoteScreenComponent extends BaseScreenComponent<Props, State> implements B | ||||
| 			} | ||||
| 		} catch (error) { | ||||
| 			reg.logger().warn('Could not attach file:', error); | ||||
| 			await dialogs.error(this, error.message); | ||||
| 			await this.props.dialogs.error(error.message); | ||||
| 			return null; | ||||
| 		} | ||||
|  | ||||
| @@ -996,7 +1003,7 @@ class NoteScreenComponent extends BaseScreenComponent<Props, State> implements B | ||||
| 			await Linking.openURL(url); | ||||
| 		} catch (error) { | ||||
| 			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 { | ||||
| 			await Linking.openURL(note.source_url); | ||||
| 		} 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' }); | ||||
| 		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 === '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} /> | ||||
|  | ||||
| 				<DialogBox | ||||
| 					// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied | ||||
| 					ref={(dialogbox: any) => { | ||||
| 						this.dialogbox = dialogbox; | ||||
| 					}} | ||||
| 				/> | ||||
| 				{noteTagDialog} | ||||
| 			</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 | ||||
| // how the new note should be rendered | ||||
| const NoteScreenWrapper = (props: Props) => { | ||||
| 	const dialogs = useContext(DialogContext); | ||||
| 	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 { _ } from '@joplin/lib/locale'; | ||||
| 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 { AppState } from '../../utils/types'; | ||||
| import { FolderEntity, NoteEntity, TagEntity } from '@joplin/lib/services/database/types'; | ||||
| import { itemIsInTrash } from '@joplin/lib/services/trash'; | ||||
| import AccessibleView from '../accessibility/AccessibleView'; | ||||
| import { Dispatch } from 'redux'; | ||||
| import { DialogContext, DialogControl } from '../DialogManager'; | ||||
| import { useContext } from 'react'; | ||||
|  | ||||
| interface Props { | ||||
| 	dispatch: Dispatch; | ||||
| @@ -46,17 +45,18 @@ interface State { | ||||
|  | ||||
| } | ||||
|  | ||||
| interface ComponentProps extends Props { | ||||
| 	dialogManager: DialogControl; | ||||
| } | ||||
|  | ||||
| type Styles = Record<string, ViewStyle|TextStyle>; | ||||
|  | ||||
| class NotesScreenComponent extends BaseScreenComponent<Props, State> { | ||||
| 	// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Partial refactor of old code from before rule was applied | ||||
| 	private dialogbox: any; | ||||
|  | ||||
| class NotesScreenComponent extends BaseScreenComponent<ComponentProps, State> { | ||||
| 	private onAppStateChangeSub_: NativeEventSubscription = null; | ||||
| 	private styles_: Record<number, Styles> = {}; | ||||
| 	private folderPickerOptions_: FolderPickerOptions; | ||||
|  | ||||
| 	public constructor(props: Props) { | ||||
| 	public constructor(props: ComponentProps) { | ||||
| 		super(props); | ||||
| 	} | ||||
|  | ||||
| @@ -99,20 +99,12 @@ class NotesScreenComponent extends BaseScreenComponent<Props, State> { | ||||
| 			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; | ||||
|  | ||||
| 		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() { | ||||
| 		if (!this.styles_) this.styles_ = {}; | ||||
| 		const themeId = this.props.themeId; | ||||
| @@ -132,14 +124,12 @@ class NotesScreenComponent extends BaseScreenComponent<Props, State> { | ||||
| 	} | ||||
|  | ||||
| 	public async componentDidMount() { | ||||
| 		BackButtonService.addHandler(this.backHandler); | ||||
| 		await this.refreshNotes(); | ||||
| 		this.onAppStateChangeSub_ = RNAppState.addEventListener('change', this.onAppStateChange_); | ||||
| 	} | ||||
|  | ||||
| 	public async componentWillUnmount() { | ||||
| 		if (this.onAppStateChangeSub_) this.onAppStateChangeSub_.remove(); | ||||
| 		BackButtonService.removeHandler(this.backHandler); | ||||
| 	} | ||||
|  | ||||
| 	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} /> | ||||
| 				<NoteList /> | ||||
| 				{actionButtonComp} | ||||
| 				<DialogBox | ||||
| 					// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied | ||||
| 					ref={(dialogbox: any) => { | ||||
| 						this.dialogbox = dialogbox; | ||||
| 					}} | ||||
| 				/> | ||||
| 			</AccessibleView> | ||||
| 		); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| const NotesScreenWrapper: React.FC<Props> = props => { | ||||
| 	const dialogManager = useContext(DialogContext); | ||||
| 	return <NotesScreenComponent {...props} dialogManager={dialogManager}/>; | ||||
| }; | ||||
|  | ||||
| const NotesScreen = connect((state: AppState) => { | ||||
| 	return { | ||||
| 		folders: state.folders, | ||||
| @@ -327,6 +316,6 @@ const NotesScreen = connect((state: AppState) => { | ||||
| 		noteSelectionEnabled: state.noteSelectionEnabled, | ||||
| 		notesOrder: stateUtils.notesOrder(state.settings), | ||||
| 	}; | ||||
| })(NotesScreenComponent); | ||||
| })(NotesScreenWrapper); | ||||
|  | ||||
| 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 { ScreenHeader } = require('../ScreenHeader'); | ||||
| const { _ } = require('@joplin/lib/locale'); | ||||
| import { ScreenHeader } from '../ScreenHeader'; | ||||
| import { _ } from '@joplin/lib/locale'; | ||||
| 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 { themeStyle } = require('../global-style'); | ||||
| import shim, { MessageBoxType } from '@joplin/lib/shim'; | ||||
| import { themeStyle } from '../global-style'; | ||||
| 
 | ||||
| class DropboxLoginScreenComponent extends BaseScreenComponent { | ||||
| 	constructor() { | ||||
| 	public constructor() { | ||||
| 		super(); | ||||
| 
 | ||||
| 		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(); | ||||
| 	} | ||||
| 
 | ||||
| 	styles() { | ||||
| 	private styles() { | ||||
| 		const themeId = this.props.themeId; | ||||
| 		const theme = themeStyle(themeId); | ||||
| 
 | ||||
| @@ -47,7 +51,7 @@ class DropboxLoginScreenComponent extends BaseScreenComponent { | ||||
| 		return this.styles_[themeId]; | ||||
| 	} | ||||
| 
 | ||||
| 	render() { | ||||
| 	public render() { | ||||
| 		const theme = themeStyle(this.props.themeId); | ||||
| 
 | ||||
| 		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) */} | ||||
| 					<View style={{ height: 200 }}></View> | ||||
| 				</ScrollView> | ||||
| 
 | ||||
| 				<DialogBox | ||||
| 					ref={dialogbox => { | ||||
| 						this.dialogbox = dialogbox; | ||||
| 					}} | ||||
| 				/> | ||||
| 			</View> | ||||
| 		); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| const DropboxLoginScreen = connect(state => { | ||||
| const DropboxLoginScreen = connect((state: AppState) => { | ||||
| 	return { | ||||
| 		themeId: state.settings.theme, | ||||
| 	}; | ||||
| })(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'); | ||||
| import ScreenHeader from '../ScreenHeader'; | ||||
| 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 { _ } from '@joplin/lib/locale'; | ||||
| 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 { SyncInfo } from '@joplin/lib/services/synchronizer/syncInfoUtils'; | ||||
| 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 { | ||||
| 	// 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 { inputPasswords, onInputPasswordChange } = useInputPasswords(props.passwords); | ||||
| 	const { inputMasterPassword, onMasterPasswordSave, onMasterPasswordChange } = useInputMasterPassword(props.masterKeys, props.activeMasterKeyId); | ||||
| 	const dialogBoxRef = useRef(null); | ||||
|  | ||||
| 	const mkComps = []; | ||||
|  | ||||
| @@ -240,7 +238,7 @@ const EncryptionConfigScreen = (props: Props) => { | ||||
|  | ||||
| 	const onToggleButtonClick = async () => { | ||||
| 		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; | ||||
|  | ||||
| 			try { | ||||
| @@ -312,7 +310,6 @@ const EncryptionConfigScreen = (props: Props) => { | ||||
| 				{nonExistingMasterKeySection} | ||||
| 				<View style={{ flex: 1, height: 20 }}></View> | ||||
| 			</ScrollView> | ||||
| 			<DialogBox ref={dialogBoxRef}/> | ||||
| 		</View> | ||||
| 	); | ||||
| }; | ||||
|   | ||||
| @@ -6,7 +6,7 @@ const Folder = require('@joplin/lib/models/Folder').default; | ||||
| const BaseModel = require('@joplin/lib/BaseModel').default; | ||||
| const { ScreenHeader } = require('../ScreenHeader'); | ||||
| const { BaseScreenComponent } = require('../base-screen'); | ||||
| const { dialogs } = require('../../utils/dialogs.js'); | ||||
| const shim = require('@joplin/lib/shim').default; | ||||
| const { _ } = require('@joplin/lib/locale'); | ||||
| const { default: FolderPicker } = require('../FolderPicker'); | ||||
| 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')); | ||||
| 			folder = await Folder.save(folder, { userSideValidation: true }); | ||||
| 		} 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; | ||||
| 		} | ||||
|  | ||||
| @@ -115,11 +115,6 @@ class FolderScreenComponent extends BaseScreenComponent { | ||||
| 					/> | ||||
| 				</View> | ||||
| 				<View style={{ flex: 1 }} /> | ||||
| 				<dialogs.DialogBox | ||||
| 					ref={dialogbox => { | ||||
| 						this.dialogbox = dialogbox; | ||||
| 					}} | ||||
| 				/> | ||||
| 			</View> | ||||
| 		); | ||||
| 	} | ||||
|   | ||||
| @@ -49,7 +49,6 @@ | ||||
|     "react": "18.3.1", | ||||
|     "react-native": "0.74.1", | ||||
|     "react-native-device-info": "10.14.0", | ||||
|     "react-native-dialogbox": "0.6.10", | ||||
|     "react-native-document-picker": "9.3.0", | ||||
|     "react-native-dropdownalert": "5.1.0", | ||||
|     "react-native-exit-app": "2.0.0", | ||||
|   | ||||
| @@ -64,7 +64,7 @@ import StatusScreen from './components/screens/status'; | ||||
| import SearchScreen from './components/screens/SearchScreen'; | ||||
| const { OneDriveLoginScreen } = require('./components/screens/onedrive-login.js'); | ||||
| 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 SideMenu, { SideMenuPosition } from './components/SideMenu'; | ||||
| import SideMenuContent from './components/side-menu-content'; | ||||
| @@ -1343,7 +1343,7 @@ class AppComponent extends React.Component { | ||||
| 					}, | ||||
| 				}, | ||||
| 			}}> | ||||
| 				<DialogManager> | ||||
| 				<DialogManager themeId={this.props.themeId}> | ||||
| 					{mainContent} | ||||
| 				</DialogManager> | ||||
| 			</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 { Alert } from 'react-native'; | ||||
| import { DialogControl, PromptButton } from '../components/DialogManager'; | ||||
| import { DialogControl } from '../components/DialogManager'; | ||||
| 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 => { | ||||
| 		const defaultButtons: PromptButton[] = [ | ||||
| 			{ | ||||
| 				text: _('OK'), | ||||
| 				onPress: () => resolve(0), | ||||
| 			}, | ||||
| 			{ | ||||
| 				text: _('Cancel'), | ||||
| 				onPress: () => resolve(1), | ||||
| 				style: 'cancel', | ||||
| 			}, | ||||
| 		]; | ||||
| 		const okButton: PromptButton = { | ||||
| 			text: _('OK'), | ||||
| 			onPress: () => resolve(0), | ||||
| 		}; | ||||
| 		const cancelButton: PromptButton = { | ||||
| 			text: _('Cancel'), | ||||
| 			onPress: () => resolve(1), | ||||
| 			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) { | ||||
| 			buttons = options.buttons.map((text, index) => { | ||||
| 				return { | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import { CommandRuntime, CommandDeclaration, CommandContext } from '../services/CommandService'; | ||||
| import { _ } from '../locale'; | ||||
| import Note from '../models/Note'; | ||||
| import shim from '../shim'; | ||||
| import shim, { MessageBoxType } from '../shim'; | ||||
|  | ||||
| export const declaration: CommandDeclaration = { | ||||
| 	name: 'permanentlyDeleteNote', | ||||
| @@ -21,7 +21,7 @@ export const runtime = (): CommandRuntime => { | ||||
| 				buttons: [_('Delete'), _('Cancel')], | ||||
| 				defaultId: 1, | ||||
| 				cancelId: 1, | ||||
| 				type: 'question', | ||||
| 				type: MessageBoxType.Confirm, | ||||
| 			}); | ||||
|  | ||||
| 			if (result === deleteIndex) { | ||||
|   | ||||
| @@ -40,6 +40,20 @@ interface AttachFileToNoteOptions { | ||||
| 	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; | ||||
|  | ||||
| // 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, | ||||
| 	// 0 -> OK | ||||
| 	// 1 -> Cancel | ||||
| 	// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied | ||||
| 	showMessageBox: (_message: string, _options: any = null): Promise<number> => { | ||||
| 	showMessageBox: (_message: string, _options: ShowMessageBoxOptions = null): Promise<number> => { | ||||
| 		throw new Error('Not implemented'); | ||||
| 	}, | ||||
|  | ||||
| 	showErrorDialog: async (message: string): Promise<void> => { | ||||
| 		await shim.showMessageBox(message, { type: MessageBoxType.Error }); | ||||
| 	}, | ||||
|  | ||||
| 	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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user