feat: add models package

This commit is contained in:
Karol Sójko
2022-07-05 20:47:11 +02:00
parent 60d1554ff7
commit b614c71e79
199 changed files with 8772 additions and 22 deletions

View File

@@ -0,0 +1,72 @@
import { DecryptedItemInterface } from './../../Abstract/Item/Interfaces/DecryptedItem'
import { ThirdPartyFeatureDescription } from '@standardnotes/features'
import { DecryptedItem } from '../../Abstract/Item/Implementations/DecryptedItem'
import { ItemContent } from '../../Abstract/Content/ItemContent'
import { ConflictStrategy } from '../../Abstract/Item/Types/ConflictStrategy'
import { DecryptedPayloadInterface } from '../../Abstract/Payload/Interfaces/DecryptedPayload'
import { HistoryEntryInterface } from '../../Runtime/History/HistoryEntryInterface'
import { Action } from './Types'
import { ComponentPackageInfo } from '../Component/PackageInfo'
export interface ActionExtensionInterface {
actions: Action[]
deprecation?: string
description: string
hosted_url?: string
name: string
package_info: ComponentPackageInfo
supported_types: string[]
url: string
}
export type ActionExtensionContent = ActionExtensionInterface & ItemContent
/**
* Related to the SNActionsService and the local Action model.
*/
export class SNActionsExtension extends DecryptedItem<ActionExtensionContent> {
public readonly actions: Action[] = []
public readonly description: string
public readonly url: string
public readonly supported_types: string[]
public readonly deprecation?: string
public readonly name: string
public readonly package_info: ComponentPackageInfo
constructor(payload: DecryptedPayloadInterface<ActionExtensionContent>) {
super(payload)
this.name = payload.content.name || ''
this.description = payload.content.description || ''
this.url = payload.content.hosted_url || payload.content.url
this.supported_types = payload.content.supported_types
this.package_info = this.payload.content.package_info || {}
this.deprecation = payload.content.deprecation
this.actions = payload.content.actions
}
public get displayName(): string {
return this.name
}
public get thirdPartyPackageInfo(): ThirdPartyFeatureDescription {
return this.package_info as ThirdPartyFeatureDescription
}
public get isListedExtension(): boolean {
return (this.package_info.identifier as string) === 'org.standardnotes.listed'
}
actionsWithContextForItem(item: DecryptedItemInterface): Action[] {
return this.actions.filter((action) => {
return action.context === item.content_type || action.context === 'Item'
})
}
/** Do not duplicate. Always keep original */
override strategyWhenConflictingWithItem(
_item: DecryptedItemInterface,
_previousRevision?: HistoryEntryInterface,
): ConflictStrategy {
return ConflictStrategy.KeepBase
}
}

View File

@@ -0,0 +1,21 @@
import { DecryptedItemMutator } from '../../Abstract/Item/Mutator/DecryptedItemMutator'
import { ActionExtensionContent } from './ActionsExtension'
import { Action } from './Types'
export class ActionsExtensionMutator extends DecryptedItemMutator<ActionExtensionContent> {
set description(description: string) {
this.mutableContent.description = description
}
set supported_types(supported_types: string[]) {
this.mutableContent.supported_types = supported_types
}
set actions(actions: Action[]) {
this.mutableContent.actions = actions
}
set deprecation(deprecation: string | undefined) {
this.mutableContent.deprecation = deprecation
}
}

View File

@@ -0,0 +1,25 @@
export enum ActionAccessType {
Encrypted = 'encrypted',
Decrypted = 'decrypted',
}
export enum ActionVerb {
Get = 'get',
Render = 'render',
Show = 'show',
Post = 'post',
Nested = 'nested',
}
export type Action = {
label: string
desc: string
running?: boolean
error?: boolean
lastExecuted?: Date
context?: string
verb: ActionVerb
url: string
access_type: ActionAccessType
subactions?: Action[]
}

View File

@@ -0,0 +1,3 @@
export * from './ActionsExtension'
export * from './ActionsExtensionMutator'
export * from './Types'

View File

@@ -0,0 +1,49 @@
import { PayloadSource } from './../../Abstract/Payload/Types/PayloadSource'
import { DecryptedPayload } from './../../Abstract/Payload/Implementations/DecryptedPayload'
import { ContentType } from '@standardnotes/common'
import { FillItemContent } from '../../Abstract/Content/ItemContent'
import { SNComponent } from './Component'
import { ComponentContent } from './ComponentContent'
import { PayloadTimestampDefaults } from '../../Abstract/Payload'
describe('component model', () => {
it('valid hosted url should ignore url', () => {
const component = new SNComponent(
new DecryptedPayload(
{
uuid: String(Math.random()),
content_type: ContentType.Component,
content: FillItemContent<ComponentContent>({
url: 'http://foo.com',
hosted_url: 'http://bar.com',
} as ComponentContent),
...PayloadTimestampDefaults(),
},
PayloadSource.Constructor,
),
)
expect(component.hasValidHostedUrl()).toBe(true)
expect(component.hosted_url).toBe('http://bar.com')
})
it('invalid hosted url should fallback to url', () => {
const component = new SNComponent(
new DecryptedPayload(
{
uuid: String(Math.random()),
content_type: ContentType.Component,
content: FillItemContent({
url: 'http://foo.com',
hosted_url: '#{foo.zoo}',
} as ComponentContent),
...PayloadTimestampDefaults(),
},
PayloadSource.Constructor,
),
)
expect(component.hasValidHostedUrl()).toBe(true)
expect(component.hosted_url).toBe('http://foo.com')
})
})

View File

