internal: incomplete vault systems behind feature flag (#2340)

This commit is contained in:
Mo
2023-06-30 09:01:56 -05:00
committed by GitHub
parent d16e401bb9
commit b032eb9c9b
638 changed files with 20321 additions and 4813 deletions

View File

@@ -1,6 +1,6 @@
import { WebApplication } from '@/Application/WebApplication'
import { ShouldPersistNoteStateKey } from '@/Components/Preferences/Panes/General/Persistence'
import { ApplicationEvent, ContentType, InternalEventBus } from '@standardnotes/snjs'
import { ApplicationEvent, ContentType, InternalEventBusInterface } from '@standardnotes/snjs'
import { PersistedStateValue, StorageKey } from '@standardnotes/ui-services'
import { CrossControllerEvent } from '../CrossControllerEvent'
@@ -8,7 +8,7 @@ export class PersistenceService {
private unsubAppEventObserver: () => void
private didHydrateOnce = false
constructor(private application: WebApplication, private eventBus: InternalEventBus) {
constructor(private application: WebApplication, private eventBus: InternalEventBusInterface) {
this.unsubAppEventObserver = this.application.addEventObserver(async (eventName) => {
if (!this.application) {
return

View File

@@ -1,6 +1,6 @@
import { destroyAllObjectProperties, isDev } from '@/Utils'
import { action, computed, makeObservable, observable, runInAction } from 'mobx'
import { ApplicationEvent, ContentType, InternalEventBus, SNNote, SNTag } from '@standardnotes/snjs'
import { ApplicationEvent, ContentType, InternalEventBusInterface, SNNote, SNTag } from '@standardnotes/snjs'
import { WebApplication } from '@/Application/WebApplication'
import { AccountMenuPane } from '@/Components/AccountMenu/AccountMenuPane'
import { AbstractViewController } from '../Abstract/AbstractViewController'
@@ -28,7 +28,7 @@ export class AccountMenuController extends AbstractViewController {
destroyAllObjectProperties(this)
}
constructor(application: WebApplication, eventBus: InternalEventBus) {
constructor(application: WebApplication, eventBus: InternalEventBusInterface) {
super(application, eventBus)
makeObservable(this, {

View File

@@ -6,7 +6,7 @@ import {
ApplicationEvent,
FeatureIdentifier,
FeatureStatus,
InternalEventBus,
InternalEventBusInterface,
InternalEventInterface,
} from '@standardnotes/snjs'
import { action, makeObservable, observable, runInAction, when } from 'mobx'
@@ -33,7 +33,7 @@ export class FeaturesController extends AbstractViewController {
destroyAllObjectProperties(this)
}
constructor(application: WebApplication, eventBus: InternalEventBus) {
constructor(application: WebApplication, eventBus: InternalEventBusInterface) {
super(application, eventBus)
this.hasFolders = this.isEntitledToFolders()

View File

@@ -21,7 +21,7 @@ import {
ClientDisplayableError,
ContentType,
FileItem,
InternalEventBus,
InternalEventBusInterface,
isFile,
Platform,
} from '@standardnotes/snjs'
@@ -68,7 +68,7 @@ export class FilesController extends AbstractViewController<FilesControllerEvent
application: WebApplication,
private notesController: NotesController,
private filePreviewModalController: FilePreviewModalController,
eventBus: InternalEventBus,
eventBus: InternalEventBusInterface,
) {
super(application, eventBus)
@@ -156,7 +156,8 @@ export class FilesController extends AbstractViewController<FilesControllerEvent
return
}
await this.application.items.associateFileWithNote(file, note)
await this.application.mutator.associateFileWithNote(file, note)
void this.application.sync.sync()
}
detachFileFromNote = async (file: FileItem) => {
@@ -168,16 +169,18 @@ export class FilesController extends AbstractViewController<FilesControllerEvent
})
return
}
await this.application.items.disassociateFileWithNote(file, note)
await this.application.mutator.disassociateFileWithNote(file, note)
void this.application.sync.sync()
}
toggleFileProtection = async (file: FileItem) => {
let result: FileItem | undefined
if (file.protected) {
result = await this.application.mutator.unprotectFile(file)
result = await this.application.protections.unprotectFile(file)
} else {
result = await this.application.mutator.protectFile(file)
result = await this.application.protections.protectFile(file)
}
void this.application.sync.sync()
const isProtected = result ? result.protected : file.protected
return isProtected
}
@@ -189,7 +192,8 @@ export class FilesController extends AbstractViewController<FilesControllerEvent
}
renameFile = async (file: FileItem, fileName: string) => {
await this.application.items.renameFile(file, fileName)
await this.application.mutator.renameFile(file, fileName)
void this.application.sync.sync()
}
handleFileAction = async (
@@ -373,7 +377,10 @@ export class FilesController extends AbstractViewController<FilesControllerEvent
return
}
const operation = await this.application.files.beginNewFileUpload(fileToUpload.size)
const operation = await this.application.files.beginNewFileUpload(
fileToUpload.size,
this.application.vaultDisplayService.exclusivelyShownVault,
)
if (operation instanceof ClientDisplayableError) {
addToast({
@@ -485,12 +492,12 @@ export class FilesController extends AbstractViewController<FilesControllerEvent
setProtectionForFiles = async (protect: boolean, files: FileItem[]) => {
if (protect) {
const protectedItems = await this.application.mutator.protectItems(files)
const protectedItems = await this.application.protections.protectItems(files)
if (protectedItems) {
this.setShowProtectedOverlay(true)
}
} else {
const unprotectedItems = await this.application.mutator.unprotectItems(files, ChallengeReason.UnprotectFile)
const unprotectedItems = await this.application.protections.unprotectItems(files, ChallengeReason.UnprotectFile)
if (unprotectedItems) {
this.setShowProtectedOverlay(false)
}

View File

@@ -149,7 +149,7 @@ export class ImportModalController {
}
}
const currentDate = new Date()
const importTagItem = this.application.mutator.createTemplateItem<TagContent, SNTag>(ContentType.Tag, {
const importTagItem = this.application.items.createTemplateItem<TagContent, SNTag>(ContentType.Tag, {
title: `Imported on ${currentDate.toLocaleString()}`,
expanded: false,
iconString: '',

View File

@@ -2,7 +2,6 @@ import { SNTag } from '@standardnotes/snjs'
import { ContentType } from '@standardnotes/common'
import { InternalEventBus } from '@standardnotes/services'
import { WebApplication } from '@/Application/WebApplication'
import { LinkingController } from '../LinkingController'
import { NavigationController } from '../Navigation/NavigationController'
import { NotesController } from '../NotesController/NotesController'
import { SearchOptionsController } from '../SearchOptionsController'
@@ -28,7 +27,6 @@ describe('item list controller', () => {
const searchOptionsController = {} as jest.Mocked<SearchOptionsController>
const notesController = {} as jest.Mocked<NotesController>
const linkingController = {} as jest.Mocked<LinkingController>
const eventBus = new InternalEventBus()
controller = new ItemListController(
@@ -37,7 +35,6 @@ describe('item list controller', () => {
searchOptionsController,
selectionController,
notesController,
linkingController,
eventBus,
)
})

View File

@@ -10,8 +10,6 @@ import {
SNNote,
SNTag,
SystemViewId,
DisplayOptions,
InternalEventBus,
InternalEventHandlerInterface,
InternalEventInterface,
FileItem,
@@ -22,6 +20,8 @@ import {
isFile,
isSmartView,
isSystemView,
NotesAndFilesDisplayControllerOptions,
InternalEventBusInterface,
} from '@standardnotes/snjs'
import { action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'
import { WebApplication } from '../../Application/WebApplication'
@@ -33,16 +33,18 @@ import { SelectedItemsController } from '../SelectedItemsController'
import { NotesController } from '../NotesController/NotesController'
import { formatDateAndTimeForNote } from '@/Utils/DateUtils'
import { PrefDefaults } from '@/Constants/PrefDefaults'
import dayjs from 'dayjs'
import dayjsAdvancedFormat from 'dayjs/plugin/advancedFormat'
dayjs.extend(dayjsAdvancedFormat)
import { LinkingController } from '../LinkingController'
import { AbstractViewController } from '../Abstract/AbstractViewController'
import { log, LoggingDomain } from '@/Logging'
import { NoteViewController } from '@/Components/NoteView/Controller/NoteViewController'
import { FileViewController } from '@/Components/NoteView/Controller/FileViewController'
import { TemplateNoteViewAutofocusBehavior } from '@/Components/NoteView/Controller/TemplateNoteViewControllerOptions'
import { ItemsReloadSource } from './ItemsReloadSource'
import { VaultDisplayServiceEvent } from '@standardnotes/ui-services'
const MinNoteCellHeight = 51.0
const DefaultListNumNotes = 20
@@ -59,7 +61,7 @@ export class ItemListController extends AbstractViewController implements Intern
renderedItems: ListableContentItem[] = []
searchSubmitted = false
showDisplayOptionsMenu = false
displayOptions: DisplayOptions = {
displayOptions: NotesAndFilesDisplayControllerOptions = {
sortBy: CollectionSort.CreatedAt,
sortDirection: 'dsc',
includePinned: true,
@@ -96,13 +98,13 @@ export class ItemListController extends AbstractViewController implements Intern
private searchOptionsController: SearchOptionsController,
private selectionController: SelectedItemsController,
private notesController: NotesController,
private linkingController: LinkingController,
eventBus: InternalEventBus,
eventBus: InternalEventBusInterface,
) {
super(application, eventBus)
eventBus.addEventHandler(this, CrossControllerEvent.TagChanged)
eventBus.addEventHandler(this, CrossControllerEvent.ActiveEditorChanged)
eventBus.addEventHandler(this, VaultDisplayServiceEvent.VaultDisplayOptionsChanged)
this.resetPagination()
@@ -222,6 +224,8 @@ export class ItemListController extends AbstractViewController implements Intern
await this.handleTagChange(payload.userTriggered)
} else if (event.type === CrossControllerEvent.ActiveEditorChanged) {
this.handleEditorChange().catch(console.error)
} else if (event.type === VaultDisplayServiceEvent.VaultDisplayOptionsChanged) {
void this.reloadItems(ItemsReloadSource.DisplayOptionsChange)
}
}
@@ -489,7 +493,7 @@ export class ItemListController extends AbstractViewController implements Intern
includeTrashed = this.displayOptions.includeTrashed ?? false
}
const criteria: DisplayOptions = {
const criteria: NotesAndFilesDisplayControllerOptions = {
sortBy: this.displayOptions.sortBy,
sortDirection: this.displayOptions.sortDirection,
tags: tag instanceof SNTag ? [tag] : [],
@@ -512,8 +516,9 @@ export class ItemListController extends AbstractViewController implements Intern
}: {
userTriggered: boolean
}): Promise<{ didReloadItems: boolean }> => {
const newDisplayOptions = {} as DisplayOptions
const newDisplayOptions = {} as NotesAndFilesDisplayControllerOptions
const newWebDisplayOptions = {} as WebDisplayOptions
const selectedTag = this.navigationController.selected
const isSystemTag = selectedTag && isSmartView(selectedTag) && isSystemView(selectedTag)
const selectedTagPreferences = isSystemTag
@@ -631,6 +636,7 @@ export class ItemListController extends AbstractViewController implements Intern
tag: activeRegularTagUuid,
createdAt,
autofocusBehavior,
vault: this.application.vaultDisplayService.exclusivelyShownVault,
},
})
}
@@ -783,7 +789,7 @@ export class ItemListController extends AbstractViewController implements Intern
const activeNote = this.application.itemControllerGroup.activeItemViewController?.item
if (activeNote && activeNote.conflictOf) {
this.application.mutator
this.application
.changeAndSaveItem(activeNote, (mutator) => {
mutator.conflictOf = undefined
})

View File

@@ -9,7 +9,11 @@ import {
FileToNoteReference,
InternalEventBus,
SNNote,
ItemsClientInterface,
ItemManagerInterface,
VaultListingInterface,
ItemInterface,
InternalFeatureService,
InternalFeature,
} from '@standardnotes/snjs'
import { FilesController } from './FilesController'
import { ItemListController } from './ItemList/ItemListController'
@@ -53,12 +57,20 @@ describe('LinkingController', () => {
let subscriptionController: SubscriptionController
beforeEach(() => {
application = {} as jest.Mocked<WebApplication>
application = {
vaults: {} as jest.Mocked<WebApplication['vaults']>,
alerts: {} as jest.Mocked<WebApplication['alerts']>,
sync: {} as jest.Mocked<WebApplication['sync']>,
mutator: {} as jest.Mocked<WebApplication['mutator']>,
} as unknown as jest.Mocked<WebApplication>
application.getPreference = jest.fn()
application.addSingleEventObserver = jest.fn()
application.streamItems = jest.fn()
application.itemControllerGroup = {} as jest.Mocked<WebApplication['itemControllerGroup']>
application.sync.sync = jest.fn()
Object.defineProperty(application, 'items', { value: {} as jest.Mocked<ItemsClientInterface> })
Object.defineProperty(application, 'items', { value: {} as jest.Mocked<ItemManagerInterface> })
navigationController = {} as jest.Mocked<NavigationController>
@@ -181,38 +193,73 @@ describe('LinkingController', () => {
})
it('should be true if active item & result are different content type & active item references result', () => {
const activeFile = createNote('test', {
uuid: 'active-file',
const activeNote = createNote('test', {
uuid: 'active-note',
references: [
{
reference_type: ContentReferenceType.FileToNote,
uuid: 'note-result',
uuid: 'file-result',
} as FileToNoteReference,
],
})
const noteResult = createFile('test', {
uuid: 'note-result',
const fileResult = createFile('test', {
uuid: 'file-result',
references: [],
})
const isNoteResultAlreadyLinked = isSearchResultAlreadyLinkedToItem(noteResult, activeFile)
const isNoteResultAlreadyLinked = isSearchResultAlreadyLinkedToItem(fileResult, activeNote)
expect(isNoteResultAlreadyLinked).toBeTruthy()
})
it('should be false if active item & result are different content type & neither references the other', () => {
const activeFile = createNote('test', {
const activeNote = createNote('test', {
uuid: 'active-file',
references: [],
})
const noteResult = createFile('test', {
const fileResult = createFile('test', {
uuid: 'note-result',
references: [],
})
const isNoteResultAlreadyLinked = isSearchResultAlreadyLinkedToItem(noteResult, activeFile)
const isNoteResultAlreadyLinked = isSearchResultAlreadyLinkedToItem(fileResult, activeNote)
expect(isNoteResultAlreadyLinked).toBeFalsy()
})
})
describe('linkItems', () => {
it('should move file to same vault as note if file does not belong to any vault', async () => {
InternalFeatureService.get().enableFeature(InternalFeature.Vaults)
application.mutator.associateFileWithNote = jest.fn().mockReturnValue({})
const moveToVaultSpy = (application.vaults.moveItemToVault = jest.fn())
const note = createNote('test', {
uuid: 'note',
references: [],
})
const file = createFile('test', {
uuid: 'file',
references: [],
})
const noteVault = {
uuid: 'note-vault',
} as jest.Mocked<VaultListingInterface>
application.vaults.getItemVault = jest.fn().mockImplementation((item: ItemInterface) => {
if (item.uuid === note.uuid) {
return noteVault
}
return undefined
})
await linkingController.linkItems(note, file)
expect(moveToVaultSpy).toHaveBeenCalled()
})
})
})

View File

@@ -1,4 +1,3 @@
import { WebApplication } from '@/Application/WebApplication'
import { FileItemActionType } from '@/Components/AttachedFilesPopover/PopoverFileItemAction'
import { NoteViewController } from '@/Components/NoteView/Controller/NoteViewController'
import { AppPaneId } from '@/Components/Panes/AppPaneMetadata'
@@ -9,13 +8,14 @@ import {
ApplicationEvent,
ContentType,
FileItem,
InternalEventBus,
naturalSort,
PrefKey,
SNNote,
SNTag,
isFile,
isNote,
InternalEventBusInterface,
isTag,
} from '@standardnotes/snjs'
import { action, computed, makeObservable, observable } from 'mobx'
import { AbstractViewController } from './Abstract/AbstractViewController'
@@ -25,6 +25,8 @@ import { ItemListController } from './ItemList/ItemListController'
import { NavigationController } from './Navigation/NavigationController'
import { SelectedItemsController } from './SelectedItemsController'
import { SubscriptionController } from './Subscription/SubscriptionController'
import { WebApplication } from '@/Application/WebApplication'
import { featureTrunkVaultsEnabled } from '@/FeatureTrunk'
export class LinkingController extends AbstractViewController {
shouldLinkToParentFolders: boolean
@@ -37,7 +39,7 @@ export class LinkingController extends AbstractViewController {
application: WebApplication,
private navigationController: NavigationController,
private selectionController: SelectedItemsController,
eventBus: InternalEventBus,
eventBus: InternalEventBusInterface,
) {
super(application, eventBus)
@@ -165,7 +167,11 @@ export class LinkingController extends AbstractViewController {
}
unlinkItems = async (item: LinkableItem, itemToUnlink: LinkableItem) => {
await this.application.items.unlinkItems(item, itemToUnlink)
try {
await this.application.mutator.unlinkItems(item, itemToUnlink)
} catch (error) {
console.error(error)
}
void this.application.sync.sync()
}
@@ -188,8 +194,36 @@ export class LinkingController extends AbstractViewController {
}
linkItems = async (item: LinkableItem, itemToLink: LinkableItem) => {
if (item instanceof SNNote) {
if (itemToLink instanceof SNNote && !this.isEntitledToNoteLinking) {
const linkNoteAndFile = async (note: SNNote, file: FileItem) => {
const updatedFile = await this.application.mutator.associateFileWithNote(file, note)
if (updatedFile && featureTrunkVaultsEnabled()) {
const noteVault = this.application.vaults.getItemVault(note)
const fileVault = this.application.vaults.getItemVault(updatedFile)
if (noteVault && !fileVault) {
await this.application.vaults.moveItemToVault(noteVault, file)
}
}
}
const linkFileAndFile = async (file1: FileItem, file2: FileItem) => {
await this.application.mutator.linkFileToFile(file1, file2)
}
const linkNoteToNote = async (note1: SNNote, note2: SNNote) => {
await this.application.mutator.linkNoteToNote(note1, note2)
}
const linkTagToNote = async (tag: SNTag, note: SNNote) => {
await this.addTagToItem(tag, note)
}
const linkTagToFile = async (tag: SNTag, file: FileItem) => {
await this.addTagToItem(tag, file)
}
if (isNote(item)) {
if (isNote(itemToLink) && !this.isEntitledToNoteLinking) {
void this.publishCrossControllerEventSync(CrossControllerEvent.DisplayPremiumModal, {
featureName: 'Note linking',
})
@@ -200,22 +234,22 @@ export class LinkingController extends AbstractViewController {
await this.ensureActiveItemIsInserted()
}
if (itemToLink instanceof FileItem) {
await this.application.items.associateFileWithNote(itemToLink, item)
} else if (itemToLink instanceof SNNote) {
await this.application.items.linkNoteToNote(item, itemToLink)
} else if (itemToLink instanceof SNTag) {
await this.addTagToItem(itemToLink, item)
if (isFile(itemToLink)) {
await linkNoteAndFile(item, itemToLink)
} else if (isNote(itemToLink)) {
await linkNoteToNote(item, itemToLink)
} else if (isTag(itemToLink)) {
await linkTagToNote(itemToLink, item)
} else {
throw Error('Invalid item type')
}
} else if (item instanceof FileItem) {
if (itemToLink instanceof SNNote) {
await this.application.items.associateFileWithNote(item, itemToLink)
} else if (itemToLink instanceof FileItem) {
await this.application.items.linkFileToFile(item, itemToLink)
} else if (itemToLink instanceof SNTag) {
await this.addTagToItem(itemToLink, item)
} else if (isFile(item)) {
if (isNote(itemToLink)) {
await linkNoteAndFile(itemToLink, item)
} else if (isFile(itemToLink)) {
await linkFileAndFile(item, itemToLink)
} else if (isTag(itemToLink)) {
await linkTagToFile(itemToLink, item)
} else {
throw Error('Invalid item to link')
}
@@ -248,8 +282,12 @@ export class LinkingController extends AbstractViewController {
createAndAddNewTag = async (title: string): Promise<SNTag> => {
await this.ensureActiveItemIsInserted()
const vault = this.application.vaultDisplayService.exclusivelyShownVault
const newTag = await this.application.mutator.findOrCreateTag(title, vault)
const activeItem = this.activeItem
const newTag = await this.application.mutator.findOrCreateTag(title)
if (activeItem) {
await this.addTagToItem(newTag, activeItem)
}
@@ -259,9 +297,9 @@ export class LinkingController extends AbstractViewController {
addTagToItem = async (tag: SNTag, item: FileItem | SNNote) => {
if (item instanceof SNNote) {
await this.application.items.addTagToNote(item, tag, this.shouldLinkToParentFolders)
await this.application.mutator.addTagToNote(item, tag, this.shouldLinkToParentFolders)
} else if (item instanceof FileItem) {
await this.application.items.addTagToFile(item, tag, this.shouldLinkToParentFolders)
await this.application.mutator.addTagToFile(item, tag, this.shouldLinkToParentFolders)
}
this.application.sync.sync().catch(console.error)

View File

@@ -1,4 +1,9 @@
import { confirmDialog, CREATE_NEW_TAG_COMMAND, NavigationControllerPersistableValue } from '@standardnotes/ui-services'
import {
confirmDialog,
CREATE_NEW_TAG_COMMAND,
NavigationControllerPersistableValue,
VaultDisplayServiceEvent,
} from '@standardnotes/ui-services'
import { STRING_DELETE_TAG } from '@/Constants/Strings'
import { MAX_MENU_SIZE_MULTIPLIER, MENU_MARGIN_FROM_APP_BORDER, SMART_TAGS_FEATURE_NAME } from '@/Constants/Constants'
import {
@@ -10,13 +15,15 @@ import {
isSystemView,
FindItem,
SystemViewId,
InternalEventBus,
InternalEventPublishStrategy,
VectorIconNameOrEmoji,
isTag,
PrefKey,
InternalEventBusInterface,
InternalEventHandlerInterface,
InternalEventInterface,
} from '@standardnotes/snjs'
import { action, computed, makeAutoObservable, makeObservable, observable, reaction, runInAction } from 'mobx'
import { action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'
import { WebApplication } from '../../Application/WebApplication'
import { FeaturesController } from '../FeaturesController'
import { destroyAllObjectProperties } from '@/Utils'
@@ -27,10 +34,11 @@ import { AbstractViewController } from '../Abstract/AbstractViewController'
import { Persistable } from '../Abstract/Persistable'
import { TagListSectionType } from '@/Components/Tags/TagListSection'
import { PaneLayout } from '../PaneController/PaneLayout'
import { TagsCountsState } from './TagsCountsState'
export class NavigationController
extends AbstractViewController
implements Persistable<NavigationControllerPersistableValue>
implements Persistable<NavigationControllerPersistableValue>, InternalEventHandlerInterface
{
tags: SNTag[] = []
smartViews: SmartView[] = []
@@ -54,9 +62,15 @@ export class NavigationController
private readonly tagsCountsState: TagsCountsState
constructor(application: WebApplication, private featuresController: FeaturesController, eventBus: InternalEventBus) {
constructor(
application: WebApplication,
private featuresController: FeaturesController,
eventBus: InternalEventBusInterface,
) {
super(application, eventBus)
eventBus.addEventHandler(this, VaultDisplayServiceEvent.VaultDisplayOptionsChanged)
this.tagsCountsState = new TagsCountsState(this.application)
this.smartViews = this.application.items.getSmartViews()
@@ -109,11 +123,9 @@ export class NavigationController
this.disposers.push(
this.application.streamItems([ContentType.Tag, ContentType.SmartView], ({ changed, removed }) => {
runInAction(() => {
this.tags = this.application.items.getDisplayableTags()
this.starredTags = this.tags.filter((tag) => tag.starred)
this.smartViews = this.application.items.getSmartViews()
this.reloadTags()
runInAction(() => {
const currentSelectedTag = this.selected_
if (!currentSelectedTag) {
@@ -173,6 +185,20 @@ export class NavigationController
)
}
private reloadTags(): void {
runInAction(() => {
this.tags = this.application.items.getDisplayableTags()
this.starredTags = this.tags.filter((tag) => tag.starred)
this.smartViews = this.application.items.getSmartViews()
})
}
async handleEvent(event: InternalEventInterface): Promise<void> {
if (event.type === VaultDisplayServiceEvent.VaultDisplayOptionsChanged) {
this.reloadTags()
}
}
private findAndSetTag = (uuid: UuidString) => {
const tagToSelect = [...this.tags, ...this.smartViews].find((tag) => tag.uuid === uuid)
if (tagToSelect) {
@@ -232,7 +258,10 @@ export class NavigationController
return
}
const createdTag = (await this.application.mutator.createTagOrSmartView(title)) as SNTag
const createdTag = await this.application.mutator.createTagOrSmartView<SNTag>(
title,
this.application.vaultDisplayService.exclusivelyShownVault,
)
const futureSiblings = this.application.items.getTagChildren(parent)
@@ -454,7 +483,7 @@ export class NavigationController
}
public async setPanelWidthForTag(tag: SNTag, width: number): Promise<void> {
await this.application.mutator.changeAndSaveItem<TagMutator>(tag, (mutator) => {
await this.application.changeAndSaveItem<TagMutator>(tag, (mutator) => {
mutator.preferences = {
...mutator.preferences,
panelWidth: width,
@@ -468,7 +497,7 @@ export class NavigationController
{ userTriggered } = { userTriggered: false },
) {
if (tag && tag.conflictOf) {
this.application.mutator
this.application
.changeAndSaveItem(tag, (mutator) => {
mutator.conflictOf = undefined
})
@@ -529,7 +558,7 @@ export class NavigationController
return
}
this.application.mutator
this.application
.changeAndSaveItem<TagMutator>(tag, (mutator) => {
mutator.expanded = expanded
})
@@ -537,7 +566,7 @@ export class NavigationController
}
public async setFavorite(tag: SNTag, favorite: boolean) {
return this.application.mutator
return this.application
.changeAndSaveItem<TagMutator>(tag, (mutator) => {
mutator.starred = favorite
})
@@ -545,7 +574,7 @@ export class NavigationController
}
public setIcon(tag: SNTag, icon: VectorIconNameOrEmoji) {
this.application.mutator
this.application
.changeAndSaveItem<TagMutator>(tag, (mutator) => {
mutator.iconString = icon as string
})
@@ -570,7 +599,7 @@ export class NavigationController
return
}
const newTag = this.application.mutator.createTemplateItem(ContentType.Tag) as SNTag
const newTag = this.application.items.createTemplateItem(ContentType.Tag) as SNTag
runInAction(() => {
this.selectedLocation = 'all'
@@ -593,7 +622,10 @@ export class NavigationController
})
}
if (shouldDelete) {
this.application.mutator.deleteItem(tag).catch(console.error)
this.application.mutator
.deleteItem(tag)
.then(() => this.application.sync.sync())
.catch(console.error)
await this.setSelectedTag(this.smartViews[0], 'views')
}
}
@@ -635,36 +667,18 @@ export class NavigationController
}
}
const insertedTag = await this.application.mutator.createTagOrSmartView(newTitle)
const insertedTag = await this.application.mutator.createTagOrSmartView<SNTag>(
newTitle,
this.application.vaultDisplayService.exclusivelyShownVault,
)
this.application.sync.sync().catch(console.error)
runInAction(() => {
void this.setSelectedTag(insertedTag as SNTag, this.selectedLocation || 'views')
void this.setSelectedTag(insertedTag, this.selectedLocation || 'views')
})
} else {
await this.application.mutator.changeAndSaveItem<TagMutator>(tag, (mutator) => {
await this.application.changeAndSaveItem<TagMutator>(tag, (mutator) => {
mutator.title = newTitle
})
}
}
}
class TagsCountsState {
public counts: { [uuid: string]: number } = {}
public constructor(private application: WebApplication) {
makeAutoObservable(this, {
counts: observable.ref,
update: action,
})
}
public update(tags: SNTag[]) {
const newCounts: { [uuid: string]: number } = Object.assign({}, this.counts)
tags.forEach((tag) => {
newCounts[tag.uuid] = this.application.items.countableNotesForTag(tag)
})
this.counts = newCounts
}
}

View File

@@ -0,0 +1,24 @@
import { SNTag } from '@standardnotes/snjs'
import { action, makeAutoObservable, observable } from 'mobx'
import { WebApplication } from '../../Application/WebApplication'
export class TagsCountsState {
public counts: { [uuid: string]: number } = {}
public constructor(private application: WebApplication) {
makeAutoObservable(this, {
counts: observable.ref,
update: action,
})
}
public update(tags: SNTag[]) {
const newCounts: { [uuid: string]: number } = Object.assign({}, this.counts)
tags.forEach((tag) => {
newCounts[tag.uuid] = this.application.items.countableNotesForTag(tag)
})
this.counts = newCounts
}
}

View File

@@ -1,5 +1,5 @@
import { storage, StorageKey } from '@standardnotes/ui-services'
import { ApplicationEvent, InternalEventBus } from '@standardnotes/snjs'
import { ApplicationEvent, InternalEventBusInterface } from '@standardnotes/snjs'
import { runInAction, makeObservable, observable, action } from 'mobx'
import { WebApplication } from '../Application/WebApplication'
import { AbstractViewController } from './Abstract/AbstractViewController'
@@ -7,7 +7,7 @@ import { AbstractViewController } from './Abstract/AbstractViewController'
export class NoAccountWarningController extends AbstractViewController {
show: boolean
constructor(application: WebApplication, eventBus: InternalEventBus) {
constructor(application: WebApplication, eventBus: InternalEventBusInterface) {
super(application, eventBus)
this.show = application.hasAccount() ? false : storage.get(StorageKey.ShowNoAccountWarning) ?? true

View File

@@ -1,5 +1,5 @@
import { WebApplication } from '@/Application/WebApplication'
import { InternalEventBus, SNNote } from '@standardnotes/snjs'
import { InternalEventBusInterface, SNNote } from '@standardnotes/snjs'
import { OPEN_NOTE_HISTORY_COMMAND } from '@standardnotes/ui-services'
import { action, makeObservable, observable } from 'mobx'
import { AbstractViewController } from '../Abstract/AbstractViewController'
@@ -13,7 +13,11 @@ export class HistoryModalController extends AbstractViewController {
this.note = undefined
}
constructor(application: WebApplication, eventBus: InternalEventBus, notesController: NotesControllerInterface) {
constructor(
application: WebApplication,
eventBus: InternalEventBusInterface,
notesController: NotesControllerInterface,
) {
super(application, eventBus)
makeObservable(this, {

View File

@@ -330,7 +330,7 @@ export class NoteHistoryController {
}
if (didConfirm) {
void this.application.mutator.changeAndSaveItem(
void this.application.changeAndSaveItem(
originalNote,
(mutator) => {
mutator.setCustomContent(revision.payload.content)
@@ -344,11 +344,13 @@ export class NoteHistoryController {
restoreRevisionAsCopy = async (revision: NonNullable<SelectedRevision>) => {
const originalNote = this.application.items.findSureItem<SNNote>(revision.payload.uuid)
const duplicatedItem = await this.application.mutator.duplicateItem(originalNote, {
const duplicatedItem = await this.application.mutator.duplicateItem(originalNote, false, {
...revision.payload.content,
title: revision.payload.content.title ? revision.payload.content.title + ' (copy)' : undefined,
})
void this.application.sync.sync()
this.selectionController.selectItem(duplicatedItem.uuid).catch(console.error)
}

View File

@@ -1,5 +1,5 @@
import { WebApplication } from '@/Application/WebApplication'
import { NoteMutator, SNNote } from '@standardnotes/models'
import { MutationType, NoteMutator, SNNote } from '@standardnotes/models'
import { InfoStrings } from '@standardnotes/snjs'
import { Deferred } from '@standardnotes/utils'
import { EditorSaveTimeoutDebounce } from '../Components/NoteView/Controller/EditorSaveTimeoutDebounce'
@@ -106,7 +106,7 @@ export class NoteSyncController {
noteMutator.preview_html = undefined
}
},
params.isUserModified,
params.isUserModified ? MutationType.UpdateUserTimestamps : MutationType.NoUpdateUserTimestamps,
)
void this.application.sync.sync().then(() => {

View File

@@ -7,10 +7,11 @@ import {
NoteMutator,
ContentType,
SNTag,
InternalEventBus,
PrefKey,
ApplicationEvent,
EditorLineWidth,
InternalEventBusInterface,
MutationType,
} from '@standardnotes/snjs'
import { makeObservable, observable, action, computed, runInAction } from 'mobx'
import { WebApplication } from '../../Application/WebApplication'
@@ -48,7 +49,7 @@ export class NotesController extends AbstractViewController implements NotesCont
application: WebApplication,
private selectionController: SelectedItemsController,
private navigationController: NavigationController,
eventBus: InternalEventBus,
eventBus: InternalEventBusInterface,
) {
super(application, eventBus)
@@ -201,7 +202,7 @@ export class NotesController extends AbstractViewController implements NotesCont
}
async changeSelectedNotes(mutate: (mutator: NoteMutator) => void): Promise<void> {
await this.application.mutator.changeItems(this.getSelectedNotesList(), mutate, false)
await this.application.mutator.changeItems(this.getSelectedNotesList(), mutate, MutationType.NoUpdateUserTimestamps)
this.application.sync.sync().catch(console.error)
}
@@ -263,9 +264,8 @@ export class NotesController extends AbstractViewController implements NotesCont
) {
this.selectionController.selectNextItem()
if (permanently) {
for (const note of this.getSelectedNotesList()) {
await this.application.mutator.deleteItem(note)
}
await this.application.mutator.deleteItems(this.getSelectedNotesList())
void this.application.sync.sync()
} else {
await this.changeSelectedNotes((mutator) => {
mutator.trashed = true
@@ -332,12 +332,14 @@ export class NotesController extends AbstractViewController implements NotesCont
async setProtectSelectedNotes(protect: boolean): Promise<void> {
const selectedNotes = this.getSelectedNotesList()
if (protect) {
await this.application.mutator.protectNotes(selectedNotes)
await this.application.protections.protectNotes(selectedNotes)
this.setShowProtectedWarning(true)
} else {
await this.application.mutator.unprotectNotes(selectedNotes)
await this.application.protections.unprotectNotes(selectedNotes)
this.setShowProtectedWarning(false)
}
void this.application.sync.sync()
}
unselectNotes(): void {
@@ -354,7 +356,7 @@ export class NotesController extends AbstractViewController implements NotesCont
(mutator) => {
mutator.toggleSpellcheck()
},
false,
MutationType.NoUpdateUserTimestamps,
)
this.application.sync.sync().catch(console.error)
}
@@ -371,7 +373,7 @@ export class NotesController extends AbstractViewController implements NotesCont
(mutator) => {
mutator.editorWidth = editorWidth
},
false,
MutationType.NoUpdateUserTimestamps,
)
this.application.sync.sync().catch(console.error)
}
@@ -380,7 +382,7 @@ export class NotesController extends AbstractViewController implements NotesCont
const selectedNotes = this.getSelectedNotesList()
await Promise.all(
selectedNotes.map(async (note) => {
await this.application.items.addTagToNote(note, tag, this.shouldLinkToParentFolders)
await this.application.mutator.addTagToNote(note, tag, this.shouldLinkToParentFolders)
}),
)
this.application.sync.sync().catch(console.error)
@@ -414,7 +416,7 @@ export class NotesController extends AbstractViewController implements NotesCont
confirmButtonStyle: 'danger',
})
) {
this.application.mutator.emptyTrash().catch(console.error)
await this.application.mutator.emptyTrash()
this.application.sync.sync().catch(console.error)
}
}

View File

@@ -3,7 +3,7 @@ import {
TOGGLE_LIST_PANE_KEYBOARD_COMMAND,
TOGGLE_NAVIGATION_PANE_KEYBOARD_COMMAND,
} from '@standardnotes/ui-services'
import { ApplicationEvent, InternalEventBus, PrefKey, removeFromArray } from '@standardnotes/snjs'
import { ApplicationEvent, InternalEventBusInterface, PrefKey, removeFromArray } from '@standardnotes/snjs'
import { AppPaneId } from '../../Components/Panes/AppPaneMetadata'
import { isMobileScreen } from '@/Utils'
import { makeObservable, observable, action, computed } from 'mobx'
@@ -35,7 +35,7 @@ export class PaneController extends AbstractViewController {
listPaneExplicitelyCollapsed = false
navigationPaneExplicitelyCollapsed = false
constructor(application: WebApplication, eventBus: InternalEventBus) {
constructor(application: WebApplication, eventBus: InternalEventBusInterface) {
super(application, eventBus)
makeObservable(this, {

View File

@@ -1,4 +1,4 @@
import { InternalEventBus } from '@standardnotes/snjs'
import { InternalEventBusInterface } from '@standardnotes/snjs'
import { action, computed, makeObservable, observable } from 'mobx'
import { PreferenceId, RootQueryParam } from '@standardnotes/ui-services'
import { AbstractViewController } from './Abstract/AbstractViewController'
@@ -10,7 +10,7 @@ export class PreferencesController extends AbstractViewController {
private _open = false
currentPane: PreferenceId = DEFAULT_PANE
constructor(application: WebApplication, eventBus: InternalEventBus) {
constructor(application: WebApplication, eventBus: InternalEventBusInterface) {
super(application, eventBus)
makeObservable<PreferencesController, '_open'>(this, {

View File

@@ -1,6 +1,6 @@
import { LoggingDomain, log } from '@/Logging'
import { loadPurchaseFlowUrl } from '@/Components/PurchaseFlow/PurchaseFlowFunctions'
import { InternalEventBus, AppleIAPProductId } from '@standardnotes/snjs'
import { AppleIAPProductId, InternalEventBusInterface } from '@standardnotes/snjs'
import { action, makeObservable, observable } from 'mobx'
import { WebApplication } from '../../Application/WebApplication'
import { AbstractViewController } from '../Abstract/AbstractViewController'
@@ -10,7 +10,7 @@ export class PurchaseFlowController extends AbstractViewController {
isOpen = false
currentPane = PurchaseFlowPane.CreateAccount
constructor(application: WebApplication, eventBus: InternalEventBus) {
constructor(application: WebApplication, eventBus: InternalEventBusInterface) {
super(application, eventBus)
makeObservable(this, {

View File

@@ -1,4 +1,4 @@
import { InternalEventBus } from '@standardnotes/snjs'
import { InternalEventBusInterface } from '@standardnotes/snjs'
import { WebApplication } from '@/Application/WebApplication'
import { action, makeObservable, observable } from 'mobx'
import { AbstractViewController } from './Abstract/AbstractViewController'
@@ -7,7 +7,7 @@ export class QuickSettingsController extends AbstractViewController {
open = false
shouldAnimateCloseMenu = false
constructor(application: WebApplication, eventBus: InternalEventBus) {
constructor(application: WebApplication, eventBus: InternalEventBusInterface) {
super(application, eventBus)
makeObservable(this, {

View File

@@ -1,4 +1,4 @@
import { ApplicationEvent, InternalEventBus } from '@standardnotes/snjs'
import { ApplicationEvent, InternalEventBusInterface } from '@standardnotes/snjs'
import { makeObservable, observable, action, runInAction } from 'mobx'
import { WebApplication } from '../Application/WebApplication'
import { AbstractViewController } from './Abstract/AbstractViewController'
@@ -8,7 +8,7 @@ export class SearchOptionsController extends AbstractViewController {
includeArchived = false
includeTrashed = false
constructor(application: WebApplication, eventBus: InternalEventBus) {
constructor(application: WebApplication, eventBus: InternalEventBusInterface) {
super(application, eventBus)
makeObservable(this, {

View File

@@ -8,10 +8,10 @@ import {
FileItem,
SNNote,
UuidString,
InternalEventBus,
isFile,
Uuids,
isNote,
InternalEventBusInterface,
} from '@standardnotes/snjs'
import { SelectionControllerPersistableValue } from '@standardnotes/ui-services'
import { action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'
@@ -36,7 +36,7 @@ export class SelectedItemsController
;(this.itemListController as unknown) = undefined
}
constructor(application: WebApplication, eventBus: InternalEventBus) {
constructor(application: WebApplication, eventBus: InternalEventBusInterface) {
super(application, eventBus)
makeObservable(this, {

View File

@@ -3,7 +3,7 @@ import {
ApplicationEvent,
ClientDisplayableError,
convertTimestampToMilliseconds,
InternalEventBus,
InternalEventBusInterface,
Invitation,
InvitationStatus,
SubscriptionClientInterface,
@@ -34,7 +34,7 @@ export class SubscriptionController extends AbstractViewController {
constructor(
application: WebApplication,
eventBus: InternalEventBus,
eventBus: InternalEventBusInterface,
private subscriptionManager: SubscriptionClientInterface,
) {
super(application, eventBus)
@@ -188,7 +188,7 @@ export class SubscriptionController extends AbstractViewController {
this.setAvailableSubscriptions(subscriptions)
}
} catch (error) {
console.error(error)
void error
}
}

View File

@@ -0,0 +1,47 @@
import { InternalEventBusInterface } from '@standardnotes/snjs'
import { WebApplication } from '@/Application/WebApplication'
import { action, makeObservable, observable } from 'mobx'
import { AbstractViewController } from './Abstract/AbstractViewController'
export class VaultSelectionMenuController extends AbstractViewController {
open = false
shouldAnimateCloseMenu = false
constructor(application: WebApplication, eventBus: InternalEventBusInterface) {
super(application, eventBus)
makeObservable(this, {
open: observable,
shouldAnimateCloseMenu: observable,
setOpen: action,
setShouldAnimateCloseMenu: action,
toggle: action,
closeVaultSelectionMenu: action,
})
}
setOpen = (open: boolean): void => {
this.open = open
}
setShouldAnimateCloseMenu = (shouldAnimate: boolean): void => {
this.shouldAnimateCloseMenu = shouldAnimate
}
toggle = (): void => {
if (this.open) {
this.closeVaultSelectionMenu()
} else {
this.setOpen(true)
}
}
closeVaultSelectionMenu = (): void => {
this.setShouldAnimateCloseMenu(true)
setTimeout(() => {
this.setOpen(false)
this.setShouldAnimateCloseMenu(false)
}, 150)
}
}

View File

@@ -13,7 +13,6 @@ import { destroyAllObjectProperties } from '@/Utils'
import {
DeinitSource,
WebOrDesktopDeviceInterface,
InternalEventBus,
SubscriptionClientInterface,
InternalEventHandlerInterface,
InternalEventInterface,
@@ -41,6 +40,7 @@ import { CrossControllerEvent } from './CrossControllerEvent'
import { EventObserverInterface } from '@/Event/EventObserverInterface'
import { ApplicationEventObserver } from '@/Event/ApplicationEventObserver'
import { ImportModalController } from './ImportModalController'
import { VaultSelectionMenuController } from './VaultSelectionMenuController'
export class ViewControllerManager implements InternalEventHandlerInterface {
readonly enableUnfinishedFeatures: boolean = window?.enabledUnfinishedFeatures
@@ -60,6 +60,7 @@ export class ViewControllerManager implements InternalEventHandlerInterface {
readonly preferencesController: PreferencesController
readonly purchaseFlowController: PurchaseFlowController
readonly quickSettingsMenuController: QuickSettingsController
readonly vaultSelectionController: VaultSelectionMenuController
readonly searchOptionsController: SearchOptionsController
readonly subscriptionController: SubscriptionController
readonly syncStatusController = new SyncStatusController()
@@ -73,50 +74,52 @@ export class ViewControllerManager implements InternalEventHandlerInterface {
public isSessionsModalVisible = false
private appEventObserverRemovers: (() => void)[] = []
private eventBus: InternalEventBus
private subscriptionManager: SubscriptionClientInterface
private persistenceService: PersistenceService
private applicationEventObserver: EventObserverInterface
private toastService: ToastServiceInterface
constructor(public application: WebApplication, private device: WebOrDesktopDeviceInterface) {
this.eventBus = new InternalEventBus()
const eventBus = application.internalEventBus
this.persistenceService = new PersistenceService(application, this.eventBus)
this.persistenceService = new PersistenceService(application, eventBus)
this.eventBus.addEventHandler(this, CrossControllerEvent.HydrateFromPersistedValues)
this.eventBus.addEventHandler(this, CrossControllerEvent.RequestValuePersistence)
eventBus.addEventHandler(this, CrossControllerEvent.HydrateFromPersistedValues)
eventBus.addEventHandler(this, CrossControllerEvent.RequestValuePersistence)
this.subscriptionManager = application.subscriptions
this.filePreviewModalController = new FilePreviewModalController(application)
this.quickSettingsMenuController = new QuickSettingsController(application, this.eventBus)
this.quickSettingsMenuController = new QuickSettingsController(application, eventBus)
this.paneController = new PaneController(application, this.eventBus)
this.vaultSelectionController = new VaultSelectionMenuController(application, eventBus)
this.preferencesController = new PreferencesController(application, this.eventBus)
this.paneController = new PaneController(application, eventBus)
this.selectionController = new SelectedItemsController(application, this.eventBus)
this.preferencesController = new PreferencesController(application, eventBus)
this.featuresController = new FeaturesController(application, this.eventBus)
this.selectionController = new SelectedItemsController(application, eventBus)
this.navigationController = new NavigationController(application, this.featuresController, this.eventBus)
this.featuresController = new FeaturesController(application, eventBus)
this.navigationController = new NavigationController(application, this.featuresController, eventBus)
this.notesController = new NotesController(
application,
this.selectionController,
this.navigationController,
this.eventBus,
eventBus,
)
this.searchOptionsController = new SearchOptionsController(application, this.eventBus)
this.searchOptionsController = new SearchOptionsController(application, eventBus)
this.linkingController = new LinkingController(
application,
this.navigationController,
this.selectionController,
this.eventBus,
eventBus,
)
this.itemListController = new ItemListController(
@@ -125,26 +128,25 @@ export class ViewControllerManager implements InternalEventHandlerInterface {
this.searchOptionsController,
this.selectionController,
this.notesController,
this.linkingController,
this.eventBus,
eventBus,
)
this.notesController.setServicesPostConstruction(this.itemListController)
this.selectionController.setServicesPostConstruction(this.itemListController)
this.noAccountWarningController = new NoAccountWarningController(application, this.eventBus)
this.noAccountWarningController = new NoAccountWarningController(application, eventBus)
this.accountMenuController = new AccountMenuController(application, this.eventBus)
this.accountMenuController = new AccountMenuController(application, eventBus)
this.subscriptionController = new SubscriptionController(application, this.eventBus, this.subscriptionManager)
this.subscriptionController = new SubscriptionController(application, eventBus, this.subscriptionManager)
this.purchaseFlowController = new PurchaseFlowController(application, this.eventBus)
this.purchaseFlowController = new PurchaseFlowController(application, eventBus)
this.filesController = new FilesController(
application,
this.notesController,
this.filePreviewModalController,
this.eventBus,
eventBus,
)
this.linkingController.setServicesPostConstruction(
@@ -153,7 +155,7 @@ export class ViewControllerManager implements InternalEventHandlerInterface {
this.subscriptionController,
)
this.historyModalController = new HistoryModalController(this.application, this.eventBus, this.notesController)
this.historyModalController = new HistoryModalController(this.application, eventBus, this.notesController)
this.importModalController = new ImportModalController(this.application, this.navigationController)
@@ -210,6 +212,7 @@ export class ViewControllerManager implements InternalEventHandlerInterface {
;(this.filePreviewModalController as unknown) = undefined
;(this.preferencesController as unknown) = undefined
;(this.quickSettingsMenuController as unknown) = undefined
;(this.vaultSelectionController as unknown) = undefined
;(this.syncStatusController as unknown) = undefined
this.persistenceService.deinit()