From b9118a90be39cef50274d174f78e5e0ad8908723 Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Thu, 10 May 2018 12:02:39 +0100 Subject: [PATCH] All: Resolves #443: Various optimisations to make dealing with large notes easier and make Katex re-rendering faster --- CliClient/tests/ArrayUtils.js | 9 +++ ElectronClient/app/gui/NoteText.jsx | 81 ++++++++++++++++++++----- ReactNativeClient/lib/ArrayUtils.js | 13 ++++ ReactNativeClient/lib/MdToHtml.js | 9 ++- ReactNativeClient/lib/MdToHtml_Katex.js | 23 ++++++- 5 files changed, 114 insertions(+), 21 deletions(-) diff --git a/CliClient/tests/ArrayUtils.js b/CliClient/tests/ArrayUtils.js index bef63a8b8..fd2a5fbde 100644 --- a/CliClient/tests/ArrayUtils.js +++ b/CliClient/tests/ArrayUtils.js @@ -44,4 +44,13 @@ describe('ArrayUtils', function() { done(); }); + it('should compare arrays', async (done) => { + expect(ArrayUtils.contentEquals([], [])).toBe(true); + expect(ArrayUtils.contentEquals(['a'], ['a'])).toBe(true); + expect(ArrayUtils.contentEquals(['b', 'a'], ['a', 'b'])).toBe(true); + expect(ArrayUtils.contentEquals(['b'], ['a', 'b'])).toBe(false); + + done(); + }); + }); \ No newline at end of file diff --git a/ElectronClient/app/gui/NoteText.jsx b/ElectronClient/app/gui/NoteText.jsx index ee0aa504c..2c1afbcb2 100644 --- a/ElectronClient/app/gui/NoteText.jsx +++ b/ElectronClient/app/gui/NoteText.jsx @@ -23,6 +23,8 @@ const fs = require('fs-extra'); const {clipboard} = require('electron') const md5 = require('md5'); const mimeUtils = require('lib/mime-utils.js').mime; +const NoteBodyViewer = require('./NoteBodyViewer.min.js'); +const ArrayUtils = require('lib/ArrayUtils'); require('brace/mode/markdown'); // https://ace.c9.io/build/kitchen-sink.html @@ -50,6 +52,7 @@ class NoteTextComponent extends React.Component { // changed by the user, this variable contains that note ID. Used // to automatically set the title. newAndNoTitleChangeNoteId: null, + bodyHtml: '', }; this.lastLoadedNoteId_ = null; @@ -58,6 +61,8 @@ class NoteTextComponent extends React.Component { this.ignoreNextEditorScroll_ = false; this.scheduleSaveTimeout_ = null; this.restoreScrollTop_ = null; + this.lastSetHtml_ = ''; + this.lastSetMarkers_ = []; // Complicated but reliable method to get editor content height // https://github.com/ajaxorg/ace/issues/2046 @@ -302,7 +307,12 @@ class NoteTextComponent extends React.Component { newState.newAndNoTitleChangeNoteId = null; } + this.lastSetHtml_ = ''; + this.lastSetMarkers_ = []; + this.setState(newState); + + this.updateHtml(newState.note ? newState.note.body : ''); } async componentWillReceiveProps(nextProps) { @@ -542,9 +552,52 @@ class NoteTextComponent extends React.Component { aceEditor_change(body) { shared.noteComponent_change(this, 'body', body); + this.scheduleHtmlUpdate(); this.scheduleSave(); } + scheduleHtmlUpdate(timeout = 500) { + if (this.scheduleHtmlUpdateIID_) { + clearTimeout(this.scheduleHtmlUpdateIID_); + this.scheduleHtmlUpdateIID_ = null; + } + + if (timeout) { + this.scheduleHtmlUpdateIID_ = setTimeout(() => { + this.updateHtml(); + }, timeout); + } else { + this.updateHtml(); + } + } + + updateHtml(body = null) { + const mdOptions = { + onResourceLoaded: () => { + this.updateHtml(); + this.forceUpdate(); + }, + postMessageSyntax: 'ipcRenderer.sendToHost', + }; + + const theme = themeStyle(this.props.theme); + + let bodyToRender = body; + if (bodyToRender === null) bodyToRender = this.state.note && this.state.note.body ? this.state.note.body : ''; + let bodyHtml = ''; + + const visiblePanes = this.props.visiblePanes || ['editor', 'viewer']; + + if (!bodyToRender.trim() && visiblePanes.indexOf('viewer') >= 0 && visiblePanes.indexOf('editor') < 0) { + // Fixes https://github.com/laurent22/joplin/issues/217 + bodyToRender = '*' + _('This note has no content. Click on "%s" to toggle the editor and edit the note.', _('Layout')) + '*'; + } + + bodyHtml = this.mdToHtml().render(bodyToRender, theme, mdOptions); + + this.setState({ bodyHtml: bodyHtml }); + } + async doCommand(command) { if (!command) return; @@ -601,6 +654,8 @@ class NoteTextComponent extends React.Component { note: Object.assign({}, note), lastSavedNote: Object.assign({}, note), }); + + this.updateHtml(note.body); } catch (error) { reg.logger().error(error); bridge().showErrorMessageBox(error.message); @@ -748,25 +803,21 @@ class NoteTextComponent extends React.Component { } if (this.state.webviewReady) { - const mdOptions = { - onResourceLoaded: () => { - this.forceUpdate(); - }, - postMessageSyntax: 'ipcRenderer.sendToHost', - }; + let html = this.state.bodyHtml; - let bodyToRender = body; - if (!bodyToRender.trim() && visiblePanes.indexOf('viewer') >= 0 && visiblePanes.indexOf('editor') < 0) { - // Fixes https://github.com/laurent22/joplin/issues/217 - bodyToRender = '*' + _('This note has no content. Click on "%s" to toggle the editor and edit the note.', _('Layout')) + '*'; + const htmlHasChanged = this.lastSetHtml_ !== html; + if (htmlHasChanged) { + this.webview_.send('setHtml', html); + this.lastSetHtml_ = html; } - const html = this.mdToHtml().render(bodyToRender, theme, mdOptions); - this.webview_.send('setHtml', html); - const search = BaseModel.byId(this.props.searches, this.props.selectedSearchId); const keywords = search ? Search.keywords(search.query_pattern) : []; - this.webview_.send('setMarkers', keywords); + + if (htmlHasChanged || !ArrayUtils.contentEquals(this.lastSetMarkers_, keywords)) { + this.lastSetMarkers_ = []; + this.webview_.send('setMarkers', keywords); + } } const toolbarItems = []; @@ -812,7 +863,7 @@ class NoteTextComponent extends React.Component { const titleBarDate = {time.formatMsToLocal(note.user_updated_time)} - const viewer = '; @@ -29,6 +42,8 @@ class MdToHtml_Katex { } async loadAssets() { + if (this.assetsLoaded_) return; + // In node, the fonts are simply copied using copycss to where Katex expects to find them, which is under app/gui/note-viewer/fonts // In React Native, it's more complicated and we need to download and copy them to the right directory. Ideally, we should embed @@ -43,6 +58,8 @@ class MdToHtml_Katex { await shim.fetchBlob('https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.9.0-beta1/fonts/KaTeX_Math-Italic.woff2', { overwrite: false, path: baseDir + '/fonts/KaTeX_Math-Italic.woff2' }); await shim.fetchBlob('https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.9.0-beta1/fonts/KaTeX_Size1-Regular.woff2', { overwrite: false, path: baseDir + '/fonts/KaTeX_Size1-Regular.woff2' }); } + + this.assetsLoaded_ = true; } }