1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00

Mobile: Improve image editor load performance (#9281)

This commit is contained in:
Henry Heino 2023-11-12 07:06:16 -08:00 committed by GitHub
parent bd1ddb8522
commit bcbba0973f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 50 additions and 20 deletions

View File

@ -48,6 +48,8 @@ interface Props {
// See react-native-webview's prop with the same name. // See react-native-webview's prop with the same name.
mixedContentMode?: 'never' | 'always'; mixedContentMode?: 'never' | 'always';
allowFileAccessFromJs?: boolean;
// Initial javascript. Must evaluate to true. // Initial javascript. Must evaluate to true.
injectedJavaScript: string; injectedJavaScript: string;
@ -143,6 +145,7 @@ const ExtendedWebView = (props: Props, ref: Ref<WebViewControl>) => {
originWhitelist={['file://*', './*', 'http://*', 'https://*']} originWhitelist={['file://*', './*', 'http://*', 'https://*']}
mixedContentMode={props.mixedContentMode} mixedContentMode={props.mixedContentMode}
allowFileAccess={true} allowFileAccess={true}
allowFileAccessFromFileURLs={props.allowFileAccessFromJs}
injectedJavaScript={props.injectedJavaScript} injectedJavaScript={props.injectedJavaScript}
onMessage={props.onMessage} onMessage={props.onMessage}
onError={props.onError} onError={props.onError}

View File

@ -19,12 +19,9 @@ const logger = Logger.create('ImageEditor');
type OnSaveCallback = (svgData: string)=> void; type OnSaveCallback = (svgData: string)=> void;
type OnCancelCallback = ()=> void; type OnCancelCallback = ()=> void;
// Returns the empty string to load from a template.
type LoadInitialSVGCallback = ()=> Promise<string>;
interface Props { interface Props {
themeId: number; themeId: number;
loadInitialSVGData: LoadInitialSVGCallback; resourceFilename: string|null;
onSave: OnSaveCallback; onSave: OnSaveCallback;
onExit: OnCancelCallback; onExit: OnCancelCallback;
} }
@ -178,7 +175,13 @@ const ImageEditor = (props: Props) => {
const injectedJavaScript = useMemo(() => ` const injectedJavaScript = useMemo(() => `
window.onerror = (message, source, lineno) => { window.onerror = (message, source, lineno) => {
window.ReactNativeWebView.postMessage( window.ReactNativeWebView.postMessage(
"error: " + message + " in file://" + source + ", line " + lineno "error: " + message + " in file://" + source + ", line " + lineno,
);
};
window.onunhandledrejection = (error) => {
window.ReactNativeWebView.postMessage(
"error: " + error.reason,
); );
}; };
@ -265,19 +268,17 @@ const ImageEditor = (props: Props) => {
}, [css]); }, [css]);
const onReadyToLoadData = useCallback(async () => { const onReadyToLoadData = useCallback(async () => {
const initialSVGData = await props.loadInitialSVGData?.() ?? '';
// It can take some time for initialSVGData to be transferred to the WebView. // It can take some time for initialSVGData to be transferred to the WebView.
// Thus, do so after the main content has been loaded. // Thus, do so after the main content has been loaded.
webviewRef.current.injectJS(`(async () => { webviewRef.current.injectJS(`(async () => {
if (window.editorControl) { if (window.editorControl) {
const initialSVGData = ${JSON.stringify(initialSVGData)}; const initialSVGPath = ${JSON.stringify(props.resourceFilename)};
const initialTemplateData = ${JSON.stringify(Setting.value('imageeditor.imageTemplate'))}; const initialTemplateData = ${JSON.stringify(Setting.value('imageeditor.imageTemplate'))};
editorControl.loadImageOrTemplate(initialSVGData, initialTemplateData); editorControl.loadImageOrTemplate(initialSVGPath, initialTemplateData);
} }
})();`); })();`);
}, [webviewRef, props.loadInitialSVGData]); }, [webviewRef, props.resourceFilename]);
const onMessage = useCallback(async (event: WebViewMessageEvent) => { const onMessage = useCallback(async (event: WebViewMessageEvent) => {
const data = event.nativeEvent.data; const data = event.nativeEvent.data;
@ -316,6 +317,7 @@ const ImageEditor = (props: Props) => {
themeId={props.themeId} themeId={props.themeId}
html={html} html={html}
injectedJavaScript={injectedJavaScript} injectedJavaScript={injectedJavaScript}
allowFileAccessFromJs={true}
onMessage={onMessage} onMessage={onMessage}
onError={onError} onError={onError}
ref={webviewRef} ref={webviewRef}

View File

@ -57,7 +57,7 @@ describe('createJsDrawEditor', () => {
}); });
// Load no image and an empty template so that autosave can start // Load no image and an empty template so that autosave can start
await editorControl.loadImageOrTemplate(undefined, '{}'); await editorControl.loadImageOrTemplate('', '{}');
expect(calledAutosaveCount).toBe(0); expect(calledAutosaveCount).toBe(0);

View File

@ -120,20 +120,48 @@ export const createJsDrawEditor = (
editor.showLoadingWarning(0); editor.showLoadingWarning(0);
editor.setReadOnly(true); editor.setReadOnly(true);
const fetchInitialSvgData = (resourceUrl: string) => {
return new Promise<string>((resolve, reject) => {
if (!resourceUrl) {
resolve('');
}
// fetch seems to be unable to request file:// URLs.
// https://github.com/react-native-webview/react-native-webview/issues/1560#issuecomment-1783611805
const request = new XMLHttpRequest();
const onError = () => {
reject(`Failed to load initial SVG data: ${request.status}, ${request.statusText}, ${request.responseText}`);
};
request.addEventListener('load', _ => {
resolve(request.responseText);
});
request.addEventListener('error', onError);
request.addEventListener('abort', onError);
request.open('GET', resourceUrl);
request.send();
});
};
const editorControl = { const editorControl = {
editor, editor,
loadImageOrTemplate: async (svgData: string|undefined, templateData: string) => { loadImageOrTemplate: async (resourceUrl: string, templateData: string) => {
// loadFromSVG shows its own loading message. Hide the original. // loadFromSVG shows its own loading message. Hide the original.
editor.hideLoadingWarning(); editor.hideLoadingWarning();
if (svgData && svgData.length > 0) { const svgData = await fetchInitialSvgData(resourceUrl);
await editor.loadFromSVG(svgData);
} else { // Load from a template if no initial data
if (svgData === '') {
await applyTemplateToEditor(editor, templateData); await applyTemplateToEditor(editor, templateData);
// The editor expects to be saved initially (without // The editor expects to be saved initially (without
// unsaved changes). Save now. // unsaved changes). Save now.
saveNow(); saveNow();
} else {
await editor.loadFromSVG(svgData);
} }
// We can now edit and save safely (without data loss). // We can now edit and save safely (without data loss).

View File

@ -81,7 +81,6 @@ class NoteScreenComponent extends BaseScreenComponent {
fromShare: false, fromShare: false,
showCamera: false, showCamera: false,
showImageEditor: false, showImageEditor: false,
loadImageEditorData: null,
imageEditorResource: null, imageEditorResource: null,
noteResources: {}, noteResources: {},
@ -851,9 +850,7 @@ class NoteScreenComponent extends BaseScreenComponent {
const filePath = Resource.fullPath(item); const filePath = Resource.fullPath(item);
this.setState({ this.setState({
showImageEditor: true, showImageEditor: true,
loadImageEditorData: async () => { imageEditorResourceFilepath: filePath,
return await shim.fsDriver().readFile(filePath);
},
imageEditorResource: item, imageEditorResource: item,
}); });
} }
@ -1302,7 +1299,7 @@ class NoteScreenComponent extends BaseScreenComponent {
return <CameraView themeId={this.props.themeId} style={{ flex: 1 }} onPhoto={this.cameraView_onPhoto} onCancel={this.cameraView_onCancel} />; return <CameraView themeId={this.props.themeId} style={{ flex: 1 }} onPhoto={this.cameraView_onPhoto} onCancel={this.cameraView_onCancel} />;
} else if (this.state.showImageEditor) { } else if (this.state.showImageEditor) {
return <ImageEditor return <ImageEditor
loadInitialSVGData={this.state.loadImageEditorData} resourceFilename={this.state.imageEditorResourceFilepath}
themeId={this.props.themeId} themeId={this.props.themeId}
onSave={this.onSaveDrawing} onSave={this.onSaveDrawing}
onExit={this.onCloseDrawing} onExit={this.onCloseDrawing}