@@ -0,0 +1,189 @@
import { isValidUrl } from '@standardnotes/utils'
import { ContentType, Uuid } from '@standardnotes/common'
import {
FeatureIdentifier,
ThirdPartyFeatureDescription,
ComponentArea,
ComponentFlag,
ComponentPermission,
FindNativeFeature,
} from '@standardnotes/features'
import { AppDataField } from '../../Abstract/Item/Types/AppDataField'
import { ComponentContent, ComponentInterface } from './ComponentContent'
import { ConflictStrategy } from '../../Abstract/Item/Types/ConflictStrategy'
import { DecryptedItem } from '../../Abstract/Item/Implementations/DecryptedItem'
import { DecryptedPayloadInterface } from '../../Abstract/Payload/Interfaces/DecryptedPayload'
import { HistoryEntryInterface } from '../../Runtime/History'
import { ItemContent } from '../../Abstract/Content/ItemContent'
import { Predicate } from '../../Runtime/Predicate/Predicate'
import { ItemInterface } from '../../Abstract/Item/Interfaces/ItemInterface'
import { DecryptedItemInterface } from './../../Abstract/Item/Interfaces/DecryptedItem'
import { ComponentPackageInfo } from './PackageInfo'
export const isComponent = (x: ItemInterface): x is SNComponent => x.content_type === ContentType.Component
export const isComponentOrTheme = (x: ItemInterface): x is SNComponent =>
x.content_type === ContentType.Component || x.content_type === ContentType.Theme
/**
* Components are mostly iframe based extensions that communicate with the SN parent
* via the postMessage API. However, a theme can also be a component, which is activated
* only by its url.
*/
export class SNComponent extends DecryptedItem<ComponentContent> implements ComponentInterface {
public readonly componentData: Record<string, unknown>
/** Items that have requested a component to be disabled in its context */
public readonly disassociatedItemIds: string[]
/** Items that have requested a component to be enabled in its context */
public readonly associatedItemIds: string[]
public readonly local_url?: string
public readonly hosted_url?: string
public readonly offlineOnly: boolean
public readonly name: string
public readonly autoupdateDisabled: boolean
public readonly package_info: ComponentPackageInfo
public readonly area: ComponentArea
public readonly permissions: ComponentPermission[] = []
public readonly valid_until: Date
public readonly active: boolean
public readonly legacy_url?: string
public readonly isMobileDefault: boolean
constructor(payload: DecryptedPayloadInterface<ComponentContent>) {
super(payload)
/** Custom data that a component can store in itself */
this.componentData = this.payload.content.componentData || {}
if (payload.content.hosted_url && isValidUrl(payload.content.hosted_url)) {
this.hosted_url = payload.content.hosted_url
} else if (payload.content.url && isValidUrl(payload.content.url)) {
this.hosted_url = payload.content.url
} else if (payload.content.legacy_url && isValidUrl(payload.content.legacy_url)) {
this.hosted_url = payload.content.legacy_url
}
this.local_url = payload.content.local_url
this.valid_until = new Date(payload.content.valid_until || 0)
this.offlineOnly = payload.content.offlineOnly
this.name = payload.content.name
this.area = payload.content.area
this.package_info = payload.content.package_info || {}
this.permissions = payload.content.permissions || []
this.active = payload.content.active
this.autoupdateDisabled = payload.content.autoupdateDisabled
this.disassociatedItemIds = payload.content.disassociatedItemIds || []
this.associatedItemIds = payload.content.associatedItemIds || []
this.isMobileDefault = payload.content.isMobileDefault
/**
* @legacy
* We don't want to set this.url directly, as we'd like to phase it out.
* If the content.url exists, we'll transfer it to legacy_url. We'll only
* need to set this if content.hosted_url is blank, otherwise,
* hosted_url is the url replacement.
*/
this.legacy_url = !payload.content.hosted_url ? payload.content.url : undefined
}
/** Do not duplicate components under most circumstances. Always keep original */
public override strategyWhenConflictingWithItem(
_item: DecryptedItemInterface,
_previousRevision?: HistoryEntryInterface,
): ConflictStrategy {
return ConflictStrategy.KeepBase
}
override get isSingleton(): boolean {
return true
}
public get displayName(): string {
return FindNativeFeature(this.identifier)?.name || this.name
}
public override singletonPredicate(): Predicate<SNComponent> {
const uniqueIdentifierPredicate = new Predicate<SNComponent>('identifier', '=', this.identifier)
return uniqueIdentifierPredicate
}
public isEditor(): boolean {
return this.area === ComponentArea.Editor
}
public isTheme(): boolean {
return this.content_type === ContentType.Theme || this.area === ComponentArea.Themes
}
public isDefaultEditor(): boolean {
return this.getAppDomainValue(AppDataField.DefaultEditor) === true
}
public getLastSize(): unknown {
return this.getAppDomainValue(AppDataField.LastSize)
}
/**
* The key used to look up data that this component may have saved to an item.
* This data will be stored on the item using this key.
*/
public getClientDataKey(): string {
if (this.legacy_url) {
return this.legacy_url
} else {
return this.uuid
}
}
public hasValidHostedUrl(): boolean {
return (this.hosted_url || this.legacy_url) != undefined
}
public override contentKeysToIgnoreWhenCheckingEquality(): (keyof ItemContent)[] {
const componentKeys: (keyof ComponentContent)[] = ['active', 'disassociatedItemIds', 'associatedItemIds']
const superKeys = super.contentKeysToIgnoreWhenCheckingEquality()
return [...componentKeys, ...superKeys] as (keyof ItemContent)[]
}
/**
* An associative component depends on being explicitly activated for a
* given item, compared to a dissaciative component, which is enabled by
* default in areas unrelated to a certain item.
*/
public static associativeAreas(): ComponentArea[] {
return [ComponentArea.Editor]
}
public isAssociative(): boolean {
return SNComponent.associativeAreas().includes(this.area)
}
public isExplicitlyEnabledForItem(uuid: Uuid): boolean {
return this.associatedItemIds.indexOf(uuid) !== -1
}
public isExplicitlyDisabledForItem(uuid: Uuid): boolean {
return this.disassociatedItemIds.indexOf(uuid) !== -1
}
public get isExpired(): boolean {
return this.valid_until.getTime() > 0 && this.valid_until <= new Date()
}
public get identifier(): FeatureIdentifier {
return this.package_info.identifier
}
public get thirdPartyPackageInfo(): ThirdPartyFeatureDescription {
return this.package_info as ThirdPartyFeatureDescription
}
public get isDeprecated(): boolean {
let flags: string[] = this.package_info.flags ?? []
flags = flags.map((flag: string) => flag.toLowerCase())
return flags.includes(ComponentFlag.Deprecated)
}
public get deprecationMessage(): string | undefined {
return this.package_info.deprecation_message
}
}

View File

@@ -0,0 +1,36 @@
import { ComponentArea, ComponentPermission } from '@standardnotes/features'
import { Uuid } from '@standardnotes/common'
import { ItemContent } from '../../Abstract/Content/ItemContent'
import { ComponentPackageInfo } from './PackageInfo'
/* eslint-disable @typescript-eslint/no-explicit-any */
export interface ComponentInterface {
componentData: Record<string, any>
/** Items that have requested a component to be disabled in its context */
disassociatedItemIds: string[]
/** Items that have requested a component to be enabled in its context */
associatedItemIds: string[]
local_url?: string
hosted_url?: string
/** @deprecated */
url?: string
offlineOnly: boolean
name: string
autoupdateDisabled: boolean
package_info: ComponentPackageInfo
area: ComponentArea
permissions: ComponentPermission[]
valid_until: Date | number
active: boolean
legacy_url?: string
isMobileDefault: boolean
isDeprecated: boolean
isExplicitlyEnabledForItem(uuid: Uuid): boolean
}
export type ComponentContent = ComponentInterface & ItemContent

View File

