fix: Fixes issue where lock screen would not use previously active theme (#2372)

This commit is contained in:
Mo
2023-07-26 15:50:08 -05:00
committed by GitHub
parent 86fc4c684d
commit d268c02ab3
88 changed files with 1118 additions and 716 deletions

View File

@@ -1,5 +0,0 @@
import { FeatureIdentifier } from './../Feature/FeatureIdentifier'
type ThirdPartyIdentifier = string
export type EditorIdentifier = FeatureIdentifier | ThirdPartyIdentifier

View File

@@ -1,16 +1,16 @@
import { FeatureIdentifier } from '@standardnotes/features' import { NativeFeatureIdentifier } from '@standardnotes/features'
import { noteTypeForEditorIdentifier, NoteType } from './NoteType' import { noteTypeForEditorIdentifier, NoteType } from './NoteType'
describe('note type', () => { describe('note type', () => {
it('should return the correct note type for editor identifier', () => { it('should return the correct note type for editor identifier', () => {
expect(noteTypeForEditorIdentifier(FeatureIdentifier.PlainEditor)).toEqual(NoteType.Plain) expect(noteTypeForEditorIdentifier(NativeFeatureIdentifier.TYPES.PlainEditor)).toEqual(NoteType.Plain)
expect(noteTypeForEditorIdentifier(FeatureIdentifier.SuperEditor)).toEqual(NoteType.Super) expect(noteTypeForEditorIdentifier(NativeFeatureIdentifier.TYPES.SuperEditor)).toEqual(NoteType.Super)
expect(noteTypeForEditorIdentifier(FeatureIdentifier.MarkdownProEditor)).toEqual(NoteType.Markdown) expect(noteTypeForEditorIdentifier(NativeFeatureIdentifier.TYPES.MarkdownProEditor)).toEqual(NoteType.Markdown)
expect(noteTypeForEditorIdentifier(FeatureIdentifier.PlusEditor)).toEqual(NoteType.RichText) expect(noteTypeForEditorIdentifier(NativeFeatureIdentifier.TYPES.PlusEditor)).toEqual(NoteType.RichText)
expect(noteTypeForEditorIdentifier(FeatureIdentifier.CodeEditor)).toEqual(NoteType.Code) expect(noteTypeForEditorIdentifier(NativeFeatureIdentifier.TYPES.CodeEditor)).toEqual(NoteType.Code)
expect(noteTypeForEditorIdentifier(FeatureIdentifier.SheetsEditor)).toEqual(NoteType.Spreadsheet) expect(noteTypeForEditorIdentifier(NativeFeatureIdentifier.TYPES.SheetsEditor)).toEqual(NoteType.Spreadsheet)
expect(noteTypeForEditorIdentifier(FeatureIdentifier.TaskEditor)).toEqual(NoteType.Task) expect(noteTypeForEditorIdentifier(NativeFeatureIdentifier.TYPES.TaskEditor)).toEqual(NoteType.Task)
expect(noteTypeForEditorIdentifier(FeatureIdentifier.TokenVaultEditor)).toEqual(NoteType.Authentication) expect(noteTypeForEditorIdentifier(NativeFeatureIdentifier.TYPES.TokenVaultEditor)).toEqual(NoteType.Authentication)
expect(noteTypeForEditorIdentifier('org.third.party')).toEqual(NoteType.Unknown) expect(noteTypeForEditorIdentifier('org.third.party')).toEqual(NoteType.Unknown)
}) })
}) })

View File

@@ -1,8 +1,6 @@
import { EditorFeatureDescription } from '../Feature/EditorFeatureDescription' import { EditorFeatureDescription } from '../Feature/EditorFeatureDescription'
import { FindNativeFeature } from '../Feature/Features' import { FindNativeFeature } from '../Feature/Features'
import { IframeComponentFeatureDescription } from '../Feature/IframeComponentFeatureDescription' import { IframeComponentFeatureDescription } from '../Feature/IframeComponentFeatureDescription'
import { FeatureIdentifier } from './../Feature/FeatureIdentifier'
import { EditorIdentifier } from './EditorIdentifier'
export enum NoteType { export enum NoteType {
Authentication = 'authentication', Authentication = 'authentication',
@@ -16,10 +14,8 @@ export enum NoteType {
Unknown = 'unknown', Unknown = 'unknown',
} }
export function noteTypeForEditorIdentifier(identifier: EditorIdentifier): NoteType { export function noteTypeForEditorIdentifier(identifier: string): NoteType {
const feature = FindNativeFeature<EditorFeatureDescription | IframeComponentFeatureDescription>( const feature = FindNativeFeature<EditorFeatureDescription | IframeComponentFeatureDescription>(identifier)
identifier as FeatureIdentifier,
)
if (feature && feature.note_type) { if (feature && feature.note_type) {
return feature.note_type return feature.note_type
} }

View File

@@ -1,5 +1,4 @@
import { PermissionName } from '../Permission/PermissionName' import { PermissionName } from '../Permission/PermissionName'
import { FeatureIdentifier } from './FeatureIdentifier'
import { ComponentFlag } from '../Component/ComponentFlag' import { ComponentFlag } from '../Component/ComponentFlag'
import { RoleFields } from './RoleFields' import { RoleFields } from './RoleFields'
@@ -14,7 +13,7 @@ export type BaseFeatureDescription = RoleFields & {
clientControlled?: boolean clientControlled?: boolean
flags?: ComponentFlag[] flags?: ComponentFlag[]
identifier: FeatureIdentifier identifier: string
marketing_url?: string marketing_url?: string
name: string name: string
no_expire?: boolean no_expire?: boolean

View File

@@ -1,9 +1,8 @@
import { PermissionName } from '../Permission/PermissionName' import { PermissionName } from '../Permission/PermissionName'
import { FeatureIdentifier } from './FeatureIdentifier'
import { RoleFields } from './RoleFields' import { RoleFields } from './RoleFields'
export type ClientFeatureDescription = RoleFields & { export type ClientFeatureDescription = RoleFields & {
identifier: FeatureIdentifier identifier: string
permission_name: PermissionName permission_name: PermissionName
description: string description: string
name: string name: string

View File

@@ -1,51 +0,0 @@
export enum FeatureIdentifier {
DailyEmailBackup = 'org.standardnotes.daily-email-backup',
Files = 'org.standardnotes.files',
FilesLowStorageTier = 'org.standardnotes.files-low-storage-tier',
FilesMaximumStorageTier = 'org.standardnotes.files-max-storage-tier',
ListedCustomDomain = 'org.standardnotes.listed-custom-domain',
NoteHistory30Days = 'org.standardnotes.note-history-30',
NoteHistory365Days = 'org.standardnotes.note-history-365',
NoteHistoryUnlimited = 'org.standardnotes.note-history-unlimited',
SignInAlerts = 'com.standardnotes.sign-in-alerts',
SmartFilters = 'org.standardnotes.smart-filters',
TagNesting = 'org.standardnotes.tag-nesting',
TwoFactorAuth = 'org.standardnotes.two-factor-auth',
UniversalSecondFactor = 'org.standardnotes.universal-second-factor',
SubscriptionSharing = 'org.standardnotes.subscription-sharing',
AutobiographyTheme = 'org.standardnotes.theme-autobiography',
DynamicTheme = 'org.standardnotes.theme-dynamic',
DarkTheme = 'org.standardnotes.theme-focus',
FuturaTheme = 'org.standardnotes.theme-futura',
MidnightTheme = 'org.standardnotes.theme-midnight',
SolarizedDarkTheme = 'org.standardnotes.theme-solarized-dark',
TitaniumTheme = 'org.standardnotes.theme-titanium',
PlainEditor = 'com.standardnotes.plain-text',
SuperEditor = 'com.standardnotes.super-editor',
CodeEditor = 'org.standardnotes.code-editor',
MarkdownProEditor = 'org.standardnotes.advanced-markdown-editor',
PlusEditor = 'org.standardnotes.plus-editor',
SheetsEditor = 'org.standardnotes.standard-sheets',
TaskEditor = 'org.standardnotes.simple-task-editor',
TokenVaultEditor = 'org.standardnotes.token-vault',
Extension = 'org.standardnotes.extension',
DeprecatedMarkdownVisualEditor = 'org.standardnotes.markdown-visual-editor',
DeprecatedBoldEditor = 'org.standardnotes.bold-editor',
DeprecatedMarkdownBasicEditor = 'org.standardnotes.simple-markdown-editor',
DeprecatedMarkdownMathEditor = 'org.standardnotes.fancy-markdown-editor',
DeprecatedMarkdownMinimistEditor = 'org.standardnotes.minimal-markdown-editor',
DeprecatedFoldersComponent = 'org.standardnotes.folders',
DeprecatedFileSafe = 'org.standardnotes.file-safe',
}
/**
* Identifier for standalone filesafe instance offered as legacy installable via extensions-server
*/
export const LegacyFileSafeIdentifier = 'org.standardnotes.legacy.file-safe'
export const ExperimentalFeatures = []

View File

@@ -1,7 +1,7 @@
import { AnyFeatureDescription } from './AnyFeatureDescription' import { AnyFeatureDescription } from './AnyFeatureDescription'
import { ThemeFeatureDescription } from './ThemeFeatureDescription' import { ThemeFeatureDescription } from './ThemeFeatureDescription'
import { EditorFeatureDescription } from './EditorFeatureDescription' import { EditorFeatureDescription } from './EditorFeatureDescription'
import { FeatureIdentifier } from './FeatureIdentifier' import { NativeFeatureIdentifier } from './NativeFeatureIdentifier'
import { serverFeatures } from '../Lists/ServerFeatures' import { serverFeatures } from '../Lists/ServerFeatures'
import { clientFeatures } from '../Lists/ClientFeatures' import { clientFeatures } from '../Lists/ClientFeatures'
import { GetDeprecatedFeatures } from '../Lists/DeprecatedFeatures' import { GetDeprecatedFeatures } from '../Lists/DeprecatedFeatures'
@@ -23,11 +23,11 @@ export function GetFeatures(): AnyFeatureDescription[] {
] ]
} }
export function FindNativeFeature<T extends AnyFeatureDescription>(identifier: FeatureIdentifier): T | undefined { export function FindNativeFeature<T extends AnyFeatureDescription>(identifier: string): T | undefined {
return GetFeatures().find((f) => f.identifier === identifier) as T return GetFeatures().find((f) => f.identifier === identifier) as T
} }
export function FindNativeTheme(identifier: FeatureIdentifier): ThemeFeatureDescription | undefined { export function FindNativeTheme(identifier: string): ThemeFeatureDescription | undefined {
return themes().find((t) => t.identifier === identifier) return themes().find((t) => t.identifier === identifier)
} }
@@ -40,11 +40,11 @@ export function GetIframeEditors(): IframeComponentFeatureDescription[] {
} }
export function GetSuperNoteFeature(): EditorFeatureDescription { export function GetSuperNoteFeature(): EditorFeatureDescription {
return FindNativeFeature(FeatureIdentifier.SuperEditor) as EditorFeatureDescription return FindNativeFeature(NativeFeatureIdentifier.TYPES.SuperEditor) as EditorFeatureDescription
} }
export function GetPlainNoteFeature(): EditorFeatureDescription { export function GetPlainNoteFeature(): EditorFeatureDescription {
return FindNativeFeature(FeatureIdentifier.PlainEditor) as EditorFeatureDescription return FindNativeFeature(NativeFeatureIdentifier.TYPES.PlainEditor) as EditorFeatureDescription
} }
export function GetNativeThemes(): ThemeFeatureDescription[] { export function GetNativeThemes(): ThemeFeatureDescription[] {
@@ -52,5 +52,5 @@ export function GetNativeThemes(): ThemeFeatureDescription[] {
} }
export function GetDarkThemeFeature(): ThemeFeatureDescription { export function GetDarkThemeFeature(): ThemeFeatureDescription {
return themes().find((t) => t.identifier === FeatureIdentifier.DarkTheme) as ThemeFeatureDescription return themes().find((t) => t.identifier === NativeFeatureIdentifier.TYPES.DarkTheme) as ThemeFeatureDescription
} }

View File

@@ -0,0 +1,75 @@
import { Result, ValueObject } from '@standardnotes/domain-core'
export interface NativeFeatureIdentifierProps {
value: string
}
export class NativeFeatureIdentifier extends ValueObject<NativeFeatureIdentifierProps> {
static readonly TYPES = {
DailyEmailBackup: 'org.standardnotes.daily-email-backup',
Files: 'org.standardnotes.files',
FilesLowStorageTier: 'org.standardnotes.files-low-storage-tier',
FilesMaximumStorageTier: 'org.standardnotes.files-max-storage-tier',
ListedCustomDomain: 'org.standardnotes.listed-custom-domain',
NoteHistory30Days: 'org.standardnotes.note-history-30',
NoteHistory365Days: 'org.standardnotes.note-history-365',
NoteHistoryUnlimited: 'org.standardnotes.note-history-unlimited',
SignInAlerts: 'com.standardnotes.sign-in-alerts',
SmartFilters: 'org.standardnotes.smart-filters',
TagNesting: 'org.standardnotes.tag-nesting',
TwoFactorAuth: 'org.standardnotes.two-factor-auth',
UniversalSecondFactor: 'org.standardnotes.universal-second-factor',
SubscriptionSharing: 'org.standardnotes.subscription-sharing',
AutobiographyTheme: 'org.standardnotes.theme-autobiography',
DynamicTheme: 'org.standardnotes.theme-dynamic',
DarkTheme: 'org.standardnotes.theme-focus',
FuturaTheme: 'org.standardnotes.theme-futura',
MidnightTheme: 'org.standardnotes.theme-midnight',
SolarizedDarkTheme: 'org.standardnotes.theme-solarized-dark',
TitaniumTheme: 'org.standardnotes.theme-titanium',
PlainEditor: 'com.standardnotes.plain-text',
SuperEditor: 'com.standardnotes.super-editor',
CodeEditor: 'org.standardnotes.code-editor',
MarkdownProEditor: 'org.standardnotes.advanced-markdown-editor',
PlusEditor: 'org.standardnotes.plus-editor',
SheetsEditor: 'org.standardnotes.standard-sheets',
TaskEditor: 'org.standardnotes.simple-task-editor',
TokenVaultEditor: 'org.standardnotes.token-vault',
Clipper: 'org.standardnotes.clipper',
DeprecatedMarkdownVisualEditor: 'org.standardnotes.markdown-visual-editor',
DeprecatedBoldEditor: 'org.standardnotes.bold-editor',
DeprecatedMarkdownBasicEditor: 'org.standardnotes.simple-markdown-editor',
DeprecatedMarkdownMathEditor: 'org.standardnotes.fancy-markdown-editor',
DeprecatedMarkdownMinimistEditor: 'org.standardnotes.minimal-markdown-editor',
DeprecatedFoldersComponent: 'org.standardnotes.folders',
DeprecatedFileSafe: 'org.standardnotes.file-safe',
LegacyFileSafeIdentifier: 'org.standardnotes.legacy.file-safe',
}
get value(): string {
return this.props.value
}
private constructor(props: NativeFeatureIdentifierProps) {
super(props)
}
static create(type: string): Result<NativeFeatureIdentifier> {
const isValidType = Object.values(this.TYPES).includes(type)
if (!isValidType) {
return Result.fail<NativeFeatureIdentifier>(`Invalid feature identifier: ${type}`)
} else {
return Result.ok<NativeFeatureIdentifier>(new NativeFeatureIdentifier({ value: type }))
}
}
}
/**
* Identifier for standalone filesafe instance offered as legacy installable via extensions-server
*/
export const ExperimentalFeatures = []

View File

@@ -1,11 +1,10 @@
import { PermissionName } from '../Permission/PermissionName' import { PermissionName } from '../Permission/PermissionName'
import { FeatureIdentifier } from './FeatureIdentifier'
import { RoleFields } from './RoleFields' import { RoleFields } from './RoleFields'
export type ServerFeatureDescription = RoleFields & { export type ServerFeatureDescription = RoleFields & {
name: string name: string
description?: string description?: string
identifier: FeatureIdentifier identifier: string
permission_name: PermissionName permission_name: PermissionName
deprecated?: boolean deprecated?: boolean
} }

View File

@@ -1,5 +1,5 @@
import { PermissionName } from '../Permission/PermissionName' import { PermissionName } from '../Permission/PermissionName'
import { FeatureIdentifier } from '../Feature/FeatureIdentifier' import { NativeFeatureIdentifier } from '../Feature/NativeFeatureIdentifier'
import { RoleName } from '@standardnotes/domain-core' import { RoleName } from '@standardnotes/domain-core'
import { ClientFeatureDescription } from '../Feature/ClientFeatureDescription' import { ClientFeatureDescription } from '../Feature/ClientFeatureDescription'
@@ -8,7 +8,7 @@ export function clientFeatures(): ClientFeatureDescription[] {
{ {
name: 'Tag Nesting', name: 'Tag Nesting',
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser], availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
identifier: FeatureIdentifier.TagNesting, identifier: NativeFeatureIdentifier.TYPES.TagNesting,
permission_name: PermissionName.TagNesting, permission_name: PermissionName.TagNesting,
description: 'Organize your tags into folders.', description: 'Organize your tags into folders.',
}, },
@@ -16,22 +16,22 @@ export function clientFeatures(): ClientFeatureDescription[] {
{ {
name: 'Smart Filters', name: 'Smart Filters',
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser], availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
identifier: FeatureIdentifier.SmartFilters, identifier: NativeFeatureIdentifier.TYPES.SmartFilters,
permission_name: PermissionName.SmartFilters, permission_name: PermissionName.SmartFilters,
description: 'Create smart filters for viewing notes matching specific criteria.', description: 'Create smart filters for viewing notes matching specific criteria.',
}, },
{ {
name: 'Encrypted files', name: 'Encrypted files',
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser], availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
identifier: FeatureIdentifier.Files, identifier: NativeFeatureIdentifier.TYPES.Files,
permission_name: PermissionName.Files, permission_name: PermissionName.Files,
description: '', description: '',
}, },
{ {
name: 'Extension', name: 'Clipper',
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser], availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
identifier: FeatureIdentifier.Extension, identifier: NativeFeatureIdentifier.TYPES.Clipper,
permission_name: PermissionName.Extension, permission_name: PermissionName.Clipper,
description: '', description: '',
}, },
] ]

View File

