diff --git a/packages/services/src/Domain/Storage/StorageKeys.ts b/packages/services/src/Domain/Storage/StorageKeys.ts index d402354eb..67775fd1c 100644 --- a/packages/services/src/Domain/Storage/StorageKeys.ts +++ b/packages/services/src/Domain/Storage/StorageKeys.ts @@ -38,6 +38,7 @@ export enum StorageKey { ExperimentalFeatures = 'experimental_features', DeinitMode = 'deinit_mode', CodeVerifier = 'code_verifier', + LaunchPriorityUuids = 'launch_priority_uuids', } export enum NonwrappedStorageKey { diff --git a/packages/snjs/lib/Services/Preferences/PreferencesService.ts b/packages/snjs/lib/Services/Preferences/PreferencesService.ts index 247cec985..3be576966 100644 --- a/packages/snjs/lib/Services/Preferences/PreferencesService.ts +++ b/packages/snjs/lib/Services/Preferences/PreferencesService.ts @@ -35,7 +35,7 @@ export class SNPreferencesService }) this.removeSyncObserver = syncService.addEventObserver((event) => { - if (event === SyncEvent.SyncCompletedWithAllItemsUploaded) { + if (event === SyncEvent.SyncCompletedWithAllItemsUploaded || event === SyncEvent.LocalDataIncrementalLoad) { void this.reload() } }) diff --git a/packages/snjs/lib/Services/Sync/SyncClientInterface.ts b/packages/snjs/lib/Services/Sync/SyncClientInterface.ts index 597d99867..7a479cff9 100644 --- a/packages/snjs/lib/Services/Sync/SyncClientInterface.ts +++ b/packages/snjs/lib/Services/Sync/SyncClientInterface.ts @@ -2,6 +2,8 @@ import { SyncOpStatus } from './SyncOpStatus' import { SyncOptions } from '@standardnotes/services' export interface SyncClientInterface { + setLaunchPriorityUuids(launchPriorityUuids: string[]): void + sync(options?: Partial): Promise isOutOfSync(): boolean diff --git a/packages/snjs/lib/Services/Sync/SyncService.ts b/packages/snjs/lib/Services/Sync/SyncService.ts index 76080516a..cdfb42c81 100644 --- a/packages/snjs/lib/Services/Sync/SyncService.ts +++ b/packages/snjs/lib/Services/Sync/SyncService.ts @@ -18,7 +18,7 @@ import { SNHistoryManager } from '../History/HistoryManager' import { SNLog } from '@Lib/Log' import { SNSessionManager } from '../Session/SessionManager' import { DiskStorageService } from '../Storage/DiskStorageService' -import { SortPayloadsByRecentAndContentPriority } from '@Lib/Services/Sync/Utils' +import { GetSortedPayloadsByPriority } from '@Lib/Services/Sync/Utils' import { SyncClientInterface } from './SyncClientInterface' import { SyncPromise } from './Types' import { SyncOpStatus } from '@Lib/Services/Sync/SyncOpStatus' @@ -56,6 +56,7 @@ import { PayloadEmitSource, getIncrementedDirtyIndex, getCurrentDirtyIndex, + ItemContent, } from '@standardnotes/models' import { AbstractService, @@ -158,6 +159,14 @@ export class SNSyncService } } + private get launchPriorityUuids() { + return this.storageService.getValue(StorageKey.LaunchPriorityUuids) ?? [] + } + + public setLaunchPriorityUuids(launchPriorityUuids: string[]) { + this.storageService.setValue(StorageKey.LaunchPriorityUuids, launchPriorityUuids) + } + public override deinit(): void { this.dealloced = true ;(this.sessionManager as unknown) = undefined @@ -272,15 +281,15 @@ export class SNSyncService }) .filter(isNotUndefined) - const payloads = SortPayloadsByRecentAndContentPriority(unsortedPayloads, this.localLoadPriorty) + const { itemsKeyPayloads, contentTypePriorityPayloads, remainingPayloads } = GetSortedPayloadsByPriority( + unsortedPayloads, + this.localLoadPriorty, + this.launchPriorityUuids, + ) - const itemsKeysPayloads = payloads.filter((payload) => { - return payload.content_type === ContentType.ItemsKey - }) + await this.processItemsKeysFirstDuringDatabaseLoad(itemsKeyPayloads) - subtractFromArray(payloads, itemsKeysPayloads) - - await this.processItemsKeysFirstDuringDatabaseLoad(itemsKeysPayloads) + await this.processPayloadBatch(contentTypePriorityPayloads) /** * Map in batches to give interface a chance to update. Note that total decryption @@ -288,45 +297,55 @@ export class SNSyncService * batches will result in the same time spent. It's the emitting/painting/rendering * that requires batch size optimization. */ - const payloadCount = payloads.length + const payloadCount = remainingPayloads.length const batchSize = this.options.loadBatchSize const numBatches = Math.ceil(payloadCount / batchSize) for (let batchIndex = 0; batchIndex < numBatches; batchIndex++) { const currentPosition = batchIndex * batchSize - const batch = payloads.slice(currentPosition, currentPosition + batchSize) - const encrypted: EncryptedPayloadInterface[] = [] - const nonencrypted: (DecryptedPayloadInterface | DeletedPayloadInterface)[] = [] - - for (const payload of batch) { - if (isEncryptedPayload(payload)) { - encrypted.push(payload) - } else { - nonencrypted.push(payload) - } - } - - const split: KeyedDecryptionSplit = { - usesItemsKeyWithKeyLookup: { - items: encrypted, - }, - } - - const results = await this.protocolService.decryptSplit(split) - - await this.payloadManager.emitPayloads([...nonencrypted, ...results], PayloadEmitSource.LocalDatabaseLoaded) - - void this.notifyEvent(SyncEvent.LocalDataIncrementalLoad) - - this.opStatus.setDatabaseLoadStatus(currentPosition, payloadCount, false) - - await sleep(1, false) + const batch = remainingPayloads.slice(currentPosition, currentPosition + batchSize) + await this.processPayloadBatch(batch, currentPosition, payloadCount) } this.databaseLoaded = true this.opStatus.setDatabaseLoadStatus(0, 0, true) } + private async processPayloadBatch( + batch: FullyFormedPayloadInterface[], + currentPosition?: number, + payloadCount?: number, + ) { + const encrypted: EncryptedPayloadInterface[] = [] + const nonencrypted: (DecryptedPayloadInterface | DeletedPayloadInterface)[] = [] + + for (const payload of batch) { + if (isEncryptedPayload(payload)) { + encrypted.push(payload) + } else { + nonencrypted.push(payload) + } + } + + const split: KeyedDecryptionSplit = { + usesItemsKeyWithKeyLookup: { + items: encrypted, + }, + } + + const results = await this.protocolService.decryptSplit(split) + + await this.payloadManager.emitPayloads([...nonencrypted, ...results], PayloadEmitSource.LocalDatabaseLoaded) + + void this.notifyEvent(SyncEvent.LocalDataIncrementalLoad) + + if (currentPosition != undefined && payloadCount != undefined) { + this.opStatus.setDatabaseLoadStatus(currentPosition, payloadCount, false) + } + + await sleep(1, false) + } + private setLastSyncToken(token: string) { this.syncToken = token return this.storageService.setValue(StorageKey.LastSyncToken, token) diff --git a/packages/snjs/lib/Services/Sync/Utils.spec.ts b/packages/snjs/lib/Services/Sync/Utils.spec.ts new file mode 100644 index 000000000..0b3490c72 --- /dev/null +++ b/packages/snjs/lib/Services/Sync/Utils.spec.ts @@ -0,0 +1,146 @@ +import { ContentType } from '@standardnotes/common' +import { FullyFormedPayloadInterface } from '@standardnotes/models' +import { GetSortedPayloadsByPriority } from './Utils' + +describe('GetSortedPayloadsByPriority', () => { + let payloads: FullyFormedPayloadInterface[] = [] + const contentTypePriority = [ContentType.ItemsKey, ContentType.UserPrefs, ContentType.Component, ContentType.Theme] + let launchPriorityUuids: string[] = [] + + it('should sort payloads based on content type priority', () => { + payloads = [ + { + content_type: ContentType.Theme, + } as FullyFormedPayloadInterface, + { + content_type: ContentType.UserPrefs, + } as FullyFormedPayloadInterface, + { + content_type: ContentType.Component, + } as FullyFormedPayloadInterface, + { + content_type: ContentType.ItemsKey, + } as FullyFormedPayloadInterface, + { + content_type: ContentType.Note, + } as FullyFormedPayloadInterface, + ] + + const { itemsKeyPayloads, contentTypePriorityPayloads, remainingPayloads } = GetSortedPayloadsByPriority( + payloads, + contentTypePriority, + launchPriorityUuids, + ) + + expect(itemsKeyPayloads.length).toBe(1) + expect(itemsKeyPayloads[0].content_type).toBe(ContentType.ItemsKey) + + expect(contentTypePriorityPayloads.length).toBe(3) + expect(contentTypePriorityPayloads[0].content_type).toBe(ContentType.UserPrefs) + expect(contentTypePriorityPayloads[1].content_type).toBe(ContentType.Component) + expect(contentTypePriorityPayloads[2].content_type).toBe(ContentType.Theme) + + expect(remainingPayloads.length).toBe(1) + expect(remainingPayloads[0].content_type).toBe(ContentType.Note) + }) + + it('should sort payloads based on launch priority uuids', () => { + const unprioritizedNoteUuid = 'unprioritized-note' + const unprioritizedTagUuid = 'unprioritized-tag' + + const prioritizedNoteUuid = 'prioritized-note' + const prioritizedTagUuid = 'prioritized-tag' + + payloads = [ + { + content_type: ContentType.Theme, + } as FullyFormedPayloadInterface, + { + content_type: ContentType.UserPrefs, + } as FullyFormedPayloadInterface, + { + content_type: ContentType.Component, + } as FullyFormedPayloadInterface, + { + content_type: ContentType.ItemsKey, + } as FullyFormedPayloadInterface, + { + content_type: ContentType.Note, + uuid: unprioritizedNoteUuid, + } as FullyFormedPayloadInterface, + { + content_type: ContentType.Tag, + uuid: unprioritizedTagUuid, + } as FullyFormedPayloadInterface, + { + content_type: ContentType.Note, + uuid: prioritizedNoteUuid, + } as FullyFormedPayloadInterface, + { + content_type: ContentType.Tag, + uuid: prioritizedTagUuid, + } as FullyFormedPayloadInterface, + ] + + launchPriorityUuids = [prioritizedNoteUuid, prioritizedTagUuid] + + const { itemsKeyPayloads, contentTypePriorityPayloads, remainingPayloads } = GetSortedPayloadsByPriority( + payloads, + contentTypePriority, + launchPriorityUuids, + ) + + expect(itemsKeyPayloads.length).toBe(1) + expect(itemsKeyPayloads[0].content_type).toBe(ContentType.ItemsKey) + + expect(contentTypePriorityPayloads.length).toBe(3) + expect(contentTypePriorityPayloads[0].content_type).toBe(ContentType.UserPrefs) + expect(contentTypePriorityPayloads[1].content_type).toBe(ContentType.Component) + expect(contentTypePriorityPayloads[2].content_type).toBe(ContentType.Theme) + + expect(remainingPayloads.length).toBe(4) + expect(remainingPayloads[0].uuid).toBe(prioritizedNoteUuid) + expect(remainingPayloads[1].uuid).toBe(prioritizedTagUuid) + expect(remainingPayloads[2].uuid).toBe(unprioritizedNoteUuid) + expect(remainingPayloads[3].uuid).toBe(unprioritizedTagUuid) + }) + + it('should sort payloads based on server updated date if same content type', () => { + const unprioritizedNoteUuid = 'unprioritized-note' + const unprioritizedTagUuid = 'unprioritized-tag' + + const prioritizedNoteUuid = 'prioritized-note' + const prioritizedTagUuid = 'prioritized-tag' + + payloads = [ + { + content_type: ContentType.Note, + uuid: unprioritizedNoteUuid, + serverUpdatedAt: new Date(1), + } as FullyFormedPayloadInterface, + { + content_type: ContentType.Tag, + uuid: unprioritizedTagUuid, + serverUpdatedAt: new Date(2), + } as FullyFormedPayloadInterface, + { + content_type: ContentType.Note, + uuid: prioritizedNoteUuid, + } as FullyFormedPayloadInterface, + { + content_type: ContentType.Tag, + uuid: prioritizedTagUuid, + } as FullyFormedPayloadInterface, + ] + + launchPriorityUuids = [prioritizedNoteUuid, prioritizedTagUuid] + + const { remainingPayloads } = GetSortedPayloadsByPriority(payloads, contentTypePriority, launchPriorityUuids) + + expect(remainingPayloads.length).toBe(4) + expect(remainingPayloads[0].uuid).toBe(prioritizedNoteUuid) + expect(remainingPayloads[1].uuid).toBe(prioritizedTagUuid) + expect(remainingPayloads[2].uuid).toBe(unprioritizedTagUuid) + expect(remainingPayloads[3].uuid).toBe(unprioritizedNoteUuid) + }) +}) diff --git a/packages/snjs/lib/Services/Sync/Utils.ts b/packages/snjs/lib/Services/Sync/Utils.ts index a12d7fda7..cf917eb4d 100644 --- a/packages/snjs/lib/Services/Sync/Utils.ts +++ b/packages/snjs/lib/Services/Sync/Utils.ts @@ -1,3 +1,4 @@ +import { UuidString } from '@Lib/Types' import { ContentType } from '@standardnotes/common' import { FullyFormedPayloadInterface } from '@standardnotes/models' @@ -6,9 +7,9 @@ import { FullyFormedPayloadInterface } from '@standardnotes/models' * whereby the earlier a content_type appears in the priorityList, * the earlier it will appear in the resulting sorted array. */ -export function SortPayloadsByRecentAndContentPriority( +function SortPayloadsByRecentAndContentPriority( payloads: FullyFormedPayloadInterface[], - priorityList: ContentType[], + contentTypePriorityList: ContentType[], ): FullyFormedPayloadInterface[] { return payloads.sort((a, b) => { const dateResult = new Date(b.serverUpdatedAt).getTime() - new Date(a.serverUpdatedAt).getTime() @@ -16,18 +17,15 @@ export function SortPayloadsByRecentAndContentPriority( let aPriority = 0 let bPriority = 0 - if (priorityList) { - aPriority = priorityList.indexOf(a.content_type) - bPriority = priorityList.indexOf(b.content_type) + aPriority = contentTypePriorityList.indexOf(a.content_type) + bPriority = contentTypePriorityList.indexOf(b.content_type) - if (aPriority === -1) { - /** Not found in list, not prioritized. Set it to max value */ - aPriority = priorityList.length - } - if (bPriority === -1) { - /** Not found in list, not prioritized. Set it to max value */ - bPriority = priorityList.length - } + if (aPriority === -1) { + aPriority = contentTypePriorityList.length + } + + if (bPriority === -1) { + bPriority = contentTypePriorityList.length } if (aPriority === bPriority) { @@ -41,3 +39,76 @@ export function SortPayloadsByRecentAndContentPriority( } }) } + +/** + * Sorts payloads according by most recently modified first, according to the priority, + * whereby the earlier a uuid appears in the priorityList, + * the earlier it will appear in the resulting sorted array. + */ +function SortPayloadsByRecentAndUuidPriority( + payloads: FullyFormedPayloadInterface[], + uuidPriorityList: UuidString[], +): FullyFormedPayloadInterface[] { + return payloads.sort((a, b) => { + const dateResult = new Date(b.serverUpdatedAt).getTime() - new Date(a.serverUpdatedAt).getTime() + + let aPriority = 0 + let bPriority = 0 + + aPriority = uuidPriorityList.indexOf(a.uuid) + bPriority = uuidPriorityList.indexOf(b.uuid) + + if (aPriority === -1) { + aPriority = uuidPriorityList.length + } + + if (bPriority === -1) { + bPriority = uuidPriorityList.length + } + + if (aPriority === bPriority) { + return dateResult + } + + if (aPriority < bPriority) { + return -1 + } else { + return 1 + } + }) +} + +export function GetSortedPayloadsByPriority( + payloads: FullyFormedPayloadInterface[], + contentTypePriorityList: ContentType[], + uuidPriorityList: UuidString[], +): { + itemsKeyPayloads: FullyFormedPayloadInterface[] + contentTypePriorityPayloads: FullyFormedPayloadInterface[] + remainingPayloads: FullyFormedPayloadInterface[] +} { + const itemsKeyPayloads: FullyFormedPayloadInterface[] = [] + const contentTypePriorityPayloads: FullyFormedPayloadInterface[] = [] + const remainingPayloads: FullyFormedPayloadInterface[] = [] + + for (let index = 0; index < payloads.length; index++) { + const payload = payloads[index] + + if (payload.content_type === ContentType.ItemsKey) { + itemsKeyPayloads.push(payload) + } else if (contentTypePriorityList.includes(payload.content_type)) { + contentTypePriorityPayloads.push(payload) + } else { + remainingPayloads.push(payload) + } + } + + return { + itemsKeyPayloads, + contentTypePriorityPayloads: SortPayloadsByRecentAndContentPriority( + contentTypePriorityPayloads, + contentTypePriorityList, + ), + remainingPayloads: SortPayloadsByRecentAndUuidPriority(remainingPayloads, uuidPriorityList), + } +} diff --git a/packages/snjs/mocha/sync_tests/online.test.js b/packages/snjs/mocha/sync_tests/online.test.js index 2621a059c..081d696ff 100644 --- a/packages/snjs/mocha/sync_tests/online.test.js +++ b/packages/snjs/mocha/sync_tests/online.test.js @@ -672,10 +672,10 @@ describe('online syncing', function () { const payload = Factory.createStorageItemPayload(contentTypes[Math.floor(i / 2)]) originalPayloads.push(payload) } - const sorted = SortPayloadsByRecentAndContentPriority(originalPayloads, ['C', 'A', 'B']) - expect(sorted[0].content_type).to.equal('C') - expect(sorted[2].content_type).to.equal('A') - expect(sorted[4].content_type).to.equal('B') + const { contentTypePriorityPayloads } = GetSortedPayloadsByPriority(originalPayloads, ['C', 'A', 'B']) + expect(contentTypePriorityPayloads[0].content_type).to.equal('C') + expect(contentTypePriorityPayloads[2].content_type).to.equal('A') + expect(contentTypePriorityPayloads[4].content_type).to.equal('B') }) it('should sign in and retrieve large number of items', async function () { diff --git a/packages/ui-services/src/StatePersistence/StatePersistence.ts b/packages/ui-services/src/StatePersistence/StatePersistence.ts new file mode 100644 index 000000000..ecd5a6a35 --- /dev/null +++ b/packages/ui-services/src/StatePersistence/StatePersistence.ts @@ -0,0 +1,17 @@ +export enum PersistenceKey { + SelectedItemsController = 'selected-items-controller', + NavigationController = 'navigation-controller', +} + +export type SelectionControllerPersistableValue = { + selectedUuids: string[] +} + +export type NavigationControllerPersistableValue = { + selectedTagUuid: string +} + +export type PersistedStateValue = { + [PersistenceKey.SelectedItemsController]: SelectionControllerPersistableValue + [PersistenceKey.NavigationController]: NavigationControllerPersistableValue +} diff --git a/packages/ui-services/src/Storage/LocalStorage.ts b/packages/ui-services/src/Storage/LocalStorage.ts index d905bc8a0..44d17b9fc 100644 --- a/packages/ui-services/src/Storage/LocalStorage.ts +++ b/packages/ui-services/src/Storage/LocalStorage.ts @@ -1,8 +1,11 @@ +import { PersistedStateValue } from '../StatePersistence/StatePersistence' + export enum StorageKey { AnonymousUserId = 'AnonymousUserId', ShowBetaWarning = 'ShowBetaWarning', ShowNoAccountWarning = 'ShowNoAccountWarning', FilesNavigationEnabled = 'FilesNavigationEnabled', + MasterStatePersistenceKey = 'master-persistence-key', } export type StorageValue = { @@ -10,6 +13,7 @@ export type StorageValue = { [StorageKey.ShowBetaWarning]: boolean [StorageKey.ShowNoAccountWarning]: boolean [StorageKey.FilesNavigationEnabled]: boolean + [StorageKey.MasterStatePersistenceKey]: PersistedStateValue } export const storage = { diff --git a/packages/ui-services/src/index.ts b/packages/ui-services/src/index.ts index 034127bab..c89f8c0eb 100644 --- a/packages/ui-services/src/index.ts +++ b/packages/ui-services/src/index.ts @@ -20,3 +20,4 @@ export * from './Storage/LocalStorage' export * from './Theme/ThemeManager' export * from './Toast/ToastService' export * from './Toast/ToastServiceInterface' +export * from './StatePersistence/StatePersistence' diff --git a/packages/web/src/javascripts/Components/ContentListView/Header/DisplayOptionsMenu.tsx b/packages/web/src/javascripts/Components/ContentListView/Header/DisplayOptionsMenu.tsx index 746e106e4..2528ff174 100644 --- a/packages/web/src/javascripts/Components/ContentListView/Header/DisplayOptionsMenu.tsx +++ b/packages/web/src/javascripts/Components/ContentListView/Header/DisplayOptionsMenu.tsx @@ -118,7 +118,7 @@ const DisplayOptionsMenu: FunctionComponent = ({ void changePreferences({ sortBy: sort }) } }, - [preferences, changePreferences, toggleSortReverse], + [preferences.sortBy, toggleSortReverse, changePreferences], ) const toggleSortByDateModified = useCallback(() => { diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/General/Persistence.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/General/Persistence.tsx index 71856a120..0e2e6571a 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/General/Persistence.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/General/Persistence.tsx @@ -17,6 +17,12 @@ const Persistence = ({ application }: Props) => { const toggleStatePersistence = (shouldPersist: boolean) => { application.setValue(ShouldPersistNoteStateKey, shouldPersist) setShouldPersistNoteState(shouldPersist) + + if (shouldPersist) { + application.getViewControllerManager().persistValues() + } else { + application.getViewControllerManager().clearPersistedValues() + } } return ( diff --git a/packages/web/src/javascripts/Controllers/Abstract/PersistenceService.ts b/packages/web/src/javascripts/Controllers/Abstract/PersistenceService.ts index ac00c12e4..422cbd86d 100644 --- a/packages/web/src/javascripts/Controllers/Abstract/PersistenceService.ts +++ b/packages/web/src/javascripts/Controllers/Abstract/PersistenceService.ts @@ -1,20 +1,12 @@ import { WebApplication } from '@/Application/Application' import { ShouldPersistNoteStateKey } from '@/Components/Preferences/Panes/General/Persistence' -import { ApplicationEvent, InternalEventBus } from '@standardnotes/snjs' +import { ApplicationEvent, ContentType, InternalEventBus } from '@standardnotes/snjs' +import { PersistedStateValue, StorageKey } from '@standardnotes/ui-services' import { CrossControllerEvent } from '../CrossControllerEvent' -const MasterPersistenceKey = 'master-persistence-key' - -export enum PersistenceKey { - SelectedItemsController = 'selected-items-controller', - NavigationController = 'navigation-controller', - ItemListController = 'item-list-controller', -} - -export type MasterPersistedValue = Record - export class PersistenceService { private unsubAppEventObserver: () => void + private didHydrateOnce = false constructor(private application: WebApplication, private eventBus: InternalEventBus) { this.unsubAppEventObserver = this.application.addEventObserver(async (eventName) => { @@ -27,31 +19,54 @@ export class PersistenceService { } async onAppEvent(eventName: ApplicationEvent) { - if (eventName === ApplicationEvent.LocalDataLoaded) { - let shouldHydrateState = this.application.getValue(ShouldPersistNoteStateKey) + if (eventName === ApplicationEvent.LocalDataLoaded && !this.didHydrateOnce) { + this.hydratePersistedValues() + this.didHydrateOnce = true + } else if (eventName === ApplicationEvent.LocalDataIncrementalLoad) { + const canHydrate = this.application.items.getItems([ContentType.Note, ContentType.Tag]).length > 0 - if (typeof shouldHydrateState === 'undefined') { - this.application.setValue(ShouldPersistNoteStateKey, true) - shouldHydrateState = true + if (!canHydrate) { + return } - this.eventBus.publish({ - type: CrossControllerEvent.HydrateFromPersistedValues, - payload: shouldHydrateState ? this.getPersistedValues() : undefined, - }) + this.hydratePersistedValues() + this.didHydrateOnce = true } } - persistValues(values: MasterPersistedValue): void { + get persistenceEnabled() { + return this.application.getValue(ShouldPersistNoteStateKey) ?? true + } + + hydratePersistedValues = () => { + this.eventBus.publish({ + type: CrossControllerEvent.HydrateFromPersistedValues, + payload: this.persistenceEnabled ? this.getPersistedValues() : undefined, + }) + } + + persistValues(values: PersistedStateValue): void { if (!this.application.isDatabaseLoaded()) { return } - this.application.setValue(MasterPersistenceKey, values) + if (!this.persistenceEnabled) { + return + } + + this.application.setValue(StorageKey.MasterStatePersistenceKey, values) } - getPersistedValues(): MasterPersistedValue { - return this.application.getValue(MasterPersistenceKey) as MasterPersistedValue + clearPersistedValues(): void { + if (!this.application.isDatabaseLoaded()) { + return + } + + this.application.removeValue(StorageKey.MasterStatePersistenceKey) + } + + getPersistedValues(): PersistedStateValue { + return this.application.getValue(StorageKey.MasterStatePersistenceKey) as PersistedStateValue } deinit() { diff --git a/packages/web/src/javascripts/Controllers/ItemList/ItemListController.ts b/packages/web/src/javascripts/Controllers/ItemList/ItemListController.ts index 69992c153..4a91c1a22 100644 --- a/packages/web/src/javascripts/Controllers/ItemList/ItemListController.ts +++ b/packages/web/src/javascripts/Controllers/ItemList/ItemListController.ts @@ -37,7 +37,6 @@ import { PrefDefaults } from '@/Constants/PrefDefaults' import dayjs from 'dayjs' import { LinkingController } from '../LinkingController' import { AbstractViewController } from '../Abstract/AbstractViewController' -import { Persistable } from '../Abstract/Persistable' import { log, LoggingDomain } from '@/Logging' const MinNoteCellHeight = 51.0 @@ -55,14 +54,7 @@ enum ItemsReloadSource { FilterTextChange, } -export type ItemListControllerPersistableValue = { - displayOptions: DisplayOptions -} - -export class ItemListController - extends AbstractViewController - implements Persistable, InternalEventHandlerInterface -{ +export class ItemListController extends AbstractViewController implements InternalEventHandlerInterface { completedFullSync = false noteFilterText = '' notes: SNNote[] = [] @@ -126,14 +118,6 @@ export class ItemListController }), ) - this.disposers.push( - reaction( - () => [this.navigationController.selected], - () => { - void this.reloadDisplayPreferences() - }, - ), - ) this.disposers.push( application.streamItems([ContentType.Tag], async ({ changed, inserted }) => { const tags = [...changed, ...inserted] @@ -142,10 +126,9 @@ export class ItemListController if (!didReloadItems) { /** A tag could have changed its relationships, so we need to reload the filter */ this.reloadNotesDisplayOptions() + void this.reloadItems(ItemsReloadSource.ItemStream) } - void this.reloadItems(ItemsReloadSource.ItemStream) - if (this.navigationController.selected && findInArray(tags, 'uuid', this.navigationController.selected.uuid)) { /** Tag title could have changed */ this.reloadPanelTitle() @@ -233,8 +216,6 @@ export class ItemListController optionsSubtitle: computed, activeControllerItem: computed, - - hydrateFromPersistedValue: action, }) window.onresize = () => { @@ -242,21 +223,6 @@ export class ItemListController } } - getPersistableValue = (): ItemListControllerPersistableValue => { - return { - displayOptions: this.displayOptions, - } - } - - hydrateFromPersistedValue = (state: ItemListControllerPersistableValue | undefined) => { - if (!state) { - return - } - if (state.displayOptions) { - this.displayOptions = state.displayOptions - } - } - async handleEvent(event: InternalEventInterface): Promise { if (event.type === CrossControllerEvent.TagChanged) { const payload = event.payload as { userTriggered: boolean } @@ -535,6 +501,7 @@ export class ItemListController } newDisplayOptions.sortBy = sortBy + const currentSortDirection = this.displayOptions.sortDirection newDisplayOptions.sortDirection = useBoolean( selectedTag?.preferences?.sortReverse, @@ -613,18 +580,14 @@ export class ItemListController await this.reloadItems(ItemsReloadSource.DisplayOptionsChange) - if ( - newDisplayOptions.sortBy !== currentSortBy && - this.shouldSelectFirstItem(ItemsReloadSource.DisplayOptionsChange) - ) { + const didSortByChange = currentSortBy !== this.displayOptions.sortBy + const didSortDirectionChange = currentSortDirection !== this.displayOptions.sortDirection + const didSortPrefChange = didSortByChange || didSortDirectionChange + + if (didSortPrefChange && this.shouldSelectFirstItem(ItemsReloadSource.DisplayOptionsChange)) { await this.selectFirstItem() } - this.eventBus.publish({ - type: CrossControllerEvent.RequestValuePersistence, - payload: undefined, - }) - return { didReloadItems: true } } @@ -825,9 +788,12 @@ export class ItemListController this.resetPagination() - this.reloadNotesDisplayOptions() + const { didReloadItems } = await this.reloadDisplayPreferences() - await this.reloadItems(userTriggered ? ItemsReloadSource.UserTriggeredTagChange : ItemsReloadSource.TagChange) + if (!didReloadItems) { + this.reloadNotesDisplayOptions() + void this.reloadItems(userTriggered ? ItemsReloadSource.UserTriggeredTagChange : ItemsReloadSource.TagChange) + } } onFilterEnter = () => { diff --git a/packages/web/src/javascripts/Controllers/Navigation/NavigationController.ts b/packages/web/src/javascripts/Controllers/Navigation/NavigationController.ts index c0aaf68a3..4cc8cc3e3 100644 --- a/packages/web/src/javascripts/Controllers/Navigation/NavigationController.ts +++ b/packages/web/src/javascripts/Controllers/Navigation/NavigationController.ts @@ -1,4 +1,4 @@ -import { confirmDialog } from '@standardnotes/ui-services' +import { confirmDialog, NavigationControllerPersistableValue } from '@standardnotes/ui-services' import { STRING_DELETE_TAG } from '@/Constants/Strings' import { MAX_MENU_SIZE_MULTIPLIER, MENU_MARGIN_FROM_APP_BORDER, SMART_TAGS_FEATURE_NAME } from '@/Constants/Constants' import { @@ -28,10 +28,6 @@ import { AbstractViewController } from '../Abstract/AbstractViewController' import { Persistable } from '../Abstract/Persistable' import { TagListSectionType } from '@/Components/Tags/TagListSection' -export type NavigationControllerPersistableValue = { - selectedTagUuid: AnyTag['uuid'] -} - export class NavigationController extends AbstractViewController implements Persistable diff --git a/packages/web/src/javascripts/Controllers/SelectedItemsController.ts b/packages/web/src/javascripts/Controllers/SelectedItemsController.ts index f73773685..604501a82 100644 --- a/packages/web/src/javascripts/Controllers/SelectedItemsController.ts +++ b/packages/web/src/javascripts/Controllers/SelectedItemsController.ts @@ -11,6 +11,7 @@ import { isFile, Uuids, } from '@standardnotes/snjs' +import { SelectionControllerPersistableValue } from '@standardnotes/ui-services' import { action, computed, makeObservable, observable, reaction, runInAction } from 'mobx' import { WebApplication } from '../Application/Application' import { AbstractViewController } from './Abstract/AbstractViewController' @@ -18,10 +19,6 @@ import { Persistable } from './Abstract/Persistable' import { CrossControllerEvent } from './CrossControllerEvent' import { ItemListController } from './ItemList/ItemListController' -export type SelectionControllerPersistableValue = { - selectedUuids: UuidString[] -} - export class SelectedItemsController extends AbstractViewController implements Persistable diff --git a/packages/web/src/javascripts/Controllers/ViewControllerManager.ts b/packages/web/src/javascripts/Controllers/ViewControllerManager.ts index 6544f850b..eac968069 100644 --- a/packages/web/src/javascripts/Controllers/ViewControllerManager.ts +++ b/packages/web/src/javascripts/Controllers/ViewControllerManager.ts @@ -1,5 +1,12 @@ import { PaneController } from './PaneController' -import { storage, StorageKey, ToastService, ToastServiceInterface } from '@standardnotes/ui-services' +import { + PersistedStateValue, + PersistenceKey, + storage, + StorageKey, + ToastService, + ToastServiceInterface, +} from '@standardnotes/ui-services' import { WebApplication } from '@/Application/Application' import { AccountMenuController } from '@/Controllers/AccountMenu/AccountMenuController' import { destroyAllObjectProperties } from '@/Utils' @@ -18,7 +25,7 @@ import { ActionsMenuController } from './ActionsMenuController' import { FeaturesController } from './FeaturesController' import { FilesController } from './FilesController' import { NotesController } from './NotesController' -import { ItemListController, ItemListControllerPersistableValue } from './ItemList/ItemListController' +import { ItemListController } from './ItemList/ItemListController' import { NoAccountWarningController } from './NoAccountWarningController' import { PreferencesController } from './PreferencesController' import { PurchaseFlowController } from './PurchaseFlow/PurchaseFlowController' @@ -26,12 +33,12 @@ import { QuickSettingsController } from './QuickSettingsController' import { SearchOptionsController } from './SearchOptionsController' import { SubscriptionController } from './Subscription/SubscriptionController' import { SyncStatusController } from './SyncStatusController' -import { NavigationController, NavigationControllerPersistableValue } from './Navigation/NavigationController' +import { NavigationController } from './Navigation/NavigationController' import { FilePreviewModalController } from './FilePreviewModalController' -import { SelectedItemsController, SelectionControllerPersistableValue } from './SelectedItemsController' +import { SelectedItemsController } from './SelectedItemsController' import { HistoryModalController } from './NoteHistory/HistoryModalController' import { LinkingController } from './LinkingController' -import { MasterPersistedValue, PersistenceKey, PersistenceService } from './Abstract/PersistenceService' +import { PersistenceService } from './Abstract/PersistenceService' import { CrossControllerEvent } from './CrossControllerEvent' import { EventObserverInterface } from '@/Event/EventObserverInterface' import { ApplicationEventObserver } from '@/Event/ApplicationEventObserver' @@ -263,29 +270,40 @@ export class ViewControllerManager implements InternalEventHandlerInterface { } persistValues = (): void => { - const values: MasterPersistedValue = { + const values: PersistedStateValue = { [PersistenceKey.SelectedItemsController]: this.selectionController.getPersistableValue(), [PersistenceKey.NavigationController]: this.navigationController.getPersistableValue(), - [PersistenceKey.ItemListController]: this.itemListController.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) } - hydrateFromPersistedValues = (values: MasterPersistedValue | undefined): void => { - const itemListState = values?.[PersistenceKey.ItemListController] as ItemListControllerPersistableValue - this.itemListController.hydrateFromPersistedValue(itemListState) + clearPersistedValues = (): void => { + this.persistenceService.clearPersistedValues() + } - const selectedItemsState = values?.[PersistenceKey.SelectedItemsController] as SelectionControllerPersistableValue - this.selectionController.hydrateFromPersistedValue(selectedItemsState) - - const navigationState = values?.[PersistenceKey.NavigationController] as NavigationControllerPersistableValue + 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 { if (event.type === CrossControllerEvent.HydrateFromPersistedValues) { - this.hydrateFromPersistedValues(event.payload as MasterPersistedValue | undefined) + this.hydrateFromPersistedValues(event.payload as PersistedStateValue | undefined) } else if (event.type === CrossControllerEvent.RequestValuePersistence) { this.persistValues() }