mirror of
https://github.com/laurent22/joplin.git
synced 2025-01-23 18:53:36 +02:00
Mobile: Accessibility: Improve dialog accessibility (#11395)
This commit is contained in:
parent
6eac8d9ccf
commit
84eab775c3
@ -575,7 +575,6 @@ packages/app-mobile/commands/openNote.js
|
||||
packages/app-mobile/commands/scrollToHash.js
|
||||
packages/app-mobile/commands/util/goToNote.js
|
||||
packages/app-mobile/commands/util/showResource.js
|
||||
packages/app-mobile/components/BackButtonDialogBox.js
|
||||
packages/app-mobile/components/BetaChip.js
|
||||
packages/app-mobile/components/CameraView/ActionButtons.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/useBarcodeScanner.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/Dropdown.test.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.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/status.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/util/goToNote.js
|
||||
packages/app-mobile/commands/util/showResource.js
|
||||
packages/app-mobile/components/BackButtonDialogBox.js
|
||||
packages/app-mobile/components/BetaChip.js
|
||||
packages/app-mobile/components/CameraView/ActionButtons.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/useBarcodeScanner.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/Dropdown.test.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.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/status.js
|
||||
packages/app-mobile/components/side-menu-content.js
|
||||
|
@ -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
|
||||
|
13
yarn.lock
13
yarn.lock
@ -8402,7 +8402,6 @@ __metadata:
|
||||
react-dom: 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
|
||||
@ -39434,18 +39433,6 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 9.3.0
|
||||
resolution: "react-native-document-picker@npm:9.3.0"
|
||||
|
Loading…
x
Reference in New Issue
Block a user