From bcbba0973f5d4cf94a7dbe54667099b1c855c47f Mon Sep 17 00:00:00 2001 From: Henry Heino <46334387+personalizedrefrigerator@users.noreply.github.com> Date: Sun, 12 Nov 2023 07:06:16 -0800 Subject: [PATCH] Mobile: Improve image editor load performance (#9281) --- .../app-mobile/components/ExtendedWebView.tsx | 3 ++ .../NoteEditor/ImageEditor/ImageEditor.tsx | 22 ++++++------ .../js-draw/createJsDrawEditor.test.ts | 2 +- .../ImageEditor/js-draw/createJsDrawEditor.ts | 36 ++++++++++++++++--- .../app-mobile/components/screens/Note.tsx | 7 ++-- 5 files changed, 50 insertions(+), 20 deletions(-) diff --git a/packages/app-mobile/components/ExtendedWebView.tsx b/packages/app-mobile/components/ExtendedWebView.tsx index aa9cedd87..d00042e38 100644 --- a/packages/app-mobile/components/ExtendedWebView.tsx +++ b/packages/app-mobile/components/ExtendedWebView.tsx @@ -48,6 +48,8 @@ interface Props { // See react-native-webview's prop with the same name. mixedContentMode?: 'never' | 'always'; + allowFileAccessFromJs?: boolean; + // Initial javascript. Must evaluate to true. injectedJavaScript: string; @@ -143,6 +145,7 @@ const ExtendedWebView = (props: Props, ref: Ref) => { originWhitelist={['file://*', './*', 'http://*', 'https://*']} mixedContentMode={props.mixedContentMode} allowFileAccess={true} + allowFileAccessFromFileURLs={props.allowFileAccessFromJs} injectedJavaScript={props.injectedJavaScript} onMessage={props.onMessage} onError={props.onError} diff --git a/packages/app-mobile/components/NoteEditor/ImageEditor/ImageEditor.tsx b/packages/app-mobile/components/NoteEditor/ImageEditor/ImageEditor.tsx index 20af2e851..32263c369 100644 --- a/packages/app-mobile/components/NoteEditor/ImageEditor/ImageEditor.tsx +++ b/packages/app-mobile/components/NoteEditor/ImageEditor/ImageEditor.tsx @@ -19,12 +19,9 @@ const logger = Logger.create('ImageEditor'); type OnSaveCallback = (svgData: string)=> void; type OnCancelCallback = ()=> void; -// Returns the empty string to load from a template. -type LoadInitialSVGCallback = ()=> Promise; - interface Props { themeId: number; - loadInitialSVGData: LoadInitialSVGCallback; + resourceFilename: string|null; onSave: OnSaveCallback; onExit: OnCancelCallback; } @@ -178,7 +175,13 @@ const ImageEditor = (props: Props) => { const injectedJavaScript = useMemo(() => ` window.onerror = (message, source, lineno) => { 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]); const onReadyToLoadData = useCallback(async () => { - const initialSVGData = await props.loadInitialSVGData?.() ?? ''; - // It can take some time for initialSVGData to be transferred to the WebView. // Thus, do so after the main content has been loaded. webviewRef.current.injectJS(`(async () => { if (window.editorControl) { - const initialSVGData = ${JSON.stringify(initialSVGData)}; + const initialSVGPath = ${JSON.stringify(props.resourceFilename)}; 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 data = event.nativeEvent.data; @@ -316,6 +317,7 @@ const ImageEditor = (props: Props) => { themeId={props.themeId} html={html} injectedJavaScript={injectedJavaScript} + allowFileAccessFromJs={true} onMessage={onMessage} onError={onError} ref={webviewRef} diff --git a/packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/createJsDrawEditor.test.ts b/packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/createJsDrawEditor.test.ts index 59ecce237..e71fbd340 100644 --- a/packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/createJsDrawEditor.test.ts +++ b/packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/createJsDrawEditor.test.ts @@ -57,7 +57,7 @@ describe('createJsDrawEditor', () => { }); // Load no image and an empty template so that autosave can start - await editorControl.loadImageOrTemplate(undefined, '{}'); + await editorControl.loadImageOrTemplate('', '{}'); expect(calledAutosaveCount).toBe(0); diff --git a/packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/createJsDrawEditor.ts b/packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/createJsDrawEditor.ts index f5283b53e..e96851f21 100644 --- a/packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/createJsDrawEditor.ts +++ b/packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/createJsDrawEditor.ts @@ -120,20 +120,48 @@ export const createJsDrawEditor = ( editor.showLoadingWarning(0); editor.setReadOnly(true); + const fetchInitialSvgData = (resourceUrl: string) => { + return new Promise((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 = { editor, - loadImageOrTemplate: async (svgData: string|undefined, templateData: string) => { + loadImageOrTemplate: async (resourceUrl: string, templateData: string) => { // loadFromSVG shows its own loading message. Hide the original. editor.hideLoadingWarning(); - if (svgData && svgData.length > 0) { - await editor.loadFromSVG(svgData); - } else { + const svgData = await fetchInitialSvgData(resourceUrl); + + // Load from a template if no initial data + if (svgData === '') { await applyTemplateToEditor(editor, templateData); // The editor expects to be saved initially (without // unsaved changes). Save now. saveNow(); + } else { + await editor.loadFromSVG(svgData); } // We can now edit and save safely (without data loss). diff --git a/packages/app-mobile/components/screens/Note.tsx b/packages/app-mobile/components/screens/Note.tsx index 5d1384f62..5536b5d22 100644 --- a/packages/app-mobile/components/screens/Note.tsx +++ b/packages/app-mobile/components/screens/Note.tsx @@ -81,7 +81,6 @@ class NoteScreenComponent extends BaseScreenComponent { fromShare: false, showCamera: false, showImageEditor: false, - loadImageEditorData: null, imageEditorResource: null, noteResources: {}, @@ -851,9 +850,7 @@ class NoteScreenComponent extends BaseScreenComponent { const filePath = Resource.fullPath(item); this.setState({ showImageEditor: true, - loadImageEditorData: async () => { - return await shim.fsDriver().readFile(filePath); - }, + imageEditorResourceFilepath: filePath, imageEditorResource: item, }); } @@ -1302,7 +1299,7 @@ class NoteScreenComponent extends BaseScreenComponent { return ; } else if (this.state.showImageEditor) { return