feat: add models package
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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[]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './ActionsExtension'
|
||||
export * from './ActionsExtensionMutator'
|
||||
export * from './Types'
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
189
packages/models/src/Domain/Syncable/Component/Component.ts
Normal file
189
packages/models/src/Domain/Syncable/Component/Component.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { FeatureDescription } from '@standardnotes/features'
|
||||
|
||||
type ThirdPartyPackageInfo = {
|
||||
version: string
|
||||
download_url?: string
|
||||
}
|
||||
|
||||
export type ComponentPackageInfo = FeatureDescription & Partial<ThirdPartyPackageInfo>
|
||||
3
packages/models/src/Domain/Syncable/Component/index.ts
Normal file
3
packages/models/src/Domain/Syncable/Component/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './Component'
|
||||
export * from './ComponentMutator'
|
||||
export * from './ComponentContent'
|
||||
35
packages/models/src/Domain/Syncable/Editor/Editor.ts
Normal file
35
packages/models/src/Domain/Syncable/Editor/Editor.ts
Normal 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
|
||||
}
|
||||
}
|
||||
1
packages/models/src/Domain/Syncable/Editor/index.ts
Normal file
1
packages/models/src/Domain/Syncable/Editor/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './Editor'
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
2
packages/models/src/Domain/Syncable/FeatureRepo/index.ts
Normal file
2
packages/models/src/Domain/Syncable/FeatureRepo/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './FeatureRepo'
|
||||
export * from './FeatureRepoMutator'
|
||||
75
packages/models/src/Domain/Syncable/File/File.spec.ts
Normal file
75
packages/models/src/Domain/Syncable/File/File.spec.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
85
packages/models/src/Domain/Syncable/File/File.ts
Normal file
85
packages/models/src/Domain/Syncable/File/File.ts
Normal 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
|
||||
}
|
||||
}
|
||||
4
packages/models/src/Domain/Syncable/File/FileMetadata.ts
Normal file
4
packages/models/src/Domain/Syncable/File/FileMetadata.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface FileMetadata {
|
||||
name: string
|
||||
mimeType: string
|
||||
}
|
||||
33
packages/models/src/Domain/Syncable/File/FileMutator.ts
Normal file
33
packages/models/src/Domain/Syncable/File/FileMutator.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export interface FileProtocolV1 {
|
||||
readonly encryptionHeader: string
|
||||
readonly key: string
|
||||
readonly remoteIdentifier: string
|
||||
}
|
||||
|
||||
export enum FileProtocolV1Constants {
|
||||
KeySize = 256,
|
||||
}
|
||||
4
packages/models/src/Domain/Syncable/File/index.ts
Normal file
4
packages/models/src/Domain/Syncable/File/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './File'
|
||||
export * from './FileMutator'
|
||||
export * from './FileMetadata'
|
||||
export * from './FileProtocolV1'
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { DecryptedItemMutator } from '../../Abstract/Item/Mutator/DecryptedItemMutator'
|
||||
|
||||
export interface ItemsKeyMutatorInterface extends DecryptedItemMutator {
|
||||
set isDefault(isDefault: boolean)
|
||||
}
|
||||
42
packages/models/src/Domain/Syncable/Note/Note.spec.ts
Normal file
42
packages/models/src/Domain/Syncable/Note/Note.spec.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
34
packages/models/src/Domain/Syncable/Note/Note.ts
Normal file
34
packages/models/src/Domain/Syncable/Note/Note.ts
Normal 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
|
||||
}
|
||||
}
|
||||
13
packages/models/src/Domain/Syncable/Note/NoteContent.ts
Normal file
13
packages/models/src/Domain/Syncable/Note/NoteContent.ts
Normal 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
|
||||
41
packages/models/src/Domain/Syncable/Note/NoteMutator.ts
Normal file
41
packages/models/src/Domain/Syncable/Note/NoteMutator.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
3
packages/models/src/Domain/Syncable/Note/index.ts
Normal file
3
packages/models/src/Domain/Syncable/Note/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './Note'
|
||||
export * from './NoteMutator'
|
||||
export * from './NoteContent'
|
||||
44
packages/models/src/Domain/Syncable/SmartView/SmartView.ts
Normal file
44
packages/models/src/Domain/Syncable/SmartView/SmartView.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
2
packages/models/src/Domain/Syncable/SmartView/index.ts
Normal file
2
packages/models/src/Domain/Syncable/SmartView/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './SmartView'
|
||||
export * from './SmartViewBuilder'
|
||||
40
packages/models/src/Domain/Syncable/Tag/Tag.spec.ts
Normal file
40
packages/models/src/Domain/Syncable/Tag/Tag.spec.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
56
packages/models/src/Domain/Syncable/Tag/Tag.ts
Normal file
56
packages/models/src/Domain/Syncable/Tag/Tag.ts
Normal 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(' ')
|
||||
}
|
||||
}
|
||||
38
packages/models/src/Domain/Syncable/Tag/TagMutator.spec.ts
Normal file
38
packages/models/src/Domain/Syncable/Tag/TagMutator.spec.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
70
packages/models/src/Domain/Syncable/Tag/TagMutator.ts
Normal file
70
packages/models/src/Domain/Syncable/Tag/TagMutator.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
2
packages/models/src/Domain/Syncable/Tag/index.ts
Normal file
2
packages/models/src/Domain/Syncable/Tag/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './Tag'
|
||||
export * from './TagMutator'
|
||||
48
packages/models/src/Domain/Syncable/Theme/Theme.ts
Normal file
48
packages/models/src/Domain/Syncable/Theme/Theme.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
25
packages/models/src/Domain/Syncable/Theme/ThemeMutator.ts
Normal file
25
packages/models/src/Domain/Syncable/Theme/ThemeMutator.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
2
packages/models/src/Domain/Syncable/Theme/index.ts
Normal file
2
packages/models/src/Domain/Syncable/Theme/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './Theme'
|
||||
export * from './ThemeMutator'
|
||||
68
packages/models/src/Domain/Syncable/UserPrefs/PrefKey.ts
Normal file
68
packages/models/src/Domain/Syncable/UserPrefs/PrefKey.ts
Normal 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
|
||||
}
|
||||
20
packages/models/src/Domain/Syncable/UserPrefs/UserPrefs.ts
Normal file
20
packages/models/src/Domain/Syncable/UserPrefs/UserPrefs.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
3
packages/models/src/Domain/Syncable/UserPrefs/index.ts
Normal file
3
packages/models/src/Domain/Syncable/UserPrefs/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './UserPrefs'
|
||||
export * from './UserPrefsMutator'
|
||||
export * from './PrefKey'
|
||||
Reference in New Issue
Block a user