diff --git a/.eslintignore b/.eslintignore
index 3cbedff200..16fe037cc6 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -276,6 +276,7 @@ packages/app-desktop/gui/NoteList/utils/defaultListRenderer.js
packages/app-desktop/gui/NoteList/utils/types.js
packages/app-desktop/gui/NoteList/utils/useItemCss.js
packages/app-desktop/gui/NoteList/utils/useRenderedNote.js
+packages/app-desktop/gui/NoteList/utils/useVisibleRange.js
packages/app-desktop/gui/NoteListControls/NoteListControls.js
packages/app-desktop/gui/NoteListControls/commands/focusSearch.js
packages/app-desktop/gui/NoteListControls/commands/index.js
diff --git a/.gitignore b/.gitignore
index 578960e78a..c7a789da9e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -262,6 +262,7 @@ packages/app-desktop/gui/NoteList/utils/defaultListRenderer.js
packages/app-desktop/gui/NoteList/utils/types.js
packages/app-desktop/gui/NoteList/utils/useItemCss.js
packages/app-desktop/gui/NoteList/utils/useRenderedNote.js
+packages/app-desktop/gui/NoteList/utils/useVisibleRange.js
packages/app-desktop/gui/NoteListControls/NoteListControls.js
packages/app-desktop/gui/NoteListControls/commands/focusSearch.js
packages/app-desktop/gui/NoteListControls/commands/index.js
diff --git a/packages/app-desktop/gui/NoteList/NoteList2.tsx b/packages/app-desktop/gui/NoteList/NoteList2.tsx
index 24a726c7d8..3a06ac61e3 100644
--- a/packages/app-desktop/gui/NoteList/NoteList2.tsx
+++ b/packages/app-desktop/gui/NoteList/NoteList2.tsx
@@ -1,5 +1,6 @@
import * as React from 'react';
-import { useMemo, useCallback } from 'react';
+import { _ } from '@joplin/lib/locale';
+import { useMemo, useCallback, useState } from 'react';
import { AppState } from '../../app.reducer';
import BaseModel, { ModelType } from '@joplin/lib/BaseModel';
const { connect } = require('react-redux');
@@ -13,8 +14,11 @@ import NoteListItem from '../NoteListItem/NoteListItem';
import useRenderedNotes from './utils/useRenderedNote';
import useItemCss from './utils/useItemCss';
import useOnContextMenu from '../NoteListItem/utils/useOnContextMenu';
+import useVisibleRange from './utils/useVisibleRange';
const NoteList = (props: Props) => {
+ const [scrollTop, setScrollTop] = useState(0);
+
const listRenderer = defaultListRenderer;
if (listRenderer.flow !== ItemFlow.TopToBottom) throw new Error('Not implemented');
@@ -23,7 +27,9 @@ const NoteList = (props: Props) => {
return listRenderer.itemSize;
}, [listRenderer.itemSize]);
- const renderedNotes = useRenderedNotes(props.notes, props.selectedNoteIds, itemSize, listRenderer);
+ const [startNoteIndex, endNoteIndex] = useVisibleRange(scrollTop, props.size, itemSize, props.notes.length);
+
+ const renderedNotes = useRenderedNotes(startNoteIndex, endNoteIndex, props.notes, props.selectedNoteIds, itemSize, listRenderer);
const noteItemStyle = useMemo(() => {
return {
@@ -74,17 +80,37 @@ const NoteList = (props: Props) => {
props.customCss
);
+ const onScroll = useCallback((event: any) => {
+ setScrollTop(event.target.scrollTop);
+ }, []);
+
+ const renderFiller = (key: string, height: number) => {
+ return
;
+ };
+
+ const renderEmptyList = () => {
+ if (props.notes.length) return null;
+ return {props.folders.length ? _('No notes in here. Create one by clicking on "New note".') : _('There is currently no notebook. Create one by clicking on "New notebook".')}
;
+ };
+
const renderNotes = () => {
+ if (!props.notes.length) return null;
+
const output: JSX.Element[] = [];
- for (const renderedNote of renderedNotes) {
+ output.push(renderFiller('top', startNoteIndex * itemSize.height));
+
+ for (let i = startNoteIndex; i <= endNoteIndex; i++) {
+ const note = props.notes[i];
+ const renderedNote = renderedNotes[note.id];
+
output.push(
{
);
}
+ output.push(renderFiller('bottom', (props.notes.length - endNoteIndex - 1) * itemSize.height));
+
return output;
};
return (
-
+
+ {renderEmptyList()}
{renderNotes()}
);
diff --git a/packages/app-desktop/gui/NoteList/NoteListSource.tsx b/packages/app-desktop/gui/NoteList/NoteListSource.tsx
index 57be29909b..feb758ec75 100644
--- a/packages/app-desktop/gui/NoteList/NoteListSource.tsx
+++ b/packages/app-desktop/gui/NoteList/NoteListSource.tsx
@@ -11,7 +11,6 @@ import NoteListItem from '../NoteListItem';
import CommandService from '@joplin/lib/services/CommandService';
import shim from '@joplin/lib/shim';
import styled from 'styled-components';
-import { themeStyle } from '@joplin/lib/theme';
import ItemList from '../ItemList';
const { connect } = require('react-redux');
import Note from '@joplin/lib/models/Note';
@@ -53,7 +52,7 @@ const NoteListComponent = (props: Props) => {
};
}, []);
- const [itemHeight, setItemHeight] = useState(34);
+ const itemHeight = 34;
const focusItemIID_ = useRef
(null);
const noteListRef = useRef(null);
@@ -408,37 +407,6 @@ const NoteListComponent = (props: Props) => {
};
}, []);
- // eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
- useEffect(() => {
- // When a note list item is styled by userchrome.css, its height is reflected.
- // Ref. https://github.com/laurent22/joplin/pull/6542
- if (dragOverTargetNoteIndex !== null) {
- // When dragged, its height should not be considered.
- // Ref. https://github.com/laurent22/joplin/issues/6639
- return;
- }
- const noteItem = Object.values(itemAnchorRefs_.current)[0]?.current;
- const actualItemHeight = noteItem?.getHeight() ?? 0;
- if (actualItemHeight >= 8) { // To avoid generating too many narrow items
- setItemHeight(actualItemHeight);
- }
- });
-
- const renderEmptyList = () => {
- if (props.notes.length) return null;
-
- const theme = themeStyle(props.themeId);
- const padding = 10;
- const emptyDivStyle = {
- padding: `${padding}px`,
- fontSize: theme.fontSize,
- color: theme.color,
- backgroundColor: theme.backgroundColor,
- fontFamily: theme.fontFamily,
- };
- return {props.folders.length ? _('No notes in here. Create one by clicking on "New note".') : _('There is currently no notebook. Create one by clicking on "New notebook".')}
;
- };
-
const renderItemList = () => {
if (!props.notes.length) return null;
@@ -461,7 +429,6 @@ const NoteListComponent = (props: Props) => {
return (
- {renderEmptyList()}
{renderItemList()}
);
diff --git a/packages/app-desktop/gui/NoteList/style.scss b/packages/app-desktop/gui/NoteList/style.scss
index 10fe949487..4125f15869 100644
--- a/packages/app-desktop/gui/NoteList/style.scss
+++ b/packages/app-desktop/gui/NoteList/style.scss
@@ -3,6 +3,15 @@
height: 100%;
background-color: var(--joplin-background-color3);
border-right: 1px solid var(--joplin-divider-color);
+ overflow-y: scroll;
+
+ > .emptylist {
+ padding: 10px;
+ font-size: var(--joplin-font-size);
+ color: var(--joplin-color);
+ background-color: var(--joplin-background-color);
+ font-family: var(--joplin-font-family);
+ }
}
.note-list-item {
diff --git a/packages/app-desktop/gui/NoteList/utils/types.ts b/packages/app-desktop/gui/NoteList/utils/types.ts
index 5bafb45b6c..c6c0de7fb5 100644
--- a/packages/app-desktop/gui/NoteList/utils/types.ts
+++ b/packages/app-desktop/gui/NoteList/utils/types.ts
@@ -19,7 +19,7 @@ export interface Props {
resizableLayoutEventEmitter: any;
isInsertingNotes: boolean;
folders: FolderEntity[];
- size: any;
+ size: Size;
searches: any[];
selectedSearchId: string;
highlightedWords: string[];
diff --git a/packages/app-desktop/gui/NoteList/utils/useRenderedNote.ts b/packages/app-desktop/gui/NoteList/utils/useRenderedNote.ts
index 963b991923..7830a298e2 100644
--- a/packages/app-desktop/gui/NoteList/utils/useRenderedNote.ts
+++ b/packages/app-desktop/gui/NoteList/utils/useRenderedNote.ts
@@ -4,58 +4,56 @@ import { NoteEntity } from '@joplin/lib/services/database/types';
import { Size } from '@joplin/utils/types';
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
import * as Mustache from 'mustache';
+import { createHash } from 'crypto';
interface RenderedNote {
id: string;
+ hash: string;
html: string;
}
-const useRenderedNotes = (notes: NoteEntity[], selectedNoteIds: string[], itemSize: Size, listRenderer: ListRenderer) => {
- const initialValue = notes.map(n => {
- return {
- id: n.id,
- html: '',
- };
- });
+const hashContent = (content: any) => {
+ return createHash('sha1').update(JSON.stringify(content)).digest('hex');
+};
- const [renderedNotes, setRenderedNotes] = useState(initialValue);
+const prepareViewProps = async (dependencies: ListRendererDepependency[], note: NoteEntity, itemSize: Size, selected: boolean) => {
+ const output: any = {};
- const prepareViewProps = async (dependencies: ListRendererDepependency[], note: NoteEntity, itemSize: Size, selected: boolean) => {
- const output: any = {};
- for (const dep of dependencies) {
+ for (const dep of dependencies) {
- if (dep.startsWith('note.')) {
- const splitted = dep.split('.');
- if (splitted.length !== 2) throw new Error(`Invalid dependency name: ${dep}`);
- const propName = splitted.pop();
- if (!output.note) output.note = {};
- if (!(propName in note)) throw new Error(`Invalid dependency name: ${dep}`);
- output.note[propName] = (note as any)[propName];
- }
-
- if (dep.startsWith('item.size.')) {
- const splitted = dep.split('.');
- if (splitted.length !== 3) throw new Error(`Invalid dependency name: ${dep}`);
- const propName = splitted.pop();
- if (!output.item) output.item = {};
- if (!output.item.size) output.item.size = {};
- if (!(propName in itemSize)) throw new Error(`Invalid dependency name: ${dep}`);
- output.item.size[propName] = (itemSize as any)[propName];
- }
-
- if (dep === 'item.selected') {
- if (!output.item) output.item = {};
- output.item.selected = selected;
- }
+ if (dep.startsWith('note.')) {
+ const splitted = dep.split('.');
+ if (splitted.length !== 2) throw new Error(`Invalid dependency name: ${dep}`);
+ const propName = splitted.pop();
+ if (!output.note) output.note = {};
+ if (!(propName in note)) throw new Error(`Invalid dependency name: ${dep}`);
+ output.note[propName] = (note as any)[propName];
}
- return output;
- };
+ if (dep.startsWith('item.size.')) {
+ const splitted = dep.split('.');
+ if (splitted.length !== 3) throw new Error(`Invalid dependency name: ${dep}`);
+ const propName = splitted.pop();
+ if (!output.item) output.item = {};
+ if (!output.item.size) output.item.size = {};
+ if (!(propName in itemSize)) throw new Error(`Invalid dependency name: ${dep}`);
+ output.item.size[propName] = (itemSize as any)[propName];
+ }
+
+ if (dep === 'item.selected') {
+ if (!output.item) output.item = {};
+ output.item.selected = selected;
+ }
+ }
+
+ return output;
+};
+
+const useRenderedNotes = (startNoteIndex: number, endNoteIndex: number, notes: NoteEntity[], selectedNoteIds: string[], itemSize: Size, listRenderer: ListRenderer) => {
+ const [renderedNotes, setRenderedNotes] = useState>({});
useAsyncEffect(async (event) => {
- const newRenderedNotes: RenderedNote[] = [];
-
- for (const note of notes) {
+ const renderNote = async (note: NoteEntity): Promise => {
const view = await listRenderer.onRenderNote(await prepareViewProps(
listRenderer.dependencies,
note,
@@ -63,16 +61,33 @@ const useRenderedNotes = (notes: NoteEntity[], selectedNoteIds: string[], itemSi
selectedNoteIds.includes(note.id)
));
- newRenderedNotes.push({
- id: note.id,
- html: Mustache.render(listRenderer.itemTemplate, view),
+ if (event.cancelled) return null;
+
+ const viewHash = hashContent(view);
+
+ setRenderedNotes(prev => {
+ if (prev[note.id] && prev[note.id].hash === viewHash) return prev;
+
+ return {
+ ...prev,
+ [note.id]: {
+ id: note.id,
+ hash: viewHash,
+ html: Mustache.render(listRenderer.itemTemplate, view),
+ },
+ };
});
+ };
+
+ const promises: Promise[] = [];
+
+ for (let i = startNoteIndex; i <= endNoteIndex; i++) {
+ const note = notes[i];
+ promises.push(renderNote(note));
}
- if (event.cancelled) return null;
-
- setRenderedNotes(newRenderedNotes);
- }, [notes, selectedNoteIds, itemSize]);
+ await Promise.all(promises);
+ }, [startNoteIndex, endNoteIndex, notes, selectedNoteIds, itemSize, listRenderer]);
return renderedNotes;
};
diff --git a/packages/app-desktop/gui/NoteList/utils/useVisibleRange.ts b/packages/app-desktop/gui/NoteList/utils/useVisibleRange.ts
new file mode 100644
index 0000000000..8f0b11214a
--- /dev/null
+++ b/packages/app-desktop/gui/NoteList/utils/useVisibleRange.ts
@@ -0,0 +1,22 @@
+import { Size } from '@joplin/utils/types';
+import { useMemo } from 'react';
+
+const useVisibleRange = (scrollTop: number, listSize: Size, itemSize: Size, noteCount: number) => {
+ const visibleItemCount = useMemo(() => {
+ return Math.ceil(listSize.height / itemSize.height);
+ }, [listSize.height, itemSize.height]);
+
+ const startNoteIndex = useMemo(() => {
+ return Math.floor(scrollTop / itemSize.height);
+ }, [scrollTop, itemSize.height]);
+
+ const endNoteIndex = useMemo(() => {
+ let output = startNoteIndex + (visibleItemCount - 1);
+ if (output >= noteCount) output = noteCount - 1;
+ return output;
+ }, [visibleItemCount, startNoteIndex, noteCount]);
+
+ return [startNoteIndex, endNoteIndex];
+};
+
+export default useVisibleRange;