@@ -3,7 +3,7 @@ import { EditorFeatureDescription } from '../Feature/EditorFeatureDescription'
import { IframeComponentFeatureDescription } from '../Feature/IframeComponentFeatureDescription' import { IframeComponentFeatureDescription } from '../Feature/IframeComponentFeatureDescription'
import { ContentType, RoleName } from '@standardnotes/domain-core' import { ContentType, RoleName } from '@standardnotes/domain-core'
import { PermissionName } from '../Permission/PermissionName' import { PermissionName } from '../Permission/PermissionName'
import { FeatureIdentifier } from '../Feature/FeatureIdentifier' import { NativeFeatureIdentifier } from '../Feature/NativeFeatureIdentifier'
import { NoteType } from '../Component/NoteType' import { NoteType } from '../Component/NoteType'
import { FillIframeEditorDefaults } from './Utilities/FillEditorComponentDefaults' import { FillIframeEditorDefaults } from './Utilities/FillEditorComponentDefaults'
import { ComponentAction } from '../Component/ComponentAction' import { ComponentAction } from '../Component/ComponentAction'
@@ -12,7 +12,7 @@ import { ComponentArea } from '../Component/ComponentArea'
export function GetDeprecatedFeatures(): AnyFeatureDescription[] { export function GetDeprecatedFeatures(): AnyFeatureDescription[] {
const bold: EditorFeatureDescription = FillIframeEditorDefaults({ const bold: EditorFeatureDescription = FillIframeEditorDefaults({
name: 'Alternative Rich Text', name: 'Alternative Rich Text',
identifier: FeatureIdentifier.DeprecatedBoldEditor, identifier: NativeFeatureIdentifier.TYPES.DeprecatedBoldEditor,
note_type: NoteType.RichText, note_type: NoteType.RichText,
file_type: 'html', file_type: 'html',
component_permissions: [ component_permissions: [
@@ -39,7 +39,7 @@ export function GetDeprecatedFeatures(): AnyFeatureDescription[] {
const markdownBasic: EditorFeatureDescription = FillIframeEditorDefaults({ const markdownBasic: EditorFeatureDescription = FillIframeEditorDefaults({
name: 'Basic Markdown', name: 'Basic Markdown',
identifier: FeatureIdentifier.DeprecatedMarkdownBasicEditor, identifier: NativeFeatureIdentifier.TYPES.DeprecatedMarkdownBasicEditor,
note_type: NoteType.Markdown, note_type: NoteType.Markdown,
spellcheckControl: true, spellcheckControl: true,
file_type: 'md', file_type: 'md',
@@ -52,7 +52,7 @@ export function GetDeprecatedFeatures(): AnyFeatureDescription[] {
const markdownAlt: EditorFeatureDescription = FillIframeEditorDefaults({ const markdownAlt: EditorFeatureDescription = FillIframeEditorDefaults({
name: 'Markdown Alternative', name: 'Markdown Alternative',
identifier: FeatureIdentifier.DeprecatedMarkdownVisualEditor, identifier: NativeFeatureIdentifier.TYPES.DeprecatedMarkdownVisualEditor,
note_type: NoteType.Markdown, note_type: NoteType.Markdown,
file_type: 'md', file_type: 'md',
deprecated: true, deprecated: true,
@@ -66,7 +66,7 @@ export function GetDeprecatedFeatures(): AnyFeatureDescription[] {
const markdownMinimist: EditorFeatureDescription = FillIframeEditorDefaults({ const markdownMinimist: EditorFeatureDescription = FillIframeEditorDefaults({
name: 'Minimal Markdown', name: 'Minimal Markdown',
identifier: FeatureIdentifier.DeprecatedMarkdownMinimistEditor, identifier: NativeFeatureIdentifier.TYPES.DeprecatedMarkdownMinimistEditor,
note_type: NoteType.Markdown, note_type: NoteType.Markdown,
file_type: 'md', file_type: 'md',
index_path: 'index.html', index_path: 'index.html',
@@ -80,7 +80,7 @@ export function GetDeprecatedFeatures(): AnyFeatureDescription[] {
const markdownMath: EditorFeatureDescription = FillIframeEditorDefaults({ const markdownMath: EditorFeatureDescription = FillIframeEditorDefaults({
name: 'Markdown with Math', name: 'Markdown with Math',
identifier: FeatureIdentifier.DeprecatedMarkdownMathEditor, identifier: NativeFeatureIdentifier.TYPES.DeprecatedMarkdownMathEditor,
spellcheckControl: true, spellcheckControl: true,
permission_name: PermissionName.MarkdownMathEditor, permission_name: PermissionName.MarkdownMathEditor,
note_type: NoteType.Markdown, note_type: NoteType.Markdown,
@@ -94,7 +94,7 @@ export function GetDeprecatedFeatures(): AnyFeatureDescription[] {
const filesafe: IframeComponentFeatureDescription = FillIframeEditorDefaults({ const filesafe: IframeComponentFeatureDescription = FillIframeEditorDefaults({
name: 'FileSafe', name: 'FileSafe',
identifier: FeatureIdentifier.DeprecatedFileSafe, identifier: NativeFeatureIdentifier.TYPES.DeprecatedFileSafe,
component_permissions: [ component_permissions: [
{ {
name: ComponentAction.StreamContextItem, name: ComponentAction.StreamContextItem,

View File

@@ -1,5 +1,5 @@
import { PermissionName } from '../Permission/PermissionName' import { PermissionName } from '../Permission/PermissionName'
import { FeatureIdentifier } from '../Feature/FeatureIdentifier' import { NativeFeatureIdentifier } from '../Feature/NativeFeatureIdentifier'
import { NoteType } from '../Component/NoteType' import { NoteType } from '../Component/NoteType'
import { FillIframeEditorDefaults } from './Utilities/FillEditorComponentDefaults' import { FillIframeEditorDefaults } from './Utilities/FillEditorComponentDefaults'
import { RoleName } from '@standardnotes/domain-core' import { RoleName } from '@standardnotes/domain-core'
@@ -9,7 +9,7 @@ export function IframeEditors(): IframeComponentFeatureDescription[] {
const code = FillIframeEditorDefaults({ const code = FillIframeEditorDefaults({
name: 'Code', name: 'Code',
spellcheckControl: true, spellcheckControl: true,
identifier: FeatureIdentifier.CodeEditor, identifier: NativeFeatureIdentifier.TYPES.CodeEditor,
permission_name: PermissionName.CodeEditor, permission_name: PermissionName.CodeEditor,
note_type: NoteType.Code, note_type: NoteType.Code,
file_type: 'txt', file_type: 'txt',
@@ -26,7 +26,7 @@ export function IframeEditors(): IframeComponentFeatureDescription[] {
name: 'Rich Text', name: 'Rich Text',
note_type: NoteType.RichText, note_type: NoteType.RichText,
file_type: 'html', file_type: 'html',
identifier: FeatureIdentifier.PlusEditor, identifier: NativeFeatureIdentifier.TYPES.PlusEditor,
permission_name: PermissionName.PlusEditor, permission_name: PermissionName.PlusEditor,
spellcheckControl: true, spellcheckControl: true,
description: description:
@@ -37,7 +37,7 @@ export function IframeEditors(): IframeComponentFeatureDescription[] {
const markdown = FillIframeEditorDefaults({ const markdown = FillIframeEditorDefaults({
name: 'Markdown', name: 'Markdown',
identifier: FeatureIdentifier.MarkdownProEditor, identifier: NativeFeatureIdentifier.TYPES.MarkdownProEditor,
note_type: NoteType.Markdown, note_type: NoteType.Markdown,
file_type: 'md', file_type: 'md',
permission_name: PermissionName.MarkdownProEditor, permission_name: PermissionName.MarkdownProEditor,
@@ -50,7 +50,7 @@ export function IframeEditors(): IframeComponentFeatureDescription[] {
const task = FillIframeEditorDefaults({ const task = FillIframeEditorDefaults({
name: 'Checklist', name: 'Checklist',
identifier: FeatureIdentifier.TaskEditor, identifier: NativeFeatureIdentifier.TYPES.TaskEditor,
note_type: NoteType.Task, note_type: NoteType.Task,
spellcheckControl: true, spellcheckControl: true,
file_type: 'md', file_type: 'md',
@@ -67,7 +67,7 @@ export function IframeEditors(): IframeComponentFeatureDescription[] {
note_type: NoteType.Authentication, note_type: NoteType.Authentication,
file_type: 'json', file_type: 'json',
interchangeable: false, interchangeable: false,
identifier: FeatureIdentifier.TokenVaultEditor, identifier: NativeFeatureIdentifier.TYPES.TokenVaultEditor,
permission_name: PermissionName.TokenVaultEditor, permission_name: PermissionName.TokenVaultEditor,
description: description:
'Encrypt and protect your 2FA secrets for all your internet accounts. Authenticator handles your 2FA secrets so that you never lose them again, or have to start over when you get a new device.', 'Encrypt and protect your 2FA secrets for all your internet accounts. Authenticator handles your 2FA secrets so that you never lose them again, or have to start over when you get a new device.',
@@ -77,7 +77,7 @@ export function IframeEditors(): IframeComponentFeatureDescription[] {
const spreadsheets = FillIframeEditorDefaults({ const spreadsheets = FillIframeEditorDefaults({
name: 'Spreadsheet', name: 'Spreadsheet',
identifier: FeatureIdentifier.SheetsEditor, identifier: NativeFeatureIdentifier.TYPES.SheetsEditor,
note_type: NoteType.Spreadsheet, note_type: NoteType.Spreadsheet,
file_type: 'json', file_type: 'json',
interchangeable: false, interchangeable: false,

View File

@@ -1,7 +1,7 @@
import { RoleName } from '@standardnotes/domain-core' import { RoleName } from '@standardnotes/domain-core'
import { NoteType } from '../Component/NoteType' import { NoteType } from '../Component/NoteType'
import { EditorFeatureDescription } from '../Feature/EditorFeatureDescription' import { EditorFeatureDescription } from '../Feature/EditorFeatureDescription'
import { FeatureIdentifier } from '../Feature/FeatureIdentifier' import { NativeFeatureIdentifier } from '../Feature/NativeFeatureIdentifier'
import { PermissionName } from '../Permission/PermissionName' import { PermissionName } from '../Permission/PermissionName'
export function nativeEditors(): EditorFeatureDescription[] { export function nativeEditors(): EditorFeatureDescription[] {
@@ -9,7 +9,7 @@ export function nativeEditors(): EditorFeatureDescription[] {
{ {
name: 'Super', name: 'Super',
note_type: NoteType.Super, note_type: NoteType.Super,
identifier: FeatureIdentifier.SuperEditor, identifier: NativeFeatureIdentifier.TYPES.SuperEditor,
spellcheckControl: true, spellcheckControl: true,
file_type: 'json', file_type: 'json',
interchangeable: false, interchangeable: false,
@@ -24,7 +24,7 @@ export function nativeEditors(): EditorFeatureDescription[] {
spellcheckControl: true, spellcheckControl: true,
file_type: 'txt', file_type: 'txt',
interchangeable: true, interchangeable: true,
identifier: FeatureIdentifier.PlainEditor, identifier: NativeFeatureIdentifier.TYPES.PlainEditor,
availableInRoles: [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser], availableInRoles: [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
permission_name: PermissionName.PlainEditor, permission_name: PermissionName.PlainEditor,
}, },

View File

@@ -1,67 +1,67 @@
import { ServerFeatureDescription } from '../Feature/ServerFeatureDescription' import { ServerFeatureDescription } from '../Feature/ServerFeatureDescription'
import { PermissionName } from '../Permission/PermissionName' import { PermissionName } from '../Permission/PermissionName'
import { FeatureIdentifier } from '../Feature/FeatureIdentifier' import { NativeFeatureIdentifier } from '../Feature/NativeFeatureIdentifier'
import { RoleName } from '@standardnotes/domain-core' import { RoleName } from '@standardnotes/domain-core'
export function serverFeatures(): ServerFeatureDescription[] { export function serverFeatures(): ServerFeatureDescription[] {
return [ return [
{ {
name: 'Two factor authentication', name: 'Two factor authentication',
identifier: FeatureIdentifier.TwoFactorAuth, identifier: NativeFeatureIdentifier.TYPES.TwoFactorAuth,
permission_name: PermissionName.TwoFactorAuth, permission_name: PermissionName.TwoFactorAuth,
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser], availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
}, },
{ {
name: 'U2F authentication', name: 'U2F authentication',
identifier: FeatureIdentifier.UniversalSecondFactor, identifier: NativeFeatureIdentifier.TYPES.UniversalSecondFactor,
permission_name: PermissionName.UniversalSecondFactor, permission_name: PermissionName.UniversalSecondFactor,
availableInRoles: [RoleName.NAMES.ProUser], availableInRoles: [RoleName.NAMES.ProUser],
}, },
{ {
name: 'Unlimited note history', name: 'Unlimited note history',
identifier: FeatureIdentifier.NoteHistoryUnlimited, identifier: NativeFeatureIdentifier.TYPES.NoteHistoryUnlimited,
permission_name: PermissionName.NoteHistoryUnlimited, permission_name: PermissionName.NoteHistoryUnlimited,
availableInRoles: [RoleName.NAMES.ProUser], availableInRoles: [RoleName.NAMES.ProUser],
}, },
{ {
name: '365 days note history', name: '365 days note history',
identifier: FeatureIdentifier.NoteHistory365Days, identifier: NativeFeatureIdentifier.TYPES.NoteHistory365Days,
permission_name: PermissionName.NoteHistory365Days, permission_name: PermissionName.NoteHistory365Days,
availableInRoles: [RoleName.NAMES.PlusUser], availableInRoles: [RoleName.NAMES.PlusUser],
}, },
{ {
name: 'Email backups', name: 'Email backups',
identifier: FeatureIdentifier.DailyEmailBackup, identifier: NativeFeatureIdentifier.TYPES.DailyEmailBackup,
permission_name: PermissionName.DailyEmailBackup, permission_name: PermissionName.DailyEmailBackup,
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser], availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
}, },
{ {
name: 'Sign-in email alerts', name: 'Sign-in email alerts',
identifier: FeatureIdentifier.SignInAlerts, identifier: NativeFeatureIdentifier.TYPES.SignInAlerts,
permission_name: PermissionName.SignInAlerts, permission_name: PermissionName.SignInAlerts,
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser], availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
}, },
{ {
name: 'Files maximum storage tier', name: 'Files maximum storage tier',
identifier: FeatureIdentifier.FilesMaximumStorageTier, identifier: NativeFeatureIdentifier.TYPES.FilesMaximumStorageTier,
permission_name: PermissionName.FilesMaximumStorageTier, permission_name: PermissionName.FilesMaximumStorageTier,
availableInRoles: [RoleName.NAMES.ProUser], availableInRoles: [RoleName.NAMES.ProUser],
}, },
{ {
name: 'Files low storage tier', name: 'Files low storage tier',
identifier: FeatureIdentifier.FilesLowStorageTier, identifier: NativeFeatureIdentifier.TYPES.FilesLowStorageTier,
permission_name: PermissionName.FilesLowStorageTier, permission_name: PermissionName.FilesLowStorageTier,
availableInRoles: [RoleName.NAMES.PlusUser], availableInRoles: [RoleName.NAMES.PlusUser],
}, },
{ {
name: 'Files medium storage tier', name: 'Files medium storage tier',
identifier: FeatureIdentifier.SubscriptionSharing, identifier: NativeFeatureIdentifier.TYPES.SubscriptionSharing,
permission_name: PermissionName.SubscriptionSharing, permission_name: PermissionName.SubscriptionSharing,
availableInRoles: [RoleName.NAMES.ProUser], availableInRoles: [RoleName.NAMES.ProUser],
}, },
{ {
name: 'Listed Custom Domain', name: 'Listed Custom Domain',
identifier: FeatureIdentifier.ListedCustomDomain, identifier: NativeFeatureIdentifier.TYPES.ListedCustomDomain,
permission_name: PermissionName.ListedCustomDomain, permission_name: PermissionName.ListedCustomDomain,
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser], availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
}, },

View File

@@ -1,13 +1,13 @@
import { ThemeFeatureDescription } from '../Feature/ThemeFeatureDescription' import { ThemeFeatureDescription } from '../Feature/ThemeFeatureDescription'
import { PermissionName } from '../Permission/PermissionName' import { PermissionName } from '../Permission/PermissionName'
import { FeatureIdentifier } from '../Feature/FeatureIdentifier' import { NativeFeatureIdentifier } from '../Feature/NativeFeatureIdentifier'
import { FillThemeComponentDefaults } from './Utilities/FillThemeComponentDefaults' import { FillThemeComponentDefaults } from './Utilities/FillThemeComponentDefaults'
import { RoleName } from '@standardnotes/domain-core' import { RoleName } from '@standardnotes/domain-core'
export function themes(): ThemeFeatureDescription[] { export function themes(): ThemeFeatureDescription[] {
const midnight: ThemeFeatureDescription = FillThemeComponentDefaults({ const midnight: ThemeFeatureDescription = FillThemeComponentDefaults({
name: 'Midnight', name: 'Midnight',
identifier: FeatureIdentifier.MidnightTheme, identifier: NativeFeatureIdentifier.TYPES.MidnightTheme,
permission_name: PermissionName.MidnightTheme, permission_name: PermissionName.MidnightTheme,
isDark: true, isDark: true,
dock_icon: { dock_icon: {
@@ -22,7 +22,7 @@ export function themes(): ThemeFeatureDescription[] {
const futura: ThemeFeatureDescription = FillThemeComponentDefaults({ const futura: ThemeFeatureDescription = FillThemeComponentDefaults({
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser], availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
name: 'Futura', name: 'Futura',
identifier: FeatureIdentifier.FuturaTheme, identifier: NativeFeatureIdentifier.TYPES.FuturaTheme,
permission_name: PermissionName.FuturaTheme, permission_name: PermissionName.FuturaTheme,
isDark: true, isDark: true,
dock_icon: { dock_icon: {
@@ -36,7 +36,7 @@ export function themes(): ThemeFeatureDescription[] {
const solarizedDark: ThemeFeatureDescription = FillThemeComponentDefaults({ const solarizedDark: ThemeFeatureDescription = FillThemeComponentDefaults({
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser], availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
name: 'Solarized Dark', name: 'Solarized Dark',
identifier: FeatureIdentifier.SolarizedDarkTheme, identifier: NativeFeatureIdentifier.TYPES.SolarizedDarkTheme,
permission_name: PermissionName.SolarizedDarkTheme, permission_name: PermissionName.SolarizedDarkTheme,
isDark: true, isDark: true,
dock_icon: { dock_icon: {
@@ -50,7 +50,7 @@ export function themes(): ThemeFeatureDescription[] {
const autobiography: ThemeFeatureDescription = FillThemeComponentDefaults({ const autobiography: ThemeFeatureDescription = FillThemeComponentDefaults({
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser], availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
name: 'Autobiography', name: 'Autobiography',
identifier: FeatureIdentifier.AutobiographyTheme, identifier: NativeFeatureIdentifier.TYPES.AutobiographyTheme,
permission_name: PermissionName.AutobiographyTheme, permission_name: PermissionName.AutobiographyTheme,
dock_icon: { dock_icon: {
type: 'circle', type: 'circle',
@@ -63,7 +63,7 @@ export function themes(): ThemeFeatureDescription[] {
const dark: ThemeFeatureDescription = FillThemeComponentDefaults({ const dark: ThemeFeatureDescription = FillThemeComponentDefaults({
availableInRoles: [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser], availableInRoles: [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
name: 'Dark', name: 'Dark',
identifier: FeatureIdentifier.DarkTheme, identifier: NativeFeatureIdentifier.TYPES.DarkTheme,
permission_name: PermissionName.FocusedTheme, permission_name: PermissionName.FocusedTheme,
clientControlled: true, clientControlled: true,
isDark: true, isDark: true,
@@ -78,7 +78,7 @@ export function themes(): ThemeFeatureDescription[] {
const titanium: ThemeFeatureDescription = FillThemeComponentDefaults({ const titanium: ThemeFeatureDescription = FillThemeComponentDefaults({
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser], availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
name: 'Titanium', name: 'Titanium',
identifier: FeatureIdentifier.TitaniumTheme, identifier: NativeFeatureIdentifier.TYPES.TitaniumTheme,
permission_name: PermissionName.TitaniumTheme, permission_name: PermissionName.TitaniumTheme,
dock_icon: { dock_icon: {
type: 'circle', type: 'circle',
@@ -91,7 +91,7 @@ export function themes(): ThemeFeatureDescription[] {
const dynamic: ThemeFeatureDescription = FillThemeComponentDefaults({ const dynamic: ThemeFeatureDescription = FillThemeComponentDefaults({
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser], availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
name: 'Dynamic Panels', name: 'Dynamic Panels',
identifier: FeatureIdentifier.DynamicTheme, identifier: NativeFeatureIdentifier.TYPES.DynamicTheme,
permission_name: PermissionName.ThemeDynamic, permission_name: PermissionName.ThemeDynamic,
layerable: true, layerable: true,
no_mobile: true, no_mobile: true,

View File

@@ -37,5 +37,5 @@ export enum PermissionName {
UniversalSecondFactor = 'server:universal-second-factor', UniversalSecondFactor = 'server:universal-second-factor',
SubscriptionSharing = 'server:subscription-sharing', SubscriptionSharing = 'server:subscription-sharing',
SuperEditor = 'editor:super-editor', SuperEditor = 'editor:super-editor',
Extension = 'app:extension', Clipper = 'app:clipper',
} }

View File

@@ -1,5 +1,5 @@
export * from './Feature/AnyFeatureDescription' export * from './Feature/AnyFeatureDescription'
export * from './Feature/FeatureIdentifier' export * from './Feature/NativeFeatureIdentifier'
export * from './Feature/Features' export * from './Feature/Features'
export * from './Feature/TypeGuards' export * from './Feature/TypeGuards'
@@ -22,4 +22,3 @@ export * from './Component/ComponentFlag'
export * from './Component/ComponentPermission' export * from './Component/ComponentPermission'
export * from './Component/NoteType' export * from './Component/NoteType'
export * from './Component/ThemeDockIcon' export * from './Component/ThemeDockIcon'
export * from './Component/EditorIdentifier'

View File

@@ -6,11 +6,7 @@ import {
NoteType, NoteType,
UIFeatureDescriptionTypes, UIFeatureDescriptionTypes,
} from '@standardnotes/features' } from '@standardnotes/features'
import { import { isUIFeatureAnIframeFeature, isItemBasedFeature, isNativeFeature } from './TypeGuards'
isUIFeatureAnIframeFeature,
isComponentOrFeatureDescriptionAComponent,
isComponentOrFeatureDescriptionAFeatureDescription,
} from './TypeGuards'
import { UIFeature } from './UIFeature' import { UIFeature } from './UIFeature'
import { ComponentInterface } from '../../Syncable/Component' import { ComponentInterface } from '../../Syncable/Component'
import { ContentType } from '@standardnotes/domain-core' import { ContentType } from '@standardnotes/domain-core'
@@ -45,7 +41,7 @@ describe('TypeGuards', () => {
uuid: 'abc-123', uuid: 'abc-123',
} as ComponentInterface } as ComponentInterface
expect(isComponentOrFeatureDescriptionAComponent(x)).toBe(true) expect(isItemBasedFeature(x)).toBe(true)
}) })
it('should return false if feature description is not a component', () => { it('should return false if feature description is not a component', () => {
@@ -53,17 +49,17 @@ describe('TypeGuards', () => {
note_type: NoteType.Super, note_type: NoteType.Super,
} as jest.Mocked<EditorFeatureDescription> } as jest.Mocked<EditorFeatureDescription>
expect(isComponentOrFeatureDescriptionAComponent(x)).toBe(false) expect(isItemBasedFeature(x)).toBe(false)
}) })
}) })
describe('isComponentOrFeatureDescriptionAFeatureDescription', () => { describe('isNativeFeature', () => {
it('should return true if x is a feature description', () => { it('should return true if x is a feature description', () => {
const x: AnyFeatureDescription = { const x: AnyFeatureDescription = {
content_type: 'TestContentType', content_type: 'TestContentType',
} as AnyFeatureDescription } as AnyFeatureDescription
expect(isComponentOrFeatureDescriptionAFeatureDescription(x)).toBe(true) expect(isNativeFeature(x)).toBe(true)
}) })
it('should return false if x is a component', () => { it('should return false if x is a component', () => {
@@ -71,7 +67,7 @@ describe('TypeGuards', () => {
uuid: 'abc-123', uuid: 'abc-123',
} as ComponentInterface } as ComponentInterface
expect(isComponentOrFeatureDescriptionAFeatureDescription(x)).toBe(false) expect(isNativeFeature(x)).toBe(false)
}) })
}) })
}) })

View File

@@ -14,14 +14,10 @@ export function isUIFeatureAnIframeFeature(
return isIframeComponentFeatureDescription(x.featureDescription) return isIframeComponentFeatureDescription(x.featureDescription)
} }
export function isComponentOrFeatureDescriptionAComponent( export function isItemBasedFeature(x: ComponentInterface | UIFeatureDescriptionTypes): x is ComponentInterface {
x: ComponentInterface | UIFeatureDescriptionTypes,
): x is ComponentInterface {
return 'uuid' in x return 'uuid' in x
} }
export function isComponentOrFeatureDescriptionAFeatureDescription( export function isNativeFeature(x: ComponentInterface | AnyFeatureDescription): x is AnyFeatureDescription {
x: ComponentInterface | AnyFeatureDescription,
): x is AnyFeatureDescription {
return !('uuid' in x) return !('uuid' in x)
} }

View File

@@ -2,7 +2,7 @@ import {
ComponentArea, ComponentArea,
ComponentPermission, ComponentPermission,
EditorFeatureDescription, EditorFeatureDescription,
FeatureIdentifier, NativeFeatureIdentifier,
NoteType, NoteType,
ThemeDockIcon, ThemeDockIcon,
UIFeatureDescriptionTypes, UIFeatureDescriptionTypes,
@@ -12,29 +12,27 @@ import {
} from '@standardnotes/features' } from '@standardnotes/features'
import { ComponentInterface } from '../../Syncable/Component/ComponentInterface' import { ComponentInterface } from '../../Syncable/Component/ComponentInterface'
import { isTheme } from '../../Syncable/Theme' import { isTheme } from '../../Syncable/Theme'
import { import { isItemBasedFeature, isNativeFeature } from './TypeGuards'
isComponentOrFeatureDescriptionAComponent,
isComponentOrFeatureDescriptionAFeatureDescription,
} from './TypeGuards'
import { UIFeatureInterface } from './UIFeatureInterface' import { UIFeatureInterface } from './UIFeatureInterface'
import { Uuid } from '@standardnotes/domain-core'
export class UIFeature<F extends UIFeatureDescriptionTypes> implements UIFeatureInterface<F> { export class UIFeature<F extends UIFeatureDescriptionTypes> implements UIFeatureInterface<F> {
constructor(public readonly item: ComponentInterface | F) {} constructor(public readonly item: ComponentInterface | F) {}
get isComponent(): boolean { get isComponent(): boolean {
return isComponentOrFeatureDescriptionAComponent(this.item) return isItemBasedFeature(this.item)
} }
get isFeatureDescription(): boolean { get isFeatureDescription(): boolean {
return isComponentOrFeatureDescriptionAFeatureDescription(this.item) return isNativeFeature(this.item)
} }
get isThemeComponent(): boolean { get isThemeComponent(): boolean {
return isComponentOrFeatureDescriptionAComponent(this.item) && isTheme(this.item) return isItemBasedFeature(this.item) && isTheme(this.item)
} }
get asComponent(): ComponentInterface { get asComponent(): ComponentInterface {
if (isComponentOrFeatureDescriptionAComponent(this.item)) { if (isItemBasedFeature(this.item)) {
return this.item return this.item
} }
@@ -42,29 +40,30 @@ export class UIFeature<F extends UIFeatureDescriptionTypes> implements UIFeature
} }
get asFeatureDescription(): F { get asFeatureDescription(): F {
if (isComponentOrFeatureDescriptionAFeatureDescription(this.item)) { if (isNativeFeature(this.item)) {
return this.item return this.item
} }
throw new Error('Cannot cast item to feature description') throw new Error('Cannot cast item to feature description')
} }
get uniqueIdentifier(): string { get uniqueIdentifier(): NativeFeatureIdentifier | Uuid {
if (isComponentOrFeatureDescriptionAFeatureDescription(this.item)) { if (isNativeFeature(this.item)) {
return this.item.identifier const nativeFeature = NativeFeatureIdentifier.create(this.item.identifier)
return nativeFeature.getValue()
} else { } else {
return this.item.uuid return Uuid.create(this.item.uuid).getValue()
} }
} }
get featureIdentifier(): FeatureIdentifier { get featureIdentifier(): string {
return this.item.identifier return this.item.identifier
} }
get noteType(): NoteType { get noteType(): NoteType {
if (isComponentOrFeatureDescriptionAFeatureDescription(this.item) && isEditorFeatureDescription(this.item)) { if (isNativeFeature(this.item) && isEditorFeatureDescription(this.item)) {
return this.item.note_type ?? NoteType.Unknown return this.item.note_type ?? NoteType.Unknown
} else if (isComponentOrFeatureDescriptionAComponent(this.item)) { } else if (isItemBasedFeature(this.item)) {
return this.item.noteType return this.item.noteType
} }
@@ -72,12 +71,9 @@ export class UIFeature<F extends UIFeatureDescriptionTypes> implements UIFeature
} }
get fileType(): EditorFeatureDescription['file_type'] { get fileType(): EditorFeatureDescription['file_type'] {
if (isComponentOrFeatureDescriptionAFeatureDescription(this.item) && isEditorFeatureDescription(this.item)) { if (isNativeFeature(this.item) && isEditorFeatureDescription(this.item)) {
return this.item.file_type return this.item.file_type
} else if ( } else if (isItemBasedFeature(this.item) && isEditorFeatureDescription(this.item.package_info)) {
isComponentOrFeatureDescriptionAComponent(this.item) &&
isEditorFeatureDescription(this.item.package_info)
) {
return this.item.package_info?.file_type ?? 'txt' return this.item.package_info?.file_type ?? 'txt'
} }
@@ -85,7 +81,7 @@ export class UIFeature<F extends UIFeatureDescriptionTypes> implements UIFeature
} }
get displayName(): string { get displayName(): string {
if (isComponentOrFeatureDescriptionAFeatureDescription(this.item)) { if (isNativeFeature(this.item)) {
return this.item.name ?? '' return this.item.name ?? ''
} else { } else {
return this.item.displayName return this.item.displayName
@@ -93,7 +89,7 @@ export class UIFeature<F extends UIFeatureDescriptionTypes> implements UIFeature
} }
get description(): string { get description(): string {
if (isComponentOrFeatureDescriptionAFeatureDescription(this.item)) { if (isNativeFeature(this.item)) {
return this.item.description ?? '' return this.item.description ?? ''
} else { } else {
return this.item.package_info.description ?? '' return this.item.package_info.description ?? ''
@@ -101,7 +97,7 @@ export class UIFeature<F extends UIFeatureDescriptionTypes> implements UIFeature
} }
get deprecationMessage(): string | undefined { get deprecationMessage(): string | undefined {
if (isComponentOrFeatureDescriptionAFeatureDescription(this.item)) { if (isNativeFeature(this.item)) {
return this.item.deprecation_message return this.item.deprecation_message
} else { } else {
return this.item.deprecationMessage return this.item.deprecationMessage
@@ -109,7 +105,7 @@ export class UIFeature<F extends UIFeatureDescriptionTypes> implements UIFeature
} }
get expirationDate(): Date | undefined { get expirationDate(): Date | undefined {
if (isComponentOrFeatureDescriptionAFeatureDescription(this.item)) { if (isNativeFeature(this.item)) {
return this.item.expires_at ? new Date(this.item.expires_at) : undefined return this.item.expires_at ? new Date(this.item.expires_at) : undefined
} else { } else {
return this.item.valid_until return this.item.valid_until
@@ -117,7 +113,7 @@ export class UIFeature<F extends UIFeatureDescriptionTypes> implements UIFeature
} }
get featureDescription(): F { get featureDescription(): F {
if (isComponentOrFeatureDescriptionAFeatureDescription(this.item)) { if (isNativeFeature(this.item)) {
return this.item return this.item
} else { } else {
return this.item.package_info as F return this.item.package_info as F
@@ -125,12 +121,9 @@ export class UIFeature<F extends UIFeatureDescriptionTypes> implements UIFeature
} }
get acquiredPermissions(): ComponentPermission[] { get acquiredPermissions(): ComponentPermission[] {
if ( if (isNativeFeature(this.item) && isIframeComponentFeatureDescription(this.item)) {
isComponentOrFeatureDescriptionAFeatureDescription(this.item) &&
isIframeComponentFeatureDescription(this.item)
) {
return this.item.component_permissions ?? [] return this.item.component_permissions ?? []
} else if (isComponentOrFeatureDescriptionAComponent(this.item)) { } else if (isItemBasedFeature(this.item)) {
return this.item.permissions return this.item.permissions
} }
@@ -146,7 +139,7 @@ export class UIFeature<F extends UIFeatureDescriptionTypes> implements UIFeature
} }
get layerable(): boolean { get layerable(): boolean {
if (isComponentOrFeatureDescriptionAComponent(this.item) && isTheme(this.item)) { if (isItemBasedFeature(this.item) && isTheme(this.item)) {
return this.item.layerable return this.item.layerable
} else if (isThemeFeatureDescription(this.asFeatureDescription)) { } else if (isThemeFeatureDescription(this.asFeatureDescription)) {
return this.asFeatureDescription.layerable ?? false return this.asFeatureDescription.layerable ?? false
@@ -156,7 +149,7 @@ export class UIFeature<F extends UIFeatureDescriptionTypes> implements UIFeature
} }
get dockIcon(): ThemeDockIcon | undefined { get dockIcon(): ThemeDockIcon | undefined {
if (isComponentOrFeatureDescriptionAComponent(this.item) && isTheme(this.item)) { if (isItemBasedFeature(this.item) && isTheme(this.item)) {
return this.item.package_info.dock_icon return this.item.package_info.dock_icon
} else if (isThemeFeatureDescription(this.asFeatureDescription)) { } else if (isThemeFeatureDescription(this.asFeatureDescription)) {
return this.asFeatureDescription.dock_icon return this.asFeatureDescription.dock_icon

View File

@@ -2,12 +2,13 @@ import {
ComponentArea, ComponentArea,
ComponentPermission, ComponentPermission,
EditorFeatureDescription, EditorFeatureDescription,
FeatureIdentifier, NativeFeatureIdentifier,
NoteType, NoteType,
ThemeDockIcon, ThemeDockIcon,
UIFeatureDescriptionTypes, UIFeatureDescriptionTypes,
} from '@standardnotes/features' } from '@standardnotes/features'
import { ComponentInterface } from '../../Syncable/Component' import { ComponentInterface } from '../../Syncable/Component'
import { Uuid } from '@standardnotes/domain-core'
export interface UIFeatureInterface<F extends UIFeatureDescriptionTypes> { export interface UIFeatureInterface<F extends UIFeatureDescriptionTypes> {
item: ComponentInterface | F item: ComponentInterface | F
@@ -16,8 +17,8 @@ export interface UIFeatureInterface<F extends UIFeatureDescriptionTypes> {
get isThemeComponent(): boolean get isThemeComponent(): boolean
get asComponent(): ComponentInterface get asComponent(): ComponentInterface
get asFeatureDescription(): F get asFeatureDescription(): F
get uniqueIdentifier(): string get uniqueIdentifier(): NativeFeatureIdentifier | Uuid
get featureIdentifier(): FeatureIdentifier get featureIdentifier(): string
get noteType(): NoteType get noteType(): NoteType
get fileType(): EditorFeatureDescription['file_type'] get fileType(): EditorFeatureDescription['file_type']
get displayName(): string get displayName(): string

View File

@@ -1,6 +1,5 @@
import { isValidUrl } from '@standardnotes/utils' import { isValidUrl } from '@standardnotes/utils'
import { import {
FeatureIdentifier,
ThirdPartyFeatureDescription, ThirdPartyFeatureDescription,
ComponentArea, ComponentArea,
ComponentFlag, ComponentFlag,
@@ -175,7 +174,7 @@ export class SNComponent extends DecryptedItem<ComponentContent> implements Comp
return this.valid_until.getTime() > 0 && this.valid_until <= new Date() return this.valid_until.getTime() > 0 && this.valid_until <= new Date()
} }
public get identifier(): FeatureIdentifier { public get identifier(): string {
return this.package_info.identifier return this.package_info.identifier
} }

View File

@@ -1,10 +1,4 @@
import { import { ComponentArea, ComponentPermission, NoteType, ThirdPartyFeatureDescription } from '@standardnotes/features'
ComponentArea,
ComponentPermission,
FeatureIdentifier,
NoteType,
ThirdPartyFeatureDescription,
} from '@standardnotes/features'
import { ComponentPackageInfo } from './PackageInfo' import { ComponentPackageInfo } from './PackageInfo'
import { DecryptedItemInterface } from '../../Abstract/Item' import { DecryptedItemInterface } from '../../Abstract/Item'
import { ComponentContent } from './ComponentContent' import { ComponentContent } from './ComponentContent'
@@ -35,7 +29,7 @@ export interface ComponentInterface extends DecryptedItemInterface<ComponentCont
isExplicitlyDisabledForItem(uuid: string): boolean isExplicitlyDisabledForItem(uuid: string): boolean
legacyIsDefaultEditor(): boolean legacyIsDefaultEditor(): boolean
get identifier(): FeatureIdentifier get identifier(): string
get noteType(): NoteType get noteType(): NoteType
get displayName(): string get displayName(): string
get deprecationMessage(): string | undefined get deprecationMessage(): string | undefined

View File

@@ -1,5 +1,5 @@
import { AppDataField } from './../../Abstract/Item/Types/AppDataField' import { AppDataField } from './../../Abstract/Item/Types/AppDataField'
import { FeatureIdentifier, NoteType } from '@standardnotes/features' import { NoteType } from '@standardnotes/features'
import { DecryptedItem } from '../../Abstract/Item/Implementations/DecryptedItem' import { DecryptedItem } from '../../Abstract/Item/Implementations/DecryptedItem'
import { ItemInterface } from '../../Abstract/Item/Interfaces/ItemInterface' import { ItemInterface } from '../../Abstract/Item/Interfaces/ItemInterface'
import { DecryptedPayloadInterface } from '../../Abstract/Payload/Interfaces/DecryptedPayload' import { DecryptedPayloadInterface } from '../../Abstract/Payload/Interfaces/DecryptedPayload'
@@ -21,7 +21,7 @@ export class SNNote extends DecryptedItem<NoteContent> implements NoteContentSpe
public readonly authorizedForListed: boolean public readonly authorizedForListed: boolean
/** The package_info.identifier of the editor (not its uuid), such as org.standardnotes.advanced-markdown */ /** The package_info.identifier of the editor (not its uuid), such as org.standardnotes.advanced-markdown */
public readonly editorIdentifier?: FeatureIdentifier | string public readonly editorIdentifier?: string
constructor(payload: DecryptedPayloadInterface<NoteContent>) { constructor(payload: DecryptedPayloadInterface<NoteContent>) {
super(payload) super(payload)

View File

@@ -1,4 +1,4 @@
import { FeatureIdentifier, NoteType } from '@standardnotes/features' import { NoteType } from '@standardnotes/features'
import { ItemContent } from '../../Abstract/Content/ItemContent' import { ItemContent } from '../../Abstract/Content/ItemContent'
import { EditorLineWidth } from '../UserPrefs' import { EditorLineWidth } from '../UserPrefs'
@@ -11,7 +11,7 @@ export interface NoteContentSpecialized {
spellcheck?: boolean spellcheck?: boolean
editorWidth?: EditorLineWidth editorWidth?: EditorLineWidth
noteType?: NoteType noteType?: NoteType
editorIdentifier?: FeatureIdentifier | string editorIdentifier?: string
authorizedForListed?: boolean authorizedForListed?: boolean
} }

View File

@@ -1,7 +1,7 @@
import { NoteMutator } from './NoteMutator' import { NoteMutator } from './NoteMutator'
import { createNote } from './../../Utilities/Test/SpecUtils' import { createNote } from './../../Utilities/Test/SpecUtils'
import { MutationType } from '../../Abstract/Item' import { MutationType } from '../../Abstract/Item'
import { FeatureIdentifier, NoteType } from '@standardnotes/features' import { NativeFeatureIdentifier, NoteType } from '@standardnotes/features'
describe('note mutator', () => { describe('note mutator', () => {
it('sets noteType', () => { it('sets noteType', () => {
@@ -16,9 +16,9 @@ describe('note mutator', () => {
it('sets componentIdentifier', () => { it('sets componentIdentifier', () => {
const note = createNote({}) const note = createNote({})
const mutator = new NoteMutator(note, MutationType.NoUpdateUserTimestamps) const mutator = new NoteMutator(note, MutationType.NoUpdateUserTimestamps)
mutator.editorIdentifier = FeatureIdentifier.MarkdownProEditor mutator.editorIdentifier = NativeFeatureIdentifier.TYPES.MarkdownProEditor
const result = mutator.getResult() const result = mutator.getResult()
expect(result.content.editorIdentifier).toEqual(FeatureIdentifier.MarkdownProEditor) expect(result.content.editorIdentifier).toEqual(NativeFeatureIdentifier.TYPES.MarkdownProEditor)
}) })
}) })

View File

@@ -3,7 +3,7 @@ import { DecryptedItemMutator } from '../../Abstract/Item/Mutator/DecryptedItemM
import { SNNote } from './Note' import { SNNote } from './Note'
import { NoteToNoteReference } from '../../Abstract/Reference/NoteToNoteReference' import { NoteToNoteReference } from '../../Abstract/Reference/NoteToNoteReference'
import { ContentReferenceType } from '../../Abstract/Item' import { ContentReferenceType } from '../../Abstract/Item'
import { FeatureIdentifier, NoteType } from '@standardnotes/features' import { NoteType } from '@standardnotes/features'
import { EditorLineWidth } from '../UserPrefs' import { EditorLineWidth } from '../UserPrefs'
import { ContentType } from '@standardnotes/domain-core' import { ContentType } from '@standardnotes/domain-core'
@@ -40,7 +40,7 @@ export class NoteMutator extends DecryptedItemMutator<NoteContent> {
this.mutableContent.noteType = noteType this.mutableContent.noteType = noteType
} }
set editorIdentifier(identifier: FeatureIdentifier | string | undefined) { set editorIdentifier(identifier: string | undefined) {
this.mutableContent.editorIdentifier = identifier this.mutableContent.editorIdentifier = identifier
} }

View File

@@ -1,4 +1,3 @@
import { EditorIdentifier } from '@standardnotes/features'
import { NewNoteTitleFormat } from '../UserPrefs' import { NewNoteTitleFormat } from '../UserPrefs'
import { CollectionSortProperty } from './../../Runtime/Collection/CollectionSort' import { CollectionSortProperty } from './../../Runtime/Collection/CollectionSort'
@@ -15,7 +14,7 @@ export interface TagPreferences {
hideEditorIcon?: boolean hideEditorIcon?: boolean
newNoteTitleFormat?: NewNoteTitleFormat newNoteTitleFormat?: NewNoteTitleFormat
customNoteTitleFormat?: string customNoteTitleFormat?: string
editorIdentifier?: EditorIdentifier editorIdentifier?: string
entryMode?: 'normal' | 'daily' entryMode?: 'normal' | 'daily'
panelWidth?: number panelWidth?: number
useTableView?: boolean useTableView?: boolean

View File

@@ -1,7 +1,3 @@
import { FeatureIdentifier } from '@standardnotes/features' export type AllComponentPreferences = Record<string, ComponentPreferencesEntry>
type UuidString = string
export type AllComponentPreferences = Record<FeatureIdentifier | UuidString, ComponentPreferencesEntry>
export type ComponentPreferencesEntry = Record<string, unknown> export type ComponentPreferencesEntry = Record<string, unknown>

View File

@@ -1,4 +1,4 @@
import { FeatureIdentifier } from '@standardnotes/features' import { NativeFeatureIdentifier } from '@standardnotes/features'
import { CollectionSort } from '../../Runtime/Collection/CollectionSort' import { CollectionSort } from '../../Runtime/Collection/CollectionSort'
import { EditorFontSize } from './EditorFontSize' import { EditorFontSize } from './EditorFontSize'
import { EditorLineHeight } from './EditorLineHeight' import { EditorLineHeight } from './EditorLineHeight'
@@ -29,7 +29,7 @@ export const PrefDefaults = {
[PrefKey.NotesHideEditorIcon]: false, [PrefKey.NotesHideEditorIcon]: false,
[PrefKey.UseSystemColorScheme]: false, [PrefKey.UseSystemColorScheme]: false,
[PrefKey.AutoLightThemeIdentifier]: 'Default', [PrefKey.AutoLightThemeIdentifier]: 'Default',
[PrefKey.AutoDarkThemeIdentifier]: FeatureIdentifier.DarkTheme, [PrefKey.AutoDarkThemeIdentifier]: NativeFeatureIdentifier.TYPES.DarkTheme,
[PrefKey.NoteAddToParentFolders]: true, [PrefKey.NoteAddToParentFolders]: true,
[PrefKey.NewNoteTitleFormat]: NewNoteTitleFormat.CurrentDateAndTime, [PrefKey.NewNoteTitleFormat]: NewNoteTitleFormat.CurrentDateAndTime,
[PrefKey.CustomNoteTitleFormat]: 'YYYY-MM-DD [at] hh:mm A', [PrefKey.CustomNoteTitleFormat]: 'YYYY-MM-DD [at] hh:mm A',
@@ -37,7 +37,7 @@ export const PrefDefaults = {
[PrefKey.PaneGesturesEnabled]: true, [PrefKey.PaneGesturesEnabled]: true,
[PrefKey.MomentsDefaultTagUuid]: undefined, [PrefKey.MomentsDefaultTagUuid]: undefined,
[PrefKey.ClipperDefaultTagUuid]: undefined, [PrefKey.ClipperDefaultTagUuid]: undefined,
[PrefKey.DefaultEditorIdentifier]: FeatureIdentifier.PlainEditor, [PrefKey.DefaultEditorIdentifier]: NativeFeatureIdentifier.TYPES.PlainEditor,
[PrefKey.SuperNoteExportFormat]: 'json', [PrefKey.SuperNoteExportFormat]: 'json',
[PrefKey.SystemViewPreferences]: {}, [PrefKey.SystemViewPreferences]: {},
[PrefKey.AuthenticatorNames]: '', [PrefKey.AuthenticatorNames]: '',

View File

@@ -1,5 +1,4 @@
import { CollectionSortProperty } from '../../Runtime/Collection/CollectionSort' import { CollectionSortProperty } from '../../Runtime/Collection/CollectionSort'
import { EditorIdentifier, FeatureIdentifier } from '@standardnotes/features'
import { SystemViewId } from '../SmartView' import { SystemViewId } from '../SmartView'
import { TagPreferences } from '../Tag' import { TagPreferences } from '../Tag'
import { NewNoteTitleFormat } from './NewNoteTitleFormat' import { NewNoteTitleFormat } from './NewNoteTitleFormat'
@@ -67,8 +66,8 @@ export type PrefValue = {
[PrefKey.NotesHideTags]: boolean [PrefKey.NotesHideTags]: boolean
[PrefKey.NotesHideEditorIcon]: boolean [PrefKey.NotesHideEditorIcon]: boolean
[PrefKey.UseSystemColorScheme]: boolean [PrefKey.UseSystemColorScheme]: boolean
[PrefKey.AutoLightThemeIdentifier]: FeatureIdentifier | 'Default' | 'Dark' [PrefKey.AutoLightThemeIdentifier]: string
[PrefKey.AutoDarkThemeIdentifier]: FeatureIdentifier | 'Default' | 'Dark' [PrefKey.AutoDarkThemeIdentifier]: string
[PrefKey.NoteAddToParentFolders]: boolean [PrefKey.NoteAddToParentFolders]: boolean
[PrefKey.NewNoteTitleFormat]: NewNoteTitleFormat [PrefKey.NewNoteTitleFormat]: NewNoteTitleFormat
[PrefKey.CustomNoteTitleFormat]: string [PrefKey.CustomNoteTitleFormat]: string
@@ -76,7 +75,7 @@ export type PrefValue = {
[PrefKey.EditorLineWidth]: EditorLineWidth [PrefKey.EditorLineWidth]: EditorLineWidth
[PrefKey.EditorFontSize]: EditorFontSize [PrefKey.EditorFontSize]: EditorFontSize
[PrefKey.UpdateSavingStatusIndicator]: boolean [PrefKey.UpdateSavingStatusIndicator]: boolean
[PrefKey.DefaultEditorIdentifier]: EditorIdentifier [PrefKey.DefaultEditorIdentifier]: string
[PrefKey.MomentsDefaultTagUuid]: string | undefined [PrefKey.MomentsDefaultTagUuid]: string | undefined
[PrefKey.ClipperDefaultTagUuid]: string | undefined [PrefKey.ClipperDefaultTagUuid]: string | undefined
[PrefKey.SystemViewPreferences]: Partial<Record<SystemViewId, TagPreferences>> [PrefKey.SystemViewPreferences]: Partial<Record<SystemViewId, TagPreferences>>

View File

@@ -3,13 +3,14 @@ import {
ComponentArea, ComponentArea,
ComponentFeatureDescription, ComponentFeatureDescription,
EditorFeatureDescription, EditorFeatureDescription,
EditorIdentifier,
IframeComponentFeatureDescription, IframeComponentFeatureDescription,
NativeFeatureIdentifier,
ThemeFeatureDescription, ThemeFeatureDescription,
} from '@standardnotes/features' } from '@standardnotes/features'
import { ActionObserver, ComponentInterface, UIFeature, PermissionDialog, SNNote, SNTag } from '@standardnotes/models' import { ActionObserver, ComponentInterface, UIFeature, PermissionDialog, SNNote, SNTag } from '@standardnotes/models'
import { DesktopManagerInterface } from '../Device/DesktopManagerInterface' import { DesktopManagerInterface } from '../Device/DesktopManagerInterface'
import { ComponentViewerInterface } from './ComponentViewerInterface' import { ComponentViewerInterface } from './ComponentViewerInterface'
import { Uuid } from '@standardnotes/domain-core'
export interface ComponentManagerInterface { export interface ComponentManagerInterface {
urlForFeature(uiFeature: UIFeature<ComponentFeatureDescription>): string | undefined urlForFeature(uiFeature: UIFeature<ComponentFeatureDescription>): string | undefined
@@ -31,12 +32,12 @@ export interface ComponentManagerInterface {
setPermissionDialogUIHandler(handler: (dialog: PermissionDialog) => void): void setPermissionDialogUIHandler(handler: (dialog: PermissionDialog) => void): void
editorForNote(note: SNNote): UIFeature<EditorFeatureDescription | IframeComponentFeatureDescription> editorForNote(note: SNNote): UIFeature<EditorFeatureDescription | IframeComponentFeatureDescription>
getDefaultEditorIdentifier(currentTag?: SNTag): EditorIdentifier getDefaultEditorIdentifier(currentTag?: SNTag): string
isThemeActive(theme: UIFeature<ThemeFeatureDescription>): boolean isThemeActive(theme: UIFeature<ThemeFeatureDescription>): boolean
toggleTheme(theme: UIFeature<ThemeFeatureDescription>): Promise<void> toggleTheme(theme: UIFeature<ThemeFeatureDescription>): Promise<void>
getActiveThemes(): UIFeature<ThemeFeatureDescription>[] getActiveThemes(): UIFeature<ThemeFeatureDescription>[]
getActiveThemesIdentifiers(): string[] getActiveThemesIdentifiers(): { features: NativeFeatureIdentifier[]; uuids: Uuid[] }
isComponentActive(component: ComponentInterface): boolean isComponentActive(component: ComponentInterface): boolean
toggleComponent(component: ComponentInterface): Promise<void> toggleComponent(component: ComponentInterface): Promise<void>

View File

@@ -1,7 +1,8 @@
import { ActionObserver, ComponentEventObserver, ComponentMessage, UIFeature } from '@standardnotes/models' import { ActionObserver, ComponentEventObserver, ComponentMessage, UIFeature } from '@standardnotes/models'
import { FeatureStatus } from '../Feature/FeatureStatus' import { FeatureStatus } from '../Feature/FeatureStatus'
import { ComponentViewerError } from './ComponentViewerError' import { ComponentViewerError } from './ComponentViewerError'
import { IframeComponentFeatureDescription } from '@standardnotes/features' import { IframeComponentFeatureDescription, NativeFeatureIdentifier } from '@standardnotes/features'
import { Uuid } from '@standardnotes/domain-core'
export interface ComponentViewerInterface { export interface ComponentViewerInterface {
readonly identifier: string readonly identifier: string
@@ -9,7 +10,7 @@ export interface ComponentViewerInterface {
readonly sessionKey?: string readonly sessionKey?: string
get url(): string get url(): string
get componentUniqueIdentifier(): string get componentUniqueIdentifier(): NativeFeatureIdentifier | Uuid
getComponentOrFeatureItem(): UIFeature<IframeComponentFeatureDescription> getComponentOrFeatureItem(): UIFeature<IframeComponentFeatureDescription>

View File

@@ -1,12 +1,15 @@
import { FeatureIdentifier } from '@standardnotes/features'
import { ComponentInterface, DecryptedItemInterface } from '@standardnotes/models' import { ComponentInterface, DecryptedItemInterface } from '@standardnotes/models'
import { FeatureStatus } from './FeatureStatus' import { FeatureStatus } from './FeatureStatus'
import { SetOfflineFeaturesFunctionResponse } from './SetOfflineFeaturesFunctionResponse' import { SetOfflineFeaturesFunctionResponse } from './SetOfflineFeaturesFunctionResponse'
import { NativeFeatureIdentifier } from '@standardnotes/features'
import { Uuid } from '@standardnotes/domain-core'
export interface FeaturesClientInterface { export interface FeaturesClientInterface {
initializeFromDisk(): void getFeatureStatus(
getFeatureStatus(featureId: FeatureIdentifier, options?: { inContextOfItem?: DecryptedItemInterface }): FeatureStatus featureId: NativeFeatureIdentifier | Uuid,
options?: { inContextOfItem?: DecryptedItemInterface },
): FeatureStatus
hasMinimumRole(role: string): boolean hasMinimumRole(role: string): boolean
hasFirstPartyOfflineSubscription(): boolean hasFirstPartyOfflineSubscription(): boolean
@@ -16,13 +19,13 @@ export interface FeaturesClientInterface {
isThirdPartyFeature(identifier: string): boolean isThirdPartyFeature(identifier: string): boolean
toggleExperimentalFeature(identifier: FeatureIdentifier): void toggleExperimentalFeature(identifier: string): void
getExperimentalFeatures(): FeatureIdentifier[] getExperimentalFeatures(): string[]
getEnabledExperimentalFeatures(): FeatureIdentifier[] getEnabledExperimentalFeatures(): string[]
enableExperimentalFeature(identifier: FeatureIdentifier): void enableExperimentalFeature(identifier: string): void
disableExperimentalFeature(identifier: FeatureIdentifier): void disableExperimentalFeature(identifier: string): void
isExperimentalFeatureEnabled(identifier: FeatureIdentifier): boolean isExperimentalFeatureEnabled(identifier: string): boolean
isExperimentalFeature(identifier: FeatureIdentifier): boolean isExperimentalFeature(identifier: string): boolean
downloadRemoteThirdPartyFeature(urlOrCode: string): Promise<ComponentInterface | undefined> downloadRemoteThirdPartyFeature(urlOrCode: string): Promise<ComponentInterface | undefined>
} }

View File

@@ -1,5 +1,5 @@
export enum SessionEvent { export enum SessionEvent {
Restored = 'SessionRestored', Restored = 'SessionEvent:SessionRestored',
Revoked = 'SessionRevoked', Revoked = 'SessionEvent:SessionRevoked',
UserKeyPairChanged = 'UserKeyPairChanged', UserKeyPairChanged = 'SessionEvent:UserKeyPairChanged',
} }

View File

@@ -18,8 +18,6 @@ export interface SessionsClientInterface {
getWorkspaceDisplayIdentifier(): string getWorkspaceDisplayIdentifier(): string
populateSessionFromDemoShareToken(token: Base64String): Promise<void> populateSessionFromDemoShareToken(token: Base64String): Promise<void>
initializeFromDisk(): Promise<void>
getUser(): User | undefined getUser(): User | undefined
isSignedIn(): boolean isSignedIn(): boolean
get userUuid(): string get userUuid(): string

View File

@@ -1,3 +1,6 @@
import { ApplicationStage } from './../Application/ApplicationStage'
import { SessionEvent } from './../Session/SessionEvent'
import { ApplicationEvent } from './../Event/ApplicationEvent'
import { StorageServiceInterface } from './../Storage/StorageServiceInterface' import { StorageServiceInterface } from './../Storage/StorageServiceInterface'
import { SessionsClientInterface } from './../Session/SessionsClientInterface' import { SessionsClientInterface } from './../Session/SessionsClientInterface'
import { SubscriptionApiServiceInterface } from '@standardnotes/api' import { SubscriptionApiServiceInterface } from '@standardnotes/api'
@@ -21,11 +24,66 @@ describe('SubscriptionManager', () => {
subscriptionApiService.listInvites = jest.fn() subscriptionApiService.listInvites = jest.fn()
sessions = {} as jest.Mocked<SessionsClientInterface> sessions = {} as jest.Mocked<SessionsClientInterface>
sessions.isSignedIn = jest.fn().mockReturnValue(true)
storage = {} as jest.Mocked<StorageServiceInterface> storage = {} as jest.Mocked<StorageServiceInterface>
internalEventBus = {} as jest.Mocked<InternalEventBusInterface> internalEventBus = {} as jest.Mocked<InternalEventBusInterface>
internalEventBus.addEventHandler = jest.fn() internalEventBus.addEventHandler = jest.fn()
internalEventBus.publish = jest.fn()
})
describe('event handling', () => {
it('should fetch subscriptions when the application has launched', async () => {
const manager = createManager()
jest.spyOn(manager, 'fetchOnlineSubscription')
jest.spyOn(manager, 'fetchAvailableSubscriptions')
await manager.handleEvent({ type: ApplicationEvent.Launched, payload: {} })
expect(manager.fetchOnlineSubscription).toHaveBeenCalledTimes(1)
expect(manager.fetchAvailableSubscriptions).toHaveBeenCalledTimes(1)
})
it('should fetch online subscription when user roles have changed', async () => {
const manager = createManager()
jest.spyOn(manager, 'fetchOnlineSubscription')
await manager.handleEvent({ type: ApplicationEvent.UserRolesChanged, payload: {} })
expect(manager.fetchOnlineSubscription).toHaveBeenCalledTimes(1)
})
it('should fetch online subscription when session is restored', async () => {
const manager = createManager()
jest.spyOn(manager, 'fetchOnlineSubscription')
await manager.handleEvent({ type: SessionEvent.Restored, payload: {} })
expect(manager.fetchOnlineSubscription).toHaveBeenCalledTimes(1)
})
it('should fetch online subscription when user has signed in', async () => {
const manager = createManager()
jest.spyOn(manager, 'fetchOnlineSubscription')
await manager.handleEvent({ type: ApplicationEvent.SignedIn, payload: {} })
expect(manager.fetchOnlineSubscription).toHaveBeenCalledTimes(1)
})
it('should handle stage change and notify event', async () => {
const manager = createManager()
jest.spyOn(manager, 'loadSubscriptionFromStorage')
storage.getValue = jest.fn().mockReturnValue({})
await manager.handleEvent({
type: ApplicationEvent.ApplicationStageChanged,
payload: { stage: ApplicationStage.StorageDecrypted_09 },
})
expect(manager.loadSubscriptionFromStorage).toHaveBeenCalled()
})
}) })
it('should invite user by email to a shared subscription', async () => { it('should invite user by email to a shared subscription', async () => {

View File

@@ -1,3 +1,4 @@
import { SessionEvent } from './../Session/SessionEvent'
import { StorageKey } from './../Storage/StorageKeys' import { StorageKey } from './../Storage/StorageKeys'
import { ApplicationStage } from './../Application/ApplicationStage' import { ApplicationStage } from './../Application/ApplicationStage'
import { StorageServiceInterface } from './../Storage/StorageServiceInterface' import { StorageServiceInterface } from './../Storage/StorageServiceInterface'
@@ -51,6 +52,7 @@ export class SubscriptionManager
} }
case ApplicationEvent.UserRolesChanged: case ApplicationEvent.UserRolesChanged:
case SessionEvent.Restored:
case ApplicationEvent.SignedIn: case ApplicationEvent.SignedIn:
void this.fetchOnlineSubscription() void this.fetchOnlineSubscription()
break break
@@ -58,13 +60,17 @@ export class SubscriptionManager
case ApplicationEvent.ApplicationStageChanged: { case ApplicationEvent.ApplicationStageChanged: {
const stage = (event.payload as ApplicationStageChangedEventPayload).stage const stage = (event.payload as ApplicationStageChangedEventPayload).stage
if (stage === ApplicationStage.StorageDecrypted_09) { if (stage === ApplicationStage.StorageDecrypted_09) {
this.onlineSubscription = this.storage.getValue(StorageKey.Subscription) this.loadSubscriptionFromStorage()
void this.notifyEvent(SubscriptionManagerEvent.DidFetchSubscription)
} }
} }
} }
} }
loadSubscriptionFromStorage(): void {
this.onlineSubscription = this.storage.getValue(StorageKey.Subscription)
void this.notifyEvent(SubscriptionManagerEvent.DidFetchSubscription)
}
hasOnlineSubscription(): boolean { hasOnlineSubscription(): boolean {
return this.onlineSubscription != undefined return this.onlineSubscription != undefined
} }
@@ -204,7 +210,7 @@ export class SubscriptionManager
void this.notifyEvent(SubscriptionManagerEvent.DidFetchSubscription) void this.notifyEvent(SubscriptionManagerEvent.DidFetchSubscription)
} }
private async fetchAvailableSubscriptions(): Promise<void> { async fetchAvailableSubscriptions(): Promise<void> {
try { try {
const response = await this.subscriptionApiService.getAvailableSubscriptions() const response = await this.subscriptionApiService.getAvailableSubscriptions()

View File

@@ -442,12 +442,8 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
this.sockets.loadWebSocketUrl() this.sockets.loadWebSocketUrl()
await this.sessions.initializeFromDisk()
this.settings.initializeFromDisk() this.settings.initializeFromDisk()
this.features.initializeFromDisk()
this.launched = true this.launched = true
await this.notifyEvent(ApplicationEvent.Launched) await this.notifyEvent(ApplicationEvent.Launched)
await this.handleStage(ApplicationStage.Launched_10) await this.handleStage(ApplicationStage.Launched_10)
@@ -1134,6 +1130,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
this.events.addEventHandler(this.dependencies.get(TYPES.SyncService), IntegrityEvent.IntegrityCheckCompleted) this.events.addEventHandler(this.dependencies.get(TYPES.SyncService), IntegrityEvent.IntegrityCheckCompleted)
this.events.addEventHandler(this.dependencies.get(TYPES.UserService), AccountEvent.SignedInOrRegistered) this.events.addEventHandler(this.dependencies.get(TYPES.UserService), AccountEvent.SignedInOrRegistered)
this.events.addEventHandler(this.dependencies.get(TYPES.SessionManager), ApiServiceEvent.SessionRefreshed) this.events.addEventHandler(this.dependencies.get(TYPES.SessionManager), ApiServiceEvent.SessionRefreshed)
this.events.addEventHandler(this.dependencies.get(TYPES.SubscriptionManager), SessionEvent.Restored)
this.events.addEventHandler(this.dependencies.get(TYPES.VaultInviteService), SyncEvent.ReceivedSharedVaultInvites) this.events.addEventHandler(this.dependencies.get(TYPES.VaultInviteService), SyncEvent.ReceivedSharedVaultInvites)
this.events.addEventHandler(this.dependencies.get(TYPES.VaultInviteService), SessionEvent.UserKeyPairChanged) this.events.addEventHandler(this.dependencies.get(TYPES.VaultInviteService), SessionEvent.UserKeyPairChanged)
@@ -1165,6 +1162,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
) )
} }
this.events.addEventHandler(this.dependencies.get(TYPES.SessionManager), ApplicationEvent.ApplicationStageChanged)
this.events.addEventHandler( this.events.addEventHandler(
this.dependencies.get(TYPES.SelfContactManager), this.dependencies.get(TYPES.SelfContactManager),
ApplicationEvent.ApplicationStageChanged, ApplicationEvent.ApplicationStageChanged,

View File

@@ -1,10 +1,9 @@
import { ApplicationStage } from '@standardnotes/services' import { ApplicationStage } from '@standardnotes/services'
import { FeatureIdentifier } from '@standardnotes/features'
import { Migration } from '@Lib/Migrations/Migration' import { Migration } from '@Lib/Migrations/Migration'
import { ThemeInterface } from '@standardnotes/models' import { ThemeInterface } from '@standardnotes/models'
import { ContentType } from '@standardnotes/domain-core' import { ContentType } from '@standardnotes/domain-core'
const NoDistractionIdentifier = 'org.standardnotes.theme-no-distraction' as FeatureIdentifier const NoDistractionIdentifier = 'org.standardnotes.theme-no-distraction'
export class Migration2_42_0 extends Migration { export class Migration2_42_0 extends Migration {
static override version(): string { static override version(): string {

View File

@@ -1,5 +1,5 @@
import { FeaturesService } from '@Lib/Services/Features/FeaturesService' import { FeaturesService } from '@Lib/Services/Features/FeaturesService'
import { ContentType } from '@standardnotes/domain-core' import { ContentType, Uuid } from '@standardnotes/domain-core'
import { import {
ActionObserver, ActionObserver,
PayloadEmitSource, PayloadEmitSource,
@@ -10,7 +10,6 @@ import {
UIFeature, UIFeature,
ComponentInterface, ComponentInterface,
PrefKey, PrefKey,
ThemeInterface,
ComponentPreferencesEntry, ComponentPreferencesEntry,
AllComponentPreferences, AllComponentPreferences,
SNNote, SNNote,
@@ -21,15 +20,14 @@ import {
import { import {
ComponentArea, ComponentArea,
FindNativeFeature, FindNativeFeature,
FeatureIdentifier,
EditorFeatureDescription, EditorFeatureDescription,
FindNativeTheme, FindNativeTheme,
IframeComponentFeatureDescription, IframeComponentFeatureDescription,
ComponentFeatureDescription, ComponentFeatureDescription,
ThemeFeatureDescription, ThemeFeatureDescription,
EditorIdentifier,
GetIframeEditors, GetIframeEditors,
GetNativeThemes, GetNativeThemes,
NativeFeatureIdentifier,
} from '@standardnotes/features' } from '@standardnotes/features'
import { Copy, removeFromArray, sleep, isNotUndefined } from '@standardnotes/utils' import { Copy, removeFromArray, sleep, isNotUndefined } from '@standardnotes/utils'
import { ComponentViewer } from '@Lib/Services/ComponentManager/ComponentViewer' import { ComponentViewer } from '@Lib/Services/ComponentManager/ComponentViewer'
@@ -287,7 +285,7 @@ export class SNComponentManager
const url = this.urlForFeature(feature) const url = this.urlForFeature(feature)
if (url) { if (url) {
this.device.registerComponentUrl(feature.uniqueIdentifier, url) this.device.registerComponentUrl(feature.uniqueIdentifier.value, url)
} }
} }
} }
@@ -372,7 +370,7 @@ export class SNComponentManager
return return
} }
const featureStatus = this.features.getFeatureStatus(uiFeature.featureIdentifier) const featureStatus = this.features.getFeatureStatus(uiFeature.uniqueIdentifier)
if (featureStatus !== FeatureStatus.Entitled) { if (featureStatus !== FeatureStatus.Entitled) {
return return
} }
@@ -398,28 +396,50 @@ export class SNComponentManager
} }
public getActiveThemes(): UIFeature<ThemeFeatureDescription>[] { public getActiveThemes(): UIFeature<ThemeFeatureDescription>[] {
const activeThemesIdentifiers = this.getActiveThemesIdentifiers() const { features, uuids } = this.getActiveThemesIdentifiers()
const thirdPartyThemes = this.items.findItems<ThemeInterface>(activeThemesIdentifiers).map((item) => { const thirdPartyThemes = uuids
return new UIFeature<ThemeFeatureDescription>(item) .map((uuid) => {
}) const component = this.items.findItem<ComponentInterface>(uuid.value)
if (component) {
return new UIFeature<ThemeFeatureDescription>(component)
}
return undefined
})
.filter(isNotUndefined)
const nativeThemes = activeThemesIdentifiers const nativeThemes = features
.map((identifier) => { .map((identifier) => {
return FindNativeTheme(identifier as FeatureIdentifier) return FindNativeTheme(identifier.value)
}) })
.filter(isNotUndefined) .filter(isNotUndefined)
.map((theme) => new UIFeature(theme)) .map((theme) => new UIFeature(theme))
const entitledThemes = [...thirdPartyThemes, ...nativeThemes].filter((theme) => { const entitledThemes = [...thirdPartyThemes, ...nativeThemes].filter((theme) => {
return this.features.getFeatureStatus(theme.featureIdentifier) === FeatureStatus.Entitled return this.features.getFeatureStatus(theme.uniqueIdentifier) === FeatureStatus.Entitled
}) })
return entitledThemes return entitledThemes
} }
public getActiveThemesIdentifiers(): string[] { public getActiveThemesIdentifiers(): { features: NativeFeatureIdentifier[]; uuids: Uuid[] } {
return this.preferences.getValue(PrefKey.ActiveThemes, undefined) ?? [] const features: NativeFeatureIdentifier[] = []
const uuids: Uuid[] = []
const strings = this.preferences.getValue(PrefKey.ActiveThemes, undefined) ?? []
for (const string of strings) {
const nativeIdentifier = NativeFeatureIdentifier.create(string)
if (!nativeIdentifier.isFailed()) {
features.push(nativeIdentifier.getValue())
}
const uuid = Uuid.create(string)
if (!uuid.isFailed()) {
uuids.push(uuid.getValue())
}
}
return { features, uuids }
} }
public async toggleComponent(component: ComponentInterface): Promise<void> { public async toggleComponent(component: ComponentInterface): Promise<void> {
@@ -437,7 +457,7 @@ export class SNComponentManager
return usecase.execute(note) return usecase.execute(note)
} }
getDefaultEditorIdentifier(currentTag?: SNTag): EditorIdentifier { getDefaultEditorIdentifier(currentTag?: SNTag): string {
const usecase = new GetDefaultEditorIdentifier(this.preferences, this.items) const usecase = new GetDefaultEditorIdentifier(this.preferences, this.items)
return usecase.execute(currentTag).getValue() return usecase.execute(currentTag).getValue()
} }
@@ -468,7 +488,7 @@ export class SNComponentManager
this.preferences.getValue(PrefKey.ComponentPreferences, undefined) ?? {}, this.preferences.getValue(PrefKey.ComponentPreferences, undefined) ?? {},
) )
const preferencesLookupKey = uiFeature.uniqueIdentifier const preferencesLookupKey = uiFeature.uniqueIdentifier.value
mutablePreferencesValue[preferencesLookupKey] = preferences mutablePreferencesValue[preferencesLookupKey] = preferences
@@ -482,7 +502,7 @@ export class SNComponentManager
return undefined return undefined
} }
const preferencesLookupKey = component.uniqueIdentifier const preferencesLookupKey = component.uniqueIdentifier.value
return preferences[preferencesLookupKey] return preferences[preferencesLookupKey]
} }
@@ -490,31 +510,31 @@ export class SNComponentManager
async addActiveTheme(theme: UIFeature<ThemeFeatureDescription>): Promise<void> { async addActiveTheme(theme: UIFeature<ThemeFeatureDescription>): Promise<void> {
const activeThemes = (this.preferences.getValue(PrefKey.ActiveThemes, undefined) ?? []).slice() const activeThemes = (this.preferences.getValue(PrefKey.ActiveThemes, undefined) ?? []).slice()
activeThemes.push(theme.uniqueIdentifier) activeThemes.push(theme.uniqueIdentifier.value)
await this.preferences.setValue(PrefKey.ActiveThemes, activeThemes) await this.preferences.setValue(PrefKey.ActiveThemes, activeThemes)
} }
async replaceActiveTheme(theme: UIFeature<ThemeFeatureDescription>): Promise<void> { async replaceActiveTheme(theme: UIFeature<ThemeFeatureDescription>): Promise<void> {
await this.preferences.setValue(PrefKey.ActiveThemes, [theme.uniqueIdentifier]) await this.preferences.setValue(PrefKey.ActiveThemes, [theme.uniqueIdentifier.value])
} }
async removeActiveTheme(theme: UIFeature<ThemeFeatureDescription>): Promise<void> { async removeActiveTheme(theme: UIFeature<ThemeFeatureDescription>): Promise<void> {
const activeThemes = this.preferences.getValue(PrefKey.ActiveThemes, undefined) ?? [] const activeThemes = this.preferences.getValue(PrefKey.ActiveThemes, undefined) ?? []
const filteredThemes = activeThemes.filter((activeTheme) => activeTheme !== theme.uniqueIdentifier) const filteredThemes = activeThemes.filter((activeTheme) => activeTheme !== theme.uniqueIdentifier.value)
await this.preferences.setValue(PrefKey.ActiveThemes, filteredThemes) await this.preferences.setValue(PrefKey.ActiveThemes, filteredThemes)
} }
isThemeActive(theme: UIFeature<ThemeFeatureDescription>): boolean { isThemeActive(theme: UIFeature<ThemeFeatureDescription>): boolean {
if (this.features.getFeatureStatus(theme.featureIdentifier) !== FeatureStatus.Entitled) { if (this.features.getFeatureStatus(theme.uniqueIdentifier) !== FeatureStatus.Entitled) {
return false return false
} }
const activeThemes = this.preferences.getValue(PrefKey.ActiveThemes, undefined) ?? [] const activeThemes = this.preferences.getValue(PrefKey.ActiveThemes, undefined) ?? []
return activeThemes.includes(theme.uniqueIdentifier) return activeThemes.includes(theme.uniqueIdentifier.value)
} }
async addActiveComponent(component: ComponentInterface): Promise<void> { async addActiveComponent(component: ComponentInterface): Promise<void> {

View File

@@ -58,6 +58,7 @@ import {
ComponentPermission, ComponentPermission,
ComponentArea, ComponentArea,
IframeComponentFeatureDescription, IframeComponentFeatureDescription,
NativeFeatureIdentifier,
} from '@standardnotes/features' } from '@standardnotes/features'
import { import {
isString, isString,
@@ -72,7 +73,7 @@ import {
isNotUndefined, isNotUndefined,
uniqueArray, uniqueArray,
} from '@standardnotes/utils' } from '@standardnotes/utils'
import { ContentType } from '@standardnotes/domain-core' import { ContentType, Uuid } from '@standardnotes/domain-core'
export class ComponentViewer implements ComponentViewerInterface { export class ComponentViewer implements ComponentViewerInterface {
private streamItems?: string[] private streamItems?: string[]
@@ -214,12 +215,12 @@ export class ComponentViewer implements ComponentViewerInterface {
this.readonly = readonly this.readonly = readonly
} }
get componentUniqueIdentifier(): string { get componentUniqueIdentifier(): NativeFeatureIdentifier | Uuid {
return this.componentOrFeature.uniqueIdentifier return this.componentOrFeature.uniqueIdentifier
} }
public getFeatureStatus(): FeatureStatus { public getFeatureStatus(): FeatureStatus {
return this.services.features.getFeatureStatus(this.componentOrFeature.featureIdentifier, { return this.services.features.getFeatureStatus(this.componentUniqueIdentifier, {
inContextOfItem: this.getContextItem(), inContextOfItem: this.getContextItem(),
}) })
} }
@@ -269,7 +270,9 @@ export class ComponentViewer implements ComponentViewerInterface {
return return
} }
const updatedComponent = items.find((item) => item.uuid === this.componentUniqueIdentifier) as ComponentInterface const updatedComponent = items.find(
(item) => item.uuid === this.componentUniqueIdentifier.value,
) as ComponentInterface
if (!updatedComponent) { if (!updatedComponent) {
return return
} }
@@ -289,7 +292,7 @@ export class ComponentViewer implements ComponentViewerInterface {
this.updateOurComponentRefFromChangedItems(nondeletedItems) this.updateOurComponentRefFromChangedItems(nondeletedItems)
const areWeOriginator = sourceKey && sourceKey === this.componentUniqueIdentifier const areWeOriginator = sourceKey && sourceKey === this.componentUniqueIdentifier.value
if (areWeOriginator) { if (areWeOriginator) {
return return
} }
@@ -326,7 +329,7 @@ export class ComponentViewer implements ComponentViewerInterface {
] ]
this.config.componentManagerFunctions.runWithPermissionsUseCase.execute( this.config.componentManagerFunctions.runWithPermissionsUseCase.execute(
this.componentUniqueIdentifier, this.componentUniqueIdentifier.value,
requiredPermissions, requiredPermissions,
() => { () => {
this.sendItemsInReply(items, this.streamItemsOriginalMessage as ComponentMessage) this.sendItemsInReply(items, this.streamItemsOriginalMessage as ComponentMessage)
@@ -341,7 +344,7 @@ export class ComponentViewer implements ComponentViewerInterface {
}, },
] as ComponentPermission[] ] as ComponentPermission[]
this.config.componentManagerFunctions.runWithPermissionsUseCase.execute( this.config.componentManagerFunctions.runWithPermissionsUseCase.execute(
this.componentUniqueIdentifier, this.componentUniqueIdentifier.value,
requiredContextPermissions, requiredContextPermissions,
() => { () => {
this.log( this.log(
@@ -414,7 +417,7 @@ export class ComponentViewer implements ComponentViewerInterface {
private getClientData(item: DecryptedItemInterface): Record<string, unknown> { private getClientData(item: DecryptedItemInterface): Record<string, unknown> {
const globalComponentData = item.getDomainData(ComponentDataDomain) || {} const globalComponentData = item.getDomainData(ComponentDataDomain) || {}
const thisComponentData = globalComponentData[this.componentUniqueIdentifier] || {} const thisComponentData = globalComponentData[this.componentUniqueIdentifier.value] || {}
return thisComponentData as Record<string, unknown> return thisComponentData as Record<string, unknown>
} }
@@ -530,7 +533,7 @@ export class ComponentViewer implements ComponentViewerInterface {
sessionKey: this.sessionKey, sessionKey: this.sessionKey,
componentData: componentData, componentData: componentData,
data: { data: {
uuid: this.componentUniqueIdentifier, uuid: this.componentUniqueIdentifier.value,
environment: environmentToString(this.config.environment), environment: environmentToString(this.config.environment),
platform: platformToString(this.config.platform), platform: platformToString(this.config.platform),
activeThemeUrls: this.config.componentManagerFunctions.urlsForActiveThemes(), activeThemeUrls: this.config.componentManagerFunctions.urlsForActiveThemes(),
@@ -602,7 +605,7 @@ export class ComponentViewer implements ComponentViewerInterface {
}, },
] ]
this.config.componentManagerFunctions.runWithPermissionsUseCase.execute( this.config.componentManagerFunctions.runWithPermissionsUseCase.execute(
this.componentUniqueIdentifier, this.componentUniqueIdentifier.value,
requiredPermissions, requiredPermissions,
() => { () => {
if (!this.streamItems) { if (!this.streamItems) {
@@ -627,7 +630,7 @@ export class ComponentViewer implements ComponentViewerInterface {
] ]
this.config.componentManagerFunctions.runWithPermissionsUseCase.execute( this.config.componentManagerFunctions.runWithPermissionsUseCase.execute(
this.componentUniqueIdentifier, this.componentUniqueIdentifier.value,
requiredPermissions, requiredPermissions,
() => { () => {
if (!this.streamContextItemOriginalMessage) { if (!this.streamContextItemOriginalMessage) {
@@ -684,7 +687,7 @@ export class ComponentViewer implements ComponentViewerInterface {
} }
this.config.componentManagerFunctions.runWithPermissionsUseCase.execute( this.config.componentManagerFunctions.runWithPermissionsUseCase.execute(
this.componentUniqueIdentifier, this.componentUniqueIdentifier.value,
requiredPermissions, requiredPermissions,
async () => { async () => {
@@ -766,13 +769,13 @@ export class ComponentViewer implements ComponentViewerInterface {
const allComponentData = Copy<Record<string, unknown>>( const allComponentData = Copy<Record<string, unknown>>(
mutator.getItem().getDomainData(ComponentDataDomain) || {}, mutator.getItem().getDomainData(ComponentDataDomain) || {},
) )
allComponentData[this.componentUniqueIdentifier] = responseItem.clientData allComponentData[this.componentUniqueIdentifier.value] = responseItem.clientData
mutator.setDomainData(allComponentData, ComponentDataDomain) mutator.setDomainData(allComponentData, ComponentDataDomain)
} }
}, },
MutationType.UpdateUserTimestamps, MutationType.UpdateUserTimestamps,
PayloadEmitSource.ComponentRetrieved, PayloadEmitSource.ComponentRetrieved,
this.componentUniqueIdentifier, this.componentUniqueIdentifier.value,
) )
this.services.sync this.services.sync
@@ -807,7 +810,7 @@ export class ComponentViewer implements ComponentViewerInterface {
] ]
this.config.componentManagerFunctions.runWithPermissionsUseCase.execute( this.config.componentManagerFunctions.runWithPermissionsUseCase.execute(
this.componentUniqueIdentifier, this.componentUniqueIdentifier.value,
requiredPermissions, requiredPermissions,
async () => { async () => {
responseItems = this.responseItemsByRemovingPrivateProperties(responseItems) responseItems = this.responseItemsByRemovingPrivateProperties(responseItems)
@@ -834,13 +837,13 @@ export class ComponentViewer implements ComponentViewerInterface {
const allComponentClientData = Copy<Record<string, unknown>>( const allComponentClientData = Copy<Record<string, unknown>>(
item.getDomainData(ComponentDataDomain) || {}, item.getDomainData(ComponentDataDomain) || {},
) )
allComponentClientData[this.componentUniqueIdentifier] = responseItem.clientData allComponentClientData[this.componentUniqueIdentifier.value] = responseItem.clientData
mutator.setDomainData(allComponentClientData, ComponentDataDomain) mutator.setDomainData(allComponentClientData, ComponentDataDomain)
} }
}, },
MutationType.UpdateUserTimestamps, MutationType.UpdateUserTimestamps,
PayloadEmitSource.ComponentCreated, PayloadEmitSource.ComponentCreated,
this.componentUniqueIdentifier, this.componentUniqueIdentifier.value,
) )
processedItems.push(item) processedItems.push(item)
} }
@@ -874,7 +877,7 @@ export class ComponentViewer implements ComponentViewerInterface {
] ]
this.config.componentManagerFunctions.runWithPermissionsUseCase.execute( this.config.componentManagerFunctions.runWithPermissionsUseCase.execute(
this.componentUniqueIdentifier, this.componentUniqueIdentifier.value,
requiredPermissions, requiredPermissions,
async () => { async () => {
const itemsData = items const itemsData = items
@@ -911,7 +914,7 @@ export class ComponentViewer implements ComponentViewerInterface {
private handleSetComponentPreferencesMessage(message: ComponentMessage): void { private handleSetComponentPreferencesMessage(message: ComponentMessage): void {
const noPermissionsRequired: ComponentPermission[] = [] const noPermissionsRequired: ComponentPermission[] = []
this.config.componentManagerFunctions.runWithPermissionsUseCase.execute( this.config.componentManagerFunctions.runWithPermissionsUseCase.execute(
this.componentUniqueIdentifier, this.componentUniqueIdentifier.value,
noPermissionsRequired, noPermissionsRequired,
async () => { async () => {
const newPreferences = <ComponentPreferencesEntry | undefined>message.data.componentData const newPreferences = <ComponentPreferencesEntry | undefined>message.data.componentData

View File

@@ -1,4 +1,4 @@
import { ComponentArea, ComponentAction, FeatureIdentifier, LegacyFileSafeIdentifier } from '@standardnotes/features' import { ComponentArea, ComponentAction, NativeFeatureIdentifier } from '@standardnotes/features'
import { ComponentMessage, MessageData, OutgoingItemMessagePayload } from '@standardnotes/models' import { ComponentMessage, MessageData, OutgoingItemMessagePayload } from '@standardnotes/models'
import { UuidString } from '@Lib/Types/UuidString' import { UuidString } from '@Lib/Types/UuidString'
import { ContentType } from '@standardnotes/domain-core' import { ContentType } from '@standardnotes/domain-core'
@@ -17,9 +17,9 @@ export type Writeable<T> = { -readonly [P in keyof T]: T[P] }
* Extensions allowed to batch stream AllowedBatchContentTypes * Extensions allowed to batch stream AllowedBatchContentTypes
*/ */
export const AllowedBatchStreaming = Object.freeze([ export const AllowedBatchStreaming = Object.freeze([
LegacyFileSafeIdentifier, NativeFeatureIdentifier.TYPES.LegacyFileSafeIdentifier,
FeatureIdentifier.DeprecatedFileSafe, NativeFeatureIdentifier.TYPES.DeprecatedFileSafe,
FeatureIdentifier.DeprecatedBoldEditor, NativeFeatureIdentifier.TYPES.DeprecatedBoldEditor,
]) ])
/** /**

View File

@@ -1,5 +1,5 @@
import { import {
FeatureIdentifier, NativeFeatureIdentifier,
FindNativeFeature, FindNativeFeature,
IframeComponentFeatureDescription, IframeComponentFeatureDescription,
UIFeatureDescriptionTypes, UIFeatureDescriptionTypes,
@@ -7,7 +7,7 @@ import {
import { DoesEditorChangeRequireAlertUseCase } from './DoesEditorChangeRequireAlert' import { DoesEditorChangeRequireAlertUseCase } from './DoesEditorChangeRequireAlert'
import { UIFeature } from '@standardnotes/models' import { UIFeature } from '@standardnotes/models'
const nativeFeatureAsUIFeature = <F extends UIFeatureDescriptionTypes>(identifier: FeatureIdentifier) => { const nativeFeatureAsUIFeature = <F extends UIFeatureDescriptionTypes>(identifier: string) => {
return new UIFeature(FindNativeFeature<F>(identifier)!) return new UIFeature(FindNativeFeature<F>(identifier)!)
} }
@@ -19,57 +19,77 @@ describe('editor change alert', () => {
}) })
it('should not require alert switching from plain editor', () => { it('should not require alert switching from plain editor', () => {
const component = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(FeatureIdentifier.MarkdownProEditor)! const component = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
NativeFeatureIdentifier.TYPES.MarkdownProEditor,
)!
const requiresAlert = usecase.execute(undefined, component) const requiresAlert = usecase.execute(undefined, component)
expect(requiresAlert).toBe(false) expect(requiresAlert).toBe(false)
}) })
it('should not require alert switching to plain editor', () => { it('should not require alert switching to plain editor', () => {
const component = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(FeatureIdentifier.MarkdownProEditor)! const component = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
NativeFeatureIdentifier.TYPES.MarkdownProEditor,
)!
const requiresAlert = usecase.execute(component, undefined) const requiresAlert = usecase.execute(component, undefined)
expect(requiresAlert).toBe(false) expect(requiresAlert).toBe(false)
}) })
it('should not require alert switching from a markdown editor', () => { it('should not require alert switching from a markdown editor', () => {
const htmlEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(FeatureIdentifier.PlusEditor)! const htmlEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
NativeFeatureIdentifier.TYPES.PlusEditor,
)!
const markdownEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>( const markdownEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
FeatureIdentifier.MarkdownProEditor, NativeFeatureIdentifier.TYPES.MarkdownProEditor,
) )
const requiresAlert = usecase.execute(markdownEditor, htmlEditor) const requiresAlert = usecase.execute(markdownEditor, htmlEditor)
expect(requiresAlert).toBe(false) expect(requiresAlert).toBe(false)
}) })
it('should not require alert switching to a markdown editor', () => { it('should not require alert switching to a markdown editor', () => {
const htmlEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(FeatureIdentifier.PlusEditor)! const htmlEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
NativeFeatureIdentifier.TYPES.PlusEditor,
)!
const markdownEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>( const markdownEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
FeatureIdentifier.MarkdownProEditor, NativeFeatureIdentifier.TYPES.MarkdownProEditor,
) )
const requiresAlert = usecase.execute(htmlEditor, markdownEditor) const requiresAlert = usecase.execute(htmlEditor, markdownEditor)
expect(requiresAlert).toBe(false) expect(requiresAlert).toBe(false)
}) })
it('should not require alert switching from & to a html editor', () => { it('should not require alert switching from & to a html editor', () => {
const htmlEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(FeatureIdentifier.PlusEditor)! const htmlEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
NativeFeatureIdentifier.TYPES.PlusEditor,
)!
const requiresAlert = usecase.execute(htmlEditor, htmlEditor) const requiresAlert = usecase.execute(htmlEditor, htmlEditor)
expect(requiresAlert).toBe(false) expect(requiresAlert).toBe(false)
}) })
it('should require alert switching from a html editor to custom editor', () => { it('should require alert switching from a html editor to custom editor', () => {
const htmlEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(FeatureIdentifier.PlusEditor)! const htmlEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
const customEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(FeatureIdentifier.TokenVaultEditor) NativeFeatureIdentifier.TYPES.PlusEditor,
)!
const customEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
NativeFeatureIdentifier.TYPES.TokenVaultEditor,
)
const requiresAlert = usecase.execute(htmlEditor, customEditor) const requiresAlert = usecase.execute(htmlEditor, customEditor)
expect(requiresAlert).toBe(true) expect(requiresAlert).toBe(true)
}) })
it('should require alert switching from a custom editor to html editor', () => { it('should require alert switching from a custom editor to html editor', () => {
const htmlEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(FeatureIdentifier.PlusEditor)! const htmlEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
const customEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(FeatureIdentifier.TokenVaultEditor) NativeFeatureIdentifier.TYPES.PlusEditor,
)!
const customEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
NativeFeatureIdentifier.TYPES.TokenVaultEditor,
)
const requiresAlert = usecase.execute(customEditor, htmlEditor) const requiresAlert = usecase.execute(customEditor, htmlEditor)
expect(requiresAlert).toBe(true) expect(requiresAlert).toBe(true)
}) })
it('should require alert switching from a custom editor to custom editor', () => { it('should require alert switching from a custom editor to custom editor', () => {
const customEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(FeatureIdentifier.TokenVaultEditor) const customEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
NativeFeatureIdentifier.TYPES.TokenVaultEditor,
)
const requiresAlert = usecase.execute(customEditor, customEditor) const requiresAlert = usecase.execute(customEditor, customEditor)
expect(requiresAlert).toBe(true) expect(requiresAlert).toBe(true)
}) })

View File

@@ -1,5 +1,5 @@
import { createNote } from '@Lib/Spec/SpecUtils' import { createNote } from '@Lib/Spec/SpecUtils'
import { FeatureIdentifier, NoteType } from '@standardnotes/features' import { NativeFeatureIdentifier, NoteType } from '@standardnotes/features'
import { EditorForNoteUseCase } from './EditorForNote' import { EditorForNoteUseCase } from './EditorForNote'
import { ItemManagerInterface } from '@standardnotes/services' import { ItemManagerInterface } from '@standardnotes/services'
@@ -17,7 +17,7 @@ describe('EditorForNote', () => {
noteType: NoteType.Plain, noteType: NoteType.Plain,
}) })
expect(usecase.execute(note).featureIdentifier).toBe(FeatureIdentifier.PlainEditor) expect(usecase.execute(note).featureIdentifier).toBe(NativeFeatureIdentifier.TYPES.PlainEditor)
}) })
it('getEditorForNote should call legacy function if no note editorIdentifier or noteType', () => { it('getEditorForNote should call legacy function if no note editorIdentifier or noteType', () => {

View File

@@ -1,7 +1,6 @@
import { import {
ComponentArea, ComponentArea,
EditorFeatureDescription, EditorFeatureDescription,
FeatureIdentifier,
FindNativeFeature, FindNativeFeature,
GetIframeAndNativeEditors, GetIframeAndNativeEditors,
GetPlainNoteFeature, GetPlainNoteFeature,
@@ -47,11 +46,9 @@ export class EditorForNoteUseCase {
} }
private componentOrNativeFeatureForIdentifier( private componentOrNativeFeatureForIdentifier(
identifier: FeatureIdentifier | string, identifier: string,
): UIFeature<EditorFeatureDescription | IframeComponentFeatureDescription> | undefined { ): UIFeature<EditorFeatureDescription | IframeComponentFeatureDescription> | undefined {
const nativeFeature = FindNativeFeature<EditorFeatureDescription | IframeComponentFeatureDescription>( const nativeFeature = FindNativeFeature<EditorFeatureDescription | IframeComponentFeatureDescription>(identifier)
identifier as FeatureIdentifier,
)
if (nativeFeature) { if (nativeFeature) {
return new UIFeature(nativeFeature) return new UIFeature(nativeFeature)
} }

View File

@@ -1,6 +1,6 @@
import { ItemManagerInterface, PreferenceServiceInterface } from '@standardnotes/services' import { ItemManagerInterface, PreferenceServiceInterface } from '@standardnotes/services'
import { GetDefaultEditorIdentifier } from './GetDefaultEditorIdentifier' import { GetDefaultEditorIdentifier } from './GetDefaultEditorIdentifier'
import { ComponentArea, FeatureIdentifier } from '@standardnotes/features' import { ComponentArea, NativeFeatureIdentifier } from '@standardnotes/features'
import { SNComponent, SNTag } from '@standardnotes/models' import { SNComponent, SNTag } from '@standardnotes/models'
describe('getDefaultEditorIdentifier', () => { describe('getDefaultEditorIdentifier', () => {
@@ -21,33 +21,33 @@ describe('getDefaultEditorIdentifier', () => {
it('should return plain editor if no default tag editor or component editor', () => { it('should return plain editor if no default tag editor or component editor', () => {
const editorIdentifier = usecase.execute().getValue() const editorIdentifier = usecase.execute().getValue()
expect(editorIdentifier).toEqual(FeatureIdentifier.PlainEditor) expect(editorIdentifier).toEqual(NativeFeatureIdentifier.TYPES.PlainEditor)
}) })
it('should return pref key based value if available', () => { it('should return pref key based value if available', () => {
preferences.getValue = jest.fn().mockReturnValue(FeatureIdentifier.SuperEditor) preferences.getValue = jest.fn().mockReturnValue(NativeFeatureIdentifier.TYPES.SuperEditor)
const editorIdentifier = usecase.execute().getValue() const editorIdentifier = usecase.execute().getValue()
expect(editorIdentifier).toEqual(FeatureIdentifier.SuperEditor) expect(editorIdentifier).toEqual(NativeFeatureIdentifier.TYPES.SuperEditor)
}) })
it('should return default tag identifier if tag supplied', () => { it('should return default tag identifier if tag supplied', () => {
const tag = { const tag = {
preferences: { preferences: {
editorIdentifier: FeatureIdentifier.SuperEditor, editorIdentifier: NativeFeatureIdentifier.TYPES.SuperEditor,
}, },
} as jest.Mocked<SNTag> } as jest.Mocked<SNTag>
const editorIdentifier = usecase.execute(tag).getValue() const editorIdentifier = usecase.execute(tag).getValue()
expect(editorIdentifier).toEqual(FeatureIdentifier.SuperEditor) expect(editorIdentifier).toEqual(NativeFeatureIdentifier.TYPES.SuperEditor)
}) })
it('should return legacy editor identifier', () => { it('should return legacy editor identifier', () => {
const editor = { const editor = {
legacyIsDefaultEditor: jest.fn().mockReturnValue(true), legacyIsDefaultEditor: jest.fn().mockReturnValue(true),
identifier: FeatureIdentifier.MarkdownProEditor, identifier: NativeFeatureIdentifier.TYPES.MarkdownProEditor,
area: ComponentArea.Editor, area: ComponentArea.Editor,
} as unknown as jest.Mocked<SNComponent> } as unknown as jest.Mocked<SNComponent>
@@ -55,6 +55,6 @@ describe('getDefaultEditorIdentifier', () => {
const editorIdentifier = usecase.execute().getValue() const editorIdentifier = usecase.execute().getValue()
expect(editorIdentifier).toEqual(FeatureIdentifier.MarkdownProEditor) expect(editorIdentifier).toEqual(NativeFeatureIdentifier.TYPES.MarkdownProEditor)
}) })
}) })

View File

@@ -1,12 +1,12 @@
import { Result, SyncUseCaseInterface } from '@standardnotes/domain-core' import { Result, SyncUseCaseInterface } from '@standardnotes/domain-core'
import { ComponentArea, EditorIdentifier, FeatureIdentifier } from '@standardnotes/features' import { ComponentArea, NativeFeatureIdentifier } from '@standardnotes/features'
import { ComponentInterface, PrefKey, SNTag } from '@standardnotes/models' import { ComponentInterface, PrefKey, SNTag } from '@standardnotes/models'
import { ItemManagerInterface, PreferenceServiceInterface } from '@standardnotes/services' import { ItemManagerInterface, PreferenceServiceInterface } from '@standardnotes/services'
export class GetDefaultEditorIdentifier implements SyncUseCaseInterface<EditorIdentifier> { export class GetDefaultEditorIdentifier implements SyncUseCaseInterface<string> {
constructor(private preferences: PreferenceServiceInterface, private items: ItemManagerInterface) {} constructor(private preferences: PreferenceServiceInterface, private items: ItemManagerInterface) {}
execute(currentTag?: SNTag): Result<EditorIdentifier> { execute(currentTag?: SNTag): Result<string> {
if (currentTag) { if (currentTag) {
const editorIdentifier = currentTag?.preferences?.editorIdentifier const editorIdentifier = currentTag?.preferences?.editorIdentifier
if (editorIdentifier) { if (editorIdentifier) {
@@ -25,7 +25,7 @@ export class GetDefaultEditorIdentifier implements SyncUseCaseInterface<EditorId
return Result.ok(matchingEditor.identifier) return Result.ok(matchingEditor.identifier)
} }
return Result.ok(FeatureIdentifier.PlainEditor) return Result.ok(NativeFeatureIdentifier.TYPES.PlainEditor)
} }
thirdPartyComponentsForArea(area: ComponentArea): ComponentInterface[] { thirdPartyComponentsForArea(area: ComponentArea): ComponentInterface[] {

View File

@@ -1,6 +1,6 @@
import { ContentType } from '@standardnotes/domain-core' import { ContentType } from '@standardnotes/domain-core'
import { import {
FeatureIdentifier, NativeFeatureIdentifier,
FindNativeFeature, FindNativeFeature,
IframeComponentFeatureDescription, IframeComponentFeatureDescription,
UIFeatureDescriptionTypes, UIFeatureDescriptionTypes,
@@ -21,7 +21,7 @@ import { GetFeatureUrl } from './GetFeatureUrl'
const desktopExtHost = 'http://localhost:123' const desktopExtHost = 'http://localhost:123'
const nativeFeatureAsUIFeature = <F extends UIFeatureDescriptionTypes>(identifier: FeatureIdentifier) => { const nativeFeatureAsUIFeature = <F extends UIFeatureDescriptionTypes>(identifier: string) => {
return new UIFeature(FindNativeFeature<F>(identifier)!) return new UIFeature(FindNativeFeature<F>(identifier)!)
} }
@@ -35,7 +35,7 @@ const thirdPartyFeature = () => {
local_url: 'sn://Extensions/non-native-identifier/dist/index.html', local_url: 'sn://Extensions/non-native-identifier/dist/index.html',
hosted_url: 'https://example.com/component', hosted_url: 'https://example.com/component',
package_info: { package_info: {
identifier: 'non-native-identifier' as FeatureIdentifier, identifier: 'non-native-identifier',
expires_at: new Date().getTime(), expires_at: new Date().getTime(),
availableInRoles: [], availableInRoles: [],
} as unknown as jest.Mocked<ComponentPackageInfo>, } as unknown as jest.Mocked<ComponentPackageInfo>,
@@ -75,7 +75,9 @@ describe('GetFeatureUrl', () => {
}) })
it('returns native path for native component', () => { it('returns native path for native component', () => {
const feature = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(FeatureIdentifier.MarkdownProEditor)! const feature = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
NativeFeatureIdentifier.TYPES.MarkdownProEditor,
)!
const url = usecase.execute(feature) const url = usecase.execute(feature)
expect(url).toEqual( expect(url).toEqual(
`${desktopExtHost}/components/${feature.featureIdentifier}/${feature.asFeatureDescription.index_path}`, `${desktopExtHost}/components/${feature.featureIdentifier}/${feature.asFeatureDescription.index_path}`,
@@ -84,7 +86,7 @@ describe('GetFeatureUrl', () => {
it('returns native path for deprecated native component', () => { it('returns native path for deprecated native component', () => {
const feature = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>( const feature = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
FeatureIdentifier.DeprecatedBoldEditor, NativeFeatureIdentifier.TYPES.DeprecatedBoldEditor,
)! )!
const url = usecase.execute(feature) const url = usecase.execute(feature)
expect(url).toEqual( expect(url).toEqual(
@@ -122,7 +124,9 @@ describe('GetFeatureUrl', () => {
}) })
it('returns native path for native feature', () => { it('returns native path for native feature', () => {
const feature = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(FeatureIdentifier.MarkdownProEditor) const feature = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
NativeFeatureIdentifier.TYPES.MarkdownProEditor,
)
const url = usecase.execute(feature) const url = usecase.execute(feature)
expect(url).toEqual( expect(url).toEqual(
`http://localhost/components/assets/${feature.featureIdentifier}/${feature.asFeatureDescription.index_path}`, `http://localhost/components/assets/${feature.featureIdentifier}/${feature.asFeatureDescription.index_path}`,

View File

@@ -2,7 +2,7 @@ import { ContentType } from '@standardnotes/domain-core'
import { import {
ComponentAction, ComponentAction,
ComponentPermission, ComponentPermission,
FeatureIdentifier, NativeFeatureIdentifier,
FindNativeFeature, FindNativeFeature,
UIFeatureDescriptionTypes, UIFeatureDescriptionTypes,
} from '@standardnotes/features' } from '@standardnotes/features'
@@ -15,7 +15,7 @@ import {
SyncServiceInterface, SyncServiceInterface,
} from '@standardnotes/services' } from '@standardnotes/services'
const nativeFeatureAsUIFeature = <F extends UIFeatureDescriptionTypes>(identifier: FeatureIdentifier) => { const nativeFeatureAsUIFeature = <F extends UIFeatureDescriptionTypes>(identifier: string) => {
return new UIFeature(FindNativeFeature<F>(identifier)!) return new UIFeature(FindNativeFeature<F>(identifier)!)
} }
@@ -43,7 +43,7 @@ describe('RunWithPermissionsUseCase', () => {
expect( expect(
usecase.areRequestedPermissionsValid( usecase.areRequestedPermissionsValid(
nativeFeatureAsUIFeature(FeatureIdentifier.MarkdownProEditor), nativeFeatureAsUIFeature(NativeFeatureIdentifier.TYPES.MarkdownProEditor),
permissions, permissions,
), ),
).toEqual(true) ).toEqual(true)
@@ -59,7 +59,7 @@ describe('RunWithPermissionsUseCase', () => {
expect( expect(
usecase.areRequestedPermissionsValid( usecase.areRequestedPermissionsValid(
nativeFeatureAsUIFeature(FeatureIdentifier.MarkdownProEditor), nativeFeatureAsUIFeature(NativeFeatureIdentifier.TYPES.MarkdownProEditor),
permissions, permissions,
), ),
).toEqual(false) ).toEqual(false)
@@ -75,7 +75,7 @@ describe('RunWithPermissionsUseCase', () => {
expect( expect(
usecase.areRequestedPermissionsValid( usecase.areRequestedPermissionsValid(
nativeFeatureAsUIFeature(FeatureIdentifier.MarkdownProEditor), nativeFeatureAsUIFeature(NativeFeatureIdentifier.TYPES.MarkdownProEditor),
permissions, permissions,
), ),
).toEqual(false) ).toEqual(false)
@@ -91,7 +91,7 @@ describe('RunWithPermissionsUseCase', () => {
expect( expect(
usecase.areRequestedPermissionsValid( usecase.areRequestedPermissionsValid(
nativeFeatureAsUIFeature(FeatureIdentifier.MarkdownProEditor), nativeFeatureAsUIFeature(NativeFeatureIdentifier.TYPES.MarkdownProEditor),
permissions, permissions,
), ),
).toEqual(false) ).toEqual(false)
@@ -107,7 +107,7 @@ describe('RunWithPermissionsUseCase', () => {
expect( expect(
usecase.areRequestedPermissionsValid( usecase.areRequestedPermissionsValid(
nativeFeatureAsUIFeature(FeatureIdentifier.DeprecatedFileSafe), nativeFeatureAsUIFeature(NativeFeatureIdentifier.TYPES.DeprecatedFileSafe),
permissions, permissions,
), ),
).toEqual(false) ).toEqual(false)
@@ -127,7 +127,7 @@ describe('RunWithPermissionsUseCase', () => {
expect( expect(
usecase.areRequestedPermissionsValid( usecase.areRequestedPermissionsValid(
nativeFeatureAsUIFeature(FeatureIdentifier.DeprecatedFileSafe), nativeFeatureAsUIFeature(NativeFeatureIdentifier.TYPES.DeprecatedFileSafe),
permissions, permissions,
), ),
).toEqual(true) ).toEqual(true)
@@ -147,7 +147,7 @@ describe('RunWithPermissionsUseCase', () => {
expect( expect(
usecase.areRequestedPermissionsValid( usecase.areRequestedPermissionsValid(
nativeFeatureAsUIFeature(FeatureIdentifier.DeprecatedBoldEditor), nativeFeatureAsUIFeature(NativeFeatureIdentifier.TYPES.DeprecatedBoldEditor),
permissions, permissions,
), ),
).toEqual(true) ).toEqual(true)
@@ -166,7 +166,10 @@ describe('RunWithPermissionsUseCase', () => {
] ]
expect( expect(
usecase.areRequestedPermissionsValid(nativeFeatureAsUIFeature(FeatureIdentifier.PlusEditor), permissions), usecase.areRequestedPermissionsValid(
nativeFeatureAsUIFeature(NativeFeatureIdentifier.TYPES.PlusEditor),
permissions,
),
).toEqual(false) ).toEqual(false)
}) })
}) })

View File

@@ -2,7 +2,6 @@ import {
ComponentAction, ComponentAction,
ComponentFeatureDescription, ComponentFeatureDescription,
ComponentPermission, ComponentPermission,
FeatureIdentifier,
FindNativeFeature, FindNativeFeature,
} from '@standardnotes/features' } from '@standardnotes/features'
import { ComponentInterface, ComponentMutator, PermissionDialog, UIFeature } from '@standardnotes/models' import { ComponentInterface, ComponentMutator, PermissionDialog, UIFeature } from '@standardnotes/models'
@@ -228,7 +227,7 @@ export class RunWithPermissionsUseCase {
} }
private findUIFeature(identifier: string): UIFeature<ComponentFeatureDescription> | undefined { private findUIFeature(identifier: string): UIFeature<ComponentFeatureDescription> | undefined {
const nativeFeature = FindNativeFeature<ComponentFeatureDescription>(identifier as FeatureIdentifier) const nativeFeature = FindNativeFeature<ComponentFeatureDescription>(identifier)
if (nativeFeature) { if (nativeFeature) {
return new UIFeature(nativeFeature) return new UIFeature(nativeFeature)
} }

View File

@@ -2,8 +2,8 @@ import { ItemInterface, SNFeatureRepo } from '@standardnotes/models'
import { SyncService } from '../Sync/SyncService' import { SyncService } from '../Sync/SyncService'
import { SettingName } from '@standardnotes/settings' import { SettingName } from '@standardnotes/settings'
import { FeaturesService } from '@Lib/Services/Features' import { FeaturesService } from '@Lib/Services/Features'
import { RoleName, ContentType } from '@standardnotes/domain-core' import { RoleName, ContentType, Uuid } from '@standardnotes/domain-core'
import { FeatureIdentifier, GetFeatures } from '@standardnotes/features' import { NativeFeatureIdentifier, GetFeatures } from '@standardnotes/features'
import { WebSocketsService } from '../Api/WebsocketsService' import { WebSocketsService } from '../Api/WebsocketsService'
import { SettingsService } from '../Settings' import { SettingsService } from '../Settings'
import { PureCryptoInterface } from '@standardnotes/sncrypto-common' import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
@@ -44,6 +44,7 @@ describe('FeaturesService', () => {
let roles: string[] let roles: string[]
let items: ItemInterface[] let items: ItemInterface[]
let internalEventBus: InternalEventBusInterface let internalEventBus: InternalEventBusInterface
let featureService: FeaturesService
const createService = () => { const createService = () => {
return new FeaturesService( return new FeaturesService(
@@ -118,23 +119,81 @@ describe('FeaturesService', () => {
internalEventBus = {} as jest.Mocked<InternalEventBusInterface> internalEventBus = {} as jest.Mocked<InternalEventBusInterface>
internalEventBus.publish = jest.fn() internalEventBus.publish = jest.fn()
internalEventBus.addEventHandler = jest.fn() internalEventBus.addEventHandler = jest.fn()
featureService = new FeaturesService(
storageService,
itemManager,
mutator,
subscriptions,
apiService,
webSocketsService,
settingsService,
userService,
syncService,
alertService,
sessionManager,
crypto,
internalEventBus,
)
}) })
describe('experimental features', () => { describe('experimental features', () => {
it('enables/disables an experimental feature', async () => { it('enables/disables an experimental feature', async () => {
storageService.getValue = jest.fn().mockReturnValue(GetFeatures()) storageService.getValue = jest.fn().mockReturnValue(GetFeatures())
const featuresService = createService() featureService.getExperimentalFeatures = jest.fn().mockReturnValue([NativeFeatureIdentifier.TYPES.PlusEditor])
featuresService.getExperimentalFeatures = jest.fn().mockReturnValue([FeatureIdentifier.PlusEditor]) featureService.initializeFromDisk()
featuresService.initializeFromDisk()
featuresService.enableExperimentalFeature(FeatureIdentifier.PlusEditor) featureService.enableExperimentalFeature(NativeFeatureIdentifier.TYPES.PlusEditor)
expect(featuresService.isExperimentalFeatureEnabled(FeatureIdentifier.PlusEditor)).toEqual(true) expect(featureService.isExperimentalFeatureEnabled(NativeFeatureIdentifier.TYPES.PlusEditor)).toEqual(true)
featuresService.disableExperimentalFeature(FeatureIdentifier.PlusEditor) featureService.disableExperimentalFeature(NativeFeatureIdentifier.TYPES.PlusEditor)
expect(featuresService.isExperimentalFeatureEnabled(FeatureIdentifier.PlusEditor)).toEqual(false) expect(featureService.isExperimentalFeatureEnabled(NativeFeatureIdentifier.TYPES.PlusEditor)).toEqual(false)
})
})
describe('hasFirstPartyOnlineSubscription', () => {
it('should be true if signed into first party server and has online subscription', () => {
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(true)
expect(featureService.hasFirstPartyOnlineSubscription()).toEqual(true)
})
it('should not be true if not signed into first party server', () => {
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(false)
subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(true)
expect(featureService.hasFirstPartyOnlineSubscription()).toEqual(false)
})
it('should not be true if no online subscription', () => {
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(false)
expect(featureService.hasFirstPartyOnlineSubscription()).toEqual(false)
})
})
describe('hasPaidAnyPartyOnlineOrOfflineSubscription', () => {
it('should return true if onlineRolesIncludePaidSubscription', () => {
featureService.onlineRolesIncludePaidSubscription = jest.fn().mockReturnValue(true)
expect(featureService.hasPaidAnyPartyOnlineOrOfflineSubscription()).toEqual(true)
})
it('should return true if hasOfflineRepo', () => {
featureService.hasOfflineRepo = jest.fn().mockReturnValue(true)
expect(featureService.hasPaidAnyPartyOnlineOrOfflineSubscription()).toEqual(true)
})
it('should return true if hasFirstPartyOnlineSubscription', () => {
featureService.hasFirstPartyOnlineSubscription = jest.fn().mockReturnValue(true)
expect(featureService.hasPaidAnyPartyOnlineOrOfflineSubscription()).toEqual(true)
}) })
}) })
@@ -148,40 +207,40 @@ describe('FeaturesService', () => {
describe('updateRoles()', () => { describe('updateRoles()', () => {
it('setRoles should notify event if roles changed', async () => { it('setRoles should notify event if roles changed', async () => {
storageService.getValue = jest.fn().mockReturnValue(roles) storageService.getValue = jest.fn().mockReturnValue(roles)
const featuresService = createService()
featuresService.initializeFromDisk()
const mock = (featuresService['notifyEvent'] = jest.fn()) featureService.initializeFromDisk()
const mock = (featureService['notifyEvent'] = jest.fn())
const newRoles = [...roles, RoleName.NAMES.PlusUser] const newRoles = [...roles, RoleName.NAMES.PlusUser]
featuresService.setOnlineRoles(newRoles) featureService.setOnlineRoles(newRoles)
expect(mock.mock.calls[0][0]).toEqual(FeaturesEvent.UserRolesChanged) expect(mock.mock.calls[0][0]).toEqual(FeaturesEvent.UserRolesChanged)
}) })
it('should notify of subscription purchase', async () => { it('should notify of subscription purchase', async () => {
storageService.getValue = jest.fn().mockReturnValue(roles) storageService.getValue = jest.fn().mockReturnValue(roles)
const featuresService = createService()
featuresService.initializeFromDisk()
const spy = jest.spyOn(featuresService, 'notifyEvent' as never) featureService.initializeFromDisk()
const spy = jest.spyOn(featureService, 'notifyEvent' as never)
const newRoles = [...roles, RoleName.NAMES.ProUser] const newRoles = [...roles, RoleName.NAMES.ProUser]
await featuresService.updateOnlineRolesWithNewValues(newRoles) await featureService.updateOnlineRolesWithNewValues(newRoles)
expect(spy.mock.calls[1][0]).toEqual(FeaturesEvent.DidPurchaseSubscription) expect(spy.mock.calls[1][0]).toEqual(FeaturesEvent.DidPurchaseSubscription)
}) })
it('should not notify of subscription purchase on initial roles load after sign in', async () => { it('should not notify of subscription purchase on initial roles load after sign in', async () => {
storageService.getValue = jest.fn().mockReturnValue(roles) storageService.getValue = jest.fn().mockReturnValue(roles)
const featuresService = createService()
featuresService.initializeFromDisk()
featuresService['onlineRoles'] = []
const spy = jest.spyOn(featuresService, 'notifyEvent' as never) featureService.initializeFromDisk()
featureService['onlineRoles'] = []
const spy = jest.spyOn(featureService, 'notifyEvent' as never)
const newRoles = [...roles, RoleName.NAMES.ProUser] const newRoles = [...roles, RoleName.NAMES.ProUser]
await featuresService.updateOnlineRolesWithNewValues(newRoles) await featureService.updateOnlineRolesWithNewValues(newRoles)
const triggeredEvents = spy.mock.calls.map((call) => call[0]) const triggeredEvents = spy.mock.calls.map((call) => call[0])
expect(triggeredEvents).not.toContain(FeaturesEvent.DidPurchaseSubscription) expect(triggeredEvents).not.toContain(FeaturesEvent.DidPurchaseSubscription)
@@ -189,11 +248,11 @@ describe('FeaturesService', () => {
it('saves new roles to storage if a role has been added', async () => { it('saves new roles to storage if a role has been added', async () => {
storageService.getValue = jest.fn().mockReturnValue(roles) storageService.getValue = jest.fn().mockReturnValue(roles)
const featuresService = createService()
featuresService.initializeFromDisk() featureService.initializeFromDisk()
const newRoles = [...roles, RoleName.NAMES.ProUser] const newRoles = [...roles, RoleName.NAMES.ProUser]
await featuresService.updateOnlineRolesWithNewValues(newRoles) await featureService.updateOnlineRolesWithNewValues(newRoles)
expect(storageService.setValue).toHaveBeenCalledWith(StorageKey.UserRoles, newRoles) expect(storageService.setValue).toHaveBeenCalledWith(StorageKey.UserRoles, newRoles)
}) })
@@ -201,134 +260,162 @@ describe('FeaturesService', () => {
const newRoles = [RoleName.NAMES.CoreUser] const newRoles = [RoleName.NAMES.CoreUser]
storageService.getValue = jest.fn().mockReturnValue(roles) storageService.getValue = jest.fn().mockReturnValue(roles)
const featuresService = createService()
featuresService.initializeFromDisk() featureService.initializeFromDisk()
await featuresService.updateOnlineRolesWithNewValues(newRoles) await featureService.updateOnlineRolesWithNewValues(newRoles)
expect(storageService.setValue).toHaveBeenCalledWith(StorageKey.UserRoles, newRoles) expect(storageService.setValue).toHaveBeenCalledWith(StorageKey.UserRoles, newRoles)
}) })
it('role-based feature status', async () => { it('role-based feature status', async () => {
const featuresService = createService()
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true) sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser]) await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(true) subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(true)
expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(FeatureStatus.Entitled) expect(
expect(featuresService.getFeatureStatus(FeatureIdentifier.SuperEditor)).toBe(FeatureStatus.Entitled) featureService.getFeatureStatus(
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.MidnightTheme).getValue(),
),
).toBe(FeatureStatus.Entitled)
expect(
featureService.getFeatureStatus(
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.SuperEditor).getValue(),
),
).toBe(FeatureStatus.Entitled)
}) })
it('feature status with no paid role', async () => { it('feature status with no paid role', async () => {
const featuresService = createService()
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true) sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser]) await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser])
subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(false) subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(false)
expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(FeatureStatus.NoUserSubscription) expect(
expect(featuresService.getFeatureStatus(FeatureIdentifier.PlusEditor)).toBe(FeatureStatus.NoUserSubscription) featureService.getFeatureStatus(
expect(featuresService.getFeatureStatus(FeatureIdentifier.SheetsEditor)).toBe(FeatureStatus.NoUserSubscription) NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.MidnightTheme).getValue(),
),
).toBe(FeatureStatus.NoUserSubscription)
expect(
featureService.getFeatureStatus(
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.PlusEditor).getValue(),
),
).toBe(FeatureStatus.NoUserSubscription)
expect(
featureService.getFeatureStatus(
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.SheetsEditor).getValue(),
),
).toBe(FeatureStatus.NoUserSubscription)
}) })
it('role-based features while not signed into first party server', async () => { it('role-based features while not signed into first party server', async () => {
const featuresService = createService()
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(false) sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(false)
await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.ProUser]) await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.ProUser])
expect(featuresService.getFeatureStatus(FeatureIdentifier.SuperEditor)).toBe(FeatureStatus.NoUserSubscription) expect(
featureService.getFeatureStatus(
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.SuperEditor).getValue(),
),
).toBe(FeatureStatus.NoUserSubscription)
}) })
it('third party feature status', async () => { it('third party feature status', async () => {
const featuresService = createService()
itemManager.getDisplayableComponents = jest itemManager.getDisplayableComponents = jest
.fn() .fn()
.mockReturnValue([{ identifier: 'third-party-theme' }, { identifier: 'third-party-editor', isExpired: true }]) .mockReturnValue([
{ uuid: '00000000-0000-0000-0000-000000000001' },
{ uuid: '00000000-0000-0000-0000-000000000002', isExpired: true },
])
await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser]) await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser])
expect(featuresService.getFeatureStatus('third-party-theme' as FeatureIdentifier)).toBe(FeatureStatus.Entitled) expect(featureService.getFeatureStatus(Uuid.create('00000000-0000-0000-0000-000000000001').getValue())).toBe(
expect(featuresService.getFeatureStatus('third-party-editor' as FeatureIdentifier)).toBe( FeatureStatus.Entitled,
)
expect(featureService.getFeatureStatus(Uuid.create('00000000-0000-0000-0000-000000000002').getValue())).toBe(
FeatureStatus.InCurrentPlanButExpired, FeatureStatus.InCurrentPlanButExpired,
) )
expect(featuresService.getFeatureStatus('missing-feature-identifier' as FeatureIdentifier)).toBe( expect(featureService.getFeatureStatus(Uuid.create('00000000-0000-0000-0000-000000000003').getValue())).toBe(
FeatureStatus.NoUserSubscription, FeatureStatus.NoUserSubscription,
) )
}) })
it('feature status should be not entitled if no account or offline repo', async () => { it('feature status should be not entitled if no account or offline repo', async () => {
const featuresService = createService() await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser])
await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser])
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(false) sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(false)
expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(FeatureStatus.NoUserSubscription) expect(
expect(featuresService.getFeatureStatus(FeatureIdentifier.TokenVaultEditor)).toBe( featureService.getFeatureStatus(
FeatureStatus.NoUserSubscription, NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.MidnightTheme).getValue(),
) ),
).toBe(FeatureStatus.NoUserSubscription)
expect(
featureService.getFeatureStatus(
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.TokenVaultEditor).getValue(),
),
).toBe(FeatureStatus.NoUserSubscription)
}) })
it('feature status for offline subscription', async () => { it('feature status for offline subscription', async () => {
const featuresService = createService() featureService.hasFirstPartyOfflineSubscription = jest.fn().mockReturnValue(true)
featureService.setOfflineRoles([RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
featuresService.hasFirstPartyOfflineSubscription = jest.fn().mockReturnValue(true) expect(
featuresService.setOfflineRoles([RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser]) featureService.getFeatureStatus(
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.MidnightTheme).getValue(),
expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(FeatureStatus.Entitled) ),
expect(featuresService.getFeatureStatus(FeatureIdentifier.TokenVaultEditor)).toBe(FeatureStatus.Entitled) ).toBe(FeatureStatus.Entitled)
expect(
featureService.getFeatureStatus(
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.TokenVaultEditor).getValue(),
),
).toBe(FeatureStatus.Entitled)
}) })
it('feature status for deprecated feature and no subscription', async () => { it('feature status for deprecated feature and no subscription', async () => {
const featuresService = createService()
subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(false) subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(false)
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true) sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
expect(featuresService.getFeatureStatus(FeatureIdentifier.DeprecatedFileSafe as FeatureIdentifier)).toBe( expect(
FeatureStatus.NoUserSubscription, featureService.getFeatureStatus(
) NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.DeprecatedFileSafe).getValue(),
),
).toBe(FeatureStatus.NoUserSubscription)
}) })
it('feature status for deprecated feature with subscription', async () => { it('feature status for deprecated feature with subscription', async () => {
const featuresService = createService()
subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(true) subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(true)
await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser]) await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
expect(featuresService.getFeatureStatus(FeatureIdentifier.DeprecatedFileSafe as FeatureIdentifier)).toBe( expect(
FeatureStatus.Entitled, featureService.getFeatureStatus(
) NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.DeprecatedFileSafe).getValue(),
),
).toBe(FeatureStatus.Entitled)
}) })
it('has paid subscription', async () => { it('has paid subscription', async () => {
const featuresService = createService() await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser])
await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser])
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true) sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(true)
expect(featuresService.hasPaidAnyPartyOnlineOrOfflineSubscription()).toBeFalsy expect(featureService.hasPaidAnyPartyOnlineOrOfflineSubscription()).toBeFalsy
await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser]) await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
expect(featuresService.hasPaidAnyPartyOnlineOrOfflineSubscription()).toEqual(true) expect(featureService.hasPaidAnyPartyOnlineOrOfflineSubscription()).toEqual(true)
}) })
it('has paid subscription should be true if offline repo and signed into third party server', async () => { it('has paid subscription should be true if offline repo and signed into third party server', async () => {
const featuresService = createService() await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser])
await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser]) featureService.hasOfflineRepo = jest.fn().mockReturnValue(true)
featuresService.hasOfflineRepo = jest.fn().mockReturnValue(true)
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(false) sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(false)
expect(featuresService.hasPaidAnyPartyOnlineOrOfflineSubscription()).toEqual(true) expect(featureService.hasPaidAnyPartyOnlineOrOfflineSubscription()).toEqual(true)
}) })
}) })
@@ -343,8 +430,7 @@ describe('FeaturesService', () => {
}, },
} as never) } as never)
const featuresService = createService() await featureService.migrateFeatureRepoToUserSetting([extensionRepoItem])
await featuresService.migrateFeatureRepoToUserSetting([extensionRepoItem])
expect(settingsService.updateSetting).toHaveBeenCalledWith( expect(settingsService.updateSetting).toHaveBeenCalledWith(
SettingName.create(SettingName.NAMES.ExtensionKey).getValue(), SettingName.create(SettingName.NAMES.ExtensionKey).getValue(),
extensionKey, extensionKey,
@@ -369,8 +455,7 @@ describe('FeaturesService', () => {
const installUrl = 'http://example.com' const installUrl = 'http://example.com'
crypto.base64Decode = jest.fn().mockReturnValue(installUrl) crypto.base64Decode = jest.fn().mockReturnValue(installUrl)
const featuresService = createService() const result = await featureService.downloadRemoteThirdPartyFeature(installUrl)
const result = await featuresService.downloadRemoteThirdPartyFeature(installUrl)
expect(result).toBeUndefined() expect(result).toBeUndefined()
}) })
@@ -389,17 +474,14 @@ describe('FeaturesService', () => {
const installUrl = 'http://example.com' const installUrl = 'http://example.com'
crypto.base64Decode = jest.fn().mockReturnValue(installUrl) crypto.base64Decode = jest.fn().mockReturnValue(installUrl)
const featuresService = createService() const result = await featureService.downloadRemoteThirdPartyFeature(installUrl)
const result = await featuresService.downloadRemoteThirdPartyFeature(installUrl)
expect(result).toBeUndefined() expect(result).toBeUndefined()
}) })
}) })
describe('sortRolesByHierarchy', () => { describe('sortRolesByHierarchy', () => {
it('should sort given roles according to role hierarchy', () => { it('should sort given roles according to role hierarchy', () => {
const featuresService = createService() const sortedRoles = featureService.rolesBySorting([
const sortedRoles = featuresService.rolesBySorting([
RoleName.NAMES.ProUser, RoleName.NAMES.ProUser,
RoleName.NAMES.CoreUser, RoleName.NAMES.CoreUser,
RoleName.NAMES.PlusUser, RoleName.NAMES.PlusUser,
@@ -411,50 +493,42 @@ describe('FeaturesService', () => {
describe('hasMinimumRole', () => { describe('hasMinimumRole', () => {
it('should be false if core user checks for plus role', async () => { it('should be false if core user checks for plus role', async () => {
const featuresService = createService() await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser])
await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser]) const hasPlusUserRole = featureService.hasMinimumRole(RoleName.NAMES.PlusUser)
const hasPlusUserRole = featuresService.hasMinimumRole(RoleName.NAMES.PlusUser)
expect(hasPlusUserRole).toBe(false) expect(hasPlusUserRole).toBe(false)
}) })
it('should be false if plus user checks for pro role', async () => { it('should be false if plus user checks for pro role', async () => {
const featuresService = createService()
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true) sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(true) subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(true)
await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.PlusUser, RoleName.NAMES.CoreUser]) await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.PlusUser, RoleName.NAMES.CoreUser])
const hasProUserRole = featuresService.hasMinimumRole(RoleName.NAMES.ProUser) const hasProUserRole = featureService.hasMinimumRole(RoleName.NAMES.ProUser)
expect(hasProUserRole).toBe(false) expect(hasProUserRole).toBe(false)
}) })
it('should be true if pro user checks for core user', async () => { it('should be true if pro user checks for core user', async () => {
const featuresService = createService()
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true) sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(true) subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(true)
await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.ProUser, RoleName.NAMES.PlusUser]) await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.ProUser, RoleName.NAMES.PlusUser])
const hasCoreUserRole = featuresService.hasMinimumRole(RoleName.NAMES.CoreUser) const hasCoreUserRole = featureService.hasMinimumRole(RoleName.NAMES.CoreUser)
expect(hasCoreUserRole).toBe(true) expect(hasCoreUserRole).toBe(true)
}) })
it('should be true if pro user checks for pro user', async () => { it('should be true if pro user checks for pro user', async () => {
const featuresService = createService()
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true) sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(true) subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(true)
await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.ProUser, RoleName.NAMES.PlusUser]) await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.ProUser, RoleName.NAMES.PlusUser])
const hasProUserRole = featuresService.hasMinimumRole(RoleName.NAMES.ProUser) const hasProUserRole = featureService.hasMinimumRole(RoleName.NAMES.ProUser)
expect(hasProUserRole).toBe(true) expect(hasProUserRole).toBe(true)
}) })

View File

@@ -1,14 +1,14 @@
import { MigrateFeatureRepoToUserSettingUseCase } from './UseCase/MigrateFeatureRepoToUserSetting' import { MigrateFeatureRepoToUserSettingUseCase } from './UseCase/MigrateFeatureRepoToUserSetting'
import { arraysEqual, removeFromArray, lastElement } from '@standardnotes/utils' import { arraysEqual, removeFromArray, lastElement } from '@standardnotes/utils'
import { ClientDisplayableError } from '@standardnotes/responses' import { ClientDisplayableError } from '@standardnotes/responses'
import { RoleName, ContentType } from '@standardnotes/domain-core' import { RoleName, ContentType, Uuid } from '@standardnotes/domain-core'
import { PROD_OFFLINE_FEATURES_URL } from '../../Hosts' import { PROD_OFFLINE_FEATURES_URL } from '../../Hosts'
import { PureCryptoInterface } from '@standardnotes/sncrypto-common' import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { WebSocketsService } from '../Api/WebsocketsService' import { WebSocketsService } from '../Api/WebsocketsService'
import { WebSocketsServiceEvent } from '../Api/WebSocketsServiceEvent' import { WebSocketsServiceEvent } from '../Api/WebSocketsServiceEvent'
import { TRUSTED_CUSTOM_EXTENSIONS_HOSTS, TRUSTED_FEATURE_HOSTS } from '@Lib/Hosts' import { TRUSTED_CUSTOM_EXTENSIONS_HOSTS, TRUSTED_FEATURE_HOSTS } from '@Lib/Hosts'
import { UserRolesChangedEvent } from '@standardnotes/domain-events' import { UserRolesChangedEvent } from '@standardnotes/domain-events'
import { ExperimentalFeatures, FindNativeFeature, FeatureIdentifier } from '@standardnotes/features' import { ExperimentalFeatures, FindNativeFeature, NativeFeatureIdentifier } from '@standardnotes/features'
import { import {
SNFeatureRepo, SNFeatureRepo,
FeatureRepoContent, FeatureRepoContent,
@@ -64,7 +64,7 @@ export class FeaturesService
{ {
private onlineRoles: string[] = [] private onlineRoles: string[] = []
private offlineRoles: string[] = [] private offlineRoles: string[] = []
private enabledExperimentalFeatures: FeatureIdentifier[] = [] private enabledExperimentalFeatures: string[] = []
private getFeatureStatusUseCase = new GetFeatureStatusUseCase(this.items) private getFeatureStatusUseCase = new GetFeatureStatusUseCase(this.items)
@@ -136,40 +136,47 @@ export class FeaturesService
) )
} }
public initializeFromDisk(): void { initializeFromDisk(): void {
this.onlineRoles = this.storage.getValue<string[]>(StorageKey.UserRoles, undefined, []) this.onlineRoles = this.storage.getValue<string[]>(StorageKey.UserRoles, undefined, [])
this.offlineRoles = this.storage.getValue<string[]>(StorageKey.OfflineUserRoles, undefined, []) this.offlineRoles = this.storage.getValue<string[]>(StorageKey.OfflineUserRoles, undefined, [])
this.enabledExperimentalFeatures = this.storage.getValue(StorageKey.ExperimentalFeatures, undefined, []) this.enabledExperimentalFeatures = this.storage.getValue(StorageKey.ExperimentalFeatures, undefined, [])
} }
async handleEvent(event: InternalEventInterface): Promise<void> { async handleEvent(event: InternalEventInterface): Promise<void> {
if (event.type === ApiServiceEvent.MetaReceived) { switch (event.type) {
if (!this.sync) { case ApiServiceEvent.MetaReceived: {
this.log('Handling events interrupted. Sync service is not yet initialized.', event) if (!this.sync) {
return this.log('Handling events interrupted. Sync service is not yet initialized.', event)
return
}
const { userRoles } = event.payload as MetaReceivedData
void this.updateOnlineRolesWithNewValues(userRoles.map((role) => role.name))
break
} }
const { userRoles } = event.payload as MetaReceivedData case ApplicationEvent.ApplicationStageChanged: {
void this.updateOnlineRolesWithNewValues(userRoles.map((role) => role.name)) const stage = (event.payload as ApplicationStageChangedEventPayload).stage
} switch (stage) {
case ApplicationStage.StorageDecrypted_09: {
if (event.type === ApplicationEvent.ApplicationStageChanged) { this.initializeFromDisk()
const stage = (event.payload as ApplicationStageChangedEventPayload).stage break
if (stage === ApplicationStage.FullSyncCompleted_13) { }
if (!this.hasFirstPartyOnlineSubscription()) { case ApplicationStage.FullSyncCompleted_13: {
const offlineRepo = this.getOfflineRepo() if (!this.hasFirstPartyOnlineSubscription()) {
const offlineRepo = this.getOfflineRepo()
if (offlineRepo) { if (offlineRepo) {
void this.downloadOfflineRoles(offlineRepo) void this.downloadOfflineRoles(offlineRepo)
}
}
break
} }
} }
} }
} }
} }
public enableExperimentalFeature(identifier: FeatureIdentifier): void { public enableExperimentalFeature(identifier: string): void {
this.enabledExperimentalFeatures.push(identifier) this.enabledExperimentalFeatures.push(identifier)
void this.storage.setValue(StorageKey.ExperimentalFeatures, this.enabledExperimentalFeatures) void this.storage.setValue(StorageKey.ExperimentalFeatures, this.enabledExperimentalFeatures)
@@ -177,7 +184,7 @@ export class FeaturesService
void this.notifyEvent(FeaturesEvent.FeaturesAvailabilityChanged) void this.notifyEvent(FeaturesEvent.FeaturesAvailabilityChanged)
} }
public disableExperimentalFeature(identifier: FeatureIdentifier): void { public disableExperimentalFeature(identifier: string): void {
removeFromArray(this.enabledExperimentalFeatures, identifier) removeFromArray(this.enabledExperimentalFeatures, identifier)
void this.storage.setValue(StorageKey.ExperimentalFeatures, this.enabledExperimentalFeatures) void this.storage.setValue(StorageKey.ExperimentalFeatures, this.enabledExperimentalFeatures)
@@ -195,7 +202,7 @@ export class FeaturesService
void this.notifyEvent(FeaturesEvent.FeaturesAvailabilityChanged) void this.notifyEvent(FeaturesEvent.FeaturesAvailabilityChanged)
} }
public toggleExperimentalFeature(identifier: FeatureIdentifier): void { public toggleExperimentalFeature(identifier: string): void {
if (this.isExperimentalFeatureEnabled(identifier)) { if (this.isExperimentalFeatureEnabled(identifier)) {
this.disableExperimentalFeature(identifier) this.disableExperimentalFeature(identifier)
} else { } else {
@@ -203,19 +210,19 @@ export class FeaturesService
} }
} }
public getExperimentalFeatures(): FeatureIdentifier[] { public getExperimentalFeatures(): string[] {
return ExperimentalFeatures return ExperimentalFeatures
} }
public isExperimentalFeature(featureId: FeatureIdentifier): boolean { public isExperimentalFeature(featureId: string): boolean {
return this.getExperimentalFeatures().includes(featureId) return this.getExperimentalFeatures().includes(featureId)
} }
public getEnabledExperimentalFeatures(): FeatureIdentifier[] { public getEnabledExperimentalFeatures(): string[] {
return this.enabledExperimentalFeatures return this.enabledExperimentalFeatures
} }
public isExperimentalFeatureEnabled(featureId: FeatureIdentifier): boolean { public isExperimentalFeatureEnabled(featureId: string): boolean {
return this.enabledExperimentalFeatures.includes(featureId) return this.enabledExperimentalFeatures.includes(featureId)
} }
@@ -302,10 +309,10 @@ export class FeaturesService
} }
hasPaidAnyPartyOnlineOrOfflineSubscription(): boolean { hasPaidAnyPartyOnlineOrOfflineSubscription(): boolean {
return this.onlineRolesIncludePaidSubscription() || this.hasOfflineRepo() return this.onlineRolesIncludePaidSubscription() || this.hasOfflineRepo() || this.hasFirstPartyOnlineSubscription()
} }
private hasFirstPartyOnlineSubscription(): boolean { hasFirstPartyOnlineSubscription(): boolean {
return this.sessions.isSignedIntoFirstPartyServer() && this.subscriptions.hasOnlineSubscription() return this.sessions.isSignedIntoFirstPartyServer() && this.subscriptions.hasOnlineSubscription()
} }
@@ -364,12 +371,13 @@ export class FeaturesService
} }
public isThirdPartyFeature(identifier: string): boolean { public isThirdPartyFeature(identifier: string): boolean {
const isNativeFeature = !!FindNativeFeature(identifier as FeatureIdentifier) const isNativeFeature = !!FindNativeFeature(identifier)
return !isNativeFeature return !isNativeFeature
} }
onlineRolesIncludePaidSubscription(): boolean { onlineRolesIncludePaidSubscription(): boolean {
const unpaidRoles = [RoleName.NAMES.CoreUser] const unpaidRoles = [RoleName.NAMES.CoreUser]
return this.onlineRoles.some((role) => !unpaidRoles.includes(role)) return this.onlineRoles.some((role) => !unpaidRoles.includes(role))
} }
@@ -392,7 +400,7 @@ export class FeaturesService
} }
public getFeatureStatus( public getFeatureStatus(
featureId: FeatureIdentifier, featureId: NativeFeatureIdentifier | Uuid,
options: { inContextOfItem?: DecryptedItemInterface } = {}, options: { inContextOfItem?: DecryptedItemInterface } = {},
): FeatureStatus { ): FeatureStatus {
return this.getFeatureStatusUseCase.execute({ return this.getFeatureStatusUseCase.execute({

View File

@@ -1,28 +1,23 @@
import { FeatureIdentifier } from '@standardnotes/features' import { NativeFeatureIdentifier } from '@standardnotes/features'
import { FeatureStatus, ItemManagerInterface } from '@standardnotes/services' import { FeatureStatus, ItemManagerInterface } from '@standardnotes/services'
import { GetFeatureStatusUseCase } from './GetFeatureStatus' import { GetFeatureStatusUseCase } from './GetFeatureStatus'
import { ComponentInterface, DecryptedItemInterface } from '@standardnotes/models' import { ComponentInterface, DecryptedItemInterface } from '@standardnotes/models'
jest.mock('@standardnotes/features', () => ({
FeatureIdentifier: {
DarkTheme: 'darkTheme',
},
FindNativeFeature: jest.fn(),
}))
import { FindNativeFeature } from '@standardnotes/features'
import { Subscription } from '@standardnotes/responses' import { Subscription } from '@standardnotes/responses'
import { Uuid } from '@standardnotes/domain-core'
describe('GetFeatureStatusUseCase', () => { describe('GetFeatureStatusUseCase', () => {
let items: jest.Mocked<ItemManagerInterface> let items: jest.Mocked<ItemManagerInterface>
let usecase: GetFeatureStatusUseCase let usecase: GetFeatureStatusUseCase
let findNativeFeature: jest.Mock<any, any>
beforeEach(() => { beforeEach(() => {
items = { items = {
getDisplayableComponents: jest.fn(), getDisplayableComponents: jest.fn(),
} as unknown as jest.Mocked<ItemManagerInterface> } as unknown as jest.Mocked<ItemManagerInterface>
usecase = new GetFeatureStatusUseCase(items) usecase = new GetFeatureStatusUseCase(items)
;(FindNativeFeature as jest.Mock).mockReturnValue(undefined) findNativeFeature = jest.fn()
usecase.findNativeFeature = findNativeFeature
findNativeFeature.mockReturnValue(undefined)
}) })
afterEach(() => { afterEach(() => {
@@ -33,7 +28,7 @@ describe('GetFeatureStatusUseCase', () => {
it('should return entitled for free features', () => { it('should return entitled for free features', () => {
expect( expect(
usecase.execute({ usecase.execute({
featureId: FeatureIdentifier.DarkTheme, featureId: NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.DarkTheme).getValue(),
hasPaidAnyPartyOnlineOrOfflineSubscription: false, hasPaidAnyPartyOnlineOrOfflineSubscription: false,
firstPartyOnlineSubscription: undefined, firstPartyOnlineSubscription: undefined,
firstPartyRoles: undefined, firstPartyRoles: undefined,
@@ -44,11 +39,11 @@ describe('GetFeatureStatusUseCase', () => {
describe('deprecated features', () => { describe('deprecated features', () => {
it('should return entitled for deprecated paid features if any subscription is active', () => { it('should return entitled for deprecated paid features if any subscription is active', () => {
;(FindNativeFeature as jest.Mock).mockReturnValue({ deprecated: true }) findNativeFeature.mockReturnValue({ deprecated: true })
expect( expect(
usecase.execute({ usecase.execute({
featureId: 'deprecatedFeature', featureId: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
hasPaidAnyPartyOnlineOrOfflineSubscription: true, hasPaidAnyPartyOnlineOrOfflineSubscription: true,
firstPartyOnlineSubscription: undefined, firstPartyOnlineSubscription: undefined,
firstPartyRoles: undefined, firstPartyRoles: undefined,
@@ -57,11 +52,11 @@ describe('GetFeatureStatusUseCase', () => {
}) })
it('should return NoUserSubscription for deprecated paid features if no subscription is active', () => { it('should return NoUserSubscription for deprecated paid features if no subscription is active', () => {
;(FindNativeFeature as jest.Mock).mockReturnValue({ deprecated: true }) findNativeFeature.mockReturnValue({ deprecated: true })
expect( expect(
usecase.execute({ usecase.execute({
featureId: 'deprecatedFeature', featureId: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
hasPaidAnyPartyOnlineOrOfflineSubscription: false, hasPaidAnyPartyOnlineOrOfflineSubscription: false,
firstPartyOnlineSubscription: undefined, firstPartyOnlineSubscription: undefined,
firstPartyRoles: undefined, firstPartyRoles: undefined,
@@ -72,11 +67,11 @@ describe('GetFeatureStatusUseCase', () => {
describe('native features', () => { describe('native features', () => {
it('should return Entitled if the context item belongs to a shared vault and user does not have subscription', () => { it('should return Entitled if the context item belongs to a shared vault and user does not have subscription', () => {
;(FindNativeFeature as jest.Mock).mockReturnValue({ deprecated: false }) findNativeFeature.mockReturnValue({ deprecated: false })
expect( expect(
usecase.execute({ usecase.execute({
featureId: 'nativeFeature', featureId: NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.AutobiographyTheme).getValue(),
firstPartyOnlineSubscription: undefined, firstPartyOnlineSubscription: undefined,
firstPartyRoles: undefined, firstPartyRoles: undefined,
hasPaidAnyPartyOnlineOrOfflineSubscription: false, hasPaidAnyPartyOnlineOrOfflineSubscription: false,
@@ -86,11 +81,11 @@ describe('GetFeatureStatusUseCase', () => {
}) })
it('should return NoUserSubscription if the context item does not belong to a shared vault and user does not have subscription', () => { it('should return NoUserSubscription if the context item does not belong to a shared vault and user does not have subscription', () => {
;(FindNativeFeature as jest.Mock).mockReturnValue({ deprecated: false }) findNativeFeature.mockReturnValue({ deprecated: false })
expect( expect(
usecase.execute({ usecase.execute({
featureId: 'nativeFeature', featureId: NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.AutobiographyTheme).getValue(),
firstPartyOnlineSubscription: undefined, firstPartyOnlineSubscription: undefined,
firstPartyRoles: undefined, firstPartyRoles: undefined,
hasPaidAnyPartyOnlineOrOfflineSubscription: false, hasPaidAnyPartyOnlineOrOfflineSubscription: false,
@@ -100,11 +95,11 @@ describe('GetFeatureStatusUseCase', () => {
}) })
it('should return NoUserSubscription for native features without subscription and roles', () => { it('should return NoUserSubscription for native features without subscription and roles', () => {
;(FindNativeFeature as jest.Mock).mockReturnValue({ deprecated: false }) findNativeFeature.mockReturnValue({ deprecated: false })
expect( expect(
usecase.execute({ usecase.execute({
featureId: 'nativeFeature', featureId: NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.AutobiographyTheme).getValue(),
firstPartyOnlineSubscription: undefined, firstPartyOnlineSubscription: undefined,
firstPartyRoles: undefined, firstPartyRoles: undefined,
hasPaidAnyPartyOnlineOrOfflineSubscription: false, hasPaidAnyPartyOnlineOrOfflineSubscription: false,
@@ -113,14 +108,14 @@ describe('GetFeatureStatusUseCase', () => {
}) })
it('should return NotInCurrentPlan for native features with roles not in available roles', () => { it('should return NotInCurrentPlan for native features with roles not in available roles', () => {
;(FindNativeFeature as jest.Mock).mockReturnValue({ findNativeFeature.mockReturnValue({
deprecated: false, deprecated: false,
availableInRoles: ['notInRole'], availableInRoles: ['notInRole'],
}) })
expect( expect(
usecase.execute({ usecase.execute({
featureId: 'nativeFeature', featureId: NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.AutobiographyTheme).getValue(),
firstPartyOnlineSubscription: undefined, firstPartyOnlineSubscription: undefined,
firstPartyRoles: { online: ['inRole'] }, firstPartyRoles: { online: ['inRole'] },
hasPaidAnyPartyOnlineOrOfflineSubscription: false, hasPaidAnyPartyOnlineOrOfflineSubscription: false,
@@ -129,14 +124,14 @@ describe('GetFeatureStatusUseCase', () => {
}) })
it('should return Entitled for native features with roles in available roles and active subscription', () => { it('should return Entitled for native features with roles in available roles and active subscription', () => {
;(FindNativeFeature as jest.Mock).mockReturnValue({ findNativeFeature.mockReturnValue({
deprecated: false, deprecated: false,
availableInRoles: ['inRole'], availableInRoles: ['inRole'],
}) })
expect( expect(
usecase.execute({ usecase.execute({
featureId: 'nativeFeature', featureId: NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.AutobiographyTheme).getValue(),
firstPartyOnlineSubscription: { firstPartyOnlineSubscription: {
endsAt: new Date(Date.now() + 10000).getTime(), endsAt: new Date(Date.now() + 10000).getTime(),
} as jest.Mocked<Subscription>, } as jest.Mocked<Subscription>,
@@ -147,14 +142,14 @@ describe('GetFeatureStatusUseCase', () => {
}) })
it('should return InCurrentPlanButExpired for native features with roles in available roles and expired subscription', () => { it('should return InCurrentPlanButExpired for native features with roles in available roles and expired subscription', () => {
;(FindNativeFeature as jest.Mock).mockReturnValue({ findNativeFeature.mockReturnValue({
deprecated: false, deprecated: false,
availableInRoles: ['inRole'], availableInRoles: ['inRole'],
}) })
expect( expect(
usecase.execute({ usecase.execute({
featureId: 'nativeFeature', featureId: NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.AutobiographyTheme).getValue(),
firstPartyOnlineSubscription: { firstPartyOnlineSubscription: {
endsAt: new Date(Date.now() - 10000).getTime(), endsAt: new Date(Date.now() - 10000).getTime(),
} as jest.Mocked<Subscription>, } as jest.Mocked<Subscription>,
@@ -168,7 +163,7 @@ describe('GetFeatureStatusUseCase', () => {
describe('third party features', () => { describe('third party features', () => {
it('should return Entitled for third-party features', () => { it('should return Entitled for third-party features', () => {
const mockComponent = { const mockComponent = {
identifier: 'thirdPartyFeature', uuid: '00000000-0000-0000-0000-000000000000',
isExpired: false, isExpired: false,
} as unknown as jest.Mocked<ComponentInterface> } as unknown as jest.Mocked<ComponentInterface>
@@ -176,7 +171,7 @@ describe('GetFeatureStatusUseCase', () => {
expect( expect(
usecase.execute({ usecase.execute({
featureId: 'thirdPartyFeature', featureId: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
hasPaidAnyPartyOnlineOrOfflineSubscription: false, hasPaidAnyPartyOnlineOrOfflineSubscription: false,
firstPartyOnlineSubscription: undefined, firstPartyOnlineSubscription: undefined,
firstPartyRoles: undefined, firstPartyRoles: undefined,
@@ -189,7 +184,7 @@ describe('GetFeatureStatusUseCase', () => {
expect( expect(
usecase.execute({ usecase.execute({
featureId: 'nonExistingThirdPartyFeature', featureId: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
hasPaidAnyPartyOnlineOrOfflineSubscription: false, hasPaidAnyPartyOnlineOrOfflineSubscription: false,
firstPartyOnlineSubscription: undefined, firstPartyOnlineSubscription: undefined,
firstPartyRoles: undefined, firstPartyRoles: undefined,
@@ -199,7 +194,7 @@ describe('GetFeatureStatusUseCase', () => {
it('should return InCurrentPlanButExpired for expired third-party features', () => { it('should return InCurrentPlanButExpired for expired third-party features', () => {
const mockComponent = { const mockComponent = {
identifier: 'thirdPartyFeature', uuid: '00000000-0000-0000-0000-000000000000',
isExpired: true, isExpired: true,
} as unknown as jest.Mocked<ComponentInterface> } as unknown as jest.Mocked<ComponentInterface>
@@ -207,7 +202,7 @@ describe('GetFeatureStatusUseCase', () => {
expect( expect(
usecase.execute({ usecase.execute({
featureId: 'thirdPartyFeature', featureId: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
hasPaidAnyPartyOnlineOrOfflineSubscription: false, hasPaidAnyPartyOnlineOrOfflineSubscription: false,
firstPartyOnlineSubscription: undefined, firstPartyOnlineSubscription: undefined,
firstPartyRoles: undefined, firstPartyRoles: undefined,

View File

@@ -1,4 +1,5 @@
import { AnyFeatureDescription, FeatureIdentifier, FindNativeFeature } from '@standardnotes/features' import { Uuid } from '@standardnotes/domain-core'
import { AnyFeatureDescription, NativeFeatureIdentifier, FindNativeFeature } from '@standardnotes/features'
import { DecryptedItemInterface } from '@standardnotes/models' import { DecryptedItemInterface } from '@standardnotes/models'
import { Subscription } from '@standardnotes/responses' import { Subscription } from '@standardnotes/responses'
import { FeatureStatus, ItemManagerInterface } from '@standardnotes/services' import { FeatureStatus, ItemManagerInterface } from '@standardnotes/services'
@@ -8,20 +9,19 @@ export class GetFeatureStatusUseCase {
constructor(private items: ItemManagerInterface) {} constructor(private items: ItemManagerInterface) {}
execute(dto: { execute(dto: {
featureId: FeatureIdentifier | string featureId: NativeFeatureIdentifier | Uuid
firstPartyOnlineSubscription: Subscription | undefined firstPartyOnlineSubscription: Subscription | undefined
firstPartyRoles: { online: string[] } | { offline: string[] } | undefined firstPartyRoles: { online: string[] } | { offline: string[] } | undefined
hasPaidAnyPartyOnlineOrOfflineSubscription: boolean hasPaidAnyPartyOnlineOrOfflineSubscription: boolean
inContextOfItem?: DecryptedItemInterface inContextOfItem?: DecryptedItemInterface
}): FeatureStatus { }): FeatureStatus {
if (this.isFreeFeature(dto.featureId as FeatureIdentifier)) { if (this.isFreeFeature(dto.featureId)) {
return FeatureStatus.Entitled return FeatureStatus.Entitled
} }
const nativeFeature = FindNativeFeature(dto.featureId as FeatureIdentifier) const nativeFeature = this.findNativeFeature(dto.featureId)
if (!nativeFeature) { if (!nativeFeature) {
return this.getThirdPartyFeatureStatus(dto.featureId as string) return this.getThirdPartyFeatureStatus(dto.featureId)
} }
if (nativeFeature.deprecated) { if (nativeFeature.deprecated) {
@@ -39,6 +39,10 @@ export class GetFeatureStatusUseCase {
}) })
} }
findNativeFeature(featureId: NativeFeatureIdentifier | Uuid): AnyFeatureDescription | undefined {
return FindNativeFeature(featureId.value)
}
private getDeprecatedNativeFeatureStatus(dto: { private getDeprecatedNativeFeatureStatus(dto: {
hasPaidAnyPartyOnlineOrOfflineSubscription: boolean hasPaidAnyPartyOnlineOrOfflineSubscription: boolean
nativeFeature: AnyFeatureDescription nativeFeature: AnyFeatureDescription
@@ -95,8 +99,8 @@ export class GetFeatureStatusUseCase {
return FeatureStatus.Entitled return FeatureStatus.Entitled
} }
private getThirdPartyFeatureStatus(featureId: string): FeatureStatus { private getThirdPartyFeatureStatus(uuid: Uuid): FeatureStatus {
const component = this.items.getDisplayableComponents().find((candidate) => candidate.identifier === featureId) const component = this.items.getDisplayableComponents().find((candidate) => candidate.uuid === uuid.value)
if (!component) { if (!component) {
return FeatureStatus.NoUserSubscription return FeatureStatus.NoUserSubscription
@@ -109,7 +113,9 @@ export class GetFeatureStatusUseCase {
return FeatureStatus.Entitled return FeatureStatus.Entitled
} }
private isFreeFeature(featureId: FeatureIdentifier) { private isFreeFeature(featureId: NativeFeatureIdentifier) {
return [FeatureIdentifier.DarkTheme, FeatureIdentifier.PlainEditor].includes(featureId) return [NativeFeatureIdentifier.TYPES.DarkTheme, NativeFeatureIdentifier.TYPES.PlainEditor].includes(
featureId.value,
)
} }
} }

View File

@@ -29,6 +29,9 @@ import {
UserKeyPairChangedEventData, UserKeyPairChangedEventData,
InternalFeatureService, InternalFeatureService,
InternalFeature, InternalFeature,
ApplicationEvent,
ApplicationStageChangedEventPayload,
ApplicationStage,
} from '@standardnotes/services' } from '@standardnotes/services'
import { Base64String, PkcKeyPair, PureCryptoInterface } from '@standardnotes/sncrypto-common' import { Base64String, PkcKeyPair, PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { import {
@@ -112,8 +115,17 @@ export class SessionManager
} }
async handleEvent(event: InternalEventInterface): Promise<void> { async handleEvent(event: InternalEventInterface): Promise<void> {
if (event.type === ApiServiceEvent.SessionRefreshed) { switch (event.type) {
this.httpService.setSession((event.payload as SessionRefreshedData).session) case ApiServiceEvent.SessionRefreshed:
this.httpService.setSession((event.payload as SessionRefreshedData).session)
break
case ApplicationEvent.ApplicationStageChanged: {
const stage = (event.payload as ApplicationStageChangedEventPayload).stage
if (stage === ApplicationStage.StorageDecrypted_09) {
await this.initializeFromDisk()
}
}
} }
} }
@@ -142,7 +154,7 @@ export class SessionManager
this.apiService.setUser(user) this.apiService.setUser(user)
} }
async initializeFromDisk(): Promise<void> { private async initializeFromDisk(): Promise<void> {
this.memoizeUser(this.storage.getValue(StorageKey.User)) this.memoizeUser(this.storage.getValue(StorageKey.User))
if (!this.user) { if (!this.user) {

View File

@@ -145,7 +145,7 @@ describe('migrations', () => {
content_type: ContentType.TYPES.Component, content_type: ContentType.TYPES.Component,
content: FillItemContent({ content: FillItemContent({
package_info: { package_info: {
identifier: FeatureIdentifier.MarkdownProEditor, identifier: NativeFeatureIdentifier.TYPES.MarkdownProEditor,
}, },
}), }),
}), }),
@@ -170,7 +170,7 @@ describe('migrations', () => {
content_type: ContentType.TYPES.Component, content_type: ContentType.TYPES.Component,
content: FillItemContent({ content: FillItemContent({
package_info: { package_info: {
identifier: FeatureIdentifier.DeprecatedBoldEditor, identifier: NativeFeatureIdentifier.TYPES.DeprecatedBoldEditor,
}, },
}), }),
}), }),

View File

@@ -369,9 +369,9 @@ describe('app models', () => {
editorIdentifier: 'foo-editor', editorIdentifier: 'foo-editor',
}) })
expect(this.application.componentManager.editorForNote(note).uniqueIdentifier).to.equal(component.uuid) expect(this.application.componentManager.editorForNote(note).uniqueIdentifier.value).to.equal(component.uuid)
const duplicate = await this.application.mutator.duplicateItem(note, true) const duplicate = await this.application.mutator.duplicateItem(note, true)
expect(this.application.componentManager.editorForNote(duplicate).uniqueIdentifier).to.equal(component.uuid) expect(this.application.componentManager.editorForNote(duplicate).uniqueIdentifier.value).to.equal(component.uuid)
}) })
}) })

View File

@@ -12,7 +12,7 @@
"scripts": { "scripts": {
"tsc": "tsc --project tsconfig.json", "tsc": "tsc --project tsconfig.json",
"lint": "eslint src --ext .ts", "lint": "eslint src --ext .ts",
"test": "jest spec" "test": "jest"
}, },
"dependencies": { "dependencies": {
"@standardnotes/common": "^1.50.0", "@standardnotes/common": "^1.50.0",

View File

@@ -1,5 +1,5 @@
import { WebApplicationInterface } from './../../WebApplication/WebApplicationInterface' import { WebApplicationInterface } from './../../WebApplication/WebApplicationInterface'
import { FeatureIdentifier, NoteType } from '@standardnotes/features' import { NativeFeatureIdentifier, NoteType } from '@standardnotes/features'
import { AegisToAuthenticatorConverter } from './AegisToAuthenticatorConverter' import { AegisToAuthenticatorConverter } from './AegisToAuthenticatorConverter'
import data from './testData' import data from './testData'
@@ -57,7 +57,7 @@ describe('AegisConverter', () => {
'[{"service":"TestMail","account":"test@test.com","secret":"TESTMAILTESTMAILTESTMAILTESTMAIL","notes":"Some note"},{"service":"Some Service","account":"test@test.com","secret":"SOMESERVICESOMESERVICESOMESERVIC","notes":"Some other service"}]', '[{"service":"TestMail","account":"test@test.com","secret":"TESTMAILTESTMAILTESTMAILTESTMAIL","notes":"Some note"},{"service":"Some Service","account":"test@test.com","secret":"SOMESERVICESOMESERVICESOMESERVIC","notes":"Some other service"}]',
) )
expect(result.content.noteType).toBe(NoteType.Authentication) expect(result.content.noteType).toBe(NoteType.Authentication)
expect(result.content.editorIdentifier).toBe(FeatureIdentifier.TokenVaultEditor) expect(result.content.editorIdentifier).toBe(NativeFeatureIdentifier.TYPES.TokenVaultEditor)
}) })
it('should create note from entries without editor info', () => { it('should create note from entries without editor info', () => {

View File

@@ -1,6 +1,6 @@
import { DecryptedTransferPayload, NoteContent } from '@standardnotes/models' import { DecryptedTransferPayload, NoteContent } from '@standardnotes/models'
import { readFileAsText } from '../Utils' import { readFileAsText } from '../Utils'
import { FeatureIdentifier, NoteType } from '@standardnotes/features' import { NativeFeatureIdentifier, NoteType } from '@standardnotes/features'
import { WebApplicationInterface } from '../../WebApplication/WebApplicationInterface' import { WebApplicationInterface } from '../../WebApplication/WebApplicationInterface'
import { ContentType } from '@standardnotes/domain-core' import { ContentType } from '@standardnotes/domain-core'
@@ -69,7 +69,7 @@ export class AegisToAuthenticatorConverter {
references: [], references: [],
...(addEditorInfo && { ...(addEditorInfo && {
noteType: NoteType.Authentication, noteType: NoteType.Authentication,
editorIdentifier: FeatureIdentifier.TokenVaultEditor, editorIdentifier: NativeFeatureIdentifier.TYPES.TokenVaultEditor,
}), }),
}, },
} }

View File

@@ -1,6 +1,6 @@
import { parseFileName } from '@standardnotes/filepicker' import { parseFileName } from '@standardnotes/filepicker'
import { FeatureStatus } from '@standardnotes/services' import { FeatureStatus } from '@standardnotes/services'
import { FeatureIdentifier } from '@standardnotes/features' import { NativeFeatureIdentifier } from '@standardnotes/features'
import { AegisToAuthenticatorConverter } from './AegisConverter/AegisToAuthenticatorConverter' import { AegisToAuthenticatorConverter } from './AegisConverter/AegisToAuthenticatorConverter'
import { EvernoteConverter } from './EvernoteConverter/EvernoteConverter' import { EvernoteConverter } from './EvernoteConverter/EvernoteConverter'
import { GoogleKeepConverter } from './GoogleKeepConverter/GoogleKeepConverter' import { GoogleKeepConverter } from './GoogleKeepConverter/GoogleKeepConverter'
@@ -64,7 +64,9 @@ export class Importer {
async getPayloadsFromFile(file: File, type: NoteImportType): Promise<DecryptedTransferPayload[]> { async getPayloadsFromFile(file: File, type: NoteImportType): Promise<DecryptedTransferPayload[]> {
if (type === 'aegis') { if (type === 'aegis') {
const isEntitledToAuthenticator = const isEntitledToAuthenticator =
this.application.features.getFeatureStatus(FeatureIdentifier.TokenVaultEditor) === FeatureStatus.Entitled this.application.features.getFeatureStatus(
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.TokenVaultEditor).getValue(),
) === FeatureStatus.Entitled
return [await this.aegisConverter.convertAegisBackupFileToNote(file, isEntitledToAuthenticator)] return [await this.aegisConverter.convertAegisBackupFileToNote(file, isEntitledToAuthenticator)]
} else if (type === 'google-keep') { } else if (type === 'google-keep') {
return [await this.googleKeepConverter.convertGoogleKeepBackupFileToNote(file, true)] return [await this.googleKeepConverter.convertGoogleKeepBackupFileToNote(file, true)]

View File

@@ -0,0 +1,58 @@
import { ActiveThemeList } from './ActiveThemeList'
import { ItemManagerInterface } from '@standardnotes/services'
import { Uuid } from '@standardnotes/domain-core'
describe('ActiveThemeList', () => {
let itemManager: ItemManagerInterface
let list: ActiveThemeList
beforeEach(() => {
itemManager = {} as ItemManagerInterface
itemManager.findItem = jest.fn()
list = new ActiveThemeList(itemManager)
})
it('should initialize with an empty list', () => {
expect(list.getList()).toEqual([])
})
it('should be empty initially', () => {
expect(list.isEmpty()).toBe(true)
})
it('should not have items that have not been added', () => {
const uuid = Uuid.create('00000000-0000-0000-0000-000000000000').getValue()
expect(list.has(uuid)).toBe(false)
})
it('should add an item to the list', () => {
const uuid = Uuid.create('00000000-0000-0000-0000-000000000000').getValue()
list.add(uuid)
expect(list.getList()).toContain(uuid)
expect(list.has(uuid)).toBe(true)
})
it('should not add a duplicate item to the list', () => {
const uuid = Uuid.create('00000000-0000-0000-0000-000000000000').getValue()
list.add(uuid)
list.add(uuid)
expect(list.getList()).toEqual([uuid])
})
it('should remove an item from the list', () => {
const uuid = Uuid.create('00000000-0000-0000-0000-000000000000').getValue()
list.add(uuid)
list.remove(uuid)
expect(list.getList()).not.toContain(uuid)
expect(list.has(uuid)).toBe(false)
})
it('should clear the list', () => {
const uuid = Uuid.create('00000000-0000-0000-0000-000000000000').getValue()
list.add(uuid)
list.clear()
expect(list.getList()).toEqual([])
expect(list.has(uuid)).toBe(false)
})
})

View File

@@ -0,0 +1,70 @@
import { UIFeature, ThemeInterface } from '@standardnotes/models'
import { ItemManagerInterface } from '@standardnotes/services'
import { NativeFeatureIdentifier, FindNativeTheme, ThemeFeatureDescription } from '@standardnotes/features'
import { Uuid } from '@standardnotes/domain-core'
export class ActiveThemeList {
private list: (NativeFeatureIdentifier | Uuid)[] = []
constructor(private items: ItemManagerInterface, initialList?: (NativeFeatureIdentifier | Uuid)[]) {
if (initialList) {
this.list = initialList
}
}
public getList(): (NativeFeatureIdentifier | Uuid)[] {
return this.list.slice()
}
public isEmpty(): boolean {
return this.list.length === 0
}
public clear(): void {
this.list = []
}
public has(candidate: NativeFeatureIdentifier | Uuid): boolean {
for (const entry of this.list) {
if (entry.equals(candidate)) {
return true
}
}
return false
}
public add(candidate: NativeFeatureIdentifier | Uuid): void {
if (!this.has(candidate)) {
this.list.push(candidate)
}
}
public remove(candidate: NativeFeatureIdentifier | Uuid): void {
this.list = this.list.filter((entry) => {
return !entry.equals(candidate)
})
}
public asThemes(): UIFeature<ThemeFeatureDescription>[] {
const results: UIFeature<ThemeFeatureDescription>[] = []
for (const entry of this.list) {
if (entry instanceof Uuid) {
const theme = this.items.findItem<ThemeInterface>(entry.value)
if (theme) {
const uiFeature = new UIFeature<ThemeFeatureDescription>(theme)
results.push(uiFeature)
}
} else {
const theme = FindNativeTheme(entry.value)
if (theme) {
const uiFeature = new UIFeature<ThemeFeatureDescription>(theme)
results.push(uiFeature)
}
}
}
return results
}
}

View File

@@ -6,7 +6,6 @@ import {
PrefKey, PrefKey,
ThemeInterface, ThemeInterface,
} from '@standardnotes/models' } from '@standardnotes/models'
import { removeFromArray } from '@standardnotes/utils'
import { import {
InternalEventBusInterface, InternalEventBusInterface,
ApplicationEvent, ApplicationEvent,
@@ -16,17 +15,19 @@ import {
PreferencesServiceEvent, PreferencesServiceEvent,
ComponentManagerInterface, ComponentManagerInterface,
} from '@standardnotes/services' } from '@standardnotes/services'
import { FeatureIdentifier, FindNativeTheme, ThemeFeatureDescription } from '@standardnotes/features' import { NativeFeatureIdentifier, FindNativeTheme, ThemeFeatureDescription } from '@standardnotes/features'
import { WebApplicationInterface } from '../WebApplication/WebApplicationInterface' import { WebApplicationInterface } from '../WebApplication/WebApplicationInterface'
import { AbstractUIServicee } from '../Abstract/AbstractUIService' import { AbstractUIServicee } from '../Abstract/AbstractUIService'
import { GetAllThemesUseCase } from './GetAllThemesUseCase' import { GetAllThemesUseCase } from './GetAllThemesUseCase'
import { Uuid } from '@standardnotes/domain-core'
import { ActiveThemeList } from './ActiveThemeList'
const CachedThemesKey = 'cachedThemes' const CachedThemesKey = 'cachedThemes'
const TimeBeforeApplyingColorScheme = 5 const TimeBeforeApplyingColorScheme = 5
const DefaultThemeIdentifier = 'Default' const DefaultThemeIdentifier = 'Default'
export class ThemeManager extends AbstractUIServicee { export class ThemeManager extends AbstractUIServicee {
private themesActiveInTheUI: string[] = [] private themesActiveInTheUI: ActiveThemeList
private lastUseDeviceThemeSettings = false private lastUseDeviceThemeSettings = false
constructor( constructor(
@@ -37,6 +38,23 @@ export class ThemeManager extends AbstractUIServicee {
) { ) {
super(application, internalEventBus) super(application, internalEventBus)
this.colorSchemeEventHandler = this.colorSchemeEventHandler.bind(this) this.colorSchemeEventHandler = this.colorSchemeEventHandler.bind(this)
this.themesActiveInTheUI = new ActiveThemeList(application.items)
}
override deinit() {
this.themesActiveInTheUI.clear()
;(this.themesActiveInTheUI as unknown) = undefined
;(this.preferences as unknown) = undefined
;(this.components as unknown) = undefined
const mq = window.matchMedia('(prefers-color-scheme: dark)')
if (mq.removeEventListener != undefined) {
mq.removeEventListener('change', this.colorSchemeEventHandler)
} else {
mq.removeListener(this.colorSchemeEventHandler)
}
super.deinit()
} }
override async onAppStart() { override async onAppStart() {
@@ -66,20 +84,32 @@ export class ThemeManager extends AbstractUIServicee {
let hasChange = false let hasChange = false
const activeThemes = this.components.getActiveThemesIdentifiers() const { features, uuids } = this.components.getActiveThemesIdentifiers()
for (const uiActiveTheme of this.themesActiveInTheUI) {
if (!activeThemes.includes(uiActiveTheme)) { const featuresList = new ActiveThemeList(this.application.items, features)
this.deactivateThemeInTheUI(uiActiveTheme) const uuidsList = new ActiveThemeList(this.application.items, uuids)
for (const active of this.themesActiveInTheUI.getList()) {
if (!featuresList.has(active) && !uuidsList.has(active)) {
this.deactivateThemeInTheUI(active)
hasChange = true hasChange = true
} }
} }
for (const activeTheme of activeThemes) { for (const feature of features) {
if (!this.themesActiveInTheUI.includes(activeTheme)) { if (!this.themesActiveInTheUI.has(feature)) {
const theme = const theme = FindNativeTheme(feature.value)
FindNativeTheme(activeTheme as FeatureIdentifier) ?? if (theme) {
this.application.items.findItem<ThemeInterface>(activeTheme) const uiFeature = new UIFeature<ThemeFeatureDescription>(theme)
this.activateTheme(uiFeature)
hasChange = true
}
}
}
for (const uuid of uuids) {
if (!this.themesActiveInTheUI.has(uuid)) {
const theme = this.application.items.findItem<ThemeInterface>(uuid.value)
if (theme) { if (theme) {
const uiFeature = new UIFeature<ThemeFeatureDescription>(theme) const uiFeature = new UIFeature<ThemeFeatureDescription>(theme)
this.activateTheme(uiFeature) this.activateTheme(uiFeature)
@@ -99,7 +129,7 @@ export class ThemeManager extends AbstractUIServicee {
switch (event) { switch (event) {
case ApplicationEvent.SignedOut: { case ApplicationEvent.SignedOut: {
this.deactivateAllThemes() this.deactivateAllThemes()
this.themesActiveInTheUI = [] this.themesActiveInTheUI.clear()
this.application?.removeValue(CachedThemesKey, StorageValueModes.Nonwrapped).catch(console.error) this.application?.removeValue(CachedThemesKey, StorageValueModes.Nonwrapped).catch(console.error)
break break
} }
@@ -158,35 +188,13 @@ export class ThemeManager extends AbstractUIServicee {
} }
} }
override deinit() {
this.themesActiveInTheUI = []
const mq = window.matchMedia('(prefers-color-scheme: dark)')
if (mq.removeEventListener != undefined) {
mq.removeEventListener('change', this.colorSchemeEventHandler)
} else {
mq.removeListener(this.colorSchemeEventHandler)
}
super.deinit()
}
private handleFeaturesAvailabilityChanged(): void { private handleFeaturesAvailabilityChanged(): void {
let hasChange = false let hasChange = false
for (const themeUuid of this.themesActiveInTheUI) { for (const theme of this.themesActiveInTheUI.asThemes()) {
const theme = this.application.items.findItem<ThemeInterface>(themeUuid) const status = this.application.features.getFeatureStatus(theme.uniqueIdentifier)
if (!theme) {
this.deactivateThemeInTheUI(themeUuid)
hasChange = true
continue
}
const status = this.application.features.getFeatureStatus(theme.identifier)
if (status !== FeatureStatus.Entitled) { if (status !== FeatureStatus.Entitled) {
this.deactivateThemeInTheUI(theme.uuid) this.deactivateThemeInTheUI(theme.uniqueIdentifier)
hasChange = true hasChange = true
} }
} }
@@ -194,7 +202,7 @@ export class ThemeManager extends AbstractUIServicee {
const activeThemes = this.components.getActiveThemes() const activeThemes = this.components.getActiveThemes()
for (const theme of activeThemes) { for (const theme of activeThemes) {
if (!this.themesActiveInTheUI.includes(theme.uniqueIdentifier)) { if (!this.themesActiveInTheUI.has(theme.uniqueIdentifier)) {
this.activateTheme(theme) this.activateTheme(theme)
hasChange = true hasChange = true
} }
@@ -245,7 +253,7 @@ export class ThemeManager extends AbstractUIServicee {
const preference = prefersDarkColorScheme ? PrefKey.AutoDarkThemeIdentifier : PrefKey.AutoLightThemeIdentifier const preference = prefersDarkColorScheme ? PrefKey.AutoDarkThemeIdentifier : PrefKey.AutoLightThemeIdentifier
const preferenceDefault = const preferenceDefault =
preference === PrefKey.AutoDarkThemeIdentifier ? FeatureIdentifier.DarkTheme : DefaultThemeIdentifier preference === PrefKey.AutoDarkThemeIdentifier ? NativeFeatureIdentifier.TYPES.DarkTheme : DefaultThemeIdentifier
const usecase = new GetAllThemesUseCase(this.application.items) const usecase = new GetAllThemesUseCase(this.application.items)
const { thirdParty, native } = usecase.execute({ excludeLayerable: false }) const { thirdParty, native } = usecase.execute({ excludeLayerable: false })
@@ -289,21 +297,20 @@ export class ThemeManager extends AbstractUIServicee {
} }
private deactivateAllThemes() { private deactivateAllThemes() {
const activeThemes = this.themesActiveInTheUI.slice() const activeThemes = this.themesActiveInTheUI.getList()
for (const uuid of activeThemes) { for (const uuid of activeThemes) {
this.deactivateThemeInTheUI(uuid) this.deactivateThemeInTheUI(uuid)
} }
} }
private activateTheme(theme: UIFeature<ThemeFeatureDescription>, skipEntitlementCheck = false) { private activateTheme(theme: UIFeature<ThemeFeatureDescription>, skipEntitlementCheck = false) {
if (this.themesActiveInTheUI.find((uuid) => uuid === theme.uniqueIdentifier)) { if (this.themesActiveInTheUI.has(theme.uniqueIdentifier)) {
return return
} }
if ( if (
!skipEntitlementCheck && !skipEntitlementCheck &&
this.application.features.getFeatureStatus(theme.featureIdentifier) !== FeatureStatus.Entitled this.application.features.getFeatureStatus(theme.uniqueIdentifier) !== FeatureStatus.Entitled
) { ) {
return return
} }
@@ -313,14 +320,14 @@ export class ThemeManager extends AbstractUIServicee {
return return
} }
this.themesActiveInTheUI.push(theme.uniqueIdentifier) this.themesActiveInTheUI.add(theme.uniqueIdentifier)
const link = document.createElement('link') const link = document.createElement('link')
link.href = url link.href = url
link.type = 'text/css' link.type = 'text/css'
link.rel = 'stylesheet' link.rel = 'stylesheet'
link.media = 'screen,print' link.media = 'screen,print'
link.id = theme.uniqueIdentifier link.id = theme.uniqueIdentifier.value
link.onload = () => { link.onload = () => {
this.syncThemeColorMetadata() this.syncThemeColorMetadata()
@@ -336,20 +343,20 @@ export class ThemeManager extends AbstractUIServicee {
document.getElementsByTagName('head')[0].appendChild(link) document.getElementsByTagName('head')[0].appendChild(link)
} }
private deactivateThemeInTheUI(uuid: string) { private deactivateThemeInTheUI(id: NativeFeatureIdentifier | Uuid) {
if (!this.themesActiveInTheUI.includes(uuid)) { if (!this.themesActiveInTheUI.has(id)) {
return return
} }
const element = document.getElementById(uuid) as HTMLLinkElement const element = document.getElementById(id.value) as HTMLLinkElement
if (element) { if (element) {
element.disabled = true element.disabled = true
element.parentNode?.removeChild(element) element.parentNode?.removeChild(element)
} }
removeFromArray(this.themesActiveInTheUI, uuid) this.themesActiveInTheUI.remove(id)
if (this.themesActiveInTheUI.length === 0 && this.application.isNativeMobileWeb()) { if (this.themesActiveInTheUI.isEmpty() && this.application.isNativeMobileWeb()) {
this.application.mobileDevice().handleThemeSchemeChange(false, '#ffffff') this.application.mobileDevice().handleThemeSchemeChange(false, '#ffffff')
} }
} }
@@ -373,11 +380,16 @@ export class ThemeManager extends AbstractUIServicee {
} }
private async cacheThemeState() { private async cacheThemeState() {
const themes = this.application.items.findItems<ThemeInterface>(this.themesActiveInTheUI) const themes = this.themesActiveInTheUI.asThemes()
const mapped = themes.map((theme) => { const mapped = themes.map((theme) => {
const payload = theme.payloadRepresentation() if (theme.isComponent) {
return CreateDecryptedLocalStorageContextPayload(payload) const payload = theme.asComponent.payloadRepresentation()
return CreateDecryptedLocalStorageContextPayload(payload)
} else {
const payload = theme.asFeatureDescription
return payload
}
}) })
return this.application.setValue(CachedThemesKey, mapped, StorageValueModes.Nonwrapped) return this.application.setValue(CachedThemesKey, mapped, StorageValueModes.Nonwrapped)
@@ -389,16 +401,25 @@ export class ThemeManager extends AbstractUIServicee {
StorageValueModes.Nonwrapped, StorageValueModes.Nonwrapped,
) )
if (cachedThemes) { if (!cachedThemes) {
const themes: ThemeInterface[] = []
for (const cachedTheme of cachedThemes) {
const payload = this.application.items.createPayloadFromObject(cachedTheme)
const theme = this.application.items.createItemFromPayload<ThemeInterface>(payload)
themes.push(theme)
}
return themes.map((theme) => new UIFeature<ThemeFeatureDescription>(theme))
} else {
return [] return []
} }
const features: UIFeature<ThemeFeatureDescription>[] = []
for (const cachedTheme of cachedThemes) {
if ('uuid' in cachedTheme) {
const payload = this.application.items.createPayloadFromObject(cachedTheme)
const theme = this.application.items.createItemFromPayload<ThemeInterface>(payload)
features.push(new UIFeature<ThemeFeatureDescription>(theme))
} else if ('identifier' in cachedTheme) {
const feature = FindNativeTheme((cachedTheme as ThemeFeatureDescription).identifier)
if (feature) {
features.push(new UIFeature<ThemeFeatureDescription>(feature))
}
}
}
return features
} }
} }

View File

@@ -22,7 +22,7 @@
"prebuild": "yarn clean", "prebuild": "yarn clean",
"build": "tsc -p tsconfig.json", "build": "tsc -p tsconfig.json",
"lint": "eslint src --ext .ts", "lint": "eslint src --ext .ts",
"test": "jest spec" "test": "jest"
}, },
"dependencies": { "dependencies": {
"@standardnotes/common": "^1.50.0", "@standardnotes/common": "^1.50.0",

View File

@@ -1,11 +1,46 @@
import * as DOMPurifyLib from 'dompurify' import * as DOMPurifyLib from 'dompurify'
import { JSDOM } from 'jsdom' import { JSDOM } from 'jsdom'
import { sortByKey, withoutLastElement } from './Utils' import { sortByKey, withoutLastElement, compareArrayReferences } from './Utils'
const window = new JSDOM('').window const window = new JSDOM('').window
const DOMPurify = DOMPurifyLib(window as never) const DOMPurify = DOMPurifyLib(window as never)
describe('Utils', () => { describe('Utils', () => {
describe('compareArrayReferences', () => {
it('should return true when both arrays are empty', () => {
expect(compareArrayReferences([], [])).toBe(true)
})
it('should return true when both arrays have the same reference', () => {
const obj = {}
expect(compareArrayReferences([obj], [obj])).toBe(true)
})
it('should return false when arrays have different lengths', () => {
const obj1 = {}
const obj2 = {}
expect(compareArrayReferences([obj1], [obj1, obj2])).toBe(false)
})
it('should return false when arrays have the same length but different references', () => {
const obj1 = {}
const obj2 = {}
expect(compareArrayReferences([obj1], [obj2])).toBe(false)
})
it('should return true when arrays have multiple identical references', () => {
const obj1 = {}
const obj2 = {}
expect(compareArrayReferences([obj1, obj2], [obj1, obj2])).toBe(true)
})
it('should return false when arrays have the same references in different order', () => {
const obj1 = {}
const obj2 = {}
expect(compareArrayReferences([obj1, obj2], [obj2, obj1])).toBe(false)
})
})
it('sanitizeHtmlString', () => { it('sanitizeHtmlString', () => {
const dirty = '<svg><animate onbegin=alert(1) attributeName=x dur=1s>' const dirty = '<svg><animate onbegin=alert(1) attributeName=x dur=1s>'
const cleaned = DOMPurify.sanitize(dirty) const cleaned = DOMPurify.sanitize(dirty)

View File

@@ -223,6 +223,10 @@ export function arrayByDifference<T>(array: T[], subtract: T[]): T[] {
return array.filter((x) => !subtract.includes(x)).concat(subtract.filter((x) => !array.includes(x))) return array.filter((x) => !subtract.includes(x)).concat(subtract.filter((x) => !array.includes(x)))
} }
export function compareArrayReferences<T>(arr1: T[], arr2: T[]) {
return arr1.length === arr2.length && arr1.every((val, index) => val === arr2[index])
}
export function compareValues<T>(left: T, right: T) { export function compareValues<T>(left: T, right: T) {
if ((left && !right) || (!left && right)) { if ((left && !right) || (!left && right)) {
return false return false

View File

@@ -6,7 +6,7 @@ import { WebApplication } from '@/Application/WebApplication'
import { import {
UIFeature, UIFeature,
EditorFeatureDescription, EditorFeatureDescription,
FeatureIdentifier, NativeFeatureIdentifier,
IframeComponentFeatureDescription, IframeComponentFeatureDescription,
NoteMutator, NoteMutator,
NoteType, NoteType,
@@ -100,7 +100,7 @@ const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
setCurrentFeature(application.componentManager.editorForNote(note)) setCurrentFeature(application.componentManager.editorForNote(note))
if (uiFeature.featureIdentifier === FeatureIdentifier.PlainEditor) { if (uiFeature.featureIdentifier === NativeFeatureIdentifier.TYPES.PlainEditor) {
reloadFont(application.getPreference(PrefKey.EditorMonospaceEnabled)) reloadFont(application.getPreference(PrefKey.EditorMonospaceEnabled))
} }
}, },
@@ -211,7 +211,7 @@ const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
return ( return (
<MenuRadioButtonItem <MenuRadioButtonItem
key={menuItem.uiFeature.uniqueIdentifier} key={menuItem.uiFeature.uniqueIdentifier.value}
onClick={onClickEditorItem} onClick={onClickEditorItem}
className={'flex-row-reversed py-2'} className={'flex-row-reversed py-2'}
checked={isSelected(menuItem)} checked={isSelected(menuItem)}

View File

@@ -141,7 +141,7 @@ const ChangeEditorMultipleMenu = ({ application, notes, setDisableClickOutside }
} }
return ( return (
<MenuItem <MenuItem
key={item.uiFeature.uniqueIdentifier} key={item.uiFeature.uniqueIdentifier.value}
onClick={onClickEditorItem} onClick={onClickEditorItem}
className={'flex-row-reversed py-2'} className={'flex-row-reversed py-2'}
> >

View File

@@ -15,7 +15,7 @@ import {
ApplicationEvent, ApplicationEvent,
ContentType, ContentType,
DecryptedItem, DecryptedItem,
FeatureIdentifier, NativeFeatureIdentifier,
FeatureStatus, FeatureStatus,
NoteContent, NoteContent,
NoteType, NoteType,
@@ -61,7 +61,10 @@ const ClipperView = ({
const [user, setUser] = useState(() => application.getUser()) const [user, setUser] = useState(() => application.getUser())
const [isEntitledToExtension, setIsEntitled] = useState( const [isEntitledToExtension, setIsEntitled] = useState(
() => application.features.getFeatureStatus(FeatureIdentifier.Extension) === FeatureStatus.Entitled, () =>
application.features.getFeatureStatus(
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.Clipper).getValue(),
) === FeatureStatus.Entitled,
) )
const isEntitledRef = useStateRef(isEntitledToExtension) const isEntitledRef = useStateRef(isEntitledToExtension)
const hasSubscription = application.hasValidFirstPartySubscription() const hasSubscription = application.hasValidFirstPartySubscription()
@@ -72,10 +75,18 @@ const ClipperView = ({
case ApplicationEvent.SignedOut: case ApplicationEvent.SignedOut:
case ApplicationEvent.UserRolesChanged: case ApplicationEvent.UserRolesChanged:
setUser(application.getUser()) setUser(application.getUser())
setIsEntitled(application.features.getFeatureStatus(FeatureIdentifier.Extension) === FeatureStatus.Entitled) setIsEntitled(
application.features.getFeatureStatus(
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.Clipper).getValue(),
) === FeatureStatus.Entitled,
)
break break
case ApplicationEvent.FeaturesAvailabilityChanged: case ApplicationEvent.FeaturesAvailabilityChanged:
setIsEntitled(application.features.getFeatureStatus(FeatureIdentifier.Extension) === FeatureStatus.Entitled) setIsEntitled(
application.features.getFeatureStatus(
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.Clipper).getValue(),
) === FeatureStatus.Entitled,
)
break break
} }
}) })
@@ -212,7 +223,7 @@ const ClipperView = ({
const note = application.items.createTemplateItem<NoteContent, SNNote>(ContentType.TYPES.Note, { const note = application.items.createTemplateItem<NoteContent, SNNote>(ContentType.TYPES.Note, {
title: clipPayload.title, title: clipPayload.title,
text: editorStateJSON, text: editorStateJSON,
editorIdentifier: FeatureIdentifier.SuperEditor, editorIdentifier: NativeFeatureIdentifier.TYPES.SuperEditor,
noteType: NoteType.Super, noteType: NoteType.Super,
references: [], references: [],
}) })

View File

@@ -168,7 +168,7 @@ const IframeFeatureView: FunctionComponent<Props> = ({ onLoad, componentViewer,
const unregisterDesktopObserver = application const unregisterDesktopObserver = application
.getDesktopService() .getDesktopService()
?.registerUpdateObserver((updatedComponent: ComponentInterface) => { ?.registerUpdateObserver((updatedComponent: ComponentInterface) => {
if (updatedComponent.uuid === uiFeature.uniqueIdentifier) { if (updatedComponent.uuid === uiFeature.uniqueIdentifier.value) {
requestReload?.(componentViewer) requestReload?.(componentViewer)
} }
}) })

View File

@@ -1,14 +1,14 @@
import { import {
FeatureIdentifier, NativeFeatureIdentifier,
NewNoteTitleFormat, NewNoteTitleFormat,
PrefKey, PrefKey,
EditorIdentifier,
TagPreferences, TagPreferences,
isSmartView, isSmartView,
isSystemView, isSystemView,
SystemViewId, SystemViewId,
PrefDefaults, PrefDefaults,
FeatureStatus, FeatureStatus,
Uuid,
} from '@standardnotes/snjs' } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { ChangeEventHandler, FunctionComponent, useCallback, useEffect, useRef, useState } from 'react' import { ChangeEventHandler, FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'
@@ -17,6 +17,11 @@ import { DropdownItem } from '@/Components/Dropdown/DropdownItem'
import { WebApplication } from '@/Application/WebApplication' import { WebApplication } from '@/Application/WebApplication'
import { AnyTag } from '@/Controllers/Navigation/AnyTagType' import { AnyTag } from '@/Controllers/Navigation/AnyTagType'
import { PreferenceMode } from './PreferenceMode' import { PreferenceMode } from './PreferenceMode'
import { EditorOption, getDropdownItemsForAllEditors } from '@/Utils/DropdownItemsForEditors'
import { classNames } from '@standardnotes/utils'
import { NoteTitleFormatOptions } from './NoteTitleFormatOptions'
import { usePremiumModal } from '@/Hooks/usePremiumModal'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import dayjsAdvancedFormat from 'dayjs/plugin/advancedFormat' import dayjsAdvancedFormat from 'dayjs/plugin/advancedFormat'
import dayjsUTC from 'dayjs/plugin/utc' import dayjsUTC from 'dayjs/plugin/utc'
@@ -24,11 +29,6 @@ import dayjsTimezone from 'dayjs/plugin/timezone'
dayjs.extend(dayjsAdvancedFormat) dayjs.extend(dayjsAdvancedFormat)
dayjs.extend(dayjsUTC) dayjs.extend(dayjsUTC)
dayjs.extend(dayjsTimezone) dayjs.extend(dayjsTimezone)
import { EditorOption, getDropdownItemsForAllEditors } from '@/Utils/DropdownItemsForEditors'
import { classNames } from '@standardnotes/utils'
import { NoteTitleFormatOptions } from './NoteTitleFormatOptions'
import { usePremiumModal } from '@/Hooks/usePremiumModal'
const PrefChangeDebounceTimeInMs = 25 const PrefChangeDebounceTimeInMs = 25
@@ -57,8 +57,8 @@ const NewNotePreferences: FunctionComponent<Props> = ({
: selectedTag.preferences : selectedTag.preferences
const [editorItems, setEditorItems] = useState<DropdownItem[]>([]) const [editorItems, setEditorItems] = useState<DropdownItem[]>([])
const [defaultEditorIdentifier, setDefaultEditorIdentifier] = useState<EditorIdentifier>( const [defaultEditorIdentifier, setDefaultEditorIdentifier] = useState<string>(
FeatureIdentifier.PlainEditor, NativeFeatureIdentifier.TYPES.PlainEditor,
) )
const [newNoteTitleFormat, setNewNoteTitleFormat] = useState<NewNoteTitleFormat>( const [newNoteTitleFormat, setNewNoteTitleFormat] = useState<NewNoteTitleFormat>(
NewNoteTitleFormat.CurrentDateAndTime, NewNoteTitleFormat.CurrentDateAndTime,
@@ -121,14 +121,19 @@ const NewNotePreferences: FunctionComponent<Props> = ({
const selectEditorForNewNoteDefault = useCallback( const selectEditorForNewNoteDefault = useCallback(
(value: EditorOption['value']) => { (value: EditorOption['value']) => {
if (application.features.getFeatureStatus(value) !== FeatureStatus.Entitled) { const uuid = Uuid.create(value)
const feature = NativeFeatureIdentifier.create(value)
if (
application.features.getFeatureStatus(!uuid.isFailed() ? uuid.getValue() : feature.getValue()) !==
FeatureStatus.Entitled
) {
const editorItem = editorItems.find((item) => item.value === value) const editorItem = editorItems.find((item) => item.value === value)
if (editorItem) { if (editorItem) {
premiumModal.activate(editorItem.label) premiumModal.activate(editorItem.label)
} }
return return
} }
setDefaultEditorIdentifier(value as FeatureIdentifier) setDefaultEditorIdentifier(value)
if (mode === 'global') { if (mode === 'global') {
void application.setPreference(PrefKey.DefaultEditorIdentifier, value) void application.setPreference(PrefKey.DefaultEditorIdentifier, value)

View File

@@ -1,7 +1,6 @@
import { WebApplication } from '@/Application/WebApplication' import { WebApplication } from '@/Application/WebApplication'
import { ContentType } from '@standardnotes/domain-core' import { ContentType } from '@standardnotes/domain-core'
import { import {
MutatorService,
SNComponentManager, SNComponentManager,
SNComponent, SNComponent,
SNTag, SNTag,
@@ -11,7 +10,7 @@ import {
ItemManagerInterface, ItemManagerInterface,
MutatorClientInterface, MutatorClientInterface,
} from '@standardnotes/snjs' } from '@standardnotes/snjs'
import { FeatureIdentifier, NoteType } from '@standardnotes/features' import { NativeFeatureIdentifier, NoteType } from '@standardnotes/features'
import { NoteViewController } from './NoteViewController' import { NoteViewController } from './NoteViewController'
describe('note view controller', () => { describe('note view controller', () => {
@@ -40,7 +39,9 @@ describe('note view controller', () => {
}) })
it('should create notes with plaintext note type', async () => { it('should create notes with plaintext note type', async () => {
application.componentManager.getDefaultEditorIdentifier = jest.fn().mockReturnValue(FeatureIdentifier.PlainEditor) application.componentManager.getDefaultEditorIdentifier = jest
.fn()
.mockReturnValue(NativeFeatureIdentifier.TYPES.PlainEditor)
const controller = new NoteViewController(application) const controller = new NoteViewController(application)
await controller.initialize() await controller.initialize()
@@ -55,13 +56,13 @@ describe('note view controller', () => {
it('should create notes with markdown note type', async () => { it('should create notes with markdown note type', async () => {
application.items.getDisplayableComponents = jest.fn().mockReturnValue([ application.items.getDisplayableComponents = jest.fn().mockReturnValue([
{ {
identifier: FeatureIdentifier.MarkdownProEditor, identifier: NativeFeatureIdentifier.TYPES.MarkdownProEditor,
} as SNComponent, } as SNComponent,
]) ])
application.componentManager.getDefaultEditorIdentifier = jest application.componentManager.getDefaultEditorIdentifier = jest
.fn() .fn()
.mockReturnValue(FeatureIdentifier.MarkdownProEditor) .mockReturnValue(NativeFeatureIdentifier.TYPES.MarkdownProEditor)
const controller = new NoteViewController(application) const controller = new NoteViewController(application)
await controller.initialize() await controller.initialize()
@@ -74,7 +75,9 @@ describe('note view controller', () => {
}) })
it('should add tag to note if default tag is set', async () => { it('should add tag to note if default tag is set', async () => {
application.componentManager.getDefaultEditorIdentifier = jest.fn().mockReturnValue(FeatureIdentifier.PlainEditor) application.componentManager.getDefaultEditorIdentifier = jest
.fn()
.mockReturnValue(NativeFeatureIdentifier.TYPES.PlainEditor)
const tag = { const tag = {
uuid: 'tag-uuid', uuid: 'tag-uuid',

View File

@@ -8,7 +8,7 @@ import { ElementIds } from '@/Constants/ElementIDs'
import { StringDeleteNote, STRING_DELETE_LOCKED_ATTEMPT, STRING_DELETE_PLACEHOLDER_ATTEMPT } from '@/Constants/Strings' import { StringDeleteNote, STRING_DELETE_LOCKED_ATTEMPT, STRING_DELETE_PLACEHOLDER_ATTEMPT } from '@/Constants/Strings'
import { log, LoggingDomain } from '@/Logging' import { log, LoggingDomain } from '@/Logging'
import { debounce, isDesktopApplication, isMobileScreen } from '@/Utils' import { debounce, isDesktopApplication, isMobileScreen } from '@/Utils'
import { classNames, pluralize } from '@standardnotes/utils' import { classNames, compareArrayReferences, pluralize } from '@standardnotes/utils'
import { import {
ApplicationEvent, ApplicationEvent,
ComponentArea, ComponentArea,
@@ -19,7 +19,6 @@ import {
EditorLineWidth, EditorLineWidth,
IframeComponentFeatureDescription, IframeComponentFeatureDescription,
isUIFeatureAnIframeFeature, isUIFeatureAnIframeFeature,
isPayloadSourceInternalChange,
isPayloadSourceRetrieved, isPayloadSourceRetrieved,
NoteType, NoteType,
PayloadEmitSource, PayloadEmitSource,
@@ -94,7 +93,6 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
onEditorComponentLoad?: () => void onEditorComponentLoad?: () => void
private removeTrashKeyObserver?: () => void private removeTrashKeyObserver?: () => void
private removeComponentStreamObserver?: () => void
private removeNoteStreamObserver?: () => void private removeNoteStreamObserver?: () => void
private removeComponentManagerObserver?: () => void private removeComponentManagerObserver?: () => void
private removeInnerNoteObserver?: () => void private removeInnerNoteObserver?: () => void
@@ -144,9 +142,6 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
super.deinit() super.deinit()
;(this.controller as unknown) = undefined ;(this.controller as unknown) = undefined
this.removeComponentStreamObserver?.()
;(this.removeComponentStreamObserver as unknown) = undefined
this.removeNoteStreamObserver?.() this.removeNoteStreamObserver?.()
;(this.removeNoteStreamObserver as unknown) = undefined ;(this.removeNoteStreamObserver as unknown) = undefined
@@ -187,14 +182,19 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
} }
override shouldComponentUpdate(_nextProps: Readonly<NoteViewProps>, nextState: Readonly<State>): boolean { override shouldComponentUpdate(_nextProps: Readonly<NoteViewProps>, nextState: Readonly<State>): boolean {
const complexObjects: (keyof State)[] = ['availableStackComponents', 'stackComponentViewers']
for (const key of Object.keys(nextState) as (keyof State)[]) { for (const key of Object.keys(nextState) as (keyof State)[]) {
if (complexObjects.includes(key)) {
continue
}
const prevValue = this.state[key] const prevValue = this.state[key]
const nextValue = nextState[key] const nextValue = nextState[key]
if (Array.isArray(prevValue) && Array.isArray(nextValue)) {
const areEqual = compareArrayReferences<unknown>(prevValue, nextValue)
if (!areEqual) {
log(LoggingDomain.NoteView, 'Rendering due to array state change', key, prevValue, nextValue)
return true
}
continue
}
if (prevValue !== nextValue) { if (prevValue !== nextValue) {
log(LoggingDomain.NoteView, 'Rendering due to state change', key, prevValue, nextValue) log(LoggingDomain.NoteView, 'Rendering due to state change', key, prevValue, nextValue)
return true return true
@@ -340,7 +340,8 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
switch (eventName) { switch (eventName) {
case ApplicationEvent.PreferencesChanged: case ApplicationEvent.PreferencesChanged:
this.reloadPreferences().catch(console.error) void this.reloadPreferences()
void this.reloadStackComponents()
break break
case ApplicationEvent.HighLatencySync: case ApplicationEvent.HighLatencySync:
this.setState({ syncTakingTooLong: true }) this.setState({ syncTakingTooLong: true })
@@ -428,23 +429,6 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
} }
streamItems() { streamItems() {
this.removeComponentStreamObserver = this.application.streamItems(
ContentType.TYPES.Component,
async ({ source }) => {
log(LoggingDomain.NoteView, 'On component stream observer', PayloadEmitSource[source])
if (isPayloadSourceInternalChange(source) || source === PayloadEmitSource.InitialObserverRegistrationPush) {
return
}
if (!this.note) {
return
}
await this.reloadStackComponents()
this.debounceReloadEditorComponent()
},
)
this.removeNoteStreamObserver = this.application.streamItems<SNNote>(ContentType.TYPES.Note, async () => { this.removeNoteStreamObserver = this.application.streamItems<SNNote>(ContentType.TYPES.Note, async () => {
if (!this.note) { if (!this.note) {
return return
@@ -740,25 +724,22 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
async reloadStackComponents() { async reloadStackComponents() {
log(LoggingDomain.NoteView, 'Reload stack components') log(LoggingDomain.NoteView, 'Reload stack components')
const stackComponents = sortAlphabetically( const enabledComponents = sortAlphabetically(
this.application.componentManager this.application.componentManager
.thirdPartyComponentsForArea(ComponentArea.EditorStack) .thirdPartyComponentsForArea(ComponentArea.EditorStack)
.filter((component) => this.application.componentManager.isComponentActive(component)), .filter((component) => this.application.componentManager.isComponentActive(component)),
) )
const enabledComponents = stackComponents.filter((component) => {
return component.isExplicitlyEnabledForItem(this.note.uuid)
})
const needsNewViewer = enabledComponents.filter((component) => { const needsNewViewer = enabledComponents.filter((component) => {
const hasExistingViewer = this.state.stackComponentViewers.find( const hasExistingViewer = this.state.stackComponentViewers.find(
(viewer) => viewer.componentUniqueIdentifier === component.uuid, (viewer) => viewer.componentUniqueIdentifier.value === component.uuid,
) )
return !hasExistingViewer return !hasExistingViewer
}) })
const needsDestroyViewer = this.state.stackComponentViewers.filter((viewer) => { const needsDestroyViewer = this.state.stackComponentViewers.filter((viewer) => {
const viewerComponentExistsInEnabledComponents = enabledComponents.find((component) => { const viewerComponentExistsInEnabledComponents = enabledComponents.find((component) => {
return component.uuid === viewer.componentUniqueIdentifier return component.uuid === viewer.componentUniqueIdentifier.value
}) })
return !viewerComponentExistsInEnabledComponents return !viewerComponentExistsInEnabledComponents
}) })
@@ -779,13 +760,15 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
this.application.componentManager.destroyComponentViewer(viewer) this.application.componentManager.destroyComponentViewer(viewer)
} }
this.setState({ this.setState({
availableStackComponents: stackComponents, availableStackComponents: enabledComponents,
stackComponentViewers: newViewers, stackComponentViewers: newViewers,
}) })
} }
stackComponentExpanded = (component: ComponentInterface): boolean => { stackComponentExpanded = (component: ComponentInterface): boolean => {
return !!this.state.stackComponentViewers.find((viewer) => viewer.componentUniqueIdentifier === component.uuid) return !!this.state.stackComponentViewers.find(
(viewer) => viewer.componentUniqueIdentifier.value === component.uuid,
)
} }
toggleStackComponent = async (component: ComponentInterface) => { toggleStackComponent = async (component: ComponentInterface) => {

View File

@@ -1,5 +1,5 @@
import { import {
FeatureIdentifier, NativeFeatureIdentifier,
FeatureStatus, FeatureStatus,
MuteMarketingEmailsOption, MuteMarketingEmailsOption,
MuteSignInEmailsOption, MuteSignInEmailsOption,
@@ -28,7 +28,9 @@ const Email: FunctionComponent<Props> = ({ application }: Props) => {
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
const isMuteSignInEmailsFeatureAvailable = const isMuteSignInEmailsFeatureAvailable =
application.features.getFeatureStatus(FeatureIdentifier.SignInAlerts) === FeatureStatus.Entitled application.features.getFeatureStatus(
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.SignInAlerts).getValue(),
) === FeatureStatus.Entitled
const updateSetting = async (settingName: SettingName, payload: string): Promise<boolean> => { const updateSetting = async (settingName: SettingName, payload: string): Promise<boolean> => {
try { try {

View File

@@ -1,4 +1,4 @@
import { FeatureStatus, FeatureIdentifier } from '@standardnotes/snjs' import { FeatureStatus, NativeFeatureIdentifier } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { FunctionComponent, useState } from 'react' import { FunctionComponent, useState } from 'react'
@@ -29,8 +29,9 @@ const SubscriptionSharing: FunctionComponent<Props> = ({ application, viewContro
const isReadOnlySession = application.sessions.isCurrentSessionReadOnly() const isReadOnlySession = application.sessions.isCurrentSessionReadOnly()
const isSubscriptionSharingFeatureAvailable = const isSubscriptionSharingFeatureAvailable =
application.features.getFeatureStatus(FeatureIdentifier.SubscriptionSharing) === FeatureStatus.Entitled && application.features.getFeatureStatus(
!isReadOnlySession NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.SubscriptionSharing).getValue(),
) === FeatureStatus.Entitled && !isReadOnlySession
const closeInviteDialog = () => setIsInviteDialogOpen(false) const closeInviteDialog = () => setIsInviteDialogOpen(false)

View File

@@ -4,7 +4,7 @@ import { usePremiumModal } from '@/Hooks/usePremiumModal'
import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator' import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator'
import Switch from '@/Components/Switch/Switch' import Switch from '@/Components/Switch/Switch'
import { WebApplication } from '@/Application/WebApplication' import { WebApplication } from '@/Application/WebApplication'
import { FeatureIdentifier, PrefKey, FeatureStatus, naturalSort, PrefDefaults } from '@standardnotes/snjs' import { PrefKey, FeatureStatus, naturalSort, PrefDefaults } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { FunctionComponent, useEffect, useState } from 'react' import { FunctionComponent, useEffect, useState } from 'react'
import { Subtitle, Title, Text } from '@/Components/Preferences/PreferencesComponents/Content' import { Subtitle, Title, Text } from '@/Components/Preferences/PreferencesComponents/Content'
@@ -50,7 +50,7 @@ const Appearance: FunctionComponent<Props> = ({ application }) => {
label: theme.displayName as string, label: theme.displayName as string,
value: theme.featureIdentifier, value: theme.featureIdentifier,
icon: icon:
application.features.getFeatureStatus(theme.featureIdentifier) !== FeatureStatus.Entitled application.features.getFeatureStatus(theme.uniqueIdentifier) !== FeatureStatus.Entitled
? PremiumFeatureIconName ? PremiumFeatureIconName
: undefined, : undefined,
} }
@@ -72,14 +72,10 @@ const Appearance: FunctionComponent<Props> = ({ application }) => {
const toggleUseDeviceSettings = () => { const toggleUseDeviceSettings = () => {
application.setPreference(PrefKey.UseSystemColorScheme, !useDeviceSettings).catch(console.error) application.setPreference(PrefKey.UseSystemColorScheme, !useDeviceSettings).catch(console.error)
if (!application.getPreference(PrefKey.AutoLightThemeIdentifier)) { if (!application.getPreference(PrefKey.AutoLightThemeIdentifier)) {
application application.setPreference(PrefKey.AutoLightThemeIdentifier, autoLightTheme).catch(console.error)
.setPreference(PrefKey.AutoLightThemeIdentifier, autoLightTheme as FeatureIdentifier)
.catch(console.error)
} }
if (!application.getPreference(PrefKey.AutoDarkThemeIdentifier)) { if (!application.getPreference(PrefKey.AutoDarkThemeIdentifier)) {
application application.setPreference(PrefKey.AutoDarkThemeIdentifier, autoDarkTheme).catch(console.error)
.setPreference(PrefKey.AutoDarkThemeIdentifier, autoDarkTheme as FeatureIdentifier)
.catch(console.error)
} }
setUseDeviceSettings(!useDeviceSettings) setUseDeviceSettings(!useDeviceSettings)
} }
@@ -90,7 +86,7 @@ const Appearance: FunctionComponent<Props> = ({ application }) => {
premiumModal.activate(`${item.label} theme`) premiumModal.activate(`${item.label} theme`)
return return
} }
application.setPreference(PrefKey.AutoLightThemeIdentifier, value as FeatureIdentifier).catch(console.error) application.setPreference(PrefKey.AutoLightThemeIdentifier, value).catch(console.error)
setAutoLightTheme(value) setAutoLightTheme(value)
} }
@@ -100,7 +96,7 @@ const Appearance: FunctionComponent<Props> = ({ application }) => {
premiumModal.activate(`${item.label} theme`) premiumModal.activate(`${item.label} theme`)
return return
} }
application.setPreference(PrefKey.AutoDarkThemeIdentifier, value as FeatureIdentifier).catch(console.error) application.setPreference(PrefKey.AutoDarkThemeIdentifier, value).catch(console.error)
setAutoDarkTheme(value) setAutoDarkTheme(value)
} }

View File

@@ -2,7 +2,7 @@ import { Text, Title } from '@/Components/Preferences/PreferencesComponents/Cont
import { WebApplication } from '@/Application/WebApplication' import { WebApplication } from '@/Application/WebApplication'
import { import {
ApplicationEvent, ApplicationEvent,
FeatureIdentifier, NativeFeatureIdentifier,
FeatureStatus, FeatureStatus,
FindNativeFeature, FindNativeFeature,
PrefKey, PrefKey,
@@ -17,7 +17,7 @@ import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator'
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery' import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
type ExperimentalFeatureItem = { type ExperimentalFeatureItem = {
identifier: FeatureIdentifier identifier: string
name: string name: string
description: string description: string
isEnabled: boolean isEnabled: boolean
@@ -55,7 +55,9 @@ const LabsPane: FunctionComponent<Props> = ({ application }) => {
name: feature?.name ?? featureIdentifier, name: feature?.name ?? featureIdentifier,
description: feature?.description ?? '', description: feature?.description ?? '',
isEnabled: application.features.isExperimentalFeatureEnabled(featureIdentifier), isEnabled: application.features.isExperimentalFeatureEnabled(featureIdentifier),
isEntitled: application.features.getFeatureStatus(featureIdentifier) === FeatureStatus.Entitled, isEntitled:
application.features.getFeatureStatus(NativeFeatureIdentifier.create(featureIdentifier).getValue()) ===
FeatureStatus.Entitled,
} }
}) })
setExperimentalFeatures(experimentalFeatures) setExperimentalFeatures(experimentalFeatures)

View File

@@ -1,4 +1,4 @@
import { FeatureIdentifier, FeatureStatus } from '@standardnotes/snjs' import { NativeFeatureIdentifier, FeatureStatus } from '@standardnotes/snjs'
import { WebApplication } from '@/Application/WebApplication' import { WebApplication } from '@/Application/WebApplication'
import { ViewControllerManager } from '@/Controllers/ViewControllerManager' import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
@@ -24,8 +24,9 @@ const Security: FunctionComponent<SecurityProps> = (props) => {
const isNativeMobileWeb = props.application.isNativeMobileWeb() const isNativeMobileWeb = props.application.isNativeMobileWeb()
const isU2FFeatureAvailable = const isU2FFeatureAvailable =
props.application.features.getFeatureStatus(FeatureIdentifier.UniversalSecondFactor) === FeatureStatus.Entitled && props.application.features.getFeatureStatus(
props.userProvider.getUser() !== undefined NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.UniversalSecondFactor).getValue(),
) === FeatureStatus.Entitled && props.userProvider.getUser() !== undefined
return ( return (
<PreferencesPane> <PreferencesPane>

View File

@@ -3,7 +3,7 @@ import {
ComponentInterface, ComponentInterface,
UIFeature, UIFeature,
ContentType, ContentType,
FeatureIdentifier, NativeFeatureIdentifier,
PreferencesServiceEvent, PreferencesServiceEvent,
ThemeFeatureDescription, ThemeFeatureDescription,
} from '@standardnotes/snjs' } from '@standardnotes/snjs'
@@ -54,7 +54,7 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = ({ quickSettingsMenuCont
(component) => (component) =>
!component.isTheme() && !component.isTheme() &&
[ComponentArea.EditorStack].includes(component.area) && [ComponentArea.EditorStack].includes(component.area) &&
component.identifier !== FeatureIdentifier.DeprecatedFoldersComponent, component.identifier !== NativeFeatureIdentifier.TYPES.DeprecatedFoldersComponent,
) )
setEditorStackComponents(toggleableComponents) setEditorStackComponents(toggleableComponents)
@@ -100,7 +100,7 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = ({ quickSettingsMenuCont
const toggleEditorStackComponent = useCallback( const toggleEditorStackComponent = useCallback(
(component: ComponentInterface) => { (component: ComponentInterface) => {
application.componentManager.toggleComponent(component).catch(console.error) void application.componentManager.toggleComponent(component)
}, },
[application], [application],
) )
@@ -141,7 +141,7 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = ({ quickSettingsMenuCont
Default Default
</MenuRadioButtonItem> </MenuRadioButtonItem>
{themes.map((theme) => ( {themes.map((theme) => (
<ThemesMenuButton uiFeature={theme} key={theme.uniqueIdentifier} /> <ThemesMenuButton uiFeature={theme} key={theme.uniqueIdentifier.value} />
))} ))}
<HorizontalSeparator classes="my-2" /> <HorizontalSeparator classes="my-2" />
<FocusModeSwitch <FocusModeSwitch

View File

@@ -1,4 +1,4 @@
import { UIFeature, FeatureIdentifier, FeatureStatus, ThemeFeatureDescription } from '@standardnotes/snjs' import { UIFeature, NativeFeatureIdentifier, FeatureStatus, ThemeFeatureDescription } from '@standardnotes/snjs'
import { FunctionComponent, MouseEventHandler, useCallback, useMemo } from 'react' import { FunctionComponent, MouseEventHandler, useCallback, useMemo } from 'react'
import Icon from '@/Components/Icon/Icon' import Icon from '@/Components/Icon/Icon'
import { usePremiumModal } from '@/Hooks/usePremiumModal' import { usePremiumModal } from '@/Hooks/usePremiumModal'
@@ -26,8 +26,8 @@ const ThemesMenuButton: FunctionComponent<Props> = ({ uiFeature }) => {
[application, uiFeature.featureIdentifier], [application, uiFeature.featureIdentifier],
) )
const isEntitledToTheme = useMemo( const isEntitledToTheme = useMemo(
() => application.features.getFeatureStatus(uiFeature.featureIdentifier) === FeatureStatus.Entitled, () => application.features.getFeatureStatus(uiFeature.uniqueIdentifier) === FeatureStatus.Entitled,
[application, uiFeature.featureIdentifier], [application, uiFeature.uniqueIdentifier],
) )
const canActivateTheme = useMemo(() => isEntitledToTheme || isThirdPartyTheme, [isEntitledToTheme, isThirdPartyTheme]) const canActivateTheme = useMemo(() => isEntitledToTheme || isThirdPartyTheme, [isEntitledToTheme, isThirdPartyTheme])
@@ -55,10 +55,10 @@ const ThemesMenuButton: FunctionComponent<Props> = ({ uiFeature }) => {
) )
const isMobile = application.isNativeMobileWeb() || isMobileScreen() const isMobile = application.isNativeMobileWeb() || isMobileScreen()
const shouldHideButton = uiFeature.featureIdentifier === FeatureIdentifier.DynamicTheme && isMobile const shouldHideButton = uiFeature.featureIdentifier === NativeFeatureIdentifier.TYPES.DynamicTheme && isMobile
const darkThemeShortcut = useMemo(() => { const darkThemeShortcut = useMemo(() => {
if (uiFeature.featureIdentifier === FeatureIdentifier.DarkTheme) { if (uiFeature.featureIdentifier === NativeFeatureIdentifier.TYPES.DarkTheme) {
return commandService.keyboardShortcutForCommand(TOGGLE_DARK_MODE_COMMAND) return commandService.keyboardShortcutForCommand(TOGGLE_DARK_MODE_COMMAND)
} }
}, [commandService, uiFeature.featureIdentifier]) }, [commandService, uiFeature.featureIdentifier])

View File

@@ -1,4 +1,4 @@
import { FeatureIdentifier } from '@standardnotes/features' import { NativeFeatureIdentifier } from '@standardnotes/features'
import { NoteType, PredicateCompoundOperator, PredicateJsonForm } from '@standardnotes/snjs' import { NoteType, PredicateCompoundOperator, PredicateJsonForm } from '@standardnotes/snjs'
import { makeObservable, observable, action } from 'mobx' import { makeObservable, observable, action } from 'mobx'
import { PredicateKeypath, PredicateKeypathTypes } from './PredicateKeypaths' import { PredicateKeypath, PredicateKeypathTypes } from './PredicateKeypaths'
@@ -59,7 +59,7 @@ export class CompoundPredicateBuilderController {
this.setPredicate(index, { value: Object.values(NoteType)[0] }) this.setPredicate(index, { value: Object.values(NoteType)[0] })
break break
case 'editorIdentifier': case 'editorIdentifier':
this.setPredicate(index, { value: FeatureIdentifier.PlainEditor }) this.setPredicate(index, { value: NativeFeatureIdentifier.TYPES.PlainEditor })
break break
case 'date': case 'date':
this.setPredicate(index, { value: '1.days.ago' }) this.setPredicate(index, { value: '1.days.ago' })

View File

@@ -7,7 +7,7 @@ import {
isPayloadSourceRetrieved, isPayloadSourceRetrieved,
PrefKey, PrefKey,
PrefDefaults, PrefDefaults,
FeatureIdentifier, NativeFeatureIdentifier,
FeatureStatus, FeatureStatus,
GetSuperNoteFeature, GetSuperNoteFeature,
} from '@standardnotes/snjs' } from '@standardnotes/snjs'
@@ -76,7 +76,12 @@ export const SuperEditor: FunctionComponent<Props> = ({
useEffect(() => { useEffect(() => {
setFeatureStatus( setFeatureStatus(
application.features.getFeatureStatus(FeatureIdentifier.SuperEditor, { inContextOfItem: note.current }), application.features.getFeatureStatus(
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.SuperEditor).getValue(),
{
inContextOfItem: note.current,
},
),
) )
}, [application.features]) }, [application.features])

View File

@@ -4,7 +4,7 @@ import { PremiumFeatureModalType } from '@/Components/PremiumFeaturesModal/Premi
import { destroyAllObjectProperties } from '@/Utils' import { destroyAllObjectProperties } from '@/Utils'
import { import {
ApplicationEvent, ApplicationEvent,
FeatureIdentifier, NativeFeatureIdentifier,
FeatureStatus, FeatureStatus,
InternalEventBusInterface, InternalEventBusInterface,
InternalEventInterface, InternalEventInterface,
@@ -100,19 +100,25 @@ export class FeaturesController extends AbstractViewController {
} }
private isEntitledToFiles(): boolean { private isEntitledToFiles(): boolean {
const status = this.application.features.getFeatureStatus(FeatureIdentifier.Files) const status = this.application.features.getFeatureStatus(
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.Files).getValue(),
)
return status === FeatureStatus.Entitled return status === FeatureStatus.Entitled
} }
private isEntitledToFolders(): boolean { private isEntitledToFolders(): boolean {
const status = this.application.features.getFeatureStatus(FeatureIdentifier.TagNesting) const status = this.application.features.getFeatureStatus(
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.TagNesting).getValue(),
)
return status === FeatureStatus.Entitled return status === FeatureStatus.Entitled
} }
private isEntitledToSmartViews(): boolean { private isEntitledToSmartViews(): boolean {
const status = this.application.features.getFeatureStatus(FeatureIdentifier.SmartFilters) const status = this.application.features.getFeatureStatus(
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.SmartFilters).getValue(),
)
return status === FeatureStatus.Entitled return status === FeatureStatus.Entitled
} }

View File

@@ -1,11 +1,15 @@
import { FeatureIdentifier } from '@standardnotes/snjs' import {
import { ComponentArea, FindNativeFeature, GetIframeAndNativeEditors } from '@standardnotes/features' ComponentArea,
FindNativeFeature,
GetIframeAndNativeEditors,
NativeFeatureIdentifier,
} from '@standardnotes/features'
import { getIconAndTintForNoteType } from './Items/Icons/getIconAndTintForNoteType' import { getIconAndTintForNoteType } from './Items/Icons/getIconAndTintForNoteType'
import { DropdownItem } from '@/Components/Dropdown/DropdownItem' import { DropdownItem } from '@/Components/Dropdown/DropdownItem'
import { WebApplicationInterface } from '@standardnotes/ui-services' import { WebApplicationInterface } from '@standardnotes/ui-services'
export type EditorOption = DropdownItem & { export type EditorOption = DropdownItem & {
value: FeatureIdentifier value: string
isLabs?: boolean isLabs?: boolean
} }
@@ -19,6 +23,7 @@ export function getDropdownItemsForAllEditors(application: WebApplicationInterfa
return { return {
label: editor.name, label: editor.name,
value: editor.identifier, value: editor.identifier,
id: NativeFeatureIdentifier.create(editor.identifier).getValue(),
...(iconType ? { icon: iconType } : null), ...(iconType ? { icon: iconType } : null),
...(tint ? { iconClassName: `text-accessory-tint-${tint}` } : null), ...(tint ? { iconClassName: `text-accessory-tint-${tint}` } : null),
} }
@@ -30,12 +35,11 @@ export function getDropdownItemsForAllEditors(application: WebApplicationInterfa
.thirdPartyComponentsForArea(ComponentArea.Editor) .thirdPartyComponentsForArea(ComponentArea.Editor)
.filter((component) => FindNativeFeature(component.identifier) === undefined) .filter((component) => FindNativeFeature(component.identifier) === undefined)
.map((editor): EditorOption => { .map((editor): EditorOption => {
const identifier = editor.package_info.identifier
const [iconType, tint] = getIconAndTintForNoteType(editor.noteType) const [iconType, tint] = getIconAndTintForNoteType(editor.noteType)
return { return {
label: editor.displayName, label: editor.displayName,
value: identifier, value: editor.uuid,
...(iconType ? { icon: iconType } : null), ...(iconType ? { icon: iconType } : null),
...(tint ? { iconClassName: `text-accessory-tint-${tint}` } : null), ...(tint ? { iconClassName: `text-accessory-tint-${tint}` } : null),
} }

View File

@@ -1,7 +1,7 @@
import { UIFeature, FeatureIdentifier, ThemeFeatureDescription } from '@standardnotes/snjs' import { UIFeature, NativeFeatureIdentifier, ThemeFeatureDescription } from '@standardnotes/snjs'
const isDarkModeTheme = (theme: UIFeature<ThemeFeatureDescription>) => const isDarkModeTheme = (theme: UIFeature<ThemeFeatureDescription>) =>
theme.featureIdentifier === FeatureIdentifier.DarkTheme theme.featureIdentifier === NativeFeatureIdentifier.TYPES.DarkTheme
export const sortThemes = (a: UIFeature<ThemeFeatureDescription>, b: UIFeature<ThemeFeatureDescription>) => { export const sortThemes = (a: UIFeature<ThemeFeatureDescription>, b: UIFeature<ThemeFeatureDescription>) => {
const aIsLayerable = a.layerable const aIsLayerable = a.layerable

View File

@@ -7,6 +7,8 @@ import {
GetSuperNoteFeature, GetSuperNoteFeature,
UIFeature, UIFeature,
IframeComponentFeatureDescription, IframeComponentFeatureDescription,
Uuid,
NativeFeatureIdentifier,
} from '@standardnotes/snjs' } from '@standardnotes/snjs'
import { EditorMenuGroup } from '@/Components/NotesOptions/EditorMenuGroup' import { EditorMenuGroup } from '@/Components/NotesOptions/EditorMenuGroup'
import { EditorMenuItem } from '@/Components/NotesOptions/EditorMenuItem' import { EditorMenuItem } from '@/Components/NotesOptions/EditorMenuItem'
@@ -29,7 +31,9 @@ const insertNativeEditorsInMap = (map: NoteTypeToEditorRowsMap, application: Web
const noteType = editorFeature.note_type const noteType = editorFeature.note_type
map[noteType].push({ map[noteType].push({
isEntitled: application.features.getFeatureStatus(editorFeature.identifier) === FeatureStatus.Entitled, isEntitled:
application.features.getFeatureStatus(NativeFeatureIdentifier.create(editorFeature.identifier).getValue()) ===
FeatureStatus.Entitled,
uiFeature: new UIFeature(editorFeature), uiFeature: new UIFeature(editorFeature),
}) })
} }
@@ -52,7 +56,7 @@ const insertInstalledComponentsInMap = (map: NoteTypeToEditorRowsMap, applicatio
const editorItem: EditorMenuItem = { const editorItem: EditorMenuItem = {
uiFeature: new UIFeature<IframeComponentFeatureDescription>(editor), uiFeature: new UIFeature<IframeComponentFeatureDescription>(editor),
isEntitled: application.features.getFeatureStatus(editor.identifier) === FeatureStatus.Entitled, isEntitled: application.features.getFeatureStatus(Uuid.create(editor.uuid).getValue()) === FeatureStatus.Entitled,
} }
map[noteType].push(editorItem) map[noteType].push(editorItem)