@@ -0,0 +1,76 @@
import { addIfUnique, removeFromArray } from '@standardnotes/utils'
import { Uuid } from '@standardnotes/common'
import { ComponentPermission, FeatureDescription } from '@standardnotes/features'
import { AppDataField } from '../../Abstract/Item/Types/AppDataField'
import { ComponentContent } from './ComponentContent'
import { DecryptedItemMutator } from '../../Abstract/Item/Mutator/DecryptedItemMutator'
export class ComponentMutator extends DecryptedItemMutator<ComponentContent> {
set active(active: boolean) {
this.mutableContent.active = active
}
set isMobileDefault(isMobileDefault: boolean) {
this.mutableContent.isMobileDefault = isMobileDefault
}
set defaultEditor(defaultEditor: boolean) {
this.setAppDataItem(AppDataField.DefaultEditor, defaultEditor)
}
set componentData(componentData: Record<string, unknown>) {
this.mutableContent.componentData = componentData
}
set package_info(package_info: FeatureDescription) {
this.mutableContent.package_info = package_info
}
set local_url(local_url: string) {
this.mutableContent.local_url = local_url
}
set hosted_url(hosted_url: string) {
this.mutableContent.hosted_url = hosted_url
}
set valid_until(valid_until: Date) {
this.mutableContent.valid_until = valid_until
}
set permissions(permissions: ComponentPermission[]) {
this.mutableContent.permissions = permissions
}
set name(name: string) {
this.mutableContent.name = name
}
set offlineOnly(offlineOnly: boolean) {
this.mutableContent.offlineOnly = offlineOnly
}
public associateWithItem(uuid: Uuid): void {
const associated = this.mutableContent.associatedItemIds || []
addIfUnique(associated, uuid)
this.mutableContent.associatedItemIds = associated
}
public disassociateWithItem(uuid: Uuid): void {
const disassociated = this.mutableContent.disassociatedItemIds || []
addIfUnique(disassociated, uuid)
this.mutableContent.disassociatedItemIds = disassociated
}
public removeAssociatedItemId(uuid: Uuid): void {
removeFromArray(this.mutableContent.associatedItemIds || [], uuid)
}
public removeDisassociatedItemId(uuid: Uuid): void {
removeFromArray(this.mutableContent.disassociatedItemIds || [], uuid)
}
public setLastSize(size: string): void {
this.setAppDataItem(AppDataField.LastSize, size)
}
}

View File

@@ -0,0 +1,8 @@
import { FeatureDescription } from '@standardnotes/features'
type ThirdPartyPackageInfo = {
version: string
download_url?: string
}
export type ComponentPackageInfo = FeatureDescription & Partial<ThirdPartyPackageInfo>

View File

@@ -0,0 +1,3 @@
export * from './Component'
export * from './ComponentMutator'
export * from './ComponentContent'

View File

@@ -0,0 +1,35 @@
import { DecryptedItem } from '../../Abstract/Item/Implementations/DecryptedItem'
import { ItemContent } from '../../Abstract/Content/ItemContent'
import { DecryptedPayloadInterface } from '../../Abstract/Payload/Interfaces/DecryptedPayload'
import { SNNote } from '../Note/Note'
interface EditorContent extends ItemContent {
notes: SNNote[]
data: Record<string, unknown>
url: string
name: string
default: boolean
systemEditor: boolean
}
/**
* @deprecated
* Editor objects are depracated in favor of SNComponent objects
*/
export class SNEditor extends DecryptedItem<EditorContent> {
public readonly notes: SNNote[] = []
public readonly data: Record<string, unknown> = {}
public readonly url: string
public readonly name: string
public readonly isDefault: boolean
public readonly systemEditor: boolean
constructor(payload: DecryptedPayloadInterface<EditorContent>) {
super(payload)
this.url = payload.content.url
this.name = payload.content.name
this.data = payload.content.data || {}
this.isDefault = payload.content.default
this.systemEditor = payload.content.systemEditor
}
}

View File

@@ -0,0 +1 @@
export * from './Editor'

View File

@@ -0,0 +1,33 @@
import { useBoolean } from '@standardnotes/utils'
import { DecryptedItem } from '../../Abstract/Item/Implementations/DecryptedItem'
import { ItemContent } from '../../Abstract/Content/ItemContent'
export interface FeatureRepoContent extends ItemContent {
migratedToUserSetting?: boolean
migratedToOfflineEntitlements?: boolean
offlineFeaturesUrl?: string
offlineKey?: string
url?: string
}
export class SNFeatureRepo extends DecryptedItem<FeatureRepoContent> {
public get migratedToUserSetting(): boolean {
return useBoolean(this.payload.content.migratedToUserSetting, false)
}
public get migratedToOfflineEntitlements(): boolean {
return useBoolean(this.payload.content.migratedToOfflineEntitlements, false)
}
public get onlineUrl(): string | undefined {
return this.payload.content.url
}
get offlineFeaturesUrl(): string | undefined {
return this.payload.content.offlineFeaturesUrl
}
get offlineKey(): string | undefined {
return this.payload.content.offlineKey
}
}

View File

@@ -0,0 +1,20 @@
import { DecryptedItemMutator } from '../../Abstract/Item/Mutator/DecryptedItemMutator'
import { FeatureRepoContent } from './FeatureRepo'
export class FeatureRepoMutator extends DecryptedItemMutator<FeatureRepoContent> {
set migratedToUserSetting(migratedToUserSetting: boolean) {
this.mutableContent.migratedToUserSetting = migratedToUserSetting
}
set migratedToOfflineEntitlements(migratedToOfflineEntitlements: boolean) {
this.mutableContent.migratedToOfflineEntitlements = migratedToOfflineEntitlements
}
set offlineFeaturesUrl(offlineFeaturesUrl: string) {
this.mutableContent.offlineFeaturesUrl = offlineFeaturesUrl
}
set offlineKey(offlineKey: string) {
this.mutableContent.offlineKey = offlineKey
}
}

View File

@@ -0,0 +1,2 @@
export * from './FeatureRepo'
export * from './FeatureRepoMutator'

View File

