refactor(web): dependency management (#2386)
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import { CrossControllerEvent } from '../CrossControllerEvent'
|
||||
import { InternalEventBusInterface, InternalEventPublishStrategy, removeFromArray } from '@standardnotes/snjs'
|
||||
import { WebApplication } from '../../Application/WebApplication'
|
||||
import { Disposer } from '@/Types/Disposer'
|
||||
|
||||
type ControllerEventObserver<Event = void, EventData = void> = (event: Event, data: EventData) => void
|
||||
@@ -10,10 +9,7 @@ export abstract class AbstractViewController<Event = void, EventData = void> {
|
||||
protected disposers: Disposer[] = []
|
||||
private eventObservers: ControllerEventObserver<Event, EventData>[] = []
|
||||
|
||||
constructor(
|
||||
public application: WebApplication,
|
||||
protected eventBus: InternalEventBusInterface,
|
||||
) {}
|
||||
constructor(protected eventBus: InternalEventBusInterface) {}
|
||||
|
||||
protected async publishCrossControllerEventSync(name: CrossControllerEvent, data?: unknown): Promise<void> {
|
||||
await this.eventBus.publishSync({ type: name, payload: data }, InternalEventPublishStrategy.SEQUENCE)
|
||||
@@ -21,7 +17,6 @@ export abstract class AbstractViewController<Event = void, EventData = void> {
|
||||
|
||||
deinit(): void {
|
||||
this.dealloced = true
|
||||
;(this.application as unknown) = undefined
|
||||
;(this.eventBus as unknown) = undefined
|
||||
|
||||
for (const disposer of this.disposers) {
|
||||
|
||||
@@ -1,44 +1,72 @@
|
||||
import { WebApplication } from '@/Application/WebApplication'
|
||||
import { ShouldPersistNoteStateKey } from '@/Components/Preferences/Panes/General/Persistence'
|
||||
import { ApplicationEvent, ContentType, InternalEventBusInterface } from '@standardnotes/snjs'
|
||||
import { PersistedStateValue, StorageKey } from '@standardnotes/ui-services'
|
||||
import {
|
||||
ApplicationEvent,
|
||||
ContentType,
|
||||
InternalEventBusInterface,
|
||||
InternalEventHandlerInterface,
|
||||
InternalEventInterface,
|
||||
ItemManagerInterface,
|
||||
StorageServiceInterface,
|
||||
SyncServiceInterface,
|
||||
} from '@standardnotes/snjs'
|
||||
import { PersistedStateValue, PersistenceKey, StorageKey } from '@standardnotes/ui-services'
|
||||
import { CrossControllerEvent } from '../CrossControllerEvent'
|
||||
import { NavigationController } from '../Navigation/NavigationController'
|
||||
import { ItemListController } from '../ItemList/ItemListController'
|
||||
|
||||
export class PersistenceService {
|
||||
private unsubAppEventObserver: () => void
|
||||
export class PersistenceService implements InternalEventHandlerInterface {
|
||||
private didHydrateOnce = false
|
||||
|
||||
constructor(
|
||||
private application: WebApplication,
|
||||
private itemListController: ItemListController,
|
||||
private navigationController: NavigationController,
|
||||
private storage: StorageServiceInterface,
|
||||
private items: ItemManagerInterface,
|
||||
private sync: SyncServiceInterface,
|
||||
private eventBus: InternalEventBusInterface,
|
||||
) {
|
||||
this.unsubAppEventObserver = this.application.addEventObserver(async (eventName) => {
|
||||
if (!this.application) {
|
||||
return
|
||||
}
|
||||
|
||||
void this.onAppEvent(eventName)
|
||||
})
|
||||
eventBus.addEventHandler(this, ApplicationEvent.LocalDataLoaded)
|
||||
eventBus.addEventHandler(this, ApplicationEvent.LocalDataIncrementalLoad)
|
||||
eventBus.addEventHandler(this, CrossControllerEvent.HydrateFromPersistedValues)
|
||||
eventBus.addEventHandler(this, CrossControllerEvent.RequestValuePersistence)
|
||||
}
|
||||
|
||||
async onAppEvent(eventName: ApplicationEvent) {
|
||||
if (eventName === ApplicationEvent.LocalDataLoaded && !this.didHydrateOnce) {
|
||||
this.hydratePersistedValues()
|
||||
this.didHydrateOnce = true
|
||||
} else if (eventName === ApplicationEvent.LocalDataIncrementalLoad) {
|
||||
const canHydrate = this.application.items.getItems([ContentType.TYPES.Note, ContentType.TYPES.Tag]).length > 0
|
||||
|
||||
if (!canHydrate) {
|
||||
return
|
||||
async handleEvent(event: InternalEventInterface): Promise<void> {
|
||||
switch (event.type) {
|
||||
case ApplicationEvent.LocalDataLoaded: {
|
||||
if (!this.didHydrateOnce) {
|
||||
this.hydratePersistedValues()
|
||||
this.didHydrateOnce = true
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
this.hydratePersistedValues()
|
||||
this.didHydrateOnce = true
|
||||
case ApplicationEvent.LocalDataIncrementalLoad: {
|
||||
const canHydrate = this.items.getItems([ContentType.TYPES.Note, ContentType.TYPES.Tag]).length > 0
|
||||
|
||||
if (!canHydrate) {
|
||||
return
|
||||
}
|
||||
|
||||
this.hydratePersistedValues()
|
||||
this.didHydrateOnce = true
|
||||
break
|
||||
}
|
||||
|
||||
case CrossControllerEvent.HydrateFromPersistedValues: {
|
||||
this.hydrateFromPersistedValues(event.payload as PersistedStateValue | undefined)
|
||||
break
|
||||
}
|
||||
|
||||
case CrossControllerEvent.RequestValuePersistence: {
|
||||
this.persistCurrentState()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get persistenceEnabled() {
|
||||
return this.application.getValue(ShouldPersistNoteStateKey) ?? true
|
||||
return this.storage.getValue(ShouldPersistNoteStateKey) ?? true
|
||||
}
|
||||
|
||||
hydratePersistedValues = () => {
|
||||
@@ -48,8 +76,37 @@ export class PersistenceService {
|
||||
})
|
||||
}
|
||||
|
||||
persistCurrentState(): void {
|
||||
const values: PersistedStateValue = {
|
||||
[PersistenceKey.ItemListController]: this.itemListController.getPersistableValue(),
|
||||
[PersistenceKey.NavigationController]: this.navigationController.getPersistableValue(),
|
||||
}
|
||||
|
||||
this.persistValues(values)
|
||||
|
||||
const selectedItemsState = values['selected-items-controller']
|
||||
const navigationSelectionState = values['navigation-controller']
|
||||
const launchPriorityUuids: string[] = []
|
||||
if (selectedItemsState.selectedUuids.length) {
|
||||
launchPriorityUuids.push(...selectedItemsState.selectedUuids)
|
||||
}
|
||||
if (navigationSelectionState.selectedTagUuid) {
|
||||
launchPriorityUuids.push(navigationSelectionState.selectedTagUuid)
|
||||
}
|
||||
|
||||
this.sync.setLaunchPriorityUuids(launchPriorityUuids)
|
||||
}
|
||||
|
||||
hydrateFromPersistedValues(values: PersistedStateValue | undefined): void {
|
||||
const navigationState = values?.[PersistenceKey.NavigationController]
|
||||
this.navigationController.hydrateFromPersistedValue(navigationState)
|
||||
|
||||
const selectedItemsState = values?.[PersistenceKey.ItemListController]
|
||||
this.itemListController.hydrateFromPersistedValue(selectedItemsState)
|
||||
}
|
||||
|
||||
persistValues(values: PersistedStateValue): void {
|
||||
if (!this.application.isDatabaseLoaded()) {
|
||||
if (!this.sync.isDatabaseLoaded()) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -57,22 +114,18 @@ export class PersistenceService {
|
||||
return
|
||||
}
|
||||
|
||||
this.application.setValue(StorageKey.MasterStatePersistenceKey, values)
|
||||
this.storage.setValue(StorageKey.MasterStatePersistenceKey, values)
|
||||
}
|
||||
|
||||
clearPersistedValues(): void {
|
||||
if (!this.application.isDatabaseLoaded()) {
|
||||
if (!this.sync.isDatabaseLoaded()) {
|
||||
return
|
||||
}
|
||||
|
||||
void this.application.removeValue(StorageKey.MasterStatePersistenceKey)
|
||||
void this.storage.removeValue(StorageKey.MasterStatePersistenceKey)
|
||||
}
|
||||
|
||||
getPersistedValues(): PersistedStateValue {
|
||||
return this.application.getValue(StorageKey.MasterStatePersistenceKey) as PersistedStateValue
|
||||
}
|
||||
|
||||
deinit() {
|
||||
this.unsubAppEventObserver()
|
||||
return this.storage.getValue(StorageKey.MasterStatePersistenceKey) as PersistedStateValue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
import { destroyAllObjectProperties, isDev } from '@/Utils'
|
||||
import { destroyAllObjectProperties } from '@/Utils'
|
||||
import { action, computed, makeObservable, observable, runInAction } from 'mobx'
|
||||
import { ApplicationEvent, ContentType, InternalEventBusInterface, SNNote, SNTag } from '@standardnotes/snjs'
|
||||
import { WebApplication } from '@/Application/WebApplication'
|
||||
import {
|
||||
ApplicationEvent,
|
||||
ContentType,
|
||||
GetHost,
|
||||
InternalEventBusInterface,
|
||||
InternalEventHandlerInterface,
|
||||
InternalEventInterface,
|
||||
ItemManagerInterface,
|
||||
SNNote,
|
||||
SNTag,
|
||||
} from '@standardnotes/snjs'
|
||||
import { AccountMenuPane } from '@/Components/AccountMenu/AccountMenuPane'
|
||||
import { AbstractViewController } from '../Abstract/AbstractViewController'
|
||||
|
||||
export class AccountMenuController extends AbstractViewController {
|
||||
export class AccountMenuController extends AbstractViewController implements InternalEventHandlerInterface {
|
||||
show = false
|
||||
signingOut = false
|
||||
otherSessionsSignOut = false
|
||||
@@ -28,8 +37,12 @@ export class AccountMenuController extends AbstractViewController {
|
||||
destroyAllObjectProperties(this)
|
||||
}
|
||||
|
||||
constructor(application: WebApplication, eventBus: InternalEventBusInterface) {
|
||||
super(application, eventBus)
|
||||
constructor(
|
||||
private items: ItemManagerInterface,
|
||||
private _getHost: GetHost,
|
||||
eventBus: InternalEventBusInterface,
|
||||
) {
|
||||
super(eventBus)
|
||||
|
||||
makeObservable(this, {
|
||||
show: observable,
|
||||
@@ -63,27 +76,26 @@ export class AccountMenuController extends AbstractViewController {
|
||||
notesAndTagsCount: computed,
|
||||
})
|
||||
|
||||
this.disposers.push(
|
||||
this.application.addEventObserver(async () => {
|
||||
runInAction(() => {
|
||||
if (isDev && window.devAccountServer) {
|
||||
this.setServer(window.devAccountServer)
|
||||
this.application.setCustomHost(window.devAccountServer).catch(console.error)
|
||||
} else {
|
||||
this.setServer(this.application.getHost())
|
||||
}
|
||||
})
|
||||
}, ApplicationEvent.Launched),
|
||||
)
|
||||
eventBus.addEventHandler(this, ApplicationEvent.Launched)
|
||||
|
||||
this.disposers.push(
|
||||
this.application.streamItems([ContentType.TYPES.Note, ContentType.TYPES.Tag], () => {
|
||||
this.items.streamItems([ContentType.TYPES.Note, ContentType.TYPES.Tag], () => {
|
||||
runInAction(() => {
|
||||
this.notesAndTags = this.application.items.getItems([ContentType.TYPES.Note, ContentType.TYPES.Tag])
|
||||
this.notesAndTags = this.items.getItems([ContentType.TYPES.Note, ContentType.TYPES.Tag])
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
async handleEvent(event: InternalEventInterface): Promise<void> {
|
||||
switch (event.type) {
|
||||
case ApplicationEvent.Launched: {
|
||||
runInAction(() => {
|
||||
this.setServer(this._getHost.execute().getValue())
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setShow = (show: boolean): void => {
|
||||
this.show = show
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
export enum CrossControllerEvent {
|
||||
TagChanged = 'TagChanged',
|
||||
ActiveEditorChanged = 'ActiveEditorChanged',
|
||||
HydrateFromPersistedValues = 'HydrateFromPersistedValues',
|
||||
RequestValuePersistence = 'RequestValuePersistence',
|
||||
DisplayPremiumModal = 'DisplayPremiumModal',
|
||||
TagChanged = 'CrossControllerEvent:TagChanged',
|
||||
ActiveEditorChanged = 'CrossControllerEvent:ActiveEditorChanged',
|
||||
HydrateFromPersistedValues = 'CrossControllerEvent:HydrateFromPersistedValues',
|
||||
RequestValuePersistence = 'CrossControllerEvent:RequestValuePersistence',
|
||||
DisplayPremiumModal = 'CrossControllerEvent:DisplayPremiumModal',
|
||||
UnselectAllNotes = 'CrossControllerEvent:UnselectAllNotes',
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FeaturesClientInterface, InternalEventHandlerInterface } from '@standardnotes/services'
|
||||
import { FeatureName } from './FeatureName'
|
||||
import { WebApplication } from '@/Application/WebApplication'
|
||||
import { PremiumFeatureModalType } from '@/Components/PremiumFeaturesModal/PremiumFeatureModalType'
|
||||
import { destroyAllObjectProperties } from '@/Utils'
|
||||
import {
|
||||
@@ -13,7 +13,7 @@ import { action, makeObservable, observable, runInAction, when } from 'mobx'
|
||||
import { AbstractViewController } from './Abstract/AbstractViewController'
|
||||
import { CrossControllerEvent } from './CrossControllerEvent'
|
||||
|
||||
export class FeaturesController extends AbstractViewController {
|
||||
export class FeaturesController extends AbstractViewController implements InternalEventHandlerInterface {
|
||||
hasFolders: boolean
|
||||
hasSmartViews: boolean
|
||||
entitledToFiles: boolean
|
||||
@@ -33,16 +33,17 @@ export class FeaturesController extends AbstractViewController {
|
||||
destroyAllObjectProperties(this)
|
||||
}
|
||||
|
||||
constructor(application: WebApplication, eventBus: InternalEventBusInterface) {
|
||||
super(application, eventBus)
|
||||
constructor(
|
||||
private features: FeaturesClientInterface,
|
||||
eventBus: InternalEventBusInterface,
|
||||
) {
|
||||
super(eventBus)
|
||||
|
||||
this.hasFolders = this.isEntitledToFolders()
|
||||
this.hasSmartViews = this.isEntitledToSmartViews()
|
||||
this.entitledToFiles = this.isEntitledToFiles()
|
||||
this.premiumAlertFeatureName = undefined
|
||||
|
||||
eventBus.addEventHandler(this, CrossControllerEvent.DisplayPremiumModal)
|
||||
|
||||
makeObservable(this, {
|
||||
hasFolders: observable,
|
||||
hasSmartViews: observable,
|
||||
@@ -54,33 +55,38 @@ export class FeaturesController extends AbstractViewController {
|
||||
showPurchaseSuccessAlert: action,
|
||||
})
|
||||
|
||||
eventBus.addEventHandler(this, CrossControllerEvent.DisplayPremiumModal)
|
||||
eventBus.addEventHandler(this, ApplicationEvent.DidPurchaseSubscription)
|
||||
eventBus.addEventHandler(this, ApplicationEvent.FeaturesAvailabilityChanged)
|
||||
eventBus.addEventHandler(this, ApplicationEvent.Launched)
|
||||
eventBus.addEventHandler(this, ApplicationEvent.LocalDataLoaded)
|
||||
eventBus.addEventHandler(this, ApplicationEvent.UserRolesChanged)
|
||||
|
||||
this.showPremiumAlert = this.showPremiumAlert.bind(this)
|
||||
this.closePremiumAlert = this.closePremiumAlert.bind(this)
|
||||
|
||||
this.disposers.push(
|
||||
application.addEventObserver(async (event) => {
|
||||
switch (event) {
|
||||
case ApplicationEvent.DidPurchaseSubscription:
|
||||
this.showPurchaseSuccessAlert()
|
||||
break
|
||||
case ApplicationEvent.FeaturesAvailabilityChanged:
|
||||
case ApplicationEvent.Launched:
|
||||
case ApplicationEvent.LocalDataLoaded:
|
||||
case ApplicationEvent.UserRolesChanged:
|
||||
runInAction(() => {
|
||||
this.hasFolders = this.isEntitledToFolders()
|
||||
this.hasSmartViews = this.isEntitledToSmartViews()
|
||||
this.entitledToFiles = this.isEntitledToFiles()
|
||||
})
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
async handleEvent(event: InternalEventInterface): Promise<void> {
|
||||
if (event.type === CrossControllerEvent.DisplayPremiumModal) {
|
||||
const payload = event.payload as { featureName: string }
|
||||
void this.showPremiumAlert(payload.featureName)
|
||||
switch (event.type) {
|
||||
case ApplicationEvent.DidPurchaseSubscription:
|
||||
this.showPurchaseSuccessAlert()
|
||||
break
|
||||
case ApplicationEvent.FeaturesAvailabilityChanged:
|
||||
case ApplicationEvent.Launched:
|
||||
case ApplicationEvent.LocalDataLoaded:
|
||||
case ApplicationEvent.UserRolesChanged:
|
||||
runInAction(() => {
|
||||
this.hasFolders = this.isEntitledToFolders()
|
||||
this.hasSmartViews = this.isEntitledToSmartViews()
|
||||
this.entitledToFiles = this.isEntitledToFiles()
|
||||
})
|
||||
break
|
||||
case CrossControllerEvent.DisplayPremiumModal:
|
||||
{
|
||||
const payload = event.payload as { featureName: string }
|
||||
void this.showPremiumAlert(payload.featureName)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +106,7 @@ export class FeaturesController extends AbstractViewController {
|
||||
}
|
||||
|
||||
private isEntitledToFiles(): boolean {
|
||||
const status = this.application.features.getFeatureStatus(
|
||||
const status = this.features.getFeatureStatus(
|
||||
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.Files).getValue(),
|
||||
)
|
||||
|
||||
@@ -108,7 +114,7 @@ export class FeaturesController extends AbstractViewController {
|
||||
}
|
||||
|
||||
private isEntitledToFolders(): boolean {
|
||||
const status = this.application.features.getFeatureStatus(
|
||||
const status = this.features.getFeatureStatus(
|
||||
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.TagNesting).getValue(),
|
||||
)
|
||||
|
||||
@@ -116,7 +122,7 @@ export class FeaturesController extends AbstractViewController {
|
||||
}
|
||||
|
||||
private isEntitledToSmartViews(): boolean {
|
||||
const status = this.application.features.getFeatureStatus(
|
||||
const status = this.features.getFeatureStatus(
|
||||
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.SmartFilters).getValue(),
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { WebApplication } from '@/Application/WebApplication'
|
||||
import { ContentType, FileItem } from '@standardnotes/snjs'
|
||||
import { ContentType, FileItem, ItemManagerInterface } from '@standardnotes/snjs'
|
||||
import { action, makeObservable, observable } from 'mobx'
|
||||
|
||||
export class FilePreviewModalController {
|
||||
@@ -9,7 +8,7 @@ export class FilePreviewModalController {
|
||||
|
||||
eventObservers: (() => void)[] = []
|
||||
|
||||
constructor(application: WebApplication) {
|
||||
constructor(items: ItemManagerInterface) {
|
||||
makeObservable(this, {
|
||||
isOpen: observable,
|
||||
currentFile: observable,
|
||||
@@ -21,7 +20,7 @@ export class FilePreviewModalController {
|
||||
})
|
||||
|
||||
this.eventObservers.push(
|
||||
application.streamItems(ContentType.TYPES.File, ({ changed, removed }) => {
|
||||
items.streamItems(ContentType.TYPES.File, ({ changed, removed }) => {
|
||||
if (!this.currentFile) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import {
|
||||
FileDownloadProgress,
|
||||
fileProgressToHumanReadableString,
|
||||
FilesClientInterface,
|
||||
OnChunkCallbackNoProgress,
|
||||
} from '@standardnotes/files'
|
||||
import { FilePreviewModalController } from './FilePreviewModalController'
|
||||
import { FileItemAction, FileItemActionType } from '@/Components/AttachedFilesPopover/PopoverFileItemAction'
|
||||
import { BYTES_IN_ONE_MEGABYTE } from '@/Constants/Constants'
|
||||
import { confirmDialog } from '@standardnotes/ui-services'
|
||||
import {
|
||||
ArchiveManager,
|
||||
confirmDialog,
|
||||
IsNativeMobileWeb,
|
||||
VaultDisplayServiceInterface,
|
||||
} from '@standardnotes/ui-services'
|
||||
import { Strings, StringUtils } from '@/Constants/Strings'
|
||||
import { concatenateUint8Arrays } from '@/Utils/ConcatenateUint8Arrays'
|
||||
import {
|
||||
@@ -17,17 +23,22 @@ import {
|
||||
parseFileName,
|
||||
} from '@standardnotes/filepicker'
|
||||
import {
|
||||
AlertService,
|
||||
ChallengeReason,
|
||||
ClientDisplayableError,
|
||||
ContentType,
|
||||
FileItem,
|
||||
InternalEventBusInterface,
|
||||
isFile,
|
||||
ItemManagerInterface,
|
||||
MobileDeviceInterface,
|
||||
MutatorClientInterface,
|
||||
Platform,
|
||||
ProtectionsClientInterface,
|
||||
SyncServiceInterface,
|
||||
} from '@standardnotes/snjs'
|
||||
import { addToast, dismissToast, ToastType, updateToast } from '@standardnotes/toast'
|
||||
import { action, makeObservable, observable, reaction } from 'mobx'
|
||||
import { WebApplication } from '../Application/WebApplication'
|
||||
import { AbstractViewController } from './Abstract/AbstractViewController'
|
||||
import { NotesController } from './NotesController/NotesController'
|
||||
import { downloadOrShareBlobBasedOnPlatform } from '@/Utils/DownloadOrShareBasedOnPlatform'
|
||||
@@ -65,12 +76,22 @@ export class FilesController extends AbstractViewController<FilesControllerEvent
|
||||
}
|
||||
|
||||
constructor(
|
||||
application: WebApplication,
|
||||
private notesController: NotesController,
|
||||
private filePreviewModalController: FilePreviewModalController,
|
||||
private archiveService: ArchiveManager,
|
||||
private vaultDisplayService: VaultDisplayServiceInterface,
|
||||
private items: ItemManagerInterface,
|
||||
private files: FilesClientInterface,
|
||||
private mutator: MutatorClientInterface,
|
||||
private sync: SyncServiceInterface,
|
||||
private protections: ProtectionsClientInterface,
|
||||
private alerts: AlertService,
|
||||
private platform: Platform,
|
||||
private mobileDevice: MobileDeviceInterface | undefined,
|
||||
private _isNativeMobileWeb: IsNativeMobileWeb,
|
||||
eventBus: InternalEventBusInterface,
|
||||
) {
|
||||
super(application, eventBus)
|
||||
super(eventBus)
|
||||
|
||||
makeObservable(this, {
|
||||
allFiles: observable,
|
||||
@@ -88,7 +109,7 @@ export class FilesController extends AbstractViewController<FilesControllerEvent
|
||||
})
|
||||
|
||||
this.disposers.push(
|
||||
application.streamItems(ContentType.TYPES.File, () => {
|
||||
items.streamItems(ContentType.TYPES.File, () => {
|
||||
this.reloadAllFiles()
|
||||
this.reloadAttachedFiles()
|
||||
}),
|
||||
@@ -117,13 +138,13 @@ export class FilesController extends AbstractViewController<FilesControllerEvent
|
||||
}
|
||||
|
||||
reloadAllFiles = () => {
|
||||
this.allFiles = this.application.items.getDisplayableFiles()
|
||||
this.allFiles = this.items.getDisplayableFiles()
|
||||
}
|
||||
|
||||
reloadAttachedFiles = () => {
|
||||
const note = this.notesController.firstSelectedNote
|
||||
if (note) {
|
||||
this.attachedFiles = this.application.items.itemsReferencingItem(note).filter(isFile)
|
||||
this.attachedFiles = this.items.itemsReferencingItem(note).filter(isFile)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,7 +158,7 @@ export class FilesController extends AbstractViewController<FilesControllerEvent
|
||||
type: ToastType.Loading,
|
||||
message: `Deleting file "${file.name}"...`,
|
||||
})
|
||||
await this.application.files.deleteFile(file)
|
||||
await this.files.deleteFile(file)
|
||||
addToast({
|
||||
type: ToastType.Success,
|
||||
message: `Deleted file "${file.name}"`,
|
||||
@@ -156,8 +177,8 @@ export class FilesController extends AbstractViewController<FilesControllerEvent
|
||||
return
|
||||
}
|
||||
|
||||
await this.application.mutator.associateFileWithNote(file, note)
|
||||
void this.application.sync.sync()
|
||||
await this.mutator.associateFileWithNote(file, note)
|
||||
void this.sync.sync()
|
||||
}
|
||||
|
||||
detachFileFromNote = async (file: FileItem) => {
|
||||
@@ -169,31 +190,31 @@ export class FilesController extends AbstractViewController<FilesControllerEvent
|
||||
})
|
||||
return
|
||||
}
|
||||
await this.application.mutator.disassociateFileWithNote(file, note)
|
||||
void this.application.sync.sync()
|
||||
await this.mutator.disassociateFileWithNote(file, note)
|
||||
void this.sync.sync()
|
||||
}
|
||||
|
||||
toggleFileProtection = async (file: FileItem) => {
|
||||
let result: FileItem | undefined
|
||||
if (file.protected) {
|
||||
result = await this.application.protections.unprotectFile(file)
|
||||
result = await this.protections.unprotectFile(file)
|
||||
} else {
|
||||
result = await this.application.protections.protectFile(file)
|
||||
result = await this.protections.protectFile(file)
|
||||
}
|
||||
void this.application.sync.sync()
|
||||
void this.sync.sync()
|
||||
const isProtected = result ? result.protected : file.protected
|
||||
return isProtected
|
||||
}
|
||||
|
||||
authorizeProtectedActionForFile = async (file: FileItem, challengeReason: ChallengeReason) => {
|
||||
const authorizedFiles = await this.application.protections.authorizeProtectedActionForItems([file], challengeReason)
|
||||
const authorizedFiles = await this.protections.authorizeProtectedActionForItems([file], challengeReason)
|
||||
const isAuthorized = authorizedFiles.length > 0 && authorizedFiles.includes(file)
|
||||
return isAuthorized
|
||||
}
|
||||
|
||||
renameFile = async (file: FileItem, fileName: string) => {
|
||||
await this.application.mutator.renameFile(file, fileName)
|
||||
void this.application.sync.sync()
|
||||
await this.mutator.renameFile(file, fileName)
|
||||
void this.sync.sync()
|
||||
}
|
||||
|
||||
handleFileAction = async (
|
||||
@@ -243,7 +264,7 @@ export class FilesController extends AbstractViewController<FilesControllerEvent
|
||||
}
|
||||
|
||||
if (!NonMutatingFileActions.includes(action.type)) {
|
||||
this.application.sync.sync().catch(console.error)
|
||||
this.sync.sync().catch(console.error)
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -273,7 +294,7 @@ export class FilesController extends AbstractViewController<FilesControllerEvent
|
||||
|
||||
let lastProgress: FileDownloadProgress | undefined
|
||||
|
||||
const result = await this.application.files.downloadFile(file, async (decryptedBytes, progress) => {
|
||||
const result = await this.files.downloadFile(file, async (decryptedBytes, progress) => {
|
||||
if (isUsingStreamingSaver) {
|
||||
await saver.pushBytes(decryptedBytes)
|
||||
} else {
|
||||
@@ -301,7 +322,16 @@ export class FilesController extends AbstractViewController<FilesControllerEvent
|
||||
const blob = new Blob([finalBytes], {
|
||||
type: file.mimeType,
|
||||
})
|
||||
await downloadOrShareBlobBasedOnPlatform(this.application, blob, file.name, false)
|
||||
// await downloadOrShareBlobBasedOnPlatform(this, blob, file.name, false)
|
||||
await downloadOrShareBlobBasedOnPlatform({
|
||||
archiveService: this.archiveService,
|
||||
platform: this.platform,
|
||||
mobileDevice: this.mobileDevice,
|
||||
blob,
|
||||
filename: file.name,
|
||||
isNativeMobileWeb: this._isNativeMobileWeb.execute().getValue(),
|
||||
showToastOnAndroid: false,
|
||||
})
|
||||
}
|
||||
|
||||
addToast({
|
||||
@@ -326,7 +356,7 @@ export class FilesController extends AbstractViewController<FilesControllerEvent
|
||||
|
||||
alertIfFileExceedsSizeLimit = (file: File): boolean => {
|
||||
if (!this.shouldUseStreamingReader && this.maxFileSize && file.size >= this.maxFileSize) {
|
||||
this.application.alerts
|
||||
this.alerts
|
||||
.alert(
|
||||
`This file exceeds the limits supported in this browser. To upload files greater than ${
|
||||
this.maxFileSize / BYTES_IN_ONE_MEGABYTE
|
||||
@@ -360,7 +390,7 @@ export class FilesController extends AbstractViewController<FilesControllerEvent
|
||||
let toastId: string | undefined
|
||||
|
||||
try {
|
||||
const minimumChunkSize = this.application.files.minimumChunkSize()
|
||||
const minimumChunkSize = this.files.minimumChunkSize()
|
||||
|
||||
const fileToUpload =
|
||||
fileOrHandle instanceof File
|
||||
@@ -377,9 +407,9 @@ export class FilesController extends AbstractViewController<FilesControllerEvent
|
||||
return
|
||||
}
|
||||
|
||||
const operation = await this.application.files.beginNewFileUpload(
|
||||
const operation = await this.files.beginNewFileUpload(
|
||||
fileToUpload.size,
|
||||
this.application.vaultDisplayService.exclusivelyShownVault,
|
||||
this.vaultDisplayService.exclusivelyShownVault,
|
||||
)
|
||||
|
||||
if (operation instanceof ClientDisplayableError) {
|
||||
@@ -401,7 +431,7 @@ export class FilesController extends AbstractViewController<FilesControllerEvent
|
||||
}
|
||||
|
||||
const onChunk: OnChunkCallbackNoProgress = async ({ data, index, isLast }) => {
|
||||
await this.application.files.pushBytesForUpload(operation, data, index, isLast)
|
||||
await this.files.pushBytesForUpload(operation, data, index, isLast)
|
||||
|
||||
const percentComplete = Math.round(operation.getProgress().percentComplete)
|
||||
if (toastId) {
|
||||
@@ -416,10 +446,10 @@ export class FilesController extends AbstractViewController<FilesControllerEvent
|
||||
|
||||
if (!fileResult.mimeType) {
|
||||
const { ext } = parseFileName(fileToUpload.name)
|
||||
fileResult.mimeType = await this.application.getArchiveService().getMimeType(ext)
|
||||
fileResult.mimeType = await this.archiveService.getMimeType(ext)
|
||||
}
|
||||
|
||||
const uploadedFile = await this.application.files.finishUpload(operation, fileResult)
|
||||
const uploadedFile = await this.files.finishUpload(operation, fileResult)
|
||||
|
||||
if (uploadedFile instanceof ClientDisplayableError) {
|
||||
addToast({
|
||||
@@ -485,28 +515,28 @@ export class FilesController extends AbstractViewController<FilesControllerEvent
|
||||
confirmButtonStyle: 'danger',
|
||||
})
|
||||
) {
|
||||
await Promise.all(files.map((file) => this.application.files.deleteFile(file)))
|
||||
void this.application.sync.sync()
|
||||
await Promise.all(files.map((file) => this.files.deleteFile(file)))
|
||||
void this.sync.sync()
|
||||
}
|
||||
}
|
||||
|
||||
setProtectionForFiles = async (protect: boolean, files: FileItem[]) => {
|
||||
if (protect) {
|
||||
const protectedItems = await this.application.protections.protectItems(files)
|
||||
const protectedItems = await this.protections.protectItems(files)
|
||||
if (protectedItems) {
|
||||
this.setShowProtectedOverlay(true)
|
||||
}
|
||||
} else {
|
||||
const unprotectedItems = await this.application.protections.unprotectItems(files, ChallengeReason.UnprotectFile)
|
||||
const unprotectedItems = await this.protections.unprotectItems(files, ChallengeReason.UnprotectFile)
|
||||
if (unprotectedItems) {
|
||||
this.setShowProtectedOverlay(false)
|
||||
}
|
||||
}
|
||||
void this.application.sync.sync()
|
||||
void this.sync.sync()
|
||||
}
|
||||
|
||||
downloadFiles = async (files: FileItem[]) => {
|
||||
if (this.application.platform === Platform.MacDesktop) {
|
||||
if (this.platform === Platform.MacDesktop) {
|
||||
for (const file of files) {
|
||||
await this.handleFileAction({
|
||||
type: FileItemActionType.DownloadFile,
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { WebApplication } from '@/Application/WebApplication'
|
||||
import { DecryptedTransferPayload, SNTag, TagContent } from '@standardnotes/models'
|
||||
import { ContentType, pluralize, UuidGenerator } from '@standardnotes/snjs'
|
||||
import {
|
||||
ContentType,
|
||||
ItemManagerInterface,
|
||||
MutatorClientInterface,
|
||||
pluralize,
|
||||
UuidGenerator,
|
||||
} from '@standardnotes/snjs'
|
||||
import { Importer, NoteImportType } from '@standardnotes/ui-services'
|
||||
import { action, makeObservable, observable } from 'mobx'
|
||||
import { NavigationController } from './Navigation/NavigationController'
|
||||
@@ -23,13 +28,14 @@ export type ImportModalFile = (
|
||||
|
||||
export class ImportModalController {
|
||||
isVisible = false
|
||||
importer: Importer
|
||||
files: ImportModalFile[] = []
|
||||
importTag: SNTag | undefined = undefined
|
||||
|
||||
constructor(
|
||||
private application: WebApplication,
|
||||
private importer: Importer,
|
||||
private navigationController: NavigationController,
|
||||
private items: ItemManagerInterface,
|
||||
private mutator: MutatorClientInterface,
|
||||
) {
|
||||
makeObservable(this, {
|
||||
isVisible: observable,
|
||||
@@ -43,8 +49,6 @@ export class ImportModalController {
|
||||
importTag: observable,
|
||||
setImportTag: action,
|
||||
})
|
||||
|
||||
this.importer = new Importer(application)
|
||||
}
|
||||
|
||||
setIsVisible = (isVisible: boolean) => {
|
||||
@@ -152,7 +156,7 @@ export class ImportModalController {
|
||||
}
|
||||
}
|
||||
const currentDate = new Date()
|
||||
const importTagItem = this.application.items.createTemplateItem<TagContent, SNTag>(ContentType.TYPES.Tag, {
|
||||
const importTagItem = this.items.createTemplateItem<TagContent, SNTag>(ContentType.TYPES.Tag, {
|
||||
title: `Imported on ${currentDate.toLocaleString()}`,
|
||||
expanded: false,
|
||||
iconString: '',
|
||||
@@ -163,7 +167,7 @@ export class ImportModalController {
|
||||
uuid: payload.uuid,
|
||||
})),
|
||||
})
|
||||
const importTag = await this.application.mutator.insertItem(importTagItem)
|
||||
const importTag = await this.mutator.insertItem(importTagItem)
|
||||
if (importTag) {
|
||||
this.setImportTag(importTag as SNTag)
|
||||
}
|
||||
|
||||
@@ -1,39 +1,51 @@
|
||||
import { ContentType, SNTag } from '@standardnotes/snjs'
|
||||
import { InternalEventBus } from '@standardnotes/services'
|
||||
import { ContentType, Result, SNTag } from '@standardnotes/snjs'
|
||||
import { InternalEventBus, ItemManagerInterface } from '@standardnotes/services'
|
||||
import { WebApplication } from '@/Application/WebApplication'
|
||||
import { NavigationController } from '../Navigation/NavigationController'
|
||||
import { NotesController } from '../NotesController/NotesController'
|
||||
import { SearchOptionsController } from '../SearchOptionsController'
|
||||
import { SelectedItemsController } from '../SelectedItemsController'
|
||||
import { ItemListController } from './ItemListController'
|
||||
import { ItemsReloadSource } from './ItemsReloadSource'
|
||||
import { IsNativeMobileWeb } from '@standardnotes/ui-services'
|
||||
import { runInAction } from 'mobx'
|
||||
|
||||
describe('item list controller', () => {
|
||||
let application: WebApplication
|
||||
let controller: ItemListController
|
||||
let navigationController: NavigationController
|
||||
let selectionController: SelectedItemsController
|
||||
|
||||
beforeEach(() => {
|
||||
application = {} as jest.Mocked<WebApplication>
|
||||
application.streamItems = jest.fn()
|
||||
application = {
|
||||
navigationController: {} as jest.Mocked<NavigationController>,
|
||||
searchOptionsController: {} as jest.Mocked<SearchOptionsController>,
|
||||
notesController: {} as jest.Mocked<NotesController>,
|
||||
isNativeMobileWebUseCase: {
|
||||
execute: jest.fn().mockReturnValue(Result.ok(false)),
|
||||
} as unknown as jest.Mocked<IsNativeMobileWeb>,
|
||||
items: {
|
||||
streamItems: jest.fn(),
|
||||
} as unknown as jest.Mocked<ItemManagerInterface>,
|
||||
} as unknown as jest.Mocked<WebApplication>
|
||||
|
||||
application.addEventObserver = jest.fn()
|
||||
application.addWebEventObserver = jest.fn()
|
||||
application.isNativeMobileWeb = jest.fn().mockReturnValue(false)
|
||||
|
||||
navigationController = {} as jest.Mocked<NavigationController>
|
||||
selectionController = {} as jest.Mocked<SelectedItemsController>
|
||||
|
||||
const searchOptionsController = {} as jest.Mocked<SearchOptionsController>
|
||||
const notesController = {} as jest.Mocked<NotesController>
|
||||
const eventBus = new InternalEventBus()
|
||||
|
||||
controller = new ItemListController(
|
||||
application,
|
||||
navigationController,
|
||||
searchOptionsController,
|
||||
selectionController,
|
||||
notesController,
|
||||
application.keyboardService,
|
||||
application.paneController,
|
||||
application.navigationController,
|
||||
application.searchOptionsController,
|
||||
application.items,
|
||||
application.preferences,
|
||||
application.itemControllerGroup,
|
||||
application.vaultDisplayService,
|
||||
application.desktopManager,
|
||||
application.protections,
|
||||
application.options,
|
||||
application.isNativeMobileWebUseCase,
|
||||
application.changeAndSaveItem,
|
||||
eventBus,
|
||||
)
|
||||
})
|
||||
@@ -42,14 +54,13 @@ describe('item list controller', () => {
|
||||
beforeEach(() => {
|
||||
controller.getFirstNonProtectedItem = jest.fn()
|
||||
|
||||
Object.defineProperty(selectionController, 'selectedUuids', {
|
||||
get: () => new Set(),
|
||||
configurable: true,
|
||||
runInAction(() => {
|
||||
controller.selectedUuids = new Set()
|
||||
})
|
||||
})
|
||||
|
||||
it('should return false is platform is native mobile web', () => {
|
||||
application.isNativeMobileWeb = jest.fn().mockReturnValue(true)
|
||||
it('should return false if platform is native mobile web', () => {
|
||||
application.isNativeMobileWebUseCase.execute = jest.fn().mockReturnValue(Result.ok(true))
|
||||
|
||||
expect(controller.shouldSelectFirstItem(ItemsReloadSource.TagChange)).toBe(false)
|
||||
})
|
||||
@@ -68,7 +79,7 @@ describe('item list controller', () => {
|
||||
content_type: ContentType.TYPES.Tag,
|
||||
} as jest.Mocked<SNTag>
|
||||
|
||||
Object.defineProperty(navigationController, 'selected', {
|
||||
Object.defineProperty(application.navigationController, 'selected', {
|
||||
get: () => tag,
|
||||
})
|
||||
|
||||
@@ -80,7 +91,7 @@ describe('item list controller', () => {
|
||||
content_type: ContentType.TYPES.Tag,
|
||||
} as jest.Mocked<SNTag>
|
||||
|
||||
Object.defineProperty(navigationController, 'selected', {
|
||||
Object.defineProperty(application.navigationController, 'selected', {
|
||||
get: () => tag,
|
||||
})
|
||||
|
||||
@@ -92,11 +103,11 @@ describe('item list controller', () => {
|
||||
content_type: ContentType.TYPES.Tag,
|
||||
} as jest.Mocked<SNTag>
|
||||
|
||||
Object.defineProperty(selectionController, 'selectedUuids', {
|
||||
get: () => new Set(['123']),
|
||||
runInAction(() => {
|
||||
controller.selectedUuids = new Set(['123'])
|
||||
})
|
||||
|
||||
Object.defineProperty(navigationController, 'selected', {
|
||||
Object.defineProperty(application.navigationController, 'selected', {
|
||||
get: () => tag,
|
||||
})
|
||||
|
||||
|
||||
@@ -23,15 +23,23 @@ import {
|
||||
NotesAndFilesDisplayControllerOptions,
|
||||
InternalEventBusInterface,
|
||||
PrefDefaults,
|
||||
ItemManagerInterface,
|
||||
PreferenceServiceInterface,
|
||||
ChangeAndSaveItem,
|
||||
DesktopManagerInterface,
|
||||
UuidString,
|
||||
ProtectionsClientInterface,
|
||||
FullyResolvedApplicationOptions,
|
||||
Uuids,
|
||||
isNote,
|
||||
ChallengeReason,
|
||||
KeyboardModifier,
|
||||
} from '@standardnotes/snjs'
|
||||
import { action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'
|
||||
import { WebApplication } from '../../Application/WebApplication'
|
||||
import { WebDisplayOptions } from './WebDisplayOptions'
|
||||
import { NavigationController } from '../Navigation/NavigationController'
|
||||
import { CrossControllerEvent } from '../CrossControllerEvent'
|
||||
import { SearchOptionsController } from '../SearchOptionsController'
|
||||
import { SelectedItemsController } from '../SelectedItemsController'
|
||||
import { NotesController } from '../NotesController/NotesController'
|
||||
import { formatDateAndTimeForNote } from '@/Utils/DateUtils'
|
||||
|
||||
import { AbstractViewController } from '../Abstract/AbstractViewController'
|
||||
@@ -40,14 +48,28 @@ import { NoteViewController } from '@/Components/NoteView/Controller/NoteViewCon
|
||||
import { FileViewController } from '@/Components/NoteView/Controller/FileViewController'
|
||||
import { TemplateNoteViewAutofocusBehavior } from '@/Components/NoteView/Controller/TemplateNoteViewControllerOptions'
|
||||
import { ItemsReloadSource } from './ItemsReloadSource'
|
||||
import { VaultDisplayServiceEvent } from '@standardnotes/ui-services'
|
||||
import {
|
||||
IsNativeMobileWeb,
|
||||
KeyboardService,
|
||||
SelectionControllerPersistableValue,
|
||||
VaultDisplayServiceEvent,
|
||||
VaultDisplayServiceInterface,
|
||||
} from '@standardnotes/ui-services'
|
||||
import { getDayjsFormattedString } from '@/Utils/GetDayjsFormattedString'
|
||||
import { ItemGroupController } from '@/Components/NoteView/Controller/ItemGroupController'
|
||||
import { Persistable } from '../Abstract/Persistable'
|
||||
import { PaneController } from '../PaneController/PaneController'
|
||||
import { requestCloseAllOpenModalsAndPopovers } from '@/Utils/CloseOpenModalsAndPopovers'
|
||||
import { PaneLayout } from '../PaneController/PaneLayout'
|
||||
|
||||
const MinNoteCellHeight = 51.0
|
||||
const DefaultListNumNotes = 20
|
||||
const ElementIdScrollContainer = 'notes-scrollable'
|
||||
|
||||
export class ItemListController extends AbstractViewController implements InternalEventHandlerInterface {
|
||||
export class ItemListController
|
||||
extends AbstractViewController
|
||||
implements InternalEventHandlerInterface, Persistable<SelectionControllerPersistableValue>
|
||||
{
|
||||
completedFullSync = false
|
||||
noteFilterText = ''
|
||||
notes: SNNote[] = []
|
||||
@@ -75,6 +97,10 @@ export class ItemListController extends AbstractViewController implements Intern
|
||||
isTableViewEnabled = false
|
||||
private reloadItemsPromise?: Promise<unknown>
|
||||
|
||||
lastSelectedItem: ListableContentItem | undefined
|
||||
selectedUuids: Set<UuidString> = observable(new Set<UuidString>())
|
||||
selectedItems: Record<UuidString, ListableContentItem> = {}
|
||||
|
||||
override deinit() {
|
||||
super.deinit()
|
||||
;(this.noteFilterText as unknown) = undefined
|
||||
@@ -82,113 +108,28 @@ export class ItemListController extends AbstractViewController implements Intern
|
||||
;(this.renderedItems as unknown) = undefined
|
||||
;(this.navigationController as unknown) = undefined
|
||||
;(this.searchOptionsController as unknown) = undefined
|
||||
;(this.selectionController as unknown) = undefined
|
||||
;(this.notesController as unknown) = undefined
|
||||
;(window.onresize as unknown) = undefined
|
||||
|
||||
destroyAllObjectProperties(this)
|
||||
}
|
||||
|
||||
constructor(
|
||||
application: WebApplication,
|
||||
private keyboardService: KeyboardService,
|
||||
private paneController: PaneController,
|
||||
private navigationController: NavigationController,
|
||||
private searchOptionsController: SearchOptionsController,
|
||||
private selectionController: SelectedItemsController,
|
||||
private notesController: NotesController,
|
||||
private itemManager: ItemManagerInterface,
|
||||
private preferences: PreferenceServiceInterface,
|
||||
private itemControllerGroup: ItemGroupController,
|
||||
private vaultDisplayService: VaultDisplayServiceInterface,
|
||||
private desktopManager: DesktopManagerInterface | undefined,
|
||||
private protections: ProtectionsClientInterface,
|
||||
private options: FullyResolvedApplicationOptions,
|
||||
private _isNativeMobileWeb: IsNativeMobileWeb,
|
||||
private _changeAndSaveItem: ChangeAndSaveItem,
|
||||
eventBus: InternalEventBusInterface,
|
||||
) {
|
||||
super(application, eventBus)
|
||||
|
||||
eventBus.addEventHandler(this, CrossControllerEvent.TagChanged)
|
||||
eventBus.addEventHandler(this, CrossControllerEvent.ActiveEditorChanged)
|
||||
eventBus.addEventHandler(this, VaultDisplayServiceEvent.VaultDisplayOptionsChanged)
|
||||
|
||||
this.resetPagination()
|
||||
|
||||
this.disposers.push(
|
||||
application.streamItems<SNNote>([ContentType.TYPES.Note, ContentType.TYPES.File], () => {
|
||||
void this.reloadItems(ItemsReloadSource.ItemStream)
|
||||
}),
|
||||
)
|
||||
|
||||
this.disposers.push(
|
||||
application.streamItems<SNTag>(
|
||||
[ContentType.TYPES.Tag, ContentType.TYPES.SmartView],
|
||||
async ({ changed, inserted }) => {
|
||||
const tags = [...changed, ...inserted]
|
||||
|
||||
const { didReloadItems } = await this.reloadDisplayPreferences({ userTriggered: false })
|
||||
if (!didReloadItems) {
|
||||
/** A tag could have changed its relationships, so we need to reload the filter */
|
||||
this.reloadNotesDisplayOptions()
|
||||
void this.reloadItems(ItemsReloadSource.ItemStream)
|
||||
}
|
||||
|
||||
if (
|
||||
this.navigationController.selected &&
|
||||
findInArray(tags, 'uuid', this.navigationController.selected.uuid)
|
||||
) {
|
||||
/** Tag title could have changed */
|
||||
this.reloadPanelTitle()
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
this.disposers.push(
|
||||
application.addEventObserver(async () => {
|
||||
void this.reloadDisplayPreferences({ userTriggered: false })
|
||||
}, ApplicationEvent.PreferencesChanged),
|
||||
)
|
||||
|
||||
this.disposers.push(
|
||||
application.addEventObserver(async () => {
|
||||
this.application.itemControllerGroup.closeAllItemControllers()
|
||||
void this.selectFirstItem()
|
||||
this.setCompletedFullSync(false)
|
||||
}, ApplicationEvent.SignedIn),
|
||||
)
|
||||
|
||||
this.disposers.push(
|
||||
application.addEventObserver(async () => {
|
||||
if (!this.completedFullSync) {
|
||||
void this.reloadItems(ItemsReloadSource.SyncEvent).then(() => {
|
||||
if (
|
||||
this.notes.length === 0 &&
|
||||
this.navigationController.selected instanceof SmartView &&
|
||||
this.navigationController.selected.uuid === SystemViewId.AllNotes &&
|
||||
this.noteFilterText === '' &&
|
||||
!this.getActiveItemController()
|
||||
) {
|
||||
this.createPlaceholderNote()?.catch(console.error)
|
||||
}
|
||||
})
|
||||
this.setCompletedFullSync(true)
|
||||
}
|
||||
}, ApplicationEvent.CompletedFullSync),
|
||||
)
|
||||
|
||||
this.disposers.push(
|
||||
application.addWebEventObserver((webEvent) => {
|
||||
if (webEvent === WebAppEvent.EditorFocused) {
|
||||
this.setShowDisplayOptionsMenu(false)
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
this.disposers.push(
|
||||
reaction(
|
||||
() => [
|
||||
this.searchOptionsController.includeProtectedContents,
|
||||
this.searchOptionsController.includeArchived,
|
||||
this.searchOptionsController.includeTrashed,
|
||||
],
|
||||
() => {
|
||||
this.reloadNotesDisplayOptions()
|
||||
void this.reloadItems(ItemsReloadSource.DisplayOptionsChange)
|
||||
},
|
||||
),
|
||||
)
|
||||
super(eventBus)
|
||||
|
||||
makeObservable(this, {
|
||||
completedFullSync: observable,
|
||||
@@ -214,21 +155,186 @@ export class ItemListController extends AbstractViewController implements Intern
|
||||
|
||||
optionsSubtitle: computed,
|
||||
activeControllerItem: computed,
|
||||
|
||||
selectedUuids: observable,
|
||||
selectedItems: observable,
|
||||
|
||||
selectedItemsCount: computed,
|
||||
selectedFiles: computed,
|
||||
selectedFilesCount: computed,
|
||||
firstSelectedItem: computed,
|
||||
|
||||
selectItem: action,
|
||||
setSelectedUuids: action,
|
||||
setSelectedItems: action,
|
||||
|
||||
hydrateFromPersistedValue: action,
|
||||
})
|
||||
|
||||
eventBus.addEventHandler(this, CrossControllerEvent.TagChanged)
|
||||
eventBus.addEventHandler(this, CrossControllerEvent.ActiveEditorChanged)
|
||||
eventBus.addEventHandler(this, VaultDisplayServiceEvent.VaultDisplayOptionsChanged)
|
||||
|
||||
this.resetPagination()
|
||||
|
||||
this.disposers.push(
|
||||
itemManager.streamItems<SNNote>([ContentType.TYPES.Note, ContentType.TYPES.File], () => {
|
||||
void this.reloadItems(ItemsReloadSource.ItemStream)
|
||||
}),
|
||||
)
|
||||
|
||||
this.disposers.push(
|
||||
itemManager.streamItems<SNTag>(
|
||||
[ContentType.TYPES.Tag, ContentType.TYPES.SmartView],
|
||||
async ({ changed, inserted }) => {
|
||||
const tags = [...changed, ...inserted]
|
||||
|
||||
const { didReloadItems } = await this.reloadDisplayPreferences({ userTriggered: false })
|
||||
if (!didReloadItems) {
|
||||
/** A tag could have changed its relationships, so we need to reload the filter */
|
||||
this.reloadNotesDisplayOptions()
|
||||
void this.reloadItems(ItemsReloadSource.ItemStream)
|
||||
}
|
||||
|
||||
if (
|
||||
this.navigationController.selected &&
|
||||
findInArray(tags, 'uuid', this.navigationController.selected.uuid)
|
||||
) {
|
||||
/** Tag title could have changed */
|
||||
this.reloadPanelTitle()
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
eventBus.addEventHandler(this, ApplicationEvent.PreferencesChanged)
|
||||
eventBus.addEventHandler(this, ApplicationEvent.SignedIn)
|
||||
eventBus.addEventHandler(this, ApplicationEvent.CompletedFullSync)
|
||||
eventBus.addEventHandler(this, WebAppEvent.EditorFocused)
|
||||
|
||||
this.disposers.push(
|
||||
reaction(
|
||||
() => [
|
||||
this.searchOptionsController.includeProtectedContents,
|
||||
this.searchOptionsController.includeArchived,
|
||||
this.searchOptionsController.includeTrashed,
|
||||
],
|
||||
() => {
|
||||
this.reloadNotesDisplayOptions()
|
||||
void this.reloadItems(ItemsReloadSource.DisplayOptionsChange)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
this.disposers.push(
|
||||
reaction(
|
||||
() => this.selectedUuids,
|
||||
() => {
|
||||
eventBus.publish({
|
||||
type: CrossControllerEvent.RequestValuePersistence,
|
||||
payload: undefined,
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
this.disposers.push(
|
||||
this.itemManager.streamItems<SNNote | FileItem>(
|
||||
[ContentType.TYPES.Note, ContentType.TYPES.File],
|
||||
({ changed, inserted, removed }) => {
|
||||
runInAction(() => {
|
||||
for (const removedItem of removed) {
|
||||
this.removeSelectedItem(removedItem.uuid)
|
||||
}
|
||||
|
||||
for (const item of [...changed, ...inserted]) {
|
||||
if (this.selectedItems[item.uuid]) {
|
||||
this.selectedItems[item.uuid] = item
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
window.onresize = () => {
|
||||
this.resetPagination(true)
|
||||
}
|
||||
}
|
||||
|
||||
getPersistableValue = (): SelectionControllerPersistableValue => {
|
||||
return {
|
||||
selectedUuids: Array.from(this.selectedUuids),
|
||||
}
|
||||
}
|
||||
|
||||
hydrateFromPersistedValue = (state: SelectionControllerPersistableValue | undefined): void => {
|
||||
if (!state) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.selectedUuids.size && state.selectedUuids.length > 0) {
|
||||
if (!this.options.allowNoteSelectionStatePersistence) {
|
||||
const items = this.itemManager.findItems(state.selectedUuids).filter((item) => !isNote(item))
|
||||
void this.selectUuids(Uuids(items))
|
||||
} else {
|
||||
void this.selectUuids(state.selectedUuids)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async handleEvent(event: InternalEventInterface): Promise<void> {
|
||||
if (event.type === CrossControllerEvent.TagChanged) {
|
||||
const payload = event.payload as { userTriggered: boolean }
|
||||
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)
|
||||
switch (event.type) {
|
||||
case CrossControllerEvent.TagChanged: {
|
||||
const payload = event.payload as { userTriggered: boolean }
|
||||
await this.handleTagChange(payload.userTriggered)
|
||||
break
|
||||
}
|
||||
|
||||
case CrossControllerEvent.ActiveEditorChanged: {
|
||||
await this.handleEditorChange()
|
||||
break
|
||||
}
|
||||
|
||||
case VaultDisplayServiceEvent.VaultDisplayOptionsChanged: {
|
||||
void this.reloadItems(ItemsReloadSource.DisplayOptionsChange)
|
||||
break
|
||||
}
|
||||
|
||||
case ApplicationEvent.PreferencesChanged: {
|
||||
void this.reloadDisplayPreferences({ userTriggered: false })
|
||||
break
|
||||
}
|
||||
|
||||
case WebAppEvent.EditorFocused: {
|
||||
this.setShowDisplayOptionsMenu(false)
|
||||
break
|
||||
}
|
||||
|
||||
case ApplicationEvent.SignedIn: {
|
||||
this.itemControllerGroup.closeAllItemControllers()
|
||||
void this.selectFirstItem()
|
||||
this.setCompletedFullSync(false)
|
||||
break
|
||||
}
|
||||
|
||||
case ApplicationEvent.CompletedFullSync: {
|
||||
if (!this.completedFullSync) {
|
||||
void this.reloadItems(ItemsReloadSource.SyncEvent).then(() => {
|
||||
if (
|
||||
this.notes.length === 0 &&
|
||||
this.navigationController.selected instanceof SmartView &&
|
||||
this.navigationController.selected.uuid === SystemViewId.AllNotes &&
|
||||
this.noteFilterText === '' &&
|
||||
!this.getActiveItemController()
|
||||
) {
|
||||
this.createPlaceholderNote()?.catch(console.error)
|
||||
}
|
||||
})
|
||||
this.setCompletedFullSync(true)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,7 +343,7 @@ export class ItemListController extends AbstractViewController implements Intern
|
||||
}
|
||||
|
||||
public getActiveItemController(): NoteViewController | FileViewController | undefined {
|
||||
return this.application.itemControllerGroup.activeItemViewController
|
||||
return this.itemControllerGroup.activeItemViewController
|
||||
}
|
||||
|
||||
public get activeControllerItem() {
|
||||
@@ -249,13 +355,13 @@ export class ItemListController extends AbstractViewController implements Intern
|
||||
return
|
||||
}
|
||||
|
||||
const note = this.application.items.findItem<SNNote>(uuid)
|
||||
const note = this.itemManager.findItem<SNNote>(uuid)
|
||||
if (!note) {
|
||||
console.warn('Tried accessing a non-existant note of UUID ' + uuid)
|
||||
return
|
||||
}
|
||||
|
||||
await this.application.itemControllerGroup.createItemController({ note })
|
||||
await this.itemControllerGroup.createItemController({ note })
|
||||
|
||||
await this.publishCrossControllerEventSync(CrossControllerEvent.ActiveEditorChanged)
|
||||
}
|
||||
@@ -265,13 +371,13 @@ export class ItemListController extends AbstractViewController implements Intern
|
||||
return
|
||||
}
|
||||
|
||||
const file = this.application.items.findItem<FileItem>(fileUuid)
|
||||
const file = this.itemManager.findItem<FileItem>(fileUuid)
|
||||
if (!file) {
|
||||
console.warn('Tried accessing a non-existant file of UUID ' + fileUuid)
|
||||
return
|
||||
}
|
||||
|
||||
await this.application.itemControllerGroup.createItemController({ file })
|
||||
await this.itemControllerGroup.createItemController({ file })
|
||||
}
|
||||
|
||||
setCompletedFullSync = (completed: boolean) => {
|
||||
@@ -315,9 +421,9 @@ export class ItemListController extends AbstractViewController implements Intern
|
||||
return
|
||||
}
|
||||
|
||||
const notes = this.application.items.getDisplayableNotes()
|
||||
const notes = this.itemManager.getDisplayableNotes()
|
||||
|
||||
const items = this.application.items.getDisplayableNotesAndFiles()
|
||||
const items = this.itemManager.getDisplayableNotesAndFiles()
|
||||
|
||||
const renderedItems = items.slice(0, this.notesToDisplay)
|
||||
|
||||
@@ -333,7 +439,7 @@ export class ItemListController extends AbstractViewController implements Intern
|
||||
}
|
||||
|
||||
private shouldLeaveSelectionUnchanged = (activeController: NoteViewController | FileViewController | undefined) => {
|
||||
const hasMultipleItemsSelected = this.selectionController.selectedItemsCount >= 2
|
||||
const hasMultipleItemsSelected = this.selectedItemsCount >= 2
|
||||
|
||||
return (
|
||||
hasMultipleItemsSelected || (activeController instanceof NoteViewController && activeController.isTemplateNote)
|
||||
@@ -420,11 +526,11 @@ export class ItemListController extends AbstractViewController implements Intern
|
||||
}
|
||||
|
||||
private shouldSelectActiveItem = (activeItem: SNNote | FileItem) => {
|
||||
return !this.selectionController.isItemSelected(activeItem)
|
||||
return !this.isItemSelected(activeItem)
|
||||
}
|
||||
|
||||
shouldSelectFirstItem = (itemsReloadSource: ItemsReloadSource) => {
|
||||
if (this.application.isNativeMobileWeb()) {
|
||||
if (this._isNativeMobileWeb.execute().getValue()) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -440,7 +546,7 @@ export class ItemListController extends AbstractViewController implements Intern
|
||||
}
|
||||
|
||||
const userChangedTag = itemsReloadSource === ItemsReloadSource.UserTriggeredTagChange
|
||||
const hasNoSelectedItem = !this.selectionController.selectedUuids.size
|
||||
const hasNoSelectedItem = !this.selectedUuids.size
|
||||
|
||||
return userChangedTag || hasNoSelectedItem
|
||||
}
|
||||
@@ -458,7 +564,7 @@ export class ItemListController extends AbstractViewController implements Intern
|
||||
if (activeController && activeItem && this.shouldCloseActiveItem(activeItem, itemsReloadSource)) {
|
||||
this.closeItemController(activeController)
|
||||
|
||||
this.selectionController.deselectItem(activeItem)
|
||||
this.deselectItem(activeItem)
|
||||
|
||||
if (this.shouldSelectFirstItem(itemsReloadSource)) {
|
||||
if (this.isTableViewEnabled && !isMobileScreen()) {
|
||||
@@ -466,11 +572,11 @@ export class ItemListController extends AbstractViewController implements Intern
|
||||
}
|
||||
|
||||
log(LoggingDomain.Selection, 'Selecting next item after closing active one')
|
||||
this.selectionController.selectNextItem({ userTriggered: false })
|
||||
this.selectNextItem({ userTriggered: false })
|
||||
}
|
||||
} else if (activeItem && this.shouldSelectActiveItem(activeItem)) {
|
||||
log(LoggingDomain.Selection, 'Selecting active item')
|
||||
await this.selectionController.selectItem(activeItem.uuid).catch(console.error)
|
||||
await this.selectItem(activeItem.uuid).catch(console.error)
|
||||
} else if (this.shouldSelectFirstItem(itemsReloadSource)) {
|
||||
await this.selectFirstItem()
|
||||
} else if (this.shouldSelectNextItemOrCreateNewNote(activeItem)) {
|
||||
@@ -511,7 +617,7 @@ export class ItemListController extends AbstractViewController implements Intern
|
||||
},
|
||||
}
|
||||
|
||||
this.application.items.setPrimaryItemDisplayOptions(criteria)
|
||||
this.itemManager.setPrimaryItemDisplayOptions(criteria)
|
||||
}
|
||||
|
||||
reloadDisplayPreferences = async ({
|
||||
@@ -525,7 +631,7 @@ export class ItemListController extends AbstractViewController implements Intern
|
||||
const selectedTag = this.navigationController.selected
|
||||
const isSystemTag = selectedTag && isSmartView(selectedTag) && isSystemView(selectedTag)
|
||||
const selectedTagPreferences = isSystemTag
|
||||
? this.application.getPreference(PrefKey.SystemViewPreferences)?.[selectedTag.uuid as SystemViewId]
|
||||
? this.preferences.getValue(PrefKey.SystemViewPreferences)?.[selectedTag.uuid as SystemViewId]
|
||||
: selectedTag?.preferences
|
||||
|
||||
this.isTableViewEnabled = Boolean(selectedTagPreferences?.useTableView)
|
||||
@@ -533,7 +639,7 @@ export class ItemListController extends AbstractViewController implements Intern
|
||||
const currentSortBy = this.displayOptions.sortBy
|
||||
let sortBy =
|
||||
selectedTagPreferences?.sortBy ||
|
||||
this.application.getPreference(PrefKey.SortNotesBy, PrefDefaults[PrefKey.SortNotesBy])
|
||||
this.preferences.getValue(PrefKey.SortNotesBy, PrefDefaults[PrefKey.SortNotesBy])
|
||||
if (sortBy === CollectionSort.UpdatedAt || (sortBy as string) === 'client_updated_at') {
|
||||
sortBy = CollectionSort.UpdatedAt
|
||||
}
|
||||
@@ -543,49 +649,49 @@ export class ItemListController extends AbstractViewController implements Intern
|
||||
newDisplayOptions.sortDirection =
|
||||
useBoolean(
|
||||
selectedTagPreferences?.sortReverse,
|
||||
this.application.getPreference(PrefKey.SortNotesReverse, PrefDefaults[PrefKey.SortNotesReverse]),
|
||||
this.preferences.getValue(PrefKey.SortNotesReverse, PrefDefaults[PrefKey.SortNotesReverse]),
|
||||
) === false
|
||||
? 'dsc'
|
||||
: 'asc'
|
||||
|
||||
newDisplayOptions.includeArchived = useBoolean(
|
||||
selectedTagPreferences?.showArchived,
|
||||
this.application.getPreference(PrefKey.NotesShowArchived, PrefDefaults[PrefKey.NotesShowArchived]),
|
||||
this.preferences.getValue(PrefKey.NotesShowArchived, PrefDefaults[PrefKey.NotesShowArchived]),
|
||||
)
|
||||
|
||||
newDisplayOptions.includeTrashed = useBoolean(
|
||||
selectedTagPreferences?.showTrashed,
|
||||
this.application.getPreference(PrefKey.NotesShowTrashed, PrefDefaults[PrefKey.NotesShowTrashed]),
|
||||
this.preferences.getValue(PrefKey.NotesShowTrashed, PrefDefaults[PrefKey.NotesShowTrashed]),
|
||||
)
|
||||
|
||||
newDisplayOptions.includePinned = !useBoolean(
|
||||
selectedTagPreferences?.hidePinned,
|
||||
this.application.getPreference(PrefKey.NotesHidePinned, PrefDefaults[PrefKey.NotesHidePinned]),
|
||||
this.preferences.getValue(PrefKey.NotesHidePinned, PrefDefaults[PrefKey.NotesHidePinned]),
|
||||
)
|
||||
|
||||
newDisplayOptions.includeProtected = !useBoolean(
|
||||
selectedTagPreferences?.hideProtected,
|
||||
this.application.getPreference(PrefKey.NotesHideProtected, PrefDefaults[PrefKey.NotesHideProtected]),
|
||||
this.preferences.getValue(PrefKey.NotesHideProtected, PrefDefaults[PrefKey.NotesHideProtected]),
|
||||
)
|
||||
|
||||
newWebDisplayOptions.hideNotePreview = useBoolean(
|
||||
selectedTagPreferences?.hideNotePreview,
|
||||
this.application.getPreference(PrefKey.NotesHideNotePreview, PrefDefaults[PrefKey.NotesHideNotePreview]),
|
||||
this.preferences.getValue(PrefKey.NotesHideNotePreview, PrefDefaults[PrefKey.NotesHideNotePreview]),
|
||||
)
|
||||
|
||||
newWebDisplayOptions.hideDate = useBoolean(
|
||||
selectedTagPreferences?.hideDate,
|
||||
this.application.getPreference(PrefKey.NotesHideDate, PrefDefaults[PrefKey.NotesHideDate]),
|
||||
this.preferences.getValue(PrefKey.NotesHideDate, PrefDefaults[PrefKey.NotesHideDate]),
|
||||
)
|
||||
|
||||
newWebDisplayOptions.hideTags = useBoolean(
|
||||
selectedTagPreferences?.hideTags,
|
||||
this.application.getPreference(PrefKey.NotesHideTags, PrefDefaults[PrefKey.NotesHideTags]),
|
||||
this.preferences.getValue(PrefKey.NotesHideTags, PrefDefaults[PrefKey.NotesHideTags]),
|
||||
)
|
||||
|
||||
newWebDisplayOptions.hideEditorIcon = useBoolean(
|
||||
selectedTagPreferences?.hideEditorIcon,
|
||||
this.application.getPreference(PrefKey.NotesHideEditorIcon, PrefDefaults[PrefKey.NotesHideEditorIcon]),
|
||||
this.preferences.getValue(PrefKey.NotesHideEditorIcon, PrefDefaults[PrefKey.NotesHideEditorIcon]),
|
||||
)
|
||||
|
||||
const displayOptionsChanged =
|
||||
@@ -633,13 +739,13 @@ export class ItemListController extends AbstractViewController implements Intern
|
||||
|
||||
const activeRegularTagUuid = selectedTag instanceof SNTag ? selectedTag.uuid : undefined
|
||||
|
||||
return this.application.itemControllerGroup.createItemController({
|
||||
return this.itemControllerGroup.createItemController({
|
||||
templateOptions: {
|
||||
title,
|
||||
tag: activeRegularTagUuid,
|
||||
createdAt,
|
||||
autofocusBehavior,
|
||||
vault: this.application.vaultDisplayService.exclusivelyShownVault,
|
||||
vault: this.vaultDisplayService.exclusivelyShownVault,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -652,12 +758,12 @@ export class ItemListController extends AbstractViewController implements Intern
|
||||
const selectedTag = this.navigationController.selected
|
||||
const isSystemTag = selectedTag && isSmartView(selectedTag) && isSystemView(selectedTag)
|
||||
const selectedTagPreferences = isSystemTag
|
||||
? this.application.getPreference(PrefKey.SystemViewPreferences)?.[selectedTag.uuid as SystemViewId]
|
||||
? this.preferences.getValue(PrefKey.SystemViewPreferences)?.[selectedTag.uuid as SystemViewId]
|
||||
: selectedTag?.preferences
|
||||
|
||||
const titleFormat =
|
||||
selectedTagPreferences?.newNoteTitleFormat ||
|
||||
this.application.getPreference(PrefKey.NewNoteTitleFormat, PrefDefaults[PrefKey.NewNoteTitleFormat])
|
||||
this.preferences.getValue(PrefKey.NewNoteTitleFormat, PrefDefaults[PrefKey.NewNoteTitleFormat])
|
||||
|
||||
if (titleFormat === NewNoteTitleFormat.CurrentNoteCount) {
|
||||
return `Note ${this.notes.length + 1}`
|
||||
@@ -666,7 +772,7 @@ export class ItemListController extends AbstractViewController implements Intern
|
||||
if (titleFormat === NewNoteTitleFormat.CustomFormat) {
|
||||
const customFormat =
|
||||
this.navigationController.selected?.preferences?.customNoteTitleFormat ||
|
||||
this.application.getPreference(PrefKey.CustomNoteTitleFormat, PrefDefaults[PrefKey.CustomNoteTitleFormat])
|
||||
this.preferences.getValue(PrefKey.CustomNoteTitleFormat, PrefDefaults[PrefKey.CustomNoteTitleFormat])
|
||||
|
||||
try {
|
||||
return getDayjsFormattedString(createdAt, customFormat)
|
||||
@@ -684,7 +790,7 @@ export class ItemListController extends AbstractViewController implements Intern
|
||||
}
|
||||
|
||||
createNewNote = async (title?: string, createdAt?: Date, autofocusBehavior?: TemplateNoteViewAutofocusBehavior) => {
|
||||
this.notesController.unselectNotes()
|
||||
void this.publishCrossControllerEventSync(CrossControllerEvent.UnselectAllNotes)
|
||||
|
||||
if (this.navigationController.isInSmartView() && !this.navigationController.isInHomeView()) {
|
||||
await this.navigationController.selectHomeNavigationView()
|
||||
@@ -694,7 +800,7 @@ export class ItemListController extends AbstractViewController implements Intern
|
||||
|
||||
const controller = await this.createNewNoteController(useTitle, createdAt, autofocusBehavior)
|
||||
|
||||
this.selectionController.scrollToItem(controller.item)
|
||||
this.scrollToItem(controller.item)
|
||||
}
|
||||
|
||||
createPlaceholderNote = () => {
|
||||
@@ -725,7 +831,7 @@ export class ItemListController extends AbstractViewController implements Intern
|
||||
void this.reloadItems(ItemsReloadSource.Pagination)
|
||||
|
||||
if (this.searchSubmitted) {
|
||||
this.application.getDesktopService()?.searchText(this.noteFilterText)
|
||||
this.desktopManager?.searchText(this.noteFilterText)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -759,7 +865,7 @@ export class ItemListController extends AbstractViewController implements Intern
|
||||
if (item) {
|
||||
log(LoggingDomain.Selection, 'Selecting first item', item.uuid)
|
||||
|
||||
await this.selectionController.selectItemWithScrollHandling(item, {
|
||||
await this.selectItemWithScrollHandling(item, {
|
||||
userTriggered: false,
|
||||
scrollIntoView: false,
|
||||
})
|
||||
@@ -773,12 +879,10 @@ export class ItemListController extends AbstractViewController implements Intern
|
||||
|
||||
if (item) {
|
||||
log(LoggingDomain.Selection, 'selectNextItemOrCreateNewNote')
|
||||
await this.selectionController
|
||||
.selectItemWithScrollHandling(item, {
|
||||
userTriggered: false,
|
||||
scrollIntoView: false,
|
||||
})
|
||||
.catch(console.error)
|
||||
await this.selectItemWithScrollHandling(item, {
|
||||
userTriggered: false,
|
||||
scrollIntoView: false,
|
||||
}).catch(console.error)
|
||||
} else {
|
||||
await this.createNewNote()
|
||||
}
|
||||
@@ -794,18 +898,16 @@ export class ItemListController extends AbstractViewController implements Intern
|
||||
}
|
||||
|
||||
handleEditorChange = async () => {
|
||||
const activeNote = this.application.itemControllerGroup.activeItemViewController?.item
|
||||
const activeNote = this.itemControllerGroup.activeItemViewController?.item
|
||||
|
||||
if (activeNote && activeNote.conflictOf) {
|
||||
this.application
|
||||
.changeAndSaveItem(activeNote, (mutator) => {
|
||||
mutator.conflictOf = undefined
|
||||
})
|
||||
.catch(console.error)
|
||||
void this._changeAndSaveItem.execute(activeNote, (mutator) => {
|
||||
mutator.conflictOf = undefined
|
||||
})
|
||||
}
|
||||
|
||||
if (this.isFiltering) {
|
||||
this.application.getDesktopService()?.searchText(this.noteFilterText)
|
||||
this.desktopManager?.searchText(this.noteFilterText)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -818,7 +920,7 @@ export class ItemListController extends AbstractViewController implements Intern
|
||||
|
||||
private closeItemController(controller: NoteViewController | FileViewController): void {
|
||||
log(LoggingDomain.Selection, 'Closing item controller', controller.runtimeId)
|
||||
this.application.itemControllerGroup.closeItemController(controller)
|
||||
this.itemControllerGroup.closeItemController(controller)
|
||||
}
|
||||
|
||||
handleTagChange = async (userTriggered: boolean) => {
|
||||
@@ -833,7 +935,7 @@ export class ItemListController extends AbstractViewController implements Intern
|
||||
|
||||
this.setNoteFilterText('')
|
||||
|
||||
this.application.getDesktopService()?.searchText()
|
||||
this.desktopManager?.searchText()
|
||||
|
||||
this.resetPagination()
|
||||
|
||||
@@ -853,7 +955,7 @@ export class ItemListController extends AbstractViewController implements Intern
|
||||
*/
|
||||
this.searchSubmitted = true
|
||||
|
||||
this.application.getDesktopService()?.searchText(this.noteFilterText)
|
||||
this.desktopManager?.searchText(this.noteFilterText)
|
||||
}
|
||||
|
||||
get isCurrentNoteTemplate(): boolean {
|
||||
@@ -894,4 +996,292 @@ export class ItemListController extends AbstractViewController implements Intern
|
||||
this.handleFilterTextChanged()
|
||||
this.resetPagination()
|
||||
}
|
||||
|
||||
get selectedItemsCount(): number {
|
||||
return Object.keys(this.selectedItems).length
|
||||
}
|
||||
|
||||
get selectedFiles(): FileItem[] {
|
||||
return this.getFilteredSelectedItems<FileItem>(ContentType.TYPES.File)
|
||||
}
|
||||
|
||||
get selectedFilesCount(): number {
|
||||
return this.selectedFiles.length
|
||||
}
|
||||
|
||||
get firstSelectedItem() {
|
||||
return Object.values(this.selectedItems)[0]
|
||||
}
|
||||
|
||||
getSelectedItems = () => {
|
||||
const uuids = Array.from(this.selectedUuids)
|
||||
return uuids.map((uuid) => this.itemManager.findSureItem<SNNote | FileItem>(uuid)).filter((item) => !!item)
|
||||
}
|
||||
|
||||
getFilteredSelectedItems = <T extends ListableContentItem = ListableContentItem>(contentType?: string): T[] => {
|
||||
return Object.values(this.selectedItems).filter((item) => {
|
||||
return !contentType ? true : item.content_type === contentType
|
||||
}) as T[]
|
||||
}
|
||||
|
||||
setSelectedItems = () => {
|
||||
this.selectedItems = Object.fromEntries(this.getSelectedItems().map((item) => [item.uuid, item]))
|
||||
}
|
||||
|
||||
setSelectedUuids = (selectedUuids: Set<UuidString>) => {
|
||||
log(LoggingDomain.Selection, 'Setting selected uuids', selectedUuids)
|
||||
this.selectedUuids = new Set(selectedUuids)
|
||||
this.setSelectedItems()
|
||||
}
|
||||
|
||||
private removeSelectedItem = (uuid: UuidString) => {
|
||||
this.selectedUuids.delete(uuid)
|
||||
this.setSelectedUuids(this.selectedUuids)
|
||||
delete this.selectedItems[uuid]
|
||||
}
|
||||
|
||||
public deselectItem = (item: { uuid: ListableContentItem['uuid'] }): void => {
|
||||
log(LoggingDomain.Selection, 'Deselecting item', item.uuid)
|
||||
this.removeSelectedItem(item.uuid)
|
||||
|
||||
if (item.uuid === this.lastSelectedItem?.uuid) {
|
||||
this.lastSelectedItem = undefined
|
||||
}
|
||||
}
|
||||
|
||||
public isItemSelected = (item: ListableContentItem): boolean => {
|
||||
return this.selectedUuids.has(item.uuid)
|
||||
}
|
||||
|
||||
private selectItemsRange = async ({
|
||||
selectedItem,
|
||||
startingIndex,
|
||||
endingIndex,
|
||||
}: {
|
||||
selectedItem?: ListableContentItem
|
||||
startingIndex?: number
|
||||
endingIndex?: number
|
||||
}): Promise<void> => {
|
||||
const items = this.renderedItems
|
||||
|
||||
const lastSelectedItemIndex = startingIndex ?? items.findIndex((item) => item.uuid == this.lastSelectedItem?.uuid)
|
||||
const selectedItemIndex = endingIndex ?? items.findIndex((item) => item.uuid == selectedItem?.uuid)
|
||||
|
||||
let itemsToSelect = []
|
||||
if (selectedItemIndex > lastSelectedItemIndex) {
|
||||
itemsToSelect = items.slice(lastSelectedItemIndex, selectedItemIndex + 1)
|
||||
} else {
|
||||
itemsToSelect = items.slice(selectedItemIndex, lastSelectedItemIndex + 1)
|
||||
}
|
||||
|
||||
const authorizedItems = await this.protections.authorizeProtectedActionForItems(
|
||||
itemsToSelect,
|
||||
ChallengeReason.SelectProtectedNote,
|
||||
)
|
||||
|
||||
for (const item of authorizedItems) {
|
||||
runInAction(() => {
|
||||
this.setSelectedUuids(this.selectedUuids.add(item.uuid))
|
||||
this.lastSelectedItem = item
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
cancelMultipleSelection = () => {
|
||||
this.keyboardService.cancelAllKeyboardModifiers()
|
||||
|
||||
const firstSelectedItem = this.firstSelectedItem
|
||||
|
||||
if (firstSelectedItem) {
|
||||
this.replaceSelection(firstSelectedItem)
|
||||
} else {
|
||||
this.deselectAll()
|
||||
}
|
||||
}
|
||||
|
||||
private replaceSelection = (item: ListableContentItem): void => {
|
||||
this.deselectAll()
|
||||
runInAction(() => this.setSelectedUuids(this.selectedUuids.add(item.uuid)))
|
||||
|
||||
this.lastSelectedItem = item
|
||||
}
|
||||
|
||||
selectAll = () => {
|
||||
void this.selectItemsRange({
|
||||
startingIndex: 0,
|
||||
endingIndex: this.listLength - 1,
|
||||
})
|
||||
}
|
||||
|
||||
deselectAll = (): void => {
|
||||
this.selectedUuids.clear()
|
||||
this.setSelectedUuids(this.selectedUuids)
|
||||
|
||||
this.lastSelectedItem = undefined
|
||||
}
|
||||
|
||||
openSingleSelectedItem = async ({ userTriggered } = { userTriggered: true }) => {
|
||||
if (this.selectedItemsCount === 1) {
|
||||
const item = this.firstSelectedItem
|
||||
|
||||
if (item.content_type === ContentType.TYPES.Note) {
|
||||
await this.openNote(item.uuid)
|
||||
} else if (item.content_type === ContentType.TYPES.File) {
|
||||
await this.openFile(item.uuid)
|
||||
}
|
||||
|
||||
if (!this.paneController.isInMobileView || userTriggered) {
|
||||
void this.paneController.setPaneLayout(PaneLayout.Editing)
|
||||
}
|
||||
|
||||
if (this.paneController.isInMobileView && userTriggered) {
|
||||
requestCloseAllOpenModalsAndPopovers()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
selectItem = async (
|
||||
uuid: UuidString,
|
||||
userTriggered?: boolean,
|
||||
): Promise<{
|
||||
didSelect: boolean
|
||||
}> => {
|
||||
const item = this.itemManager.findItem<ListableContentItem>(uuid)
|
||||
|
||||
if (!item) {
|
||||
return {
|
||||
didSelect: false,
|
||||
}
|
||||
}
|
||||
|
||||
log(LoggingDomain.Selection, 'Select item', item.uuid)
|
||||
|
||||
const supportsMultipleSelection = this.options.allowMultipleSelection
|
||||
const hasMeta = this.keyboardService.activeModifiers.has(KeyboardModifier.Meta)
|
||||
const hasCtrl = this.keyboardService.activeModifiers.has(KeyboardModifier.Ctrl)
|
||||
const hasShift = this.keyboardService.activeModifiers.has(KeyboardModifier.Shift)
|
||||
const hasMoreThanOneSelected = this.selectedItemsCount > 1
|
||||
const isAuthorizedForAccess = await this.protections.authorizeItemAccess(item)
|
||||
|
||||
if (supportsMultipleSelection && userTriggered && (hasMeta || hasCtrl)) {
|
||||
if (this.selectedUuids.has(uuid) && hasMoreThanOneSelected) {
|
||||
this.removeSelectedItem(uuid)
|
||||
} else if (isAuthorizedForAccess) {
|
||||
this.selectedUuids.add(uuid)
|
||||
this.setSelectedUuids(this.selectedUuids)
|
||||
this.lastSelectedItem = item
|
||||
}
|
||||
} else if (supportsMultipleSelection && userTriggered && hasShift) {
|
||||
await this.selectItemsRange({ selectedItem: item })
|
||||
} else {
|
||||
const shouldSelectNote = hasMoreThanOneSelected || !this.selectedUuids.has(uuid)
|
||||
if (shouldSelectNote && isAuthorizedForAccess) {
|
||||
this.replaceSelection(item)
|
||||
}
|
||||
}
|
||||
|
||||
await this.openSingleSelectedItem({ userTriggered: userTriggered ?? false })
|
||||
|
||||
return {
|
||||
didSelect: this.selectedUuids.has(uuid),
|
||||
}
|
||||
}
|
||||
|
||||
selectItemWithScrollHandling = async (
|
||||
item: {
|
||||
uuid: ListableContentItem['uuid']
|
||||
},
|
||||
{ userTriggered = false, scrollIntoView = true, animated = true },
|
||||
): Promise<void> => {
|
||||
const { didSelect } = await this.selectItem(item.uuid, userTriggered)
|
||||
|
||||
const avoidMobileScrollingDueToIncompatibilityWithPaneAnimations = isMobileScreen()
|
||||
|
||||
if (didSelect && scrollIntoView && !avoidMobileScrollingDueToIncompatibilityWithPaneAnimations) {
|
||||
this.scrollToItem(item, animated)
|
||||
}
|
||||
}
|
||||
|
||||
scrollToItem = (item: { uuid: ListableContentItem['uuid'] }, animated = true): void => {
|
||||
const itemElement = document.getElementById(item.uuid)
|
||||
itemElement?.scrollIntoView({
|
||||
behavior: animated ? 'smooth' : 'auto',
|
||||
})
|
||||
}
|
||||
|
||||
selectUuids = async (uuids: UuidString[], userTriggered = false) => {
|
||||
const itemsForUuids = this.itemManager.findItems(uuids).filter((item) => !isFile(item))
|
||||
|
||||
if (itemsForUuids.length < 1) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!userTriggered && itemsForUuids.some((item) => item.protected && isFile(item))) {
|
||||
return
|
||||
}
|
||||
|
||||
this.setSelectedUuids(new Set(Uuids(itemsForUuids)))
|
||||
|
||||
if (itemsForUuids.length === 1) {
|
||||
void this.openSingleSelectedItem({ userTriggered })
|
||||
}
|
||||
}
|
||||
|
||||
selectNextItem = ({ userTriggered } = { userTriggered: true }) => {
|
||||
const displayableItems = this.items
|
||||
|
||||
const currentIndex = displayableItems.findIndex((candidate) => {
|
||||
return candidate.uuid === this.lastSelectedItem?.uuid
|
||||
})
|
||||
|
||||
let nextIndex = currentIndex + 1
|
||||
|
||||
while (nextIndex < displayableItems.length) {
|
||||
const nextItem = displayableItems[nextIndex]
|
||||
|
||||
nextIndex++
|
||||
|
||||
if (nextItem.protected) {
|
||||
continue
|
||||
}
|
||||
|
||||
this.selectItemWithScrollHandling(nextItem, { userTriggered }).catch(console.error)
|
||||
|
||||
const nextNoteElement = document.getElementById(nextItem.uuid)
|
||||
|
||||
nextNoteElement?.focus()
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
selectPreviousItem = () => {
|
||||
const displayableItems = this.items
|
||||
|
||||
if (!this.lastSelectedItem) {
|
||||
return
|
||||
}
|
||||
|
||||
const currentIndex = displayableItems.indexOf(this.lastSelectedItem)
|
||||
|
||||
let previousIndex = currentIndex - 1
|
||||
|
||||
while (previousIndex >= 0) {
|
||||
const previousItem = displayableItems[previousIndex]
|
||||
|
||||
previousIndex--
|
||||
|
||||
if (previousItem.protected) {
|
||||
continue
|
||||
}
|
||||
|
||||
this.selectItemWithScrollHandling(previousItem, { userTriggered: true }).catch(console.error)
|
||||
|
||||
const previousNoteElement = document.getElementById(previousItem.uuid)
|
||||
|
||||
previousNoteElement?.focus()
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,12 +14,12 @@ import {
|
||||
ItemInterface,
|
||||
InternalFeatureService,
|
||||
InternalFeature,
|
||||
PreferenceServiceInterface,
|
||||
} from '@standardnotes/snjs'
|
||||
import { FilesController } from './FilesController'
|
||||
import { ItemListController } from './ItemList/ItemListController'
|
||||
import { LinkingController } from './LinkingController'
|
||||
import { NavigationController } from './Navigation/NavigationController'
|
||||
import { SelectedItemsController } from './SelectedItemsController'
|
||||
import { SubscriptionController } from './Subscription/SubscriptionController'
|
||||
import { getLinkingSearchResults } from '@/Utils/Items/Search/getSearchResults'
|
||||
|
||||
@@ -46,44 +46,52 @@ const createFile = (name: string, options?: Partial<FileItem>) => {
|
||||
}
|
||||
|
||||
describe('LinkingController', () => {
|
||||
let linkingController: LinkingController
|
||||
let application: WebApplication
|
||||
let navigationController: NavigationController
|
||||
let selectionController: SelectedItemsController
|
||||
let eventBus: InternalEventBus
|
||||
|
||||
let itemListController: ItemListController
|
||||
let filesController: FilesController
|
||||
let subscriptionController: SubscriptionController
|
||||
|
||||
beforeEach(() => {
|
||||
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']>,
|
||||
preferences: {
|
||||
getValue: jest.fn().mockReturnValue(true),
|
||||
} as unknown as jest.Mocked<PreferenceServiceInterface>,
|
||||
itemControllerGroup: {} as jest.Mocked<WebApplication['itemControllerGroup']>,
|
||||
navigationController: {} as jest.Mocked<NavigationController>,
|
||||
itemListController: {} as jest.Mocked<ItemListController>,
|
||||
filesController: {} as jest.Mocked<FilesController>,
|
||||
subscriptionController: {} as jest.Mocked<SubscriptionController>,
|
||||
} as unknown as jest.Mocked<WebApplication>
|
||||
|
||||
application.getPreference = jest.fn()
|
||||
application.addSingleEventObserver = jest.fn()
|
||||
application.streamItems = jest.fn()
|
||||
application.sync.sync = jest.fn()
|
||||
|
||||
Object.defineProperty(application, 'items', { value: {} as jest.Mocked<ItemManagerInterface> })
|
||||
|
||||
navigationController = {} as jest.Mocked<NavigationController>
|
||||
|
||||
selectionController = {} as jest.Mocked<SelectedItemsController>
|
||||
|
||||
eventBus = {} as jest.Mocked<InternalEventBus>
|
||||
eventBus.addEventHandler = jest.fn()
|
||||
|
||||
itemListController = {} as jest.Mocked<ItemListController>
|
||||
filesController = {} as jest.Mocked<FilesController>
|
||||
subscriptionController = {} as jest.Mocked<SubscriptionController>
|
||||
|
||||
linkingController = new LinkingController(application, navigationController, selectionController, eventBus)
|
||||
linkingController.setServicesPostConstruction(itemListController, filesController, subscriptionController)
|
||||
Object.defineProperty(application, 'linkingController', {
|
||||
get: () =>
|
||||
new LinkingController(
|
||||
application.itemListController,
|
||||
application.filesController,
|
||||
application.subscriptionController,
|
||||
application.navigationController,
|
||||
application.itemControllerGroup,
|
||||
application.vaultDisplayService,
|
||||
application.preferences,
|
||||
application.items,
|
||||
application.mutator,
|
||||
application.sync,
|
||||
application.vaults,
|
||||
eventBus,
|
||||
),
|
||||
configurable: true,
|
||||
})
|
||||
})
|
||||
|
||||
describe('isValidSearchResult', () => {
|
||||
@@ -257,7 +265,7 @@ describe('LinkingController', () => {
|
||||
return undefined
|
||||
})
|
||||
|
||||
await linkingController.linkItems(note, file)
|
||||
await application.linkingController.linkItems(note, file)
|
||||
|
||||
expect(moveToVaultSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -17,6 +17,13 @@ import {
|
||||
InternalEventBusInterface,
|
||||
isTag,
|
||||
PrefDefaults,
|
||||
PreferenceServiceInterface,
|
||||
InternalEventHandlerInterface,
|
||||
InternalEventInterface,
|
||||
ItemManagerInterface,
|
||||
MutatorClientInterface,
|
||||
SyncServiceInterface,
|
||||
VaultServiceInterface,
|
||||
} from '@standardnotes/snjs'
|
||||
import { action, computed, makeObservable, observable } from 'mobx'
|
||||
import { AbstractViewController } from './Abstract/AbstractViewController'
|
||||
@@ -24,25 +31,30 @@ import { CrossControllerEvent } from './CrossControllerEvent'
|
||||
import { FilesController } from './FilesController'
|
||||
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'
|
||||
import { ItemGroupController } from '@/Components/NoteView/Controller/ItemGroupController'
|
||||
import { VaultDisplayServiceInterface } from '@standardnotes/ui-services'
|
||||
|
||||
export class LinkingController extends AbstractViewController {
|
||||
export class LinkingController extends AbstractViewController implements InternalEventHandlerInterface {
|
||||
shouldLinkToParentFolders: boolean
|
||||
isLinkingPanelOpen = false
|
||||
private itemListController!: ItemListController
|
||||
private filesController!: FilesController
|
||||
private subscriptionController!: SubscriptionController
|
||||
|
||||
constructor(
|
||||
application: WebApplication,
|
||||
private itemListController: ItemListController,
|
||||
private filesController: FilesController,
|
||||
private subscriptionController: SubscriptionController,
|
||||
private navigationController: NavigationController,
|
||||
private selectionController: SelectedItemsController,
|
||||
private itemControllerGroup: ItemGroupController,
|
||||
private vaultDisplayService: VaultDisplayServiceInterface,
|
||||
private preferences: PreferenceServiceInterface,
|
||||
private items: ItemManagerInterface,
|
||||
private mutator: MutatorClientInterface,
|
||||
private sync: SyncServiceInterface,
|
||||
private vaults: VaultServiceInterface,
|
||||
eventBus: InternalEventBusInterface,
|
||||
) {
|
||||
super(application, eventBus)
|
||||
super(eventBus)
|
||||
|
||||
makeObservable(this, {
|
||||
isLinkingPanelOpen: observable,
|
||||
@@ -52,29 +64,26 @@ export class LinkingController extends AbstractViewController {
|
||||
setIsLinkingPanelOpen: action,
|
||||
})
|
||||
|
||||
this.shouldLinkToParentFolders = application.getPreference(
|
||||
this.shouldLinkToParentFolders = preferences.getValue(
|
||||
PrefKey.NoteAddToParentFolders,
|
||||
PrefDefaults[PrefKey.NoteAddToParentFolders],
|
||||
)
|
||||
|
||||
this.disposers.push(
|
||||
this.application.addSingleEventObserver(ApplicationEvent.PreferencesChanged, async () => {
|
||||
this.shouldLinkToParentFolders = this.application.getPreference(
|
||||
eventBus.addEventHandler(this, ApplicationEvent.PreferencesChanged)
|
||||
}
|
||||
|
||||
handleEvent(event: InternalEventInterface): Promise<void> {
|
||||
switch (event.type) {
|
||||
case ApplicationEvent.PreferencesChanged: {
|
||||
this.shouldLinkToParentFolders = this.preferences.getValue(
|
||||
PrefKey.NoteAddToParentFolders,
|
||||
PrefDefaults[PrefKey.NoteAddToParentFolders],
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public setServicesPostConstruction(
|
||||
itemListController: ItemListController,
|
||||
filesController: FilesController,
|
||||
subscriptionController: SubscriptionController,
|
||||
) {
|
||||
this.itemListController = itemListController
|
||||
this.filesController = filesController
|
||||
this.subscriptionController = subscriptionController
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
get isEntitledToNoteLinking() {
|
||||
@@ -86,20 +95,20 @@ export class LinkingController extends AbstractViewController {
|
||||
}
|
||||
|
||||
get activeItem() {
|
||||
return this.application.itemControllerGroup.activeItemViewController?.item
|
||||
return this.itemControllerGroup.activeItemViewController?.item
|
||||
}
|
||||
|
||||
getFilesLinksForItem = (item: LinkableItem | undefined) => {
|
||||
if (!item || this.application.items.isTemplateItem(item)) {
|
||||
if (!item || this.items.isTemplateItem(item)) {
|
||||
return {
|
||||
filesLinkedToItem: [],
|
||||
filesLinkingToItem: [],
|
||||
}
|
||||
}
|
||||
|
||||
const referencesOfItem = naturalSort(this.application.items.referencesForItem(item).filter(isFile), 'title')
|
||||
const referencesOfItem = naturalSort(this.items.referencesForItem(item).filter(isFile), 'title')
|
||||
|
||||
const referencingItem = naturalSort(this.application.items.itemsReferencingItem(item).filter(isFile), 'title')
|
||||
const referencingItem = naturalSort(this.items.itemsReferencingItem(item).filter(isFile), 'title')
|
||||
|
||||
if (item.content_type === ContentType.TYPES.File) {
|
||||
return {
|
||||
@@ -119,15 +128,15 @@ export class LinkingController extends AbstractViewController {
|
||||
return
|
||||
}
|
||||
|
||||
return this.application.items.getSortedTagsForItem(item).map((tag) => createLinkFromItem(tag, 'linked'))
|
||||
return this.items.getSortedTagsForItem(item).map((tag) => createLinkFromItem(tag, 'linked'))
|
||||
}
|
||||
|
||||
getLinkedNotesForItem = (item: LinkableItem | undefined) => {
|
||||
if (!item || this.application.items.isTemplateItem(item)) {
|
||||
if (!item || this.items.isTemplateItem(item)) {
|
||||
return []
|
||||
}
|
||||
|
||||
return naturalSort(this.application.items.referencesForItem(item).filter(isNote), 'title').map((item) =>
|
||||
return naturalSort(this.items.referencesForItem(item).filter(isNote), 'title').map((item) =>
|
||||
createLinkFromItem(item, 'linked'),
|
||||
)
|
||||
}
|
||||
@@ -137,7 +146,7 @@ export class LinkingController extends AbstractViewController {
|
||||
return []
|
||||
}
|
||||
|
||||
return naturalSort(this.application.items.itemsReferencingItem(item).filter(isNote), 'title').map((item) =>
|
||||
return naturalSort(this.items.itemsReferencingItem(item).filter(isNote), 'title').map((item) =>
|
||||
createLinkFromItem(item, 'linked-by'),
|
||||
)
|
||||
}
|
||||
@@ -150,7 +159,7 @@ export class LinkingController extends AbstractViewController {
|
||||
return AppPaneId.Items
|
||||
} else if (item instanceof SNNote) {
|
||||
await this.navigationController.selectHomeNavigationView()
|
||||
const { didSelect } = await this.selectionController.selectItem(item.uuid, true)
|
||||
const { didSelect } = await this.itemListController.selectItem(item.uuid, true)
|
||||
if (didSelect) {
|
||||
return AppPaneId.Editor
|
||||
}
|
||||
@@ -169,16 +178,16 @@ export class LinkingController extends AbstractViewController {
|
||||
|
||||
unlinkItems = async (item: LinkableItem, itemToUnlink: LinkableItem) => {
|
||||
try {
|
||||
await this.application.mutator.unlinkItems(item, itemToUnlink)
|
||||
await this.mutator.unlinkItems(item, itemToUnlink)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
void this.application.sync.sync()
|
||||
void this.sync.sync()
|
||||
}
|
||||
|
||||
unlinkItemFromSelectedItem = async (itemToUnlink: LinkableItem) => {
|
||||
const selectedItem = this.selectionController.firstSelectedItem
|
||||
const selectedItem = this.itemListController.firstSelectedItem
|
||||
|
||||
if (!selectedItem) {
|
||||
return
|
||||
@@ -196,25 +205,25 @@ export class LinkingController extends AbstractViewController {
|
||||
|
||||
linkItems = async (item: LinkableItem, itemToLink: LinkableItem) => {
|
||||
const linkNoteAndFile = async (note: SNNote, file: FileItem) => {
|
||||
const updatedFile = await this.application.mutator.associateFileWithNote(file, note)
|
||||
const updatedFile = await this.mutator.associateFileWithNote(file, note)
|
||||
|
||||
if (featureTrunkVaultsEnabled()) {
|
||||
if (updatedFile) {
|
||||
const noteVault = this.application.vaults.getItemVault(note)
|
||||
const fileVault = this.application.vaults.getItemVault(updatedFile)
|
||||
const noteVault = this.vaults.getItemVault(note)
|
||||
const fileVault = this.vaults.getItemVault(updatedFile)
|
||||
if (noteVault && !fileVault) {
|
||||
await this.application.vaults.moveItemToVault(noteVault, file)
|
||||
await this.vaults.moveItemToVault(noteVault, file)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const linkFileAndFile = async (file1: FileItem, file2: FileItem) => {
|
||||
await this.application.mutator.linkFileToFile(file1, file2)
|
||||
await this.mutator.linkFileToFile(file1, file2)
|
||||
}
|
||||
|
||||
const linkNoteToNote = async (note1: SNNote, note2: SNNote) => {
|
||||
await this.application.mutator.linkNoteToNote(note1, note2)
|
||||
await this.mutator.linkNoteToNote(note1, note2)
|
||||
}
|
||||
|
||||
const linkTagToNote = async (tag: SNTag, note: SNNote) => {
|
||||
@@ -260,7 +269,7 @@ export class LinkingController extends AbstractViewController {
|
||||
throw new Error('First item must be a note or file')
|
||||
}
|
||||
|
||||
void this.application.sync.sync()
|
||||
void this.sync.sync()
|
||||
}
|
||||
|
||||
linkItemToSelectedItem = async (itemToLink: LinkableItem): Promise<boolean> => {
|
||||
@@ -286,9 +295,9 @@ export class LinkingController extends AbstractViewController {
|
||||
createAndAddNewTag = async (title: string): Promise<SNTag> => {
|
||||
await this.ensureActiveItemIsInserted()
|
||||
|
||||
const vault = this.application.vaultDisplayService.exclusivelyShownVault
|
||||
const vault = this.vaultDisplayService.exclusivelyShownVault
|
||||
|
||||
const newTag = await this.application.mutator.findOrCreateTag(title, vault)
|
||||
const newTag = await this.mutator.findOrCreateTag(title, vault)
|
||||
|
||||
const activeItem = this.activeItem
|
||||
if (activeItem) {
|
||||
@@ -300,11 +309,11 @@ export class LinkingController extends AbstractViewController {
|
||||
|
||||
addTagToItem = async (tag: SNTag, item: FileItem | SNNote) => {
|
||||
if (item instanceof SNNote) {
|
||||
await this.application.mutator.addTagToNote(item, tag, this.shouldLinkToParentFolders)
|
||||
await this.mutator.addTagToNote(item, tag, this.shouldLinkToParentFolders)
|
||||
} else if (item instanceof FileItem) {
|
||||
await this.application.mutator.addTagToFile(item, tag, this.shouldLinkToParentFolders)
|
||||
await this.mutator.addTagToFile(item, tag, this.shouldLinkToParentFolders)
|
||||
}
|
||||
|
||||
this.application.sync.sync().catch(console.error)
|
||||
this.sync.sync().catch(console.error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,26 @@
|
||||
import { addToast, dismissToast, ToastType } from '@standardnotes/toast'
|
||||
import { ApplicationEvent, InternalEventBusInterface, StorageKey } from '@standardnotes/services'
|
||||
import {
|
||||
ApplicationEvent,
|
||||
DesktopDeviceInterface,
|
||||
InternalEventBusInterface,
|
||||
InternalEventHandlerInterface,
|
||||
InternalEventInterface,
|
||||
ItemManagerInterface,
|
||||
PreferenceServiceInterface,
|
||||
ProtectionEvent,
|
||||
ProtectionsClientInterface,
|
||||
StorageKey,
|
||||
StorageServiceInterface,
|
||||
} from '@standardnotes/services'
|
||||
import { isDev } from '@/Utils'
|
||||
import { FileItem, PrefKey, sleep, SNTag } from '@standardnotes/snjs'
|
||||
import { FilesController } from '../FilesController'
|
||||
import { action, makeObservable, observable } from 'mobx'
|
||||
import { AbstractViewController } from '@/Controllers/Abstract/AbstractViewController'
|
||||
import { WebApplication } from '@/Application/WebApplication'
|
||||
import { dateToStringStyle1 } from '@/Utils/DateUtils'
|
||||
import { PhotoRecorder } from './PhotoRecorder'
|
||||
import { LinkingController } from '../LinkingController'
|
||||
import { IsMobileDevice } from '@standardnotes/ui-services'
|
||||
|
||||
const EVERY_HOUR = 1000 * 60 * 60
|
||||
const EVERY_TEN_SECONDS = 1000 * 10
|
||||
@@ -15,33 +28,22 @@ const DEBUG_MODE = isDev && false
|
||||
|
||||
const DELAY_AFTER_STARTING_CAMERA_TO_ALLOW_MOBILE_AUTOFOCUS = 2000
|
||||
|
||||
export class MomentsService extends AbstractViewController {
|
||||
export class MomentsService extends AbstractViewController implements InternalEventHandlerInterface {
|
||||
isEnabled = false
|
||||
private intervalReference: ReturnType<typeof setInterval> | undefined
|
||||
|
||||
constructor(
|
||||
application: WebApplication,
|
||||
private filesController: FilesController,
|
||||
private linkingController: LinkingController,
|
||||
private storage: StorageServiceInterface,
|
||||
private preferences: PreferenceServiceInterface,
|
||||
private items: ItemManagerInterface,
|
||||
private protections: ProtectionsClientInterface,
|
||||
private desktopDevice: DesktopDeviceInterface | undefined,
|
||||
private _isMobileDevice: IsMobileDevice,
|
||||
eventBus: InternalEventBusInterface,
|
||||
) {
|
||||
super(application, eventBus)
|
||||
|
||||
this.disposers.push(
|
||||
application.addEventObserver(async () => {
|
||||
this.isEnabled = (this.application.getValue(StorageKey.MomentsEnabled) as boolean) ?? false
|
||||
if (this.isEnabled) {
|
||||
void this.beginTakingPhotos()
|
||||
}
|
||||
}, ApplicationEvent.LocalDataLoaded),
|
||||
|
||||
application.addEventObserver(async () => {
|
||||
this.pauseMoments()
|
||||
}, ApplicationEvent.BiometricsSoftLockEngaged),
|
||||
|
||||
application.addEventObserver(async () => {
|
||||
this.resumeMoments()
|
||||
}, ApplicationEvent.BiometricsSoftLockDisengaged),
|
||||
)
|
||||
super(eventBus)
|
||||
|
||||
makeObservable(this, {
|
||||
isEnabled: observable,
|
||||
@@ -49,16 +51,41 @@ export class MomentsService extends AbstractViewController {
|
||||
enableMoments: action,
|
||||
disableMoments: action,
|
||||
})
|
||||
|
||||
eventBus.addEventHandler(this, ApplicationEvent.LocalDataLoaded)
|
||||
eventBus.addEventHandler(this, ProtectionEvent.BiometricsSoftLockEngaged)
|
||||
eventBus.addEventHandler(this, ProtectionEvent.BiometricsSoftLockDisengaged)
|
||||
}
|
||||
|
||||
async handleEvent(event: InternalEventInterface): Promise<void> {
|
||||
switch (event.type) {
|
||||
case ApplicationEvent.LocalDataLoaded: {
|
||||
this.isEnabled = (this.storage.getValue(StorageKey.MomentsEnabled) as boolean) ?? false
|
||||
if (this.isEnabled) {
|
||||
void this.beginTakingPhotos()
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case ProtectionEvent.BiometricsSoftLockEngaged: {
|
||||
this.pauseMoments()
|
||||
break
|
||||
}
|
||||
|
||||
case ProtectionEvent.BiometricsSoftLockDisengaged: {
|
||||
this.resumeMoments()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override deinit() {
|
||||
super.deinit()
|
||||
;(this.application as unknown) = undefined
|
||||
;(this.filesController as unknown) = undefined
|
||||
}
|
||||
|
||||
public enableMoments = (): void => {
|
||||
this.application.setValue(StorageKey.MomentsEnabled, true)
|
||||
this.storage.setValue(StorageKey.MomentsEnabled, true)
|
||||
|
||||
this.isEnabled = true
|
||||
|
||||
@@ -66,7 +93,7 @@ export class MomentsService extends AbstractViewController {
|
||||
}
|
||||
|
||||
public disableMoments = (): void => {
|
||||
this.application.setValue(StorageKey.MomentsEnabled, false)
|
||||
this.storage.setValue(StorageKey.MomentsEnabled, false)
|
||||
|
||||
this.isEnabled = false
|
||||
|
||||
@@ -101,15 +128,15 @@ export class MomentsService extends AbstractViewController {
|
||||
}
|
||||
|
||||
private getDefaultTag(): SNTag | undefined {
|
||||
const defaultTagId = this.application.getPreference(PrefKey.MomentsDefaultTagUuid)
|
||||
const defaultTagId = this.preferences.getValue(PrefKey.MomentsDefaultTagUuid)
|
||||
|
||||
if (defaultTagId) {
|
||||
return this.application.items.findItem(defaultTagId)
|
||||
return this.items.findItem(defaultTagId)
|
||||
}
|
||||
}
|
||||
|
||||
public takePhoto = async (): Promise<FileItem | undefined> => {
|
||||
const isAppLocked = await this.application.isLocked()
|
||||
const isAppLocked = await this.protections.isLocked()
|
||||
|
||||
if (isAppLocked) {
|
||||
return
|
||||
@@ -127,8 +154,8 @@ export class MomentsService extends AbstractViewController {
|
||||
})
|
||||
}
|
||||
|
||||
if (this.application.desktopDevice) {
|
||||
const granted = await this.application.desktopDevice.askForMediaAccess('camera')
|
||||
if (this.desktopDevice) {
|
||||
const granted = await this.desktopDevice.askForMediaAccess('camera')
|
||||
if (!granted) {
|
||||
if (toastId) {
|
||||
dismissToast(toastId)
|
||||
@@ -147,7 +174,7 @@ export class MomentsService extends AbstractViewController {
|
||||
const camera = new PhotoRecorder()
|
||||
await camera.initialize()
|
||||
|
||||
if (this.application.isMobileDevice) {
|
||||
if (this._isMobileDevice.execute().getValue()) {
|
||||
await sleep(DELAY_AFTER_STARTING_CAMERA_TO_ALLOW_MOBILE_AUTOFOCUS)
|
||||
}
|
||||
|
||||
@@ -168,12 +195,12 @@ export class MomentsService extends AbstractViewController {
|
||||
|
||||
if (uploadedFile) {
|
||||
if (isAppInForeground) {
|
||||
void this.application.linkingController.linkItemToSelectedItem(uploadedFile)
|
||||
void this.linkingController.linkItemToSelectedItem(uploadedFile)
|
||||
}
|
||||
|
||||
const defaultTag = this.getDefaultTag()
|
||||
if (defaultTag) {
|
||||
void this.application.linkingController.linkItems(uploadedFile, defaultTag)
|
||||
void this.linkingController.linkItems(uploadedFile, defaultTag)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import {
|
||||
confirmDialog,
|
||||
CREATE_NEW_TAG_COMMAND,
|
||||
KeyboardService,
|
||||
NavigationControllerPersistableValue,
|
||||
VaultDisplayService,
|
||||
VaultDisplayServiceEvent,
|
||||
} from '@standardnotes/ui-services'
|
||||
import { STRING_DELETE_TAG } from '@/Constants/Strings'
|
||||
@@ -22,9 +24,14 @@ import {
|
||||
InternalEventBusInterface,
|
||||
InternalEventHandlerInterface,
|
||||
InternalEventInterface,
|
||||
ItemManagerInterface,
|
||||
SyncServiceInterface,
|
||||
MutatorClientInterface,
|
||||
AlertService,
|
||||
PreferenceServiceInterface,
|
||||
ChangeAndSaveItem,
|
||||
} from '@standardnotes/snjs'
|
||||
import { action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'
|
||||
import { WebApplication } from '../../Application/WebApplication'
|
||||
import { FeaturesController } from '../FeaturesController'
|
||||
import { destroyAllObjectProperties } from '@/Utils'
|
||||
import { isValidFutureSiblings, rootTags, tagSiblings } from './Utils'
|
||||
@@ -35,6 +42,7 @@ import { Persistable } from '../Abstract/Persistable'
|
||||
import { TagListSectionType } from '@/Components/Tags/TagListSection'
|
||||
import { PaneLayout } from '../PaneController/PaneLayout'
|
||||
import { TagsCountsState } from './TagsCountsState'
|
||||
import { PaneController } from '../PaneController/PaneController'
|
||||
|
||||
export class NavigationController
|
||||
extends AbstractViewController
|
||||
@@ -63,16 +71,24 @@ export class NavigationController
|
||||
private readonly tagsCountsState: TagsCountsState
|
||||
|
||||
constructor(
|
||||
application: WebApplication,
|
||||
private featuresController: FeaturesController,
|
||||
private vaultDisplayService: VaultDisplayService,
|
||||
private keyboardService: KeyboardService,
|
||||
private paneController: PaneController,
|
||||
private sync: SyncServiceInterface,
|
||||
private mutator: MutatorClientInterface,
|
||||
private items: ItemManagerInterface,
|
||||
private preferences: PreferenceServiceInterface,
|
||||
private alerts: AlertService,
|
||||
private _changeAndSaveItem: ChangeAndSaveItem,
|
||||
eventBus: InternalEventBusInterface,
|
||||
) {
|
||||
super(application, eventBus)
|
||||
super(eventBus)
|
||||
|
||||
eventBus.addEventHandler(this, VaultDisplayServiceEvent.VaultDisplayOptionsChanged)
|
||||
|
||||
this.tagsCountsState = new TagsCountsState(this.application)
|
||||
this.smartViews = this.application.items.getSmartViews()
|
||||
this.tagsCountsState = new TagsCountsState(items)
|
||||
this.smartViews = items.getSmartViews()
|
||||
|
||||
makeObservable(this, {
|
||||
tags: observable,
|
||||
@@ -122,7 +138,7 @@ export class NavigationController
|
||||
})
|
||||
|
||||
this.disposers.push(
|
||||
this.application.streamItems([ContentType.TYPES.Tag, ContentType.TYPES.SmartView], ({ changed, removed }) => {
|
||||
this.items.streamItems([ContentType.TYPES.Tag, ContentType.TYPES.SmartView], ({ changed, removed }) => {
|
||||
this.reloadTags()
|
||||
|
||||
runInAction(() => {
|
||||
@@ -150,12 +166,12 @@ export class NavigationController
|
||||
)
|
||||
|
||||
this.disposers.push(
|
||||
this.application.items.addNoteCountChangeObserver((tagUuid) => {
|
||||
this.items.addNoteCountChangeObserver((tagUuid) => {
|
||||
if (!tagUuid) {
|
||||
this.setAllNotesCount(this.application.items.allCountableNotesCount())
|
||||
this.setAllFilesCount(this.application.items.allCountableFilesCount())
|
||||
this.setAllNotesCount(this.items.allCountableNotesCount())
|
||||
this.setAllFilesCount(this.items.allCountableFilesCount())
|
||||
} else {
|
||||
const tag = this.application.items.findItem<SNTag>(tagUuid)
|
||||
const tag = this.items.findItem<SNTag>(tagUuid)
|
||||
if (tag) {
|
||||
this.tagsCountsState.update([tag])
|
||||
}
|
||||
@@ -176,7 +192,7 @@ export class NavigationController
|
||||
)
|
||||
|
||||
this.disposers.push(
|
||||
this.application.keyboardService.addCommandHandler({
|
||||
this.keyboardService.addCommandHandler({
|
||||
command: CREATE_NEW_TAG_COMMAND,
|
||||
onKeyDown: () => {
|
||||
this.createNewTemplate()
|
||||
@@ -187,9 +203,9 @@ export class NavigationController
|
||||
|
||||
private reloadTags(): void {
|
||||
runInAction(() => {
|
||||
this.tags = this.application.items.getDisplayableTags()
|
||||
this.tags = this.items.getDisplayableTags()
|
||||
this.starredTags = this.tags.filter((tag) => tag.starred)
|
||||
this.smartViews = this.application.items.getSmartViews()
|
||||
this.smartViews = this.items.getSmartViews()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -258,14 +274,14 @@ export class NavigationController
|
||||
return
|
||||
}
|
||||
|
||||
const createdTag = await this.application.mutator.createTagOrSmartView<SNTag>(
|
||||
const createdTag = await this.mutator.createTagOrSmartView<SNTag>(
|
||||
title,
|
||||
this.application.vaultDisplayService.exclusivelyShownVault,
|
||||
this.vaultDisplayService.exclusivelyShownVault,
|
||||
)
|
||||
|
||||
const futureSiblings = this.application.items.getTagChildren(parent)
|
||||
const futureSiblings = this.items.getTagChildren(parent)
|
||||
|
||||
if (!isValidFutureSiblings(this.application, futureSiblings, createdTag)) {
|
||||
if (!isValidFutureSiblings(this.alerts, futureSiblings, createdTag)) {
|
||||
this.setAddingSubtagTo(undefined)
|
||||
this.remove(createdTag, false).catch(console.error)
|
||||
return
|
||||
@@ -273,7 +289,7 @@ export class NavigationController
|
||||
|
||||
this.assignParent(createdTag.uuid, parent.uuid).catch(console.error)
|
||||
|
||||
this.application.sync.sync().catch(console.error)
|
||||
this.sync.sync().catch(console.error)
|
||||
|
||||
runInAction(() => {
|
||||
void this.setSelectedTag(createdTag as SNTag, 'all')
|
||||
@@ -301,7 +317,7 @@ export class NavigationController
|
||||
tagUsesTableView(tag: AnyTag): boolean {
|
||||
const isSystemView = tag instanceof SmartView && Object.values(SystemViewId).includes(tag.uuid as SystemViewId)
|
||||
const useTableView = isSystemView
|
||||
? this.application.getPreference(PrefKey.SystemViewPreferences)?.[tag.uuid as SystemViewId]
|
||||
? this.preferences.getValue(PrefKey.SystemViewPreferences)?.[tag.uuid as SystemViewId]
|
||||
: tag?.preferences
|
||||
return Boolean(useTableView)
|
||||
}
|
||||
@@ -390,7 +406,7 @@ export class NavigationController
|
||||
}
|
||||
|
||||
public get allLocalRootTags(): SNTag[] {
|
||||
if (this.editing_ instanceof SNTag && this.application.items.isTemplateItem(this.editing_)) {
|
||||
if (this.editing_ instanceof SNTag && this.items.isTemplateItem(this.editing_)) {
|
||||
return [this.editing_, ...this.rootTags]
|
||||
}
|
||||
return this.rootTags
|
||||
@@ -401,11 +417,11 @@ export class NavigationController
|
||||
}
|
||||
|
||||
getChildren(tag: SNTag): SNTag[] {
|
||||
if (this.application.items.isTemplateItem(tag)) {
|
||||
if (this.items.isTemplateItem(tag)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const children = this.application.items.getTagChildren(tag)
|
||||
const children = this.items.getTagChildren(tag)
|
||||
|
||||
const childrenUuids = children.map((childTag) => childTag.uuid)
|
||||
const childrenTags = this.tags.filter((tag) => childrenUuids.includes(tag.uuid))
|
||||
@@ -413,45 +429,45 @@ export class NavigationController
|
||||
}
|
||||
|
||||
isValidTagParent(parent: SNTag, tag: SNTag): boolean {
|
||||
return this.application.items.isValidTagParent(parent, tag)
|
||||
return this.items.isValidTagParent(parent, tag)
|
||||
}
|
||||
|
||||
public hasParent(tagUuid: UuidString): boolean {
|
||||
const item = this.application.items.findItem(tagUuid)
|
||||
const item = this.items.findItem(tagUuid)
|
||||
return !!item && !!(item as SNTag).parentId
|
||||
}
|
||||
|
||||
public async assignParent(tagUuid: string, futureParentUuid: string | undefined): Promise<void> {
|
||||
const tag = this.application.items.findItem(tagUuid) as SNTag
|
||||
const tag = this.items.findItem(tagUuid) as SNTag
|
||||
|
||||
const currentParent = this.application.items.getTagParent(tag)
|
||||
const currentParent = this.items.getTagParent(tag)
|
||||
const currentParentUuid = currentParent?.uuid
|
||||
|
||||
if (currentParentUuid === futureParentUuid) {
|
||||
return
|
||||
}
|
||||
|
||||
const futureParent = futureParentUuid && (this.application.items.findItem(futureParentUuid) as SNTag)
|
||||
const futureParent = futureParentUuid && (this.items.findItem(futureParentUuid) as SNTag)
|
||||
|
||||
if (!futureParent) {
|
||||
const futureSiblings = rootTags(this.application)
|
||||
if (!isValidFutureSiblings(this.application, futureSiblings, tag)) {
|
||||
const futureSiblings = rootTags(this.items)
|
||||
if (!isValidFutureSiblings(this.alerts, futureSiblings, tag)) {
|
||||
return
|
||||
}
|
||||
await this.application.mutator.unsetTagParent(tag)
|
||||
await this.mutator.unsetTagParent(tag)
|
||||
} else {
|
||||
const futureSiblings = this.application.items.getTagChildren(futureParent)
|
||||
if (!isValidFutureSiblings(this.application, futureSiblings, tag)) {
|
||||
const futureSiblings = this.items.getTagChildren(futureParent)
|
||||
if (!isValidFutureSiblings(this.alerts, futureSiblings, tag)) {
|
||||
return
|
||||
}
|
||||
await this.application.mutator.setTagParent(futureParent, tag)
|
||||
await this.mutator.setTagParent(futureParent, tag)
|
||||
}
|
||||
|
||||
await this.application.sync.sync()
|
||||
await this.sync.sync()
|
||||
}
|
||||
|
||||
get rootTags(): SNTag[] {
|
||||
return this.tags.filter((tag) => !this.application.items.getTagParent(tag))
|
||||
return this.tags.filter((tag) => !this.items.getTagParent(tag))
|
||||
}
|
||||
|
||||
get tagsCount(): number {
|
||||
@@ -483,7 +499,7 @@ export class NavigationController
|
||||
}
|
||||
|
||||
public async setPanelWidthForTag(tag: SNTag, width: number): Promise<void> {
|
||||
await this.application.changeAndSaveItem<TagMutator>(tag, (mutator) => {
|
||||
await this._changeAndSaveItem.execute<TagMutator>(tag, (mutator) => {
|
||||
mutator.preferences = {
|
||||
...mutator.preferences,
|
||||
panelWidth: width,
|
||||
@@ -497,17 +513,17 @@ export class NavigationController
|
||||
{ userTriggered } = { userTriggered: false },
|
||||
) {
|
||||
if (tag && tag.conflictOf) {
|
||||
this.application
|
||||
.changeAndSaveItem(tag, (mutator) => {
|
||||
this._changeAndSaveItem
|
||||
.execute(tag, (mutator) => {
|
||||
mutator.conflictOf = undefined
|
||||
})
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
if (tag && (this.isTagFilesView(tag) || this.tagUsesTableView(tag))) {
|
||||
this.application.paneController.setPaneLayout(PaneLayout.TableView)
|
||||
this.paneController.setPaneLayout(PaneLayout.TableView)
|
||||
} else if (userTriggered) {
|
||||
this.application.paneController.setPaneLayout(PaneLayout.ItemSelection)
|
||||
this.paneController.setPaneLayout(PaneLayout.ItemSelection)
|
||||
}
|
||||
|
||||
this.previouslySelected_ = this.selected_
|
||||
@@ -516,7 +532,7 @@ export class NavigationController
|
||||
this.setSelectedTagInstance(tag)
|
||||
this.selectedLocation = location
|
||||
|
||||
if (tag && this.application.items.isTemplateItem(tag)) {
|
||||
if (tag && this.items.isTemplateItem(tag)) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -558,24 +574,24 @@ export class NavigationController
|
||||
return
|
||||
}
|
||||
|
||||
this.application
|
||||
.changeAndSaveItem<TagMutator>(tag, (mutator) => {
|
||||
this._changeAndSaveItem
|
||||
.execute<TagMutator>(tag, (mutator) => {
|
||||
mutator.expanded = expanded
|
||||
})
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
public async setFavorite(tag: SNTag, favorite: boolean) {
|
||||
return this.application
|
||||
.changeAndSaveItem<TagMutator>(tag, (mutator) => {
|
||||
return this._changeAndSaveItem
|
||||
.execute<TagMutator>(tag, (mutator) => {
|
||||
mutator.starred = favorite
|
||||
})
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
public setIcon(tag: SNTag, icon: VectorIconNameOrEmoji) {
|
||||
this.application
|
||||
.changeAndSaveItem<TagMutator>(tag, (mutator) => {
|
||||
this._changeAndSaveItem
|
||||
.execute<TagMutator>(tag, (mutator) => {
|
||||
mutator.iconString = icon as string
|
||||
})
|
||||
.catch(console.error)
|
||||
@@ -593,13 +609,13 @@ export class NavigationController
|
||||
}
|
||||
|
||||
public createNewTemplate() {
|
||||
const isAlreadyEditingATemplate = this.editing_ && this.application.items.isTemplateItem(this.editing_)
|
||||
const isAlreadyEditingATemplate = this.editing_ && this.items.isTemplateItem(this.editing_)
|
||||
|
||||
if (isAlreadyEditingATemplate) {
|
||||
return
|
||||
}
|
||||
|
||||
const newTag = this.application.items.createTemplateItem(ContentType.TYPES.Tag) as SNTag
|
||||
const newTag = this.items.createTemplateItem(ContentType.TYPES.Tag) as SNTag
|
||||
|
||||
runInAction(() => {
|
||||
this.selectedLocation = 'all'
|
||||
@@ -622,9 +638,9 @@ export class NavigationController
|
||||
})
|
||||
}
|
||||
if (shouldDelete) {
|
||||
this.application.mutator
|
||||
this.mutator
|
||||
.deleteItem(tag)
|
||||
.then(() => this.application.sync.sync())
|
||||
.then(() => this.sync.sync())
|
||||
.catch(console.error)
|
||||
await this.setSelectedTag(this.smartViews[0], 'views')
|
||||
}
|
||||
@@ -633,9 +649,9 @@ export class NavigationController
|
||||
public async save(tag: SNTag | SmartView, newTitle: string) {
|
||||
const hasEmptyTitle = newTitle.length === 0
|
||||
const hasNotChangedTitle = newTitle === tag.title
|
||||
const isTemplateChange = this.application.items.isTemplateItem(tag)
|
||||
const isTemplateChange = this.items.isTemplateItem(tag)
|
||||
|
||||
const siblings = tag instanceof SNTag ? tagSiblings(this.application, tag) : []
|
||||
const siblings = tag instanceof SNTag ? tagSiblings(this.items, tag) : []
|
||||
const hasDuplicatedTitle = siblings.some((other) => other.title.toLowerCase() === newTitle.toLowerCase())
|
||||
|
||||
runInAction(() => {
|
||||
@@ -653,12 +669,12 @@ export class NavigationController
|
||||
if (isTemplateChange) {
|
||||
this.undoCreateNewTag()
|
||||
}
|
||||
this.application.alerts?.alert('A tag with this name already exists.').catch(console.error)
|
||||
this.alerts.alert('A tag with this name already exists.').catch(console.error)
|
||||
return
|
||||
}
|
||||
|
||||
if (isTemplateChange) {
|
||||
const isSmartViewTitle = this.application.items.isSmartViewTitle(newTitle)
|
||||
const isSmartViewTitle = this.items.isSmartViewTitle(newTitle)
|
||||
|
||||
if (isSmartViewTitle) {
|
||||
if (!this.featuresController.hasSmartViews) {
|
||||
@@ -667,16 +683,16 @@ export class NavigationController
|
||||
}
|
||||
}
|
||||
|
||||
const insertedTag = await this.application.mutator.createTagOrSmartView<SNTag>(
|
||||
const insertedTag = await this.mutator.createTagOrSmartView<SNTag>(
|
||||
newTitle,
|
||||
this.application.vaultDisplayService.exclusivelyShownVault,
|
||||
this.vaultDisplayService.exclusivelyShownVault,
|
||||
)
|
||||
this.application.sync.sync().catch(console.error)
|
||||
this.sync.sync().catch(console.error)
|
||||
runInAction(() => {
|
||||
void this.setSelectedTag(insertedTag, this.selectedLocation || 'views')
|
||||
})
|
||||
} else {
|
||||
await this.application.changeAndSaveItem<TagMutator>(tag, (mutator) => {
|
||||
await this._changeAndSaveItem.execute<TagMutator>(tag, (mutator) => {
|
||||
mutator.title = newTitle
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { SNTag } from '@standardnotes/snjs'
|
||||
import { ItemManagerInterface, 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) {
|
||||
public constructor(private items: ItemManagerInterface) {
|
||||
makeAutoObservable(this, {
|
||||
counts: observable.ref,
|
||||
update: action,
|
||||
@@ -16,7 +15,7 @@ export class TagsCountsState {
|
||||
const newCounts: { [uuid: string]: number } = Object.assign({}, this.counts)
|
||||
|
||||
tags.forEach((tag) => {
|
||||
newCounts[tag.uuid] = this.application.items.countableNotesForTag(tag)
|
||||
newCounts[tag.uuid] = this.items.countableNotesForTag(tag)
|
||||
})
|
||||
|
||||
this.counts = newCounts
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
import { SNApplication, SNTag } from '@standardnotes/snjs'
|
||||
import { AlertService, ItemManagerInterface, SNTag } from '@standardnotes/snjs'
|
||||
|
||||
export const rootTags = (application: SNApplication): SNTag[] => {
|
||||
const hasNoParent = (tag: SNTag) => !application.items.getTagParent(tag)
|
||||
export const rootTags = (items: ItemManagerInterface): SNTag[] => {
|
||||
const hasNoParent = (tag: SNTag) => !items.getTagParent(tag)
|
||||
|
||||
const allTags = application.items.getDisplayableTags()
|
||||
const allTags = items.getDisplayableTags()
|
||||
const rootTags = allTags.filter(hasNoParent)
|
||||
|
||||
return rootTags
|
||||
}
|
||||
|
||||
export const tagSiblings = (application: SNApplication, tag: SNTag): SNTag[] => {
|
||||
export const tagSiblings = (items: ItemManagerInterface, tag: SNTag): SNTag[] => {
|
||||
const withoutCurrentTag = (tags: SNTag[]) => tags.filter((other) => other.uuid !== tag.uuid)
|
||||
|
||||
const isTemplateTag = application.items.isTemplateItem(tag)
|
||||
const parentTag = !isTemplateTag && application.items.getTagParent(tag)
|
||||
const isTemplateTag = items.isTemplateItem(tag)
|
||||
const parentTag = !isTemplateTag && items.getTagParent(tag)
|
||||
|
||||
if (parentTag) {
|
||||
const siblingsAndTag = application.items.getTagChildren(parentTag)
|
||||
const siblingsAndTag = items.getTagChildren(parentTag)
|
||||
return withoutCurrentTag(siblingsAndTag)
|
||||
}
|
||||
|
||||
return withoutCurrentTag(rootTags(application))
|
||||
return withoutCurrentTag(rootTags(items))
|
||||
}
|
||||
|
||||
export const isValidFutureSiblings = (application: SNApplication, futureSiblings: SNTag[], tag: SNTag): boolean => {
|
||||
export const isValidFutureSiblings = (alerts: AlertService, futureSiblings: SNTag[], tag: SNTag): boolean => {
|
||||
const siblingWithSameName = futureSiblings.find((otherTag) => otherTag.title === tag.title)
|
||||
|
||||
if (siblingWithSameName) {
|
||||
application.alerts
|
||||
alerts
|
||||
?.alert(
|
||||
`A tag with the name ${tag.title} already exists at this destination. Please rename this tag before moving and try again.`,
|
||||
)
|
||||
|
||||
@@ -1,34 +1,27 @@
|
||||
import { storage, StorageKey } from '@standardnotes/ui-services'
|
||||
import { ApplicationEvent, InternalEventBusInterface } from '@standardnotes/snjs'
|
||||
import {
|
||||
ApplicationEvent,
|
||||
InternalEventBusInterface,
|
||||
InternalEventHandlerInterface,
|
||||
InternalEventInterface,
|
||||
SessionsClientInterface,
|
||||
} from '@standardnotes/snjs'
|
||||
import { runInAction, makeObservable, observable, action } from 'mobx'
|
||||
import { WebApplication } from '../Application/WebApplication'
|
||||
import { AbstractViewController } from './Abstract/AbstractViewController'
|
||||
|
||||
export class NoAccountWarningController extends AbstractViewController {
|
||||
export class NoAccountWarningController extends AbstractViewController implements InternalEventHandlerInterface {
|
||||
show: boolean
|
||||
|
||||
constructor(application: WebApplication, eventBus: InternalEventBusInterface) {
|
||||
super(application, eventBus)
|
||||
constructor(
|
||||
private sessions: SessionsClientInterface,
|
||||
eventBus: InternalEventBusInterface,
|
||||
) {
|
||||
super(eventBus)
|
||||
|
||||
this.show = application.hasAccount() ? false : storage.get(StorageKey.ShowNoAccountWarning) ?? true
|
||||
this.show = sessions.isSignedIn() ? false : storage.get(StorageKey.ShowNoAccountWarning) ?? true
|
||||
|
||||
this.disposers.push(
|
||||
application.addEventObserver(async () => {
|
||||
runInAction(() => {
|
||||
this.show = false
|
||||
})
|
||||
}, ApplicationEvent.SignedIn),
|
||||
)
|
||||
|
||||
this.disposers.push(
|
||||
application.addEventObserver(async () => {
|
||||
if (application.hasAccount()) {
|
||||
runInAction(() => {
|
||||
this.show = false
|
||||
})
|
||||
}
|
||||
}, ApplicationEvent.Started),
|
||||
)
|
||||
eventBus.addEventHandler(this, ApplicationEvent.SignedIn)
|
||||
eventBus.addEventHandler(this, ApplicationEvent.Started)
|
||||
|
||||
makeObservable(this, {
|
||||
show: observable,
|
||||
@@ -36,6 +29,23 @@ export class NoAccountWarningController extends AbstractViewController {
|
||||
})
|
||||
}
|
||||
|
||||
async handleEvent(event: InternalEventInterface): Promise<void> {
|
||||
switch (event.type) {
|
||||
case ApplicationEvent.SignedIn:
|
||||
runInAction(() => {
|
||||
this.show = false
|
||||
})
|
||||
break
|
||||
case ApplicationEvent.Started:
|
||||
if (this.sessions.isSignedIn()) {
|
||||
runInAction(() => {
|
||||
this.show = false
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
hide = (): void => {
|
||||
this.show = false
|
||||
storage.set(StorageKey.ShowNoAccountWarning, false)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { WebApplication } from '@/Application/WebApplication'
|
||||
import { InternalEventBusInterface, SNNote } from '@standardnotes/snjs'
|
||||
import { OPEN_NOTE_HISTORY_COMMAND } from '@standardnotes/ui-services'
|
||||
import { KeyboardService, OPEN_NOTE_HISTORY_COMMAND } from '@standardnotes/ui-services'
|
||||
import { action, makeObservable, observable } from 'mobx'
|
||||
import { AbstractViewController } from '../Abstract/AbstractViewController'
|
||||
import { NotesControllerInterface } from '../NotesController/NotesControllerInterface'
|
||||
@@ -14,11 +13,11 @@ export class HistoryModalController extends AbstractViewController {
|
||||
}
|
||||
|
||||
constructor(
|
||||
application: WebApplication,
|
||||
eventBus: InternalEventBusInterface,
|
||||
notesController: NotesControllerInterface,
|
||||
keyboardService: KeyboardService,
|
||||
eventBus: InternalEventBusInterface,
|
||||
) {
|
||||
super(application, eventBus)
|
||||
super(eventBus)
|
||||
|
||||
makeObservable(this, {
|
||||
note: observable,
|
||||
@@ -26,7 +25,7 @@ export class HistoryModalController extends AbstractViewController {
|
||||
})
|
||||
|
||||
this.disposers.push(
|
||||
application.keyboardService.addCommandHandler({
|
||||
keyboardService.addCommandHandler({
|
||||
command: OPEN_NOTE_HISTORY_COMMAND,
|
||||
onKeyDown: () => {
|
||||
this.openModal(notesController.firstSelectedNote)
|
||||
|
||||
@@ -1,42 +1,38 @@
|
||||
import { WebApplication } from '@/Application/WebApplication'
|
||||
import { RevisionType } from '@/Components/RevisionHistoryModal/RevisionType'
|
||||
import {
|
||||
LegacyHistoryEntry,
|
||||
ListGroup,
|
||||
RemoteRevisionListGroup,
|
||||
sortRevisionListIntoGroups,
|
||||
} from '@/Components/RevisionHistoryModal/utils'
|
||||
import { sortRevisionListIntoGroups } from '@/Components/RevisionHistoryModal/utils'
|
||||
import { STRING_RESTORE_LOCKED_ATTEMPT } from '@/Constants/Strings'
|
||||
import { confirmDialog } from '@standardnotes/ui-services'
|
||||
import {
|
||||
Action,
|
||||
ActionVerb,
|
||||
ActionsService,
|
||||
AlertService,
|
||||
ButtonType,
|
||||
ChangeAndSaveItem,
|
||||
DeleteRevision,
|
||||
FeaturesClientInterface,
|
||||
GetRevision,
|
||||
HistoryEntry,
|
||||
HistoryServiceInterface,
|
||||
ItemManagerInterface,
|
||||
ListRevisions,
|
||||
MutatorClientInterface,
|
||||
NoteHistoryEntry,
|
||||
PayloadEmitSource,
|
||||
RevisionMetadata,
|
||||
SNNote,
|
||||
SyncServiceInterface,
|
||||
} from '@standardnotes/snjs'
|
||||
import { makeObservable, observable, action } from 'mobx'
|
||||
import { SelectedItemsController } from '../SelectedItemsController'
|
||||
|
||||
type RemoteHistory = RemoteRevisionListGroup[]
|
||||
|
||||
type SessionHistory = ListGroup<NoteHistoryEntry>[]
|
||||
|
||||
type LegacyHistory = Action[]
|
||||
|
||||
type SelectedRevision = HistoryEntry | LegacyHistoryEntry | undefined
|
||||
|
||||
type SelectedEntry = RevisionMetadata | NoteHistoryEntry | Action | undefined
|
||||
|
||||
export enum RevisionContentState {
|
||||
Idle,
|
||||
Loading,
|
||||
Loaded,
|
||||
NotEntitled,
|
||||
}
|
||||
import {
|
||||
RemoteHistory,
|
||||
SessionHistory,
|
||||
LegacyHistory,
|
||||
SelectedRevision,
|
||||
SelectedEntry,
|
||||
RevisionContentState,
|
||||
} from './Types'
|
||||
import { ItemListController } from '../ItemList/ItemListController'
|
||||
|
||||
export class NoteHistoryController {
|
||||
remoteHistory: RemoteHistory = []
|
||||
@@ -52,12 +48,20 @@ export class NoteHistoryController {
|
||||
currentTab = RevisionType.Remote
|
||||
|
||||
constructor(
|
||||
private application: WebApplication,
|
||||
private note: SNNote,
|
||||
private selectionController: SelectedItemsController,
|
||||
private itemListController: ItemListController,
|
||||
private features: FeaturesClientInterface,
|
||||
private items: ItemManagerInterface,
|
||||
private mutator: MutatorClientInterface,
|
||||
private sync: SyncServiceInterface,
|
||||
private actions: ActionsService,
|
||||
private history: HistoryServiceInterface,
|
||||
private alerts: AlertService,
|
||||
private _getRevision: GetRevision,
|
||||
private _listRevisions: ListRevisions,
|
||||
private _deleteRevision: DeleteRevision,
|
||||
private _changeAndSaveItem: ChangeAndSaveItem,
|
||||
) {
|
||||
void this.fetchAllHistory()
|
||||
|
||||
makeObservable(this, {
|
||||
selectedRevision: observable,
|
||||
setSelectedRevision: action,
|
||||
@@ -84,6 +88,8 @@ export class NoteHistoryController {
|
||||
contentState: observable,
|
||||
setContentState: action,
|
||||
})
|
||||
|
||||
void this.fetchAllHistory()
|
||||
}
|
||||
|
||||
setSelectedRevision = (revision: SelectedRevision) => {
|
||||
@@ -119,7 +125,7 @@ export class NoteHistoryController {
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.application.features.hasMinimumRole(entry.required_role)) {
|
||||
if (!this.features.hasMinimumRole(entry.required_role)) {
|
||||
this.setContentState(RevisionContentState.NotEntitled)
|
||||
this.setSelectedRevision(undefined)
|
||||
return
|
||||
@@ -130,7 +136,7 @@ export class NoteHistoryController {
|
||||
|
||||
try {
|
||||
this.setSelectedEntry(entry)
|
||||
const remoteRevisionOrError = await this.application.getRevision.execute({
|
||||
const remoteRevisionOrError = await this._getRevision.execute({
|
||||
itemUuid: this.note.uuid,
|
||||
revisionUuid: entry.uuid,
|
||||
})
|
||||
@@ -162,7 +168,7 @@ export class NoteHistoryController {
|
||||
|
||||
this.setSelectedEntry(entry)
|
||||
|
||||
const response = await this.application.actions.runAction(entry.subactions[0], this.note)
|
||||
const response = await this.actions.runAction(entry.subactions[0], this.note)
|
||||
|
||||
if (!response) {
|
||||
throw new Error('Could not fetch revision')
|
||||
@@ -241,7 +247,7 @@ export class NoteHistoryController {
|
||||
if (this.note) {
|
||||
this.setIsFetchingRemoteHistory(true)
|
||||
try {
|
||||
const revisionsListOrError = await this.application.listRevisions.execute({ itemUuid: this.note.uuid })
|
||||
const revisionsListOrError = await this._listRevisions.execute({ itemUuid: this.note.uuid })
|
||||
if (revisionsListOrError.isFailed()) {
|
||||
throw new Error(revisionsListOrError.getError())
|
||||
}
|
||||
@@ -261,14 +267,14 @@ export class NoteHistoryController {
|
||||
}
|
||||
|
||||
fetchLegacyHistory = async () => {
|
||||
const actionExtensions = this.application.actions.getExtensions()
|
||||
const actionExtensions = this.actions.getExtensions()
|
||||
|
||||
actionExtensions.forEach(async (ext) => {
|
||||
if (!this.note) {
|
||||
return
|
||||
}
|
||||
|
||||
const actionExtension = await this.application.actions.loadExtensionInContextOfItem(ext, this.note)
|
||||
const actionExtension = await this.actions.loadExtensionInContextOfItem(ext, this.note)
|
||||
|
||||
if (!actionExtension) {
|
||||
return
|
||||
@@ -296,9 +302,7 @@ export class NoteHistoryController {
|
||||
}
|
||||
|
||||
this.setSessionHistory(
|
||||
sortRevisionListIntoGroups<NoteHistoryEntry>(
|
||||
this.application.history.sessionHistoryForItem(this.note) as NoteHistoryEntry[],
|
||||
),
|
||||
sortRevisionListIntoGroups<NoteHistoryEntry>(this.history.sessionHistoryForItem(this.note) as NoteHistoryEntry[]),
|
||||
)
|
||||
await this.fetchRemoteHistory()
|
||||
await this.fetchLegacyHistory()
|
||||
@@ -313,10 +317,10 @@ export class NoteHistoryController {
|
||||
}
|
||||
|
||||
restoreRevision = async (revision: NonNullable<SelectedRevision>) => {
|
||||
const originalNote = this.application.items.findItem<SNNote>(revision.payload.uuid)
|
||||
const originalNote = this.items.findItem<SNNote>(revision.payload.uuid)
|
||||
|
||||
if (originalNote?.locked) {
|
||||
this.application.alerts.alert(STRING_RESTORE_LOCKED_ATTEMPT).catch(console.error)
|
||||
this.alerts.alert(STRING_RESTORE_LOCKED_ATTEMPT).catch(console.error)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -330,7 +334,7 @@ export class NoteHistoryController {
|
||||
}
|
||||
|
||||
if (didConfirm) {
|
||||
void this.application.changeAndSaveItem(
|
||||
void this._changeAndSaveItem.execute(
|
||||
originalNote,
|
||||
(mutator) => {
|
||||
mutator.setCustomContent(revision.payload.content)
|
||||
@@ -342,20 +346,20 @@ export class NoteHistoryController {
|
||||
}
|
||||
|
||||
restoreRevisionAsCopy = async (revision: NonNullable<SelectedRevision>) => {
|
||||
const originalNote = this.application.items.findSureItem<SNNote>(revision.payload.uuid)
|
||||
const originalNote = this.items.findSureItem<SNNote>(revision.payload.uuid)
|
||||
|
||||
const duplicatedItem = await this.application.mutator.duplicateItem(originalNote, false, {
|
||||
const duplicatedItem = await this.mutator.duplicateItem(originalNote, false, {
|
||||
...revision.payload.content,
|
||||
title: revision.payload.content.title ? revision.payload.content.title + ' (copy)' : undefined,
|
||||
})
|
||||
|
||||
void this.application.sync.sync()
|
||||
void this.sync.sync()
|
||||
|
||||
this.selectionController.selectItem(duplicatedItem.uuid).catch(console.error)
|
||||
this.itemListController.selectItem(duplicatedItem.uuid).catch(console.error)
|
||||
}
|
||||
|
||||
deleteRemoteRevision = async (revisionEntry: RevisionMetadata) => {
|
||||
const shouldDelete = await this.application.alerts.confirm(
|
||||
const shouldDelete = await this.alerts.confirm(
|
||||
'Are you sure you want to delete this revision?',
|
||||
'Delete revision?',
|
||||
'Delete revision',
|
||||
@@ -367,7 +371,7 @@ export class NoteHistoryController {
|
||||
return
|
||||
}
|
||||
|
||||
const deleteRevisionOrError = await this.application.deleteRevision.execute({
|
||||
const deleteRevisionOrError = await this._deleteRevision.execute({
|
||||
itemUuid: this.note.uuid,
|
||||
revisionUuid: revisionEntry.uuid,
|
||||
})
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { LegacyHistoryEntry, ListGroup, RemoteRevisionListGroup } from '@/Components/RevisionHistoryModal/utils'
|
||||
|
||||
import { Action, HistoryEntry, NoteHistoryEntry, RevisionMetadata } from '@standardnotes/snjs'
|
||||
|
||||
export type RemoteHistory = RemoteRevisionListGroup[]
|
||||
|
||||
export type SessionHistory = ListGroup<NoteHistoryEntry>[]
|
||||
|
||||
export type LegacyHistory = Action[]
|
||||
|
||||
export type SelectedRevision = HistoryEntry | LegacyHistoryEntry | undefined
|
||||
|
||||
export type SelectedEntry = RevisionMetadata | NoteHistoryEntry | Action | undefined
|
||||
|
||||
export enum RevisionContentState {
|
||||
Idle,
|
||||
Loading,
|
||||
Loaded,
|
||||
NotEntitled,
|
||||
}
|
||||
@@ -1,8 +1,15 @@
|
||||
import { WebApplication } from '@/Application/WebApplication'
|
||||
import { MutationType, NoteMutator, SNNote } from '@standardnotes/models'
|
||||
import { InfoStrings } from '@standardnotes/snjs'
|
||||
import {
|
||||
AlertService,
|
||||
InfoStrings,
|
||||
ItemManagerInterface,
|
||||
MutatorClientInterface,
|
||||
SessionsClientInterface,
|
||||
SyncServiceInterface,
|
||||
} from '@standardnotes/snjs'
|
||||
import { Deferred } from '@standardnotes/utils'
|
||||
import { EditorSaveTimeoutDebounce } from '../Components/NoteView/Controller/EditorSaveTimeoutDebounce'
|
||||
import { IsNativeMobileWeb } from '@standardnotes/ui-services'
|
||||
|
||||
const NotePreviewCharLimit = 160
|
||||
|
||||
@@ -24,8 +31,13 @@ export class NoteSyncController {
|
||||
private saveTimeout?: ReturnType<typeof setTimeout>
|
||||
|
||||
constructor(
|
||||
private application: WebApplication,
|
||||
private item: SNNote,
|
||||
private items: ItemManagerInterface,
|
||||
private mutator: MutatorClientInterface,
|
||||
private sessions: SessionsClientInterface,
|
||||
private sync: SyncServiceInterface,
|
||||
private alerts: AlertService,
|
||||
private _isNativeMobileWeb: IsNativeMobileWeb,
|
||||
) {}
|
||||
|
||||
setItem(item: SNNote) {
|
||||
@@ -41,7 +53,6 @@ export class NoteSyncController {
|
||||
}
|
||||
this.savingLocallyPromise = null
|
||||
this.saveTimeout = undefined
|
||||
;(this.application as unknown) = undefined
|
||||
;(this.item as unknown) = undefined
|
||||
}
|
||||
|
||||
@@ -52,11 +63,11 @@ export class NoteSyncController {
|
||||
clearTimeout(this.saveTimeout)
|
||||
}
|
||||
|
||||
const noDebounce = params.bypassDebouncer || this.application.noAccount()
|
||||
const noDebounce = params.bypassDebouncer || this.sessions.isSignedOut()
|
||||
|
||||
const syncDebouceMs = noDebounce
|
||||
? EditorSaveTimeoutDebounce.ImmediateChange
|
||||
: this.application.isNativeMobileWeb()
|
||||
: this._isNativeMobileWeb.execute().getValue()
|
||||
? EditorSaveTimeoutDebounce.NativeMobileWeb
|
||||
: EditorSaveTimeoutDebounce.Desktop
|
||||
|
||||
@@ -76,12 +87,12 @@ export class NoteSyncController {
|
||||
}
|
||||
|
||||
private async undebouncedSave(params: NoteSaveFunctionParams): Promise<void> {
|
||||
if (!this.application.items.findItem(this.item.uuid)) {
|
||||
void this.application.alerts.alert(InfoStrings.InvalidNote)
|
||||
if (!this.items.findItem(this.item.uuid)) {
|
||||
void this.alerts.alert(InfoStrings.InvalidNote)
|
||||
return
|
||||
}
|
||||
|
||||
await this.application.mutator.changeItem(
|
||||
await this.mutator.changeItem(
|
||||
this.item,
|
||||
(mutator) => {
|
||||
const noteMutator = mutator as NoteMutator
|
||||
@@ -112,7 +123,7 @@ export class NoteSyncController {
|
||||
params.isUserModified ? MutationType.UpdateUserTimestamps : MutationType.NoUpdateUserTimestamps,
|
||||
)
|
||||
|
||||
void this.application.sync.sync().then(() => {
|
||||
void this.sync.sync().then(() => {
|
||||
params.onRemoteSyncComplete?.()
|
||||
})
|
||||
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { destroyAllObjectProperties } from '@/Utils'
|
||||
import { confirmDialog, PIN_NOTE_COMMAND, STAR_NOTE_COMMAND } from '@standardnotes/ui-services'
|
||||
import {
|
||||
confirmDialog,
|
||||
GetItemTags,
|
||||
IsGlobalSpellcheckEnabled,
|
||||
KeyboardService,
|
||||
PIN_NOTE_COMMAND,
|
||||
STAR_NOTE_COMMAND,
|
||||
} from '@standardnotes/ui-services'
|
||||
import { StringEmptyTrash, Strings, StringUtils } from '@/Constants/Strings'
|
||||
import { MENU_MARGIN_FROM_APP_BORDER } from '@/Constants/Constants'
|
||||
import {
|
||||
@@ -13,16 +20,27 @@ import {
|
||||
InternalEventBusInterface,
|
||||
MutationType,
|
||||
PrefDefaults,
|
||||
PreferenceServiceInterface,
|
||||
InternalEventHandlerInterface,
|
||||
InternalEventInterface,
|
||||
ItemManagerInterface,
|
||||
MutatorClientInterface,
|
||||
SyncServiceInterface,
|
||||
AlertService,
|
||||
ProtectionsClientInterface,
|
||||
} from '@standardnotes/snjs'
|
||||
import { makeObservable, observable, action, computed, runInAction } from 'mobx'
|
||||
import { WebApplication } from '../../Application/WebApplication'
|
||||
import { AbstractViewController } from '../Abstract/AbstractViewController'
|
||||
import { SelectedItemsController } from '../SelectedItemsController'
|
||||
import { ItemListController } from '../ItemList/ItemListController'
|
||||
import { NavigationController } from '../Navigation/NavigationController'
|
||||
import { NotesControllerInterface } from './NotesControllerInterface'
|
||||
import { ItemGroupController } from '@/Components/NoteView/Controller/ItemGroupController'
|
||||
import { CrossControllerEvent } from '../CrossControllerEvent'
|
||||
import { ItemListController } from '../ItemList/ItemListController'
|
||||
|
||||
export class NotesController extends AbstractViewController implements NotesControllerInterface {
|
||||
export class NotesController
|
||||
extends AbstractViewController
|
||||
implements NotesControllerInterface, InternalEventHandlerInterface
|
||||
{
|
||||
shouldLinkToParentFolders: boolean
|
||||
lastSelectedNote: SNNote | undefined
|
||||
contextMenuOpen = false
|
||||
@@ -33,25 +51,23 @@ export class NotesController extends AbstractViewController implements NotesCont
|
||||
contextMenuClickLocation: { x: number; y: number } = { x: 0, y: 0 }
|
||||
contextMenuMaxHeight: number | 'auto' = 'auto'
|
||||
showProtectedWarning = false
|
||||
private itemListController!: ItemListController
|
||||
|
||||
override deinit() {
|
||||
super.deinit()
|
||||
;(this.lastSelectedNote as unknown) = undefined
|
||||
;(this.selectionController as unknown) = undefined
|
||||
;(this.navigationController as unknown) = undefined
|
||||
;(this.itemListController as unknown) = undefined
|
||||
|
||||
destroyAllObjectProperties(this)
|
||||
}
|
||||
|
||||
constructor(
|
||||
application: WebApplication,
|
||||
private selectionController: SelectedItemsController,
|
||||
private itemListController: ItemListController,
|
||||
private navigationController: NavigationController,
|
||||
private itemControllerGroup: ItemGroupController,
|
||||
private keyboardService: KeyboardService,
|
||||
private preferences: PreferenceServiceInterface,
|
||||
private items: ItemManagerInterface,
|
||||
private mutator: MutatorClientInterface,
|
||||
private sync: SyncServiceInterface,
|
||||
private protections: ProtectionsClientInterface,
|
||||
private alerts: AlertService,
|
||||
private _isGlobalSpellcheckEnabled: IsGlobalSpellcheckEnabled,
|
||||
private _getItemTags: GetItemTags,
|
||||
eventBus: InternalEventBusInterface,
|
||||
) {
|
||||
super(application, eventBus)
|
||||
super(eventBus)
|
||||
|
||||
makeObservable(this, {
|
||||
contextMenuOpen: observable,
|
||||
@@ -71,39 +87,32 @@ export class NotesController extends AbstractViewController implements NotesCont
|
||||
unselectNotes: action,
|
||||
})
|
||||
|
||||
this.shouldLinkToParentFolders = application.getPreference(
|
||||
this.shouldLinkToParentFolders = preferences.getValue(
|
||||
PrefKey.NoteAddToParentFolders,
|
||||
PrefDefaults[PrefKey.NoteAddToParentFolders],
|
||||
)
|
||||
|
||||
eventBus.addEventHandler(this, ApplicationEvent.PreferencesChanged)
|
||||
eventBus.addEventHandler(this, CrossControllerEvent.UnselectAllNotes)
|
||||
|
||||
this.disposers.push(
|
||||
this.application.keyboardService.addCommandHandler({
|
||||
this.keyboardService.addCommandHandler({
|
||||
command: PIN_NOTE_COMMAND,
|
||||
onKeyDown: () => {
|
||||
this.togglePinSelectedNotes()
|
||||
},
|
||||
}),
|
||||
this.application.keyboardService.addCommandHandler({
|
||||
this.keyboardService.addCommandHandler({
|
||||
command: STAR_NOTE_COMMAND,
|
||||
onKeyDown: () => {
|
||||
this.toggleStarSelectedNotes()
|
||||
},
|
||||
}),
|
||||
this.application.addSingleEventObserver(ApplicationEvent.PreferencesChanged, async () => {
|
||||
this.shouldLinkToParentFolders = this.application.getPreference(
|
||||
PrefKey.NoteAddToParentFolders,
|
||||
PrefDefaults[PrefKey.NoteAddToParentFolders],
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
public setServicesPostConstruction(itemListController: ItemListController) {
|
||||
this.itemListController = itemListController
|
||||
|
||||
this.disposers.push(
|
||||
this.application.itemControllerGroup.addActiveControllerChangeObserver(() => {
|
||||
const controllers = this.application.itemControllerGroup.itemControllers
|
||||
this.itemControllerGroup.addActiveControllerChangeObserver(() => {
|
||||
const controllers = this.itemControllerGroup.itemControllers
|
||||
|
||||
const activeNoteUuids = controllers.map((controller) => controller.item.uuid)
|
||||
|
||||
@@ -111,15 +120,35 @@ export class NotesController extends AbstractViewController implements NotesCont
|
||||
|
||||
for (const selectedId of selectedUuids) {
|
||||
if (!activeNoteUuids.includes(selectedId)) {
|
||||
this.selectionController.deselectItem({ uuid: selectedId })
|
||||
this.itemListController.deselectItem({ uuid: selectedId })
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
async handleEvent(event: InternalEventInterface): Promise<void> {
|
||||
if (event.type === ApplicationEvent.PreferencesChanged) {
|
||||
this.shouldLinkToParentFolders = this.preferences.getValue(
|
||||
PrefKey.NoteAddToParentFolders,
|
||||
PrefDefaults[PrefKey.NoteAddToParentFolders],
|
||||
)
|
||||
} else if (event.type === CrossControllerEvent.UnselectAllNotes) {
|
||||
this.unselectNotes()
|
||||
}
|
||||
}
|
||||
|
||||
override deinit() {
|
||||
super.deinit()
|
||||
;(this.lastSelectedNote as unknown) = undefined
|
||||
;(this.itemListController as unknown) = undefined
|
||||
;(this.navigationController as unknown) = undefined
|
||||
|
||||
destroyAllObjectProperties(this)
|
||||
}
|
||||
|
||||
public get selectedNotes(): SNNote[] {
|
||||
return this.selectionController.getFilteredSelectedItems<SNNote>(ContentType.TYPES.Note)
|
||||
return this.itemListController.getFilteredSelectedItems<SNNote>(ContentType.TYPES.Note)
|
||||
}
|
||||
|
||||
get firstSelectedNote(): SNNote | undefined {
|
||||
@@ -135,7 +164,7 @@ export class NotesController extends AbstractViewController implements NotesCont
|
||||
}
|
||||
|
||||
get trashedNotesCount(): number {
|
||||
return this.application.items.trashedItems.length
|
||||
return this.items.trashedItems.length
|
||||
}
|
||||
|
||||
setContextMenuOpen = (open: boolean) => {
|
||||
@@ -202,8 +231,8 @@ export class NotesController extends AbstractViewController implements NotesCont
|
||||
}
|
||||
|
||||
async changeSelectedNotes(mutate: (mutator: NoteMutator) => void): Promise<void> {
|
||||
await this.application.mutator.changeItems(this.getSelectedNotesList(), mutate, MutationType.NoUpdateUserTimestamps)
|
||||
this.application.sync.sync().catch(console.error)
|
||||
await this.mutator.changeItems(this.getSelectedNotesList(), mutate, MutationType.NoUpdateUserTimestamps)
|
||||
this.sync.sync().catch(console.error)
|
||||
}
|
||||
|
||||
setHideSelectedNotePreviews(hide: boolean): void {
|
||||
@@ -243,7 +272,7 @@ export class NotesController extends AbstractViewController implements NotesCont
|
||||
async deleteNotes(permanently: boolean): Promise<boolean> {
|
||||
if (this.getSelectedNotesList().some((note) => note.locked)) {
|
||||
const text = StringUtils.deleteLockedNotesAttempt(this.selectedNotesCount)
|
||||
this.application.alerts.alert(text).catch(console.error)
|
||||
this.alerts.alert(text).catch(console.error)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -262,10 +291,10 @@ export class NotesController extends AbstractViewController implements NotesCont
|
||||
confirmButtonStyle: 'danger',
|
||||
})
|
||||
) {
|
||||
this.selectionController.selectNextItem()
|
||||
this.itemListController.selectNextItem()
|
||||
if (permanently) {
|
||||
await this.application.mutator.deleteItems(this.getSelectedNotesList())
|
||||
void this.application.sync.sync()
|
||||
await this.mutator.deleteItems(this.getSelectedNotesList())
|
||||
void this.sync.sync()
|
||||
} else {
|
||||
await this.changeSelectedNotes((mutator) => {
|
||||
mutator.trashed = true
|
||||
@@ -313,9 +342,7 @@ export class NotesController extends AbstractViewController implements NotesCont
|
||||
|
||||
async setArchiveSelectedNotes(archived: boolean): Promise<void> {
|
||||
if (this.getSelectedNotesList().some((note) => note.locked)) {
|
||||
this.application.alerts
|
||||
.alert(StringUtils.archiveLockedNotesAttempt(archived, this.selectedNotesCount))
|
||||
.catch(console.error)
|
||||
this.alerts.alert(StringUtils.archiveLockedNotesAttempt(archived, this.selectedNotesCount)).catch(console.error)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -324,7 +351,7 @@ export class NotesController extends AbstractViewController implements NotesCont
|
||||
})
|
||||
|
||||
runInAction(() => {
|
||||
this.selectionController.deselectAll()
|
||||
this.itemListController.deselectAll()
|
||||
this.contextMenuOpen = false
|
||||
})
|
||||
}
|
||||
@@ -332,76 +359,77 @@ export class NotesController extends AbstractViewController implements NotesCont
|
||||
async setProtectSelectedNotes(protect: boolean): Promise<void> {
|
||||
const selectedNotes = this.getSelectedNotesList()
|
||||
if (protect) {
|
||||
await this.application.protections.protectNotes(selectedNotes)
|
||||
await this.protections.protectNotes(selectedNotes)
|
||||
this.setShowProtectedWarning(true)
|
||||
} else {
|
||||
await this.application.protections.unprotectNotes(selectedNotes)
|
||||
await this.protections.unprotectNotes(selectedNotes)
|
||||
this.setShowProtectedWarning(false)
|
||||
}
|
||||
|
||||
void this.application.sync.sync()
|
||||
void this.sync.sync()
|
||||
}
|
||||
|
||||
unselectNotes(): void {
|
||||
this.selectionController.deselectAll()
|
||||
this.itemListController.deselectAll()
|
||||
}
|
||||
|
||||
getSpellcheckStateForNote(note: SNNote) {
|
||||
return note.spellcheck != undefined ? note.spellcheck : this.application.isGlobalSpellcheckEnabled()
|
||||
return note.spellcheck != undefined ? note.spellcheck : this._isGlobalSpellcheckEnabled.execute().getValue()
|
||||
}
|
||||
|
||||
async toggleGlobalSpellcheckForNote(note: SNNote) {
|
||||
await this.application.mutator.changeItem<NoteMutator>(
|
||||
await this.mutator.changeItem<NoteMutator>(
|
||||
note,
|
||||
(mutator) => {
|
||||
mutator.toggleSpellcheck()
|
||||
},
|
||||
MutationType.NoUpdateUserTimestamps,
|
||||
)
|
||||
this.application.sync.sync().catch(console.error)
|
||||
this.sync.sync().catch(console.error)
|
||||
}
|
||||
|
||||
getEditorWidthForNote(note: SNNote) {
|
||||
return (
|
||||
note.editorWidth ?? this.application.getPreference(PrefKey.EditorLineWidth, PrefDefaults[PrefKey.EditorLineWidth])
|
||||
)
|
||||
return note.editorWidth ?? this.preferences.getValue(PrefKey.EditorLineWidth, PrefDefaults[PrefKey.EditorLineWidth])
|
||||
}
|
||||
|
||||
async setNoteEditorWidth(note: SNNote, editorWidth: EditorLineWidth) {
|
||||
await this.application.mutator.changeItem<NoteMutator>(
|
||||
await this.mutator.changeItem<NoteMutator>(
|
||||
note,
|
||||
(mutator) => {
|
||||
mutator.editorWidth = editorWidth
|
||||
},
|
||||
MutationType.NoUpdateUserTimestamps,
|
||||
)
|
||||
this.application.sync.sync().catch(console.error)
|
||||
this.sync.sync().catch(console.error)
|
||||
}
|
||||
|
||||
async addTagToSelectedNotes(tag: SNTag): Promise<void> {
|
||||
const selectedNotes = this.getSelectedNotesList()
|
||||
await Promise.all(
|
||||
selectedNotes.map(async (note) => {
|
||||
await this.application.mutator.addTagToNote(note, tag, this.shouldLinkToParentFolders)
|
||||
await this.mutator.addTagToNote(note, tag, this.shouldLinkToParentFolders)
|
||||
}),
|
||||
)
|
||||
this.application.sync.sync().catch(console.error)
|
||||
this.sync.sync().catch(console.error)
|
||||
}
|
||||
|
||||
async removeTagFromSelectedNotes(tag: SNTag): Promise<void> {
|
||||
const selectedNotes = this.getSelectedNotesList()
|
||||
await this.application.mutator.changeItem(tag, (mutator) => {
|
||||
await this.mutator.changeItem(tag, (mutator) => {
|
||||
for (const note of selectedNotes) {
|
||||
mutator.removeItemAsRelationship(note)
|
||||
}
|
||||
})
|
||||
this.application.sync.sync().catch(console.error)
|
||||
this.sync.sync().catch(console.error)
|
||||
}
|
||||
|
||||
isTagInSelectedNotes(tag: SNTag): boolean {
|
||||
const selectedNotes = this.getSelectedNotesList()
|
||||
return selectedNotes.every((note) =>
|
||||
this.application.getItemTags(note).find((noteTag) => noteTag.uuid === tag.uuid),
|
||||
this._getItemTags
|
||||
.execute(note)
|
||||
.getValue()
|
||||
.find((noteTag) => noteTag.uuid === tag.uuid),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -416,8 +444,8 @@ export class NotesController extends AbstractViewController implements NotesCont
|
||||
confirmButtonStyle: 'danger',
|
||||
})
|
||||
) {
|
||||
await this.application.mutator.emptyTrash()
|
||||
this.application.sync.sync().catch(console.error)
|
||||
await this.mutator.emptyTrash()
|
||||
this.sync.sync().catch(console.error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { PanesForLayout } from './../../Application/UseCase/PanesForLayout'
|
||||
import {
|
||||
InternalEventHandlerInterface,
|
||||
InternalEventInterface,
|
||||
PreferenceServiceInterface,
|
||||
} from '@standardnotes/services'
|
||||
import {
|
||||
KeyboardService,
|
||||
TOGGLE_FOCUS_MODE_COMMAND,
|
||||
TOGGLE_LIST_PANE_KEYBOARD_COMMAND,
|
||||
TOGGLE_NAVIGATION_PANE_KEYBOARD_COMMAND,
|
||||
@@ -15,12 +22,10 @@ import { isMobileScreen } from '@/Utils'
|
||||
import { makeObservable, observable, action, computed } from 'mobx'
|
||||
import { Disposer } from '@/Types/Disposer'
|
||||
import { MediaQueryBreakpoints } from '@/Hooks/useMediaQuery'
|
||||
import { WebApplication } from '@/Application/WebApplication'
|
||||
import { AbstractViewController } from '../Abstract/AbstractViewController'
|
||||
import { log, LoggingDomain } from '@/Logging'
|
||||
import { PaneLayout } from './PaneLayout'
|
||||
import { panesForLayout } from './panesForLayout'
|
||||
import { getIsTabletOrMobileScreen } from '@/Hooks/useIsTabletOrMobileScreen'
|
||||
import { IsTabletOrMobileScreen } from '@/Application/UseCase/IsTabletOrMobileScreen'
|
||||
|
||||
const MinimumNavPanelWidth = PrefDefaults[PrefKey.TagsPanelWidth]
|
||||
const MinimumNotesPanelWidth = PrefDefaults[PrefKey.NotesPanelWidth]
|
||||
@@ -28,7 +33,7 @@ const FOCUS_MODE_CLASS_NAME = 'focus-mode'
|
||||
const DISABLING_FOCUS_MODE_CLASS_NAME = 'disable-focus-mode'
|
||||
const FOCUS_MODE_ANIMATION_DURATION = 1255
|
||||
|
||||
export class PaneController extends AbstractViewController {
|
||||
export class PaneController extends AbstractViewController implements InternalEventHandlerInterface {
|
||||
isInMobileView = isMobileScreen()
|
||||
protected disposers: Disposer[] = []
|
||||
panes: AppPaneId[] = []
|
||||
@@ -40,8 +45,14 @@ export class PaneController extends AbstractViewController {
|
||||
listPaneExplicitelyCollapsed = false
|
||||
navigationPaneExplicitelyCollapsed = false
|
||||
|
||||
constructor(application: WebApplication, eventBus: InternalEventBusInterface) {
|
||||
super(application, eventBus)
|
||||
constructor(
|
||||
private preferences: PreferenceServiceInterface,
|
||||
private keyboardService: KeyboardService,
|
||||
private _isTabletOrMobileScreen: IsTabletOrMobileScreen,
|
||||
private _panesForLayout: PanesForLayout,
|
||||
eventBus: InternalEventBusInterface,
|
||||
) {
|
||||
super(eventBus)
|
||||
|
||||
makeObservable(this, {
|
||||
panes: observable,
|
||||
@@ -70,10 +81,10 @@ export class PaneController extends AbstractViewController {
|
||||
setFocusModeEnabled: action,
|
||||
})
|
||||
|
||||
this.setCurrentNavPanelWidth(application.getPreference(PrefKey.TagsPanelWidth, MinimumNavPanelWidth))
|
||||
this.setCurrentItemsPanelWidth(application.getPreference(PrefKey.NotesPanelWidth, MinimumNotesPanelWidth))
|
||||
this.setCurrentNavPanelWidth(preferences.getValue(PrefKey.TagsPanelWidth, MinimumNavPanelWidth))
|
||||
this.setCurrentItemsPanelWidth(preferences.getValue(PrefKey.NotesPanelWidth, MinimumNotesPanelWidth))
|
||||
|
||||
const screen = getIsTabletOrMobileScreen(application)
|
||||
const screen = this._isTabletOrMobileScreen.execute().getValue()
|
||||
|
||||
this.panes = screen.isTabletOrMobile
|
||||
? [AppPaneId.Navigation, AppPaneId.Items]
|
||||
@@ -86,13 +97,10 @@ export class PaneController extends AbstractViewController {
|
||||
mediaQuery.addListener(this.mediumScreenMQHandler)
|
||||
}
|
||||
|
||||
this.disposers.push(
|
||||
application.addEventObserver(async () => {
|
||||
this.setCurrentNavPanelWidth(application.getPreference(PrefKey.TagsPanelWidth, MinimumNavPanelWidth))
|
||||
this.setCurrentItemsPanelWidth(application.getPreference(PrefKey.NotesPanelWidth, MinimumNotesPanelWidth))
|
||||
}, ApplicationEvent.PreferencesChanged),
|
||||
eventBus.addEventHandler(this, ApplicationEvent.PreferencesChanged)
|
||||
|
||||
application.keyboardService.addCommandHandler({
|
||||
this.disposers.push(
|
||||
keyboardService.addCommandHandler({
|
||||
command: TOGGLE_FOCUS_MODE_COMMAND,
|
||||
onKeyDown: (event) => {
|
||||
event.preventDefault()
|
||||
@@ -100,14 +108,14 @@ export class PaneController extends AbstractViewController {
|
||||
return true
|
||||
},
|
||||
}),
|
||||
application.keyboardService.addCommandHandler({
|
||||
keyboardService.addCommandHandler({
|
||||
command: TOGGLE_LIST_PANE_KEYBOARD_COMMAND,
|
||||
onKeyDown: (event) => {
|
||||
event.preventDefault()
|
||||
this.toggleListPane()
|
||||
},
|
||||
}),
|
||||
application.keyboardService.addCommandHandler({
|
||||
keyboardService.addCommandHandler({
|
||||
command: TOGGLE_NAVIGATION_PANE_KEYBOARD_COMMAND,
|
||||
onKeyDown: (event) => {
|
||||
event.preventDefault()
|
||||
@@ -117,6 +125,13 @@ export class PaneController extends AbstractViewController {
|
||||
)
|
||||
}
|
||||
|
||||
async handleEvent(event: InternalEventInterface): Promise<void> {
|
||||
if (event.type === ApplicationEvent.PreferencesChanged) {
|
||||
this.setCurrentNavPanelWidth(this.preferences.getValue(PrefKey.TagsPanelWidth, MinimumNavPanelWidth))
|
||||
this.setCurrentItemsPanelWidth(this.preferences.getValue(PrefKey.NotesPanelWidth, MinimumNotesPanelWidth))
|
||||
}
|
||||
}
|
||||
|
||||
setCurrentNavPanelWidth(width: number) {
|
||||
this.currentNavPanelWidth = width
|
||||
}
|
||||
@@ -158,7 +173,7 @@ export class PaneController extends AbstractViewController {
|
||||
setPaneLayout = (layout: PaneLayout) => {
|
||||
log(LoggingDomain.Panes, 'Set pane layout', layout)
|
||||
|
||||
const panes = panesForLayout(layout, this.application)
|
||||
const panes = this._panesForLayout.execute(layout).getValue()
|
||||
|
||||
if (panes.includes(AppPaneId.Items) && this.listPaneExplicitelyCollapsed) {
|
||||
removeFromArray(panes, AppPaneId.Items)
|
||||
|
||||
@@ -1,34 +1,4 @@
|
||||
import { AppPaneId } from '../../Components/Panes/AppPaneMetadata'
|
||||
import { PaneLayout } from './PaneLayout'
|
||||
import { WebApplication } from '@/Application/WebApplication'
|
||||
import { getIsTabletOrMobileScreen } from '@/Hooks/useIsTabletOrMobileScreen'
|
||||
|
||||
export function panesForLayout(layout: PaneLayout, application: WebApplication): AppPaneId[] {
|
||||
const screen = getIsTabletOrMobileScreen(application)
|
||||
if (screen.isTablet) {
|
||||
if (layout === PaneLayout.TagSelection || layout === PaneLayout.TableView) {
|
||||
return [AppPaneId.Navigation, AppPaneId.Items]
|
||||
} else if (layout === PaneLayout.ItemSelection || layout === PaneLayout.Editing) {
|
||||
return [AppPaneId.Items, AppPaneId.Editor]
|
||||
}
|
||||
} else if (screen.isMobile) {
|
||||
if (layout === PaneLayout.TagSelection) {
|
||||
return [AppPaneId.Navigation]
|
||||
} else if (layout === PaneLayout.ItemSelection || layout === PaneLayout.TableView) {
|
||||
return [AppPaneId.Navigation, AppPaneId.Items]
|
||||
} else if (layout === PaneLayout.Editing) {
|
||||
return [AppPaneId.Navigation, AppPaneId.Items, AppPaneId.Editor]
|
||||
}
|
||||
} else {
|
||||
if (layout === PaneLayout.TableView) {
|
||||
return [AppPaneId.Navigation, AppPaneId.Items]
|
||||
} else {
|
||||
return [AppPaneId.Navigation, AppPaneId.Items, AppPaneId.Editor]
|
||||
}
|
||||
}
|
||||
|
||||
throw Error('Unhandled pane layout')
|
||||
}
|
||||
|
||||
export function isPanesChangeLeafDismiss(from: AppPaneId[], to: AppPaneId[]): boolean {
|
||||
const fromWithoutLast = from.slice(0, from.length - 1)
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { InternalEventBusInterface } from '@standardnotes/snjs'
|
||||
import { action, computed, makeObservable, observable } from 'mobx'
|
||||
import { PreferenceId, RootQueryParam } from '@standardnotes/ui-services'
|
||||
import { PreferenceId, RootQueryParam, RouteServiceInterface } from '@standardnotes/ui-services'
|
||||
import { AbstractViewController } from './Abstract/AbstractViewController'
|
||||
import { WebApplication } from '@/Application/WebApplication'
|
||||
|
||||
const DEFAULT_PANE: PreferenceId = 'account'
|
||||
|
||||
@@ -10,8 +9,11 @@ export class PreferencesController extends AbstractViewController {
|
||||
private _open = false
|
||||
currentPane: PreferenceId = DEFAULT_PANE
|
||||
|
||||
constructor(application: WebApplication, eventBus: InternalEventBusInterface) {
|
||||
super(application, eventBus)
|
||||
constructor(
|
||||
private routeService: RouteServiceInterface,
|
||||
eventBus: InternalEventBusInterface,
|
||||
) {
|
||||
super(eventBus)
|
||||
|
||||
makeObservable<PreferencesController, '_open'>(this, {
|
||||
_open: observable,
|
||||
@@ -34,7 +36,7 @@ export class PreferencesController extends AbstractViewController {
|
||||
closePreferences = (): void => {
|
||||
this._open = false
|
||||
this.currentPane = DEFAULT_PANE
|
||||
this.application.routeService.removeQueryParameterFromURL(RootQueryParam.Settings)
|
||||
this.routeService.removeQueryParameterFromURL(RootQueryParam.Settings)
|
||||
}
|
||||
|
||||
get isOpen(): boolean {
|
||||
|
||||
@@ -1,17 +1,33 @@
|
||||
import {
|
||||
AlertService,
|
||||
LegacyApiServiceInterface,
|
||||
MobileDeviceInterface,
|
||||
SessionsClientInterface,
|
||||
SubscriptionManagerInterface,
|
||||
} from '@standardnotes/services'
|
||||
import { LoggingDomain, log } from '@/Logging'
|
||||
import { loadPurchaseFlowUrl } from '@/Components/PurchaseFlow/PurchaseFlowFunctions'
|
||||
import { AppleIAPProductId, InternalEventBusInterface } from '@standardnotes/snjs'
|
||||
import { action, makeObservable, observable } from 'mobx'
|
||||
import { WebApplication } from '../../Application/WebApplication'
|
||||
import { AbstractViewController } from '../Abstract/AbstractViewController'
|
||||
import { PurchaseFlowPane } from './PurchaseFlowPane'
|
||||
import { LoadPurchaseFlowUrl } from '@/Application/UseCase/LoadPurchaseFlowUrl'
|
||||
import { IsNativeIOS } from '@standardnotes/ui-services'
|
||||
|
||||
export class PurchaseFlowController extends AbstractViewController {
|
||||
isOpen = false
|
||||
currentPane = PurchaseFlowPane.CreateAccount
|
||||
|
||||
constructor(application: WebApplication, eventBus: InternalEventBusInterface) {
|
||||
super(application, eventBus)
|
||||
constructor(
|
||||
private sessions: SessionsClientInterface,
|
||||
private subscriptions: SubscriptionManagerInterface,
|
||||
private legacyApi: LegacyApiServiceInterface,
|
||||
private alerts: AlertService,
|
||||
private mobileDevice: MobileDeviceInterface | undefined,
|
||||
private _loadPurchaseFlowUrl: LoadPurchaseFlowUrl,
|
||||
private _isNativeIOS: IsNativeIOS,
|
||||
eventBus: InternalEventBusInterface,
|
||||
) {
|
||||
super(eventBus)
|
||||
|
||||
makeObservable(this, {
|
||||
isOpen: observable,
|
||||
@@ -28,45 +44,46 @@ export class PurchaseFlowController extends AbstractViewController {
|
||||
}
|
||||
|
||||
openPurchaseFlow = async (plan = AppleIAPProductId.ProPlanYearly) => {
|
||||
const user = this.application.getUser()
|
||||
const user = this.sessions.getUser()
|
||||
if (!user) {
|
||||
this.isOpen = true
|
||||
return
|
||||
}
|
||||
|
||||
if (this.application.isNativeIOS()) {
|
||||
if (this._isNativeIOS.execute().getValue()) {
|
||||
await this.beginIosIapPurchaseFlow(plan)
|
||||
} else {
|
||||
await loadPurchaseFlowUrl(this.application)
|
||||
await this._loadPurchaseFlowUrl.execute()
|
||||
}
|
||||
}
|
||||
|
||||
openPurchaseWebpage = () => {
|
||||
loadPurchaseFlowUrl(this.application).catch((err) => {
|
||||
console.error(err)
|
||||
this.application.alerts.alert(err).catch(console.error)
|
||||
})
|
||||
openPurchaseWebpage = async () => {
|
||||
const result = await this._loadPurchaseFlowUrl.execute()
|
||||
if (result.isFailed()) {
|
||||
console.error(result.getError())
|
||||
void this.alerts.alert(result.getError())
|
||||
}
|
||||
}
|
||||
|
||||
beginIosIapPurchaseFlow = async (plan: AppleIAPProductId): Promise<void> => {
|
||||
const result = await this.application.mobileDevice().purchaseSubscriptionIAP(plan)
|
||||
const result = await this.mobileDevice?.purchaseSubscriptionIAP(plan)
|
||||
|
||||
log(LoggingDomain.Purchasing, 'BeginIosIapPurchaseFlow result', result)
|
||||
|
||||
if (!result) {
|
||||
void this.application.alerts.alert('Your purchase was canceled or failed. Please try again.')
|
||||
void this.alerts.alert('Your purchase was canceled or failed. Please try again.')
|
||||
return
|
||||
}
|
||||
|
||||
const showGenericError = () => {
|
||||
void this.application.alerts.alert(
|
||||
void this.alerts.alert(
|
||||
'There was an error confirming your purchase. Please contact support at help@standardnotes.com.',
|
||||
)
|
||||
}
|
||||
|
||||
log(LoggingDomain.Purchasing, 'Confirming result with our server')
|
||||
|
||||
const token = await this.application.getNewSubscriptionToken()
|
||||
const token = await this.legacyApi.getNewSubscriptionToken()
|
||||
|
||||
if (!token) {
|
||||
log(LoggingDomain.Purchasing, 'Unable to generate subscription token')
|
||||
@@ -74,12 +91,12 @@ export class PurchaseFlowController extends AbstractViewController {
|
||||
return
|
||||
}
|
||||
|
||||
const confirmResult = await this.application.subscriptions.confirmAppleIAP(result, token)
|
||||
const confirmResult = await this.subscriptions.confirmAppleIAP(result, token)
|
||||
|
||||
log(LoggingDomain.Purchasing, 'Server confirm result', confirmResult)
|
||||
|
||||
if (confirmResult) {
|
||||
void this.application.alerts.alert(
|
||||
void this.alerts.alert(
|
||||
'Please allow a few minutes for your subscription benefits to activate. You will see a confirmation alert in the app when your subscription is ready.',
|
||||
'Your purchase was successful!',
|
||||
)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { InternalEventBusInterface } from '@standardnotes/snjs'
|
||||
import { WebApplication } from '@/Application/WebApplication'
|
||||
import { action, makeObservable, observable } from 'mobx'
|
||||
import { AbstractViewController } from './Abstract/AbstractViewController'
|
||||
|
||||
@@ -7,8 +6,8 @@ export class QuickSettingsController extends AbstractViewController {
|
||||
open = false
|
||||
shouldAnimateCloseMenu = false
|
||||
|
||||
constructor(application: WebApplication, eventBus: InternalEventBusInterface) {
|
||||
super(application, eventBus)
|
||||
constructor(eventBus: InternalEventBusInterface) {
|
||||
super(eventBus)
|
||||
|
||||
makeObservable(this, {
|
||||
open: observable,
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
import { ApplicationEvent, InternalEventBusInterface } from '@standardnotes/snjs'
|
||||
import { ProtectionsClientInterface } from '@standardnotes/services'
|
||||
import {
|
||||
ApplicationEvent,
|
||||
InternalEventBusInterface,
|
||||
InternalEventHandlerInterface,
|
||||
InternalEventInterface,
|
||||
} from '@standardnotes/snjs'
|
||||
import { makeObservable, observable, action, runInAction } from 'mobx'
|
||||
import { WebApplication } from '../Application/WebApplication'
|
||||
import { AbstractViewController } from './Abstract/AbstractViewController'
|
||||
|
||||
export class SearchOptionsController extends AbstractViewController {
|
||||
export class SearchOptionsController extends AbstractViewController implements InternalEventHandlerInterface {
|
||||
includeProtectedContents = false
|
||||
includeArchived = false
|
||||
includeTrashed = false
|
||||
|
||||
constructor(application: WebApplication, eventBus: InternalEventBusInterface) {
|
||||
super(application, eventBus)
|
||||
constructor(
|
||||
private protections: ProtectionsClientInterface,
|
||||
eventBus: InternalEventBusInterface,
|
||||
) {
|
||||
super(eventBus)
|
||||
|
||||
makeObservable(this, {
|
||||
includeProtectedContents: observable,
|
||||
@@ -22,14 +30,16 @@ export class SearchOptionsController extends AbstractViewController {
|
||||
refreshIncludeProtectedContents: action,
|
||||
})
|
||||
|
||||
this.disposers.push(
|
||||
this.application.addEventObserver(async () => {
|
||||
this.refreshIncludeProtectedContents()
|
||||
}, ApplicationEvent.UnprotectedSessionBegan),
|
||||
this.application.addEventObserver(async () => {
|
||||
this.refreshIncludeProtectedContents()
|
||||
}, ApplicationEvent.UnprotectedSessionExpired),
|
||||
)
|
||||
eventBus.addEventHandler(this, ApplicationEvent.UnprotectedSessionBegan)
|
||||
eventBus.addEventHandler(this, ApplicationEvent.UnprotectedSessionExpired)
|
||||
}
|
||||
|
||||
async handleEvent(event: InternalEventInterface): Promise<void> {
|
||||
if (event.type === ApplicationEvent.UnprotectedSessionBegan) {
|
||||
this.refreshIncludeProtectedContents()
|
||||
} else if (event.type === ApplicationEvent.UnprotectedSessionExpired) {
|
||||
this.refreshIncludeProtectedContents()
|
||||
}
|
||||
}
|
||||
|
||||
toggleIncludeArchived = (): void => {
|
||||
@@ -41,14 +51,14 @@ export class SearchOptionsController extends AbstractViewController {
|
||||
}
|
||||
|
||||
refreshIncludeProtectedContents = (): void => {
|
||||
this.includeProtectedContents = this.application.hasUnprotectedAccessSession()
|
||||
this.includeProtectedContents = this.protections.hasUnprotectedAccessSession()
|
||||
}
|
||||
|
||||
toggleIncludeProtectedContents = async (): Promise<void> => {
|
||||
if (this.includeProtectedContents) {
|
||||
this.includeProtectedContents = false
|
||||
} else {
|
||||
await this.application.authorizeSearchingProtectedNotesText()
|
||||
await this.protections.authorizeSearchingProtectedNotesText()
|
||||
runInAction(() => {
|
||||
this.refreshIncludeProtectedContents()
|
||||
})
|
||||
|
||||
@@ -1,407 +0,0 @@
|
||||
import { isMobileScreen } from '@/Utils'
|
||||
import { ListableContentItem } from '@/Components/ContentListView/Types/ListableContentItem'
|
||||
import { log, LoggingDomain } from '@/Logging'
|
||||
import {
|
||||
ChallengeReason,
|
||||
ContentType,
|
||||
KeyboardModifier,
|
||||
FileItem,
|
||||
SNNote,
|
||||
UuidString,
|
||||
isFile,
|
||||
Uuids,
|
||||
isNote,
|
||||
InternalEventBusInterface,
|
||||
} from '@standardnotes/snjs'
|
||||
import { SelectionControllerPersistableValue } from '@standardnotes/ui-services'
|
||||
import { action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'
|
||||
import { WebApplication } from '../Application/WebApplication'
|
||||
import { AbstractViewController } from './Abstract/AbstractViewController'
|
||||
import { Persistable } from './Abstract/Persistable'
|
||||
import { CrossControllerEvent } from './CrossControllerEvent'
|
||||
import { ItemListController } from './ItemList/ItemListController'
|
||||
import { PaneLayout } from './PaneController/PaneLayout'
|
||||
import { requestCloseAllOpenModalsAndPopovers } from '@/Utils/CloseOpenModalsAndPopovers'
|
||||
|
||||
export class SelectedItemsController
|
||||
extends AbstractViewController
|
||||
implements Persistable<SelectionControllerPersistableValue>
|
||||
{
|
||||
lastSelectedItem: ListableContentItem | undefined
|
||||
selectedUuids: Set<UuidString> = observable(new Set<UuidString>())
|
||||
selectedItems: Record<UuidString, ListableContentItem> = {}
|
||||
private itemListController!: ItemListController
|
||||
|
||||
override deinit(): void {
|
||||
super.deinit()
|
||||
;(this.itemListController as unknown) = undefined
|
||||
}
|
||||
|
||||
constructor(application: WebApplication, eventBus: InternalEventBusInterface) {
|
||||
super(application, eventBus)
|
||||
|
||||
makeObservable(this, {
|
||||
selectedUuids: observable,
|
||||
selectedItems: observable,
|
||||
|
||||
selectedItemsCount: computed,
|
||||
selectedFiles: computed,
|
||||
selectedFilesCount: computed,
|
||||
firstSelectedItem: computed,
|
||||
|
||||
selectItem: action,
|
||||
setSelectedUuids: action,
|
||||
setSelectedItems: 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) {
|
||||
if (!this.application.options.allowNoteSelectionStatePersistence) {
|
||||
const items = this.application.items.findItems(state.selectedUuids).filter((item) => !isNote(item))
|
||||
void this.selectUuids(Uuids(items))
|
||||
} else {
|
||||
void this.selectUuids(state.selectedUuids)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public setServicesPostConstruction(itemListController: ItemListController) {
|
||||
this.itemListController = itemListController
|
||||
|
||||
this.disposers.push(
|
||||
this.application.streamItems<SNNote | FileItem>(
|
||||
[ContentType.TYPES.Note, ContentType.TYPES.File],
|
||||
({ changed, inserted, removed }) => {
|
||||
runInAction(() => {
|
||||
for (const removedItem of removed) {
|
||||
this.removeSelectedItem(removedItem.uuid)
|
||||
}
|
||||
|
||||
for (const item of [...changed, ...inserted]) {
|
||||
if (this.selectedItems[item.uuid]) {
|
||||
this.selectedItems[item.uuid] = item
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private get keyboardService() {
|
||||
return this.application.keyboardService
|
||||
}
|
||||
|
||||
get selectedItemsCount(): number {
|
||||
return Object.keys(this.selectedItems).length
|
||||
}
|
||||
|
||||
get selectedFiles(): FileItem[] {
|
||||
return this.getFilteredSelectedItems<FileItem>(ContentType.TYPES.File)
|
||||
}
|
||||
|
||||
get selectedFilesCount(): number {
|
||||
return this.selectedFiles.length
|
||||
}
|
||||
|
||||
get firstSelectedItem() {
|
||||
return Object.values(this.selectedItems)[0]
|
||||
}
|
||||
|
||||
getSelectedItems = () => {
|
||||
const uuids = Array.from(this.selectedUuids)
|
||||
return uuids.map((uuid) => this.application.items.findSureItem<SNNote | FileItem>(uuid)).filter((item) => !!item)
|
||||
}
|
||||
|
||||
getFilteredSelectedItems = <T extends ListableContentItem = ListableContentItem>(contentType?: string): T[] => {
|
||||
return Object.values(this.selectedItems).filter((item) => {
|
||||
return !contentType ? true : item.content_type === contentType
|
||||
}) as T[]
|
||||
}
|
||||
|
||||
setSelectedItems = () => {
|
||||
this.selectedItems = Object.fromEntries(this.getSelectedItems().map((item) => [item.uuid, item]))
|
||||
}
|
||||
|
||||
setSelectedUuids = (selectedUuids: Set<UuidString>) => {
|
||||
log(LoggingDomain.Selection, 'Setting selected uuids', selectedUuids)
|
||||
this.selectedUuids = new Set(selectedUuids)
|
||||
this.setSelectedItems()
|
||||
}
|
||||
|
||||
private removeSelectedItem = (uuid: UuidString) => {
|
||||
this.selectedUuids.delete(uuid)
|
||||
this.setSelectedUuids(this.selectedUuids)
|
||||
delete this.selectedItems[uuid]
|
||||
}
|
||||
|
||||
public deselectItem = (item: { uuid: ListableContentItem['uuid'] }): void => {
|
||||
log(LoggingDomain.Selection, 'Deselecting item', item.uuid)
|
||||
this.removeSelectedItem(item.uuid)
|
||||
|
||||
if (item.uuid === this.lastSelectedItem?.uuid) {
|
||||
this.lastSelectedItem = undefined
|
||||
}
|
||||
}
|
||||
|
||||
public isItemSelected = (item: ListableContentItem): boolean => {
|
||||
return this.selectedUuids.has(item.uuid)
|
||||
}
|
||||
|
||||
private selectItemsRange = async ({
|
||||
selectedItem,
|
||||
startingIndex,
|
||||
endingIndex,
|
||||
}: {
|
||||
selectedItem?: ListableContentItem
|
||||
startingIndex?: number
|
||||
endingIndex?: number
|
||||
}): Promise<void> => {
|
||||
const items = this.itemListController.renderedItems
|
||||
|
||||
const lastSelectedItemIndex = startingIndex ?? items.findIndex((item) => item.uuid == this.lastSelectedItem?.uuid)
|
||||
const selectedItemIndex = endingIndex ?? items.findIndex((item) => item.uuid == selectedItem?.uuid)
|
||||
|
||||
let itemsToSelect = []
|
||||
if (selectedItemIndex > lastSelectedItemIndex) {
|
||||
itemsToSelect = items.slice(lastSelectedItemIndex, selectedItemIndex + 1)
|
||||
} else {
|
||||
itemsToSelect = items.slice(selectedItemIndex, lastSelectedItemIndex + 1)
|
||||
}
|
||||
|
||||
const authorizedItems = await this.application.protections.authorizeProtectedActionForItems(
|
||||
itemsToSelect,
|
||||
ChallengeReason.SelectProtectedNote,
|
||||
)
|
||||
|
||||
for (const item of authorizedItems) {
|
||||
runInAction(() => {
|
||||
this.setSelectedUuids(this.selectedUuids.add(item.uuid))
|
||||
this.lastSelectedItem = item
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
cancelMultipleSelection = () => {
|
||||
this.keyboardService.cancelAllKeyboardModifiers()
|
||||
|
||||
const firstSelectedItem = this.firstSelectedItem
|
||||
|
||||
if (firstSelectedItem) {
|
||||
this.replaceSelection(firstSelectedItem)
|
||||
} else {
|
||||
this.deselectAll()
|
||||
}
|
||||
}
|
||||
|
||||
private replaceSelection = (item: ListableContentItem): void => {
|
||||
this.deselectAll()
|
||||
runInAction(() => this.setSelectedUuids(this.selectedUuids.add(item.uuid)))
|
||||
|
||||
this.lastSelectedItem = item
|
||||
}
|
||||
|
||||
selectAll = () => {
|
||||
void this.selectItemsRange({
|
||||
startingIndex: 0,
|
||||
endingIndex: this.itemListController.listLength - 1,
|
||||
})
|
||||
}
|
||||
|
||||
deselectAll = (): void => {
|
||||
this.selectedUuids.clear()
|
||||
this.setSelectedUuids(this.selectedUuids)
|
||||
|
||||
this.lastSelectedItem = undefined
|
||||
}
|
||||
|
||||
openSingleSelectedItem = async ({ userTriggered } = { userTriggered: true }) => {
|
||||
if (this.selectedItemsCount === 1) {
|
||||
const item = this.firstSelectedItem
|
||||
|
||||
if (item.content_type === ContentType.TYPES.Note) {
|
||||
await this.itemListController.openNote(item.uuid)
|
||||
} else if (item.content_type === ContentType.TYPES.File) {
|
||||
await this.itemListController.openFile(item.uuid)
|
||||
}
|
||||
|
||||
if (!this.application.paneController.isInMobileView || userTriggered) {
|
||||
void this.application.paneController.setPaneLayout(PaneLayout.Editing)
|
||||
}
|
||||
|
||||
if (this.application.paneController.isInMobileView && userTriggered) {
|
||||
requestCloseAllOpenModalsAndPopovers()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
selectItem = async (
|
||||
uuid: UuidString,
|
||||
userTriggered?: boolean,
|
||||
): Promise<{
|
||||
didSelect: boolean
|
||||
}> => {
|
||||
const item = this.application.items.findItem<ListableContentItem>(uuid)
|
||||
|
||||
if (!item) {
|
||||
return {
|
||||
didSelect: false,
|
||||
}
|
||||
}
|
||||
|
||||
log(LoggingDomain.Selection, 'Select item', item.uuid)
|
||||
|
||||
const supportsMultipleSelection = this.application.options.allowMultipleSelection
|
||||
const hasMeta = this.keyboardService.activeModifiers.has(KeyboardModifier.Meta)
|
||||
const hasCtrl = this.keyboardService.activeModifiers.has(KeyboardModifier.Ctrl)
|
||||
const hasShift = this.keyboardService.activeModifiers.has(KeyboardModifier.Shift)
|
||||
const hasMoreThanOneSelected = this.selectedItemsCount > 1
|
||||
const isAuthorizedForAccess = await this.application.protections.authorizeItemAccess(item)
|
||||
|
||||
if (supportsMultipleSelection && userTriggered && (hasMeta || hasCtrl)) {
|
||||
if (this.selectedUuids.has(uuid) && hasMoreThanOneSelected) {
|
||||
this.removeSelectedItem(uuid)
|
||||
} else if (isAuthorizedForAccess) {
|
||||
this.selectedUuids.add(uuid)
|
||||
this.setSelectedUuids(this.selectedUuids)
|
||||
this.lastSelectedItem = item
|
||||
}
|
||||
} else if (supportsMultipleSelection && userTriggered && hasShift) {
|
||||
await this.selectItemsRange({ selectedItem: item })
|
||||
} else {
|
||||
const shouldSelectNote = hasMoreThanOneSelected || !this.selectedUuids.has(uuid)
|
||||
if (shouldSelectNote && isAuthorizedForAccess) {
|
||||
this.replaceSelection(item)
|
||||
}
|
||||
}
|
||||
|
||||
await this.openSingleSelectedItem({ userTriggered: userTriggered ?? false })
|
||||
|
||||
return {
|
||||
didSelect: this.selectedUuids.has(uuid),
|
||||
}
|
||||
}
|
||||
|
||||
selectItemWithScrollHandling = async (
|
||||
item: {
|
||||
uuid: ListableContentItem['uuid']
|
||||
},
|
||||
{ userTriggered = false, scrollIntoView = true, animated = true },
|
||||
): Promise<void> => {
|
||||
const { didSelect } = await this.selectItem(item.uuid, userTriggered)
|
||||
|
||||
const avoidMobileScrollingDueToIncompatibilityWithPaneAnimations = isMobileScreen()
|
||||
|
||||
if (didSelect && scrollIntoView && !avoidMobileScrollingDueToIncompatibilityWithPaneAnimations) {
|
||||
this.scrollToItem(item, animated)
|
||||
}
|
||||
}
|
||||
|
||||
scrollToItem = (item: { uuid: ListableContentItem['uuid'] }, animated = true): void => {
|
||||
const itemElement = document.getElementById(item.uuid)
|
||||
itemElement?.scrollIntoView({
|
||||
behavior: animated ? 'smooth' : 'auto',
|
||||
})
|
||||
}
|
||||
|
||||
selectUuids = async (uuids: UuidString[], userTriggered = false) => {
|
||||
const itemsForUuids = this.application.items.findItems(uuids).filter((item) => !isFile(item))
|
||||
|
||||
if (itemsForUuids.length < 1) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!userTriggered && itemsForUuids.some((item) => item.protected && isFile(item))) {
|
||||
return
|
||||
}
|
||||
|
||||
this.setSelectedUuids(new Set(Uuids(itemsForUuids)))
|
||||
|
||||
if (itemsForUuids.length === 1) {
|
||||
void this.openSingleSelectedItem({ userTriggered })
|
||||
}
|
||||
}
|
||||
|
||||
selectNextItem = ({ userTriggered } = { userTriggered: true }) => {
|
||||
const displayableItems = this.itemListController.items
|
||||
|
||||
const currentIndex = displayableItems.findIndex((candidate) => {
|
||||
return candidate.uuid === this.lastSelectedItem?.uuid
|
||||
})
|
||||
|
||||
let nextIndex = currentIndex + 1
|
||||
|
||||
while (nextIndex < displayableItems.length) {
|
||||
const nextItem = displayableItems[nextIndex]
|
||||
|
||||
nextIndex++
|
||||
|
||||
if (nextItem.protected) {
|
||||
continue
|
||||
}
|
||||
|
||||
this.selectItemWithScrollHandling(nextItem, { userTriggered }).catch(console.error)
|
||||
|
||||
const nextNoteElement = document.getElementById(nextItem.uuid)
|
||||
|
||||
nextNoteElement?.focus()
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
selectPreviousItem = () => {
|
||||
const displayableItems = this.itemListController.items
|
||||
|
||||
if (!this.lastSelectedItem) {
|
||||
return
|
||||
}
|
||||
|
||||
const currentIndex = displayableItems.indexOf(this.lastSelectedItem)
|
||||
|
||||
let previousIndex = currentIndex - 1
|
||||
|
||||
while (previousIndex >= 0) {
|
||||
const previousItem = displayableItems[previousIndex]
|
||||
|
||||
previousIndex--
|
||||
|
||||
if (previousItem.protected) {
|
||||
continue
|
||||
}
|
||||
|
||||
this.selectItemWithScrollHandling(previousItem, { userTriggered: true }).catch(console.error)
|
||||
|
||||
const previousNoteElement = document.getElementById(previousItem.uuid)
|
||||
|
||||
previousNoteElement?.focus()
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,37 +2,34 @@ import { Subscription } from '@standardnotes/responses'
|
||||
import { destroyAllObjectProperties } from '@/Utils'
|
||||
import {
|
||||
ApplicationEvent,
|
||||
FeaturesClientInterface,
|
||||
InternalEventBusInterface,
|
||||
InternalEventHandlerInterface,
|
||||
InternalEventInterface,
|
||||
Invitation,
|
||||
InvitationStatus,
|
||||
SessionsClientInterface,
|
||||
SubscriptionManagerEvent,
|
||||
SubscriptionManagerInterface,
|
||||
} from '@standardnotes/snjs'
|
||||
import { computed, makeObservable, observable, runInAction } from 'mobx'
|
||||
import { WebApplication } from '../../Application/WebApplication'
|
||||
import { AbstractViewController } from '../Abstract/AbstractViewController'
|
||||
|
||||
export class SubscriptionController extends AbstractViewController {
|
||||
export class SubscriptionController extends AbstractViewController implements InternalEventHandlerInterface {
|
||||
private readonly ALLOWED_SUBSCRIPTION_INVITATIONS = 5
|
||||
|
||||
subscriptionInvitations: Invitation[] | undefined = undefined
|
||||
hasAccount: boolean
|
||||
onlineSubscription: Subscription | undefined = undefined
|
||||
|
||||
override deinit() {
|
||||
super.deinit()
|
||||
;(this.subscriptionInvitations as unknown) = undefined
|
||||
|
||||
destroyAllObjectProperties(this)
|
||||
}
|
||||
|
||||
constructor(
|
||||
application: WebApplication,
|
||||
private subscriptions: SubscriptionManagerInterface,
|
||||
private sessions: SessionsClientInterface,
|
||||
private features: FeaturesClientInterface,
|
||||
eventBus: InternalEventBusInterface,
|
||||
private subscriptionManager: SubscriptionManagerInterface,
|
||||
) {
|
||||
super(application, eventBus)
|
||||
this.hasAccount = application.hasAccount()
|
||||
super(eventBus)
|
||||
this.hasAccount = sessions.isSignedIn()
|
||||
|
||||
makeObservable(this, {
|
||||
subscriptionInvitations: observable,
|
||||
@@ -45,52 +42,62 @@ export class SubscriptionController extends AbstractViewController {
|
||||
allInvitationsUsed: computed,
|
||||
})
|
||||
|
||||
this.disposers.push(
|
||||
application.addEventObserver(async () => {
|
||||
if (application.hasAccount()) {
|
||||
eventBus.addEventHandler(this, ApplicationEvent.Launched)
|
||||
eventBus.addEventHandler(this, ApplicationEvent.SignedIn)
|
||||
eventBus.addEventHandler(this, ApplicationEvent.UserRolesChanged)
|
||||
eventBus.addEventHandler(this, SubscriptionManagerEvent.DidFetchSubscription)
|
||||
}
|
||||
|
||||
override deinit() {
|
||||
super.deinit()
|
||||
;(this.subscriptionInvitations as unknown) = undefined
|
||||
|
||||
destroyAllObjectProperties(this)
|
||||
}
|
||||
|
||||
async handleEvent(event: InternalEventInterface): Promise<void> {
|
||||
switch (event.type) {
|
||||
case ApplicationEvent.Launched: {
|
||||
if (this.sessions.isSignedIn()) {
|
||||
this.reloadSubscriptionInvitations().catch(console.error)
|
||||
}
|
||||
runInAction(() => {
|
||||
this.hasAccount = application.hasAccount()
|
||||
this.hasAccount = this.sessions.isSignedIn()
|
||||
})
|
||||
}, ApplicationEvent.Launched),
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
this.disposers.push(
|
||||
application.addEventObserver(async () => {
|
||||
case ApplicationEvent.SignedIn: {
|
||||
this.reloadSubscriptionInvitations().catch(console.error)
|
||||
runInAction(() => {
|
||||
this.hasAccount = application.hasAccount()
|
||||
this.hasAccount = this.sessions.isSignedIn()
|
||||
})
|
||||
}, ApplicationEvent.SignedIn),
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
this.disposers.push(
|
||||
application.subscriptions.addEventObserver(async (event) => {
|
||||
if (event === SubscriptionManagerEvent.DidFetchSubscription) {
|
||||
runInAction(() => {
|
||||
this.onlineSubscription = application.subscriptions.getOnlineSubscription()
|
||||
})
|
||||
}
|
||||
}),
|
||||
)
|
||||
case SubscriptionManagerEvent.DidFetchSubscription: {
|
||||
runInAction(() => {
|
||||
this.onlineSubscription = this.subscriptions.getOnlineSubscription()
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
this.disposers.push(
|
||||
application.addEventObserver(async () => {
|
||||
case ApplicationEvent.UserRolesChanged: {
|
||||
this.reloadSubscriptionInvitations().catch(console.error)
|
||||
}, ApplicationEvent.UserRolesChanged),
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get hasFirstPartyOnlineOrOfflineSubscription(): boolean {
|
||||
if (this.application.sessions.isSignedIn()) {
|
||||
if (!this.application.sessions.isSignedIntoFirstPartyServer()) {
|
||||
if (this.sessions.isSignedIn()) {
|
||||
if (!this.sessions.isSignedIntoFirstPartyServer()) {
|
||||
return false
|
||||
}
|
||||
|
||||
return this.application.subscriptions.getOnlineSubscription() !== undefined
|
||||
return this.subscriptions.getOnlineSubscription() !== undefined
|
||||
} else {
|
||||
return this.application.features.hasFirstPartyOfflineSubscription()
|
||||
return this.features.hasFirstPartyOfflineSubscription()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +118,7 @@ export class SubscriptionController extends AbstractViewController {
|
||||
}
|
||||
|
||||
async sendSubscriptionInvitation(inviteeEmail: string): Promise<boolean> {
|
||||
const success = await this.subscriptionManager.inviteToSubscription(inviteeEmail)
|
||||
const success = await this.subscriptions.inviteToSubscription(inviteeEmail)
|
||||
|
||||
if (success) {
|
||||
await this.reloadSubscriptionInvitations()
|
||||
@@ -121,7 +128,7 @@ export class SubscriptionController extends AbstractViewController {
|
||||
}
|
||||
|
||||
async cancelSubscriptionInvitation(invitationUuid: string): Promise<boolean> {
|
||||
const success = await this.subscriptionManager.cancelInvitation(invitationUuid)
|
||||
const success = await this.subscriptions.cancelInvitation(invitationUuid)
|
||||
|
||||
if (success) {
|
||||
await this.reloadSubscriptionInvitations()
|
||||
@@ -131,6 +138,6 @@ export class SubscriptionController extends AbstractViewController {
|
||||
}
|
||||
|
||||
private async reloadSubscriptionInvitations(): Promise<void> {
|
||||
this.subscriptionInvitations = await this.subscriptionManager.listSubscriptionInvitations()
|
||||
this.subscriptionInvitations = await this.subscriptions.listSubscriptionInvitations()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { InternalEventBusInterface } from '@standardnotes/snjs'
|
||||
import { WebApplication } from '@/Application/WebApplication'
|
||||
import { action, makeObservable, observable } from 'mobx'
|
||||
import { AbstractViewController } from './Abstract/AbstractViewController'
|
||||
|
||||
@@ -7,8 +6,8 @@ export class VaultSelectionMenuController extends AbstractViewController {
|
||||
open = false
|
||||
shouldAnimateCloseMenu = false
|
||||
|
||||
constructor(application: WebApplication, eventBus: InternalEventBusInterface) {
|
||||
super(application, eventBus)
|
||||
constructor(eventBus: InternalEventBusInterface) {
|
||||
super(eventBus)
|
||||
|
||||
makeObservable(this, {
|
||||
open: observable,
|
||||
|
||||
@@ -1,322 +0,0 @@
|
||||
import { PaneController } from './PaneController/PaneController'
|
||||
import {
|
||||
PersistedStateValue,
|
||||
PersistenceKey,
|
||||
storage,
|
||||
StorageKey,
|
||||
ToastService,
|
||||
ToastServiceInterface,
|
||||
} from '@standardnotes/ui-services'
|
||||
import { WebApplication } from '@/Application/WebApplication'
|
||||
import { AccountMenuController } from '@/Controllers/AccountMenu/AccountMenuController'
|
||||
import { destroyAllObjectProperties } from '@/Utils'
|
||||
import {
|
||||
DeinitSource,
|
||||
WebOrDesktopDeviceInterface,
|
||||
SubscriptionManagerInterface,
|
||||
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/NotesController'
|
||||
import { ItemListController } from './ItemList/ItemListController'
|
||||
import { NoAccountWarningController } from './NoAccountWarningController'
|
||||
import { PreferencesController } from './PreferencesController'
|
||||
import { PurchaseFlowController } from './PurchaseFlow/PurchaseFlowController'
|
||||
import { QuickSettingsController } from './QuickSettingsController'
|
||||
import { SearchOptionsController } from './SearchOptionsController'
|
||||
import { SubscriptionController } from './Subscription/SubscriptionController'
|
||||
import { SyncStatusController } from './SyncStatusController'
|
||||
import { NavigationController } from './Navigation/NavigationController'
|
||||
import { FilePreviewModalController } from './FilePreviewModalController'
|
||||
import { SelectedItemsController } from './SelectedItemsController'
|
||||
import { HistoryModalController } from './NoteHistory/HistoryModalController'
|
||||
import { LinkingController } from './LinkingController'
|
||||
import { PersistenceService } from './Abstract/PersistenceService'
|
||||
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
|
||||
|
||||
private unsubAppEventObserver!: () => void
|
||||
showBetaWarning: boolean
|
||||
public dealloced = false
|
||||
|
||||
readonly accountMenuController: AccountMenuController
|
||||
readonly actionsMenuController = new ActionsMenuController()
|
||||
readonly featuresController: FeaturesController
|
||||
readonly filePreviewModalController: FilePreviewModalController
|
||||
readonly filesController: FilesController
|
||||
readonly noAccountWarningController: NoAccountWarningController
|
||||
readonly notesController: NotesController
|
||||
readonly itemListController: ItemListController
|
||||
readonly preferencesController: PreferencesController
|
||||
readonly purchaseFlowController: PurchaseFlowController
|
||||
readonly quickSettingsMenuController: QuickSettingsController
|
||||
readonly vaultSelectionController: VaultSelectionMenuController
|
||||
readonly searchOptionsController: SearchOptionsController
|
||||
readonly subscriptionController: SubscriptionController
|
||||
readonly syncStatusController = new SyncStatusController()
|
||||
readonly navigationController: NavigationController
|
||||
readonly selectionController: SelectedItemsController
|
||||
readonly historyModalController: HistoryModalController
|
||||
readonly linkingController: LinkingController
|
||||
readonly paneController: PaneController
|
||||
readonly importModalController: ImportModalController
|
||||
|
||||
public isSessionsModalVisible = false
|
||||
|
||||
private appEventObserverRemovers: (() => void)[] = []
|
||||
|
||||
private subscriptionManager: SubscriptionManagerInterface
|
||||
private persistenceService: PersistenceService
|
||||
private applicationEventObserver: EventObserverInterface
|
||||
private toastService: ToastServiceInterface
|
||||
|
||||
constructor(
|
||||
public application: WebApplication,
|
||||
private device: WebOrDesktopDeviceInterface,
|
||||
) {
|
||||
const eventBus = application.events
|
||||
|
||||
this.persistenceService = new PersistenceService(application, eventBus)
|
||||
|
||||
eventBus.addEventHandler(this, CrossControllerEvent.HydrateFromPersistedValues)
|
||||
eventBus.addEventHandler(this, CrossControllerEvent.RequestValuePersistence)
|
||||
|
||||
this.subscriptionManager = application.subscriptions
|
||||
|
||||
this.filePreviewModalController = new FilePreviewModalController(application)
|
||||
|
||||
this.quickSettingsMenuController = new QuickSettingsController(application, eventBus)
|
||||
|
||||
this.vaultSelectionController = new VaultSelectionMenuController(application, eventBus)
|
||||
|
||||
this.paneController = new PaneController(application, eventBus)
|
||||
|
||||
this.preferencesController = new PreferencesController(application, eventBus)
|
||||
|
||||
this.selectionController = new SelectedItemsController(application, eventBus)
|
||||
|
||||
this.featuresController = new FeaturesController(application, eventBus)
|
||||
|
||||
this.navigationController = new NavigationController(application, this.featuresController, eventBus)
|
||||
|
||||
this.notesController = new NotesController(
|
||||
application,
|
||||
this.selectionController,
|
||||
this.navigationController,
|
||||
eventBus,
|
||||
)
|
||||
|
||||
this.searchOptionsController = new SearchOptionsController(application, eventBus)
|
||||
|
||||
this.linkingController = new LinkingController(
|
||||
application,
|
||||
this.navigationController,
|
||||
this.selectionController,
|
||||
eventBus,
|
||||
)
|
||||
|
||||
this.itemListController = new ItemListController(
|
||||
application,
|
||||
this.navigationController,
|
||||
this.searchOptionsController,
|
||||
this.selectionController,
|
||||
this.notesController,
|
||||
eventBus,
|
||||
)
|
||||
|
||||
this.notesController.setServicesPostConstruction(this.itemListController)
|
||||
this.selectionController.setServicesPostConstruction(this.itemListController)
|
||||
|
||||
this.noAccountWarningController = new NoAccountWarningController(application, eventBus)
|
||||
|
||||
this.accountMenuController = new AccountMenuController(application, eventBus)
|
||||
|
||||
this.subscriptionController = new SubscriptionController(application, eventBus, this.subscriptionManager)
|
||||
|
||||
this.purchaseFlowController = new PurchaseFlowController(application, eventBus)
|
||||
|
||||
this.filesController = new FilesController(
|
||||
application,
|
||||
this.notesController,
|
||||
this.filePreviewModalController,
|
||||
eventBus,
|
||||
)
|
||||
|
||||
this.linkingController.setServicesPostConstruction(
|
||||
this.itemListController,
|
||||
this.filesController,
|
||||
this.subscriptionController,
|
||||
)
|
||||
|
||||
this.historyModalController = new HistoryModalController(this.application, eventBus, this.notesController)
|
||||
|
||||
this.importModalController = new ImportModalController(this.application, this.navigationController)
|
||||
|
||||
this.toastService = new ToastService()
|
||||
|
||||
this.applicationEventObserver = new ApplicationEventObserver(
|
||||
application,
|
||||
application.routeService,
|
||||
this.purchaseFlowController,
|
||||
this.accountMenuController,
|
||||
this.preferencesController,
|
||||
this.syncStatusController,
|
||||
application.sync,
|
||||
application.sessions,
|
||||
application.subscriptions,
|
||||
this.toastService,
|
||||
application.user,
|
||||
)
|
||||
|
||||
this.addAppEventObserver()
|
||||
|
||||
if (this.device.appVersion.includes('-beta')) {
|
||||
this.showBetaWarning = storage.get(StorageKey.ShowBetaWarning) ?? true
|
||||
} else {
|
||||
this.showBetaWarning = false
|
||||
}
|
||||
|
||||
makeObservable(this, {
|
||||
showBetaWarning: observable,
|
||||
isSessionsModalVisible: observable,
|
||||
preferencesController: observable,
|
||||
|
||||
openSessionsModal: action,
|
||||
closeSessionsModal: action,
|
||||
})
|
||||
}
|
||||
|
||||
deinit(source: DeinitSource): void {
|
||||
this.dealloced = true
|
||||
;(this.application as unknown) = undefined
|
||||
|
||||
if (source === DeinitSource.SignOut) {
|
||||
storage.remove(StorageKey.ShowBetaWarning)
|
||||
this.noAccountWarningController.reset()
|
||||
}
|
||||
|
||||
this.unsubAppEventObserver?.()
|
||||
;(this.unsubAppEventObserver as unknown) = undefined
|
||||
|
||||
this.appEventObserverRemovers.forEach((remover) => remover())
|
||||
this.appEventObserverRemovers.length = 0
|
||||
;(this.device as unknown) = undefined
|
||||
this.filePreviewModalController.deinit()
|
||||
;(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()
|
||||
;(this.persistenceService as unknown) = undefined
|
||||
|
||||
this.actionsMenuController.reset()
|
||||
;(this.actionsMenuController as unknown) = undefined
|
||||
|
||||
this.featuresController.deinit()
|
||||
;(this.featuresController as unknown) = undefined
|
||||
|
||||
this.accountMenuController.deinit()
|
||||
;(this.accountMenuController as unknown) = undefined
|
||||
|
||||
this.filesController.deinit()
|
||||
;(this.filesController as unknown) = undefined
|
||||
|
||||
this.noAccountWarningController.deinit()
|
||||
;(this.noAccountWarningController as unknown) = undefined
|
||||
|
||||
this.notesController.deinit()
|
||||
;(this.notesController as unknown) = undefined
|
||||
|
||||
this.itemListController.deinit()
|
||||
;(this.itemListController as unknown) = undefined
|
||||
|
||||
this.linkingController.deinit()
|
||||
;(this.linkingController as unknown) = undefined
|
||||
|
||||
this.purchaseFlowController.deinit()
|
||||
;(this.purchaseFlowController as unknown) = undefined
|
||||
|
||||
this.searchOptionsController.deinit()
|
||||
;(this.searchOptionsController as unknown) = undefined
|
||||
|
||||
this.subscriptionController.deinit()
|
||||
;(this.subscriptionController as unknown) = undefined
|
||||
|
||||
this.navigationController.deinit()
|
||||
;(this.navigationController as unknown) = undefined
|
||||
|
||||
this.historyModalController.deinit()
|
||||
;(this.historyModalController as unknown) = undefined
|
||||
|
||||
this.paneController.deinit()
|
||||
;(this.paneController as unknown) = undefined
|
||||
|
||||
destroyAllObjectProperties(this)
|
||||
}
|
||||
|
||||
openSessionsModal = () => {
|
||||
this.isSessionsModalVisible = true
|
||||
}
|
||||
|
||||
closeSessionsModal = () => {
|
||||
this.isSessionsModalVisible = false
|
||||
}
|
||||
|
||||
addAppEventObserver() {
|
||||
this.unsubAppEventObserver = this.application.addEventObserver(
|
||||
this.applicationEventObserver.handle.bind(this.applicationEventObserver),
|
||||
)
|
||||
}
|
||||
|
||||
persistValues = (): void => {
|
||||
const values: PersistedStateValue = {
|
||||
[PersistenceKey.SelectedItemsController]: this.selectionController.getPersistableValue(),
|
||||
[PersistenceKey.NavigationController]: this.navigationController.getPersistableValue(),
|
||||
}
|
||||
|
||||
this.persistenceService.persistValues(values)
|
||||
|
||||
const selectedItemsState = values['selected-items-controller']
|
||||
const navigationSelectionState = values['navigation-controller']
|
||||
const launchPriorityUuids: string[] = []
|
||||
if (selectedItemsState.selectedUuids.length) {
|
||||
launchPriorityUuids.push(...selectedItemsState.selectedUuids)
|
||||
}
|
||||
if (navigationSelectionState.selectedTagUuid) {
|
||||
launchPriorityUuids.push(navigationSelectionState.selectedTagUuid)
|
||||
}
|
||||
this.application.sync.setLaunchPriorityUuids(launchPriorityUuids)
|
||||
}
|
||||
|
||||
clearPersistedValues = (): void => {
|
||||
this.persistenceService.clearPersistedValues()
|
||||
}
|
||||
|
||||
hydrateFromPersistedValues = (values: PersistedStateValue | undefined): void => {
|
||||
const navigationState = values?.[PersistenceKey.NavigationController]
|
||||
this.navigationController.hydrateFromPersistedValue(navigationState)
|
||||
|
||||
const selectedItemsState = values?.[PersistenceKey.SelectedItemsController]
|
||||
this.selectionController.hydrateFromPersistedValue(selectedItemsState)
|
||||
}
|
||||
|
||||
async handleEvent(event: InternalEventInterface): Promise<void> {
|
||||
if (event.type === CrossControllerEvent.HydrateFromPersistedValues) {
|
||||
this.hydrateFromPersistedValues(event.payload as PersistedStateValue | undefined)
|
||||
} else if (event.type === CrossControllerEvent.RequestValuePersistence) {
|
||||
this.persistValues()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user