Desktop: Fixed race condition when loading a note while another one is still loading. Improved performance when loading large note.

pull/1742/head
Laurent Cozic 2019-07-17 22:42:53 +01:00
parent 30d0dfb424
commit fbb3543818
1 changed files with 94 additions and 87 deletions

View File

@ -71,6 +71,7 @@ class NoteTextComponent extends React.Component {
newNote: null, newNote: null,
noteTags: [], noteTags: [],
showRevisions: false, showRevisions: false,
loading: false,
// If the current note was just created, and the title has never been // If the current note was just created, and the title has never been
// changed by the user, this variable contains that note ID. Used // changed by the user, this variable contains that note ID. Used
@ -95,6 +96,7 @@ class NoteTextComponent extends React.Component {
this.lastSetMarkers_ = ''; this.lastSetMarkers_ = '';
this.lastSetMarkersOptions_ = {}; this.lastSetMarkersOptions_ = {};
this.selectionRange_ = null; this.selectionRange_ = null;
this.lastComponentUpdateNoteId_ = null;
this.noteSearchBar_ = React.createRef(); this.noteSearchBar_ = React.createRef();
// Complicated but reliable method to get editor content height // Complicated but reliable method to get editor content height
@ -388,6 +390,14 @@ class NoteTextComponent extends React.Component {
this.webviewRef().closeDevTools(); this.webviewRef().closeDevTools();
} }
} }
const currentNoteId = this.state.note ? this.state.note.id : null;
if (this.lastComponentUpdateNoteId_ !== currentNoteId) {
const undoManager = this.editor_.editor.getSession().getUndoManager();
undoManager.reset();
this.editor_.editor.getSession().setUndoManager(undoManager);
this.lastComponentUpdateNoteId_ = currentNoteId;
}
} }
webviewRef() { webviewRef() {
@ -397,6 +407,8 @@ class NoteTextComponent extends React.Component {
} }
async saveIfNeeded(saveIfNewNote = false, options = {}) { async saveIfNeeded(saveIfNewNote = false, options = {}) {
if (this.state.loading) return;
const forceSave = saveIfNewNote && (this.state.note && !this.state.note.id); const forceSave = saveIfNewNote && (this.state.note && !this.state.note.id);
if (this.scheduleSaveTimeout_) clearTimeout(this.scheduleSaveTimeout_); if (this.scheduleSaveTimeout_) clearTimeout(this.scheduleSaveTimeout_);
@ -447,6 +459,12 @@ class NoteTextComponent extends React.Component {
await this.saveIfNeeded(); await this.saveIfNeeded();
const defer = () => {
this.setState({ loading: false });
}
this.setState({ loading: true });
const previousNote = this.state.note ? Object.assign({}, this.state.note) : null; const previousNote = this.state.note ? Object.assign({}, this.state.note) : null;
const stateNoteId = this.state.note ? this.state.note.id : null; const stateNoteId = this.state.note ? this.state.note.id : null;
@ -470,14 +488,14 @@ class NoteTextComponent extends React.Component {
noteTags = await Tag.tagsByNoteId(noteId); noteTags = await Tag.tagsByNoteId(noteId);
this.lastLoadedNoteId_ = noteId; this.lastLoadedNoteId_ = noteId;
note = noteId ? await Note.load(noteId) : null; note = noteId ? await Note.load(noteId) : null;
if (noteId !== this.lastLoadedNoteId_) return; // Race condition - current note was changed while this one was loading if (noteId !== this.lastLoadedNoteId_) return defer(); // Race condition - current note was changed while this one was loading
if (options.noReloadIfLocalChanges && this.isModified()) return; if (options.noReloadIfLocalChanges && this.isModified()) return defer();
// If the note hasn't been changed, exit now // If the note hasn't been changed, exit now
if (this.state.note && note) { if (this.state.note && note) {
let diff = Note.diffObjects(this.state.note, note); let diff = Note.diffObjects(this.state.note, note);
delete diff.type_; delete diff.type_;
if (!Object.getOwnPropertyNames(diff).length) return; if (!Object.getOwnPropertyNames(diff).length) return defer();
} }
} }
@ -515,22 +533,6 @@ class NoteTextComponent extends React.Component {
} }
if (this.editor_) { if (this.editor_) {
// Calling setValue here does two things:
// 1. It sets the initial value as recorded by the undo manager. If we were to set it instead to "" and wait for the render
// phase to set the value, the initial value would still be "", which means pressing "undo" on a note that has just loaded
// would clear it.
// 2. It resets the undo manager - fixes https://github.com/laurent22/joplin/issues/355
// Note: calling undoManager.reset() doesn't work
try {
this.editor_.editor.getSession().setValue(note && note.body? note.body : '');
} catch (error) {
if (error.message === "Cannot read property 'match' of undefined") {
// The internals of Ace Editor throws an exception when creating a new note,
// but that can be ignored.
} else {
console.error(error);
}
}
this.editor_.editor.clearSelection(); this.editor_.editor.clearSelection();
this.editor_.editor.moveCursorTo(0,0); this.editor_.editor.moveCursorTo(0,0);
@ -595,7 +597,9 @@ class NoteTextComponent extends React.Component {
// if (newState.note) await shared.refreshAttachedResources(this, newState.note.body); // if (newState.note) await shared.refreshAttachedResources(this, newState.note.body);
this.updateHtml(newState.note ? newState.note.markup_language : null, newState.note ? newState.note.body : ''); await this.updateHtml(newState.note ? newState.note.markup_language : null, newState.note ? newState.note.body : '');
defer();
} }
async componentWillReceiveProps(nextProps) { async componentWillReceiveProps(nextProps) {
@ -1416,6 +1420,8 @@ class NoteTextComponent extends React.Component {
} }
createToolbarItems(note) { createToolbarItems(note) {
const markupLanguage = note.markup_language;
const toolbarItems = []; const toolbarItems = [];
if (note && this.state.folder && ['Search', 'Tag'].includes(this.props.notesParentType)) { if (note && this.state.folder && ['Search', 'Tag'].includes(this.props.notesParentType)) {
toolbarItems.push({ toolbarItems.push({
@ -1428,7 +1434,6 @@ class NoteTextComponent extends React.Component {
noteId: note.id, noteId: note.id,
}); });
}, },
// enabled: false,
}); });
} }
@ -1451,83 +1456,85 @@ class NoteTextComponent extends React.Component {
}); });
} }
toolbarItems.push({ if (note.markup_language === Note.MARKUP_LANGUAGE_MARKDOWN) {
tooltip: _('Bold'), toolbarItems.push({
iconName: 'fa-bold', tooltip: _('Bold'),
onClick: () => { return this.commandTextBold(); }, iconName: 'fa-bold',
}); onClick: () => { return this.commandTextBold(); },
});
toolbarItems.push({ toolbarItems.push({
tooltip: _('Italic'), tooltip: _('Italic'),
iconName: 'fa-italic', iconName: 'fa-italic',
onClick: () => { return this.commandTextItalic(); }, onClick: () => { return this.commandTextItalic(); },
}); });
toolbarItems.push({ toolbarItems.push({
type: 'separator', type: 'separator',
}); });
toolbarItems.push({ toolbarItems.push({
tooltip: _('Hyperlink'), tooltip: _('Hyperlink'),
iconName: 'fa-link', iconName: 'fa-link',
onClick: () => { return this.commandTextLink(); }, onClick: () => { return this.commandTextLink(); },
}); });
toolbarItems.push({ toolbarItems.push({
tooltip: _('Code'), tooltip: _('Code'),
iconName: 'fa-code', iconName: 'fa-code',
onClick: () => { return this.commandTextCode(); }, onClick: () => { return this.commandTextCode(); },
}); });
toolbarItems.push({ toolbarItems.push({
tooltip: _('Attach file'), tooltip: _('Attach file'),
iconName: 'fa-paperclip', iconName: 'fa-paperclip',
onClick: () => { return this.commandAttachFile(); }, onClick: () => { return this.commandAttachFile(); },
}); });
toolbarItems.push({ toolbarItems.push({
type: 'separator', type: 'separator',
}); });
toolbarItems.push({ toolbarItems.push({
tooltip: _('Numbered List'), tooltip: _('Numbered List'),
iconName: 'fa-list-ol', iconName: 'fa-list-ol',
onClick: () => { return this.commandTextListOl(); }, onClick: () => { return this.commandTextListOl(); },
}); });
toolbarItems.push({ toolbarItems.push({
tooltip: _('Bulleted List'), tooltip: _('Bulleted List'),
iconName: 'fa-list-ul', iconName: 'fa-list-ul',
onClick: () => { return this.commandTextListUl(); }, onClick: () => { return this.commandTextListUl(); },
}); });
toolbarItems.push({ toolbarItems.push({
tooltip: _('Checkbox'), tooltip: _('Checkbox'),
iconName: 'fa-check-square', iconName: 'fa-check-square',
onClick: () => { return this.commandTextCheckbox(); }, onClick: () => { return this.commandTextCheckbox(); },
}); });
toolbarItems.push({ toolbarItems.push({
tooltip: _('Heading'), tooltip: _('Heading'),
iconName: 'fa-header', iconName: 'fa-header',
onClick: () => { return this.commandTextHeading(); }, onClick: () => { return this.commandTextHeading(); },
}); });
toolbarItems.push({ toolbarItems.push({
tooltip: _('Horizontal Rule'), tooltip: _('Horizontal Rule'),
iconName: 'fa-ellipsis-h', iconName: 'fa-ellipsis-h',
onClick: () => { return this.commandTextHorizontalRule(); }, onClick: () => { return this.commandTextHorizontalRule(); },
}); });
toolbarItems.push({ toolbarItems.push({
tooltip: _('Insert Date Time'), tooltip: _('Insert Date Time'),
iconName: 'fa-calendar-plus-o', iconName: 'fa-calendar-plus-o',
onClick: () => { return this.commandDateTime(); }, onClick: () => { return this.commandDateTime(); },
}); });
toolbarItems.push({ toolbarItems.push({
type: 'separator', type: 'separator',
}); });
}
if (note && this.props.watchedNoteFiles.indexOf(note.id) >= 0) { if (note && this.props.watchedNoteFiles.indexOf(note.id) >= 0) {
toolbarItems.push({ toolbarItems.push({
@ -1814,7 +1821,7 @@ class NoteTextComponent extends React.Component {
const toolbarItems = this.createToolbarItems(note); const toolbarItems = this.createToolbarItems(note);
const toolbar = markupLanguage !== Note.MARKUP_LANGUAGE_MARKDOWN ? null : <Toolbar const toolbar = <Toolbar
style={toolbarStyle} style={toolbarStyle}
items={toolbarItems} items={toolbarItems}
/> />
@ -1853,7 +1860,7 @@ class NoteTextComponent extends React.Component {
delete editorRootStyle.fontSize; delete editorRootStyle.fontSize;
const editor = <AceEditor const editor = <AceEditor
value={body} value={body}
mode="markdown" mode={markupLanguage === Note.MARKUP_LANGUAGE_HTML ? 'text' : 'markdown'}
theme={editorRootStyle.editorTheme} theme={editorRootStyle.editorTheme}
style={editorRootStyle} style={editorRootStyle}
width={editorStyle.width + 'px'} width={editorStyle.width + 'px'}