From aeae2b644f7390fa1708221737decd03a5193cb9 Mon Sep 17 00:00:00 2001 From: Mo Date: Tue, 17 May 2022 20:02:34 -0500 Subject: [PATCH] refactor: notes state management (#1031) --- .../ChangeEditor/ChangeEditorMenu.tsx | 4 +- .../Components/NoteGroupView/index.tsx | 3 +- .../Components/NoteTags/NoteTag.tsx | 2 +- .../Components/NotesList/index.tsx | 2 +- .../Components/NotesOptions/NotesOptions.tsx | 5 +- .../Components/NotesView/index.tsx | 3 +- .../javascripts/UIModels/AppState/AppState.ts | 113 +------- .../UIModels/AppState/NoteTagsState.ts | 17 +- .../UIModels/AppState/NotesState.ts | 79 +++--- .../UIModels/AppState/NotesViewState.ts | 252 +++++++++++------- .../UIModels/AppState/TagsState.ts | 31 ++- package.json | 2 +- yarn.lock | 8 +- 13 files changed, 255 insertions(+), 266 deletions(-) diff --git a/app/assets/javascripts/Components/ChangeEditor/ChangeEditorMenu.tsx b/app/assets/javascripts/Components/ChangeEditor/ChangeEditorMenu.tsx index ad0345d6b..889a920cc 100644 --- a/app/assets/javascripts/Components/ChangeEditor/ChangeEditorMenu.tsx +++ b/app/assets/javascripts/Components/ChangeEditor/ChangeEditorMenu.tsx @@ -88,9 +88,7 @@ export const ChangeEditorMenu: FunctionComponent = ({ const transactions: TransactionalMutation[] = [] - if (application.getAppState().getActiveNoteController()?.isTemplateNote) { - await application.getAppState().getActiveNoteController().insertTemplatedNote() - } + await application.getAppState().notesView.insertCurrentIfTemplate() if (note.locked) { application.alertService.alert(STRING_EDIT_LOCKED_ATTEMPT).catch(console.error) diff --git a/app/assets/javascripts/Components/NoteGroupView/index.tsx b/app/assets/javascripts/Components/NoteGroupView/index.tsx index a5da0cd2d..5824aba6e 100644 --- a/app/assets/javascripts/Components/NoteGroupView/index.tsx +++ b/app/assets/javascripts/Components/NoteGroupView/index.tsx @@ -31,7 +31,6 @@ export class NoteGroupView extends PureComponent { const controllerGroup = this.application.noteControllerGroup this.removeChangeObserver = this.application.noteControllerGroup.addActiveControllerChangeObserver(() => { const controllers = controllerGroup.noteControllers - this.setState({ controllers: controllers, }) @@ -63,7 +62,7 @@ export class NoteGroupView extends PureComponent { {!this.state.showMultipleSelectedNotes && ( <> {this.state.controllers.map((controller) => { - return + return })} )} diff --git a/app/assets/javascripts/Components/NoteTags/NoteTag.tsx b/app/assets/javascripts/Components/NoteTags/NoteTag.tsx index 489c15290..fd61345b2 100644 --- a/app/assets/javascripts/Components/NoteTags/NoteTag.tsx +++ b/app/assets/javascripts/Components/NoteTags/NoteTag.tsx @@ -42,7 +42,7 @@ export const NoteTag = observer(({ appState, tag }: Props) => { (event: MouseEvent) => { if (tagClicked && event.target !== deleteTagRef.current) { setTagClicked(false) - appState.selectedTag = tag + appState.tags.selected = tag } else { setTagClicked(true) } diff --git a/app/assets/javascripts/Components/NotesList/index.tsx b/app/assets/javascripts/Components/NotesList/index.tsx index b22f4d51d..690cc911a 100644 --- a/app/assets/javascripts/Components/NotesList/index.tsx +++ b/app/assets/javascripts/Components/NotesList/index.tsx @@ -30,7 +30,7 @@ export const NotesList: FunctionComponent = observer( if (hideTags) { return [] } - const selectedTag = appState.selectedTag + const selectedTag = appState.tags.selected if (!selectedTag) { return [] } diff --git a/app/assets/javascripts/Components/NotesOptions/NotesOptions.tsx b/app/assets/javascripts/Components/NotesOptions/NotesOptions.tsx index c92da9c76..5b1dfcbe7 100644 --- a/app/assets/javascripts/Components/NotesOptions/NotesOptions.tsx +++ b/app/assets/javascripts/Components/NotesOptions/NotesOptions.tsx @@ -34,6 +34,7 @@ const DeletePermanentlyButton = ({ closeOnBlur, onClick }: DeletePermanentlyButt const iconClass = 'color-neutral mr-2' const iconClassDanger = 'color-danger mr-2' const iconClassWarning = 'color-warning mr-2' +const iconClassSuccess = 'color-success mr-2' const getWordCount = (text: string) => { if (text.trim().length === 0) { @@ -400,8 +401,8 @@ export const NotesOptions = observer(({ application, appState, closeOnBlur }: No await appState.notes.setTrashSelectedNotes(false) }} > - - Restore + + Restore = observer(({ application, appS optionsSubtitle, panelTitle, renderedNotes, - selectedNotes, searchBarElement, paginate, panelWidth, } = appState.notesView + const { selectedNotes } = appState.notes + const createNewNote = useCallback(() => appState.notesView.createNewNote(), [appState]) const onFilterEnter = useCallback(() => appState.notesView.onFilterEnter(), [appState]) const clearFilterText = useCallback(() => appState.notesView.clearFilterText(), [appState]) diff --git a/app/assets/javascripts/UIModels/AppState/AppState.ts b/app/assets/javascripts/UIModels/AppState/AppState.ts index 3fde70684..4e85b1656 100644 --- a/app/assets/javascripts/UIModels/AppState/AppState.ts +++ b/app/assets/javascripts/UIModels/AppState/AppState.ts @@ -6,18 +6,13 @@ import { ApplicationEvent, ContentType, DeinitSource, - NoteViewController, PrefKey, SNNote, - SmartView, SNTag, - SystemViewId, removeFromArray, - Uuid, - PayloadEmitSource, WebOrDesktopDeviceInterface, } from '@standardnotes/snjs' -import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx' +import { action, IReactionDisposer, makeObservable, observable, reaction } from 'mobx' import { ActionsMenuState } from './ActionsMenuState' import { FeaturesState } from './FeaturesState' import { FilesState } from './FilesState' @@ -56,7 +51,7 @@ export enum EventSource { Script, } -type ObserverCallback = (event: AppStateEvent, data?: any) => Promise +type ObserverCallback = (event: AppStateEvent, data?: unknown) => Promise export class AppState extends AbstractState { readonly enableUnfinishedFeatures: boolean = window?.enabledUnfinishedFeatures @@ -68,8 +63,6 @@ export class AppState extends AbstractState { onVisibilityChange: () => void showBetaWarning: boolean - private multiEditorSupport = false - readonly accountMenu: AccountMenuState readonly actionsMenu = new ActionsMenuState() readonly features: FeaturesState @@ -116,7 +109,6 @@ export class AppState extends AbstractState { this.notesView = new NotesViewState(application, this, this.appEventObserverRemovers) this.files = new FilesState(application) this.addAppEventObserver() - this.streamNotesAndTags() this.onVisibilityChange = () => { const visible = document.visibilityState === 'visible' const event = visible ? AppStateEvent.WindowDidFocus : AppStateEvent.WindowDidBlur @@ -131,8 +123,6 @@ export class AppState extends AbstractState { } makeObservable(this, { - selectedTag: computed, - showBetaWarning: observable, isSessionsModalVisible: observable, preferences: observable, @@ -236,47 +226,6 @@ export class AppState extends AbstractState { return this.device.appVersion } - async openNewNote(title?: string) { - if (!this.multiEditorSupport) { - this.closeActiveNoteController() - } - - const selectedTag = this.selectedTag - - const activeRegularTagUuid = selectedTag && selectedTag instanceof SNTag ? selectedTag.uuid : undefined - - await this.application.noteControllerGroup.createNoteView(undefined, title, activeRegularTagUuid) - } - - getActiveNoteController() { - return this.application.noteControllerGroup.noteControllers[0] - } - - getNoteControllers() { - return this.application.noteControllerGroup.noteControllers - } - - closeNoteController(controller: NoteViewController) { - this.application.noteControllerGroup.closeNoteView(controller) - } - - closeActiveNoteController() { - this.application.noteControllerGroup.closeActiveNoteView() - } - - closeAllNoteControllers() { - this.application.noteControllerGroup.closeAllNoteViews() - } - - noteControllerForNote(uuid: Uuid) { - for (const controller of this.getNoteControllers()) { - if (controller.note.uuid === uuid) { - return controller - } - } - return undefined - } - isGlobalSpellcheckEnabled(): boolean { return this.application.getPreference(PrefKey.EditorSpellcheck, true) } @@ -309,62 +258,6 @@ export class AppState extends AbstractState { ) } - public get selectedTag(): SNTag | SmartView | undefined { - return this.tags.selected - } - - public set selectedTag(tag: SNTag | SmartView | undefined) { - this.tags.selected = tag - } - - streamNotesAndTags() { - this.application.streamItems( - [ContentType.Note, ContentType.Tag], - async ({ changed, inserted, removed, source }) => { - if (![PayloadEmitSource.PreSyncSave, PayloadEmitSource.RemoteRetrieved].includes(source)) { - return - } - - const removedNotes = removed.filter((i) => i.content_type === ContentType.Note) - - for (const removedNote of removedNotes) { - const noteController = this.noteControllerForNote(removedNote.uuid) - if (noteController) { - this.closeNoteController(noteController) - } - } - - const changedOrInserted = [...changed, ...inserted].filter((i) => i.content_type === ContentType.Note) - - const selectedTag = this.tags.selected - - const isBrowswingTrashedNotes = - selectedTag instanceof SmartView && selectedTag.uuid === SystemViewId.TrashedNotes - - const isBrowsingArchivedNotes = - selectedTag instanceof SmartView && selectedTag.uuid === SystemViewId.ArchivedNotes - - for (const note of changedOrInserted) { - const noteController = this.noteControllerForNote(note.uuid) - if (!noteController) { - continue - } - - if (note.trashed && !isBrowswingTrashedNotes && !this.searchOptions.includeTrashed) { - this.closeNoteController(noteController) - } else if ( - note.archived && - !isBrowsingArchivedNotes && - !this.searchOptions.includeArchived && - !this.application.getPreference(PrefKey.NotesShowArchived, false) - ) { - this.closeNoteController(noteController) - } - } - }, - ) - } - addAppEventObserver() { this.unsubAppEventObserver = this.application.addEventObserver(async (eventName) => { switch (eventName) { @@ -412,7 +305,7 @@ export class AppState extends AbstractState { } } - async notifyEvent(eventName: AppStateEvent, data?: any) { + async notifyEvent(eventName: AppStateEvent, data?: unknown) { /** * Timeout is particularly important so we can give all initial * controllers a chance to construct before propogting any events * diff --git a/app/assets/javascripts/UIModels/AppState/NoteTagsState.ts b/app/assets/javascripts/UIModels/AppState/NoteTagsState.ts index fb0393687..671622d8a 100644 --- a/app/assets/javascripts/UIModels/AppState/NoteTagsState.ts +++ b/app/assets/javascripts/UIModels/AppState/NoteTagsState.ts @@ -62,10 +62,6 @@ export class NoteTagsState extends AbstractState { ) } - get activeNote(): SNNote | undefined { - return this.appState.notes.activeNoteController?.note - } - get autocompleteTagHintVisible(): boolean { return ( this.autocompleteSearchQuery !== '' && @@ -149,7 +145,10 @@ export class NoteTagsState extends AbstractState { } searchActiveNoteAutocompleteTags(): void { - const newResults = this.application.items.searchTags(this.autocompleteSearchQuery, this.activeNote) + const newResults = this.application.items.searchTags( + this.autocompleteSearchQuery, + this.appState.notesView.activeControllerNote, + ) this.setAutocompleteTagResults(newResults) } @@ -158,7 +157,8 @@ export class NoteTagsState extends AbstractState { } reloadTags(): void { - const { activeNote } = this + const activeNote = this.appState.notesView.activeControllerNote + if (activeNote) { const tags = this.application.items.getSortedTagsForNote(activeNote) this.setTags(tags) @@ -173,7 +173,7 @@ export class NoteTagsState extends AbstractState { } async addTagToActiveNote(tag: SNTag): Promise { - const { activeNote } = this + const activeNote = this.appState.notesView.activeControllerNote if (activeNote) { await this.application.items.addTagToNote(activeNote, tag, this.addNoteToParentFolders) @@ -183,7 +183,8 @@ export class NoteTagsState extends AbstractState { } async removeTagFromActiveNote(tag: SNTag): Promise { - const { activeNote } = this + const activeNote = this.appState.notesView.activeControllerNote + if (activeNote) { await this.application.mutator.changeItem(tag, (mutator) => { mutator.removeItemAsRelationship(activeNote) diff --git a/app/assets/javascripts/UIModels/AppState/NotesState.ts b/app/assets/javascripts/UIModels/AppState/NotesState.ts index 13988a892..a8c638c19 100644 --- a/app/assets/javascripts/UIModels/AppState/NotesState.ts +++ b/app/assets/javascripts/UIModels/AppState/NotesState.ts @@ -3,16 +3,7 @@ import { confirmDialog } from '@/Services/AlertService' import { KeyboardModifier } from '@/Services/IOService' import { StringEmptyTrash, Strings, StringUtils } from '@/Strings' import { MENU_MARGIN_FROM_APP_BORDER } from '@/Constants' -import { - UuidString, - SNNote, - NoteMutator, - ContentType, - SNTag, - ChallengeReason, - NoteViewController, - DeinitSource, -} from '@standardnotes/snjs' +import { UuidString, SNNote, NoteMutator, ContentType, SNTag, ChallengeReason, DeinitSource } from '@standardnotes/snjs' import { makeObservable, observable, action, computed, runInAction } from 'mobx' import { WebApplication } from '../Application' import { AppState } from './AppState' @@ -81,11 +72,21 @@ export class NotesState extends AbstractState { } }) }), - ) - } - get activeNoteController(): NoteViewController | undefined { - return this.application.noteControllerGroup.noteControllers[0] + this.application.noteControllerGroup.addActiveControllerChangeObserver(() => { + const controllers = this.application.noteControllerGroup.noteControllers + + const activeNoteUuids = controllers.map((c) => c.note.uuid) + + const selectedUuids = this.getSelectedNotesList().map((n) => n.uuid) + + for (const selectedId of selectedUuids) { + if (!activeNoteUuids.includes(selectedId)) { + delete this.selectedNotes[selectedId] + } + } + }), + ) } get selectedNotesCount(): number { @@ -132,11 +133,18 @@ export class NotesState extends AbstractState { return } + if (this.selectedNotes[uuid]) { + return + } + const hasMeta = this.io.activeModifiers.has(KeyboardModifier.Meta) const hasCtrl = this.io.activeModifiers.has(KeyboardModifier.Ctrl) const hasShift = this.io.activeModifiers.has(KeyboardModifier.Shift) - if (userTriggered && (hasMeta || hasCtrl)) { + const isMultipleSelectSingle = userTriggered && (hasMeta || hasCtrl) + const isMultipleSelectRange = userTriggered && hasShift + + if (isMultipleSelectSingle) { if (this.selectedNotes[uuid]) { delete this.selectedNotes[uuid] } else if (await this.application.authorizeNoteAccess(note)) { @@ -145,7 +153,7 @@ export class NotesState extends AbstractState { this.lastSelectedNote = note }) } - } else if (userTriggered && hasShift) { + } else if (isMultipleSelectRange) { await this.selectNotesRange(note) } else { const shouldSelectNote = this.selectedNotesCount > 1 || !this.selectedNotes[uuid] @@ -165,7 +173,7 @@ export class NotesState extends AbstractState { } private async openNote(noteUuid: string): Promise { - if (this.activeNoteController?.note?.uuid === noteUuid) { + if (this.appState.notesView.activeControllerNote?.uuid === noteUuid) { return } @@ -175,16 +183,21 @@ export class NotesState extends AbstractState { return } - if (this.activeNoteController) { - this.application.noteControllerGroup.closeActiveNoteView() - } - - await this.application.noteControllerGroup.createNoteView(noteUuid) + await this.application.noteControllerGroup.createNoteController(noteUuid) this.appState.noteTags.reloadTags() + await this.onActiveEditorChanged() } + async createNewNoteController(title?: string) { + const selectedTag = this.appState.tags.selected + + const activeRegularTagUuid = selectedTag && selectedTag instanceof SNTag ? selectedTag.uuid : undefined + + await this.application.noteControllerGroup.createNoteController(undefined, title, activeRegularTagUuid) + } + setContextMenuOpen(open: boolean): void { this.contextMenuOpen = open } @@ -249,7 +262,7 @@ export class NotesState extends AbstractState { } async changeSelectedNotes(mutate: (mutator: NoteMutator) => void): Promise { - await this.application.mutator.changeItems(Object.values(this.selectedNotes), mutate, false) + await this.application.mutator.changeItems(this.getSelectedNotesList(), mutate, false) this.application.sync.sync().catch(console.error) } @@ -290,7 +303,7 @@ export class NotesState extends AbstractState { } async deleteNotes(permanently: boolean): Promise { - if (Object.values(this.selectedNotes).some((note) => note.locked)) { + if (this.getSelectedNotesList().some((note) => note.locked)) { const text = StringUtils.deleteLockedNotesAttempt(this.selectedNotesCount) this.application.alertService.alert(text).catch(console.error) return false @@ -299,7 +312,7 @@ export class NotesState extends AbstractState { const title = Strings.trashNotesTitle let noteTitle = undefined if (this.selectedNotesCount === 1) { - const selectedNote = Object.values(this.selectedNotes)[0] + const selectedNote = this.getSelectedNotesList()[0] noteTitle = selectedNote.title.length ? `'${selectedNote.title}'` : 'this note' } const text = StringUtils.deleteNotes(permanently, this.selectedNotesCount, noteTitle) @@ -312,7 +325,7 @@ export class NotesState extends AbstractState { }) ) { if (permanently) { - for (const note of Object.values(this.selectedNotes)) { + for (const note of this.getSelectedNotesList()) { await this.application.mutator.deleteItem(note) delete this.selectedNotes[note.uuid] } @@ -334,7 +347,7 @@ export class NotesState extends AbstractState { } async setArchiveSelectedNotes(archived: boolean): Promise { - if (Object.values(this.selectedNotes).some((note) => note.locked)) { + if (this.getSelectedNotesList().some((note) => note.locked)) { this.application.alertService .alert(StringUtils.archiveLockedNotesAttempt(archived, this.selectedNotesCount)) .catch(console.error) @@ -352,7 +365,7 @@ export class NotesState extends AbstractState { } async setProtectSelectedNotes(protect: boolean): Promise { - const selectedNotes = Object.values(this.selectedNotes) + const selectedNotes = this.getSelectedNotesList() if (protect) { await this.application.mutator.protectNotes(selectedNotes) this.setShowProtectedWarning(true) @@ -382,7 +395,7 @@ export class NotesState extends AbstractState { } async addTagToSelectedNotes(tag: SNTag): Promise { - const selectedNotes = Object.values(this.selectedNotes) + const selectedNotes = this.getSelectedNotesList() const parentChainTags = this.application.items.getTagParentChain(tag) const tagsToAdd = [...parentChainTags, tag] await Promise.all( @@ -398,7 +411,7 @@ export class NotesState extends AbstractState { } async removeTagFromSelectedNotes(tag: SNTag): Promise { - const selectedNotes = Object.values(this.selectedNotes) + const selectedNotes = this.getSelectedNotesList() await this.application.mutator.changeItem(tag, (mutator) => { for (const note of selectedNotes) { mutator.removeItemAsRelationship(note) @@ -408,7 +421,7 @@ export class NotesState extends AbstractState { } isTagInSelectedNotes(tag: SNTag): boolean { - const selectedNotes = Object.values(this.selectedNotes) + const selectedNotes = this.getSelectedNotesList() return selectedNotes.every((note) => this.appState.getNoteTags(note).find((noteTag) => noteTag.uuid === tag.uuid)) } @@ -428,6 +441,10 @@ export class NotesState extends AbstractState { } } + private getSelectedNotesList(): SNNote[] { + return Object.values(this.selectedNotes) + } + private get io() { return this.application.io } diff --git a/app/assets/javascripts/UIModels/AppState/NotesViewState.ts b/app/assets/javascripts/UIModels/AppState/NotesViewState.ts index d71e4e9ef..028293104 100644 --- a/app/assets/javascripts/UIModels/AppState/NotesViewState.ts +++ b/app/assets/javascripts/UIModels/AppState/NotesViewState.ts @@ -7,14 +7,14 @@ import { DeinitSource, findInArray, NotesDisplayCriteria, + NoteViewController, PrefKey, SmartView, SNNote, SNTag, SystemViewId, - UuidString, } from '@standardnotes/snjs' -import { action, autorun, computed, makeObservable, observable, reaction } from 'mobx' +import { action, computed, makeObservable, observable, reaction } from 'mobx' import { AppState, AppStateEvent } from '.' import { WebApplication } from '../Application' import { AbstractState } from './AbstractState' @@ -47,7 +47,6 @@ export class NotesViewState extends AbstractState { panelWidth = 0 renderedNotes: SNNote[] = [] searchSubmitted = false - selectedNotes: Record = {} showDisplayOptionsMenu = false displayOptions = { sortBy: CollectionSort.CreatedAt, @@ -61,13 +60,13 @@ export class NotesViewState extends AbstractState { hideNotePreview: false, hideEditorIcon: false, } + private reloadNotesPromise?: Promise override deinit(source: DeinitSource) { super.deinit(source) ;(this.noteFilterText as unknown) = undefined ;(this.notes as unknown) = undefined ;(this.renderedNotes as unknown) = undefined - ;(this.selectedNotes as unknown) = undefined ;(window.onresize as unknown) = undefined destroyAllObjectProperties(this) @@ -80,65 +79,45 @@ export class NotesViewState extends AbstractState { appObservers.push( application.streamItems(ContentType.Note, () => { - this.reloadNotes() - - const activeNote = appState.notes.activeNoteController?.note - - if (appState.notes.selectedNotesCount < 2) { - if (activeNote) { - const browsingTrashedNotes = - appState.selectedTag instanceof SmartView && appState.selectedTag?.uuid === SystemViewId.TrashedNotes - - if (activeNote.trashed && !browsingTrashedNotes && !appState?.searchOptions.includeTrashed) { - this.selectNextOrCreateNew() - } else if (!this.selectedNotes[activeNote.uuid]) { - this.selectNote(activeNote).catch(console.error) - } - } else { - this.selectFirstNote() - } - } + void this.reloadNotes() }), application.streamItems([ContentType.Tag], async ({ changed, inserted }) => { const tags = [...changed, ...inserted] + /** A tag could have changed its relationships, so we need to reload the filter */ this.reloadNotesDisplayOptions() - this.reloadNotes() - if (appState.selectedTag && findInArray(tags, 'uuid', appState.selectedTag.uuid)) { + void this.reloadNotes() + + if (appState.tags.selected && findInArray(tags, 'uuid', appState.tags.selected.uuid)) { /** Tag title could have changed */ this.reloadPanelTitle() } }), application.addEventObserver(async () => { - this.reloadPreferences() + void this.reloadPreferences() }, ApplicationEvent.PreferencesChanged), application.addEventObserver(async () => { - appState.closeAllNoteControllers() - this.selectFirstNote() + this.application.noteControllerGroup.closeAllNoteControllers() + void this.selectFirstNote() this.setCompletedFullSync(false) }, ApplicationEvent.SignedIn), application.addEventObserver(async () => { - this.reloadNotes() - if ( - this.notes.length === 0 && - appState.selectedTag instanceof SmartView && - appState.selectedTag.uuid === SystemViewId.AllNotes && - this.noteFilterText === '' && - !appState.notes.activeNoteController - ) { - this.createPlaceholderNote()?.catch(console.error) - } + void this.reloadNotes().then(() => { + if ( + this.notes.length === 0 && + appState.tags.selected instanceof SmartView && + appState.tags.selected.uuid === SystemViewId.AllNotes && + this.noteFilterText === '' && + !this.getActiveNoteController() + ) { + this.createPlaceholderNote()?.catch(console.error) + } + }) this.setCompletedFullSync(true) }, ApplicationEvent.CompletedFullSync), - autorun(() => { - if (appState.notes.selectedNotes) { - this.syncSelectedNotes() - } - }), - reaction( () => [ appState.searchOptions.includeProtectedContents, @@ -147,7 +126,7 @@ export class NotesViewState extends AbstractState { ], () => { this.reloadNotesDisplayOptions() - this.reloadNotes() + void this.reloadNotes() }, ), @@ -170,7 +149,6 @@ export class NotesViewState extends AbstractState { notesToDisplay: observable, panelTitle: observable, renderedNotes: observable, - selectedNotes: observable, showDisplayOptionsMenu: observable, reloadNotes: action, @@ -179,7 +157,6 @@ export class NotesViewState extends AbstractState { resetPagination: action, setCompletedFullSync: action, setNoteFilterText: action, - syncSelectedNotes: action, setShowDisplayOptionsMenu: action, onFilterEnter: action, handleFilterTextChanged: action, @@ -192,6 +169,14 @@ export class NotesViewState extends AbstractState { } } + public getActiveNoteController(): NoteViewController | undefined { + return this.application.noteControllerGroup.activeNoteViewController + } + + public get activeControllerNote(): SNNote | undefined { + return this.getActiveNoteController()?.note + } + setCompletedFullSync = (completed: boolean) => { this.completedFullSync = completed } @@ -208,36 +193,96 @@ export class NotesViewState extends AbstractState { return !!this.noteFilterText && this.noteFilterText.length > 0 } - get activeEditorNote() { - return this.appState.notes.activeNoteController?.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}` + } else if (this.appState.tags.selected) { + title = `${this.appState.tags.selected.title}` } + this.panelTitle = title } - reloadNotes = () => { - const tag = this.appState.selectedTag + reloadNotes = async (): Promise => { + if (this.reloadNotesPromise) { + await this.reloadNotesPromise + } + + this.reloadNotesPromise = this.performReloadNotes() + + await this.reloadNotesPromise + } + + private async performReloadNotes() { + const tag = this.appState.tags.selected if (!tag) { return } + const notes = this.application.items.getDisplayableNotes() + const renderedNotes = notes.slice(0, this.notesToDisplay) this.notes = notes + this.renderedNotes = renderedNotes + + await this.recomputeSelectionAfterNotesReload() + this.reloadPanelTitle() } + private async recomputeSelectionAfterNotesReload() { + const appState = this.appState + const activeController = this.getActiveNoteController() + const activeNote = activeController?.note + const isSearching = this.noteFilterText.length > 0 + const hasMultipleNotesSelected = appState.notes.selectedNotesCount >= 2 + + if (hasMultipleNotesSelected) { + return + } + + if (!activeNote) { + await this.selectFirstNote() + + return + } + + if (activeController.isTemplateNote) { + return + } + + const noteExistsInUpdatedResults = this.notes.find((note) => note.uuid === activeNote.uuid) + if (!noteExistsInUpdatedResults && !isSearching) { + this.application.noteControllerGroup.closeNoteController(activeController) + + this.selectNextNote() + + return + } + + const showTrashedNotes = + (appState.tags.selected instanceof SmartView && appState.tags.selected?.uuid === SystemViewId.TrashedNotes) || + appState?.searchOptions.includeTrashed + + const showArchivedNotes = + (appState.tags.selected instanceof SmartView && appState.tags.selected.uuid === SystemViewId.ArchivedNotes) || + appState.searchOptions.includeArchived || + this.application.getPreference(PrefKey.NotesShowArchived, false) + + if ((activeNote.trashed && !showTrashedNotes) || (activeNote.archived && !showArchivedNotes)) { + await this.selectNextOrCreateNew() + } else if (!this.appState.notes.selectedNotes[activeNote.uuid]) { + await this.selectNoteWithScrollHandling(activeNote).catch(console.error) + } + } + reloadNotesDisplayOptions = () => { - const tag = this.appState.selectedTag + const tag = this.appState.tags.selected const searchText = this.noteFilterText.toLowerCase() const isSearching = searchText.length @@ -266,10 +311,11 @@ export class NotesViewState extends AbstractState { includeProtectedNoteText: this.appState.searchOptions.includeProtectedContents, }, }) + this.application.items.setNotesDisplayCriteria(criteria) } - reloadPreferences = () => { + reloadPreferences = async () => { const freshDisplayOptions = {} as DisplayOptions const currentSortBy = this.displayOptions.sortBy let sortBy = this.application.getPreference(PrefKey.SortNotesBy, CollectionSort.CreatedAt) @@ -287,6 +333,7 @@ export class NotesViewState extends AbstractState { freshDisplayOptions.hideDate = this.application.getPreference(PrefKey.NotesHideDate, false) freshDisplayOptions.hideTags = this.application.getPreference(PrefKey.NotesHideTags, true) freshDisplayOptions.hideEditorIcon = this.application.getPreference(PrefKey.NotesHideEditorIcon, false) + const displayOptionsChanged = freshDisplayOptions.sortBy !== this.displayOptions.sortBy || freshDisplayOptions.sortReverse !== this.displayOptions.sortReverse || @@ -296,12 +343,14 @@ export class NotesViewState extends AbstractState { freshDisplayOptions.hideProtected !== this.displayOptions.hideProtected || freshDisplayOptions.hideEditorIcon !== this.displayOptions.hideEditorIcon || freshDisplayOptions.hideTags !== this.displayOptions.hideTags + this.displayOptions = freshDisplayOptions + if (displayOptionsChanged) { this.reloadNotesDisplayOptions() } - this.reloadNotes() + await this.reloadNotes() const width = this.application.getPreference(PrefKey.NotesPanelWidth) if (width) { @@ -309,28 +358,29 @@ export class NotesViewState extends AbstractState { } if (freshDisplayOptions.sortBy !== currentSortBy) { - this.selectFirstNote() + await this.selectFirstNote() } } createNewNote = async () => { this.appState.notes.unselectNotes() + let title = `Note ${this.notes.length + 1}` if (this.isFiltering) { title = this.noteFilterText } - await this.appState.openNewNote(title) + await this.appState.notes.createNewNoteController(title) - this.reloadNotes() this.appState.noteTags.reloadTags() } createPlaceholderNote = () => { - const selectedTag = this.appState.selectedTag + const selectedTag = this.appState.tags.selected if (selectedTag && selectedTag instanceof SmartView && selectedTag.uuid !== SystemViewId.AllNotes) { return } + return this.createNewNote() } @@ -363,7 +413,8 @@ export class NotesViewState extends AbstractState { paginate = () => { this.notesToDisplay += this.pageSize - this.reloadNotes() + + void this.reloadNotes() if (this.searchSubmitted) { this.application.getDesktopService()?.searchText(this.noteFilterText) @@ -390,8 +441,13 @@ export class NotesViewState extends AbstractState { return document.getElementById(ELEMENT_ID_SCROLL_CONTAINER) } - selectNote = async (note: SNNote, userTriggered?: boolean, scrollIntoView = true): Promise => { + selectNoteWithScrollHandling = async ( + note: SNNote, + userTriggered?: boolean, + scrollIntoView = true, + ): Promise => { await this.appState.notes.selectNote(note.uuid, userTriggered) + if (scrollIntoView) { const noteElement = document.getElementById(`note-${note.uuid}`) noteElement?.scrollIntoView({ @@ -400,10 +456,12 @@ export class NotesViewState extends AbstractState { } } - selectFirstNote = () => { + selectFirstNote = async () => { const note = this.getFirstNonProtectedNote() + if (note) { - this.selectNote(note, false, false).catch(console.error) + await this.selectNoteWithScrollHandling(note, false, false) + this.resetScrollPosition() } } @@ -411,33 +469,37 @@ export class NotesViewState extends AbstractState { selectNextNote = () => { const displayableNotes = this.notes const currentIndex = displayableNotes.findIndex((candidate) => { - return candidate.uuid === this.activeEditorNote?.uuid + return candidate.uuid === this.activeControllerNote?.uuid }) + if (currentIndex + 1 < displayableNotes.length) { const nextNote = displayableNotes[currentIndex + 1] - this.selectNote(nextNote).catch(console.error) + + this.selectNoteWithScrollHandling(nextNote).catch(console.error) + const nextNoteElement = document.getElementById(`note-${nextNote.uuid}`) nextNoteElement?.focus() } } - selectNextOrCreateNew = () => { + selectNextOrCreateNew = async () => { const note = this.getFirstNonProtectedNote() + if (note) { - this.selectNote(note, false, false).catch(console.error) + await this.selectNoteWithScrollHandling(note, false, false).catch(console.error) } else { - this.appState.closeActiveNoteController() + await this.createNewNote() } } selectPreviousNote = () => { const displayableNotes = this.notes - if (this.activeEditorNote) { - const currentIndex = displayableNotes.indexOf(this.activeEditorNote) + if (this.activeControllerNote) { + const currentIndex = displayableNotes.indexOf(this.activeControllerNote) if (currentIndex - 1 >= 0) { const previousNote = displayableNotes[currentIndex - 1] - this.selectNote(previousNote).catch(console.error) + this.selectNoteWithScrollHandling(previousNote).catch(console.error) const previousNoteElement = document.getElementById(`note-${previousNote.uuid}`) previousNoteElement?.focus() return true @@ -450,16 +512,17 @@ export class NotesViewState extends AbstractState { } setNoteFilterText = (text: string) => { + if (text === this.noteFilterText) { + return + } + this.noteFilterText = text this.handleFilterTextChanged() } - syncSelectedNotes = () => { - this.selectedNotes = this.appState.notes.selectedNotes - } - handleEditorChange = async () => { - const activeNote = this.appState.getActiveNoteController()?.note + const activeNote = this.application.noteControllerGroup.activeNoteViewController?.note + if (activeNote && activeNote.conflictOf) { this.application.mutator .changeAndSaveItem(activeNote, (mutator) => { @@ -481,6 +544,11 @@ export class NotesViewState extends AbstractState { } handleTagChange = () => { + const activeNoteController = this.getActiveNoteController() + if (activeNoteController?.isTemplateNote) { + this.application.noteControllerGroup.closeNoteController(activeNoteController) + } + this.resetScrollPosition() this.setShowDisplayOptionsMenu(false) @@ -491,21 +559,9 @@ export class NotesViewState extends AbstractState { 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() - const hasSomeNotes = this.notes.length > 0 - - if (hasSomeNotes) { - this.selectFirstNote() - } else if (dbLoaded) { - if (this.activeEditorNote && !this.notes.includes(this.activeEditorNote)) { - this.appState.closeActiveNoteController() - } - } + void this.reloadNotes() } onFilterEnter = () => { @@ -519,12 +575,24 @@ export class NotesViewState extends AbstractState { this.application.getDesktopService()?.searchText(this.noteFilterText) } + public async insertCurrentIfTemplate(): Promise { + const controller = this.getActiveNoteController() + + if (!controller) { + return + } + + if (controller.isTemplateNote) { + await controller.insertTemplatedNote() + } + } + handleFilterTextChanged = () => { if (this.searchSubmitted) { this.searchSubmitted = false } this.reloadNotesDisplayOptions() - this.reloadNotes() + void this.reloadNotes() } clearFilterText = () => { diff --git a/app/assets/javascripts/UIModels/AppState/TagsState.ts b/app/assets/javascripts/UIModels/AppState/TagsState.ts index fff5bf2a9..565d5a3d5 100644 --- a/app/assets/javascripts/UIModels/AppState/TagsState.ts +++ b/app/assets/javascripts/UIModels/AppState/TagsState.ts @@ -137,19 +137,25 @@ export class TagsState extends AbstractState { this.smartViews = this.application.items.getSmartViews() - const selectedTag = this.selected_ + const currrentSelectedTag = this.selected_ - if (selectedTag && !isSystemView(selectedTag as SmartView)) { - if (FindItem(removed, selectedTag.uuid)) { - this.selected_ = this.smartViews[0] - } + if (!currrentSelectedTag) { + this.setSelectedTagInstance(this.smartViews[0]) - const updated = FindItem(changed, selectedTag.uuid) - if (updated) { - this.selected_ = updated as AnyTag - } + return + } + + if (isSystemView(currrentSelectedTag as SmartView)) { + return + } + + if (FindItem(removed, currrentSelectedTag.uuid)) { + this.setSelectedTagInstance(this.smartViews[0]) } else { - this.selected_ = this.smartViews[0] + const updated = FindItem(changed, currrentSelectedTag.uuid) + if (updated) { + this.setSelectedTagInstance(updated as AnyTag) + } } }) }), @@ -379,6 +385,11 @@ export class TagsState extends AbstractState { } this.previouslySelected_ = this.selected_ + + this.setSelectedTagInstance(tag) + } + + private setSelectedTagInstance(tag: AnyTag | undefined): void { this.selected_ = tag } diff --git a/package.json b/package.json index d0e9d6f42..4dd55acec 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "@standardnotes/filepicker": "1.14.8", "@standardnotes/icons": "^1.1.7", "@standardnotes/sncrypto-web": "1.9.2", - "@standardnotes/snjs": "2.109.3", + "@standardnotes/snjs": "2.109.4", "@standardnotes/stylekit": "5.27.0", "@zip.js/zip.js": "^2.4.10", "mobx": "^6.5.0", diff --git a/yarn.lock b/yarn.lock index 4223cd025..44c6955ea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2401,10 +2401,10 @@ buffer "^6.0.3" libsodium-wrappers "^0.7.9" -"@standardnotes/snjs@2.109.3": - version "2.109.3" - resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.109.3.tgz#a6fcd8af317caf340c6099b5e7cfb36f1ec7a1ba" - integrity sha512-RIV22G99ZuolxrHTrh12i9IicxsXYZ9kK0mzfG1uO3E7BtFyvKgaoX5Yugi305Q0WsLzf4bWaeAuWl7i3nTJ0Q== +"@standardnotes/snjs@2.109.4": + version "2.109.4" + resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.109.4.tgz#46c1915b2c8d43e0806fd9ad8bb9868f2ae3a3e8" + integrity sha512-DLoLR9RI517zz0N+sA6iHYY8bJ1rGVgOFyKWQVHjJajPwnCllEvETLGKz4IodfAao4lxU9Q3xPQ31q6VyCvWxQ== dependencies: "@standardnotes/auth" "^3.18.16" "@standardnotes/common" "^1.21.0"