feat: persist selected tag & note locally (#1851)
This commit is contained in:
@@ -1,6 +1,5 @@
|
|||||||
import { WebApplication } from '@/Application/Application'
|
import { WebApplication } from '@/Application/Application'
|
||||||
import { KeyboardKey } from '@standardnotes/ui-services'
|
import { KeyboardKey } from '@standardnotes/ui-services'
|
||||||
import { UuidString } from '@standardnotes/snjs'
|
|
||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
import { FunctionComponent, KeyboardEventHandler, UIEventHandler, useCallback } from 'react'
|
import { FunctionComponent, KeyboardEventHandler, UIEventHandler, useCallback } from 'react'
|
||||||
import { FOCUSABLE_BUT_NOT_TABBABLE, NOTES_LIST_SCROLL_THRESHOLD } from '@/Constants/Constants'
|
import { FOCUSABLE_BUT_NOT_TABBABLE, NOTES_LIST_SCROLL_THRESHOLD } from '@/Constants/Constants'
|
||||||
@@ -22,7 +21,7 @@ type Props = {
|
|||||||
navigationController: NavigationController
|
navigationController: NavigationController
|
||||||
notesController: NotesController
|
notesController: NotesController
|
||||||
selectionController: SelectedItemsController
|
selectionController: SelectedItemsController
|
||||||
selectedItems: Record<UuidString, ListableContentItem>
|
selectedUuids: SelectedItemsController['selectedUuids']
|
||||||
paginate: () => void
|
paginate: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,7 +33,7 @@ const ContentList: FunctionComponent<Props> = ({
|
|||||||
navigationController,
|
navigationController,
|
||||||
notesController,
|
notesController,
|
||||||
selectionController,
|
selectionController,
|
||||||
selectedItems,
|
selectedUuids,
|
||||||
paginate,
|
paginate,
|
||||||
}) => {
|
}) => {
|
||||||
const { selectPreviousItem, selectNextItem } = selectionController
|
const { selectPreviousItem, selectNextItem } = selectionController
|
||||||
@@ -82,7 +81,7 @@ const ContentList: FunctionComponent<Props> = ({
|
|||||||
key={item.uuid}
|
key={item.uuid}
|
||||||
application={application}
|
application={application}
|
||||||
item={item}
|
item={item}
|
||||||
selected={!!selectedItems[item.uuid]}
|
selected={selectedUuids.has(item.uuid)}
|
||||||
hideDate={hideDate}
|
hideDate={hideDate}
|
||||||
hidePreview={hideNotePreview}
|
hidePreview={hideNotePreview}
|
||||||
hideTags={hideTags}
|
hideTags={hideTags}
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ const ContentListView: FunctionComponent<Props> = ({
|
|||||||
searchBarElement,
|
searchBarElement,
|
||||||
} = itemListController
|
} = itemListController
|
||||||
|
|
||||||
const { selectedItems, selectNextItem, selectPreviousItem } = selectionController
|
const { selectedUuids, selectNextItem, selectPreviousItem } = selectionController
|
||||||
|
|
||||||
const isFilesSmartView = useMemo(
|
const isFilesSmartView = useMemo(
|
||||||
() => navigationController.selected?.uuid === SystemViewId.Files,
|
() => navigationController.selected?.uuid === SystemViewId.Files,
|
||||||
@@ -276,7 +276,7 @@ const ContentListView: FunctionComponent<Props> = ({
|
|||||||
{renderedItems.length ? (
|
{renderedItems.length ? (
|
||||||
<ContentList
|
<ContentList
|
||||||
items={renderedItems}
|
items={renderedItems}
|
||||||
selectedItems={selectedItems}
|
selectedUuids={selectedUuids}
|
||||||
application={application}
|
application={application}
|
||||||
paginate={paginate}
|
paginate={paginate}
|
||||||
filesController={filesController}
|
filesController={filesController}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { SwitchProps } from '@/Components/Switch/SwitchProps'
|
|||||||
import { IconType } from '@standardnotes/snjs'
|
import { IconType } from '@standardnotes/snjs'
|
||||||
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
|
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
|
||||||
import { MenuItemType } from './MenuItemType'
|
import { MenuItemType } from './MenuItemType'
|
||||||
import RadioIndicator from '../RadioIndicator/RadioIndicator'
|
import RadioIndicator from '../Radio/RadioIndicator'
|
||||||
|
|
||||||
type MenuItemProps = {
|
type MenuItemProps = {
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import LabsPane from './Labs/Labs'
|
|||||||
import Advanced from '@/Components/Preferences/Panes/General/Advanced/AdvancedSection'
|
import Advanced from '@/Components/Preferences/Panes/General/Advanced/AdvancedSection'
|
||||||
import PreferencesPane from '../../PreferencesComponents/PreferencesPane'
|
import PreferencesPane from '../../PreferencesComponents/PreferencesPane'
|
||||||
import PlaintextDefaults from './PlaintextDefaults'
|
import PlaintextDefaults from './PlaintextDefaults'
|
||||||
|
import Persistence from './Persistence'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
viewControllerManager: ViewControllerManager
|
viewControllerManager: ViewControllerManager
|
||||||
@@ -18,6 +19,7 @@ type Props = {
|
|||||||
|
|
||||||
const General: FunctionComponent<Props> = ({ viewControllerManager, application, extensionsLatestVersions }) => (
|
const General: FunctionComponent<Props> = ({ viewControllerManager, application, extensionsLatestVersions }) => (
|
||||||
<PreferencesPane>
|
<PreferencesPane>
|
||||||
|
<Persistence application={application} />
|
||||||
<PlaintextDefaults application={application} />
|
<PlaintextDefaults application={application} />
|
||||||
<Defaults application={application} />
|
<Defaults application={application} />
|
||||||
<Tools application={application} />
|
<Tools application={application} />
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { WebApplication } from '@/Application/Application'
|
||||||
|
import StyledRadioInput from '@/Components/Radio/StyledRadioInput'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { Title } from '../../PreferencesComponents/Content'
|
||||||
|
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
|
||||||
|
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
application: WebApplication
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShouldPersistNoteStateKey = 'ShouldPersistNoteState'
|
||||||
|
|
||||||
|
const Persistence = ({ application }: Props) => {
|
||||||
|
const [shouldPersistNoteState, setShouldPersistNoteState] = useState(application.getValue(ShouldPersistNoteStateKey))
|
||||||
|
|
||||||
|
const toggleStatePersistence = (shouldPersist: boolean) => {
|
||||||
|
application.setValue(ShouldPersistNoteStateKey, shouldPersist)
|
||||||
|
setShouldPersistNoteState(shouldPersist)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PreferencesGroup>
|
||||||
|
<PreferencesSegment>
|
||||||
|
<Title className="mb-2">When opening the app, show...</Title>
|
||||||
|
<label className="mb-2 flex items-center gap-2 text-sm font-medium">
|
||||||
|
<StyledRadioInput
|
||||||
|
name="state-persistence"
|
||||||
|
checked={!shouldPersistNoteState}
|
||||||
|
onChange={(event) => {
|
||||||
|
toggleStatePersistence(!event.target.checked)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
The first note in the list
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center gap-2 text-sm font-medium">
|
||||||
|
<StyledRadioInput
|
||||||
|
name="state-persistence"
|
||||||
|
checked={!!shouldPersistNoteState}
|
||||||
|
onChange={(event) => {
|
||||||
|
toggleStatePersistence(event.target.checked)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
The last viewed note
|
||||||
|
</label>
|
||||||
|
</PreferencesSegment>
|
||||||
|
</PreferencesGroup>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Persistence
|
||||||
@@ -16,7 +16,7 @@ import FocusModeSwitch from './FocusModeSwitch'
|
|||||||
import ThemesMenuButton from './ThemesMenuButton'
|
import ThemesMenuButton from './ThemesMenuButton'
|
||||||
import { ThemeItem } from './ThemeItem'
|
import { ThemeItem } from './ThemeItem'
|
||||||
import { sortThemes } from '@/Utils/SortThemes'
|
import { sortThemes } from '@/Utils/SortThemes'
|
||||||
import RadioIndicator from '../RadioIndicator/RadioIndicator'
|
import RadioIndicator from '../Radio/RadioIndicator'
|
||||||
import HorizontalSeparator from '../Shared/HorizontalSeparator'
|
import HorizontalSeparator from '../Shared/HorizontalSeparator'
|
||||||
import { QuickSettingsController } from '@/Controllers/QuickSettingsController'
|
import { QuickSettingsController } from '@/Controllers/QuickSettingsController'
|
||||||
import PanelSettingsSection from './PanelSettingsSection'
|
import PanelSettingsSection from './PanelSettingsSection'
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import Icon from '@/Components/Icon/Icon'
|
|||||||
import { usePremiumModal } from '@/Hooks/usePremiumModal'
|
import { usePremiumModal } from '@/Hooks/usePremiumModal'
|
||||||
import Switch from '@/Components/Switch/Switch'
|
import Switch from '@/Components/Switch/Switch'
|
||||||
import { ThemeItem } from './ThemeItem'
|
import { ThemeItem } from './ThemeItem'
|
||||||
import RadioIndicator from '../RadioIndicator/RadioIndicator'
|
import RadioIndicator from '../Radio/RadioIndicator'
|
||||||
import { PremiumFeatureIconClass, PremiumFeatureIconName } from '../Icon/PremiumFeatureIcon'
|
import { PremiumFeatureIconClass, PremiumFeatureIconName } from '../Icon/PremiumFeatureIcon'
|
||||||
import { isMobileScreen } from '@/Utils'
|
import { isMobileScreen } from '@/Utils'
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { classNames } from '@/Utils/ConcatenateClassNames'
|
||||||
|
import { ComponentPropsWithoutRef } from 'react'
|
||||||
|
import RadioIndicator from './RadioIndicator'
|
||||||
|
|
||||||
|
type Props = ComponentPropsWithoutRef<'input'>
|
||||||
|
|
||||||
|
const StyledRadioInput = (props: Props) => {
|
||||||
|
return (
|
||||||
|
<div className="flex">
|
||||||
|
<input type="radio" className={classNames('h-0 w-0 opacity-0', props.className)} {...props} />
|
||||||
|
<RadioIndicator checked={!!props.checked} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StyledRadioInput
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
|
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
|
||||||
import { FunctionComponent, ReactNode } from 'react'
|
import { FunctionComponent, ReactNode } from 'react'
|
||||||
import RadioIndicator from '../RadioIndicator/RadioIndicator'
|
import RadioIndicator from '../Radio/RadioIndicator'
|
||||||
|
|
||||||
type HistoryListItemProps = {
|
type HistoryListItemProps = {
|
||||||
isSelected: boolean
|
isSelected: boolean
|
||||||
|
|||||||
@@ -63,7 +63,9 @@ const SmartViewsListItem: FunctionComponent<Props> = ({ view, tagsState }) => {
|
|||||||
}, [setTitle, view])
|
}, [setTitle, view])
|
||||||
|
|
||||||
const selectCurrentTag = useCallback(async () => {
|
const selectCurrentTag = useCallback(async () => {
|
||||||
await tagsState.setSelectedTag(view)
|
await tagsState.setSelectedTag(view, {
|
||||||
|
userTriggered: true,
|
||||||
|
})
|
||||||
toggleAppPane(AppPaneId.Items)
|
toggleAppPane(AppPaneId.Items)
|
||||||
}, [tagsState, toggleAppPane, view])
|
}, [tagsState, toggleAppPane, view])
|
||||||
|
|
||||||
|
|||||||
@@ -88,7 +88,9 @@ export const TagsListItem: FunctionComponent<Props> = observer(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const selectCurrentTag = useCallback(async () => {
|
const selectCurrentTag = useCallback(async () => {
|
||||||
await tagsState.setSelectedTag(tag)
|
await tagsState.setSelectedTag(tag, {
|
||||||
|
userTriggered: true,
|
||||||
|
})
|
||||||
toggleAppPane(AppPaneId.Items)
|
toggleAppPane(AppPaneId.Items)
|
||||||
}, [tagsState, tag, toggleAppPane])
|
}, [tagsState, tag, toggleAppPane])
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export interface Persistable<T> {
|
||||||
|
getPersistableValue(): T
|
||||||
|
hydrateFromPersistedValue(value: T | undefined): void
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { WebApplication } from '@/Application/Application'
|
||||||
|
import { ShouldPersistNoteStateKey } from '@/Components/Preferences/Panes/General/Persistence'
|
||||||
|
import { ApplicationEvent, InternalEventBus } from '@standardnotes/snjs'
|
||||||
|
import { CrossControllerEvent } from '../CrossControllerEvent'
|
||||||
|
|
||||||
|
const MasterPersistenceKey = 'master-persistence-key'
|
||||||
|
|
||||||
|
export enum PersistenceKey {
|
||||||
|
SelectedItemsController = 'selected-items-controller',
|
||||||
|
NavigationController = 'navigation-controller',
|
||||||
|
ItemListController = 'item-list-controller',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MasterPersistedValue = Record<PersistenceKey, unknown>
|
||||||
|
|
||||||
|
export class PersistenceService {
|
||||||
|
private unsubAppEventObserver: () => void
|
||||||
|
|
||||||
|
constructor(private application: WebApplication, private eventBus: InternalEventBus) {
|
||||||
|
this.unsubAppEventObserver = this.application.addEventObserver(async (eventName) => {
|
||||||
|
if (!this.application) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
void this.onAppEvent(eventName)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async onAppEvent(eventName: ApplicationEvent) {
|
||||||
|
if (eventName === ApplicationEvent.LocalDataLoaded) {
|
||||||
|
let shouldHydrateState = this.application.getValue(ShouldPersistNoteStateKey)
|
||||||
|
|
||||||
|
if (typeof shouldHydrateState === 'undefined') {
|
||||||
|
this.application.setValue(ShouldPersistNoteStateKey, true)
|
||||||
|
shouldHydrateState = true
|
||||||
|
}
|
||||||
|
|
||||||
|
this.eventBus.publish({
|
||||||
|
type: CrossControllerEvent.HydrateFromPersistedValues,
|
||||||
|
payload: shouldHydrateState ? this.getPersistedValues() : undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
persistValues(values: MasterPersistedValue): void {
|
||||||
|
if (!this.application.isDatabaseLoaded()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.application.setValue(MasterPersistenceKey, values)
|
||||||
|
}
|
||||||
|
|
||||||
|
getPersistedValues(): MasterPersistedValue {
|
||||||
|
return this.application.getValue(MasterPersistenceKey) as MasterPersistedValue
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit() {
|
||||||
|
this.unsubAppEventObserver()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
export enum CrossControllerEvent {
|
export enum CrossControllerEvent {
|
||||||
TagChanged = 'TagChanged',
|
TagChanged = 'TagChanged',
|
||||||
ActiveEditorChanged = 'ActiveEditorChanged',
|
ActiveEditorChanged = 'ActiveEditorChanged',
|
||||||
|
HydrateFromPersistedValues = 'HydrateFromPersistedValues',
|
||||||
|
RequestValuePersistence = 'RequestValuePersistence',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import {
|
|||||||
} from '@standardnotes/snjs'
|
} from '@standardnotes/snjs'
|
||||||
import { action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'
|
import { action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'
|
||||||
import { WebApplication } from '../../Application/Application'
|
import { WebApplication } from '../../Application/Application'
|
||||||
import { AbstractViewController } from '../Abstract/AbstractViewController'
|
|
||||||
import { WebDisplayOptions } from './WebDisplayOptions'
|
import { WebDisplayOptions } from './WebDisplayOptions'
|
||||||
import { NavigationController } from '../Navigation/NavigationController'
|
import { NavigationController } from '../Navigation/NavigationController'
|
||||||
import { CrossControllerEvent } from '../CrossControllerEvent'
|
import { CrossControllerEvent } from '../CrossControllerEvent'
|
||||||
@@ -33,6 +32,8 @@ import { formatDateAndTimeForNote } from '@/Utils/DateUtils'
|
|||||||
import { PrefDefaults } from '@/Constants/PrefDefaults'
|
import { PrefDefaults } from '@/Constants/PrefDefaults'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { LinkingController } from '../LinkingController'
|
import { LinkingController } from '../LinkingController'
|
||||||
|
import { AbstractViewController } from '../Abstract/AbstractViewController'
|
||||||
|
import { Persistable } from '../Abstract/Persistable'
|
||||||
|
|
||||||
const MinNoteCellHeight = 51.0
|
const MinNoteCellHeight = 51.0
|
||||||
const DefaultListNumNotes = 20
|
const DefaultListNumNotes = 20
|
||||||
@@ -45,10 +46,18 @@ enum ItemsReloadSource {
|
|||||||
DisplayOptionsChange,
|
DisplayOptionsChange,
|
||||||
Pagination,
|
Pagination,
|
||||||
TagChange,
|
TagChange,
|
||||||
|
UserTriggeredTagChange,
|
||||||
FilterTextChange,
|
FilterTextChange,
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ItemListController extends AbstractViewController implements InternalEventHandlerInterface {
|
export type ItemListControllerPersistableValue = {
|
||||||
|
displayOptions: DisplayOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ItemListController
|
||||||
|
extends AbstractViewController
|
||||||
|
implements Persistable<ItemListControllerPersistableValue>, InternalEventHandlerInterface
|
||||||
|
{
|
||||||
completedFullSync = false
|
completedFullSync = false
|
||||||
noteFilterText = ''
|
noteFilterText = ''
|
||||||
notes: SNNote[] = []
|
notes: SNNote[] = []
|
||||||
@@ -204,6 +213,7 @@ export class ItemListController extends AbstractViewController implements Intern
|
|||||||
handleFilterTextChanged: action,
|
handleFilterTextChanged: action,
|
||||||
|
|
||||||
optionsSubtitle: computed,
|
optionsSubtitle: computed,
|
||||||
|
activeControllerItem: computed,
|
||||||
})
|
})
|
||||||
|
|
||||||
window.onresize = () => {
|
window.onresize = () => {
|
||||||
@@ -211,9 +221,25 @@ export class ItemListController extends AbstractViewController implements Intern
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPersistableValue = (): ItemListControllerPersistableValue => {
|
||||||
|
return {
|
||||||
|
displayOptions: this.displayOptions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hydrateFromPersistedValue = (state: ItemListControllerPersistableValue | undefined) => {
|
||||||
|
if (!state) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (state.displayOptions) {
|
||||||
|
this.displayOptions = state.displayOptions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async handleEvent(event: InternalEventInterface): Promise<void> {
|
async handleEvent(event: InternalEventInterface): Promise<void> {
|
||||||
if (event.type === CrossControllerEvent.TagChanged) {
|
if (event.type === CrossControllerEvent.TagChanged) {
|
||||||
this.handleTagChange()
|
const payload = event.payload as { userTriggered: boolean }
|
||||||
|
this.handleTagChange(payload.userTriggered)
|
||||||
} else if (event.type === CrossControllerEvent.ActiveEditorChanged) {
|
} else if (event.type === CrossControllerEvent.ActiveEditorChanged) {
|
||||||
this.handleEditorChange().catch(console.error)
|
this.handleEditorChange().catch(console.error)
|
||||||
}
|
}
|
||||||
@@ -335,8 +361,10 @@ export class ItemListController extends AbstractViewController implements Intern
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private shouldSelectFirstItem = (itemsReloadSource: ItemsReloadSource, activeItem: SNNote | FileItem | undefined) => {
|
private shouldSelectFirstItem = (itemsReloadSource: ItemsReloadSource) => {
|
||||||
return itemsReloadSource === ItemsReloadSource.TagChange || !activeItem
|
return (
|
||||||
|
itemsReloadSource === ItemsReloadSource.UserTriggeredTagChange || !this.selectionController.selectedUuids.size
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private shouldCloseActiveItem = (activeItem: SNNote | FileItem | undefined) => {
|
private shouldCloseActiveItem = (activeItem: SNNote | FileItem | undefined) => {
|
||||||
@@ -359,7 +387,7 @@ export class ItemListController extends AbstractViewController implements Intern
|
|||||||
}
|
}
|
||||||
|
|
||||||
private shouldSelectActiveItem = (activeItem: SNNote | FileItem | undefined) => {
|
private shouldSelectActiveItem = (activeItem: SNNote | FileItem | undefined) => {
|
||||||
return activeItem && !this.selectionController.selectedItems[activeItem.uuid]
|
return activeItem && !this.selectionController.isItemSelected(activeItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
private async recomputeSelectionAfterItemsReload(itemsReloadSource: ItemsReloadSource) {
|
private async recomputeSelectionAfterItemsReload(itemsReloadSource: ItemsReloadSource) {
|
||||||
@@ -371,7 +399,7 @@ export class ItemListController extends AbstractViewController implements Intern
|
|||||||
|
|
||||||
const activeItem = activeController?.item
|
const activeItem = activeController?.item
|
||||||
|
|
||||||
if (this.shouldSelectFirstItem(itemsReloadSource, activeItem)) {
|
if (this.shouldSelectFirstItem(itemsReloadSource)) {
|
||||||
await this.selectFirstItem()
|
await this.selectFirstItem()
|
||||||
} else if (this.shouldCloseActiveItem(activeItem) && activeController) {
|
} else if (this.shouldCloseActiveItem(activeItem) && activeController) {
|
||||||
this.closeItemController(activeController)
|
this.closeItemController(activeController)
|
||||||
@@ -500,6 +528,11 @@ export class ItemListController extends AbstractViewController implements Intern
|
|||||||
if (newDisplayOptions.sortBy !== currentSortBy) {
|
if (newDisplayOptions.sortBy !== currentSortBy) {
|
||||||
await this.selectFirstItem()
|
await this.selectFirstItem()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.eventBus.publish({
|
||||||
|
type: CrossControllerEvent.RequestValuePersistence,
|
||||||
|
payload: undefined,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async createNewNoteController(title?: string) {
|
async createNewNoteController(title?: string) {
|
||||||
@@ -664,7 +697,7 @@ export class ItemListController extends AbstractViewController implements Intern
|
|||||||
this.application.itemControllerGroup.closeItemController(controller)
|
this.application.itemControllerGroup.closeItemController(controller)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleTagChange = () => {
|
handleTagChange = (userTriggered: boolean) => {
|
||||||
const activeNoteController = this.getActiveItemController()
|
const activeNoteController = this.getActiveItemController()
|
||||||
if (activeNoteController instanceof NoteViewController && activeNoteController.isTemplateNote) {
|
if (activeNoteController instanceof NoteViewController && activeNoteController.isTemplateNote) {
|
||||||
this.closeItemController(activeNoteController)
|
this.closeItemController(activeNoteController)
|
||||||
@@ -682,7 +715,7 @@ export class ItemListController extends AbstractViewController implements Intern
|
|||||||
|
|
||||||
this.reloadNotesDisplayOptions()
|
this.reloadNotesDisplayOptions()
|
||||||
|
|
||||||
void this.reloadItems(ItemsReloadSource.TagChange)
|
void this.reloadItems(userTriggered ? ItemsReloadSource.UserTriggeredTagChange : ItemsReloadSource.TagChange)
|
||||||
}
|
}
|
||||||
|
|
||||||
onFilterEnter = () => {
|
onFilterEnter = () => {
|
||||||
|
|||||||
@@ -15,19 +15,28 @@ import {
|
|||||||
InternalEventBus,
|
InternalEventBus,
|
||||||
InternalEventPublishStrategy,
|
InternalEventPublishStrategy,
|
||||||
} from '@standardnotes/snjs'
|
} from '@standardnotes/snjs'
|
||||||
import { action, computed, makeAutoObservable, makeObservable, observable, runInAction } from 'mobx'
|
import { action, computed, makeAutoObservable, makeObservable, observable, reaction, runInAction } from 'mobx'
|
||||||
import { WebApplication } from '../../Application/Application'
|
import { WebApplication } from '../../Application/Application'
|
||||||
import { FeaturesController } from '../FeaturesController'
|
import { FeaturesController } from '../FeaturesController'
|
||||||
import { AbstractViewController } from '../Abstract/AbstractViewController'
|
|
||||||
import { destroyAllObjectProperties } from '@/Utils'
|
import { destroyAllObjectProperties } from '@/Utils'
|
||||||
import { isValidFutureSiblings, rootTags, tagSiblings } from './Utils'
|
import { isValidFutureSiblings, rootTags, tagSiblings } from './Utils'
|
||||||
import { AnyTag } from './AnyTagType'
|
import { AnyTag } from './AnyTagType'
|
||||||
import { CrossControllerEvent } from '../CrossControllerEvent'
|
import { CrossControllerEvent } from '../CrossControllerEvent'
|
||||||
|
import { AbstractViewController } from '../Abstract/AbstractViewController'
|
||||||
|
import { Persistable } from '../Abstract/Persistable'
|
||||||
|
|
||||||
export class NavigationController extends AbstractViewController {
|
export type NavigationControllerPersistableValue = {
|
||||||
|
selectedTagUuid: AnyTag['uuid']
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NavigationController
|
||||||
|
extends AbstractViewController
|
||||||
|
implements Persistable<NavigationControllerPersistableValue>
|
||||||
|
{
|
||||||
tags: SNTag[] = []
|
tags: SNTag[] = []
|
||||||
smartViews: SmartView[] = []
|
smartViews: SmartView[] = []
|
||||||
allNotesCount_ = 0
|
allNotesCount_ = 0
|
||||||
|
selectedUuid: AnyTag['uuid'] | undefined = undefined
|
||||||
selected_: AnyTag | undefined
|
selected_: AnyTag | undefined
|
||||||
previouslySelected_: AnyTag | undefined
|
previouslySelected_: AnyTag | undefined
|
||||||
editing_: SNTag | SmartView | undefined
|
editing_: SNTag | SmartView | undefined
|
||||||
@@ -63,12 +72,12 @@ export class NavigationController extends AbstractViewController {
|
|||||||
allNotesCount: computed,
|
allNotesCount: computed,
|
||||||
setAllNotesCount: action,
|
setAllNotesCount: action,
|
||||||
|
|
||||||
selected_: observable.ref,
|
selected_: observable,
|
||||||
previouslySelected_: observable.ref,
|
previouslySelected_: observable.ref,
|
||||||
previouslySelected: computed,
|
previouslySelected: computed,
|
||||||
editing_: observable.ref,
|
editing_: observable.ref,
|
||||||
selected: computed,
|
selected: computed,
|
||||||
selectedUuid: computed,
|
selectedUuid: observable,
|
||||||
editingTag: computed,
|
editingTag: computed,
|
||||||
|
|
||||||
addingSubtagTo: observable,
|
addingSubtagTo: observable,
|
||||||
@@ -94,6 +103,8 @@ export class NavigationController extends AbstractViewController {
|
|||||||
setContextMenuMaxHeight: action,
|
setContextMenuMaxHeight: action,
|
||||||
|
|
||||||
isInFilesView: computed,
|
isInFilesView: computed,
|
||||||
|
|
||||||
|
hydrateFromPersistedValue: action,
|
||||||
})
|
})
|
||||||
|
|
||||||
this.disposers.push(
|
this.disposers.push(
|
||||||
@@ -103,25 +114,23 @@ export class NavigationController extends AbstractViewController {
|
|||||||
|
|
||||||
this.smartViews = this.application.items.getSmartViews()
|
this.smartViews = this.application.items.getSmartViews()
|
||||||
|
|
||||||
const currrentSelectedTag = this.selected_
|
const currentSelectedTag = this.selected_
|
||||||
|
|
||||||
if (!currrentSelectedTag) {
|
|
||||||
this.setSelectedTagInstance(this.smartViews[0])
|
|
||||||
|
|
||||||
|
if (!currentSelectedTag) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedReference =
|
const updatedReference =
|
||||||
FindItem(changed, currrentSelectedTag.uuid) || FindItem(this.smartViews, currrentSelectedTag.uuid)
|
FindItem(changed, currentSelectedTag.uuid) || FindItem(this.smartViews, currentSelectedTag.uuid)
|
||||||
if (updatedReference) {
|
if (updatedReference) {
|
||||||
this.setSelectedTagInstance(updatedReference as AnyTag)
|
this.setSelectedTagInstance(updatedReference as AnyTag)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSystemView(currrentSelectedTag as SmartView)) {
|
if (isSystemView(currentSelectedTag as SmartView)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (FindItem(removed, currrentSelectedTag.uuid)) {
|
if (FindItem(removed, currentSelectedTag.uuid)) {
|
||||||
this.setSelectedTagInstance(this.smartViews[0])
|
this.setSelectedTagInstance(this.smartViews[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -140,6 +149,52 @@ export class NavigationController extends AbstractViewController {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
this.disposers.push(
|
||||||
|
reaction(
|
||||||
|
() => this.selectedUuid,
|
||||||
|
() => {
|
||||||
|
eventBus.publish({
|
||||||
|
type: CrossControllerEvent.RequestValuePersistence,
|
||||||
|
payload: undefined,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
findAndSetTag = (uuid: UuidString) => {
|
||||||
|
const tagToSelect = [...this.tags, ...this.smartViews].find((tag) => tag.uuid === uuid)
|
||||||
|
if (tagToSelect) {
|
||||||
|
void this.setSelectedTag(tagToSelect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectHydratedTagOrDefault = () => {
|
||||||
|
if (this.selectedUuid && !this.selected_) {
|
||||||
|
this.findAndSetTag(this.selectedUuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.selectedUuid) {
|
||||||
|
void this.selectHomeNavigationView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getPersistableValue = (): NavigationControllerPersistableValue => {
|
||||||
|
return {
|
||||||
|
selectedTagUuid: this.selectedUuid ? this.selectedUuid : SystemViewId.AllNotes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hydrateFromPersistedValue = (state: NavigationControllerPersistableValue | undefined) => {
|
||||||
|
if (!state) {
|
||||||
|
void this.selectHomeNavigationView()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (state.selectedTagUuid) {
|
||||||
|
this.selectedUuid = state.selectedTagUuid
|
||||||
|
this.selectHydratedTagOrDefault()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override deinit() {
|
override deinit() {
|
||||||
@@ -358,7 +413,7 @@ export class NavigationController extends AbstractViewController {
|
|||||||
return this.selected_
|
return this.selected_
|
||||||
}
|
}
|
||||||
|
|
||||||
public async setSelectedTag(tag: AnyTag | undefined) {
|
public async setSelectedTag(tag: AnyTag | undefined, { userTriggered } = { userTriggered: false }) {
|
||||||
if (tag && tag.conflictOf) {
|
if (tag && tag.conflictOf) {
|
||||||
this.application.mutator
|
this.application.mutator
|
||||||
.changeAndSaveItem(tag, (mutator) => {
|
.changeAndSaveItem(tag, (mutator) => {
|
||||||
@@ -384,7 +439,7 @@ export class NavigationController extends AbstractViewController {
|
|||||||
await this.eventBus.publishSync(
|
await this.eventBus.publishSync(
|
||||||
{
|
{
|
||||||
type: CrossControllerEvent.TagChanged,
|
type: CrossControllerEvent.TagChanged,
|
||||||
payload: { tag, previousTag: this.previouslySelected_ },
|
payload: { tag, previousTag: this.previouslySelected_, userTriggered: userTriggered },
|
||||||
},
|
},
|
||||||
InternalEventPublishStrategy.SEQUENCE,
|
InternalEventPublishStrategy.SEQUENCE,
|
||||||
)
|
)
|
||||||
@@ -407,7 +462,10 @@ export class NavigationController extends AbstractViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private setSelectedTagInstance(tag: AnyTag | undefined): void {
|
private setSelectedTagInstance(tag: AnyTag | undefined): void {
|
||||||
runInAction(() => (this.selected_ = tag))
|
runInAction(() => {
|
||||||
|
this.selected_ = tag
|
||||||
|
this.selectedUuid = tag ? tag.uuid : undefined
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
public setExpanded(tag: SNTag, expanded: boolean) {
|
public setExpanded(tag: SNTag, expanded: boolean) {
|
||||||
@@ -418,10 +476,6 @@ export class NavigationController extends AbstractViewController {
|
|||||||
.catch(console.error)
|
.catch(console.error)
|
||||||
}
|
}
|
||||||
|
|
||||||
public get selectedUuid(): UuidString | undefined {
|
|
||||||
return this.selected_?.uuid
|
|
||||||
}
|
|
||||||
|
|
||||||
public get editingTag(): SNTag | SmartView | undefined {
|
public get editingTag(): SNTag | SmartView | undefined {
|
||||||
return this.editing_
|
return this.editing_
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,20 +63,6 @@ export class NotesController extends AbstractViewController {
|
|||||||
this.itemListController = itemListController
|
this.itemListController = itemListController
|
||||||
|
|
||||||
this.disposers.push(
|
this.disposers.push(
|
||||||
this.application.streamItems<SNNote>(ContentType.Note, ({ changed, inserted, removed }) => {
|
|
||||||
runInAction(() => {
|
|
||||||
for (const removedNote of removed) {
|
|
||||||
this.selectionController.deselectItem(removedNote)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const note of [...changed, ...inserted]) {
|
|
||||||
if (this.selectionController.isItemSelected(note)) {
|
|
||||||
this.selectionController.updateReferenceOfSelectedItem(note)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
|
|
||||||
this.application.itemControllerGroup.addActiveControllerChangeObserver(() => {
|
this.application.itemControllerGroup.addActiveControllerChangeObserver(() => {
|
||||||
const controllers = this.application.itemControllerGroup.itemControllers
|
const controllers = this.application.itemControllerGroup.itemControllers
|
||||||
|
|
||||||
@@ -278,7 +264,7 @@ export class NotesController extends AbstractViewController {
|
|||||||
})
|
})
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.selectionController.setSelectedItems({})
|
this.selectionController.deselectAll()
|
||||||
this.contextMenuOpen = false
|
this.contextMenuOpen = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -295,7 +281,7 @@ export class NotesController extends AbstractViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
unselectNotes(): void {
|
unselectNotes(): void {
|
||||||
this.selectionController.setSelectedItems({})
|
this.selectionController.deselectAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
getSpellcheckStateForNote(note: SNNote) {
|
getSpellcheckStateForNote(note: SNNote) {
|
||||||
|
|||||||
@@ -7,17 +7,25 @@ import {
|
|||||||
SNNote,
|
SNNote,
|
||||||
UuidString,
|
UuidString,
|
||||||
InternalEventBus,
|
InternalEventBus,
|
||||||
|
isFile,
|
||||||
} from '@standardnotes/snjs'
|
} from '@standardnotes/snjs'
|
||||||
import { action, computed, makeObservable, observable, runInAction } from 'mobx'
|
import { action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'
|
||||||
import { WebApplication } from '../Application/Application'
|
import { WebApplication } from '../Application/Application'
|
||||||
import { AbstractViewController } from './Abstract/AbstractViewController'
|
import { AbstractViewController } from './Abstract/AbstractViewController'
|
||||||
|
import { Persistable } from './Abstract/Persistable'
|
||||||
|
import { CrossControllerEvent } from './CrossControllerEvent'
|
||||||
import { ItemListController } from './ItemList/ItemListController'
|
import { ItemListController } from './ItemList/ItemListController'
|
||||||
|
|
||||||
type SelectedItems = Record<UuidString, ListableContentItem>
|
export type SelectionControllerPersistableValue = {
|
||||||
|
selectedUuids: UuidString[]
|
||||||
|
}
|
||||||
|
|
||||||
export class SelectedItemsController extends AbstractViewController {
|
export class SelectedItemsController
|
||||||
|
extends AbstractViewController
|
||||||
|
implements Persistable<SelectionControllerPersistableValue>
|
||||||
|
{
|
||||||
lastSelectedItem: ListableContentItem | undefined
|
lastSelectedItem: ListableContentItem | undefined
|
||||||
selectedItems: SelectedItems = {}
|
selectedUuids: Set<UuidString> = observable(new Set<UuidString>())
|
||||||
private itemListController!: ItemListController
|
private itemListController!: ItemListController
|
||||||
|
|
||||||
override deinit(): void {
|
override deinit(): void {
|
||||||
@@ -29,7 +37,7 @@ export class SelectedItemsController extends AbstractViewController {
|
|||||||
super(application, eventBus)
|
super(application, eventBus)
|
||||||
|
|
||||||
makeObservable(this, {
|
makeObservable(this, {
|
||||||
selectedItems: observable,
|
selectedUuids: observable,
|
||||||
|
|
||||||
selectedItemsCount: computed,
|
selectedItemsCount: computed,
|
||||||
selectedFiles: computed,
|
selectedFiles: computed,
|
||||||
@@ -37,30 +45,50 @@ export class SelectedItemsController extends AbstractViewController {
|
|||||||
firstSelectedItem: computed,
|
firstSelectedItem: computed,
|
||||||
|
|
||||||
selectItem: action,
|
selectItem: action,
|
||||||
setSelectedItems: action,
|
setSelectedUuids: action,
|
||||||
|
|
||||||
|
hydrateFromPersistedValue: action,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.disposers.push(
|
||||||
|
reaction(
|
||||||
|
() => this.selectedUuids,
|
||||||
|
() => {
|
||||||
|
eventBus.publish({
|
||||||
|
type: CrossControllerEvent.RequestValuePersistence,
|
||||||
|
payload: undefined,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
getPersistableValue = (): SelectionControllerPersistableValue => {
|
||||||
|
return {
|
||||||
|
selectedUuids: Array.from(this.selectedUuids),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hydrateFromPersistedValue = (state: SelectionControllerPersistableValue | undefined): void => {
|
||||||
|
if (!state) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!this.selectedUuids.size && state.selectedUuids.length > 0) {
|
||||||
|
void this.selectUuids(state.selectedUuids)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public setServicesPostConstruction(itemListController: ItemListController) {
|
public setServicesPostConstruction(itemListController: ItemListController) {
|
||||||
this.itemListController = itemListController
|
this.itemListController = itemListController
|
||||||
|
|
||||||
this.disposers.push(
|
this.disposers.push(
|
||||||
this.application.streamItems<SNNote | FileItem>(
|
this.application.streamItems<SNNote | FileItem>([ContentType.Note, ContentType.File], ({ removed }) => {
|
||||||
[ContentType.Note, ContentType.File],
|
runInAction(() => {
|
||||||
({ changed, inserted, removed }) => {
|
for (const removedNote of removed) {
|
||||||
runInAction(() => {
|
this.removeFromSelectedUuids(removedNote.uuid)
|
||||||
for (const removedNote of removed) {
|
}
|
||||||
delete this.selectedItems[removedNote.uuid]
|
})
|
||||||
}
|
}),
|
||||||
|
|
||||||
for (const item of [...changed, ...inserted]) {
|
|
||||||
if (this.selectedItems[item.uuid]) {
|
|
||||||
this.selectedItems[item.uuid] = item
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +97,7 @@ export class SelectedItemsController extends AbstractViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get selectedItemsCount(): number {
|
get selectedItemsCount(): number {
|
||||||
return Object.keys(this.selectedItems).length
|
return this.selectedUuids.size
|
||||||
}
|
}
|
||||||
|
|
||||||
get selectedFiles(): FileItem[] {
|
get selectedFiles(): FileItem[] {
|
||||||
@@ -81,21 +109,31 @@ export class SelectedItemsController extends AbstractViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get firstSelectedItem() {
|
get firstSelectedItem() {
|
||||||
return this.getSelectedItems()[0]
|
return this.application.items.findSureItem(Array.from(this.selectedUuids)[0]) as ListableContentItem
|
||||||
}
|
}
|
||||||
|
|
||||||
getSelectedItems = <T extends ListableContentItem = ListableContentItem>(contentType?: ContentType): T[] => {
|
getSelectedItems = <T extends ListableContentItem = ListableContentItem>(contentType?: ContentType): T[] => {
|
||||||
return Object.values(this.selectedItems).filter((item) => {
|
const uuids = Array.from(this.selectedUuids)
|
||||||
return !contentType ? true : item.content_type === contentType
|
return uuids.length > 0
|
||||||
}) as T[]
|
? (uuids
|
||||||
|
.map((uuid) => this.application.items.findSureItem(uuid))
|
||||||
|
.filter((item) => {
|
||||||
|
return !contentType ? true : item.content_type === contentType
|
||||||
|
}) as T[])
|
||||||
|
: []
|
||||||
}
|
}
|
||||||
|
|
||||||
setSelectedItems = (selectedItems: SelectedItems) => {
|
setSelectedUuids = (selectedUuids: Set<UuidString>) => {
|
||||||
this.selectedItems = selectedItems
|
this.selectedUuids = new Set(selectedUuids)
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeFromSelectedUuids = (uuid: UuidString) => {
|
||||||
|
this.selectedUuids.delete(uuid)
|
||||||
|
this.setSelectedUuids(this.selectedUuids)
|
||||||
}
|
}
|
||||||
|
|
||||||
public deselectItem = (item: { uuid: ListableContentItem['uuid'] }): void => {
|
public deselectItem = (item: { uuid: ListableContentItem['uuid'] }): void => {
|
||||||
delete this.selectedItems[item.uuid]
|
this.removeFromSelectedUuids(item.uuid)
|
||||||
|
|
||||||
if (item.uuid === this.lastSelectedItem?.uuid) {
|
if (item.uuid === this.lastSelectedItem?.uuid) {
|
||||||
this.lastSelectedItem = undefined
|
this.lastSelectedItem = undefined
|
||||||
@@ -103,11 +141,7 @@ export class SelectedItemsController extends AbstractViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public isItemSelected = (item: ListableContentItem): boolean => {
|
public isItemSelected = (item: ListableContentItem): boolean => {
|
||||||
return this.selectedItems[item.uuid] != undefined
|
return this.selectedUuids.has(item.uuid)
|
||||||
}
|
|
||||||
|
|
||||||
public updateReferenceOfSelectedItem = (item: ListableContentItem): void => {
|
|
||||||
this.selectedItems[item.uuid] = item
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private selectItemsRange = async ({
|
private selectItemsRange = async ({
|
||||||
@@ -138,7 +172,7 @@ export class SelectedItemsController extends AbstractViewController {
|
|||||||
|
|
||||||
for (const item of authorizedItems) {
|
for (const item of authorizedItems) {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.selectedItems[item.uuid] = item
|
this.setSelectedUuids(this.selectedUuids.add(item.uuid))
|
||||||
this.lastSelectedItem = item
|
this.lastSelectedItem = item
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -147,7 +181,7 @@ export class SelectedItemsController extends AbstractViewController {
|
|||||||
cancelMultipleSelection = () => {
|
cancelMultipleSelection = () => {
|
||||||
this.io.cancelAllKeyboardModifiers()
|
this.io.cancelAllKeyboardModifiers()
|
||||||
|
|
||||||
const firstSelectedItem = this.getSelectedItems()[0]
|
const firstSelectedItem = this.firstSelectedItem
|
||||||
|
|
||||||
if (firstSelectedItem) {
|
if (firstSelectedItem) {
|
||||||
this.replaceSelection(firstSelectedItem)
|
this.replaceSelection(firstSelectedItem)
|
||||||
@@ -157,9 +191,8 @@ export class SelectedItemsController extends AbstractViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private replaceSelection = (item: ListableContentItem): void => {
|
private replaceSelection = (item: ListableContentItem): void => {
|
||||||
this.setSelectedItems({
|
this.deselectAll()
|
||||||
[item.uuid]: item,
|
this.setSelectedUuids(this.selectedUuids.add(item.uuid))
|
||||||
})
|
|
||||||
|
|
||||||
this.lastSelectedItem = item
|
this.lastSelectedItem = item
|
||||||
}
|
}
|
||||||
@@ -171,12 +204,25 @@ export class SelectedItemsController extends AbstractViewController {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private deselectAll = (): void => {
|
deselectAll = (): void => {
|
||||||
this.setSelectedItems({})
|
this.selectedUuids.clear()
|
||||||
|
this.setSelectedUuids(this.selectedUuids)
|
||||||
|
|
||||||
this.lastSelectedItem = undefined
|
this.lastSelectedItem = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openSingleSelectedItem = async () => {
|
||||||
|
if (this.selectedItemsCount === 1) {
|
||||||
|
const item = this.firstSelectedItem
|
||||||
|
|
||||||
|
if (item.content_type === ContentType.Note) {
|
||||||
|
await this.itemListController.openNote(item.uuid)
|
||||||
|
} else if (item.content_type === ContentType.File) {
|
||||||
|
await this.itemListController.openFile(item.uuid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
selectItem = async (
|
selectItem = async (
|
||||||
uuid: UuidString,
|
uuid: UuidString,
|
||||||
userTriggered?: boolean,
|
userTriggered?: boolean,
|
||||||
@@ -197,33 +243,25 @@ export class SelectedItemsController extends AbstractViewController {
|
|||||||
const isAuthorizedForAccess = await this.application.protections.authorizeItemAccess(item)
|
const isAuthorizedForAccess = await this.application.protections.authorizeItemAccess(item)
|
||||||
|
|
||||||
if (userTriggered && (hasMeta || hasCtrl)) {
|
if (userTriggered && (hasMeta || hasCtrl)) {
|
||||||
if (this.selectedItems[uuid] && hasMoreThanOneSelected) {
|
if (this.selectedUuids.has(uuid) && hasMoreThanOneSelected) {
|
||||||
delete this.selectedItems[uuid]
|
this.removeFromSelectedUuids(uuid)
|
||||||
} else if (isAuthorizedForAccess) {
|
} else if (isAuthorizedForAccess) {
|
||||||
this.selectedItems[uuid] = item
|
this.setSelectedUuids(this.selectedUuids.add(uuid))
|
||||||
this.lastSelectedItem = item
|
this.lastSelectedItem = item
|
||||||
}
|
}
|
||||||
} else if (userTriggered && hasShift) {
|
} else if (userTriggered && hasShift) {
|
||||||
await this.selectItemsRange({ selectedItem: item })
|
await this.selectItemsRange({ selectedItem: item })
|
||||||
} else {
|
} else {
|
||||||
const shouldSelectNote = hasMoreThanOneSelected || !this.selectedItems[uuid]
|
const shouldSelectNote = hasMoreThanOneSelected || !this.selectedUuids.has(uuid)
|
||||||
if (shouldSelectNote && isAuthorizedForAccess) {
|
if (shouldSelectNote && isAuthorizedForAccess) {
|
||||||
this.replaceSelection(item)
|
this.replaceSelection(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.selectedItemsCount === 1) {
|
await this.openSingleSelectedItem()
|
||||||
const item = Object.values(this.selectedItems)[0]
|
|
||||||
|
|
||||||
if (item.content_type === ContentType.Note) {
|
|
||||||
await this.itemListController.openNote(item.uuid)
|
|
||||||
} else if (item.content_type === ContentType.File) {
|
|
||||||
await this.itemListController.openFile(item.uuid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
didSelect: this.selectedItems[uuid] != undefined,
|
didSelect: this.selectedUuids.has(uuid),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,6 +281,20 @@ export class SelectedItemsController extends AbstractViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
selectUuids = async (uuids: UuidString[], userTriggered = false) => {
|
||||||
|
const itemsForUuids = this.application.items.findItems(uuids)
|
||||||
|
if (itemsForUuids.length < 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!userTriggered && itemsForUuids.some((item) => item.protected && isFile(item))) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.setSelectedUuids(new Set(uuids))
|
||||||
|
if (itemsForUuids.length === 1) {
|
||||||
|
void this.openSingleSelectedItem()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
selectNextItem = () => {
|
selectNextItem = () => {
|
||||||
const displayableItems = this.itemListController.items
|
const displayableItems = this.itemListController.items
|
||||||
|
|
||||||
|
|||||||
@@ -11,13 +11,15 @@ import {
|
|||||||
ItemCounterInterface,
|
ItemCounterInterface,
|
||||||
ItemCounter,
|
ItemCounter,
|
||||||
SubscriptionClientInterface,
|
SubscriptionClientInterface,
|
||||||
|
InternalEventHandlerInterface,
|
||||||
|
InternalEventInterface,
|
||||||
} from '@standardnotes/snjs'
|
} from '@standardnotes/snjs'
|
||||||
import { action, makeObservable, observable } from 'mobx'
|
import { action, makeObservable, observable } from 'mobx'
|
||||||
import { ActionsMenuController } from './ActionsMenuController'
|
import { ActionsMenuController } from './ActionsMenuController'
|
||||||
import { FeaturesController } from './FeaturesController'
|
import { FeaturesController } from './FeaturesController'
|
||||||
import { FilesController } from './FilesController'
|
import { FilesController } from './FilesController'
|
||||||
import { NotesController } from './NotesController'
|
import { NotesController } from './NotesController'
|
||||||
import { ItemListController } from './ItemList/ItemListController'
|
import { ItemListController, ItemListControllerPersistableValue } from './ItemList/ItemListController'
|
||||||
import { NoAccountWarningController } from './NoAccountWarningController'
|
import { NoAccountWarningController } from './NoAccountWarningController'
|
||||||
import { PreferencesController } from './PreferencesController'
|
import { PreferencesController } from './PreferencesController'
|
||||||
import { PurchaseFlowController } from './PurchaseFlow/PurchaseFlowController'
|
import { PurchaseFlowController } from './PurchaseFlow/PurchaseFlowController'
|
||||||
@@ -25,14 +27,16 @@ import { QuickSettingsController } from './QuickSettingsController'
|
|||||||
import { SearchOptionsController } from './SearchOptionsController'
|
import { SearchOptionsController } from './SearchOptionsController'
|
||||||
import { SubscriptionController } from './Subscription/SubscriptionController'
|
import { SubscriptionController } from './Subscription/SubscriptionController'
|
||||||
import { SyncStatusController } from './SyncStatusController'
|
import { SyncStatusController } from './SyncStatusController'
|
||||||
import { NavigationController } from './Navigation/NavigationController'
|
import { NavigationController, NavigationControllerPersistableValue } from './Navigation/NavigationController'
|
||||||
import { FilePreviewModalController } from './FilePreviewModalController'
|
import { FilePreviewModalController } from './FilePreviewModalController'
|
||||||
import { SelectedItemsController } from './SelectedItemsController'
|
import { SelectedItemsController, SelectionControllerPersistableValue } from './SelectedItemsController'
|
||||||
import { HistoryModalController } from './NoteHistory/HistoryModalController'
|
import { HistoryModalController } from './NoteHistory/HistoryModalController'
|
||||||
import { AccountMenuPane } from '@/Components/AccountMenu/AccountMenuPane'
|
import { AccountMenuPane } from '@/Components/AccountMenu/AccountMenuPane'
|
||||||
import { LinkingController } from './LinkingController'
|
import { LinkingController } from './LinkingController'
|
||||||
|
import { MasterPersistedValue, PersistenceKey, PersistenceService } from './Abstract/PersistenceService'
|
||||||
|
import { CrossControllerEvent } from './CrossControllerEvent'
|
||||||
|
|
||||||
export class ViewControllerManager {
|
export class ViewControllerManager implements InternalEventHandlerInterface {
|
||||||
readonly enableUnfinishedFeatures: boolean = window?.enabledUnfinishedFeatures
|
readonly enableUnfinishedFeatures: boolean = window?.enabledUnfinishedFeatures
|
||||||
|
|
||||||
private unsubAppEventObserver!: () => void
|
private unsubAppEventObserver!: () => void
|
||||||
@@ -65,10 +69,16 @@ export class ViewControllerManager {
|
|||||||
private eventBus: InternalEventBus
|
private eventBus: InternalEventBus
|
||||||
private itemCounter: ItemCounterInterface
|
private itemCounter: ItemCounterInterface
|
||||||
private subscriptionManager: SubscriptionClientInterface
|
private subscriptionManager: SubscriptionClientInterface
|
||||||
|
private persistenceService: PersistenceService
|
||||||
|
|
||||||
constructor(public application: WebApplication, private device: WebOrDesktopDeviceInterface) {
|
constructor(public application: WebApplication, private device: WebOrDesktopDeviceInterface) {
|
||||||
this.eventBus = new InternalEventBus()
|
this.eventBus = new InternalEventBus()
|
||||||
|
|
||||||
|
this.persistenceService = new PersistenceService(application, this.eventBus)
|
||||||
|
|
||||||
|
this.eventBus.addEventHandler(this, CrossControllerEvent.HydrateFromPersistedValues)
|
||||||
|
this.eventBus.addEventHandler(this, CrossControllerEvent.RequestValuePersistence)
|
||||||
|
|
||||||
this.itemCounter = new ItemCounter()
|
this.itemCounter = new ItemCounter()
|
||||||
|
|
||||||
this.subscriptionManager = application.subscriptions
|
this.subscriptionManager = application.subscriptions
|
||||||
@@ -173,6 +183,9 @@ export class ViewControllerManager {
|
|||||||
;(this.quickSettingsMenuController as unknown) = undefined
|
;(this.quickSettingsMenuController as unknown) = undefined
|
||||||
;(this.syncStatusController as unknown) = undefined
|
;(this.syncStatusController as unknown) = undefined
|
||||||
|
|
||||||
|
this.persistenceService.deinit()
|
||||||
|
;(this.persistenceService as unknown) = undefined
|
||||||
|
|
||||||
this.actionsMenuController.reset()
|
this.actionsMenuController.reset()
|
||||||
;(this.actionsMenuController as unknown) = undefined
|
;(this.actionsMenuController as unknown) = undefined
|
||||||
|
|
||||||
@@ -264,4 +277,33 @@ export class ViewControllerManager {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
persistValues = (): void => {
|
||||||
|
const values: MasterPersistedValue = {
|
||||||
|
[PersistenceKey.SelectedItemsController]: this.selectionController.getPersistableValue(),
|
||||||
|
[PersistenceKey.NavigationController]: this.navigationController.getPersistableValue(),
|
||||||
|
[PersistenceKey.ItemListController]: this.itemListController.getPersistableValue(),
|
||||||
|
}
|
||||||
|
|
||||||
|
this.persistenceService.persistValues(values)
|
||||||
|
}
|
||||||
|
|
||||||
|
hydrateFromPersistedValues = (values: MasterPersistedValue | undefined): void => {
|
||||||
|
const itemListState = values?.[PersistenceKey.ItemListController] as ItemListControllerPersistableValue
|
||||||
|
this.itemListController.hydrateFromPersistedValue(itemListState)
|
||||||
|
|
||||||
|
const selectedItemsState = values?.[PersistenceKey.SelectedItemsController] as SelectionControllerPersistableValue
|
||||||
|
this.selectionController.hydrateFromPersistedValue(selectedItemsState)
|
||||||
|
|
||||||
|
const navigationState = values?.[PersistenceKey.NavigationController] as NavigationControllerPersistableValue
|
||||||
|
this.navigationController.hydrateFromPersistedValue(navigationState)
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleEvent(event: InternalEventInterface): Promise<void> {
|
||||||
|
if (event.type === CrossControllerEvent.HydrateFromPersistedValues) {
|
||||||
|
this.hydrateFromPersistedValues(event.payload as MasterPersistedValue | undefined)
|
||||||
|
} else if (event.type === CrossControllerEvent.RequestValuePersistence) {
|
||||||
|
this.persistValues()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user