You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-07-16 00:14:34 +02:00
Chore: Mobile: Add internal support for taking multiple pictures from a camera component (#12357)
This commit is contained in:
@ -620,9 +620,12 @@ packages/app-mobile/components/CameraView/Camera/index.js
|
||||
packages/app-mobile/components/CameraView/Camera/types.js
|
||||
packages/app-mobile/components/CameraView/CameraView.test.js
|
||||
packages/app-mobile/components/CameraView/CameraView.js
|
||||
packages/app-mobile/components/CameraView/CameraViewMultiPage.test.js
|
||||
packages/app-mobile/components/CameraView/CameraViewMultiPage.js
|
||||
packages/app-mobile/components/CameraView/ScannedBarcodes.js
|
||||
packages/app-mobile/components/CameraView/types.js
|
||||
packages/app-mobile/components/CameraView/utils/fitRectIntoBounds.js
|
||||
packages/app-mobile/components/CameraView/utils/testing.js
|
||||
packages/app-mobile/components/CameraView/utils/useBarcodeScanner.js
|
||||
packages/app-mobile/components/Checkbox.js
|
||||
packages/app-mobile/components/DialogManager/PromptButton.js
|
||||
@ -888,6 +891,7 @@ packages/app-mobile/utils/fs-driver/testUtil/createFilesFromPathRecord.js
|
||||
packages/app-mobile/utils/fs-driver/testUtil/verifyDirectoryMatches.js
|
||||
packages/app-mobile/utils/getPackageInfo.js
|
||||
packages/app-mobile/utils/getVersionInfoText.js
|
||||
packages/app-mobile/utils/hooks/useBackHandler.js
|
||||
packages/app-mobile/utils/hooks/useKeyboardState.js
|
||||
packages/app-mobile/utils/hooks/useOnLongPressProps.js
|
||||
packages/app-mobile/utils/hooks/useReduceMotionEnabled.js
|
||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -595,9 +595,12 @@ packages/app-mobile/components/CameraView/Camera/index.js
|
||||
packages/app-mobile/components/CameraView/Camera/types.js
|
||||
packages/app-mobile/components/CameraView/CameraView.test.js
|
||||
packages/app-mobile/components/CameraView/CameraView.js
|
||||
packages/app-mobile/components/CameraView/CameraViewMultiPage.test.js
|
||||
packages/app-mobile/components/CameraView/CameraViewMultiPage.js
|
||||
packages/app-mobile/components/CameraView/ScannedBarcodes.js
|
||||
packages/app-mobile/components/CameraView/types.js
|
||||
packages/app-mobile/components/CameraView/utils/fitRectIntoBounds.js
|
||||
packages/app-mobile/components/CameraView/utils/testing.js
|
||||
packages/app-mobile/components/CameraView/utils/useBarcodeScanner.js
|
||||
packages/app-mobile/components/Checkbox.js
|
||||
packages/app-mobile/components/DialogManager/PromptButton.js
|
||||
@ -863,6 +866,7 @@ packages/app-mobile/utils/fs-driver/testUtil/createFilesFromPathRecord.js
|
||||
packages/app-mobile/utils/fs-driver/testUtil/verifyDirectoryMatches.js
|
||||
packages/app-mobile/utils/getPackageInfo.js
|
||||
packages/app-mobile/utils/getVersionInfoText.js
|
||||
packages/app-mobile/utils/hooks/useBackHandler.js
|
||||
packages/app-mobile/utils/hooks/useKeyboardState.js
|
||||
packages/app-mobile/utils/hooks/useOnLongPressProps.js
|
||||
packages/app-mobile/utils/hooks/useReduceMotionEnabled.js
|
||||
|
@ -136,17 +136,16 @@ const ActionButtons: React.FC<Props> = props => {
|
||||
</View>
|
||||
);
|
||||
|
||||
|
||||
return <>
|
||||
<View style={styles.buttonRowContainerTop}>
|
||||
<IconButton
|
||||
{props.onCancelPhoto && <IconButton
|
||||
themeId={props.themeId}
|
||||
iconName='ionicon arrow-back'
|
||||
containerStyle={styles.buttonContainer}
|
||||
iconStyle={styles.buttonContent}
|
||||
onPress={props.onCancelPhoto}
|
||||
description={_('Back')}
|
||||
/>
|
||||
/>}
|
||||
</View>
|
||||
{props.cameraReady ? cameraActions : <ActivityIndicator/>}
|
||||
</>;
|
||||
|
@ -9,10 +9,12 @@ import { TextInput } from 'react-native';
|
||||
const Camera = (props: Props, ref: ForwardedRef<CameraRef>) => {
|
||||
useImperativeHandle(ref, () => ({
|
||||
takePictureAsync: async () => {
|
||||
const path = `${shim.fsDriver().getCacheDirectoryPath()}/test-photo.svg`;
|
||||
const parentDir = shim.fsDriver().getCacheDirectoryPath();
|
||||
await shim.fsDriver().mkdir(parentDir);
|
||||
const path = `${parentDir}/test-photo.svg`;
|
||||
await shim.fsDriver().writeFile(
|
||||
path,
|
||||
`<svg viewBox="0 0 232 78" width="232" height="78" version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg">
|
||||
`<svg viewBox="0 -70 232 78" width="232" height="78" version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg">
|
||||
<text style="font-family: serif; font-size: 104px; fill: rgb(128, 51, 128);">Test!</text>
|
||||
</svg>`,
|
||||
'utf8',
|
||||
|
@ -4,6 +4,7 @@ import { CameraResult } from './types';
|
||||
import { fireEvent, render, screen } from '../../utils/testing/testingLibrary';
|
||||
import createMockReduxStore from '../../utils/testing/createMockReduxStore';
|
||||
import TestProviderStack from '../testing/TestProviderStack';
|
||||
import { acceptCameraPermission, rejectCameraPermission, setQrCodeData, startCamera } from './utils/testing';
|
||||
|
||||
interface WrapperProps {
|
||||
onPhoto?: (result: CameraResult)=> void;
|
||||
@ -24,26 +25,6 @@ const CameraViewWrapper: React.FC<WrapperProps> = props => {
|
||||
</TestProviderStack>;
|
||||
};
|
||||
|
||||
const rejectCameraPermission = () => {
|
||||
const rejectPermissionButton = screen.getByRole('button', { name: 'Reject permission' });
|
||||
fireEvent.press(rejectPermissionButton);
|
||||
};
|
||||
|
||||
const acceptCameraPermission = () => {
|
||||
const acceptPermissionButton = screen.getByRole('button', { name: 'Accept permission' });
|
||||
fireEvent.press(acceptPermissionButton);
|
||||
};
|
||||
|
||||
const startCamera = () => {
|
||||
const startCameraButton = screen.getByRole('button', { name: 'On camera ready' });
|
||||
fireEvent.press(startCameraButton);
|
||||
};
|
||||
|
||||
const setQrCodeData = (data: string) => {
|
||||
const qrCodeDataInput = screen.getByPlaceholderText('QR code data');
|
||||
fireEvent.changeText(qrCodeDataInput, data);
|
||||
};
|
||||
|
||||
describe('CameraView', () => {
|
||||
test('should hide permissions error if camera permission is granted', async () => {
|
||||
const view = render(<CameraViewWrapper/>);
|
||||
@ -85,3 +66,4 @@ describe('CameraView', () => {
|
||||
view.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { Text, StyleSheet, Linking, View, Platform, useWindowDimensions } from 'react-native';
|
||||
@ -10,15 +10,15 @@ import ActionButtons from './ActionButtons';
|
||||
import { CameraDirection } from '@joplin/lib/models/settings/builtInMetadata';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import { LinkButton, PrimaryButton } from '../buttons';
|
||||
import BackButtonService from '../../services/BackButtonService';
|
||||
import { themeStyle } from '../global-style';
|
||||
import fitRectIntoBounds from './utils/fitRectIntoBounds';
|
||||
import useBarcodeScanner from './utils/useBarcodeScanner';
|
||||
import ScannedBarcodes from './ScannedBarcodes';
|
||||
import { CameraRef } from './Camera/types';
|
||||
import Camera from './Camera';
|
||||
import { CameraResult } from './types';
|
||||
import Camera from './Camera/index.jest';
|
||||
import { CameraResult, OnInsertBarcode } from './types';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import useBackHandler from '../../utils/hooks/useBackHandler';
|
||||
|
||||
const logger = Logger.create('CameraView');
|
||||
|
||||
@ -28,8 +28,10 @@ interface Props {
|
||||
cameraType: CameraDirection;
|
||||
cameraRatio: string;
|
||||
onPhoto: (data: CameraResult)=> void;
|
||||
onCancel: ()=> void;
|
||||
onInsertBarcode: (barcodeText: string)=> void;
|
||||
// If null, cancelling should be handled by the parent
|
||||
// component
|
||||
onCancel: (()=> void)|null;
|
||||
onInsertBarcode: OnInsertBarcode;
|
||||
}
|
||||
|
||||
interface UseStyleProps {
|
||||
@ -107,16 +109,7 @@ const CameraViewComponent: React.FC<Props> = props => {
|
||||
const cameraRef = useRef<CameraRef|null>(null);
|
||||
const [cameraReady, setCameraReady] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = () => {
|
||||
props.onCancel();
|
||||
return true;
|
||||
};
|
||||
BackButtonService.addHandler(handler);
|
||||
return () => {
|
||||
BackButtonService.removeHandler(handler);
|
||||
};
|
||||
}, [props.onCancel]);
|
||||
useBackHandler(props.onCancel);
|
||||
|
||||
const onCameraReverse = useCallback(() => {
|
||||
const newDirection = props.cameraType === CameraDirection.Front ? CameraDirection.Back : CameraDirection.Front;
|
||||
@ -166,7 +159,7 @@ const CameraViewComponent: React.FC<Props> = props => {
|
||||
overlay = <View style={styles.errorContainer}>
|
||||
<Text>{_('Missing camera permission')}</Text>
|
||||
<LinkButton onPress={() => Linking.openSettings()}>{_('Open settings')}</LinkButton>
|
||||
<PrimaryButton onPress={props.onCancel}>{_('Go back')}</PrimaryButton>
|
||||
{props.onCancel && <PrimaryButton onPress={props.onCancel}>{_('Go back')}</PrimaryButton>}
|
||||
</View>;
|
||||
} else {
|
||||
overlay = <>
|
||||
|
@ -0,0 +1,85 @@
|
||||
import * as React from 'react';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import CameraViewMultiPage, { OnComplete } from './CameraViewMultiPage';
|
||||
import { CameraResult, OnInsertBarcode } from './types';
|
||||
import { Store } from 'redux';
|
||||
import { AppState } from '../../utils/types';
|
||||
import TestProviderStack from '../testing/TestProviderStack';
|
||||
import createMockReduxStore from '../../utils/testing/createMockReduxStore';
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react-native';
|
||||
import { startCamera, takePhoto } from './utils/testing';
|
||||
|
||||
interface WrapperProps {
|
||||
onCancel?: ()=> void;
|
||||
onInsertBarcode?: OnInsertBarcode;
|
||||
onComplete?: OnComplete;
|
||||
}
|
||||
|
||||
let store: Store<AppState>;
|
||||
const WrappedCamera: React.FC<WrapperProps> = ({
|
||||
onCancel = jest.fn(),
|
||||
onComplete = jest.fn(),
|
||||
onInsertBarcode = jest.fn(),
|
||||
}) => {
|
||||
return <TestProviderStack store={store}>
|
||||
<CameraViewMultiPage
|
||||
themeId={Setting.THEME_LIGHT}
|
||||
onCancel={onCancel}
|
||||
onComplete={onComplete}
|
||||
onInsertBarcode={onInsertBarcode}
|
||||
/>
|
||||
</TestProviderStack>;
|
||||
};
|
||||
|
||||
const getNextButton = () => screen.getByRole('button', { name: 'Next' });
|
||||
const queryPhotoCount = () => screen.queryByTestId('photo-count');
|
||||
|
||||
describe('CameraViewMultiPage', () => {
|
||||
beforeEach(() => {
|
||||
store = createMockReduxStore();
|
||||
});
|
||||
|
||||
test('next button should be disabled until a photo has been taken', async () => {
|
||||
render(<WrappedCamera/>);
|
||||
expect(getNextButton()).toBeDisabled();
|
||||
startCamera();
|
||||
// Should still be disabled after starting the camera
|
||||
expect(getNextButton()).toBeDisabled();
|
||||
|
||||
await takePhoto();
|
||||
await waitFor(() => {
|
||||
expect(getNextButton()).not.toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
test('should show a count of the number of photos taken', async () => {
|
||||
render(<WrappedCamera/>);
|
||||
startCamera();
|
||||
|
||||
expect(queryPhotoCount()).toBeNull();
|
||||
|
||||
for (let i = 1; i < 3; i++) {
|
||||
await takePhoto();
|
||||
await waitFor(() => {
|
||||
expect(queryPhotoCount()).toHaveTextContent(String(i));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('pressing "Next" should call onComplete with photo URI(s)', async () => {
|
||||
const onComplete = jest.fn();
|
||||
render(<WrappedCamera onComplete={onComplete}/>);
|
||||
startCamera();
|
||||
|
||||
await takePhoto();
|
||||
await waitFor(() => {
|
||||
expect(getNextButton()).not.toBeDisabled();
|
||||
});
|
||||
|
||||
fireEvent.press(getNextButton());
|
||||
|
||||
const imageResults: CameraResult[] = onComplete.mock.lastCall[0];
|
||||
expect(imageResults).toHaveLength(1);
|
||||
expect(imageResults[0].uri).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,146 @@
|
||||
import * as React from 'react';
|
||||
import { CameraResult } from './types';
|
||||
import { View, StyleSheet, Platform, ImageBackground, ViewStyle, TextStyle } from 'react-native';
|
||||
import CameraView from './CameraView';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { themeStyle } from '../global-style';
|
||||
import { Button, Text } from 'react-native-paper';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
|
||||
import shim from '@joplin/lib/shim';
|
||||
|
||||
export type OnComplete = (photos: CameraResult[])=> void;
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
onCancel: ()=> void;
|
||||
onComplete: OnComplete;
|
||||
onInsertBarcode: (barcodeText: string)=> void;
|
||||
}
|
||||
|
||||
const useStyle = (themeId: number) => {
|
||||
return useMemo(() => {
|
||||
const theme = themeStyle(themeId);
|
||||
return StyleSheet.create({
|
||||
camera: {
|
||||
flex: 1,
|
||||
},
|
||||
root: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
},
|
||||
bottomRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
photoWrapper: {
|
||||
flexGrow: 1,
|
||||
minHeight: 82,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
|
||||
imagePreview: {
|
||||
maxWidth: 70,
|
||||
flexShrink: 1,
|
||||
flexGrow: 1,
|
||||
alignContent: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
imageCountText: {
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
marginTop: 'auto',
|
||||
padding: 2,
|
||||
borderRadius: 4,
|
||||
backgroundColor: theme.backgroundColor2,
|
||||
color: theme.color2,
|
||||
},
|
||||
});
|
||||
}, [themeId]);
|
||||
};
|
||||
|
||||
interface PhotoProps {
|
||||
source: CameraResult;
|
||||
backgroundStyle: ViewStyle;
|
||||
textStyle: TextStyle;
|
||||
label: number;
|
||||
}
|
||||
|
||||
const PhotoPreview: React.FC<PhotoProps> = ({ source, label, backgroundStyle, textStyle }) => {
|
||||
const [uri, setUri] = useState('');
|
||||
|
||||
useAsyncEffect(async (event) => {
|
||||
if (Platform.OS === 'web') {
|
||||
const file = await shim.fsDriver().fileAtPath(source.uri);
|
||||
if (event.cancelled) return;
|
||||
|
||||
const uri = URL.createObjectURL(file);
|
||||
setUri(uri);
|
||||
|
||||
event.onCleanup(() => {
|
||||
URL.revokeObjectURL(uri);
|
||||
});
|
||||
} else {
|
||||
setUri(source.uri);
|
||||
}
|
||||
}, [source]);
|
||||
return <ImageBackground
|
||||
style={backgroundStyle}
|
||||
resizeMode='contain'
|
||||
source={{ uri }}
|
||||
accessibilityLabel={_('%d photo(s) taken', label)}
|
||||
>
|
||||
<Text
|
||||
style={textStyle}
|
||||
testID='photo-count'
|
||||
>{label}</Text>
|
||||
</ImageBackground>;
|
||||
};
|
||||
|
||||
const CameraViewMultiPage: React.FC<Props> = ({
|
||||
onInsertBarcode, onCancel, onComplete, themeId,
|
||||
}) => {
|
||||
const [photos, setPhotos] = useState<CameraResult[]>([]);
|
||||
const onPhoto = useCallback((data: CameraResult) => {
|
||||
setPhotos(photos => [...photos, data]);
|
||||
}, []);
|
||||
|
||||
const onDonePressed = useCallback(() => {
|
||||
onComplete(photos);
|
||||
}, [photos, onComplete]);
|
||||
|
||||
const styles = useStyle(themeId);
|
||||
const renderLastPhoto = () => {
|
||||
if (!photos.length) return null;
|
||||
|
||||
return <PhotoPreview
|
||||
label={photos.length}
|
||||
source={photos[photos.length - 1]}
|
||||
backgroundStyle={styles.imagePreview}
|
||||
textStyle={styles.imageCountText}
|
||||
/>;
|
||||
};
|
||||
|
||||
return <View style={styles.root}>
|
||||
<CameraView
|
||||
onCancel={null}
|
||||
onInsertBarcode={onInsertBarcode}
|
||||
style={styles.camera}
|
||||
onPhoto={onPhoto}
|
||||
/>
|
||||
<View style={styles.bottomRow}>
|
||||
<Button icon='arrow-left' onPress={onCancel}>{_('Back')}</Button>
|
||||
<View style={styles.photoWrapper}>
|
||||
{renderLastPhoto()}
|
||||
</View>
|
||||
<Button
|
||||
icon='arrow-right'
|
||||
disabled={photos.length === 0}
|
||||
onPress={onDonePressed}
|
||||
>{_('Next')}</Button>
|
||||
</View>
|
||||
</View>;
|
||||
};
|
||||
|
||||
export default CameraViewMultiPage;
|
@ -8,11 +8,12 @@ import DismissibleDialog, { DialogSize } from '../DismissibleDialog';
|
||||
import { Chip, Text } from 'react-native-paper';
|
||||
import { isCallbackUrl, parseCallbackUrl } from '@joplin/lib/callbackUrlUtils';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import { OnInsertBarcode } from './types';
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
codeScanner: BarcodeScanner;
|
||||
onInsertCode: (codeText: string)=> void;
|
||||
onInsertCode: OnInsertBarcode;
|
||||
}
|
||||
|
||||
const useStyles = () => {
|
||||
|
@ -1,4 +1,6 @@
|
||||
|
||||
export type OnInsertBarcode = (barcodeText: string)=> void;
|
||||
|
||||
export interface CameraResult {
|
||||
uri: string;
|
||||
type: string;
|
||||
|
28
packages/app-mobile/components/CameraView/utils/testing.ts
Normal file
28
packages/app-mobile/components/CameraView/utils/testing.ts
Normal file
@ -0,0 +1,28 @@
|
||||
// Utilities for use with the CameraView.jest.tsx mock
|
||||
|
||||
import { fireEvent, screen } from '@testing-library/react-native';
|
||||
|
||||
export const rejectCameraPermission = () => {
|
||||
const rejectPermissionButton = screen.getByRole('button', { name: 'Reject permission' });
|
||||
fireEvent.press(rejectPermissionButton);
|
||||
};
|
||||
|
||||
export const acceptCameraPermission = () => {
|
||||
const acceptPermissionButton = screen.getByRole('button', { name: 'Accept permission' });
|
||||
fireEvent.press(acceptPermissionButton);
|
||||
};
|
||||
|
||||
export const startCamera = () => {
|
||||
const startCameraButton = screen.getByRole('button', { name: 'On camera ready' });
|
||||
fireEvent.press(startCameraButton);
|
||||
};
|
||||
|
||||
export const takePhoto = async () => {
|
||||
const takePhotoButton = await screen.findByRole('button', { name: 'Take photo' });
|
||||
fireEvent.press(takePhotoButton);
|
||||
};
|
||||
|
||||
export const setQrCodeData = (data: string) => {
|
||||
const qrCodeDataInput = screen.getByPlaceholderText('QR code data');
|
||||
fireEvent.changeText(qrCodeDataInput, data);
|
||||
};
|
@ -34,7 +34,6 @@ import { themeStyle, editorFont } from '../../global-style';
|
||||
import shared, { BaseNoteScreenComponent, Props as BaseProps } from '@joplin/lib/components/shared/note-screen-shared';
|
||||
import SelectDateTimeDialog from '../../SelectDateTimeDialog';
|
||||
import ShareExtension from '../../../utils/ShareExtension.js';
|
||||
import CameraView from '../../CameraView/CameraView';
|
||||
import { FolderEntity, NoteEntity, ResourceEntity } from '@joplin/lib/services/database/types';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import ImageEditor from '../../NoteEditor/ImageEditor/ImageEditor';
|
||||
@ -68,6 +67,7 @@ import getActivePluginEditorView from '@joplin/lib/services/plugins/utils/getAct
|
||||
import EditorPluginHandler from '@joplin/lib/services/plugins/EditorPluginHandler';
|
||||
import AudioRecordingBanner from '../../voiceTyping/AudioRecordingBanner';
|
||||
import SpeechToTextBanner from '../../voiceTyping/SpeechToTextBanner';
|
||||
import CameraView from '../../CameraView/CameraView';
|
||||
import ShareNoteDialog from '../ShareNoteDialog';
|
||||
import stateToWhenClauseContext from '../../../services/commands/stateToWhenClauseContext';
|
||||
import { defaultWindowId } from '@joplin/lib/reducer';
|
||||
@ -837,6 +837,7 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
pickerResponse: PickerResponse,
|
||||
fileType: string,
|
||||
): Promise<ResourceEntity|null> {
|
||||
logger.debug('Attaching file:', pickerResponse?.uri);
|
||||
if (!pickerResponse) {
|
||||
// User has cancelled
|
||||
return null;
|
||||
@ -918,11 +919,17 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
return resource;
|
||||
}
|
||||
|
||||
private cameraView_onPhoto(data: CameraResult) {
|
||||
void this.attachFile(
|
||||
data,
|
||||
private async cameraView_onPhoto(data: CameraResult|CameraResult[]) {
|
||||
if (!Array.isArray(data)) {
|
||||
data = [data];
|
||||
}
|
||||
|
||||
for (const item of data) {
|
||||
await this.attachFile(
|
||||
item,
|
||||
'image',
|
||||
);
|
||||
}
|
||||
|
||||
this.setState({ showCamera: false });
|
||||
}
|
||||
@ -1524,10 +1531,10 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
|
||||
if (this.state.showCamera) {
|
||||
return <CameraView
|
||||
style={{ flex: 1 }}
|
||||
onPhoto={this.cameraView_onPhoto}
|
||||
onInsertBarcode={this.cameraView_onInsertBarcode}
|
||||
onCancel={this.cameraView_onCancel}
|
||||
style={{ flex: 1 }}
|
||||
/>;
|
||||
} else if (this.state.showImageEditor) {
|
||||
return <ImageEditor
|
||||
|
@ -48,7 +48,7 @@ const useVoiceTyping = ({ locale, provider, onSetPreview, onText }: UseVoiceTypi
|
||||
|
||||
const [redownloadCounter, setRedownloadCounter] = useState(0);
|
||||
|
||||
useQueuedAsyncEffect(async (event: AsyncEffectEvent) => {
|
||||
useQueuedAsyncEffect(async (event) => {
|
||||
try {
|
||||
// Reset the error: If starting voice typing again resolves the error, the error
|
||||
// should be hidden (and voice typing should start).
|
||||
|
20
packages/app-mobile/utils/hooks/useBackHandler.ts
Normal file
20
packages/app-mobile/utils/hooks/useBackHandler.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { useEffect } from 'react';
|
||||
import BackButtonService from '../../services/BackButtonService';
|
||||
|
||||
type OnBackPress = ()=>(void|boolean);
|
||||
|
||||
const useBackHandler = (onBackPress: OnBackPress|null) => {
|
||||
useEffect(() => {
|
||||
if (!onBackPress) return () => {};
|
||||
|
||||
const handler = () => {
|
||||
return !!(onBackPress() ?? true);
|
||||
};
|
||||
BackButtonService.addHandler(handler);
|
||||
return () => {
|
||||
BackButtonService.removeHandler(handler);
|
||||
};
|
||||
}, [onBackPress]);
|
||||
};
|
||||
|
||||
export default useBackHandler;
|
@ -1,8 +1,11 @@
|
||||
import shim from '../shim';
|
||||
const { useEffect } = shim.react();
|
||||
|
||||
type CleanupCallback = ()=> void;
|
||||
|
||||
export interface AsyncEffectEvent {
|
||||
cancelled: boolean;
|
||||
onCleanup: (callback: CleanupCallback)=> void;
|
||||
}
|
||||
|
||||
export type EffectFunction = (event: AsyncEffectEvent)=> Promise<void>;
|
||||
@ -10,10 +13,24 @@ export type EffectFunction = (event: AsyncEffectEvent)=> Promise<void>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
export default function(effect: EffectFunction, dependencies: any[]) {
|
||||
useEffect(() => {
|
||||
const event: AsyncEffectEvent = { cancelled: false };
|
||||
const onCleanupCallbacks: CleanupCallback[] = [];
|
||||
const event: AsyncEffectEvent = {
|
||||
cancelled: false,
|
||||
onCleanup: (callback) => {
|
||||
if (event.cancelled) {
|
||||
callback();
|
||||
} else {
|
||||
onCleanupCallbacks.push(callback);
|
||||
}
|
||||
},
|
||||
};
|
||||
void effect(event);
|
||||
return () => {
|
||||
event.cancelled = true;
|
||||
|
||||
for (const callback of onCleanupCallbacks) {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, dependencies);
|
||||
|
Reference in New Issue
Block a user