refactor: Move notes_view to React (#761)

This commit is contained in:
Aman Harwara
2021-12-21 20:31:11 +05:30
committed by GitHub
parent f120af3b43
commit 270fcbc3bc
20 changed files with 1495 additions and 1142 deletions

View File

@@ -23,6 +23,7 @@ import {
} from 'mobx';
import { ActionsMenuState } from './actions_menu_state';
import { NotesState } from './notes_state';
import { NotesViewState } from './notes_view_state';
import { NoteTagsState } from './note_tags_state';
import { NoAccountWarningState } from './no_account_warning_state';
import { PreferencesState } from './preferences_state';
@@ -86,6 +87,7 @@ export class AppState {
readonly searchOptions: SearchOptionsState;
readonly notes: NotesState;
readonly tags: TagsState;
readonly notesView: NotesViewState;
isSessionsModalVisible = false;
private appEventObserverRemovers: (() => void)[] = [];
@@ -127,6 +129,11 @@ export class AppState {
this.appEventObserverRemovers
);
this.purchaseFlow = new PurchaseFlowState(application);
this.notesView = new NotesViewState(
application,
this,
this.appEventObserverRemovers
);
this.addAppEventObserver();
this.streamNotesAndTags();
this.onVisibilityChange = () => {

View File

@@ -0,0 +1,541 @@
import {
ApplicationEvent,
CollectionSort,
ContentType,
findInArray,
NotesDisplayCriteria,
PrefKey,
SNNote,
SNTag,
UuidString,
} from '@standardnotes/snjs';
import {
action,
autorun,
computed,
makeObservable,
observable,
reaction,
} from 'mobx';
import { AppState, AppStateEvent } from '.';
import { WebApplication } from '../application';
const MIN_NOTE_CELL_HEIGHT = 51.0;
const DEFAULT_LIST_NUM_NOTES = 20;
const ELEMENT_ID_SEARCH_BAR = 'search-bar';
const ELEMENT_ID_SCROLL_CONTAINER = 'notes-scrollable';
export type DisplayOptions = {
sortBy: CollectionSort;
sortReverse: boolean;
hidePinned: boolean;
showArchived: boolean;
showTrashed: boolean;
hideProtected: boolean;
hideTags: boolean;
hideNotePreview: boolean;
hideDate: boolean;
};
export class NotesViewState {
completedFullSync = false;
noteFilterText = '';
notes: SNNote[] = [];
notesToDisplay = 0;
pageSize = 0;
panelTitle = 'All Notes';
renderedNotes: SNNote[] = [];
searchSubmitted = false;
selectedNotes: Record<UuidString, SNNote> = {};
showDisplayOptionsMenu = false;
displayOptions = {
sortBy: CollectionSort.CreatedAt,
sortReverse: false,
hidePinned: false,
showArchived: false,
showTrashed: false,
hideProtected: false,
hideTags: true,
hideDate: false,
hideNotePreview: false,
};
constructor(
private application: WebApplication,
private appState: AppState,
appObservers: (() => void)[]
) {
this.resetPagination();
appObservers.push(
application.streamItems(ContentType.Note, () => {
this.reloadNotes();
const activeNote = this.appState.notes.activeEditor?.note;
if (this.application.getAppState().notes.selectedNotesCount < 2) {
if (activeNote) {
const discarded = activeNote.deleted || activeNote.trashed;
if (
discarded &&
!this.appState?.selectedTag?.isTrashTag &&
!this.appState?.searchOptions.includeTrashed
) {
this.selectNextOrCreateNew();
} else if (!this.selectedNotes[activeNote.uuid]) {
this.selectNote(activeNote);
}
} else {
this.selectFirstNote();
}
}
}),
application.streamItems([ContentType.Tag], async (items) => {
const tags = items as SNTag[];
/** A tag could have changed its relationships, so we need to reload the filter */
this.reloadNotesDisplayOptions();
this.reloadNotes();
if (findInArray(tags, 'uuid', this.appState.selectedTag?.uuid)) {
/** Tag title could have changed */
this.reloadPanelTitle();
}
}),
application.addEventObserver(async () => {
this.reloadPreferences();
}, ApplicationEvent.PreferencesChanged),
application.addEventObserver(async () => {
this.appState.closeAllEditors();
this.selectFirstNote();
this.setCompletedFullSync(false);
}, ApplicationEvent.SignedIn),
application.addEventObserver(async () => {
this.reloadNotes();
if (
this.notes.length === 0 &&
this.appState.selectedTag?.isAllTag &&
this.noteFilterText === ''
) {
this.createPlaceholderNote();
}
this.setCompletedFullSync(true);
}, ApplicationEvent.CompletedFullSync),
autorun(() => {
if (appState.notes.selectedNotes) {
this.syncSelectedNotes();
}
}),
reaction(
() => [
appState.searchOptions.includeProtectedContents,
appState.searchOptions.includeArchived,
appState.searchOptions.includeTrashed,
],
() => {
this.reloadNotesDisplayOptions();
this.reloadNotes();
}
),
appState.addObserver(async (eventName) => {
if (eventName === AppStateEvent.TagChanged) {
this.handleTagChange();
} else if (eventName === AppStateEvent.ActiveEditorChanged) {
this.handleEditorChange();
} else if (eventName === AppStateEvent.EditorFocused) {
this.toggleDisplayOptionsMenu(false);
}
})
);
makeObservable(this, {
completedFullSync: observable,
displayOptions: observable.struct,
noteFilterText: observable,
notes: observable,
notesToDisplay: observable,
panelTitle: observable,
renderedNotes: observable,
selectedNotes: observable,
showDisplayOptionsMenu: observable,
reloadNotes: action,
reloadPanelTitle: action,
reloadPreferences: action,
resetPagination: action,
setCompletedFullSync: action,
setNoteFilterText: action,
syncSelectedNotes: action,
toggleDisplayOptionsMenu: action,
onFilterEnter: action,
handleFilterTextChanged: action,
optionsSubtitle: computed,
});
window.onresize = () => {
this.resetPagination(true);
};
}
setCompletedFullSync = (completed: boolean) => {
this.completedFullSync = completed;
};
toggleDisplayOptionsMenu = (enabled: boolean) => {
this.showDisplayOptionsMenu = enabled;
};
get searchBarElement() {
return document.getElementById(ELEMENT_ID_SEARCH_BAR);
}
get isFiltering(): boolean {
return !!this.noteFilterText && this.noteFilterText.length > 0;
}
get activeEditorNote() {
return this.appState.notes.activeEditor?.note;
}
reloadPanelTitle = () => {
let title = this.panelTitle;
if (this.isFiltering) {
const resultCount = this.notes.length;
title = `${resultCount} search results`;
} else if (this.appState.selectedTag) {
title = `${this.appState.selectedTag.title}`;
}
this.panelTitle = title;
};
reloadNotes = () => {
const tag = this.appState.selectedTag;
if (!tag) {
return;
}
const notes = this.application.getDisplayableItems(
ContentType.Note
) as SNNote[];
const renderedNotes = notes.slice(0, this.notesToDisplay);
this.notes = notes;
this.renderedNotes = renderedNotes;
this.reloadPanelTitle();
};
reloadNotesDisplayOptions = () => {
const tag = this.appState.selectedTag;
const searchText = this.noteFilterText.toLowerCase();
const isSearching = searchText.length;
let includeArchived: boolean;
let includeTrashed: boolean;
if (isSearching) {
includeArchived = this.appState.searchOptions.includeArchived;
includeTrashed = this.appState.searchOptions.includeTrashed;
} else {
includeArchived = this.displayOptions.showArchived ?? false;
includeTrashed = this.displayOptions.showTrashed ?? false;
}
const criteria = NotesDisplayCriteria.Create({
sortProperty: this.displayOptions.sortBy as CollectionSort,
sortDirection: this.displayOptions.sortReverse ? 'asc' : 'dsc',
tags: tag ? [tag] : [],
includeArchived,
includeTrashed,
includePinned: !this.displayOptions.hidePinned,
includeProtected: !this.displayOptions.hideProtected,
searchQuery: {
query: searchText,
includeProtectedNoteText:
this.appState.searchOptions.includeProtectedContents,
},
});
this.application.setNotesDisplayCriteria(criteria);
};
reloadPreferences = () => {
const freshDisplayOptions = {} as DisplayOptions;
const currentSortBy = this.displayOptions.sortBy;
let sortBy = this.application.getPreference(
PrefKey.SortNotesBy,
CollectionSort.CreatedAt
);
if (
sortBy === CollectionSort.UpdatedAt ||
(sortBy as string) === 'client_updated_at'
) {
/** Use UserUpdatedAt instead */
sortBy = CollectionSort.UpdatedAt;
}
freshDisplayOptions.sortBy = sortBy;
freshDisplayOptions.sortReverse = this.application.getPreference(
PrefKey.SortNotesReverse,
false
);
freshDisplayOptions.showArchived = this.application.getPreference(
PrefKey.NotesShowArchived,
false
);
freshDisplayOptions.showTrashed = this.application.getPreference(
PrefKey.NotesShowTrashed,
false
) as boolean;
freshDisplayOptions.hidePinned = this.application.getPreference(
PrefKey.NotesHidePinned,
false
);
freshDisplayOptions.hideProtected = this.application.getPreference(
PrefKey.NotesHideProtected,
false
);
freshDisplayOptions.hideNotePreview = this.application.getPreference(
PrefKey.NotesHideNotePreview,
false
);
freshDisplayOptions.hideDate = this.application.getPreference(
PrefKey.NotesHideDate,
false
);
freshDisplayOptions.hideTags = this.application.getPreference(
PrefKey.NotesHideTags,
true
);
const displayOptionsChanged =
freshDisplayOptions.sortBy !== this.displayOptions.sortBy ||
freshDisplayOptions.sortReverse !== this.displayOptions.sortReverse ||
freshDisplayOptions.hidePinned !== this.displayOptions.hidePinned ||
freshDisplayOptions.showArchived !== this.displayOptions.showArchived ||
freshDisplayOptions.showTrashed !== this.displayOptions.showTrashed ||
freshDisplayOptions.hideProtected !== this.displayOptions.hideProtected ||
freshDisplayOptions.hideTags !== this.displayOptions.hideTags;
this.displayOptions = freshDisplayOptions;
if (displayOptionsChanged) {
this.reloadNotesDisplayOptions();
}
this.reloadNotes();
if (freshDisplayOptions.sortBy !== currentSortBy) {
this.selectFirstNote();
}
};
createNewNote = async (focusNewNote = true) => {
this.appState.notes.unselectNotes();
let title = `Note ${this.notes.length + 1}`;
if (this.isFiltering) {
title = this.noteFilterText;
}
await this.appState.createEditor(title);
this.reloadNotes();
this.appState.noteTags.reloadTags();
const noteTitleEditorElement = document.getElementById('note-title-editor');
if (focusNewNote) {
noteTitleEditorElement?.focus();
}
};
createPlaceholderNote = () => {
const selectedTag = this.appState.selectedTag;
if (selectedTag && selectedTag.isSmartTag && !selectedTag.isAllTag) {
return;
}
return this.createNewNote(false);
};
get optionsSubtitle(): string {
let base = '';
if (this.displayOptions.sortBy === CollectionSort.CreatedAt) {
base += ' Date Added';
} else if (this.displayOptions.sortBy === CollectionSort.UpdatedAt) {
base += ' Date Modified';
} else if (this.displayOptions.sortBy === CollectionSort.Title) {
base += ' Title';
}
if (this.displayOptions.showArchived) {
base += ' | + Archived';
}
if (this.displayOptions.showTrashed) {
base += ' | + Trashed';
}
if (this.displayOptions.hidePinned) {
base += ' | Pinned';
}
if (this.displayOptions.hideProtected) {
base += ' | Protected';
}
if (this.displayOptions.sortReverse) {
base += ' | Reversed';
}
return base;
}
paginate = () => {
this.notesToDisplay += this.pageSize;
this.reloadNotes();
if (this.searchSubmitted) {
this.application.getDesktopService().searchText(this.noteFilterText);
}
};
resetPagination = (keepCurrentIfLarger = false) => {
const clientHeight = document.documentElement.clientHeight;
this.pageSize = Math.ceil(clientHeight / MIN_NOTE_CELL_HEIGHT);
if (this.pageSize === 0) {
this.pageSize = DEFAULT_LIST_NUM_NOTES;
}
if (keepCurrentIfLarger && this.notesToDisplay > this.pageSize) {
return;
}
this.notesToDisplay = this.pageSize;
};
getFirstNonProtectedNote = () => {
return this.notes.find((note) => !note.protected);
};
get notesListScrollContainer() {
return document.getElementById(ELEMENT_ID_SCROLL_CONTAINER);
}
selectNote = async (
note: SNNote,
userTriggered?: boolean,
scrollIntoView = true
): Promise<void> => {
await this.appState.notes.selectNote(note.uuid, userTriggered);
if (scrollIntoView) {
const noteElement = document.getElementById(`note-${note.uuid}`);
noteElement?.scrollIntoView({
behavior: 'smooth',
});
}
};
selectFirstNote = () => {
const note = this.getFirstNonProtectedNote();
if (note) {
this.selectNote(note, false, false);
this.resetScrollPosition();
}
};
selectNextNote = () => {
const displayableNotes = this.notes;
const currentIndex = displayableNotes.findIndex((candidate) => {
return candidate.uuid === this.activeEditorNote?.uuid;
});
if (currentIndex + 1 < displayableNotes.length) {
const nextNote = displayableNotes[currentIndex + 1];
this.selectNote(nextNote);
const nextNoteElement = document.getElementById(`note-${nextNote.uuid}`);
nextNoteElement?.focus();
}
};
selectNextOrCreateNew = () => {
const note = this.getFirstNonProtectedNote();
if (note) {
this.selectNote(note, false, false);
} else {
this.appState.closeActiveEditor();
}
};
selectPreviousNote = () => {
const displayableNotes = this.notes;
if (this.activeEditorNote) {
const currentIndex = displayableNotes.indexOf(this.activeEditorNote);
if (currentIndex - 1 >= 0) {
const previousNote = displayableNotes[currentIndex - 1];
this.selectNote(previousNote);
const previousNoteElement = document.getElementById(
`note-${previousNote.uuid}`
);
previousNoteElement?.focus();
return true;
} else {
return false;
}
}
};
setNoteFilterText = (text: string) => {
this.noteFilterText = text;
};
syncSelectedNotes = () => {
this.selectedNotes = this.appState.notes.selectedNotes;
};
handleEditorChange = async () => {
const activeNote = this.appState.getActiveEditor()?.note;
if (activeNote && activeNote.conflictOf) {
this.application.changeAndSaveItem(activeNote.uuid, (mutator) => {
mutator.conflictOf = undefined;
});
}
if (this.isFiltering) {
this.application.getDesktopService().searchText(this.noteFilterText);
}
};
resetScrollPosition = () => {
if (this.notesListScrollContainer) {
this.notesListScrollContainer.scrollTop = 0;
this.notesListScrollContainer.scrollLeft = 0;
}
};
handleTagChange = () => {
this.resetScrollPosition();
this.toggleDisplayOptionsMenu(false);
this.setNoteFilterText('');
this.application.getDesktopService().searchText();
this.resetPagination();
/* Capture db load state before beginning reloadNotes,
since this status may change during reload */
const dbLoaded = this.application.isDatabaseLoaded();
this.reloadNotesDisplayOptions();
this.reloadNotes();
if (this.notes.length > 0) {
this.selectFirstNote();
} else if (dbLoaded) {
if (
this.activeEditorNote &&
!this.notes.includes(this.activeEditorNote)
) {
this.appState.closeActiveEditor();
}
}
};
onFilterEnter = () => {
/**
* For Desktop, performing a search right away causes
* input to lose focus. We wait until user explicity hits
* enter before highlighting desktop search results.
*/
this.searchSubmitted = true;
this.application.getDesktopService().searchText(this.noteFilterText);
};
handleFilterTextChanged = () => {
if (this.searchSubmitted) {
this.searchSubmitted = false;
}
this.reloadNotesDisplayOptions();
this.reloadNotes();
};
onSearchInputBlur = () => {
this.appState.searchOptions.refreshIncludeProtectedContents();
};
clearFilterText = () => {
this.setNoteFilterText('');
this.onFilterEnter();
this.handleFilterTextChanged();
this.resetPagination();
};
}

View File

@@ -0,0 +1,291 @@
import {
PanelSide,
ResizeFinishCallback,
} from '@/directives/views/panelResizer';
import { debounce } from '@/utils';
import { ApplicationEvent, PrefKey } from '@standardnotes/snjs';
import { action, computed, makeObservable, observable } from 'mobx';
import { WebApplication } from './application';
export type PanelResizerProps = {
alwaysVisible?: boolean;
application: WebApplication;
collapsable: boolean;
defaultWidth?: number;
hoverable?: boolean;
minWidth?: number;
panel: HTMLDivElement;
prefKey: PrefKey;
resizeFinishCallback?: ResizeFinishCallback;
side: PanelSide;
widthEventCallback?: () => void;
};
export class PanelResizerState {
private application: WebApplication;
alwaysVisible: boolean;
collapsable: boolean;
collapsed = false;
currentMinWidth = 0;
defaultWidth: number;
hoverable: boolean;
lastDownX = 0;
lastLeft = 0;
lastWidth = 0;
panel: HTMLDivElement;
pressed = false;
prefKey: PrefKey;
resizeFinishCallback?: ResizeFinishCallback;
side: PanelSide;
startLeft = 0;
startWidth = 0;
widthBeforeLastDblClick = 0;
widthEventCallback?: () => void;
constructor({
alwaysVisible,
application,
defaultWidth,
hoverable,
collapsable,
minWidth,
panel,
prefKey,
resizeFinishCallback,
side,
widthEventCallback,
}: PanelResizerProps) {
this.alwaysVisible = alwaysVisible ?? false;
this.application = application;
this.collapsable = collapsable ?? false;
this.collapsed = false;
this.currentMinWidth = minWidth ?? 0;
this.defaultWidth = defaultWidth ?? 0;
this.hoverable = hoverable ?? true;
this.lastDownX = 0;
this.lastLeft = this.startLeft;
this.lastWidth = this.startWidth;
this.panel = panel;
this.prefKey = prefKey;
this.pressed = false;
this.side = side;
this.startLeft = this.panel.offsetLeft;
this.startWidth = this.panel.scrollWidth;
this.widthBeforeLastDblClick = 0;
this.widthEventCallback = widthEventCallback;
this.resizeFinishCallback = resizeFinishCallback;
application.addEventObserver(async () => {
const changedWidth = application.getPreference(prefKey) as number;
if (changedWidth !== this.lastWidth) this.setWidth(changedWidth, true);
}, ApplicationEvent.PreferencesChanged);
makeObservable(this, {
pressed: observable,
collapsed: observable,
onMouseUp: action,
onMouseDown: action,
onDblClick: action,
handleWidthEvent: action,
handleLeftEvent: action,
setWidth: action,
setMinWidth: action,
reloadDefaultValues: action,
appFrame: computed,
});
document.addEventListener('mouseup', this.onMouseUp.bind(this));
document.addEventListener('mousemove', this.onMouseMove.bind(this));
if (this.side === PanelSide.Right) {
window.addEventListener(
'resize',
debounce(this.handleResize.bind(this), 250)
);
}
}
get appFrame() {
return document.getElementById('app')?.getBoundingClientRect() as DOMRect;
}
getParentRect() {
return (this.panel.parentNode as HTMLElement).getBoundingClientRect();
}
isAtMaxWidth = () => {
return (
Math.round(this.lastWidth + this.lastLeft) ===
Math.round(this.getParentRect().width)
);
};
isCollapsed() {
return this.lastWidth <= this.currentMinWidth;
}
reloadDefaultValues = () => {
this.startWidth = this.isAtMaxWidth()
? this.getParentRect().width
: this.panel.scrollWidth;
this.lastWidth = this.startWidth;
};
finishSettingWidth = () => {
if (!this.collapsable) {
return;
}
this.collapsed = this.isCollapsed();
};
setWidth = (width: number, finish = false) => {
if (width < this.currentMinWidth) {
width = this.currentMinWidth;
}
const parentRect = this.getParentRect();
if (width > parentRect.width) {
width = parentRect.width;
}
const maxWidth = this.appFrame.width - this.panel.getBoundingClientRect().x;
if (width > maxWidth) {
width = maxWidth;
}
if (Math.round(width + this.lastLeft) === Math.round(parentRect.width)) {
this.panel.style.width = `calc(100% - ${this.lastLeft}px)`;
} else {
this.panel.style.width = width + 'px';
}
this.lastWidth = width;
if (finish) {
this.finishSettingWidth();
if (this.resizeFinishCallback) {
this.resizeFinishCallback(
this.lastWidth,
this.lastLeft,
this.isAtMaxWidth(),
this.isCollapsed()
);
}
}
this.application.setPreference(this.prefKey, this.lastWidth);
};
setLeft = (left: number) => {
this.panel.style.left = left + 'px';
this.lastLeft = left;
};
onDblClick = () => {
const collapsed = this.isCollapsed();
if (collapsed) {
this.setWidth(this.widthBeforeLastDblClick || this.defaultWidth);
} else {
this.widthBeforeLastDblClick = this.lastWidth;
this.setWidth(this.currentMinWidth);
}
this.application.setPreference(this.prefKey, this.lastWidth);
this.finishSettingWidth();
if (this.resizeFinishCallback) {
this.resizeFinishCallback(
this.lastWidth,
this.lastLeft,
this.isAtMaxWidth(),
this.isCollapsed()
);
}
};
handleWidthEvent(event?: MouseEvent) {
if (this.widthEventCallback) {
this.widthEventCallback();
}
let x;
if (event) {
x = event.clientX;
} else {
/** Coming from resize event */
x = 0;
this.lastDownX = 0;
}
const deltaX = x - this.lastDownX;
const newWidth = this.startWidth + deltaX;
this.setWidth(newWidth, false);
}
handleLeftEvent(event: MouseEvent) {
const panelRect = this.panel.getBoundingClientRect();
const x = event.clientX || panelRect.x;
let deltaX = x - this.lastDownX;
let newLeft = this.startLeft + deltaX;
if (newLeft < 0) {
newLeft = 0;
deltaX = -this.startLeft;
}
const parentRect = this.getParentRect();
let newWidth = this.startWidth - deltaX;
if (newWidth < this.currentMinWidth) {
newWidth = this.currentMinWidth;
}
if (newWidth > parentRect.width) {
newWidth = parentRect.width;
}
if (newLeft + newWidth > parentRect.width) {
newLeft = parentRect.width - newWidth;
}
this.setLeft(newLeft);
this.setWidth(newWidth, false);
}
handleResize = () => {
this.reloadDefaultValues();
this.handleWidthEvent();
this.finishSettingWidth();
};
onMouseDown = (event: MouseEvent) => {
this.pressed = true;
this.lastDownX = event.clientX;
this.startWidth = this.panel.scrollWidth;
this.startLeft = this.panel.offsetLeft;
};
onMouseUp = () => {
if (!this.pressed) {
return;
}
this.pressed = false;
const isMaxWidth = this.isAtMaxWidth();
if (this.resizeFinishCallback) {
this.resizeFinishCallback(
this.lastWidth,
this.lastLeft,
isMaxWidth,
this.isCollapsed()
);
}
this.finishSettingWidth();
};
onMouseMove(event: MouseEvent) {
if (!this.pressed) {
return;
}
event.preventDefault();
if (this.side === PanelSide.Left) {
this.handleLeftEvent(event);
} else {
this.handleWidthEvent(event);
}
}
setMinWidth = (minWidth?: number) => {
this.currentMinWidth = minWidth ?? this.currentMinWidth;
};
}