@@ -0,0 +1,75 @@
import { ConflictStrategy } from './../../Abstract/Item/Types/ConflictStrategy'
import { ContentType } from '@standardnotes/common'
import { FillItemContent } from '../../Abstract/Content/ItemContent'
import { DecryptedPayload, PayloadTimestampDefaults } from '../../Abstract/Payload'
import { FileContent, FileItem } from './File'
import { UuidGenerator } from '@standardnotes/utils'
UuidGenerator.SetGenerator(() => String(Math.random()))
describe('file', () => {
const createFile = (content: Partial<FileContent> = {}): FileItem => {
return new FileItem(
new DecryptedPayload<FileContent>({
uuid: '123',
content_type: ContentType.File,
content: FillItemContent<FileContent>({
name: 'name.png',
key: 'secret',
remoteIdentifier: 'A',
encryptionHeader: 'header',
encryptedChunkSizes: [1, 2, 3],
...content,
}),
dirty: true,
...PayloadTimestampDefaults(),
}),
)
}
const copyFile = (file: FileItem, override: Partial<FileContent> = {}): FileItem => {
return new FileItem(
file.payload.copy({
content: {
...file.content,
...override,
} as FileContent,
}),
)
}
it('should not copy on name conflict', () => {
const file = createFile({ name: 'file.png' })
const conflictedFile = copyFile(file, { name: 'different.png' })
expect(file.strategyWhenConflictingWithItem(conflictedFile)).toEqual(ConflictStrategy.KeepBase)
})
it('should copy on key conflict', () => {
const file = createFile({ name: 'file.png' })
const conflictedFile = copyFile(file, { key: 'different-secret' })
expect(file.strategyWhenConflictingWithItem(conflictedFile)).toEqual(ConflictStrategy.KeepBaseDuplicateApply)
})
it('should copy on header conflict', () => {
const file = createFile({ name: 'file.png' })
const conflictedFile = copyFile(file, { encryptionHeader: 'different-header' })
expect(file.strategyWhenConflictingWithItem(conflictedFile)).toEqual(ConflictStrategy.KeepBaseDuplicateApply)
})
it('should copy on identifier conflict', () => {
const file = createFile({ name: 'file.png' })
const conflictedFile = copyFile(file, { remoteIdentifier: 'different-identifier' })
expect(file.strategyWhenConflictingWithItem(conflictedFile)).toEqual(ConflictStrategy.KeepBaseDuplicateApply)
})
it('should copy on chunk sizes conflict', () => {
const file = createFile({ name: 'file.png' })
const conflictedFile = copyFile(file, { encryptedChunkSizes: [10, 9, 8] })
expect(file.strategyWhenConflictingWithItem(conflictedFile)).toEqual(ConflictStrategy.KeepBaseDuplicateApply)
})
})

View File

@@ -0,0 +1,85 @@
import { DecryptedItem } from '../../Abstract/Item/Implementations/DecryptedItem'
import { ItemContent } from '../../Abstract/Content/ItemContent'
import { DecryptedPayloadInterface } from '../../Abstract/Payload/Interfaces/DecryptedPayload'
import { FileMetadata } from './FileMetadata'
import { FileProtocolV1 } from './FileProtocolV1'
import { SortableItem } from '../../Runtime/Collection/CollectionSort'
import { ConflictStrategy } from '../../Abstract/Item'
type EncryptedBytesLength = number
type DecryptedBytesLength = number
interface SizesDeprecatedDueToAmbiguousNaming {
size?: DecryptedBytesLength
chunkSizes?: EncryptedBytesLength[]
}
interface Sizes {
decryptedSize: DecryptedBytesLength
encryptedChunkSizes: EncryptedBytesLength[]
}
interface FileContentWithoutSize {
remoteIdentifier: string
name: string
key: string
encryptionHeader: string
mimeType: string
}
export type FileContentSpecialized = FileContentWithoutSize & FileMetadata & SizesDeprecatedDueToAmbiguousNaming & Sizes
export type FileContent = FileContentSpecialized & ItemContent
export class FileItem
extends DecryptedItem<FileContent>
implements FileContentWithoutSize, Sizes, FileProtocolV1, FileMetadata, SortableItem
{
public readonly remoteIdentifier: string
public readonly name: string
public readonly key: string
public readonly encryptionHeader: string
public readonly mimeType: string
public readonly decryptedSize: DecryptedBytesLength
public readonly encryptedChunkSizes: EncryptedBytesLength[]
constructor(payload: DecryptedPayloadInterface<FileContent>) {
super(payload)
this.remoteIdentifier = this.content.remoteIdentifier
this.name = this.content.name
this.key = this.content.key
if (this.content.size && this.content.chunkSizes) {
this.decryptedSize = this.content.size
this.encryptedChunkSizes = this.content.chunkSizes
} else {
this.decryptedSize = this.content.decryptedSize
this.encryptedChunkSizes = this.content.encryptedChunkSizes
}
this.encryptionHeader = this.content.encryptionHeader
this.mimeType = this.content.mimeType
}
public override strategyWhenConflictingWithItem(item: FileItem): ConflictStrategy {
if (
item.key !== this.key ||
item.encryptionHeader !== this.encryptionHeader ||
item.remoteIdentifier !== this.remoteIdentifier ||
JSON.stringify(item.encryptedChunkSizes) !== JSON.stringify(this.encryptedChunkSizes)
) {
return ConflictStrategy.KeepBaseDuplicateApply
}
return ConflictStrategy.KeepBase
}
public get encryptedSize(): number {
return this.encryptedChunkSizes.reduce((total, chunk) => total + chunk, 0)
}
public get title(): string {
return this.name
}
}

View File

@@ -0,0 +1,4 @@
export interface FileMetadata {
name: string
mimeType: string
}

View File

@@ -0,0 +1,33 @@
import { ContentType } from '@standardnotes/common'
import { SNNote } from '../Note/Note'
import { FileContent } from './File'
import { FileToNoteReference } from '../../Abstract/Reference/FileToNoteReference'
import { ContenteReferenceType } from '../../Abstract/Reference/ContenteReferenceType'
import { DecryptedItemMutator } from '../../Abstract/Item/Mutator/DecryptedItemMutator'
export class FileMutator extends DecryptedItemMutator<FileContent> {
set name(newName: string) {
this.mutableContent.name = newName
}
set encryptionHeader(encryptionHeader: string) {
this.mutableContent.encryptionHeader = encryptionHeader
}
public addNote(note: SNNote): void {
const reference: FileToNoteReference = {
reference_type: ContenteReferenceType.FileToNote,
content_type: ContentType.Note,
uuid: note.uuid,
}
const references = this.mutableContent.references || []
references.push(reference)
this.mutableContent.references = references
}
public removeNote(note: SNNote): void {
const references = this.immutableItem.references.filter((ref) => ref.uuid !== note.uuid)
this.mutableContent.references = references
}
}

View File

@@ -0,0 +1,9 @@
export interface FileProtocolV1 {
readonly encryptionHeader: string
readonly key: string
readonly remoteIdentifier: string
}
export enum FileProtocolV1Constants {
KeySize = 256,
}

View File

@@ -0,0 +1,4 @@
export * from './File'
export * from './FileMutator'
export * from './FileMetadata'
export * from './FileProtocolV1'

View File

