Improve contextPane notes list performance
This commit is contained in:
parent
65369ebb47
commit
a74c012b65
3 changed files with 68 additions and 17 deletions
|
@ -26,7 +26,7 @@
|
||||||
import React, { forwardRef, useImperativeHandle, useState, memo } from 'react';
|
import React, { forwardRef, useImperativeHandle, useState, memo } from 'react';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
|
|
||||||
const MAX_ALL_NOTES = 7;
|
const MAX_UNEXPANDED_ALL_NOTES = 7;
|
||||||
|
|
||||||
const NoteRow = memo(({ id, title, body, date, onClick, onContextMenu, parentItemType, parentTitle }) => {
|
const NoteRow = memo(({ id, title, body, date, onClick, onContextMenu, parentItemType, parentTitle }) => {
|
||||||
return (
|
return (
|
||||||
|
@ -54,15 +54,32 @@ const NoteRow = memo(({ id, title, body, date, onClick, onContextMenu, parentIte
|
||||||
const NotesList = forwardRef(({ onClick, onContextMenu, onAddChildButtonDown, onAddStandaloneButtonDown }, ref) => {
|
const NotesList = forwardRef(({ onClick, onContextMenu, onAddChildButtonDown, onAddStandaloneButtonDown }, ref) => {
|
||||||
const [notes, setNotes] = useState([]);
|
const [notes, setNotes] = useState([]);
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
const [numVisible, setNumVisible] = useState(0);
|
||||||
const [hasParent, setHasParent] = useState(true);
|
const [hasParent, setHasParent] = useState(true);
|
||||||
useImperativeHandle(ref, () => ({ setNotes, setExpanded, setHasParent }));
|
|
||||||
|
const _setExpanded = (value) => {
|
||||||
|
setExpanded(value);
|
||||||
|
if (value) {
|
||||||
|
setNumVisible(numVisible + 1000);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
setNumVisible(0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
setNotes,
|
||||||
|
setHasParent,
|
||||||
|
setExpanded: _setExpanded
|
||||||
|
}));
|
||||||
|
|
||||||
function handleClickMore() {
|
function handleClickMore() {
|
||||||
setExpanded(true);
|
_setExpanded(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
let childNotes = notes.filter(x => x.isCurrentChild);
|
let childNotes = notes.filter(x => x.isCurrentChild);
|
||||||
let allNotes = notes.filter(x => !x.isCurrentChild);
|
let allNotes = notes.filter(x => !x.isCurrentChild);
|
||||||
|
let visibleNotes = allNotes.slice(0, expanded ? numVisible : MAX_UNEXPANDED_ALL_NOTES);
|
||||||
return (
|
return (
|
||||||
<div className="notes-list">
|
<div className="notes-list">
|
||||||
{hasParent && <section>
|
{hasParent && <section>
|
||||||
|
@ -80,12 +97,12 @@ const NotesList = forwardRef(({ onClick, onContextMenu, onAddChildButtonDown, on
|
||||||
<button onMouseDown={onAddStandaloneButtonDown}>+</button>
|
<button onMouseDown={onAddStandaloneButtonDown}>+</button>
|
||||||
</div>
|
</div>
|
||||||
{!allNotes.length && <div className="empty-row">{Zotero.getString('pane.context.noNotes')}</div>}
|
{!allNotes.length && <div className="empty-row">{Zotero.getString('pane.context.noNotes')}</div>}
|
||||||
{(expanded ? allNotes : allNotes.slice(0, MAX_ALL_NOTES))
|
{visibleNotes.map(note => <NoteRow key={note.id} {...note}
|
||||||
.map(note => <NoteRow key={note.id} {...note}
|
|
||||||
onClick={onClick} onContextMenu={onContextMenu}/>)}
|
onClick={onClick} onContextMenu={onContextMenu}/>)}
|
||||||
{!expanded && allNotes.length > MAX_ALL_NOTES
|
{allNotes.length > visibleNotes.length
|
||||||
&& <div className="more-row" onClick={handleClickMore}>{
|
&& <div className="more-row" onClick={handleClickMore}>{
|
||||||
Zotero.getString('general.numMore', Zotero.Utilities.numberFormat([allNotes.length - MAX_ALL_NOTES], 0))
|
Zotero.getString('general.numMore', Zotero.Utilities.numberFormat(
|
||||||
|
[(allNotes.length - visibleNotes.length) - MAX_UNEXPANDED_ALL_NOTES], 0))
|
||||||
}</div>
|
}</div>
|
||||||
}
|
}
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -156,6 +156,9 @@ var ZoteroContextPane = new function () {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (action == 'select') {
|
else if (action == 'select') {
|
||||||
|
// It seems that changing `hidden` or `collapsed` values might
|
||||||
|
// be related with significant slow down when there are too many
|
||||||
|
// DOM nodes (i.e. 10k notes)
|
||||||
if (Zotero_Tabs.selectedIndex == 0) {
|
if (Zotero_Tabs.selectedIndex == 0) {
|
||||||
_contextPaneSplitter.setAttribute('hidden', true);
|
_contextPaneSplitter.setAttribute('hidden', true);
|
||||||
_contextPane.setAttribute('collapsed', true);
|
_contextPane.setAttribute('collapsed', true);
|
||||||
|
@ -470,9 +473,27 @@ var ZoteroContextPane = new function () {
|
||||||
|
|
||||||
var notesListRef = React.createRef();
|
var notesListRef = React.createRef();
|
||||||
|
|
||||||
|
function _isVisible() {
|
||||||
|
let splitter = Zotero.Prefs.get('layout') == 'stacked'
|
||||||
|
? _contextPaneSplitterStacked : _contextPaneSplitter;
|
||||||
|
|
||||||
|
return Zotero_Tabs.selectedID != 'zotero-pane'
|
||||||
|
&& _panesDeck.selectedIndex == 1
|
||||||
|
&& context.node.selectedIndex == 0
|
||||||
|
&& splitter.getAttribute('state') != 'collapsed';
|
||||||
|
}
|
||||||
|
|
||||||
async function _updateNotesList(useCached) {
|
async function _updateNotesList(useCached) {
|
||||||
var query = input.value;
|
var query = input.value;
|
||||||
var notes;
|
var notes;
|
||||||
|
|
||||||
|
// Calls itself and debounces until notes list becomes
|
||||||
|
// visible, and then updates
|
||||||
|
if (!useCached && !_isVisible()) {
|
||||||
|
context.update();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (useCached && context.cachedNotes.length) {
|
if (useCached && context.cachedNotes.length) {
|
||||||
notes = context.cachedNotes;
|
notes = context.cachedNotes;
|
||||||
}
|
}
|
||||||
|
@ -490,22 +511,25 @@ var ZoteroContextPane = new function () {
|
||||||
notes = await s.search();
|
notes = await s.search();
|
||||||
notes = Zotero.Items.get(notes);
|
notes = Zotero.Items.get(notes);
|
||||||
notes.sort((a, b) => {
|
notes.sort((a, b) => {
|
||||||
a = a.getField('dateModified');
|
a = a.dateModified;
|
||||||
b = b.getField('dateModified');
|
b = b.dateModified;
|
||||||
return b.localeCompare(a);
|
return (a > b ? -1 : (a < b ? 1 : 0));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let cachedNotesIndex = new Map();
|
||||||
|
for (let cachedNote of context.cachedNotes) {
|
||||||
|
cachedNotesIndex.set(cachedNote.id, cachedNote);
|
||||||
|
}
|
||||||
notes = notes.map(note => {
|
notes = notes.map(note => {
|
||||||
var parentItem = note.parentItem;
|
var parentItem = note.parentItem;
|
||||||
// If neither note nor parent item is affected try to return the cached note
|
// If neither note nor parent item is affected try to return the cached note
|
||||||
if (!context.affectedIDs.has(note.id)
|
if (!context.affectedIDs.has(note.id)
|
||||||
&& (!parentItem || !context.affectedIDs.has(parentItem.id))) {
|
&& (!parentItem || !context.affectedIDs.has(parentItem.id))) {
|
||||||
let cachedNote = context.cachedNotes.find(x => x.id == note.id);
|
let cachedNote = cachedNotesIndex.get(note.id);
|
||||||
if (cachedNote) {
|
if (cachedNote) {
|
||||||
return cachedNote;
|
return cachedNote;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var text = note.note;
|
var text = note.note;
|
||||||
text = Zotero.Utilities.unescapeHTML(text);
|
text = Zotero.Utilities.unescapeHTML(text);
|
||||||
text = text.trim();
|
text = text.trim();
|
||||||
|
@ -513,7 +537,6 @@ var ZoteroContextPane = new function () {
|
||||||
var parts = text.split('\n').map(x => x.trim()).filter(x => x.length);
|
var parts = text.split('\n').map(x => x.trim()).filter(x => x.length);
|
||||||
var title = parts[0] && parts[0].slice(0, Zotero.Notes.MAX_TITLE_LENGTH);
|
var title = parts[0] && parts[0].slice(0, Zotero.Notes.MAX_TITLE_LENGTH);
|
||||||
var date = Zotero.Date.sqlToDate(note.dateModified, true);
|
var date = Zotero.Date.sqlToDate(note.dateModified, true);
|
||||||
// This takes half of the CPU time
|
|
||||||
date = Zotero.Date.toFriendlyDate(date);
|
date = Zotero.Date.toFriendlyDate(date);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -852,17 +852,28 @@ Zotero.Date = new function(){
|
||||||
return Zotero.getString("date.relative." + key + "." + (n ? "multiple" : "one"), n);
|
return Zotero.getString("date.relative." + key + "." + (n ? "multiple" : "one"), n);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initializing `toFriendlyDate` formatters, since
|
||||||
|
// `toLocaleDateString` is extremely slow (4500ms vs 200ms
|
||||||
|
// for 10k calls)
|
||||||
|
var _friendlyDateTodayFormatter = new Intl.DateTimeFormat(
|
||||||
|
false, { hour: 'numeric', minute: 'numeric' });
|
||||||
|
|
||||||
|
var _friendlyDateWeekFormatter = new Intl.DateTimeFormat(
|
||||||
|
false, { weekday: 'long' });
|
||||||
|
|
||||||
|
var _friendlyDateRegularFormatter = new Intl.DateTimeFormat(
|
||||||
|
false, { year: '2-digit', month: 'numeric', day: 'numeric' });
|
||||||
|
|
||||||
this.toFriendlyDate = function (date) {
|
this.toFriendlyDate = function (date) {
|
||||||
// 6:14:36 PM
|
// 6:14:36 PM
|
||||||
if (isToday(date)) {
|
if (isToday(date)) {
|
||||||
return date.toLocaleString(false, { hour: 'numeric', minute: 'numeric' })
|
return _friendlyDateTodayFormatter.format(date);
|
||||||
}
|
}
|
||||||
// 'Thursday'
|
// 'Thursday'
|
||||||
if (isThisWeek(date)) {
|
if (isThisWeek(date)) {
|
||||||
return date.toLocaleString(false, { weekday: 'long' });
|
return _friendlyDateWeekFormatter.format(date);
|
||||||
}
|
}
|
||||||
return date.toLocaleDateString(false, { year: '2-digit', month: 'numeric', day: 'numeric' });
|
return _friendlyDateRegularFormatter.format(date);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue