internal: incomplete vault systems behind feature flag (#2340)
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
export type AsymmetricMessageDataCommon = {
|
||||
recipientUuid: string
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { AsymmetricMessageSenderKeypairChanged } from './MessageTypes/AsymmetricMessageSenderKeypairChanged'
|
||||
import { AsymmetricMessageSharedVaultInvite } from './MessageTypes/AsymmetricMessageSharedVaultInvite'
|
||||
import { AsymmetricMessageSharedVaultMetadataChanged } from './MessageTypes/AsymmetricMessageSharedVaultMetadataChanged'
|
||||
import { AsymmetricMessageSharedVaultRootKeyChanged } from './MessageTypes/AsymmetricMessageSharedVaultRootKeyChanged'
|
||||
import { AsymmetricMessageTrustedContactShare } from './MessageTypes/AsymmetricMessageTrustedContactShare'
|
||||
|
||||
export type AsymmetricMessagePayload =
|
||||
| AsymmetricMessageSharedVaultRootKeyChanged
|
||||
| AsymmetricMessageTrustedContactShare
|
||||
| AsymmetricMessageSenderKeypairChanged
|
||||
| AsymmetricMessageSharedVaultInvite
|
||||
| AsymmetricMessageSharedVaultMetadataChanged
|
||||
@@ -0,0 +1,7 @@
|
||||
export enum AsymmetricMessagePayloadType {
|
||||
ContactShare = 'contact-share',
|
||||
SharedVaultRootKeyChanged = 'shared-vault-root-key-changed',
|
||||
SenderKeypairChanged = 'sender-keypair-changed',
|
||||
SharedVaultInvite = 'shared-vault-invite',
|
||||
SharedVaultMetadataChanged = 'shared-vault-metadata-changed',
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { AsymmetricMessageDataCommon } from '../AsymmetricMessageDataCommon'
|
||||
import { AsymmetricMessagePayloadType } from '../AsymmetricMessagePayloadType'
|
||||
|
||||
export type AsymmetricMessageSenderKeypairChanged = {
|
||||
type: AsymmetricMessagePayloadType.SenderKeypairChanged
|
||||
data: AsymmetricMessageDataCommon & {
|
||||
newEncryptionPublicKey: string
|
||||
newSigningPublicKey: string
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { KeySystemRootKeyContentSpecialized } from '../../../Syncable/KeySystemRootKey/KeySystemRootKeyContent'
|
||||
import { TrustedContactContentSpecialized } from '../../../Syncable/TrustedContact/TrustedContactContent'
|
||||
import { AsymmetricMessageDataCommon } from '../AsymmetricMessageDataCommon'
|
||||
import { AsymmetricMessagePayloadType } from '../AsymmetricMessagePayloadType'
|
||||
|
||||
export type AsymmetricMessageSharedVaultInvite = {
|
||||
type: AsymmetricMessagePayloadType.SharedVaultInvite
|
||||
data: AsymmetricMessageDataCommon & {
|
||||
rootKey: KeySystemRootKeyContentSpecialized
|
||||
trustedContacts: TrustedContactContentSpecialized[]
|
||||
metadata: {
|
||||
name: string
|
||||
description?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { AsymmetricMessageDataCommon } from '../AsymmetricMessageDataCommon'
|
||||
import { AsymmetricMessagePayloadType } from '../AsymmetricMessagePayloadType'
|
||||
|
||||
export type AsymmetricMessageSharedVaultMetadataChanged = {
|
||||
type: AsymmetricMessagePayloadType.SharedVaultMetadataChanged
|
||||
data: AsymmetricMessageDataCommon & {
|
||||
sharedVaultUuid: string
|
||||
name: string
|
||||
description?: string
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { KeySystemRootKeyContentSpecialized } from '../../../Syncable/KeySystemRootKey/KeySystemRootKeyContent'
|
||||
import { AsymmetricMessageDataCommon } from '../AsymmetricMessageDataCommon'
|
||||
import { AsymmetricMessagePayloadType } from '../AsymmetricMessagePayloadType'
|
||||
|
||||
export type AsymmetricMessageSharedVaultRootKeyChanged = {
|
||||
type: AsymmetricMessagePayloadType.SharedVaultRootKeyChanged
|
||||
data: AsymmetricMessageDataCommon & { rootKey: KeySystemRootKeyContentSpecialized }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { TrustedContactContentSpecialized } from '../../../Syncable/TrustedContact/TrustedContactContent'
|
||||
import { AsymmetricMessageDataCommon } from '../AsymmetricMessageDataCommon'
|
||||
import { AsymmetricMessagePayloadType } from '../AsymmetricMessagePayloadType'
|
||||
|
||||
export type AsymmetricMessageTrustedContactShare = {
|
||||
type: AsymmetricMessagePayloadType.ContactShare
|
||||
data: AsymmetricMessageDataCommon & { trustedContact: TrustedContactContentSpecialized }
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { ItemCounter } from './ItemCounter'
|
||||
import { NoteContent } from '../../../Syncable/Note/NoteContent'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { DecryptedItem, EncryptedItem } from '../../../Abstract/Item'
|
||||
import { DecryptedPayload, EncryptedPayload, PayloadTimestampDefaults } from '../../../Abstract/Payload'
|
||||
import { ItemCollection } from './ItemCollection'
|
||||
import { FillItemContent } from '../../../Abstract/Content/ItemContent'
|
||||
import { TagItemsIndex } from './TagItemsIndex'
|
||||
import { ItemDelta } from '../../Index/ItemDelta'
|
||||
import { AnyItemInterface } from '../../../Abstract/Item/Interfaces/UnionTypes'
|
||||
|
||||
@@ -48,7 +48,7 @@ describe('tag notes index', () => {
|
||||
|
||||
it('should count both notes and files', () => {
|
||||
const collection = new ItemCollection()
|
||||
const index = new TagItemsIndex(collection)
|
||||
const index = new ItemCounter(collection)
|
||||
|
||||
const decryptedNote = createDecryptedItem('note')
|
||||
const decryptedFile = createDecryptedItem('file')
|
||||
@@ -61,7 +61,7 @@ describe('tag notes index', () => {
|
||||
|
||||
it('should decrement count after decrypted note becomes errored', () => {
|
||||
const collection = new ItemCollection()
|
||||
const index = new TagItemsIndex(collection)
|
||||
const index = new ItemCounter(collection)
|
||||
|
||||
const decryptedItem = createDecryptedItem()
|
||||
collection.set(decryptedItem)
|
||||
@@ -4,25 +4,28 @@ import { isTag, SNTag } from '../../../Syncable/Tag/Tag'
|
||||
import { SNIndex } from '../../Index/SNIndex'
|
||||
import { ItemCollection } from './ItemCollection'
|
||||
import { ItemDelta } from '../../Index/ItemDelta'
|
||||
import { isDecryptedItem, ItemInterface } from '../../../Abstract/Item'
|
||||
import { DecryptedItemInterface, isDecryptedItem, ItemInterface } from '../../../Abstract/Item'
|
||||
import { CriteriaValidatorInterface } from '../../Display/Validator/CriteriaValidatorInterface'
|
||||
import { CollectionCriteriaValidator } from '../../Display/Validator/CollectionCriteriaValidator'
|
||||
import { ExcludeVaultsCriteriaValidator } from '../../Display/Validator/ExcludeVaultsCriteriaValidator'
|
||||
import { ExclusiveVaultCriteriaValidator } from '../../Display/Validator/ExclusiveVaultCriteriaValidator'
|
||||
import { HiddenContentCriteriaValidator } from '../../Display/Validator/HiddenContentCriteriaValidator'
|
||||
import { CustomFilterCriteriaValidator } from '../../Display/Validator/CustomFilterCriteriaValidator'
|
||||
import { AnyDisplayOptions, VaultDisplayOptions } from '../../Display'
|
||||
import { isExclusioanaryOptionsValue } from '../../Display/VaultDisplayOptionsTypes'
|
||||
|
||||
type AllNotesUuidSignifier = undefined
|
||||
export type TagItemCountChangeObserver = (tagUuid: string | AllNotesUuidSignifier) => void
|
||||
|
||||
export class TagItemsIndex implements SNIndex {
|
||||
export class ItemCounter implements SNIndex {
|
||||
private tagToItemsMap: Partial<Record<string, Set<string>>> = {}
|
||||
private allCountableItems = new Set<string>()
|
||||
private countableItemsByType = new Map<ContentType, Set<string>>()
|
||||
private displayOptions?: AnyDisplayOptions
|
||||
private vaultDisplayOptions?: VaultDisplayOptions
|
||||
|
||||
constructor(private collection: ItemCollection, public observers: TagItemCountChangeObserver[] = []) {}
|
||||
|
||||
private isItemCountable = (item: ItemInterface) => {
|
||||
if (isDecryptedItem(item)) {
|
||||
return !item.archived && !item.trashed && !item.conflictOf
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public addCountChangeObserver(observer: TagItemCountChangeObserver): () => void {
|
||||
this.observers.push(observer)
|
||||
|
||||
@@ -32,10 +35,14 @@ export class TagItemsIndex implements SNIndex {
|
||||
}
|
||||
}
|
||||
|
||||
private notifyObservers(tagUuid: string | undefined) {
|
||||
for (const observer of this.observers) {
|
||||
observer(tagUuid)
|
||||
}
|
||||
public setDisplayOptions(options: AnyDisplayOptions) {
|
||||
this.displayOptions = options
|
||||
this.receiveItemChanges(this.collection.all())
|
||||
}
|
||||
|
||||
public setVaultDisplayOptions(options: VaultDisplayOptions) {
|
||||
this.vaultDisplayOptions = options
|
||||
this.receiveItemChanges(this.collection.all())
|
||||
}
|
||||
|
||||
public allCountableItemsCount(): number {
|
||||
@@ -64,6 +71,47 @@ export class TagItemsIndex implements SNIndex {
|
||||
this.receiveTagChanges(tags)
|
||||
}
|
||||
|
||||
private passesAllFilters(element: DecryptedItemInterface): boolean {
|
||||
if (!this.displayOptions) {
|
||||
return true
|
||||
}
|
||||
|
||||
const filters: CriteriaValidatorInterface[] = [new CollectionCriteriaValidator(this.collection, element)]
|
||||
|
||||
if (this.vaultDisplayOptions) {
|
||||
const options = this.vaultDisplayOptions.getOptions()
|
||||
if (isExclusioanaryOptionsValue(options)) {
|
||||
filters.push(new ExcludeVaultsCriteriaValidator([...options.exclude, ...options.locked], element))
|
||||
} else {
|
||||
filters.push(new ExclusiveVaultCriteriaValidator(options.exclusive, element))
|
||||
}
|
||||
}
|
||||
|
||||
if ('hiddenContentTypes' in this.displayOptions && this.displayOptions.hiddenContentTypes) {
|
||||
filters.push(new HiddenContentCriteriaValidator(this.displayOptions.hiddenContentTypes, element))
|
||||
}
|
||||
|
||||
if ('customFilter' in this.displayOptions && this.displayOptions.customFilter) {
|
||||
filters.push(new CustomFilterCriteriaValidator(this.displayOptions.customFilter, element))
|
||||
}
|
||||
|
||||
return filters.every((f) => f.passes())
|
||||
}
|
||||
|
||||
private isItemCountable = (item: ItemInterface) => {
|
||||
if (isDecryptedItem(item)) {
|
||||
const passesFilters = this.passesAllFilters(item)
|
||||
return passesFilters && !item.archived && !item.trashed && !item.conflictOf
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private notifyObservers(tagUuid: string | undefined) {
|
||||
for (const observer of this.observers) {
|
||||
observer(tagUuid)
|
||||
}
|
||||
}
|
||||
|
||||
private receiveTagChanges(tags: SNTag[]): void {
|
||||
for (const tag of tags) {
|
||||
const uuids = tag.references
|
||||
@@ -1,15 +1,16 @@
|
||||
import { ConflictDelta } from './Conflict'
|
||||
import { PayloadEmitSource } from '../../Abstract/Payload'
|
||||
import { FullyFormedPayloadInterface, PayloadEmitSource } from '../../Abstract/Payload'
|
||||
import { ImmutablePayloadCollection } from '../Collection/Payload/ImmutablePayloadCollection'
|
||||
import { HistoryMap } from '../History'
|
||||
import { extendSyncDelta, SyncDeltaEmit } from './Abstract/DeltaEmit'
|
||||
import { SyncDeltaInterface } from './Abstract/SyncDeltaInterface'
|
||||
import { payloadByFinalizingSyncState } from './Utilities/ApplyDirtyState'
|
||||
import { ConflictConflictingDataParams } from '@standardnotes/responses'
|
||||
|
||||
export class DeltaRemoteDataConflicts implements SyncDeltaInterface {
|
||||
constructor(
|
||||
readonly baseCollection: ImmutablePayloadCollection,
|
||||
readonly applyCollection: ImmutablePayloadCollection,
|
||||
readonly conflicts: ConflictConflictingDataParams<FullyFormedPayloadInterface>[],
|
||||
readonly historyMap: HistoryMap,
|
||||
) {}
|
||||
|
||||
@@ -20,18 +21,18 @@ export class DeltaRemoteDataConflicts implements SyncDeltaInterface {
|
||||
source: PayloadEmitSource.RemoteRetrieved,
|
||||
}
|
||||
|
||||
for (const apply of this.applyCollection.all()) {
|
||||
const base = this.baseCollection.find(apply.uuid)
|
||||
for (const conflict of this.conflicts) {
|
||||
const base = this.baseCollection.find(conflict.server_item.uuid)
|
||||
|
||||
const isBaseDeleted = base == undefined
|
||||
|
||||
if (isBaseDeleted) {
|
||||
result.emits.push(payloadByFinalizingSyncState(apply, this.baseCollection))
|
||||
result.emits.push(payloadByFinalizingSyncState(conflict.server_item, this.baseCollection))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
const delta = new ConflictDelta(this.baseCollection, base, apply, this.historyMap)
|
||||
const delta = new ConflictDelta(this.baseCollection, base, conflict.server_item, this.historyMap)
|
||||
|
||||
extendSyncDelta(result, delta.result())
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { FillItemContent } from '../../Abstract/Content/ItemContent'
|
||||
import { DecryptedPayload, PayloadTimestampDefaults } from '../../Abstract/Payload'
|
||||
import { DecryptedPayload, FullyFormedPayloadInterface, PayloadTimestampDefaults } from '../../Abstract/Payload'
|
||||
import { NoteContent } from '../../Syncable/Note'
|
||||
import { PayloadCollection } from '../Collection/Payload/PayloadCollection'
|
||||
import { DeltaRemoteRejected } from './RemoteRejected'
|
||||
import { ImmutablePayloadCollection } from '../Collection/Payload/ImmutablePayloadCollection'
|
||||
import { ConflictParams, ConflictType } from '@standardnotes/responses'
|
||||
import { UuidGenerator } from '@standardnotes/utils'
|
||||
|
||||
UuidGenerator.SetGenerator(() => String(Math.random()))
|
||||
|
||||
describe('remote rejected delta', () => {
|
||||
it('rejected payloads should not map onto app state', async () => {
|
||||
@@ -30,10 +34,12 @@ describe('remote rejected delta', () => {
|
||||
dirty: true,
|
||||
})
|
||||
|
||||
const delta = new DeltaRemoteRejected(
|
||||
ImmutablePayloadCollection.FromCollection(baseCollection),
|
||||
ImmutablePayloadCollection.WithPayloads([rejectedPayload]),
|
||||
)
|
||||
const entry: ConflictParams<FullyFormedPayloadInterface> = {
|
||||
type: ConflictType.ContentTypeError,
|
||||
unsaved_item: rejectedPayload,
|
||||
} as unknown as ConflictParams<FullyFormedPayloadInterface>
|
||||
|
||||
const delta = new DeltaRemoteRejected(ImmutablePayloadCollection.FromCollection(baseCollection), [entry])
|
||||
|
||||
const result = delta.result()
|
||||
const payload = result.emits[0] as DecryptedPayload<NoteContent>
|
||||
|
||||
@@ -1,35 +1,48 @@
|
||||
import { PayloadSource } from '../../Abstract/Payload/Types/PayloadSource'
|
||||
import { PayloadEmitSource } from '../../Abstract/Payload'
|
||||
import { DeletedPayload, FullyFormedPayloadInterface, PayloadEmitSource } from '../../Abstract/Payload'
|
||||
import { ImmutablePayloadCollection } from '../Collection/Payload/ImmutablePayloadCollection'
|
||||
import { SyncDeltaEmit } from './Abstract/DeltaEmit'
|
||||
import { SyncDeltaInterface } from './Abstract/SyncDeltaInterface'
|
||||
import { SyncResolvedPayload } from './Utilities/SyncResolvedPayload'
|
||||
import { BuildSyncResolvedParams, SyncResolvedPayload } from './Utilities/SyncResolvedPayload'
|
||||
import {
|
||||
ConflictParams,
|
||||
ConflictParamsWithServerItem,
|
||||
ConflictParamsWithUnsavedItem,
|
||||
ConflictParamsWithServerAndUnsavedItem,
|
||||
conflictParamsHasServerItemAndUnsavedItem,
|
||||
conflictParamsHasOnlyServerItem,
|
||||
conflictParamsHasOnlyUnsavedItem,
|
||||
ConflictType,
|
||||
} from '@standardnotes/responses'
|
||||
import { PayloadsByDuplicating } from '../../Utilities/Payload/PayloadsByDuplicating'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
|
||||
export class DeltaRemoteRejected implements SyncDeltaInterface {
|
||||
constructor(
|
||||
readonly baseCollection: ImmutablePayloadCollection,
|
||||
readonly applyCollection: ImmutablePayloadCollection,
|
||||
readonly conflicts: ConflictParams<FullyFormedPayloadInterface>[],
|
||||
) {}
|
||||
|
||||
public result(): SyncDeltaEmit {
|
||||
const results: SyncResolvedPayload[] = []
|
||||
|
||||
for (const apply of this.applyCollection.all()) {
|
||||
const base = this.baseCollection.find(apply.uuid)
|
||||
const vaultErrors: ConflictType[] = [
|
||||
ConflictType.SharedVaultInsufficientPermissionsError,
|
||||
ConflictType.SharedVaultNotMemberError,
|
||||
ConflictType.SharedVaultInvalidState,
|
||||
ConflictType.SharedVaultSnjsVersionError,
|
||||
]
|
||||
|
||||
if (!base) {
|
||||
continue
|
||||
for (const conflict of this.conflicts) {
|
||||
if (vaultErrors.includes(conflict.type)) {
|
||||
results.push(...this.handleVaultError(conflict))
|
||||
} else if (conflictParamsHasServerItemAndUnsavedItem(conflict)) {
|
||||
results.push(...this.getResultForConflictWithServerItemAndUnsavedItem(conflict))
|
||||
} else if (conflictParamsHasOnlyServerItem(conflict)) {
|
||||
results.push(...this.getResultForConflictWithOnlyServerItem(conflict))
|
||||
} else if (conflictParamsHasOnlyUnsavedItem(conflict)) {
|
||||
results.push(...this.getResultForConflictWithOnlyUnsavedItem(conflict))
|
||||
}
|
||||
|
||||
const result = base.copyAsSyncResolved(
|
||||
{
|
||||
dirty: false,
|
||||
lastSyncEnd: new Date(),
|
||||
},
|
||||
PayloadSource.RemoteSaved,
|
||||
)
|
||||
|
||||
results.push(result)
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -37,4 +50,177 @@ export class DeltaRemoteRejected implements SyncDeltaInterface {
|
||||
source: PayloadEmitSource.RemoteSaved,
|
||||
}
|
||||
}
|
||||
|
||||
private handleVaultError(conflict: ConflictParams<FullyFormedPayloadInterface>): SyncResolvedPayload[] {
|
||||
const base = this.baseCollection.find(conflict.unsaved_item.uuid)
|
||||
if (!base) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (conflict.type === ConflictType.SharedVaultNotMemberError) {
|
||||
return this.resultByDuplicatingBasePayloadAsNonVaultedAndRemovingBaseItemLocally(base)
|
||||
}
|
||||
|
||||
if (base.content_type === ContentType.KeySystemItemsKey) {
|
||||
return this.discardChangesOfBasePayload(base)
|
||||
}
|
||||
|
||||
if (conflict.server_item) {
|
||||
return this.resultByDuplicatingBasePayloadAsNonVaultedAndTakingServerPayloadAsCanonical(
|
||||
base,
|
||||
conflict.server_item,
|
||||
)
|
||||
} else {
|
||||
return this.resultByDuplicatingBasePayloadAsNonVaultedAndDiscardingChangesOfOriginal(base)
|
||||
}
|
||||
}
|
||||
|
||||
private discardChangesOfBasePayload(base: FullyFormedPayloadInterface): SyncResolvedPayload[] {
|
||||
const undirtiedPayload = base.copyAsSyncResolved(
|
||||
{
|
||||
dirty: false,
|
||||
lastSyncEnd: new Date(),
|
||||
},
|
||||
PayloadSource.RemoteSaved,
|
||||
)
|
||||
|
||||
return [undirtiedPayload]
|
||||
}
|
||||
|
||||
private getResultForConflictWithOnlyUnsavedItem(
|
||||
conflict: ConflictParamsWithUnsavedItem<FullyFormedPayloadInterface>,
|
||||
): SyncResolvedPayload[] {
|
||||
const base = this.baseCollection.find(conflict.unsaved_item.uuid)
|
||||
if (!base) {
|
||||
return []
|
||||
}
|
||||
|
||||
const result = base.copyAsSyncResolved(
|
||||
{
|
||||
dirty: false,
|
||||
lastSyncEnd: new Date(),
|
||||
},
|
||||
PayloadSource.RemoteSaved,
|
||||
)
|
||||
|
||||
return [result]
|
||||
}
|
||||
|
||||
private getResultForConflictWithOnlyServerItem(
|
||||
conflict: ConflictParamsWithServerItem<FullyFormedPayloadInterface>,
|
||||
): SyncResolvedPayload[] {
|
||||
const base = this.baseCollection.find(conflict.server_item.uuid)
|
||||
if (!base) {
|
||||
return []
|
||||
}
|
||||
|
||||
return this.resultByDuplicatingBasePayloadIntoNewUuidAndTakingServerPayloadAsCanonical(base, conflict.server_item)
|
||||
}
|
||||
|
||||
private getResultForConflictWithServerItemAndUnsavedItem(
|
||||
conflict: ConflictParamsWithServerAndUnsavedItem<FullyFormedPayloadInterface>,
|
||||
): SyncResolvedPayload[] {
|
||||
const base = this.baseCollection.find(conflict.server_item.uuid)
|
||||
if (!base) {
|
||||
return []
|
||||
}
|
||||
|
||||
return this.resultByDuplicatingBasePayloadIntoNewUuidAndTakingServerPayloadAsCanonical(base, conflict.server_item)
|
||||
}
|
||||
|
||||
private resultByDuplicatingBasePayloadIntoNewUuidAndTakingServerPayloadAsCanonical(
|
||||
basePayload: FullyFormedPayloadInterface,
|
||||
serverPayload: FullyFormedPayloadInterface,
|
||||
): SyncResolvedPayload[] {
|
||||
const duplicateBasePayloadIntoNewUuid = PayloadsByDuplicating({
|
||||
payload: basePayload,
|
||||
baseCollection: this.baseCollection,
|
||||
isConflict: true,
|
||||
source: serverPayload.source,
|
||||
})
|
||||
|
||||
const takeServerPayloadAsCanonical = serverPayload.copyAsSyncResolved(
|
||||
{
|
||||
lastSyncBegan: basePayload.lastSyncBegan,
|
||||
dirty: false,
|
||||
lastSyncEnd: new Date(),
|
||||
},
|
||||
serverPayload.source,
|
||||
)
|
||||
|
||||
return duplicateBasePayloadIntoNewUuid.concat([takeServerPayloadAsCanonical])
|
||||
}
|
||||
|
||||
private resultByDuplicatingBasePayloadAsNonVaultedAndTakingServerPayloadAsCanonical(
|
||||
basePayload: FullyFormedPayloadInterface,
|
||||
serverPayload: FullyFormedPayloadInterface,
|
||||
): SyncResolvedPayload[] {
|
||||
const duplicateBasePayloadIntoNewUuid = PayloadsByDuplicating({
|
||||
payload: basePayload.copy({
|
||||
key_system_identifier: undefined,
|
||||
shared_vault_uuid: undefined,
|
||||
}),
|
||||
baseCollection: this.baseCollection,
|
||||
isConflict: true,
|
||||
source: serverPayload.source,
|
||||
})
|
||||
|
||||
const takeServerPayloadAsCanonical = serverPayload.copyAsSyncResolved(
|
||||
{
|
||||
lastSyncBegan: basePayload.lastSyncBegan,
|
||||
dirty: false,
|
||||
lastSyncEnd: new Date(),
|
||||
},
|
||||
serverPayload.source,
|
||||
)
|
||||
|
||||
return duplicateBasePayloadIntoNewUuid.concat([takeServerPayloadAsCanonical])
|
||||
}
|
||||
|
||||
private resultByDuplicatingBasePayloadAsNonVaultedAndDiscardingChangesOfOriginal(
|
||||
basePayload: FullyFormedPayloadInterface,
|
||||
): SyncResolvedPayload[] {
|
||||
const duplicateBasePayloadWithoutVault = PayloadsByDuplicating({
|
||||
payload: basePayload.copy({
|
||||
key_system_identifier: undefined,
|
||||
shared_vault_uuid: undefined,
|
||||
}),
|
||||
baseCollection: this.baseCollection,
|
||||
isConflict: true,
|
||||
source: basePayload.source,
|
||||
})
|
||||
|
||||
return [...duplicateBasePayloadWithoutVault, ...this.discardChangesOfBasePayload(basePayload)]
|
||||
}
|
||||
|
||||
private resultByDuplicatingBasePayloadAsNonVaultedAndRemovingBaseItemLocally(
|
||||
basePayload: FullyFormedPayloadInterface,
|
||||
): SyncResolvedPayload[] {
|
||||
const duplicateBasePayloadWithoutVault = PayloadsByDuplicating({
|
||||
payload: basePayload.copy({
|
||||
key_system_identifier: undefined,
|
||||
shared_vault_uuid: undefined,
|
||||
}),
|
||||
baseCollection: this.baseCollection,
|
||||
isConflict: true,
|
||||
source: basePayload.source,
|
||||
})
|
||||
|
||||
const locallyDeletedBasePayload = new DeletedPayload(
|
||||
{
|
||||
...basePayload,
|
||||
content: undefined,
|
||||
deleted: true,
|
||||
key_system_identifier: undefined,
|
||||
shared_vault_uuid: undefined,
|
||||
...BuildSyncResolvedParams({
|
||||
dirty: false,
|
||||
lastSyncEnd: new Date(),
|
||||
}),
|
||||
},
|
||||
PayloadSource.RemoteSaved,
|
||||
)
|
||||
|
||||
return [...duplicateBasePayloadWithoutVault, locallyDeletedBasePayload as SyncResolvedPayload]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ export class DeltaRemoteRetrieved implements SyncDeltaInterface {
|
||||
* or if the item is locally dirty, filter it out of retrieved_items, and add to potential conflicts.
|
||||
*/
|
||||
for (const apply of this.applyCollection.all()) {
|
||||
if (apply.content_type === ContentType.ItemsKey) {
|
||||
if (apply.content_type === ContentType.ItemsKey || apply.content_type === ContentType.KeySystemItemsKey) {
|
||||
const itemsKeyDeltaEmit = new ItemsKeyDelta(this.baseCollection, [apply]).result()
|
||||
|
||||
extendSyncDelta(result, itemsKeyDeltaEmit)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ServerSyncSavedContextualPayload } from './../../Abstract/Contextual/ServerSyncSaved'
|
||||
import { ServerSyncSavedContextualPayload } from '../../Abstract/Contextual/ServerSyncSaved'
|
||||
import { DeletedPayload } from './../../Abstract/Payload/Implementations/DeletedPayload'
|
||||
import { ImmutablePayloadCollection } from '../Collection/Payload/ImmutablePayloadCollection'
|
||||
import { PayloadSource } from '../../Abstract/Payload/Types/PayloadSource'
|
||||
|
||||
@@ -2,10 +2,11 @@ import { extendArray, filterFromArray, Uuids } from '@standardnotes/utils'
|
||||
import { ImmutablePayloadCollection } from '../Collection/Payload/ImmutablePayloadCollection'
|
||||
import { PayloadsByAlternatingUuid } from '../../Utilities/Payload/PayloadsByAlternatingUuid'
|
||||
import { isDecryptedPayload } from '../../Abstract/Payload/Interfaces/TypeCheck'
|
||||
import { PayloadEmitSource } from '../../Abstract/Payload'
|
||||
import { FullyFormedPayloadInterface, PayloadEmitSource } from '../../Abstract/Payload'
|
||||
import { SyncDeltaEmit } from './Abstract/DeltaEmit'
|
||||
import { SyncDeltaInterface } from './Abstract/SyncDeltaInterface'
|
||||
import { SyncResolvedPayload } from './Utilities/SyncResolvedPayload'
|
||||
import { ConflictUuidConflictParams } from '@standardnotes/responses'
|
||||
|
||||
/**
|
||||
* UUID conflicts can occur if a user attmpts to import an old data
|
||||
@@ -15,22 +16,22 @@ import { SyncResolvedPayload } from './Utilities/SyncResolvedPayload'
|
||||
export class DeltaRemoteUuidConflicts implements SyncDeltaInterface {
|
||||
constructor(
|
||||
readonly baseCollection: ImmutablePayloadCollection,
|
||||
readonly applyCollection: ImmutablePayloadCollection,
|
||||
readonly conflicts: ConflictUuidConflictParams<FullyFormedPayloadInterface>[],
|
||||
) {}
|
||||
|
||||
public result(): SyncDeltaEmit {
|
||||
const results: SyncResolvedPayload[] = []
|
||||
const baseCollectionCopy = this.baseCollection.mutableCopy()
|
||||
|
||||
for (const apply of this.applyCollection.all()) {
|
||||
for (const conflict of this.conflicts) {
|
||||
/**
|
||||
* The payload in question may have been modified as part of alternating a uuid for
|
||||
* another item. For example, alternating a uuid for a note will also affect the
|
||||
* referencing tag, which would be added to `results`, but could also be inside
|
||||
* of this.applyCollection. In this case we'd prefer the most recently modified value.
|
||||
*/
|
||||
const moreRecent = results.find((r) => r.uuid === apply.uuid)
|
||||
const useApply = moreRecent || apply
|
||||
const moreRecent = results.find((r) => r.uuid === conflict.unsaved_item.uuid)
|
||||
const useApply = moreRecent || conflict.unsaved_item
|
||||
|
||||
if (!isDecryptedPayload(useApply)) {
|
||||
continue
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { createNoteWithContent } from '../../Utilities/Test/SpecUtils'
|
||||
import { ItemCollection } from '../Collection/Item/ItemCollection'
|
||||
import { SNNote } from '../../Syncable/Note/Note'
|
||||
import { itemsMatchingOptions } from './Search/SearchUtilities'
|
||||
import { FilterDisplayOptions } from './DisplayOptions'
|
||||
import { notesAndFilesMatchingOptions } from './Search/SearchUtilities'
|
||||
import { NotesAndFilesDisplayOptions } from './DisplayOptions'
|
||||
|
||||
describe('item display options', () => {
|
||||
const collectionWithNotes = function (titles: (string | undefined)[] = [], bodies: string[] = []) {
|
||||
@@ -23,31 +23,31 @@ describe('item display options', () => {
|
||||
it('string query title', () => {
|
||||
const query = 'foo'
|
||||
|
||||
const options: FilterDisplayOptions = {
|
||||
const options: NotesAndFilesDisplayOptions = {
|
||||
searchQuery: { query: query, includeProtectedNoteText: true },
|
||||
}
|
||||
} as jest.Mocked<NotesAndFilesDisplayOptions>
|
||||
const collection = collectionWithNotes(['hello', 'fobar', 'foobar', 'foo'])
|
||||
expect(itemsMatchingOptions(options, collection.all() as SNNote[], collection)).toHaveLength(2)
|
||||
expect(notesAndFilesMatchingOptions(options, collection.all() as SNNote[], collection)).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('string query text', async function () {
|
||||
const query = 'foo'
|
||||
const options: FilterDisplayOptions = {
|
||||
const options: NotesAndFilesDisplayOptions = {
|
||||
searchQuery: { query: query, includeProtectedNoteText: true },
|
||||
}
|
||||
} as jest.Mocked<NotesAndFilesDisplayOptions>
|
||||
const collection = collectionWithNotes(
|
||||
[undefined, undefined, undefined, undefined],
|
||||
['hello', 'fobar', 'foobar', 'foo'],
|
||||
)
|
||||
expect(itemsMatchingOptions(options, collection.all() as SNNote[], collection)).toHaveLength(2)
|
||||
expect(notesAndFilesMatchingOptions(options, collection.all() as SNNote[], collection)).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('string query title and text', async function () {
|
||||
const query = 'foo'
|
||||
const options: FilterDisplayOptions = {
|
||||
const options: NotesAndFilesDisplayOptions = {
|
||||
searchQuery: { query: query, includeProtectedNoteText: true },
|
||||
}
|
||||
} as jest.Mocked<NotesAndFilesDisplayOptions>
|
||||
const collection = collectionWithNotes(['hello', 'foobar'], ['foo', 'fobar'])
|
||||
expect(itemsMatchingOptions(options, collection.all() as SNNote[], collection)).toHaveLength(2)
|
||||
expect(notesAndFilesMatchingOptions(options, collection.all() as SNNote[], collection)).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,21 +5,28 @@ import { CollectionSortDirection, CollectionSortProperty } from '../Collection/C
|
||||
import { SearchQuery } from './Search/Types'
|
||||
import { DisplayControllerCustomFilter } from './Types'
|
||||
|
||||
export type DisplayOptions = FilterDisplayOptions & DisplayControllerOptions
|
||||
|
||||
export interface FilterDisplayOptions {
|
||||
tags?: SNTag[]
|
||||
views?: SmartView[]
|
||||
searchQuery?: SearchQuery
|
||||
export interface GenericDisplayOptions {
|
||||
includePinned?: boolean
|
||||
includeProtected?: boolean
|
||||
includeTrashed?: boolean
|
||||
includeArchived?: boolean
|
||||
}
|
||||
|
||||
export interface DisplayControllerOptions {
|
||||
sortBy: CollectionSortProperty
|
||||
sortDirection: CollectionSortDirection
|
||||
export interface NotesAndFilesDisplayOptions extends GenericDisplayOptions {
|
||||
tags?: SNTag[]
|
||||
views?: SmartView[]
|
||||
searchQuery?: SearchQuery
|
||||
hiddenContentTypes?: ContentType[]
|
||||
customFilter?: DisplayControllerCustomFilter
|
||||
}
|
||||
|
||||
export type TagsDisplayOptions = GenericDisplayOptions
|
||||
|
||||
export interface DisplayControllerDisplayOptions extends GenericDisplayOptions {
|
||||
sortBy: CollectionSortProperty
|
||||
sortDirection: CollectionSortDirection
|
||||
}
|
||||
|
||||
export type NotesAndFilesDisplayControllerOptions = NotesAndFilesDisplayOptions & DisplayControllerDisplayOptions
|
||||
export type TagsDisplayControllerOptions = TagsDisplayOptions & DisplayControllerDisplayOptions
|
||||
export type AnyDisplayOptions = NotesAndFilesDisplayOptions | TagsDisplayOptions | GenericDisplayOptions
|
||||
|
||||
@@ -5,11 +5,11 @@ import { CompoundPredicate } from '../Predicate/CompoundPredicate'
|
||||
import { ItemWithTags } from './Search/ItemWithTags'
|
||||
import { itemMatchesQuery, itemPassesFilters } from './Search/SearchUtilities'
|
||||
import { ItemFilter, ReferenceLookupCollection, SearchableDecryptedItem } from './Search/Types'
|
||||
import { FilterDisplayOptions } from './DisplayOptions'
|
||||
import { NotesAndFilesDisplayOptions } from './DisplayOptions'
|
||||
import { SystemViewId } from '../../Syncable/SmartView'
|
||||
|
||||
export function computeUnifiedFilterForDisplayOptions(
|
||||
options: FilterDisplayOptions,
|
||||
options: NotesAndFilesDisplayOptions,
|
||||
collection: ReferenceLookupCollection,
|
||||
additionalFilters: ItemFilter[] = [],
|
||||
): ItemFilter {
|
||||
@@ -21,7 +21,7 @@ export function computeUnifiedFilterForDisplayOptions(
|
||||
}
|
||||
|
||||
export function computeFiltersForDisplayOptions(
|
||||
options: FilterDisplayOptions,
|
||||
options: NotesAndFilesDisplayOptions,
|
||||
collection: ReferenceLookupCollection,
|
||||
): ItemFilter[] {
|
||||
const filters: ItemFilter[] = []
|
||||
|
||||
@@ -2,11 +2,19 @@ import { ContentType } from '@standardnotes/common'
|
||||
import { compareValues } from '@standardnotes/utils'
|
||||
import { isDeletedItem, isEncryptedItem } from '../../Abstract/Item'
|
||||
import { ItemDelta } from '../Index/ItemDelta'
|
||||
import { DisplayControllerOptions } from './DisplayOptions'
|
||||
import { AnyDisplayOptions, DisplayControllerDisplayOptions, GenericDisplayOptions } from './DisplayOptions'
|
||||
import { sortTwoItems } from './SortTwoItems'
|
||||
import { UuidToSortedPositionMap, DisplayItem, ReadonlyItemCollection } from './Types'
|
||||
import { CriteriaValidatorInterface } from './Validator/CriteriaValidatorInterface'
|
||||
import { CollectionCriteriaValidator } from './Validator/CollectionCriteriaValidator'
|
||||
import { CustomFilterCriteriaValidator } from './Validator/CustomFilterCriteriaValidator'
|
||||
import { ExcludeVaultsCriteriaValidator } from './Validator/ExcludeVaultsCriteriaValidator'
|
||||
import { ExclusiveVaultCriteriaValidator } from './Validator/ExclusiveVaultCriteriaValidator'
|
||||
import { HiddenContentCriteriaValidator } from './Validator/HiddenContentCriteriaValidator'
|
||||
import { VaultDisplayOptions } from './VaultDisplayOptions'
|
||||
import { isExclusioanaryOptionsValue } from './VaultDisplayOptionsTypes'
|
||||
|
||||
export class ItemDisplayController<I extends DisplayItem> {
|
||||
export class ItemDisplayController<I extends DisplayItem, O extends AnyDisplayOptions = GenericDisplayOptions> {
|
||||
private sortMap: UuidToSortedPositionMap = {}
|
||||
private sortedItems: I[] = []
|
||||
private needsSort = true
|
||||
@@ -14,7 +22,8 @@ export class ItemDisplayController<I extends DisplayItem> {
|
||||
constructor(
|
||||
private readonly collection: ReadonlyItemCollection,
|
||||
public readonly contentTypes: ContentType[],
|
||||
private options: DisplayControllerOptions,
|
||||
private options: DisplayControllerDisplayOptions & O,
|
||||
private vaultOptions?: VaultDisplayOptions,
|
||||
) {
|
||||
this.filterThenSortElements(this.collection.all(this.contentTypes) as I[])
|
||||
}
|
||||
@@ -23,7 +32,18 @@ export class ItemDisplayController<I extends DisplayItem> {
|
||||
return this.sortedItems
|
||||
}
|
||||
|
||||
setDisplayOptions(displayOptions: Partial<DisplayControllerOptions>): void {
|
||||
public getDisplayOptions(): DisplayControllerDisplayOptions & O {
|
||||
return this.options
|
||||
}
|
||||
|
||||
setVaultDisplayOptions(vaultOptions?: VaultDisplayOptions): void {
|
||||
this.vaultOptions = vaultOptions
|
||||
this.needsSort = true
|
||||
|
||||
this.filterThenSortElements(this.collection.all(this.contentTypes) as I[])
|
||||
}
|
||||
|
||||
setDisplayOptions(displayOptions: Partial<DisplayControllerDisplayOptions & O>): void {
|
||||
this.options = { ...this.options, ...displayOptions }
|
||||
this.needsSort = true
|
||||
|
||||
@@ -37,6 +57,29 @@ export class ItemDisplayController<I extends DisplayItem> {
|
||||
this.filterThenSortElements(items as I[])
|
||||
}
|
||||
|
||||
private passesAllFilters(element: I): boolean {
|
||||
const filters: CriteriaValidatorInterface[] = [new CollectionCriteriaValidator(this.collection, element)]
|
||||
|
||||
if (this.vaultOptions) {
|
||||
const options = this.vaultOptions.getOptions()
|
||||
if (isExclusioanaryOptionsValue(options)) {
|
||||
filters.push(new ExcludeVaultsCriteriaValidator([...options.exclude, ...options.locked], element))
|
||||
} else {
|
||||
filters.push(new ExclusiveVaultCriteriaValidator(options.exclusive, element))
|
||||
}
|
||||
}
|
||||
|
||||
if ('hiddenContentTypes' in this.options && this.options.hiddenContentTypes) {
|
||||
filters.push(new HiddenContentCriteriaValidator(this.options.hiddenContentTypes, element))
|
||||
}
|
||||
|
||||
if ('customFilter' in this.options && this.options.customFilter) {
|
||||
filters.push(new CustomFilterCriteriaValidator(this.options.customFilter, element))
|
||||
}
|
||||
|
||||
return filters.every((f) => f.passes())
|
||||
}
|
||||
|
||||
private filterThenSortElements(elements: I[]): void {
|
||||
for (const element of elements) {
|
||||
const previousIndex = this.sortMap[element.uuid]
|
||||
@@ -61,13 +104,7 @@ export class ItemDisplayController<I extends DisplayItem> {
|
||||
continue
|
||||
}
|
||||
|
||||
const passes = !this.collection.has(element.uuid)
|
||||
? false
|
||||
: this.options.hiddenContentTypes?.includes(element.content_type)
|
||||
? false
|
||||
: this.options.customFilter
|
||||
? this.options.customFilter(element)
|
||||
: true
|
||||
const passes = this.passesAllFilters(element)
|
||||
|
||||
if (passes) {
|
||||
if (previousElement != undefined) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { SNTag } from '../../../Syncable/Tag'
|
||||
import { FilterDisplayOptions } from '../DisplayOptions'
|
||||
import { NotesAndFilesDisplayOptions } from '../DisplayOptions'
|
||||
import { computeFiltersForDisplayOptions } from '../DisplayOptionsToFilters'
|
||||
import { SearchableItem } from './SearchableItem'
|
||||
import { ReferenceLookupCollection, ItemFilter, SearchQuery, SearchableDecryptedItem } from './Types'
|
||||
@@ -13,8 +13,8 @@ enum MatchResult {
|
||||
Uuid = 5,
|
||||
}
|
||||
|
||||
export function itemsMatchingOptions(
|
||||
options: FilterDisplayOptions,
|
||||
export function notesAndFilesMatchingOptions(
|
||||
options: NotesAndFilesDisplayOptions,
|
||||
fromItems: SearchableDecryptedItem[],
|
||||
collection: ReferenceLookupCollection,
|
||||
): SearchableItem[] {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { ItemInterface } from '../../../Abstract/Item'
|
||||
import { ReadonlyItemCollection } from '../Types'
|
||||
import { CriteriaValidatorInterface } from './CriteriaValidatorInterface'
|
||||
|
||||
export class CollectionCriteriaValidator implements CriteriaValidatorInterface {
|
||||
constructor(private collection: ReadonlyItemCollection, private element: ItemInterface) {}
|
||||
|
||||
public passes(): boolean {
|
||||
return this.collection.has(this.element.uuid)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface CriteriaValidatorInterface {
|
||||
passes(): boolean
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { DecryptedItemInterface } from '../../../Abstract/Item'
|
||||
import { DisplayControllerCustomFilter } from '../Types'
|
||||
import { CriteriaValidatorInterface } from './CriteriaValidatorInterface'
|
||||
|
||||
export class CustomFilterCriteriaValidator implements CriteriaValidatorInterface {
|
||||
constructor(private customFilter: DisplayControllerCustomFilter, private element: DecryptedItemInterface) {}
|
||||
|
||||
public passes(): boolean {
|
||||
return this.customFilter(this.element)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { CriteriaValidatorInterface } from './CriteriaValidatorInterface'
|
||||
import { DecryptedItemInterface } from '../../../Abstract/Item'
|
||||
import { KeySystemIdentifier } from '../../../Syncable/KeySystemRootKey/KeySystemIdentifier'
|
||||
|
||||
export class ExcludeVaultsCriteriaValidator implements CriteriaValidatorInterface {
|
||||
constructor(private excludeVaults: KeySystemIdentifier[], private element: DecryptedItemInterface) {}
|
||||
|
||||
public passes(): boolean {
|
||||
const doesElementBelongToExcludedVault = this.excludeVaults.some(
|
||||
(vault) => this.element.key_system_identifier === vault,
|
||||
)
|
||||
|
||||
return !doesElementBelongToExcludedVault
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { CriteriaValidatorInterface } from './CriteriaValidatorInterface'
|
||||
import { DecryptedItemInterface } from '../../../Abstract/Item'
|
||||
import { KeySystemIdentifier } from '../../../Syncable/KeySystemRootKey/KeySystemIdentifier'
|
||||
|
||||
export class ExclusiveVaultCriteriaValidator implements CriteriaValidatorInterface {
|
||||
constructor(private exclusiveVault: KeySystemIdentifier, private element: DecryptedItemInterface) {}
|
||||
|
||||
public passes(): boolean {
|
||||
return this.element.key_system_identifier === this.exclusiveVault
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { DecryptedItemInterface } from './../../../Abstract/Item/Interfaces/DecryptedItem'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { CriteriaValidatorInterface } from './CriteriaValidatorInterface'
|
||||
|
||||
export class HiddenContentCriteriaValidator implements CriteriaValidatorInterface {
|
||||
constructor(private hiddenContentTypes: ContentType[], private element: DecryptedItemInterface) {}
|
||||
|
||||
public passes(): boolean {
|
||||
return !this.hiddenContentTypes.includes(this.element.content_type)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { VaultListingInterface } from '../../Syncable/VaultListing/VaultListingInterface'
|
||||
import { uniqueArray } from '@standardnotes/utils'
|
||||
import {
|
||||
ExclusioanaryOptions,
|
||||
ExclusiveOptions,
|
||||
VaultDisplayOptionsPersistable,
|
||||
isExclusioanaryOptionsValue,
|
||||
} from './VaultDisplayOptionsTypes'
|
||||
import { KeySystemIdentifier } from '../../Syncable/KeySystemRootKey/KeySystemIdentifier'
|
||||
|
||||
function KeySystemIdentifiers(vaults: VaultListingInterface[]): KeySystemIdentifier[] {
|
||||
return vaults.map((vault) => vault.systemIdentifier)
|
||||
}
|
||||
|
||||
export class VaultDisplayOptions {
|
||||
constructor(private readonly options: ExclusioanaryOptions | ExclusiveOptions) {}
|
||||
|
||||
public getOptions(): ExclusioanaryOptions | ExclusiveOptions {
|
||||
return this.options
|
||||
}
|
||||
|
||||
public getExclusivelyShownVault(): KeySystemIdentifier {
|
||||
if (isExclusioanaryOptionsValue(this.options)) {
|
||||
throw new Error('Not in exclusive display mode')
|
||||
}
|
||||
|
||||
return this.options.exclusive
|
||||
}
|
||||
|
||||
public isInExclusiveDisplayMode(): boolean {
|
||||
return !isExclusioanaryOptionsValue(this.options)
|
||||
}
|
||||
|
||||
public isVaultExplicitelyExcluded(vault: VaultListingInterface): boolean {
|
||||
if (isExclusioanaryOptionsValue(this.options)) {
|
||||
return this.options.exclude.some((excludedVault) => excludedVault === vault.systemIdentifier)
|
||||
} else if (this.options.exclusive) {
|
||||
return this.options.exclusive !== vault.systemIdentifier
|
||||
}
|
||||
|
||||
throw new Error('Invalid vault display options')
|
||||
}
|
||||
|
||||
isVaultExclusivelyShown(vault: VaultListingInterface): boolean {
|
||||
return !isExclusioanaryOptionsValue(this.options) && this.options.exclusive === vault.systemIdentifier
|
||||
}
|
||||
|
||||
isVaultDisabledOrLocked(vault: VaultListingInterface): boolean {
|
||||
if (isExclusioanaryOptionsValue(this.options)) {
|
||||
const matchingLocked = this.options.locked.find((lockedVault) => lockedVault === vault.systemIdentifier)
|
||||
if (matchingLocked) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return this.isVaultExplicitelyExcluded(vault)
|
||||
}
|
||||
|
||||
getPersistableValue(): VaultDisplayOptionsPersistable {
|
||||
return this.options
|
||||
}
|
||||
|
||||
newOptionsByIntakingLockedVaults(lockedVaults: VaultListingInterface[]): VaultDisplayOptions {
|
||||
if (isExclusioanaryOptionsValue(this.options)) {
|
||||
return new VaultDisplayOptions({ exclude: this.options.exclude, locked: KeySystemIdentifiers(lockedVaults) })
|
||||
} else {
|
||||
return new VaultDisplayOptions({ exclusive: this.options.exclusive })
|
||||
}
|
||||
}
|
||||
|
||||
newOptionsByExcludingVault(vault: VaultListingInterface, lockedVaults: VaultListingInterface[]): VaultDisplayOptions {
|
||||
return this.newOptionsByExcludingVaults([vault], lockedVaults)
|
||||
}
|
||||
|
||||
newOptionsByExcludingVaults(
|
||||
vaults: VaultListingInterface[],
|
||||
lockedVaults: VaultListingInterface[],
|
||||
): VaultDisplayOptions {
|
||||
if (isExclusioanaryOptionsValue(this.options)) {
|
||||
return new VaultDisplayOptions({
|
||||
exclude: uniqueArray([...this.options.exclude, ...KeySystemIdentifiers(vaults)]),
|
||||
locked: KeySystemIdentifiers(lockedVaults),
|
||||
})
|
||||
} else {
|
||||
return new VaultDisplayOptions({
|
||||
exclude: KeySystemIdentifiers(vaults),
|
||||
locked: KeySystemIdentifiers(lockedVaults),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
newOptionsByUnexcludingVault(
|
||||
vault: VaultListingInterface,
|
||||
lockedVaults: VaultListingInterface[],
|
||||
): VaultDisplayOptions {
|
||||
if (isExclusioanaryOptionsValue(this.options)) {
|
||||
return new VaultDisplayOptions({
|
||||
exclude: this.options.exclude.filter((excludedVault) => excludedVault !== vault.systemIdentifier),
|
||||
locked: KeySystemIdentifiers(lockedVaults),
|
||||
})
|
||||
} else {
|
||||
return new VaultDisplayOptions({ exclude: [], locked: KeySystemIdentifiers(lockedVaults) })
|
||||
}
|
||||
}
|
||||
|
||||
static FromPersistableValue(value: VaultDisplayOptionsPersistable): VaultDisplayOptions {
|
||||
return new VaultDisplayOptions(value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { KeySystemIdentifier } from '../../Syncable/KeySystemRootKey/KeySystemIdentifier'
|
||||
|
||||
export type ExclusioanaryOptions = { exclude: KeySystemIdentifier[]; locked: KeySystemIdentifier[] }
|
||||
export type ExclusiveOptions = { exclusive: KeySystemIdentifier }
|
||||
|
||||
export function isExclusioanaryOptionsValue(
|
||||
options: ExclusioanaryOptions | ExclusiveOptions,
|
||||
): options is ExclusioanaryOptions {
|
||||
return 'exclude' in options || 'locked' in options
|
||||
}
|
||||
|
||||
export type VaultDisplayOptionsPersistable = ExclusioanaryOptions | ExclusiveOptions
|
||||
@@ -6,3 +6,5 @@ export * from './Search/SearchableItem'
|
||||
export * from './Search/SearchUtilities'
|
||||
export * from './Search/Types'
|
||||
export * from './Types'
|
||||
export * from './VaultDisplayOptions'
|
||||
export * from './VaultDisplayOptionsTypes'
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
|
||||
export function ContentTypeUsesKeySystemRootKeyEncryption(contentType: ContentType): boolean {
|
||||
return contentType === ContentType.KeySystemItemsKey
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { ContentTypesUsingRootKeyEncryption } from './ContentTypesUsingRootKeyEncryption'
|
||||
|
||||
export function ContentTypeUsesRootKeyEncryption(contentType: ContentType): boolean {
|
||||
return ContentTypesUsingRootKeyEncryption().includes(contentType)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
|
||||
export function ContentTypesUsingRootKeyEncryption(): ContentType[] {
|
||||
return [
|
||||
ContentType.RootKey,
|
||||
ContentType.ItemsKey,
|
||||
ContentType.EncryptedStorage,
|
||||
ContentType.TrustedContact,
|
||||
ContentType.KeySystemRootKey,
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
export type PersistentSignatureData =
|
||||
| {
|
||||
required: true
|
||||
contentHash: string
|
||||
result: {
|
||||
passes: boolean
|
||||
publicKey: string
|
||||
signature: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
required: false
|
||||
contentHash: string
|
||||
result?: {
|
||||
passes: boolean
|
||||
publicKey: string
|
||||
signature: string
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user