@@ -0,0 +1,19 @@
import { ProtocolVersion } from '@standardnotes/common'
import { DecryptedItemInterface } from './../../Abstract/Item/Interfaces/DecryptedItem'
import { ItemContent, SpecializedContent } from '../../Abstract/Content/ItemContent'
export interface ItemsKeyContentSpecialized extends SpecializedContent {
version: ProtocolVersion
isDefault?: boolean | undefined
itemsKey: string
dataAuthenticationKey?: string
}
export type ItemsKeyContent = ItemsKeyContentSpecialized & ItemContent
export interface ItemsKeyInterface extends DecryptedItemInterface<ItemsKeyContent> {
get keyVersion(): ProtocolVersion
get isDefault(): boolean | undefined
get itemsKey(): string
get dataAuthenticationKey(): string | undefined
}

View File

@@ -0,0 +1,5 @@
import { DecryptedItemMutator } from '../../Abstract/Item/Mutator/DecryptedItemMutator'
export interface ItemsKeyMutatorInterface extends DecryptedItemMutator {
set isDefault(isDefault: boolean)
}

View File

@@ -0,0 +1,42 @@
import { createNote } from './../../Utilities/Test/SpecUtils'
describe('SNNote Tests', () => {
it('should safely type required fields of Note when creating from PayloadContent', () => {
const note = createNote({
title: 'Expected string',
text: ['unexpected array'] as never,
preview_plain: 'Expected preview',
preview_html: {} as never,
hidePreview: 'string' as never,
})
expect([
typeof note.title,
typeof note.text,
typeof note.preview_html,
typeof note.preview_plain,
typeof note.hidePreview,
]).toStrictEqual(['string', 'string', 'string', 'string', 'boolean'])
})
it('should preserve falsy values when casting from PayloadContent', () => {
const note = createNote({
preview_plain: null as never,
preview_html: undefined,
})
expect(note.preview_plain).toBeFalsy()
expect(note.preview_html).toBeFalsy()
})
it('should set mobilePrefersPlainEditor when given a valid choice', () => {
const selected = createNote({
mobilePrefersPlainEditor: true,
})
const unselected = createNote()
expect(selected.mobilePrefersPlainEditor).toBeTruthy()
expect(unselected.mobilePrefersPlainEditor).toBe(undefined)
})
})

View File

@@ -0,0 +1,34 @@
import { ContentType } from '@standardnotes/common'
import { DecryptedItem } from '../../Abstract/Item/Implementations/DecryptedItem'
import { ItemInterface } from '../../Abstract/Item/Interfaces/ItemInterface'
import { AppDataField } from '../../Abstract/Item/Types/AppDataField'
import { DecryptedPayloadInterface } from '../../Abstract/Payload/Interfaces/DecryptedPayload'
import { NoteContent, NoteContentSpecialized } from './NoteContent'
export const isNote = (x: ItemInterface): x is SNNote => x.content_type === ContentType.Note
export class SNNote extends DecryptedItem<NoteContent> implements NoteContentSpecialized {
public readonly title: string
public readonly text: string
public readonly mobilePrefersPlainEditor?: boolean
public readonly hidePreview: boolean = false
public readonly preview_plain: string
public readonly preview_html: string
public readonly prefersPlainEditor: boolean
public readonly spellcheck?: boolean
constructor(payload: DecryptedPayloadInterface<NoteContent>) {
super(payload)
this.title = String(this.payload.content.title || '')
this.text = String(this.payload.content.text || '')
this.preview_plain = String(this.payload.content.preview_plain || '')
this.preview_html = String(this.payload.content.preview_html || '')
this.hidePreview = Boolean(this.payload.content.hidePreview)
this.spellcheck = this.payload.content.spellcheck
this.prefersPlainEditor = this.getAppDomainValueWithDefault(AppDataField.PrefersPlainEditor, false)
this.mobilePrefersPlainEditor = this.payload.content.mobilePrefersPlainEditor
}
}

View File

@@ -0,0 +1,13 @@
import { ItemContent } from '../../Abstract/Content/ItemContent'
export interface NoteContentSpecialized {
title: string
text: string
mobilePrefersPlainEditor?: boolean
hidePreview?: boolean
preview_plain?: string
preview_html?: string
spellcheck?: boolean
}
export type NoteContent = NoteContentSpecialized & ItemContent

View File

@@ -0,0 +1,41 @@
import { AppDataField } from '../../Abstract/Item/Types/AppDataField'
import { NoteContent } from './NoteContent'
import { DecryptedItemMutator } from '../../Abstract/Item/Mutator/DecryptedItemMutator'
export class NoteMutator extends DecryptedItemMutator<NoteContent> {
set title(title: string) {
this.mutableContent.title = title
}
set text(text: string) {
this.mutableContent.text = text
}
set hidePreview(hidePreview: boolean) {
this.mutableContent.hidePreview = hidePreview
}
set preview_plain(preview_plain: string) {
this.mutableContent.preview_plain = preview_plain
}
set preview_html(preview_html: string | undefined) {
this.mutableContent.preview_html = preview_html
}
set prefersPlainEditor(prefersPlainEditor: boolean) {
this.setAppDataItem(AppDataField.PrefersPlainEditor, prefersPlainEditor)
}
set spellcheck(spellcheck: boolean) {
this.mutableContent.spellcheck = spellcheck
}
toggleSpellcheck(): void {
if (this.mutableContent.spellcheck == undefined) {
this.mutableContent.spellcheck = false
} else {
this.mutableContent.spellcheck = !this.mutableContent.spellcheck
}
}
}

View File

@@ -0,0 +1,3 @@
export * from './Note'
export * from './NoteMutator'
export * from './NoteContent'

View File

@@ -0,0 +1,44 @@
import { DecryptedItem } from '../../Abstract/Item/Implementations/DecryptedItem'
import { PredicateInterface, PredicateJsonForm } from '../../Runtime/Predicate/Interface'
import { predicateFromJson } from '../../Runtime/Predicate/Generators'
import { ItemContent } from '../../Abstract/Content/ItemContent'
import { DecryptedPayloadInterface } from '../../Abstract/Payload/Interfaces/DecryptedPayload'
export const SMART_TAG_DSL_PREFIX = '!['
export enum SystemViewId {
AllNotes = 'all-notes',
Files = 'files',
ArchivedNotes = 'archived-notes',
TrashedNotes = 'trashed-notes',
UntaggedNotes = 'untagged-notes',
}
export interface SmartViewContent extends ItemContent {
title: string
predicate: PredicateJsonForm
}
export function isSystemView(view: SmartView): boolean {
return Object.values(SystemViewId).includes(view.uuid as SystemViewId)
}
/**
* A tag that defines a predicate that consumers can use
* to retrieve a dynamic list of items.
*/
export class SmartView extends DecryptedItem<SmartViewContent> {
public readonly predicate!: PredicateInterface<DecryptedItem>
public readonly title: string
constructor(payload: DecryptedPayloadInterface<SmartViewContent>) {
super(payload)
this.title = String(this.content.title || '')
try {
this.predicate = this.content.predicate && predicateFromJson(this.content.predicate)
} catch (error) {
console.error(error)
}
}
}

