feat: persist selected tag & note locally (#1851)

This commit is contained in:
Aman Harwara
2022-10-20 18:56:59 +05:30
committed by GitHub
parent 6c47f95748
commit 4432f1cb4c
20 changed files with 421 additions and 116 deletions

View File

@@ -1,6 +1,5 @@
import { WebApplication } from '@/Application/Application'
import { KeyboardKey } from '@standardnotes/ui-services'
import { UuidString } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite'
import { FunctionComponent, KeyboardEventHandler, UIEventHandler, useCallback } from 'react'
import { FOCUSABLE_BUT_NOT_TABBABLE, NOTES_LIST_SCROLL_THRESHOLD } from '@/Constants/Constants'
@@ -22,7 +21,7 @@ type Props = {
navigationController: NavigationController
notesController: NotesController
selectionController: SelectedItemsController
selectedItems: Record<UuidString, ListableContentItem>
selectedUuids: SelectedItemsController['selectedUuids']
paginate: () => void
}
@@ -34,7 +33,7 @@ const ContentList: FunctionComponent<Props> = ({
navigationController,
notesController,
selectionController,
selectedItems,
selectedUuids,
paginate,
}) => {
const { selectPreviousItem, selectNextItem } = selectionController
@@ -82,7 +81,7 @@ const ContentList: FunctionComponent<Props> = ({
key={item.uuid}
application={application}
item={item}
selected={!!selectedItems[item.uuid]}
selected={selectedUuids.has(item.uuid)}
hideDate={hideDate}
hidePreview={hideNotePreview}
hideTags={hideTags}

View File

@@ -109,7 +109,7 @@ const ContentListView: FunctionComponent<Props> = ({
searchBarElement,
} = itemListController
const { selectedItems, selectNextItem, selectPreviousItem } = selectionController
const { selectedUuids, selectNextItem, selectPreviousItem } = selectionController
const isFilesSmartView = useMemo(
() => navigationController.selected?.uuid === SystemViewId.Files,
@@ -276,7 +276,7 @@ const ContentListView: FunctionComponent<Props> = ({
{renderedItems.length ? (
<ContentList
items={renderedItems}
selectedItems={selectedItems}
selectedUuids={selectedUuids}
application={application}
paginate={paginate}
filesController={filesController}

View File

@@ -5,7 +5,7 @@ import { SwitchProps } from '@/Components/Switch/SwitchProps'
import { IconType } from '@standardnotes/snjs'
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
import { MenuItemType } from './MenuItemType'
import RadioIndicator from '../RadioIndicator/RadioIndicator'
import RadioIndicator from '../Radio/RadioIndicator'
type MenuItemProps = {
children: ReactNode

View File

@@ -9,6 +9,7 @@ import LabsPane from './Labs/Labs'
import Advanced from '@/Components/Preferences/Panes/General/Advanced/AdvancedSection'
import PreferencesPane from '../../PreferencesComponents/PreferencesPane'
import PlaintextDefaults from './PlaintextDefaults'
import Persistence from './Persistence'
type Props = {
viewControllerManager: ViewControllerManager
@@ -18,6 +19,7 @@ type Props = {
const General: FunctionComponent<Props> = ({ viewControllerManager, application, extensionsLatestVersions }) => (
<PreferencesPane>
<Persistence application={application} />
<PlaintextDefaults application={application} />
<Defaults application={application} />
<Tools application={application} />

View File

@@ -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

View File

@@ -16,7 +16,7 @@ import FocusModeSwitch from './FocusModeSwitch'
import ThemesMenuButton from './ThemesMenuButton'
import { ThemeItem } from './ThemeItem'
import { sortThemes } from '@/Utils/SortThemes'
import RadioIndicator from '../RadioIndicator/RadioIndicator'
import RadioIndicator from '../Radio/RadioIndicator'
import HorizontalSeparator from '../Shared/HorizontalSeparator'
import { QuickSettingsController } from '@/Controllers/QuickSettingsController'
import PanelSettingsSection from './PanelSettingsSection'

View File

@@ -5,7 +5,7 @@ import Icon from '@/Components/Icon/Icon'
import { usePremiumModal } from '@/Hooks/usePremiumModal'
import Switch from '@/Components/Switch/Switch'
import { ThemeItem } from './ThemeItem'
import RadioIndicator from '../RadioIndicator/RadioIndicator'
import RadioIndicator from '../Radio/RadioIndicator'
import { PremiumFeatureIconClass, PremiumFeatureIconName } from '../Icon/PremiumFeatureIcon'
import { isMobileScreen } from '@/Utils'

View File

@@ -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

View File

@@ -1,6 +1,6 @@
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
import { FunctionComponent, ReactNode } from 'react'
import RadioIndicator from '../RadioIndicator/RadioIndicator'
import RadioIndicator from '../Radio/RadioIndicator'
type HistoryListItemProps = {
isSelected: boolean

View File

@@ -63,7 +63,9 @@ const SmartViewsListItem: FunctionComponent<Props> = ({ view, tagsState }) => {
}, [setTitle, view])
const selectCurrentTag = useCallback(async () => {
await tagsState.setSelectedTag(view)
await tagsState.setSelectedTag(view, {
userTriggered: true,
})
toggleAppPane(AppPaneId.Items)
}, [tagsState, toggleAppPane, view])

View File

@@ -88,7 +88,9 @@ export const TagsListItem: FunctionComponent<Props> = observer(
)
const selectCurrentTag = useCallback(async () => {
await tagsState.setSelectedTag(tag)
await tagsState.setSelectedTag(tag, {
userTriggered: true,
})
toggleAppPane(AppPaneId.Items)
}, [tagsState, tag, toggleAppPane])

View File

@@ -0,0 +1,4 @@
export interface Persistable<T> {
getPersistableValue(): T
hydrateFromPersistedValue(value: T | undefined): void
}

View File

@@ -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()
}
}

View File

@@ -1,4 +1,6 @@
export enum CrossControllerEvent {
TagChanged = 'TagChanged',
ActiveEditorChanged = 'ActiveEditorChanged',
HydrateFromPersistedValues = 'HydrateFromPersistedValues',
RequestValuePersistence = 'RequestValuePersistence',
}

View File

@@ -22,7 +22,6 @@ import {
} from '@standardnotes/snjs'
import { action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'
import { WebApplication } from '../../Application/Application'
import { AbstractViewController } from '../Abstract/AbstractViewController'
import { WebDisplayOptions } from './WebDisplayOptions'
import { NavigationController } from '../Navigation/NavigationController'
import { CrossControllerEvent } from '../CrossControllerEvent'
@@ -33,6 +32,8 @@ import { formatDateAndTimeForNote } from '@/Utils/DateUtils'
import { PrefDefaults } from '@/Constants/PrefDefaults'
import dayjs from 'dayjs'
import { LinkingController } from '../LinkingController'
import { AbstractViewController } from '../Abstract/AbstractViewController'
import { Persistable } from '../Abstract/Persistable'
const MinNoteCellHeight = 51.0
const DefaultListNumNotes = 20
@@ -45,10 +46,18 @@ enum ItemsReloadSource {
DisplayOptionsChange,
Pagination,
TagChange,
UserTriggeredTagChange,
FilterTextChange,
}
export class ItemListController extends AbstractViewController implements InternalEventHandlerInterface {
export type ItemListControllerPersistableValue = {
displayOptions: DisplayOptions
}
export class ItemListController
extends AbstractViewController
implements Persistable<ItemListControllerPersistableValue>, InternalEventHandlerInterface
{
completedFullSync = false
noteFilterText = ''
notes: SNNote[] = []
@@ -204,6 +213,7 @@ export class ItemListController extends AbstractViewController implements Intern
handleFilterTextChanged: action,
optionsSubtitle: computed,
activeControllerItem: computed,
})
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> {
if (event.type === CrossControllerEvent.TagChanged) {
this.handleTagChange()
const payload = event.payload as { userTriggered: boolean }
this.handleTagChange(payload.userTriggered)
} else if (event.type === CrossControllerEvent.ActiveEditorChanged) {
this.handleEditorChange().catch(console.error)
}
@@ -335,8 +361,10 @@ export class ItemListController extends AbstractViewController implements Intern
)
}
private shouldSelectFirstItem = (itemsReloadSource: ItemsReloadSource, activeItem: SNNote | FileItem | undefined) => {
return itemsReloadSource === ItemsReloadSource.TagChange || !activeItem
private shouldSelectFirstItem = (itemsReloadSource: ItemsReloadSource) => {
return (
itemsReloadSource === ItemsReloadSource.UserTriggeredTagChange || !this.selectionController.selectedUuids.size
)
}
private shouldCloseActiveItem = (activeItem: SNNote | FileItem | undefined) => {
@@ -359,7 +387,7 @@ export class ItemListController extends AbstractViewController implements Intern
}
private shouldSelectActiveItem = (activeItem: SNNote | FileItem | undefined) => {
return activeItem && !this.selectionController.selectedItems[activeItem.uuid]
return activeItem && !this.selectionController.isItemSelected(activeItem)
}
private async recomputeSelectionAfterItemsReload(itemsReloadSource: ItemsReloadSource) {
@@ -371,7 +399,7 @@ export class ItemListController extends AbstractViewController implements Intern
const activeItem = activeController?.item
if (this.shouldSelectFirstItem(itemsReloadSource, activeItem)) {
if (this.shouldSelectFirstItem(itemsReloadSource)) {
await this.selectFirstItem()
} else if (this.shouldCloseActiveItem(activeItem) && activeController) {
this.closeItemController(activeController)
@@ -500,6 +528,11 @@ export class ItemListController extends AbstractViewController implements Intern
if (newDisplayOptions.sortBy !== currentSortBy) {
await this.selectFirstItem()
}
this.eventBus.publish({
type: CrossControllerEvent.RequestValuePersistence,
payload: undefined,
})
}
async createNewNoteController(title?: string) {
@@ -664,7 +697,7 @@ export class ItemListController extends AbstractViewController implements Intern
this.application.itemControllerGroup.closeItemController(controller)
}
handleTagChange = () => {
handleTagChange = (userTriggered: boolean) => {
const activeNoteController = this.getActiveItemController()
if (activeNoteController instanceof NoteViewController && activeNoteController.isTemplateNote) {
this.closeItemController(activeNoteController)
@@ -682,7 +715,7 @@ export class ItemListController extends AbstractViewController implements Intern
this.reloadNotesDisplayOptions()
void this.reloadItems(ItemsReloadSource.TagChange)
void this.reloadItems(userTriggered ? ItemsReloadSource.UserTriggeredTagChange : ItemsReloadSource.TagChange)
}
onFilterEnter = () => {

View File

@@ -15,19 +15,28 @@ import {
InternalEventBus,
InternalEventPublishStrategy,
} 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 { FeaturesController } from '../FeaturesController'
import { AbstractViewController } from '../Abstract/AbstractViewController'
import { destroyAllObjectProperties } from '@/Utils'
import { isValidFutureSiblings, rootTags, tagSiblings } from './Utils'
import { AnyTag } from './AnyTagType'
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[] = []
smartViews: SmartView[] = []
allNotesCount_ = 0
selectedUuid: AnyTag['uuid'] | undefined = undefined
selected_: AnyTag | undefined
previouslySelected_: AnyTag | undefined
editing_: SNTag | SmartView | undefined
@@ -63,12 +72,12 @@ export class NavigationController extends AbstractViewController {
allNotesCount: computed,
setAllNotesCount: action,
selected_: observable.ref,
selected_: observable,
previouslySelected_: observable.ref,
previouslySelected: computed,
editing_: observable.ref,
selected: computed,
selectedUuid: computed,
selectedUuid: observable,
editingTag: computed,
addingSubtagTo: observable,
@@ -94,6 +103,8 @@ export class NavigationController extends AbstractViewController {
setContextMenuMaxHeight: action,
isInFilesView: computed,
hydrateFromPersistedValue: action,
})
this.disposers.push(
@@ -103,25 +114,23 @@ export class NavigationController extends AbstractViewController {
this.smartViews = this.application.items.getSmartViews()
const currrentSelectedTag = this.selected_
if (!currrentSelectedTag) {
this.setSelectedTagInstance(this.smartViews[0])
const currentSelectedTag = this.selected_
if (!currentSelectedTag) {
return
}
const updatedReference =
FindItem(changed, currrentSelectedTag.uuid) || FindItem(this.smartViews, currrentSelectedTag.uuid)
FindItem(changed, currentSelectedTag.uuid) || FindItem(this.smartViews, currentSelectedTag.uuid)
if (updatedReference) {
this.setSelectedTagInstance(updatedReference as AnyTag)
}
if (isSystemView(currrentSelectedTag as SmartView)) {
if (isSystemView(currentSelectedTag as SmartView)) {
return
}
if (FindItem(removed, currrentSelectedTag.uuid)) {
if (FindItem(removed, currentSelectedTag.uuid)) {
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() {
@@ -358,7 +413,7 @@ export class NavigationController extends AbstractViewController {
return this.selected_
}
public async setSelectedTag(tag: AnyTag | undefined) {
public async setSelectedTag(tag: AnyTag | undefined, { userTriggered } = { userTriggered: false }) {
if (tag && tag.conflictOf) {
this.application.mutator
.changeAndSaveItem(tag, (mutator) => {
@@ -384,7 +439,7 @@ export class NavigationController extends AbstractViewController {
await this.eventBus.publishSync(
{
type: CrossControllerEvent.TagChanged,
payload: { tag, previousTag: this.previouslySelected_ },
payload: { tag, previousTag: this.previouslySelected_, userTriggered: userTriggered },
},
InternalEventPublishStrategy.SEQUENCE,
)
@@ -407,7 +462,10 @@ export class NavigationController extends AbstractViewController {
}
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) {
@@ -418,10 +476,6 @@ export class NavigationController extends AbstractViewController {
.catch(console.error)
}
public get selectedUuid(): UuidString | undefined {
return this.selected_?.uuid
}
public get editingTag(): SNTag | SmartView | undefined {
return this.editing_
}

View File

@@ -63,20 +63,6 @@ export class NotesController extends AbstractViewController {
this.itemListController = itemListController
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(() => {
const controllers = this.application.itemControllerGroup.itemControllers
@@ -278,7 +264,7 @@ export class NotesController extends AbstractViewController {
})
runInAction(() => {
this.selectionController.setSelectedItems({})
this.selectionController.deselectAll()
this.contextMenuOpen = false
})
}
@@ -295,7 +281,7 @@ export class NotesController extends AbstractViewController {
}
unselectNotes(): void {
this.selectionController.setSelectedItems({})
this.selectionController.deselectAll()
}
getSpellcheckStateForNote(note: SNNote) {

View File

@@ -7,17 +7,25 @@ import {
SNNote,
UuidString,
InternalEventBus,
isFile,
} 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 { AbstractViewController } from './Abstract/AbstractViewController'
import { Persistable } from './Abstract/Persistable'
import { CrossControllerEvent } from './CrossControllerEvent'
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
selectedItems: SelectedItems = {}
selectedUuids: Set<UuidString> = observable(new Set<UuidString>())
private itemListController!: ItemListController
override deinit(): void {
@@ -29,7 +37,7 @@ export class SelectedItemsController extends AbstractViewController {
super(application, eventBus)
makeObservable(this, {
selectedItems: observable,
selectedUuids: observable,
selectedItemsCount: computed,
selectedFiles: computed,
@@ -37,30 +45,50 @@ export class SelectedItemsController extends AbstractViewController {
firstSelectedItem: computed,
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) {
this.itemListController = itemListController
this.disposers.push(
this.application.streamItems<SNNote | FileItem>(
[ContentType.Note, ContentType.File],
({ changed, inserted, removed }) => {
runInAction(() => {
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
}
}
})
},
),
this.application.streamItems<SNNote | FileItem>([ContentType.Note, ContentType.File], ({ removed }) => {
runInAction(() => {
for (const removedNote of removed) {
this.removeFromSelectedUuids(removedNote.uuid)
}
})
}),
)
}
@@ -69,7 +97,7 @@ export class SelectedItemsController extends AbstractViewController {
}
get selectedItemsCount(): number {
return Object.keys(this.selectedItems).length
return this.selectedUuids.size
}
get selectedFiles(): FileItem[] {
@@ -81,21 +109,31 @@ export class SelectedItemsController extends AbstractViewController {
}
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[] => {
return Object.values(this.selectedItems).filter((item) => {
return !contentType ? true : item.content_type === contentType
}) as T[]
const uuids = Array.from(this.selectedUuids)
return uuids.length > 0
? (uuids
.map((uuid) => this.application.items.findSureItem(uuid))
.filter((item) => {
return !contentType ? true : item.content_type === contentType
}) as T[])
: []
}
setSelectedItems = (selectedItems: SelectedItems) => {
this.selectedItems = selectedItems
setSelectedUuids = (selectedUuids: Set<UuidString>) => {
this.selectedUuids = new Set(selectedUuids)
}
private removeFromSelectedUuids = (uuid: UuidString) => {
this.selectedUuids.delete(uuid)
this.setSelectedUuids(this.selectedUuids)
}
public deselectItem = (item: { uuid: ListableContentItem['uuid'] }): void => {
delete this.selectedItems[item.uuid]
this.removeFromSelectedUuids(item.uuid)
if (item.uuid === this.lastSelectedItem?.uuid) {
this.lastSelectedItem = undefined
@@ -103,11 +141,7 @@ export class SelectedItemsController extends AbstractViewController {
}
public isItemSelected = (item: ListableContentItem): boolean => {
return this.selectedItems[item.uuid] != undefined
}
public updateReferenceOfSelectedItem = (item: ListableContentItem): void => {
this.selectedItems[item.uuid] = item
return this.selectedUuids.has(item.uuid)
}
private selectItemsRange = async ({
@@ -138,7 +172,7 @@ export class SelectedItemsController extends AbstractViewController {
for (const item of authorizedItems) {
runInAction(() => {
this.selectedItems[item.uuid] = item
this.setSelectedUuids(this.selectedUuids.add(item.uuid))
this.lastSelectedItem = item
})
}
@@ -147,7 +181,7 @@ export class SelectedItemsController extends AbstractViewController {
cancelMultipleSelection = () => {
this.io.cancelAllKeyboardModifiers()
const firstSelectedItem = this.getSelectedItems()[0]
const firstSelectedItem = this.firstSelectedItem
if (firstSelectedItem) {
this.replaceSelection(firstSelectedItem)
@@ -157,9 +191,8 @@ export class SelectedItemsController extends AbstractViewController {
}
private replaceSelection = (item: ListableContentItem): void => {
this.setSelectedItems({
[item.uuid]: item,
})
this.deselectAll()
this.setSelectedUuids(this.selectedUuids.add(item.uuid))
this.lastSelectedItem = item
}
@@ -171,12 +204,25 @@ export class SelectedItemsController extends AbstractViewController {
})
}
private deselectAll = (): void => {
this.setSelectedItems({})
deselectAll = (): void => {
this.selectedUuids.clear()
this.setSelectedUuids(this.selectedUuids)
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 (
uuid: UuidString,
userTriggered?: boolean,
@@ -197,33 +243,25 @@ export class SelectedItemsController extends AbstractViewController {
const isAuthorizedForAccess = await this.application.protections.authorizeItemAccess(item)
if (userTriggered && (hasMeta || hasCtrl)) {
if (this.selectedItems[uuid] && hasMoreThanOneSelected) {
delete this.selectedItems[uuid]
if (this.selectedUuids.has(uuid) && hasMoreThanOneSelected) {
this.removeFromSelectedUuids(uuid)
} else if (isAuthorizedForAccess) {
this.selectedItems[uuid] = item
this.setSelectedUuids(this.selectedUuids.add(uuid))
this.lastSelectedItem = item
}
} else if (userTriggered && hasShift) {
await this.selectItemsRange({ selectedItem: item })
} else {
const shouldSelectNote = hasMoreThanOneSelected || !this.selectedItems[uuid]
const shouldSelectNote = hasMoreThanOneSelected || !this.selectedUuids.has(uuid)
if (shouldSelectNote && isAuthorizedForAccess) {
this.replaceSelection(item)
}
}
if (this.selectedItemsCount === 1) {
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)
}
}
await this.openSingleSelectedItem()
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 = () => {
const displayableItems = this.itemListController.items

View File

@@ -11,13 +11,15 @@ import {
ItemCounterInterface,
ItemCounter,
SubscriptionClientInterface,
InternalEventHandlerInterface,
InternalEventInterface,
} from '@standardnotes/snjs'
import { action, makeObservable, observable } from 'mobx'
import { ActionsMenuController } from './ActionsMenuController'
import { FeaturesController } from './FeaturesController'
import { FilesController } from './FilesController'
import { NotesController } from './NotesController'
import { ItemListController } from './ItemList/ItemListController'
import { ItemListController, ItemListControllerPersistableValue } from './ItemList/ItemListController'
import { NoAccountWarningController } from './NoAccountWarningController'
import { PreferencesController } from './PreferencesController'
import { PurchaseFlowController } from './PurchaseFlow/PurchaseFlowController'
@@ -25,14 +27,16 @@ import { QuickSettingsController } from './QuickSettingsController'
import { SearchOptionsController } from './SearchOptionsController'
import { SubscriptionController } from './Subscription/SubscriptionController'
import { SyncStatusController } from './SyncStatusController'
import { NavigationController } from './Navigation/NavigationController'
import { NavigationController, NavigationControllerPersistableValue } from './Navigation/NavigationController'
import { FilePreviewModalController } from './FilePreviewModalController'
import { SelectedItemsController } from './SelectedItemsController'
import { SelectedItemsController, SelectionControllerPersistableValue } from './SelectedItemsController'
import { HistoryModalController } from './NoteHistory/HistoryModalController'
import { AccountMenuPane } from '@/Components/AccountMenu/AccountMenuPane'
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
private unsubAppEventObserver!: () => void
@@ -65,10 +69,16 @@ export class ViewControllerManager {
private eventBus: InternalEventBus
private itemCounter: ItemCounterInterface
private subscriptionManager: SubscriptionClientInterface
private persistenceService: PersistenceService
constructor(public application: WebApplication, private device: WebOrDesktopDeviceInterface) {
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.subscriptionManager = application.subscriptions
@@ -173,6 +183,9 @@ export class ViewControllerManager {
;(this.quickSettingsMenuController as unknown) = undefined
;(this.syncStatusController as unknown) = undefined
this.persistenceService.deinit()
;(this.persistenceService as unknown) = undefined
this.actionsMenuController.reset()
;(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()
}
}
}