View File

@@ -0,0 +1,179 @@
import { DecryptedPayload } from './../../Abstract/Payload/Implementations/DecryptedPayload'
import { SNNote } from '../Note/Note'
import { SmartViewContent, SmartView, SystemViewId } from './SmartView'
import { ItemWithTags } from '../../Runtime/Display/Search/ItemWithTags'
import { ContentType } from '@standardnotes/common'
import { FillItemContent } from '../../Abstract/Content/ItemContent'
import { Predicate } from '../../Runtime/Predicate/Predicate'
import { CompoundPredicate } from '../../Runtime/Predicate/CompoundPredicate'
import { PayloadTimestampDefaults } from '../../Abstract/Payload'
import { FilterDisplayOptions } from '../../Runtime/Display'
import { FileItem } from '../File'
export function BuildSmartViews(
options: FilterDisplayOptions,
{ supportsFileNavigation = false }: { supportsFileNavigation: boolean },
): SmartView[] {
const notes = new SmartView(
new DecryptedPayload({
uuid: SystemViewId.AllNotes,
content_type: ContentType.SmartView,
...PayloadTimestampDefaults(),
content: FillItemContent<SmartViewContent>({
title: 'Notes',
predicate: allNotesPredicate(options).toJson(),
}),
}),
)
const files = new SmartView(
new DecryptedPayload({
uuid: SystemViewId.Files,
content_type: ContentType.SmartView,
...PayloadTimestampDefaults(),
content: FillItemContent<SmartViewContent>({
title: 'Files',
predicate: filesPredicate(options).toJson(),
}),
}),
)
const archived = new SmartView(
new DecryptedPayload({
uuid: SystemViewId.ArchivedNotes,
content_type: ContentType.SmartView,
...PayloadTimestampDefaults(),
content: FillItemContent<SmartViewContent>({
title: 'Archived',
predicate: archivedNotesPredicate(options).toJson(),
}),
}),
)
const trash = new SmartView(
new DecryptedPayload({
uuid: SystemViewId.TrashedNotes,
content_type: ContentType.SmartView,
...PayloadTimestampDefaults(),
content: FillItemContent<SmartViewContent>({
title: 'Trash',
predicate: trashedNotesPredicate(options).toJson(),
}),
}),
)
const untagged = new SmartView(
new DecryptedPayload({
uuid: SystemViewId.UntaggedNotes,
content_type: ContentType.SmartView,
...PayloadTimestampDefaults(),
content: FillItemContent<SmartViewContent>({
title: 'Untagged',
predicate: untaggedNotesPredicate(options).toJson(),
}),
}),
)
if (supportsFileNavigation) {
return [notes, files, archived, trash, untagged]
} else {
return [notes, archived, trash, untagged]
}
}
function allNotesPredicate(options: FilterDisplayOptions) {
const subPredicates: Predicate<SNNote>[] = [new Predicate('content_type', '=', ContentType.Note)]
if (options.includeTrashed === false) {
subPredicates.push(new Predicate('trashed', '=', false))
}
if (options.includeArchived === false) {
subPredicates.push(new Predicate('archived', '=', false))
}
if (options.includeProtected === false) {
subPredicates.push(new Predicate('protected', '=', false))
}
if (options.includePinned === false) {
subPredicates.push(new Predicate('pinned', '=', false))
}
const predicate = new CompoundPredicate('and', subPredicates)
return predicate
}
function filesPredicate(options: FilterDisplayOptions) {
const subPredicates: Predicate<FileItem>[] = [new Predicate('content_type', '=', ContentType.File)]
if (options.includeTrashed === false) {
subPredicates.push(new Predicate('trashed', '=', false))
}
if (options.includeArchived === false) {
subPredicates.push(new Predicate('archived', '=', false))
}
if (options.includeProtected === false) {
subPredicates.push(new Predicate('protected', '=', false))
}
if (options.includePinned === false) {
subPredicates.push(new Predicate('pinned', '=', false))
}
const predicate = new CompoundPredicate('and', subPredicates)
return predicate
}
function archivedNotesPredicate(options: FilterDisplayOptions) {
const subPredicates: Predicate<SNNote>[] = [
new Predicate('archived', '=', true),
new Predicate('content_type', '=', ContentType.Note),
]
if (options.includeTrashed === false) {
subPredicates.push(new Predicate('trashed', '=', false))
}
if (options.includeProtected === false) {
subPredicates.push(new Predicate('protected', '=', false))
}
if (options.includePinned === false) {
subPredicates.push(new Predicate('pinned', '=', false))
}
const predicate = new CompoundPredicate('and', subPredicates)
return predicate
}
function trashedNotesPredicate(options: FilterDisplayOptions) {
const subPredicates: Predicate<SNNote>[] = [
new Predicate('trashed', '=', true),
new Predicate('content_type', '=', ContentType.Note),
]
if (options.includeArchived === false) {
subPredicates.push(new Predicate('archived', '=', false))
}
if (options.includeProtected === false) {
subPredicates.push(new Predicate('protected', '=', false))
}
if (options.includePinned === false) {
subPredicates.push(new Predicate('pinned', '=', false))
}
const predicate = new CompoundPredicate('and', subPredicates)
return predicate
}
function untaggedNotesPredicate(options: FilterDisplayOptions) {
const subPredicates = [
new Predicate('content_type', '=', ContentType.Note),
new Predicate<ItemWithTags>('tagsCount', '=', 0),
]
if (options.includeArchived === false) {
subPredicates.push(new Predicate('archived', '=', false))
}
if (options.includeProtected === false) {
subPredicates.push(new Predicate('protected', '=', false))
}
if (options.includePinned === false) {
subPredicates.push(new Predicate('pinned', '=', false))
}
const predicate = new CompoundPredicate('and', subPredicates)
return predicate
}

View File

@@ -0,0 +1,2 @@
export * from './SmartView'
export * from './SmartViewBuilder'

View File

@@ -0,0 +1,40 @@
import { PayloadSource } from './../../Abstract/Payload/Types/PayloadSource'
import { DecryptedPayload } from './../../Abstract/Payload/Implementations/DecryptedPayload'
import { SNTag, TagContent } from './Tag'
import { ContentType } from '@standardnotes/common'
import { FillItemContent } from '../../Abstract/Content/ItemContent'
import { ContentReference } from '../../Abstract/Reference/ContentReference'
import { PayloadTimestampDefaults } from '../../Abstract/Payload'
const randUuid = () => String(Math.random())
const create = (title: string, references: ContentReference[] = []): SNTag => {
const tag = new SNTag(
new DecryptedPayload(
{
uuid: randUuid(),
content_type: ContentType.Tag,
content: FillItemContent({
title,
references,
} as TagContent),
...PayloadTimestampDefaults(),
},
PayloadSource.Constructor,
),
)
return tag
}
describe('SNTag Tests', () => {
it('should count notes in the basic case', () => {
const tag = create('helloworld', [
{ uuid: randUuid(), content_type: ContentType.Note },
{ uuid: randUuid(), content_type: ContentType.Note },
{ uuid: randUuid(), content_type: ContentType.Tag },
])
expect(tag.noteCount).toEqual(2)
})
})

View File

@@ -0,0 +1,56 @@
import { ContentType, Uuid } from '@standardnotes/common'
import { DecryptedItem } from '../../Abstract/Item/Implementations/DecryptedItem'
import { ItemInterface } from '../../Abstract/Item/Interfaces/ItemInterface'
import { ItemContent } from '../../Abstract/Content/ItemContent'
import { ContentReference } from '../../Abstract/Reference/ContentReference'
import { isTagToParentTagReference } from '../../Abstract/Reference/Functions'
import { DecryptedPayloadInterface } from '../../Abstract/Payload/Interfaces/DecryptedPayload'
export const TagFolderDelimitter = '.'
interface TagInterface {
title: string
expanded: boolean
}
export type TagContent = TagInterface & ItemContent
export const isTag = (x: ItemInterface): x is SNTag => x.content_type === ContentType.Tag
export class SNTag extends DecryptedItem<TagContent> implements TagInterface {
public readonly title: string
/** Whether to render child tags in view hierarchy. Opposite of collapsed. */
public readonly expanded: boolean
constructor(payload: DecryptedPayloadInterface<TagContent>) {
super(payload)
this.title = this.payload.content.title || ''
this.expanded = this.payload.content.expanded != undefined ? this.payload.content.expanded : true
}
get noteReferences(): ContentReference[] {
const references = this.payload.references
return references.filter((ref) => ref.content_type === ContentType.Note)
}
get noteCount(): number {
return this.noteReferences.length
}
public get parentId(): Uuid | undefined {
const reference = this.references.find(isTagToParentTagReference)
return reference?.uuid
}
public static arrayToDisplayString(tags: SNTag[]): string {
return tags
.sort((a, b) => {
return a.title > b.title ? 1 : -1
})
.map((tag) => {
return '#' + tag.title
})
.join(' ')
}
}

View File

@@ -0,0 +1,38 @@
import { ContentType } from '@standardnotes/common'
import { ContenteReferenceType, MutationType } from '../../Abstract/Item'
import { createFile, createTag } from '../../Utilities/Test/SpecUtils'
import { SNTag } from './Tag'
import { TagMutator } from './TagMutator'
describe('tag mutator', () => {
it('should add file to tag', () => {
const file = createFile()
const tag = createTag()
const mutator = new TagMutator(tag, MutationType.UpdateUserTimestamps)
mutator.addFile(file)
const result = mutator.getResult()
expect(result.content.references[0]).toEqual({
uuid: file.uuid,
content_type: ContentType.File,
reference_type: ContenteReferenceType.TagToFile,
})
})
it('should remove file from tag', () => {
const file = createFile()
const tag = createTag()
const addMutator = new TagMutator(tag, MutationType.UpdateUserTimestamps)
addMutator.addFile(file)
const addResult = addMutator.getResult()
const mutatedTag = new SNTag(addResult)
const removeMutator = new TagMutator(mutatedTag, MutationType.UpdateUserTimestamps)
removeMutator.removeFile(file)
const removeResult = removeMutator.getResult()
expect(removeResult.content.references).toHaveLength(0)
})
})

View File

@@ -0,0 +1,70 @@
import { ContentType } from '@standardnotes/common'
import { TagContent, SNTag } from './Tag'
import { FileItem } from '../File'
import { SNNote } from '../Note'
import { isTagToParentTagReference } from '../../Abstract/Reference/Functions'
import { TagToParentTagReference } from '../../Abstract/Reference/TagToParentTagReference'
import { ContenteReferenceType } from '../../Abstract/Reference/ContenteReferenceType'
import { DecryptedItemMutator } from '../../Abstract/Item/Mutator/DecryptedItemMutator'
import { TagToFileReference } from '../../Abstract/Reference/TagToFileReference'
export class TagMutator extends DecryptedItemMutator<TagContent> {
set title(title: string) {
this.mutableContent.title = title
}
set expanded(expanded: boolean) {
this.mutableContent.expanded = expanded
}
public makeChildOf(tag: SNTag): void {
const references = this.immutableItem.references.filter((ref) => !isTagToParentTagReference(ref))
const reference: TagToParentTagReference = {
reference_type: ContenteReferenceType.TagToParentTag,
content_type: ContentType.Tag,
uuid: tag.uuid,
}
references.push(reference)
this.mutableContent.references = references
}
public unsetParent(): void {
this.mutableContent.references = this.immutableItem.references.filter((ref) => !isTagToParentTagReference(ref))
}
public addFile(file: FileItem): void {
if (this.immutableItem.isReferencingItem(file)) {
return
}
const reference: TagToFileReference = {
reference_type: ContenteReferenceType.TagToFile,
content_type: ContentType.File,
uuid: file.uuid,
}
this.mutableContent.references.push(reference)
}
public removeFile(file: FileItem): void {
this.mutableContent.references = this.mutableContent.references.filter((r) => r.uuid !== file.uuid)
}
public addNote(note: SNNote): void {
if (this.immutableItem.isReferencingItem(note)) {
return
}
this.mutableContent.references.push({
uuid: note.uuid,
content_type: note.content_type,
})
}
public removeNote(note: SNNote): void {
this.mutableContent.references = this.mutableContent.references.filter((r) => r.uuid !== note.uuid)
}
}

View File

@@ -0,0 +1,2 @@
export * from './Tag'
export * from './TagMutator'

View File

@@ -0,0 +1,48 @@
import { ComponentArea } from '@standardnotes/features'
import { SNComponent } from '../Component/Component'
import { ConflictStrategy } from '../../Abstract/Item/Types/ConflictStrategy'
import { AppDataField } from '../../Abstract/Item/Types/AppDataField'
import { HistoryEntryInterface } from '../../Runtime/History'
import { DecryptedItemInterface, ItemInterface } from '../../Abstract/Item'
import { ContentType } from '@standardnotes/common'
import { useBoolean } from '@standardnotes/utils'
export const isTheme = (x: ItemInterface): x is SNTheme => x.content_type === ContentType.Theme
export class SNTheme extends SNComponent {
public override area: ComponentArea = ComponentArea.Themes
isLayerable(): boolean {
return useBoolean(this.package_info && this.package_info.layerable, false)
}
/** Do not duplicate under most circumstances. Always keep original */
override strategyWhenConflictingWithItem(
_item: DecryptedItemInterface,
_previousRevision?: HistoryEntryInterface,
): ConflictStrategy {
return ConflictStrategy.KeepBase
}
getMobileRules() {
return (
this.getAppDomainValue(AppDataField.MobileRules) || {
constants: {},
rules: {},
}
)
}
/** Same as getMobileRules but without default value. */
hasMobileRules() {
return this.getAppDomainValue(AppDataField.MobileRules)
}
getNotAvailOnMobile() {
return this.getAppDomainValue(AppDataField.NotAvailableOnMobile)
}
isMobileActive() {
return this.getAppDomainValue(AppDataField.MobileActive)
}
}

View File

@@ -0,0 +1,25 @@
import { AppDataField } from '../../Abstract/Item/Types/AppDataField'
import { ComponentContent } from '../Component/ComponentContent'
import { DecryptedItemMutator } from '../../Abstract/Item/Mutator/DecryptedItemMutator'
export class ThemeMutator extends DecryptedItemMutator<ComponentContent> {
setMobileRules(rules: unknown) {
this.setAppDataItem(AppDataField.MobileRules, rules)
}
setNotAvailOnMobile(notAvailable: boolean) {
this.setAppDataItem(AppDataField.NotAvailableOnMobile, notAvailable)
}
set local_url(local_url: string) {
this.mutableContent.local_url = local_url
}
/**
* We must not use .active because if you set that to true, it will also
* activate that theme on desktop/web
*/
setMobileActive(active: boolean) {
this.setAppDataItem(AppDataField.MobileActive, active)
}
}

View File

@@ -0,0 +1,2 @@
export * from './Theme'
export * from './ThemeMutator'

View File

@@ -0,0 +1,68 @@
import { CollectionSortProperty } from '../../Runtime/Collection/CollectionSort'
import { FeatureIdentifier } from '@standardnotes/features'
export enum PrefKey {
TagsPanelWidth = 'tagsPanelWidth',
NotesPanelWidth = 'notesPanelWidth',
EditorWidth = 'editorWidth',
EditorLeft = 'editorLeft',
EditorMonospaceEnabled = 'monospaceFont',
EditorSpellcheck = 'spellcheck',
EditorResizersEnabled = 'marginResizersEnabled',
SortNotesBy = 'sortBy',
SortNotesReverse = 'sortReverse',
NotesShowArchived = 'showArchived',
NotesShowTrashed = 'showTrashed',
NotesHideProtected = 'hideProtected',
NotesHidePinned = 'hidePinned',
NotesHideNotePreview = 'hideNotePreview',
NotesHideDate = 'hideDate',
NotesHideTags = 'hideTags',
NotesHideEditorIcon = 'hideEditorIcon',
UseSystemColorScheme = 'useSystemColorScheme',
AutoLightThemeIdentifier = 'autoLightThemeIdentifier',
AutoDarkThemeIdentifier = 'autoDarkThemeIdentifier',
NoteAddToParentFolders = 'noteAddToParentFolders',
MobileSortNotesBy = 'mobileSortBy',
MobileSortNotesReverse = 'mobileSortReverse',
MobileNotesHideNotePreview = 'mobileHideNotePreview',
MobileNotesHideDate = 'mobileHideDate',
MobileNotesHideTags = 'mobileHideTags',
MobileLastExportDate = 'mobileLastExportDate',
MobileDoNotShowAgainUnsupportedEditors = 'mobileDoNotShowAgainUnsupportedEditors',
MobileSelectedTagUuid = 'mobileSelectedTagUuid',
MobileNotesHideEditorIcon = 'mobileHideEditorIcon',
}
export type PrefValue = {
[PrefKey.TagsPanelWidth]: number
[PrefKey.NotesPanelWidth]: number
[PrefKey.EditorWidth]: number | null
[PrefKey.EditorLeft]: number | null
[PrefKey.EditorMonospaceEnabled]: boolean
[PrefKey.EditorSpellcheck]: boolean
[PrefKey.EditorResizersEnabled]: boolean
[PrefKey.SortNotesBy]: CollectionSortProperty
[PrefKey.SortNotesReverse]: boolean
[PrefKey.NotesShowArchived]: boolean
[PrefKey.NotesShowTrashed]: boolean
[PrefKey.NotesHidePinned]: boolean
[PrefKey.NotesHideProtected]: boolean
[PrefKey.NotesHideNotePreview]: boolean
[PrefKey.NotesHideDate]: boolean
[PrefKey.NotesHideTags]: boolean
[PrefKey.NotesHideEditorIcon]: boolean
[PrefKey.UseSystemColorScheme]: boolean
[PrefKey.AutoLightThemeIdentifier]: FeatureIdentifier | 'Default'
[PrefKey.AutoDarkThemeIdentifier]: FeatureIdentifier | 'Default'
[PrefKey.NoteAddToParentFolders]: boolean
[PrefKey.MobileSortNotesBy]: CollectionSortProperty
[PrefKey.MobileSortNotesReverse]: boolean
[PrefKey.MobileNotesHideNotePreview]: boolean
[PrefKey.MobileNotesHideDate]: boolean
[PrefKey.MobileNotesHideTags]: boolean
[PrefKey.MobileLastExportDate]: Date | undefined
[PrefKey.MobileDoNotShowAgainUnsupportedEditors]: boolean
[PrefKey.MobileSelectedTagUuid]: string | undefined
[PrefKey.MobileNotesHideEditorIcon]: boolean
}

View File

@@ -0,0 +1,20 @@
import { DecryptedItem } from '../../Abstract/Item/Implementations/DecryptedItem'
import { ContentType } from '@standardnotes/common'
import { Predicate } from '../../Runtime/Predicate/Predicate'
import { PrefKey, PrefValue } from './PrefKey'
export class SNUserPrefs extends DecryptedItem {
static singletonPredicate = new Predicate('content_type', '=', ContentType.UserPrefs)
override get isSingleton(): true {
return true
}
override singletonPredicate(): Predicate<SNUserPrefs> {
return SNUserPrefs.singletonPredicate
}
getPref<K extends PrefKey>(key: K): PrefValue[K] | undefined {
return this.getAppDomainValue(key)
}
}

View File

@@ -0,0 +1,8 @@
import { DecryptedItemMutator } from '../../Abstract/Item/Mutator/DecryptedItemMutator'
import { PrefKey, PrefValue } from './PrefKey'
export class UserPrefsMutator extends DecryptedItemMutator {
setPref<K extends PrefKey>(key: K, value: PrefValue[K]): void {
this.setAppDataItem(key, value)
}
}

View File

@@ -0,0 +1,3 @@
export * from './UserPrefs'
export * from './UserPrefsMutator'
export * from './PrefKey'