fix: Fixes issue where lock screen would not use previously active theme (#2372)
This commit is contained in:
@@ -1,5 +0,0 @@
|
||||
import { FeatureIdentifier } from './../Feature/FeatureIdentifier'
|
||||
|
||||
type ThirdPartyIdentifier = string
|
||||
|
||||
export type EditorIdentifier = FeatureIdentifier | ThirdPartyIdentifier
|
||||
@@ -1,16 +1,16 @@
|
||||
import { FeatureIdentifier } from '@standardnotes/features'
|
||||
import { NativeFeatureIdentifier } from '@standardnotes/features'
|
||||
import { noteTypeForEditorIdentifier, NoteType } from './NoteType'
|
||||
|
||||
describe('note type', () => {
|
||||
it('should return the correct note type for editor identifier', () => {
|
||||
expect(noteTypeForEditorIdentifier(FeatureIdentifier.PlainEditor)).toEqual(NoteType.Plain)
|
||||
expect(noteTypeForEditorIdentifier(FeatureIdentifier.SuperEditor)).toEqual(NoteType.Super)
|
||||
expect(noteTypeForEditorIdentifier(FeatureIdentifier.MarkdownProEditor)).toEqual(NoteType.Markdown)
|
||||
expect(noteTypeForEditorIdentifier(FeatureIdentifier.PlusEditor)).toEqual(NoteType.RichText)
|
||||
expect(noteTypeForEditorIdentifier(FeatureIdentifier.CodeEditor)).toEqual(NoteType.Code)
|
||||
expect(noteTypeForEditorIdentifier(FeatureIdentifier.SheetsEditor)).toEqual(NoteType.Spreadsheet)
|
||||
expect(noteTypeForEditorIdentifier(FeatureIdentifier.TaskEditor)).toEqual(NoteType.Task)
|
||||
expect(noteTypeForEditorIdentifier(FeatureIdentifier.TokenVaultEditor)).toEqual(NoteType.Authentication)
|
||||
expect(noteTypeForEditorIdentifier(NativeFeatureIdentifier.TYPES.PlainEditor)).toEqual(NoteType.Plain)
|
||||
expect(noteTypeForEditorIdentifier(NativeFeatureIdentifier.TYPES.SuperEditor)).toEqual(NoteType.Super)
|
||||
expect(noteTypeForEditorIdentifier(NativeFeatureIdentifier.TYPES.MarkdownProEditor)).toEqual(NoteType.Markdown)
|
||||
expect(noteTypeForEditorIdentifier(NativeFeatureIdentifier.TYPES.PlusEditor)).toEqual(NoteType.RichText)
|
||||
expect(noteTypeForEditorIdentifier(NativeFeatureIdentifier.TYPES.CodeEditor)).toEqual(NoteType.Code)
|
||||
expect(noteTypeForEditorIdentifier(NativeFeatureIdentifier.TYPES.SheetsEditor)).toEqual(NoteType.Spreadsheet)
|
||||
expect(noteTypeForEditorIdentifier(NativeFeatureIdentifier.TYPES.TaskEditor)).toEqual(NoteType.Task)
|
||||
expect(noteTypeForEditorIdentifier(NativeFeatureIdentifier.TYPES.TokenVaultEditor)).toEqual(NoteType.Authentication)
|
||||
expect(noteTypeForEditorIdentifier('org.third.party')).toEqual(NoteType.Unknown)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { EditorFeatureDescription } from '../Feature/EditorFeatureDescription'
|
||||
import { FindNativeFeature } from '../Feature/Features'
|
||||
import { IframeComponentFeatureDescription } from '../Feature/IframeComponentFeatureDescription'
|
||||
import { FeatureIdentifier } from './../Feature/FeatureIdentifier'
|
||||
import { EditorIdentifier } from './EditorIdentifier'
|
||||
|
||||
export enum NoteType {
|
||||
Authentication = 'authentication',
|
||||
@@ -16,10 +14,8 @@ export enum NoteType {
|
||||
Unknown = 'unknown',
|
||||
}
|
||||
|
||||
export function noteTypeForEditorIdentifier(identifier: EditorIdentifier): NoteType {
|
||||
const feature = FindNativeFeature<EditorFeatureDescription | IframeComponentFeatureDescription>(
|
||||
identifier as FeatureIdentifier,
|
||||
)
|
||||
export function noteTypeForEditorIdentifier(identifier: string): NoteType {
|
||||
const feature = FindNativeFeature<EditorFeatureDescription | IframeComponentFeatureDescription>(identifier)
|
||||
if (feature && feature.note_type) {
|
||||
return feature.note_type
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { PermissionName } from '../Permission/PermissionName'
|
||||
import { FeatureIdentifier } from './FeatureIdentifier'
|
||||
import { ComponentFlag } from '../Component/ComponentFlag'
|
||||
import { RoleFields } from './RoleFields'
|
||||
|
||||
@@ -14,7 +13,7 @@ export type BaseFeatureDescription = RoleFields & {
|
||||
clientControlled?: boolean
|
||||
|
||||
flags?: ComponentFlag[]
|
||||
identifier: FeatureIdentifier
|
||||
identifier: string
|
||||
marketing_url?: string
|
||||
name: string
|
||||
no_expire?: boolean
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { PermissionName } from '../Permission/PermissionName'
|
||||
import { FeatureIdentifier } from './FeatureIdentifier'
|
||||
import { RoleFields } from './RoleFields'
|
||||
|
||||
export type ClientFeatureDescription = RoleFields & {
|
||||
identifier: FeatureIdentifier
|
||||
identifier: string
|
||||
permission_name: PermissionName
|
||||
description: string
|
||||
name: string
|
||||
|
||||
@@ -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 = []
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AnyFeatureDescription } from './AnyFeatureDescription'
|
||||
import { ThemeFeatureDescription } from './ThemeFeatureDescription'
|
||||
import { EditorFeatureDescription } from './EditorFeatureDescription'
|
||||
import { FeatureIdentifier } from './FeatureIdentifier'
|
||||
import { NativeFeatureIdentifier } from './NativeFeatureIdentifier'
|
||||
import { serverFeatures } from '../Lists/ServerFeatures'
|
||||
import { clientFeatures } from '../Lists/ClientFeatures'
|
||||
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
|
||||
}
|
||||
|
||||
export function FindNativeTheme(identifier: FeatureIdentifier): ThemeFeatureDescription | undefined {
|
||||
export function FindNativeTheme(identifier: string): ThemeFeatureDescription | undefined {
|
||||
return themes().find((t) => t.identifier === identifier)
|
||||
}
|
||||
|
||||
@@ -40,11 +40,11 @@ export function GetIframeEditors(): IframeComponentFeatureDescription[] {
|
||||
}
|
||||
|
||||
export function GetSuperNoteFeature(): EditorFeatureDescription {
|
||||
return FindNativeFeature(FeatureIdentifier.SuperEditor) as EditorFeatureDescription
|
||||
return FindNativeFeature(NativeFeatureIdentifier.TYPES.SuperEditor) as EditorFeatureDescription
|
||||
}
|
||||
|
||||
export function GetPlainNoteFeature(): EditorFeatureDescription {
|
||||
return FindNativeFeature(FeatureIdentifier.PlainEditor) as EditorFeatureDescription
|
||||
return FindNativeFeature(NativeFeatureIdentifier.TYPES.PlainEditor) as EditorFeatureDescription
|
||||
}
|
||||
|
||||
export function GetNativeThemes(): ThemeFeatureDescription[] {
|
||||
@@ -52,5 +52,5 @@ export function GetNativeThemes(): 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
|
||||
}
|
||||
|
||||
@@ -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 = []
|
||||
@@ -1,11 +1,10 @@
|
||||
import { PermissionName } from '../Permission/PermissionName'
|
||||
import { FeatureIdentifier } from './FeatureIdentifier'
|
||||
import { RoleFields } from './RoleFields'
|
||||
|
||||
export type ServerFeatureDescription = RoleFields & {
|
||||
name: string
|
||||
description?: string
|
||||
identifier: FeatureIdentifier
|
||||
identifier: string
|
||||
permission_name: PermissionName
|
||||
deprecated?: boolean
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PermissionName } from '../Permission/PermissionName'
|
||||
import { FeatureIdentifier } from '../Feature/FeatureIdentifier'
|
||||
import { NativeFeatureIdentifier } from '../Feature/NativeFeatureIdentifier'
|
||||
import { RoleName } from '@standardnotes/domain-core'
|
||||
import { ClientFeatureDescription } from '../Feature/ClientFeatureDescription'
|
||||
|
||||
@@ -8,7 +8,7 @@ export function clientFeatures(): ClientFeatureDescription[] {
|
||||
{
|
||||
name: 'Tag Nesting',
|
||||
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||
identifier: FeatureIdentifier.TagNesting,
|
||||
identifier: NativeFeatureIdentifier.TYPES.TagNesting,
|
||||
permission_name: PermissionName.TagNesting,
|
||||
description: 'Organize your tags into folders.',
|
||||
},
|
||||
@@ -16,22 +16,22 @@ export function clientFeatures(): ClientFeatureDescription[] {
|
||||
{
|
||||
name: 'Smart Filters',
|
||||
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||
identifier: FeatureIdentifier.SmartFilters,
|
||||
identifier: NativeFeatureIdentifier.TYPES.SmartFilters,
|
||||
permission_name: PermissionName.SmartFilters,
|
||||
description: 'Create smart filters for viewing notes matching specific criteria.',
|
||||
},
|
||||
{
|
||||
name: 'Encrypted files',
|
||||
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||
identifier: FeatureIdentifier.Files,
|
||||
identifier: NativeFeatureIdentifier.TYPES.Files,
|
||||
permission_name: PermissionName.Files,
|
||||
description: '',
|
||||
},
|
||||
{
|
||||
name: 'Extension',
|
||||
name: 'Clipper',
|
||||
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||
identifier: FeatureIdentifier.Extension,
|
||||
permission_name: PermissionName.Extension,
|
||||
identifier: NativeFeatureIdentifier.TYPES.Clipper,
|
||||
permission_name: PermissionName.Clipper,
|
||||
description: '',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -3,7 +3,7 @@ import { EditorFeatureDescription } from '../Feature/EditorFeatureDescription'
|
||||
import { IframeComponentFeatureDescription } from '../Feature/IframeComponentFeatureDescription'
|
||||
import { ContentType, RoleName } from '@standardnotes/domain-core'
|
||||
import { PermissionName } from '../Permission/PermissionName'
|
||||
import { FeatureIdentifier } from '../Feature/FeatureIdentifier'
|
||||
import { NativeFeatureIdentifier } from '../Feature/NativeFeatureIdentifier'
|
||||
import { NoteType } from '../Component/NoteType'
|
||||
import { FillIframeEditorDefaults } from './Utilities/FillEditorComponentDefaults'
|
||||
import { ComponentAction } from '../Component/ComponentAction'
|
||||
@@ -12,7 +12,7 @@ import { ComponentArea } from '../Component/ComponentArea'
|
||||
export function GetDeprecatedFeatures(): AnyFeatureDescription[] {
|
||||
const bold: EditorFeatureDescription = FillIframeEditorDefaults({
|
||||
name: 'Alternative Rich Text',
|
||||
identifier: FeatureIdentifier.DeprecatedBoldEditor,
|
||||
identifier: NativeFeatureIdentifier.TYPES.DeprecatedBoldEditor,
|
||||
note_type: NoteType.RichText,
|
||||
file_type: 'html',
|
||||
component_permissions: [
|
||||
@@ -39,7 +39,7 @@ export function GetDeprecatedFeatures(): AnyFeatureDescription[] {
|
||||
|
||||
const markdownBasic: EditorFeatureDescription = FillIframeEditorDefaults({
|
||||
name: 'Basic Markdown',
|
||||
identifier: FeatureIdentifier.DeprecatedMarkdownBasicEditor,
|
||||
identifier: NativeFeatureIdentifier.TYPES.DeprecatedMarkdownBasicEditor,
|
||||
note_type: NoteType.Markdown,
|
||||
spellcheckControl: true,
|
||||
file_type: 'md',
|
||||
@@ -52,7 +52,7 @@ export function GetDeprecatedFeatures(): AnyFeatureDescription[] {
|
||||
|
||||
const markdownAlt: EditorFeatureDescription = FillIframeEditorDefaults({
|
||||
name: 'Markdown Alternative',
|
||||
identifier: FeatureIdentifier.DeprecatedMarkdownVisualEditor,
|
||||
identifier: NativeFeatureIdentifier.TYPES.DeprecatedMarkdownVisualEditor,
|
||||
note_type: NoteType.Markdown,
|
||||
file_type: 'md',
|
||||
deprecated: true,
|
||||
@@ -66,7 +66,7 @@ export function GetDeprecatedFeatures(): AnyFeatureDescription[] {
|
||||
|
||||
const markdownMinimist: EditorFeatureDescription = FillIframeEditorDefaults({
|
||||
name: 'Minimal Markdown',
|
||||
identifier: FeatureIdentifier.DeprecatedMarkdownMinimistEditor,
|
||||
identifier: NativeFeatureIdentifier.TYPES.DeprecatedMarkdownMinimistEditor,
|
||||
note_type: NoteType.Markdown,
|
||||
file_type: 'md',
|
||||
index_path: 'index.html',
|
||||
@@ -80,7 +80,7 @@ export function GetDeprecatedFeatures(): AnyFeatureDescription[] {
|
||||
|
||||
const markdownMath: EditorFeatureDescription = FillIframeEditorDefaults({
|
||||
name: 'Markdown with Math',
|
||||
identifier: FeatureIdentifier.DeprecatedMarkdownMathEditor,
|
||||
identifier: NativeFeatureIdentifier.TYPES.DeprecatedMarkdownMathEditor,
|
||||
spellcheckControl: true,
|
||||
permission_name: PermissionName.MarkdownMathEditor,
|
||||
note_type: NoteType.Markdown,
|
||||
@@ -94,7 +94,7 @@ export function GetDeprecatedFeatures(): AnyFeatureDescription[] {
|
||||
|
||||
const filesafe: IframeComponentFeatureDescription = FillIframeEditorDefaults({
|
||||
name: 'FileSafe',
|
||||
identifier: FeatureIdentifier.DeprecatedFileSafe,
|
||||
identifier: NativeFeatureIdentifier.TYPES.DeprecatedFileSafe,
|
||||
component_permissions: [
|
||||
{
|
||||
name: ComponentAction.StreamContextItem,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PermissionName } from '../Permission/PermissionName'
|
||||
import { FeatureIdentifier } from '../Feature/FeatureIdentifier'
|
||||
import { NativeFeatureIdentifier } from '../Feature/NativeFeatureIdentifier'
|
||||
import { NoteType } from '../Component/NoteType'
|
||||
import { FillIframeEditorDefaults } from './Utilities/FillEditorComponentDefaults'
|
||||
import { RoleName } from '@standardnotes/domain-core'
|
||||
@@ -9,7 +9,7 @@ export function IframeEditors(): IframeComponentFeatureDescription[] {
|
||||
const code = FillIframeEditorDefaults({
|
||||
name: 'Code',
|
||||
spellcheckControl: true,
|
||||
identifier: FeatureIdentifier.CodeEditor,
|
||||
identifier: NativeFeatureIdentifier.TYPES.CodeEditor,
|
||||
permission_name: PermissionName.CodeEditor,
|
||||
note_type: NoteType.Code,
|
||||
file_type: 'txt',
|
||||
@@ -26,7 +26,7 @@ export function IframeEditors(): IframeComponentFeatureDescription[] {
|
||||
name: 'Rich Text',
|
||||
note_type: NoteType.RichText,
|
||||
file_type: 'html',
|
||||
identifier: FeatureIdentifier.PlusEditor,
|
||||
identifier: NativeFeatureIdentifier.TYPES.PlusEditor,
|
||||
permission_name: PermissionName.PlusEditor,
|
||||
spellcheckControl: true,
|
||||
description:
|
||||
@@ -37,7 +37,7 @@ export function IframeEditors(): IframeComponentFeatureDescription[] {
|
||||
|
||||
const markdown = FillIframeEditorDefaults({
|
||||
name: 'Markdown',
|
||||
identifier: FeatureIdentifier.MarkdownProEditor,
|
||||
identifier: NativeFeatureIdentifier.TYPES.MarkdownProEditor,
|
||||
note_type: NoteType.Markdown,
|
||||
file_type: 'md',
|
||||
permission_name: PermissionName.MarkdownProEditor,
|
||||
@@ -50,7 +50,7 @@ export function IframeEditors(): IframeComponentFeatureDescription[] {
|
||||
|
||||
const task = FillIframeEditorDefaults({
|
||||
name: 'Checklist',
|
||||
identifier: FeatureIdentifier.TaskEditor,
|
||||
identifier: NativeFeatureIdentifier.TYPES.TaskEditor,
|
||||
note_type: NoteType.Task,
|
||||
spellcheckControl: true,
|
||||
file_type: 'md',
|
||||
@@ -67,7 +67,7 @@ export function IframeEditors(): IframeComponentFeatureDescription[] {
|
||||
note_type: NoteType.Authentication,
|
||||
file_type: 'json',
|
||||
interchangeable: false,
|
||||
identifier: FeatureIdentifier.TokenVaultEditor,
|
||||
identifier: NativeFeatureIdentifier.TYPES.TokenVaultEditor,
|
||||
permission_name: PermissionName.TokenVaultEditor,
|
||||
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.',
|
||||
@@ -77,7 +77,7 @@ export function IframeEditors(): IframeComponentFeatureDescription[] {
|
||||
|
||||
const spreadsheets = FillIframeEditorDefaults({
|
||||
name: 'Spreadsheet',
|
||||
identifier: FeatureIdentifier.SheetsEditor,
|
||||
identifier: NativeFeatureIdentifier.TYPES.SheetsEditor,
|
||||
note_type: NoteType.Spreadsheet,
|
||||
file_type: 'json',
|
||||
interchangeable: false,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { RoleName } from '@standardnotes/domain-core'
|
||||
import { NoteType } from '../Component/NoteType'
|
||||
import { EditorFeatureDescription } from '../Feature/EditorFeatureDescription'
|
||||
import { FeatureIdentifier } from '../Feature/FeatureIdentifier'
|
||||
import { NativeFeatureIdentifier } from '../Feature/NativeFeatureIdentifier'
|
||||
import { PermissionName } from '../Permission/PermissionName'
|
||||
|
||||
export function nativeEditors(): EditorFeatureDescription[] {
|
||||
@@ -9,7 +9,7 @@ export function nativeEditors(): EditorFeatureDescription[] {
|
||||
{
|
||||
name: 'Super',
|
||||
note_type: NoteType.Super,
|
||||
identifier: FeatureIdentifier.SuperEditor,
|
||||
identifier: NativeFeatureIdentifier.TYPES.SuperEditor,
|
||||
spellcheckControl: true,
|
||||
file_type: 'json',
|
||||
interchangeable: false,
|
||||
@@ -24,7 +24,7 @@ export function nativeEditors(): EditorFeatureDescription[] {
|
||||
spellcheckControl: true,
|
||||
file_type: 'txt',
|
||||
interchangeable: true,
|
||||
identifier: FeatureIdentifier.PlainEditor,
|
||||
identifier: NativeFeatureIdentifier.TYPES.PlainEditor,
|
||||
availableInRoles: [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||
permission_name: PermissionName.PlainEditor,
|
||||
},
|
||||
|
||||
@@ -1,67 +1,67 @@
|
||||
import { ServerFeatureDescription } from '../Feature/ServerFeatureDescription'
|
||||
import { PermissionName } from '../Permission/PermissionName'
|
||||
import { FeatureIdentifier } from '../Feature/FeatureIdentifier'
|
||||
import { NativeFeatureIdentifier } from '../Feature/NativeFeatureIdentifier'
|
||||
import { RoleName } from '@standardnotes/domain-core'
|
||||
|
||||
export function serverFeatures(): ServerFeatureDescription[] {
|
||||
return [
|
||||
{
|
||||
name: 'Two factor authentication',
|
||||
identifier: FeatureIdentifier.TwoFactorAuth,
|
||||
identifier: NativeFeatureIdentifier.TYPES.TwoFactorAuth,
|
||||
permission_name: PermissionName.TwoFactorAuth,
|
||||
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||
},
|
||||
{
|
||||
name: 'U2F authentication',
|
||||
identifier: FeatureIdentifier.UniversalSecondFactor,
|
||||
identifier: NativeFeatureIdentifier.TYPES.UniversalSecondFactor,
|
||||
permission_name: PermissionName.UniversalSecondFactor,
|
||||
availableInRoles: [RoleName.NAMES.ProUser],
|
||||
},
|
||||
{
|
||||
name: 'Unlimited note history',
|
||||
identifier: FeatureIdentifier.NoteHistoryUnlimited,
|
||||
identifier: NativeFeatureIdentifier.TYPES.NoteHistoryUnlimited,
|
||||
permission_name: PermissionName.NoteHistoryUnlimited,
|
||||
availableInRoles: [RoleName.NAMES.ProUser],
|
||||
},
|
||||
{
|
||||
name: '365 days note history',
|
||||
identifier: FeatureIdentifier.NoteHistory365Days,
|
||||
identifier: NativeFeatureIdentifier.TYPES.NoteHistory365Days,
|
||||
permission_name: PermissionName.NoteHistory365Days,
|
||||
availableInRoles: [RoleName.NAMES.PlusUser],
|
||||
},
|
||||
{
|
||||
name: 'Email backups',
|
||||
identifier: FeatureIdentifier.DailyEmailBackup,
|
||||
identifier: NativeFeatureIdentifier.TYPES.DailyEmailBackup,
|
||||
permission_name: PermissionName.DailyEmailBackup,
|
||||
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||
},
|
||||
{
|
||||
name: 'Sign-in email alerts',
|
||||
identifier: FeatureIdentifier.SignInAlerts,
|
||||
identifier: NativeFeatureIdentifier.TYPES.SignInAlerts,
|
||||
permission_name: PermissionName.SignInAlerts,
|
||||
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||
},
|
||||
{
|
||||
name: 'Files maximum storage tier',
|
||||
identifier: FeatureIdentifier.FilesMaximumStorageTier,
|
||||
identifier: NativeFeatureIdentifier.TYPES.FilesMaximumStorageTier,
|
||||
permission_name: PermissionName.FilesMaximumStorageTier,
|
||||
availableInRoles: [RoleName.NAMES.ProUser],
|
||||
},
|
||||
{
|
||||
name: 'Files low storage tier',
|
||||
identifier: FeatureIdentifier.FilesLowStorageTier,
|
||||
identifier: NativeFeatureIdentifier.TYPES.FilesLowStorageTier,
|
||||
permission_name: PermissionName.FilesLowStorageTier,
|
||||
availableInRoles: [RoleName.NAMES.PlusUser],
|
||||
},
|
||||
{
|
||||
name: 'Files medium storage tier',
|
||||
identifier: FeatureIdentifier.SubscriptionSharing,
|
||||
identifier: NativeFeatureIdentifier.TYPES.SubscriptionSharing,
|
||||
permission_name: PermissionName.SubscriptionSharing,
|
||||
availableInRoles: [RoleName.NAMES.ProUser],
|
||||
},
|
||||
{
|
||||
name: 'Listed Custom Domain',
|
||||
identifier: FeatureIdentifier.ListedCustomDomain,
|
||||
identifier: NativeFeatureIdentifier.TYPES.ListedCustomDomain,
|
||||
permission_name: PermissionName.ListedCustomDomain,
|
||||
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||
},
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { ThemeFeatureDescription } from '../Feature/ThemeFeatureDescription'
|
||||
import { PermissionName } from '../Permission/PermissionName'
|
||||
import { FeatureIdentifier } from '../Feature/FeatureIdentifier'
|
||||
import { NativeFeatureIdentifier } from '../Feature/NativeFeatureIdentifier'
|
||||
import { FillThemeComponentDefaults } from './Utilities/FillThemeComponentDefaults'
|
||||
import { RoleName } from '@standardnotes/domain-core'
|
||||
|
||||
export function themes(): ThemeFeatureDescription[] {
|
||||
const midnight: ThemeFeatureDescription = FillThemeComponentDefaults({
|
||||
name: 'Midnight',
|
||||
identifier: FeatureIdentifier.MidnightTheme,
|
||||
identifier: NativeFeatureIdentifier.TYPES.MidnightTheme,
|
||||
permission_name: PermissionName.MidnightTheme,
|
||||
isDark: true,
|
||||
dock_icon: {
|
||||
@@ -22,7 +22,7 @@ export function themes(): ThemeFeatureDescription[] {
|
||||
const futura: ThemeFeatureDescription = FillThemeComponentDefaults({
|
||||
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||
name: 'Futura',
|
||||
identifier: FeatureIdentifier.FuturaTheme,
|
||||
identifier: NativeFeatureIdentifier.TYPES.FuturaTheme,
|
||||
permission_name: PermissionName.FuturaTheme,
|
||||
isDark: true,
|
||||
dock_icon: {
|
||||
@@ -36,7 +36,7 @@ export function themes(): ThemeFeatureDescription[] {
|
||||
const solarizedDark: ThemeFeatureDescription = FillThemeComponentDefaults({
|
||||
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||
name: 'Solarized Dark',
|
||||
identifier: FeatureIdentifier.SolarizedDarkTheme,
|
||||
identifier: NativeFeatureIdentifier.TYPES.SolarizedDarkTheme,
|
||||
permission_name: PermissionName.SolarizedDarkTheme,
|
||||
isDark: true,
|
||||
dock_icon: {
|
||||
@@ -50,7 +50,7 @@ export function themes(): ThemeFeatureDescription[] {
|
||||
const autobiography: ThemeFeatureDescription = FillThemeComponentDefaults({
|
||||
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||
name: 'Autobiography',
|
||||
identifier: FeatureIdentifier.AutobiographyTheme,
|
||||
identifier: NativeFeatureIdentifier.TYPES.AutobiographyTheme,
|
||||
permission_name: PermissionName.AutobiographyTheme,
|
||||
dock_icon: {
|
||||
type: 'circle',
|
||||
@@ -63,7 +63,7 @@ export function themes(): ThemeFeatureDescription[] {
|
||||
const dark: ThemeFeatureDescription = FillThemeComponentDefaults({
|
||||
availableInRoles: [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||
name: 'Dark',
|
||||
identifier: FeatureIdentifier.DarkTheme,
|
||||
identifier: NativeFeatureIdentifier.TYPES.DarkTheme,
|
||||
permission_name: PermissionName.FocusedTheme,
|
||||
clientControlled: true,
|
||||
isDark: true,
|
||||
@@ -78,7 +78,7 @@ export function themes(): ThemeFeatureDescription[] {
|
||||
const titanium: ThemeFeatureDescription = FillThemeComponentDefaults({
|
||||
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||
name: 'Titanium',
|
||||
identifier: FeatureIdentifier.TitaniumTheme,
|
||||
identifier: NativeFeatureIdentifier.TYPES.TitaniumTheme,
|
||||
permission_name: PermissionName.TitaniumTheme,
|
||||
dock_icon: {
|
||||
type: 'circle',
|
||||
@@ -91,7 +91,7 @@ export function themes(): ThemeFeatureDescription[] {
|
||||
const dynamic: ThemeFeatureDescription = FillThemeComponentDefaults({
|
||||
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||
name: 'Dynamic Panels',
|
||||
identifier: FeatureIdentifier.DynamicTheme,
|
||||
identifier: NativeFeatureIdentifier.TYPES.DynamicTheme,
|
||||
permission_name: PermissionName.ThemeDynamic,
|
||||
layerable: true,
|
||||
no_mobile: true,
|
||||
|
||||
@@ -37,5 +37,5 @@ export enum PermissionName {
|
||||
UniversalSecondFactor = 'server:universal-second-factor',
|
||||
SubscriptionSharing = 'server:subscription-sharing',
|
||||
SuperEditor = 'editor:super-editor',
|
||||
Extension = 'app:extension',
|
||||
Clipper = 'app:clipper',
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export * from './Feature/AnyFeatureDescription'
|
||||
export * from './Feature/FeatureIdentifier'
|
||||
export * from './Feature/NativeFeatureIdentifier'
|
||||
export * from './Feature/Features'
|
||||
export * from './Feature/TypeGuards'
|
||||
|
||||
@@ -22,4 +22,3 @@ export * from './Component/ComponentFlag'
|
||||
export * from './Component/ComponentPermission'
|
||||
export * from './Component/NoteType'
|
||||
export * from './Component/ThemeDockIcon'
|
||||
export * from './Component/EditorIdentifier'
|
||||
|
||||
@@ -6,11 +6,7 @@ import {
|
||||
NoteType,
|
||||
UIFeatureDescriptionTypes,
|
||||
} from '@standardnotes/features'
|
||||
import {
|
||||
isUIFeatureAnIframeFeature,
|
||||
isComponentOrFeatureDescriptionAComponent,
|
||||
isComponentOrFeatureDescriptionAFeatureDescription,
|
||||
} from './TypeGuards'
|
||||
import { isUIFeatureAnIframeFeature, isItemBasedFeature, isNativeFeature } from './TypeGuards'
|
||||
import { UIFeature } from './UIFeature'
|
||||
import { ComponentInterface } from '../../Syncable/Component'
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
@@ -45,7 +41,7 @@ describe('TypeGuards', () => {
|
||||
uuid: 'abc-123',
|
||||
} as ComponentInterface
|
||||
|
||||
expect(isComponentOrFeatureDescriptionAComponent(x)).toBe(true)
|
||||
expect(isItemBasedFeature(x)).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false if feature description is not a component', () => {
|
||||
@@ -53,17 +49,17 @@ describe('TypeGuards', () => {
|
||||
note_type: NoteType.Super,
|
||||
} 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', () => {
|
||||
const x: AnyFeatureDescription = {
|
||||
content_type: 'TestContentType',
|
||||
} as AnyFeatureDescription
|
||||
|
||||
expect(isComponentOrFeatureDescriptionAFeatureDescription(x)).toBe(true)
|
||||
expect(isNativeFeature(x)).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false if x is a component', () => {
|
||||
@@ -71,7 +67,7 @@ describe('TypeGuards', () => {
|
||||
uuid: 'abc-123',
|
||||
} as ComponentInterface
|
||||
|
||||
expect(isComponentOrFeatureDescriptionAFeatureDescription(x)).toBe(false)
|
||||
expect(isNativeFeature(x)).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -14,14 +14,10 @@ export function isUIFeatureAnIframeFeature(
|
||||
return isIframeComponentFeatureDescription(x.featureDescription)
|
||||
}
|
||||
|
||||
export function isComponentOrFeatureDescriptionAComponent(
|
||||
x: ComponentInterface | UIFeatureDescriptionTypes,
|
||||
): x is ComponentInterface {
|
||||
export function isItemBasedFeature(x: ComponentInterface | UIFeatureDescriptionTypes): x is ComponentInterface {
|
||||
return 'uuid' in x
|
||||
}
|
||||
|
||||
export function isComponentOrFeatureDescriptionAFeatureDescription(
|
||||
x: ComponentInterface | AnyFeatureDescription,
|
||||
): x is AnyFeatureDescription {
|
||||
export function isNativeFeature(x: ComponentInterface | AnyFeatureDescription): x is AnyFeatureDescription {
|
||||
return !('uuid' in x)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
ComponentArea,
|
||||
ComponentPermission,
|
||||
EditorFeatureDescription,
|
||||
FeatureIdentifier,
|
||||
NativeFeatureIdentifier,
|
||||
NoteType,
|
||||
ThemeDockIcon,
|
||||
UIFeatureDescriptionTypes,
|
||||
@@ -12,29 +12,27 @@ import {
|
||||
} from '@standardnotes/features'
|
||||
import { ComponentInterface } from '../../Syncable/Component/ComponentInterface'
|
||||
import { isTheme } from '../../Syncable/Theme'
|
||||
import {
|
||||
isComponentOrFeatureDescriptionAComponent,
|
||||
isComponentOrFeatureDescriptionAFeatureDescription,
|
||||
} from './TypeGuards'
|
||||
import { isItemBasedFeature, isNativeFeature } from './TypeGuards'
|
||||
import { UIFeatureInterface } from './UIFeatureInterface'
|
||||
import { Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
export class UIFeature<F extends UIFeatureDescriptionTypes> implements UIFeatureInterface<F> {
|
||||
constructor(public readonly item: ComponentInterface | F) {}
|
||||
|
||||
get isComponent(): boolean {
|
||||
return isComponentOrFeatureDescriptionAComponent(this.item)
|
||||
return isItemBasedFeature(this.item)
|
||||
}
|
||||
|
||||
get isFeatureDescription(): boolean {
|
||||
return isComponentOrFeatureDescriptionAFeatureDescription(this.item)
|
||||
return isNativeFeature(this.item)
|
||||
}
|
||||
|
||||
get isThemeComponent(): boolean {
|
||||
return isComponentOrFeatureDescriptionAComponent(this.item) && isTheme(this.item)
|
||||
return isItemBasedFeature(this.item) && isTheme(this.item)
|
||||
}
|
||||
|
||||
get asComponent(): ComponentInterface {
|
||||
if (isComponentOrFeatureDescriptionAComponent(this.item)) {
|
||||
if (isItemBasedFeature(this.item)) {
|
||||
return this.item
|
||||
}
|
||||
|
||||
@@ -42,29 +40,30 @@ export class UIFeature<F extends UIFeatureDescriptionTypes> implements UIFeature
|
||||
}
|
||||
|
||||
get asFeatureDescription(): F {
|
||||
if (isComponentOrFeatureDescriptionAFeatureDescription(this.item)) {
|
||||
if (isNativeFeature(this.item)) {
|
||||
return this.item
|
||||
}
|
||||
|
||||
throw new Error('Cannot cast item to feature description')
|
||||
}
|
||||
|
||||
get uniqueIdentifier(): string {
|
||||
if (isComponentOrFeatureDescriptionAFeatureDescription(this.item)) {
|
||||
return this.item.identifier
|
||||
get uniqueIdentifier(): NativeFeatureIdentifier | Uuid {
|
||||
if (isNativeFeature(this.item)) {
|
||||
const nativeFeature = NativeFeatureIdentifier.create(this.item.identifier)
|
||||
return nativeFeature.getValue()
|
||||
} else {
|
||||
return this.item.uuid
|
||||
return Uuid.create(this.item.uuid).getValue()
|
||||
}
|
||||
}
|
||||
|
||||
get featureIdentifier(): FeatureIdentifier {
|
||||
get featureIdentifier(): string {
|
||||
return this.item.identifier
|
||||
}
|
||||
|
||||
get noteType(): NoteType {
|
||||
if (isComponentOrFeatureDescriptionAFeatureDescription(this.item) && isEditorFeatureDescription(this.item)) {
|
||||
if (isNativeFeature(this.item) && isEditorFeatureDescription(this.item)) {
|
||||
return this.item.note_type ?? NoteType.Unknown
|
||||
} else if (isComponentOrFeatureDescriptionAComponent(this.item)) {
|
||||
} else if (isItemBasedFeature(this.item)) {
|
||||
return this.item.noteType
|
||||
}
|
||||
|
||||
@@ -72,12 +71,9 @@ export class UIFeature<F extends UIFeatureDescriptionTypes> implements UIFeature
|
||||
}
|
||||
|
||||
get fileType(): EditorFeatureDescription['file_type'] {
|
||||
if (isComponentOrFeatureDescriptionAFeatureDescription(this.item) && isEditorFeatureDescription(this.item)) {
|
||||
if (isNativeFeature(this.item) && isEditorFeatureDescription(this.item)) {
|
||||
return this.item.file_type
|
||||
} else if (
|
||||
isComponentOrFeatureDescriptionAComponent(this.item) &&
|
||||
isEditorFeatureDescription(this.item.package_info)
|
||||
) {
|
||||
} else if (isItemBasedFeature(this.item) && isEditorFeatureDescription(this.item.package_info)) {
|
||||
return this.item.package_info?.file_type ?? 'txt'
|
||||
}
|
||||
|
||||
@@ -85,7 +81,7 @@ export class UIFeature<F extends UIFeatureDescriptionTypes> implements UIFeature
|
||||
}
|
||||
|
||||
get displayName(): string {
|
||||
if (isComponentOrFeatureDescriptionAFeatureDescription(this.item)) {
|
||||
if (isNativeFeature(this.item)) {
|
||||
return this.item.name ?? ''
|
||||
} else {
|
||||
return this.item.displayName
|
||||
@@ -93,7 +89,7 @@ export class UIFeature<F extends UIFeatureDescriptionTypes> implements UIFeature
|
||||
}
|
||||
|
||||
get description(): string {
|
||||
if (isComponentOrFeatureDescriptionAFeatureDescription(this.item)) {
|
||||
if (isNativeFeature(this.item)) {
|
||||
return this.item.description ?? ''
|
||||
} else {
|
||||
return this.item.package_info.description ?? ''
|
||||
@@ -101,7 +97,7 @@ export class UIFeature<F extends UIFeatureDescriptionTypes> implements UIFeature
|
||||
}
|
||||
|
||||
get deprecationMessage(): string | undefined {
|
||||
if (isComponentOrFeatureDescriptionAFeatureDescription(this.item)) {
|
||||
if (isNativeFeature(this.item)) {
|
||||
return this.item.deprecation_message
|
||||
} else {
|
||||
return this.item.deprecationMessage
|
||||
@@ -109,7 +105,7 @@ export class UIFeature<F extends UIFeatureDescriptionTypes> implements UIFeature
|
||||
}
|
||||
|
||||
get expirationDate(): Date | undefined {
|
||||
if (isComponentOrFeatureDescriptionAFeatureDescription(this.item)) {
|
||||
if (isNativeFeature(this.item)) {
|
||||
return this.item.expires_at ? new Date(this.item.expires_at) : undefined
|
||||
} else {
|
||||
return this.item.valid_until
|
||||
@@ -117,7 +113,7 @@ export class UIFeature<F extends UIFeatureDescriptionTypes> implements UIFeature
|
||||
}
|
||||
|
||||
get featureDescription(): F {
|
||||
if (isComponentOrFeatureDescriptionAFeatureDescription(this.item)) {
|
||||
if (isNativeFeature(this.item)) {
|
||||
return this.item
|
||||
} else {
|
||||
return this.item.package_info as F
|
||||
@@ -125,12 +121,9 @@ export class UIFeature<F extends UIFeatureDescriptionTypes> implements UIFeature
|
||||
}
|
||||
|
||||
get acquiredPermissions(): ComponentPermission[] {
|
||||
if (
|
||||
isComponentOrFeatureDescriptionAFeatureDescription(this.item) &&
|
||||
isIframeComponentFeatureDescription(this.item)
|
||||
) {
|
||||
if (isNativeFeature(this.item) && isIframeComponentFeatureDescription(this.item)) {
|
||||
return this.item.component_permissions ?? []
|
||||
} else if (isComponentOrFeatureDescriptionAComponent(this.item)) {
|
||||
} else if (isItemBasedFeature(this.item)) {
|
||||
return this.item.permissions
|
||||
}
|
||||
|
||||
@@ -146,7 +139,7 @@ export class UIFeature<F extends UIFeatureDescriptionTypes> implements UIFeature
|
||||
}
|
||||
|
||||
get layerable(): boolean {
|
||||
if (isComponentOrFeatureDescriptionAComponent(this.item) && isTheme(this.item)) {
|
||||
if (isItemBasedFeature(this.item) && isTheme(this.item)) {
|
||||
return this.item.layerable
|
||||
} else if (isThemeFeatureDescription(this.asFeatureDescription)) {
|
||||
return this.asFeatureDescription.layerable ?? false
|
||||
@@ -156,7 +149,7 @@ export class UIFeature<F extends UIFeatureDescriptionTypes> implements UIFeature
|
||||
}
|
||||
|
||||
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
|
||||
} else if (isThemeFeatureDescription(this.asFeatureDescription)) {
|
||||
return this.asFeatureDescription.dock_icon
|
||||
|
||||
@@ -2,12 +2,13 @@ import {
|
||||
ComponentArea,
|
||||
ComponentPermission,
|
||||
EditorFeatureDescription,
|
||||
FeatureIdentifier,
|
||||
NativeFeatureIdentifier,
|
||||
NoteType,
|
||||
ThemeDockIcon,
|
||||
UIFeatureDescriptionTypes,
|
||||
} from '@standardnotes/features'
|
||||
import { ComponentInterface } from '../../Syncable/Component'
|
||||
import { Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
export interface UIFeatureInterface<F extends UIFeatureDescriptionTypes> {
|
||||
item: ComponentInterface | F
|
||||
@@ -16,8 +17,8 @@ export interface UIFeatureInterface<F extends UIFeatureDescriptionTypes> {
|
||||
get isThemeComponent(): boolean
|
||||
get asComponent(): ComponentInterface
|
||||
get asFeatureDescription(): F
|
||||
get uniqueIdentifier(): string
|
||||
get featureIdentifier(): FeatureIdentifier
|
||||
get uniqueIdentifier(): NativeFeatureIdentifier | Uuid
|
||||
get featureIdentifier(): string
|
||||
get noteType(): NoteType
|
||||
get fileType(): EditorFeatureDescription['file_type']
|
||||
get displayName(): string
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { isValidUrl } from '@standardnotes/utils'
|
||||
import {
|
||||
FeatureIdentifier,
|
||||
ThirdPartyFeatureDescription,
|
||||
ComponentArea,
|
||||
ComponentFlag,
|
||||
@@ -175,7 +174,7 @@ export class SNComponent extends DecryptedItem<ComponentContent> implements Comp
|
||||
return this.valid_until.getTime() > 0 && this.valid_until <= new Date()
|
||||
}
|
||||
|
||||
public get identifier(): FeatureIdentifier {
|
||||
public get identifier(): string {
|
||||
return this.package_info.identifier
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import {
|
||||
ComponentArea,
|
||||
ComponentPermission,
|
||||
FeatureIdentifier,
|
||||
NoteType,
|
||||
ThirdPartyFeatureDescription,
|
||||
} from '@standardnotes/features'
|
||||
import { ComponentArea, ComponentPermission, NoteType, ThirdPartyFeatureDescription } from '@standardnotes/features'
|
||||
import { ComponentPackageInfo } from './PackageInfo'
|
||||
import { DecryptedItemInterface } from '../../Abstract/Item'
|
||||
import { ComponentContent } from './ComponentContent'
|
||||
@@ -35,7 +29,7 @@ export interface ComponentInterface extends DecryptedItemInterface<ComponentCont
|
||||
isExplicitlyDisabledForItem(uuid: string): boolean
|
||||
legacyIsDefaultEditor(): boolean
|
||||
|
||||
get identifier(): FeatureIdentifier
|
||||
get identifier(): string
|
||||
get noteType(): NoteType
|
||||
get displayName(): string
|
||||
get deprecationMessage(): string | undefined
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { ItemInterface } from '../../Abstract/Item/Interfaces/ItemInterface'
|
||||
import { DecryptedPayloadInterface } from '../../Abstract/Payload/Interfaces/DecryptedPayload'
|
||||
@@ -21,7 +21,7 @@ export class SNNote extends DecryptedItem<NoteContent> implements NoteContentSpe
|
||||
public readonly authorizedForListed: boolean
|
||||
|
||||
/** 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>) {
|
||||
super(payload)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FeatureIdentifier, NoteType } from '@standardnotes/features'
|
||||
import { NoteType } from '@standardnotes/features'
|
||||
import { ItemContent } from '../../Abstract/Content/ItemContent'
|
||||
import { EditorLineWidth } from '../UserPrefs'
|
||||
|
||||
@@ -11,7 +11,7 @@ export interface NoteContentSpecialized {
|
||||
spellcheck?: boolean
|
||||
editorWidth?: EditorLineWidth
|
||||
noteType?: NoteType
|
||||
editorIdentifier?: FeatureIdentifier | string
|
||||
editorIdentifier?: string
|
||||
authorizedForListed?: boolean
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NoteMutator } from './NoteMutator'
|
||||
import { createNote } from './../../Utilities/Test/SpecUtils'
|
||||
import { MutationType } from '../../Abstract/Item'
|
||||
import { FeatureIdentifier, NoteType } from '@standardnotes/features'
|
||||
import { NativeFeatureIdentifier, NoteType } from '@standardnotes/features'
|
||||
|
||||
describe('note mutator', () => {
|
||||
it('sets noteType', () => {
|
||||
@@ -16,9 +16,9 @@ describe('note mutator', () => {
|
||||
it('sets componentIdentifier', () => {
|
||||
const note = createNote({})
|
||||
const mutator = new NoteMutator(note, MutationType.NoUpdateUserTimestamps)
|
||||
mutator.editorIdentifier = FeatureIdentifier.MarkdownProEditor
|
||||
mutator.editorIdentifier = NativeFeatureIdentifier.TYPES.MarkdownProEditor
|
||||
const result = mutator.getResult()
|
||||
|
||||
expect(result.content.editorIdentifier).toEqual(FeatureIdentifier.MarkdownProEditor)
|
||||
expect(result.content.editorIdentifier).toEqual(NativeFeatureIdentifier.TYPES.MarkdownProEditor)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,7 +3,7 @@ import { DecryptedItemMutator } from '../../Abstract/Item/Mutator/DecryptedItemM
|
||||
import { SNNote } from './Note'
|
||||
import { NoteToNoteReference } from '../../Abstract/Reference/NoteToNoteReference'
|
||||
import { ContentReferenceType } from '../../Abstract/Item'
|
||||
import { FeatureIdentifier, NoteType } from '@standardnotes/features'
|
||||
import { NoteType } from '@standardnotes/features'
|
||||
import { EditorLineWidth } from '../UserPrefs'
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
|
||||
@@ -40,7 +40,7 @@ export class NoteMutator extends DecryptedItemMutator<NoteContent> {
|
||||
this.mutableContent.noteType = noteType
|
||||
}
|
||||
|
||||
set editorIdentifier(identifier: FeatureIdentifier | string | undefined) {
|
||||
set editorIdentifier(identifier: string | undefined) {
|
||||
this.mutableContent.editorIdentifier = identifier
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { EditorIdentifier } from '@standardnotes/features'
|
||||
import { NewNoteTitleFormat } from '../UserPrefs'
|
||||
import { CollectionSortProperty } from './../../Runtime/Collection/CollectionSort'
|
||||
|
||||
@@ -15,7 +14,7 @@ export interface TagPreferences {
|
||||
hideEditorIcon?: boolean
|
||||
newNoteTitleFormat?: NewNoteTitleFormat
|
||||
customNoteTitleFormat?: string
|
||||
editorIdentifier?: EditorIdentifier
|
||||
editorIdentifier?: string
|
||||
entryMode?: 'normal' | 'daily'
|
||||
panelWidth?: number
|
||||
useTableView?: boolean
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
import { FeatureIdentifier } from '@standardnotes/features'
|
||||
|
||||
type UuidString = string
|
||||
|
||||
export type AllComponentPreferences = Record<FeatureIdentifier | UuidString, ComponentPreferencesEntry>
|
||||
export type AllComponentPreferences = Record<string, ComponentPreferencesEntry>
|
||||
|
||||
export type ComponentPreferencesEntry = Record<string, unknown>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FeatureIdentifier } from '@standardnotes/features'
|
||||
import { NativeFeatureIdentifier } from '@standardnotes/features'
|
||||
import { CollectionSort } from '../../Runtime/Collection/CollectionSort'
|
||||
import { EditorFontSize } from './EditorFontSize'
|
||||
import { EditorLineHeight } from './EditorLineHeight'
|
||||
@@ -29,7 +29,7 @@ export const PrefDefaults = {
|
||||
[PrefKey.NotesHideEditorIcon]: false,
|
||||
[PrefKey.UseSystemColorScheme]: false,
|
||||
[PrefKey.AutoLightThemeIdentifier]: 'Default',
|
||||
[PrefKey.AutoDarkThemeIdentifier]: FeatureIdentifier.DarkTheme,
|
||||
[PrefKey.AutoDarkThemeIdentifier]: NativeFeatureIdentifier.TYPES.DarkTheme,
|
||||
[PrefKey.NoteAddToParentFolders]: true,
|
||||
[PrefKey.NewNoteTitleFormat]: NewNoteTitleFormat.CurrentDateAndTime,
|
||||
[PrefKey.CustomNoteTitleFormat]: 'YYYY-MM-DD [at] hh:mm A',
|
||||
@@ -37,7 +37,7 @@ export const PrefDefaults = {
|
||||
[PrefKey.PaneGesturesEnabled]: true,
|
||||
[PrefKey.MomentsDefaultTagUuid]: undefined,
|
||||
[PrefKey.ClipperDefaultTagUuid]: undefined,
|
||||
[PrefKey.DefaultEditorIdentifier]: FeatureIdentifier.PlainEditor,
|
||||
[PrefKey.DefaultEditorIdentifier]: NativeFeatureIdentifier.TYPES.PlainEditor,
|
||||
[PrefKey.SuperNoteExportFormat]: 'json',
|
||||
[PrefKey.SystemViewPreferences]: {},
|
||||
[PrefKey.AuthenticatorNames]: '',
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { CollectionSortProperty } from '../../Runtime/Collection/CollectionSort'
|
||||
import { EditorIdentifier, FeatureIdentifier } from '@standardnotes/features'
|
||||
import { SystemViewId } from '../SmartView'
|
||||
import { TagPreferences } from '../Tag'
|
||||
import { NewNoteTitleFormat } from './NewNoteTitleFormat'
|
||||
@@ -67,8 +66,8 @@ export type PrefValue = {
|
||||
[PrefKey.NotesHideTags]: boolean
|
||||
[PrefKey.NotesHideEditorIcon]: boolean
|
||||
[PrefKey.UseSystemColorScheme]: boolean
|
||||
[PrefKey.AutoLightThemeIdentifier]: FeatureIdentifier | 'Default' | 'Dark'
|
||||
[PrefKey.AutoDarkThemeIdentifier]: FeatureIdentifier | 'Default' | 'Dark'
|
||||
[PrefKey.AutoLightThemeIdentifier]: string
|
||||
[PrefKey.AutoDarkThemeIdentifier]: string
|
||||
[PrefKey.NoteAddToParentFolders]: boolean
|
||||
[PrefKey.NewNoteTitleFormat]: NewNoteTitleFormat
|
||||
[PrefKey.CustomNoteTitleFormat]: string
|
||||
@@ -76,7 +75,7 @@ export type PrefValue = {
|
||||
[PrefKey.EditorLineWidth]: EditorLineWidth
|
||||
[PrefKey.EditorFontSize]: EditorFontSize
|
||||
[PrefKey.UpdateSavingStatusIndicator]: boolean
|
||||
[PrefKey.DefaultEditorIdentifier]: EditorIdentifier
|
||||
[PrefKey.DefaultEditorIdentifier]: string
|
||||
[PrefKey.MomentsDefaultTagUuid]: string | undefined
|
||||
[PrefKey.ClipperDefaultTagUuid]: string | undefined
|
||||
[PrefKey.SystemViewPreferences]: Partial<Record<SystemViewId, TagPreferences>>
|
||||
|
||||
@@ -3,13 +3,14 @@ import {
|
||||
ComponentArea,
|
||||
ComponentFeatureDescription,
|
||||
EditorFeatureDescription,
|
||||
EditorIdentifier,
|
||||
IframeComponentFeatureDescription,
|
||||
NativeFeatureIdentifier,
|
||||
ThemeFeatureDescription,
|
||||
} from '@standardnotes/features'
|
||||
import { ActionObserver, ComponentInterface, UIFeature, PermissionDialog, SNNote, SNTag } from '@standardnotes/models'
|
||||
import { DesktopManagerInterface } from '../Device/DesktopManagerInterface'
|
||||
import { ComponentViewerInterface } from './ComponentViewerInterface'
|
||||
import { Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
export interface ComponentManagerInterface {
|
||||
urlForFeature(uiFeature: UIFeature<ComponentFeatureDescription>): string | undefined
|
||||
@@ -31,12 +32,12 @@ export interface ComponentManagerInterface {
|
||||
setPermissionDialogUIHandler(handler: (dialog: PermissionDialog) => void): void
|
||||
|
||||
editorForNote(note: SNNote): UIFeature<EditorFeatureDescription | IframeComponentFeatureDescription>
|
||||
getDefaultEditorIdentifier(currentTag?: SNTag): EditorIdentifier
|
||||
getDefaultEditorIdentifier(currentTag?: SNTag): string
|
||||
|
||||
isThemeActive(theme: UIFeature<ThemeFeatureDescription>): boolean
|
||||
toggleTheme(theme: UIFeature<ThemeFeatureDescription>): Promise<void>
|
||||
getActiveThemes(): UIFeature<ThemeFeatureDescription>[]
|
||||
getActiveThemesIdentifiers(): string[]
|
||||
getActiveThemesIdentifiers(): { features: NativeFeatureIdentifier[]; uuids: Uuid[] }
|
||||
|
||||
isComponentActive(component: ComponentInterface): boolean
|
||||
toggleComponent(component: ComponentInterface): Promise<void>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { ActionObserver, ComponentEventObserver, ComponentMessage, UIFeature } from '@standardnotes/models'
|
||||
import { FeatureStatus } from '../Feature/FeatureStatus'
|
||||
import { ComponentViewerError } from './ComponentViewerError'
|
||||
import { IframeComponentFeatureDescription } from '@standardnotes/features'
|
||||
import { IframeComponentFeatureDescription, NativeFeatureIdentifier } from '@standardnotes/features'
|
||||
import { Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
export interface ComponentViewerInterface {
|
||||
readonly identifier: string
|
||||
@@ -9,7 +10,7 @@ export interface ComponentViewerInterface {
|
||||
readonly sessionKey?: string
|
||||
|
||||
get url(): string
|
||||
get componentUniqueIdentifier(): string
|
||||
get componentUniqueIdentifier(): NativeFeatureIdentifier | Uuid
|
||||
|
||||
getComponentOrFeatureItem(): UIFeature<IframeComponentFeatureDescription>
|
||||
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { FeatureIdentifier } from '@standardnotes/features'
|
||||
import { ComponentInterface, DecryptedItemInterface } from '@standardnotes/models'
|
||||
|
||||
import { FeatureStatus } from './FeatureStatus'
|
||||
import { SetOfflineFeaturesFunctionResponse } from './SetOfflineFeaturesFunctionResponse'
|
||||
import { NativeFeatureIdentifier } from '@standardnotes/features'
|
||||
import { Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
export interface FeaturesClientInterface {
|
||||
initializeFromDisk(): void
|
||||
getFeatureStatus(featureId: FeatureIdentifier, options?: { inContextOfItem?: DecryptedItemInterface }): FeatureStatus
|
||||
getFeatureStatus(
|
||||
featureId: NativeFeatureIdentifier | Uuid,
|
||||
options?: { inContextOfItem?: DecryptedItemInterface },
|
||||
): FeatureStatus
|
||||
hasMinimumRole(role: string): boolean
|
||||
|
||||
hasFirstPartyOfflineSubscription(): boolean
|
||||
@@ -16,13 +19,13 @@ export interface FeaturesClientInterface {
|
||||
|
||||
isThirdPartyFeature(identifier: string): boolean
|
||||
|
||||
toggleExperimentalFeature(identifier: FeatureIdentifier): void
|
||||
getExperimentalFeatures(): FeatureIdentifier[]
|
||||
getEnabledExperimentalFeatures(): FeatureIdentifier[]
|
||||
enableExperimentalFeature(identifier: FeatureIdentifier): void
|
||||
disableExperimentalFeature(identifier: FeatureIdentifier): void
|
||||
isExperimentalFeatureEnabled(identifier: FeatureIdentifier): boolean
|
||||
isExperimentalFeature(identifier: FeatureIdentifier): boolean
|
||||
toggleExperimentalFeature(identifier: string): void
|
||||
getExperimentalFeatures(): string[]
|
||||
getEnabledExperimentalFeatures(): string[]
|
||||
enableExperimentalFeature(identifier: string): void
|
||||
disableExperimentalFeature(identifier: string): void
|
||||
isExperimentalFeatureEnabled(identifier: string): boolean
|
||||
isExperimentalFeature(identifier: string): boolean
|
||||
|
||||
downloadRemoteThirdPartyFeature(urlOrCode: string): Promise<ComponentInterface | undefined>
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export enum SessionEvent {
|
||||
Restored = 'SessionRestored',
|
||||
Revoked = 'SessionRevoked',
|
||||
UserKeyPairChanged = 'UserKeyPairChanged',
|
||||
Restored = 'SessionEvent:SessionRestored',
|
||||
Revoked = 'SessionEvent:SessionRevoked',
|
||||
UserKeyPairChanged = 'SessionEvent:UserKeyPairChanged',
|
||||
}
|
||||
|
||||
@@ -18,8 +18,6 @@ export interface SessionsClientInterface {
|
||||
getWorkspaceDisplayIdentifier(): string
|
||||
populateSessionFromDemoShareToken(token: Base64String): Promise<void>
|
||||
|
||||
initializeFromDisk(): Promise<void>
|
||||
|
||||
getUser(): User | undefined
|
||||
isSignedIn(): boolean
|
||||
get userUuid(): string
|
||||
|
||||
@@ -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 { SessionsClientInterface } from './../Session/SessionsClientInterface'
|
||||
import { SubscriptionApiServiceInterface } from '@standardnotes/api'
|
||||
@@ -21,11 +24,66 @@ describe('SubscriptionManager', () => {
|
||||
subscriptionApiService.listInvites = jest.fn()
|
||||
|
||||
sessions = {} as jest.Mocked<SessionsClientInterface>
|
||||
sessions.isSignedIn = jest.fn().mockReturnValue(true)
|
||||
|
||||
storage = {} as jest.Mocked<StorageServiceInterface>
|
||||
|
||||
internalEventBus = {} as jest.Mocked<InternalEventBusInterface>
|
||||
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 () => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { SessionEvent } from './../Session/SessionEvent'
|
||||
import { StorageKey } from './../Storage/StorageKeys'
|
||||
import { ApplicationStage } from './../Application/ApplicationStage'
|
||||
import { StorageServiceInterface } from './../Storage/StorageServiceInterface'
|
||||
@@ -51,6 +52,7 @@ export class SubscriptionManager
|
||||
}
|
||||
|
||||
case ApplicationEvent.UserRolesChanged:
|
||||
case SessionEvent.Restored:
|
||||
case ApplicationEvent.SignedIn:
|
||||
void this.fetchOnlineSubscription()
|
||||
break
|
||||
@@ -58,13 +60,17 @@ export class SubscriptionManager
|
||||
case ApplicationEvent.ApplicationStageChanged: {
|
||||
const stage = (event.payload as ApplicationStageChangedEventPayload).stage
|
||||
if (stage === ApplicationStage.StorageDecrypted_09) {
|
||||
this.onlineSubscription = this.storage.getValue(StorageKey.Subscription)
|
||||
void this.notifyEvent(SubscriptionManagerEvent.DidFetchSubscription)
|
||||
this.loadSubscriptionFromStorage()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loadSubscriptionFromStorage(): void {
|
||||
this.onlineSubscription = this.storage.getValue(StorageKey.Subscription)
|
||||
void this.notifyEvent(SubscriptionManagerEvent.DidFetchSubscription)
|
||||
}
|
||||
|
||||
hasOnlineSubscription(): boolean {
|
||||
return this.onlineSubscription != undefined
|
||||
}
|
||||
@@ -204,7 +210,7 @@ export class SubscriptionManager
|
||||
void this.notifyEvent(SubscriptionManagerEvent.DidFetchSubscription)
|
||||
}
|
||||
|
||||
private async fetchAvailableSubscriptions(): Promise<void> {
|
||||
async fetchAvailableSubscriptions(): Promise<void> {
|
||||
try {
|
||||
const response = await this.subscriptionApiService.getAvailableSubscriptions()
|
||||
|
||||
|
||||
@@ -442,12 +442,8 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
|
||||
this.sockets.loadWebSocketUrl()
|
||||
|
||||
await this.sessions.initializeFromDisk()
|
||||
|
||||
this.settings.initializeFromDisk()
|
||||
|
||||
this.features.initializeFromDisk()
|
||||
|
||||
this.launched = true
|
||||
await this.notifyEvent(ApplicationEvent.Launched)
|
||||
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.UserService), AccountEvent.SignedInOrRegistered)
|
||||
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), 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.dependencies.get(TYPES.SelfContactManager),
|
||||
ApplicationEvent.ApplicationStageChanged,
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { ApplicationStage } from '@standardnotes/services'
|
||||
import { FeatureIdentifier } from '@standardnotes/features'
|
||||
import { Migration } from '@Lib/Migrations/Migration'
|
||||
import { ThemeInterface } from '@standardnotes/models'
|
||||
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 {
|
||||
static override version(): string {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FeaturesService } from '@Lib/Services/Features/FeaturesService'
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
import { ContentType, Uuid } from '@standardnotes/domain-core'
|
||||
import {
|
||||
ActionObserver,
|
||||
PayloadEmitSource,
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
UIFeature,
|
||||
ComponentInterface,
|
||||
PrefKey,
|
||||
ThemeInterface,
|
||||
ComponentPreferencesEntry,
|
||||
AllComponentPreferences,
|
||||
SNNote,
|
||||
@@ -21,15 +20,14 @@ import {
|
||||
import {
|
||||
ComponentArea,
|
||||
FindNativeFeature,
|
||||
FeatureIdentifier,
|
||||
EditorFeatureDescription,
|
||||
FindNativeTheme,
|
||||
IframeComponentFeatureDescription,
|
||||
ComponentFeatureDescription,
|
||||
ThemeFeatureDescription,
|
||||
EditorIdentifier,
|
||||
GetIframeEditors,
|
||||
GetNativeThemes,
|
||||
NativeFeatureIdentifier,
|
||||
} from '@standardnotes/features'
|
||||
import { Copy, removeFromArray, sleep, isNotUndefined } from '@standardnotes/utils'
|
||||
import { ComponentViewer } from '@Lib/Services/ComponentManager/ComponentViewer'
|
||||
@@ -287,7 +285,7 @@ export class SNComponentManager
|
||||
const url = this.urlForFeature(feature)
|
||||
|
||||
if (url) {
|
||||
this.device.registerComponentUrl(feature.uniqueIdentifier, url)
|
||||
this.device.registerComponentUrl(feature.uniqueIdentifier.value, url)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -372,7 +370,7 @@ export class SNComponentManager
|
||||
return
|
||||
}
|
||||
|
||||
const featureStatus = this.features.getFeatureStatus(uiFeature.featureIdentifier)
|
||||
const featureStatus = this.features.getFeatureStatus(uiFeature.uniqueIdentifier)
|
||||
if (featureStatus !== FeatureStatus.Entitled) {
|
||||
return
|
||||
}
|
||||
@@ -398,28 +396,50 @@ export class SNComponentManager
|
||||
}
|
||||
|
||||
public getActiveThemes(): UIFeature<ThemeFeatureDescription>[] {
|
||||
const activeThemesIdentifiers = this.getActiveThemesIdentifiers()
|
||||
const { features, uuids } = this.getActiveThemesIdentifiers()
|
||||
|
||||
const thirdPartyThemes = this.items.findItems<ThemeInterface>(activeThemesIdentifiers).map((item) => {
|
||||
return new UIFeature<ThemeFeatureDescription>(item)
|
||||
})
|
||||
const thirdPartyThemes = uuids
|
||||
.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) => {
|
||||
return FindNativeTheme(identifier as FeatureIdentifier)
|
||||
return FindNativeTheme(identifier.value)
|
||||
})
|
||||
.filter(isNotUndefined)
|
||||
.map((theme) => new UIFeature(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
|
||||
}
|
||||
|
||||
public getActiveThemesIdentifiers(): string[] {
|
||||
return this.preferences.getValue(PrefKey.ActiveThemes, undefined) ?? []
|
||||
public getActiveThemesIdentifiers(): { features: NativeFeatureIdentifier[]; uuids: Uuid[] } {
|
||||
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> {
|
||||
@@ -437,7 +457,7 @@ export class SNComponentManager
|
||||
return usecase.execute(note)
|
||||
}
|
||||
|
||||
getDefaultEditorIdentifier(currentTag?: SNTag): EditorIdentifier {
|
||||
getDefaultEditorIdentifier(currentTag?: SNTag): string {
|
||||
const usecase = new GetDefaultEditorIdentifier(this.preferences, this.items)
|
||||
return usecase.execute(currentTag).getValue()
|
||||
}
|
||||
@@ -468,7 +488,7 @@ export class SNComponentManager
|
||||
this.preferences.getValue(PrefKey.ComponentPreferences, undefined) ?? {},
|
||||
)
|
||||
|
||||
const preferencesLookupKey = uiFeature.uniqueIdentifier
|
||||
const preferencesLookupKey = uiFeature.uniqueIdentifier.value
|
||||
|
||||
mutablePreferencesValue[preferencesLookupKey] = preferences
|
||||
|
||||
@@ -482,7 +502,7 @@ export class SNComponentManager
|
||||
return undefined
|
||||
}
|
||||
|
||||
const preferencesLookupKey = component.uniqueIdentifier
|
||||
const preferencesLookupKey = component.uniqueIdentifier.value
|
||||
|
||||
return preferences[preferencesLookupKey]
|
||||
}
|
||||
@@ -490,31 +510,31 @@ export class SNComponentManager
|
||||
async addActiveTheme(theme: UIFeature<ThemeFeatureDescription>): Promise<void> {
|
||||
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)
|
||||
}
|
||||
|
||||
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> {
|
||||
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)
|
||||
}
|
||||
|
||||
isThemeActive(theme: UIFeature<ThemeFeatureDescription>): boolean {
|
||||
if (this.features.getFeatureStatus(theme.featureIdentifier) !== FeatureStatus.Entitled) {
|
||||
if (this.features.getFeatureStatus(theme.uniqueIdentifier) !== FeatureStatus.Entitled) {
|
||||
return false
|
||||
}
|
||||
|
||||
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> {
|
||||
|
||||
@@ -58,6 +58,7 @@ import {
|
||||
ComponentPermission,
|
||||
ComponentArea,
|
||||
IframeComponentFeatureDescription,
|
||||
NativeFeatureIdentifier,
|
||||
} from '@standardnotes/features'
|
||||
import {
|
||||
isString,
|
||||
@@ -72,7 +73,7 @@ import {
|
||||
isNotUndefined,
|
||||
uniqueArray,
|
||||
} from '@standardnotes/utils'
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
import { ContentType, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
export class ComponentViewer implements ComponentViewerInterface {
|
||||
private streamItems?: string[]
|
||||
@@ -214,12 +215,12 @@ export class ComponentViewer implements ComponentViewerInterface {
|
||||
this.readonly = readonly
|
||||
}
|
||||
|
||||
get componentUniqueIdentifier(): string {
|
||||
get componentUniqueIdentifier(): NativeFeatureIdentifier | Uuid {
|
||||
return this.componentOrFeature.uniqueIdentifier
|
||||
}
|
||||
|
||||
public getFeatureStatus(): FeatureStatus {
|
||||
return this.services.features.getFeatureStatus(this.componentOrFeature.featureIdentifier, {
|
||||
return this.services.features.getFeatureStatus(this.componentUniqueIdentifier, {
|
||||
inContextOfItem: this.getContextItem(),
|
||||
})
|
||||
}
|
||||
@@ -269,7 +270,9 @@ export class ComponentViewer implements ComponentViewerInterface {
|
||||
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) {
|
||||
return
|
||||
}
|
||||
@@ -289,7 +292,7 @@ export class ComponentViewer implements ComponentViewerInterface {
|
||||
|
||||
this.updateOurComponentRefFromChangedItems(nondeletedItems)
|
||||
|
||||
const areWeOriginator = sourceKey && sourceKey === this.componentUniqueIdentifier
|
||||
const areWeOriginator = sourceKey && sourceKey === this.componentUniqueIdentifier.value
|
||||
if (areWeOriginator) {
|
||||
return
|
||||
}
|
||||
@@ -326,7 +329,7 @@ export class ComponentViewer implements ComponentViewerInterface {
|
||||
]
|
||||
|
||||
this.config.componentManagerFunctions.runWithPermissionsUseCase.execute(
|
||||
this.componentUniqueIdentifier,
|
||||
this.componentUniqueIdentifier.value,
|
||||
requiredPermissions,
|
||||
() => {
|
||||
this.sendItemsInReply(items, this.streamItemsOriginalMessage as ComponentMessage)
|
||||
@@ -341,7 +344,7 @@ export class ComponentViewer implements ComponentViewerInterface {
|
||||
},
|
||||
] as ComponentPermission[]
|
||||
this.config.componentManagerFunctions.runWithPermissionsUseCase.execute(
|
||||
this.componentUniqueIdentifier,
|
||||
this.componentUniqueIdentifier.value,
|
||||
requiredContextPermissions,
|
||||
() => {
|
||||
this.log(
|
||||
@@ -414,7 +417,7 @@ export class ComponentViewer implements ComponentViewerInterface {
|
||||
|
||||
private getClientData(item: DecryptedItemInterface): Record<string, unknown> {
|
||||
const globalComponentData = item.getDomainData(ComponentDataDomain) || {}
|
||||
const thisComponentData = globalComponentData[this.componentUniqueIdentifier] || {}
|
||||
const thisComponentData = globalComponentData[this.componentUniqueIdentifier.value] || {}
|
||||
return thisComponentData as Record<string, unknown>
|
||||
}
|
||||
|
||||
@@ -530,7 +533,7 @@ export class ComponentViewer implements ComponentViewerInterface {
|
||||
sessionKey: this.sessionKey,
|
||||
componentData: componentData,
|
||||
data: {
|
||||
uuid: this.componentUniqueIdentifier,
|
||||
uuid: this.componentUniqueIdentifier.value,
|
||||
environment: environmentToString(this.config.environment),
|
||||
platform: platformToString(this.config.platform),
|
||||
activeThemeUrls: this.config.componentManagerFunctions.urlsForActiveThemes(),
|
||||
@@ -602,7 +605,7 @@ export class ComponentViewer implements ComponentViewerInterface {
|
||||
},
|
||||
]
|
||||
this.config.componentManagerFunctions.runWithPermissionsUseCase.execute(
|
||||
this.componentUniqueIdentifier,
|
||||
this.componentUniqueIdentifier.value,
|
||||
requiredPermissions,
|
||||
() => {
|
||||
if (!this.streamItems) {
|
||||
@@ -627,7 +630,7 @@ export class ComponentViewer implements ComponentViewerInterface {
|
||||
]
|
||||
|
||||
this.config.componentManagerFunctions.runWithPermissionsUseCase.execute(
|
||||
this.componentUniqueIdentifier,
|
||||
this.componentUniqueIdentifier.value,
|
||||
requiredPermissions,
|
||||
() => {
|
||||
if (!this.streamContextItemOriginalMessage) {
|
||||
@@ -684,7 +687,7 @@ export class ComponentViewer implements ComponentViewerInterface {
|
||||
}
|
||||
|
||||
this.config.componentManagerFunctions.runWithPermissionsUseCase.execute(
|
||||
this.componentUniqueIdentifier,
|
||||
this.componentUniqueIdentifier.value,
|
||||
requiredPermissions,
|
||||
|
||||
async () => {
|
||||
@@ -766,13 +769,13 @@ export class ComponentViewer implements ComponentViewerInterface {
|
||||
const allComponentData = Copy<Record<string, unknown>>(
|
||||
mutator.getItem().getDomainData(ComponentDataDomain) || {},
|
||||
)
|
||||
allComponentData[this.componentUniqueIdentifier] = responseItem.clientData
|
||||
allComponentData[this.componentUniqueIdentifier.value] = responseItem.clientData
|
||||
mutator.setDomainData(allComponentData, ComponentDataDomain)
|
||||
}
|
||||
},
|
||||
MutationType.UpdateUserTimestamps,
|
||||
PayloadEmitSource.ComponentRetrieved,
|
||||
this.componentUniqueIdentifier,
|
||||
this.componentUniqueIdentifier.value,
|
||||
)
|
||||
|
||||
this.services.sync
|
||||
@@ -807,7 +810,7 @@ export class ComponentViewer implements ComponentViewerInterface {
|
||||
]
|
||||
|
||||
this.config.componentManagerFunctions.runWithPermissionsUseCase.execute(
|
||||
this.componentUniqueIdentifier,
|
||||
this.componentUniqueIdentifier.value,
|
||||
requiredPermissions,
|
||||
async () => {
|
||||
responseItems = this.responseItemsByRemovingPrivateProperties(responseItems)
|
||||
@@ -834,13 +837,13 @@ export class ComponentViewer implements ComponentViewerInterface {
|
||||
const allComponentClientData = Copy<Record<string, unknown>>(
|
||||
item.getDomainData(ComponentDataDomain) || {},
|
||||
)
|
||||
allComponentClientData[this.componentUniqueIdentifier] = responseItem.clientData
|
||||
allComponentClientData[this.componentUniqueIdentifier.value] = responseItem.clientData
|
||||
mutator.setDomainData(allComponentClientData, ComponentDataDomain)
|
||||
}
|
||||
},
|
||||
MutationType.UpdateUserTimestamps,
|
||||
PayloadEmitSource.ComponentCreated,
|
||||
this.componentUniqueIdentifier,
|
||||
this.componentUniqueIdentifier.value,
|
||||
)
|
||||
processedItems.push(item)
|
||||
}
|
||||
@@ -874,7 +877,7 @@ export class ComponentViewer implements ComponentViewerInterface {
|
||||
]
|
||||
|
||||
this.config.componentManagerFunctions.runWithPermissionsUseCase.execute(
|
||||
this.componentUniqueIdentifier,
|
||||
this.componentUniqueIdentifier.value,
|
||||
requiredPermissions,
|
||||
async () => {
|
||||
const itemsData = items
|
||||
@@ -911,7 +914,7 @@ export class ComponentViewer implements ComponentViewerInterface {
|
||||
private handleSetComponentPreferencesMessage(message: ComponentMessage): void {
|
||||
const noPermissionsRequired: ComponentPermission[] = []
|
||||
this.config.componentManagerFunctions.runWithPermissionsUseCase.execute(
|
||||
this.componentUniqueIdentifier,
|
||||
this.componentUniqueIdentifier.value,
|
||||
noPermissionsRequired,
|
||||
async () => {
|
||||
const newPreferences = <ComponentPreferencesEntry | undefined>message.data.componentData
|
||||
|
||||
@@ -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 { UuidString } from '@Lib/Types/UuidString'
|
||||
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
|
||||
*/
|
||||
export const AllowedBatchStreaming = Object.freeze([
|
||||
LegacyFileSafeIdentifier,
|
||||
FeatureIdentifier.DeprecatedFileSafe,
|
||||
FeatureIdentifier.DeprecatedBoldEditor,
|
||||
NativeFeatureIdentifier.TYPES.LegacyFileSafeIdentifier,
|
||||
NativeFeatureIdentifier.TYPES.DeprecatedFileSafe,
|
||||
NativeFeatureIdentifier.TYPES.DeprecatedBoldEditor,
|
||||
])
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {
|
||||
FeatureIdentifier,
|
||||
NativeFeatureIdentifier,
|
||||
FindNativeFeature,
|
||||
IframeComponentFeatureDescription,
|
||||
UIFeatureDescriptionTypes,
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
import { DoesEditorChangeRequireAlertUseCase } from './DoesEditorChangeRequireAlert'
|
||||
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)!)
|
||||
}
|
||||
|
||||
@@ -19,57 +19,77 @@ describe('editor change alert', () => {
|
||||
})
|
||||
|
||||
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)
|
||||
expect(requiresAlert).toBe(false)
|
||||
})
|
||||
|
||||
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)
|
||||
expect(requiresAlert).toBe(false)
|
||||
})
|
||||
|
||||
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>(
|
||||
FeatureIdentifier.MarkdownProEditor,
|
||||
NativeFeatureIdentifier.TYPES.MarkdownProEditor,
|
||||
)
|
||||
const requiresAlert = usecase.execute(markdownEditor, htmlEditor)
|
||||
expect(requiresAlert).toBe(false)
|
||||
})
|
||||
|
||||
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>(
|
||||
FeatureIdentifier.MarkdownProEditor,
|
||||
NativeFeatureIdentifier.TYPES.MarkdownProEditor,
|
||||
)
|
||||
const requiresAlert = usecase.execute(htmlEditor, markdownEditor)
|
||||
expect(requiresAlert).toBe(false)
|
||||
})
|
||||
|
||||
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)
|
||||
expect(requiresAlert).toBe(false)
|
||||
})
|
||||
|
||||
it('should require alert switching from a html editor to custom editor', () => {
|
||||
const htmlEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(FeatureIdentifier.PlusEditor)!
|
||||
const customEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(FeatureIdentifier.TokenVaultEditor)
|
||||
const htmlEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
|
||||
NativeFeatureIdentifier.TYPES.PlusEditor,
|
||||
)!
|
||||
const customEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
|
||||
NativeFeatureIdentifier.TYPES.TokenVaultEditor,
|
||||
)
|
||||
const requiresAlert = usecase.execute(htmlEditor, customEditor)
|
||||
expect(requiresAlert).toBe(true)
|
||||
})
|
||||
|
||||
it('should require alert switching from a custom editor to html editor', () => {
|
||||
const htmlEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(FeatureIdentifier.PlusEditor)!
|
||||
const customEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(FeatureIdentifier.TokenVaultEditor)
|
||||
const htmlEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
|
||||
NativeFeatureIdentifier.TYPES.PlusEditor,
|
||||
)!
|
||||
const customEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
|
||||
NativeFeatureIdentifier.TYPES.TokenVaultEditor,
|
||||
)
|
||||
const requiresAlert = usecase.execute(customEditor, htmlEditor)
|
||||
expect(requiresAlert).toBe(true)
|
||||
})
|
||||
|
||||
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)
|
||||
expect(requiresAlert).toBe(true)
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createNote } from '@Lib/Spec/SpecUtils'
|
||||
import { FeatureIdentifier, NoteType } from '@standardnotes/features'
|
||||
import { NativeFeatureIdentifier, NoteType } from '@standardnotes/features'
|
||||
import { EditorForNoteUseCase } from './EditorForNote'
|
||||
import { ItemManagerInterface } from '@standardnotes/services'
|
||||
|
||||
@@ -17,7 +17,7 @@ describe('EditorForNote', () => {
|
||||
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', () => {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {
|
||||
ComponentArea,
|
||||
EditorFeatureDescription,
|
||||
FeatureIdentifier,
|
||||
FindNativeFeature,
|
||||
GetIframeAndNativeEditors,
|
||||
GetPlainNoteFeature,
|
||||
@@ -47,11 +46,9 @@ export class EditorForNoteUseCase {
|
||||
}
|
||||
|
||||
private componentOrNativeFeatureForIdentifier(
|
||||
identifier: FeatureIdentifier | string,
|
||||
identifier: string,
|
||||
): UIFeature<EditorFeatureDescription | IframeComponentFeatureDescription> | undefined {
|
||||
const nativeFeature = FindNativeFeature<EditorFeatureDescription | IframeComponentFeatureDescription>(
|
||||
identifier as FeatureIdentifier,
|
||||
)
|
||||
const nativeFeature = FindNativeFeature<EditorFeatureDescription | IframeComponentFeatureDescription>(identifier)
|
||||
if (nativeFeature) {
|
||||
return new UIFeature(nativeFeature)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ItemManagerInterface, PreferenceServiceInterface } from '@standardnotes/services'
|
||||
import { GetDefaultEditorIdentifier } from './GetDefaultEditorIdentifier'
|
||||
import { ComponentArea, FeatureIdentifier } from '@standardnotes/features'
|
||||
import { ComponentArea, NativeFeatureIdentifier } from '@standardnotes/features'
|
||||
import { SNComponent, SNTag } from '@standardnotes/models'
|
||||
|
||||
describe('getDefaultEditorIdentifier', () => {
|
||||
@@ -21,33 +21,33 @@ describe('getDefaultEditorIdentifier', () => {
|
||||
it('should return plain editor if no default tag editor or component editor', () => {
|
||||
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', () => {
|
||||
preferences.getValue = jest.fn().mockReturnValue(FeatureIdentifier.SuperEditor)
|
||||
preferences.getValue = jest.fn().mockReturnValue(NativeFeatureIdentifier.TYPES.SuperEditor)
|
||||
|
||||
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', () => {
|
||||
const tag = {
|
||||
preferences: {
|
||||
editorIdentifier: FeatureIdentifier.SuperEditor,
|
||||
editorIdentifier: NativeFeatureIdentifier.TYPES.SuperEditor,
|
||||
},
|
||||
} as jest.Mocked<SNTag>
|
||||
|
||||
const editorIdentifier = usecase.execute(tag).getValue()
|
||||
|
||||
expect(editorIdentifier).toEqual(FeatureIdentifier.SuperEditor)
|
||||
expect(editorIdentifier).toEqual(NativeFeatureIdentifier.TYPES.SuperEditor)
|
||||
})
|
||||
|
||||
it('should return legacy editor identifier', () => {
|
||||
const editor = {
|
||||
legacyIsDefaultEditor: jest.fn().mockReturnValue(true),
|
||||
identifier: FeatureIdentifier.MarkdownProEditor,
|
||||
identifier: NativeFeatureIdentifier.TYPES.MarkdownProEditor,
|
||||
area: ComponentArea.Editor,
|
||||
} as unknown as jest.Mocked<SNComponent>
|
||||
|
||||
@@ -55,6 +55,6 @@ describe('getDefaultEditorIdentifier', () => {
|
||||
|
||||
const editorIdentifier = usecase.execute().getValue()
|
||||
|
||||
expect(editorIdentifier).toEqual(FeatureIdentifier.MarkdownProEditor)
|
||||
expect(editorIdentifier).toEqual(NativeFeatureIdentifier.TYPES.MarkdownProEditor)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
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 { ItemManagerInterface, PreferenceServiceInterface } from '@standardnotes/services'
|
||||
|
||||
export class GetDefaultEditorIdentifier implements SyncUseCaseInterface<EditorIdentifier> {
|
||||
export class GetDefaultEditorIdentifier implements SyncUseCaseInterface<string> {
|
||||
constructor(private preferences: PreferenceServiceInterface, private items: ItemManagerInterface) {}
|
||||
|
||||
execute(currentTag?: SNTag): Result<EditorIdentifier> {
|
||||
execute(currentTag?: SNTag): Result<string> {
|
||||
if (currentTag) {
|
||||
const editorIdentifier = currentTag?.preferences?.editorIdentifier
|
||||
if (editorIdentifier) {
|
||||
@@ -25,7 +25,7 @@ export class GetDefaultEditorIdentifier implements SyncUseCaseInterface<EditorId
|
||||
return Result.ok(matchingEditor.identifier)
|
||||
}
|
||||
|
||||
return Result.ok(FeatureIdentifier.PlainEditor)
|
||||
return Result.ok(NativeFeatureIdentifier.TYPES.PlainEditor)
|
||||
}
|
||||
|
||||
thirdPartyComponentsForArea(area: ComponentArea): ComponentInterface[] {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
import {
|
||||
FeatureIdentifier,
|
||||
NativeFeatureIdentifier,
|
||||
FindNativeFeature,
|
||||
IframeComponentFeatureDescription,
|
||||
UIFeatureDescriptionTypes,
|
||||
@@ -21,7 +21,7 @@ import { GetFeatureUrl } from './GetFeatureUrl'
|
||||
|
||||
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)!)
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ const thirdPartyFeature = () => {
|
||||
local_url: 'sn://Extensions/non-native-identifier/dist/index.html',
|
||||
hosted_url: 'https://example.com/component',
|
||||
package_info: {
|
||||
identifier: 'non-native-identifier' as FeatureIdentifier,
|
||||
identifier: 'non-native-identifier',
|
||||
expires_at: new Date().getTime(),
|
||||
availableInRoles: [],
|
||||
} as unknown as jest.Mocked<ComponentPackageInfo>,
|
||||
@@ -75,7 +75,9 @@ describe('GetFeatureUrl', () => {
|
||||
})
|
||||
|
||||
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)
|
||||
expect(url).toEqual(
|
||||
`${desktopExtHost}/components/${feature.featureIdentifier}/${feature.asFeatureDescription.index_path}`,
|
||||
@@ -84,7 +86,7 @@ describe('GetFeatureUrl', () => {
|
||||
|
||||
it('returns native path for deprecated native component', () => {
|
||||
const feature = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
|
||||
FeatureIdentifier.DeprecatedBoldEditor,
|
||||
NativeFeatureIdentifier.TYPES.DeprecatedBoldEditor,
|
||||
)!
|
||||
const url = usecase.execute(feature)
|
||||
expect(url).toEqual(
|
||||
@@ -122,7 +124,9 @@ describe('GetFeatureUrl', () => {
|
||||
})
|
||||
|
||||
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)
|
||||
expect(url).toEqual(
|
||||
`http://localhost/components/assets/${feature.featureIdentifier}/${feature.asFeatureDescription.index_path}`,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ContentType } from '@standardnotes/domain-core'
|
||||
import {
|
||||
ComponentAction,
|
||||
ComponentPermission,
|
||||
FeatureIdentifier,
|
||||
NativeFeatureIdentifier,
|
||||
FindNativeFeature,
|
||||
UIFeatureDescriptionTypes,
|
||||
} from '@standardnotes/features'
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
SyncServiceInterface,
|
||||
} from '@standardnotes/services'
|
||||
|
||||
const nativeFeatureAsUIFeature = <F extends UIFeatureDescriptionTypes>(identifier: FeatureIdentifier) => {
|
||||
const nativeFeatureAsUIFeature = <F extends UIFeatureDescriptionTypes>(identifier: string) => {
|
||||
return new UIFeature(FindNativeFeature<F>(identifier)!)
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ describe('RunWithPermissionsUseCase', () => {
|
||||
|
||||
expect(
|
||||
usecase.areRequestedPermissionsValid(
|
||||
nativeFeatureAsUIFeature(FeatureIdentifier.MarkdownProEditor),
|
||||
nativeFeatureAsUIFeature(NativeFeatureIdentifier.TYPES.MarkdownProEditor),
|
||||
permissions,
|
||||
),
|
||||
).toEqual(true)
|
||||
@@ -59,7 +59,7 @@ describe('RunWithPermissionsUseCase', () => {
|
||||
|
||||
expect(
|
||||
usecase.areRequestedPermissionsValid(
|
||||
nativeFeatureAsUIFeature(FeatureIdentifier.MarkdownProEditor),
|
||||
nativeFeatureAsUIFeature(NativeFeatureIdentifier.TYPES.MarkdownProEditor),
|
||||
permissions,
|
||||
),
|
||||
).toEqual(false)
|
||||
@@ -75,7 +75,7 @@ describe('RunWithPermissionsUseCase', () => {
|
||||
|
||||
expect(
|
||||
usecase.areRequestedPermissionsValid(
|
||||
nativeFeatureAsUIFeature(FeatureIdentifier.MarkdownProEditor),
|
||||
nativeFeatureAsUIFeature(NativeFeatureIdentifier.TYPES.MarkdownProEditor),
|
||||
permissions,
|
||||
),
|
||||
).toEqual(false)
|
||||
@@ -91,7 +91,7 @@ describe('RunWithPermissionsUseCase', () => {
|
||||
|
||||
expect(
|
||||
usecase.areRequestedPermissionsValid(
|
||||
nativeFeatureAsUIFeature(FeatureIdentifier.MarkdownProEditor),
|
||||
nativeFeatureAsUIFeature(NativeFeatureIdentifier.TYPES.MarkdownProEditor),
|
||||
permissions,
|
||||
),
|
||||
).toEqual(false)
|
||||
@@ -107,7 +107,7 @@ describe('RunWithPermissionsUseCase', () => {
|
||||
|
||||
expect(
|
||||
usecase.areRequestedPermissionsValid(
|
||||
nativeFeatureAsUIFeature(FeatureIdentifier.DeprecatedFileSafe),
|
||||
nativeFeatureAsUIFeature(NativeFeatureIdentifier.TYPES.DeprecatedFileSafe),
|
||||
permissions,
|
||||
),
|
||||
).toEqual(false)
|
||||
@@ -127,7 +127,7 @@ describe('RunWithPermissionsUseCase', () => {
|
||||
|
||||
expect(
|
||||
usecase.areRequestedPermissionsValid(
|
||||
nativeFeatureAsUIFeature(FeatureIdentifier.DeprecatedFileSafe),
|
||||
nativeFeatureAsUIFeature(NativeFeatureIdentifier.TYPES.DeprecatedFileSafe),
|
||||
permissions,
|
||||
),
|
||||
).toEqual(true)
|
||||
@@ -147,7 +147,7 @@ describe('RunWithPermissionsUseCase', () => {
|
||||
|
||||
expect(
|
||||
usecase.areRequestedPermissionsValid(
|
||||
nativeFeatureAsUIFeature(FeatureIdentifier.DeprecatedBoldEditor),
|
||||
nativeFeatureAsUIFeature(NativeFeatureIdentifier.TYPES.DeprecatedBoldEditor),
|
||||
permissions,
|
||||
),
|
||||
).toEqual(true)
|
||||
@@ -166,7 +166,10 @@ describe('RunWithPermissionsUseCase', () => {
|
||||
]
|
||||
|
||||
expect(
|
||||
usecase.areRequestedPermissionsValid(nativeFeatureAsUIFeature(FeatureIdentifier.PlusEditor), permissions),
|
||||
usecase.areRequestedPermissionsValid(
|
||||
nativeFeatureAsUIFeature(NativeFeatureIdentifier.TYPES.PlusEditor),
|
||||
permissions,
|
||||
),
|
||||
).toEqual(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,7 +2,6 @@ import {
|
||||
ComponentAction,
|
||||
ComponentFeatureDescription,
|
||||
ComponentPermission,
|
||||
FeatureIdentifier,
|
||||
FindNativeFeature,
|
||||
} from '@standardnotes/features'
|
||||
import { ComponentInterface, ComponentMutator, PermissionDialog, UIFeature } from '@standardnotes/models'
|
||||
@@ -228,7 +227,7 @@ export class RunWithPermissionsUseCase {
|
||||
}
|
||||
|
||||
private findUIFeature(identifier: string): UIFeature<ComponentFeatureDescription> | undefined {
|
||||
const nativeFeature = FindNativeFeature<ComponentFeatureDescription>(identifier as FeatureIdentifier)
|
||||
const nativeFeature = FindNativeFeature<ComponentFeatureDescription>(identifier)
|
||||
if (nativeFeature) {
|
||||
return new UIFeature(nativeFeature)
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ import { ItemInterface, SNFeatureRepo } from '@standardnotes/models'
|
||||
import { SyncService } from '../Sync/SyncService'
|
||||
import { SettingName } from '@standardnotes/settings'
|
||||
import { FeaturesService } from '@Lib/Services/Features'
|
||||
import { RoleName, ContentType } from '@standardnotes/domain-core'
|
||||
import { FeatureIdentifier, GetFeatures } from '@standardnotes/features'
|
||||
import { RoleName, ContentType, Uuid } from '@standardnotes/domain-core'
|
||||
import { NativeFeatureIdentifier, GetFeatures } from '@standardnotes/features'
|
||||
import { WebSocketsService } from '../Api/WebsocketsService'
|
||||
import { SettingsService } from '../Settings'
|
||||
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
@@ -44,6 +44,7 @@ describe('FeaturesService', () => {
|
||||
let roles: string[]
|
||||
let items: ItemInterface[]
|
||||
let internalEventBus: InternalEventBusInterface
|
||||
let featureService: FeaturesService
|
||||
|
||||
const createService = () => {
|
||||
return new FeaturesService(
|
||||
@@ -118,23 +119,81 @@ describe('FeaturesService', () => {
|
||||
internalEventBus = {} as jest.Mocked<InternalEventBusInterface>
|
||||
internalEventBus.publish = 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', () => {
|
||||
it('enables/disables an experimental feature', async () => {
|
||||
storageService.getValue = jest.fn().mockReturnValue(GetFeatures())
|
||||
|
||||
const featuresService = createService()
|
||||
featuresService.getExperimentalFeatures = jest.fn().mockReturnValue([FeatureIdentifier.PlusEditor])
|
||||
featuresService.initializeFromDisk()
|
||||
featureService.getExperimentalFeatures = jest.fn().mockReturnValue([NativeFeatureIdentifier.TYPES.PlusEditor])
|
||||
featureService.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()', () => {
|
||||
it('setRoles should notify event if roles changed', async () => {
|
||||
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]
|
||||
featuresService.setOnlineRoles(newRoles)
|
||||
featureService.setOnlineRoles(newRoles)
|
||||
|
||||
expect(mock.mock.calls[0][0]).toEqual(FeaturesEvent.UserRolesChanged)
|
||||
})
|
||||
|
||||
it('should notify of subscription purchase', async () => {
|
||||
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]
|
||||
await featuresService.updateOnlineRolesWithNewValues(newRoles)
|
||||
await featureService.updateOnlineRolesWithNewValues(newRoles)
|
||||
|
||||
expect(spy.mock.calls[1][0]).toEqual(FeaturesEvent.DidPurchaseSubscription)
|
||||
})
|
||||
|
||||
it('should not notify of subscription purchase on initial roles load after sign in', async () => {
|
||||
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]
|
||||
await featuresService.updateOnlineRolesWithNewValues(newRoles)
|
||||
await featureService.updateOnlineRolesWithNewValues(newRoles)
|
||||
|
||||
const triggeredEvents = spy.mock.calls.map((call) => call[0])
|
||||
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 () => {
|
||||
storageService.getValue = jest.fn().mockReturnValue(roles)
|
||||
const featuresService = createService()
|
||||
featuresService.initializeFromDisk()
|
||||
|
||||
featureService.initializeFromDisk()
|
||||
|
||||
const newRoles = [...roles, RoleName.NAMES.ProUser]
|
||||
await featuresService.updateOnlineRolesWithNewValues(newRoles)
|
||||
await featureService.updateOnlineRolesWithNewValues(newRoles)
|
||||
expect(storageService.setValue).toHaveBeenCalledWith(StorageKey.UserRoles, newRoles)
|
||||
})
|
||||
|
||||
@@ -201,134 +260,162 @@ describe('FeaturesService', () => {
|
||||
const newRoles = [RoleName.NAMES.CoreUser]
|
||||
|
||||
storageService.getValue = jest.fn().mockReturnValue(roles)
|
||||
const featuresService = createService()
|
||||
featuresService.initializeFromDisk()
|
||||
await featuresService.updateOnlineRolesWithNewValues(newRoles)
|
||||
|
||||
featureService.initializeFromDisk()
|
||||
await featureService.updateOnlineRolesWithNewValues(newRoles)
|
||||
|
||||
expect(storageService.setValue).toHaveBeenCalledWith(StorageKey.UserRoles, newRoles)
|
||||
})
|
||||
|
||||
it('role-based feature status', async () => {
|
||||
const featuresService = createService()
|
||||
|
||||
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)
|
||||
|
||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(FeatureStatus.Entitled)
|
||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.SuperEditor)).toBe(FeatureStatus.Entitled)
|
||||
expect(
|
||||
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 () => {
|
||||
const featuresService = createService()
|
||||
|
||||
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
|
||||
|
||||
await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser])
|
||||
await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser])
|
||||
subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(false)
|
||||
|
||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(FeatureStatus.NoUserSubscription)
|
||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.PlusEditor)).toBe(FeatureStatus.NoUserSubscription)
|
||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.SheetsEditor)).toBe(FeatureStatus.NoUserSubscription)
|
||||
expect(
|
||||
featureService.getFeatureStatus(
|
||||
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 () => {
|
||||
const featuresService = createService()
|
||||
|
||||
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 () => {
|
||||
const featuresService = createService()
|
||||
|
||||
itemManager.getDisplayableComponents = jest
|
||||
.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(featuresService.getFeatureStatus('third-party-editor' as FeatureIdentifier)).toBe(
|
||||
expect(featureService.getFeatureStatus(Uuid.create('00000000-0000-0000-0000-000000000001').getValue())).toBe(
|
||||
FeatureStatus.Entitled,
|
||||
)
|
||||
expect(featureService.getFeatureStatus(Uuid.create('00000000-0000-0000-0000-000000000002').getValue())).toBe(
|
||||
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,
|
||||
)
|
||||
})
|
||||
|
||||
it('feature status should be not entitled if no account or offline repo', async () => {
|
||||
const featuresService = createService()
|
||||
|
||||
await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser])
|
||||
await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser])
|
||||
|
||||
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(false)
|
||||
|
||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(FeatureStatus.NoUserSubscription)
|
||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.TokenVaultEditor)).toBe(
|
||||
FeatureStatus.NoUserSubscription,
|
||||
)
|
||||
expect(
|
||||
featureService.getFeatureStatus(
|
||||
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 () => {
|
||||
const featuresService = createService()
|
||||
featureService.hasFirstPartyOfflineSubscription = jest.fn().mockReturnValue(true)
|
||||
featureService.setOfflineRoles([RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
|
||||
|
||||
featuresService.hasFirstPartyOfflineSubscription = jest.fn().mockReturnValue(true)
|
||||
featuresService.setOfflineRoles([RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
|
||||
|
||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(FeatureStatus.Entitled)
|
||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.TokenVaultEditor)).toBe(FeatureStatus.Entitled)
|
||||
expect(
|
||||
featureService.getFeatureStatus(
|
||||
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.MidnightTheme).getValue(),
|
||||
),
|
||||
).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 () => {
|
||||
const featuresService = createService()
|
||||
|
||||
subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(false)
|
||||
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
|
||||
|
||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.DeprecatedFileSafe as FeatureIdentifier)).toBe(
|
||||
FeatureStatus.NoUserSubscription,
|
||||
)
|
||||
expect(
|
||||
featureService.getFeatureStatus(
|
||||
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.DeprecatedFileSafe).getValue(),
|
||||
),
|
||||
).toBe(FeatureStatus.NoUserSubscription)
|
||||
})
|
||||
|
||||
it('feature status for deprecated feature with subscription', async () => {
|
||||
const featuresService = createService()
|
||||
|
||||
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(
|
||||
FeatureStatus.Entitled,
|
||||
)
|
||||
expect(
|
||||
featureService.getFeatureStatus(
|
||||
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.DeprecatedFileSafe).getValue(),
|
||||
),
|
||||
).toBe(FeatureStatus.Entitled)
|
||||
})
|
||||
|
||||
it('has paid subscription', async () => {
|
||||
const featuresService = createService()
|
||||
|
||||
await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser])
|
||||
await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser])
|
||||
|
||||
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 () => {
|
||||
const featuresService = createService()
|
||||
await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser])
|
||||
|
||||
await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser])
|
||||
|
||||
featuresService.hasOfflineRepo = jest.fn().mockReturnValue(true)
|
||||
featureService.hasOfflineRepo = jest.fn().mockReturnValue(true)
|
||||
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(false)
|
||||
|
||||
expect(featuresService.hasPaidAnyPartyOnlineOrOfflineSubscription()).toEqual(true)
|
||||
expect(featureService.hasPaidAnyPartyOnlineOrOfflineSubscription()).toEqual(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -343,8 +430,7 @@ describe('FeaturesService', () => {
|
||||
},
|
||||
} as never)
|
||||
|
||||
const featuresService = createService()
|
||||
await featuresService.migrateFeatureRepoToUserSetting([extensionRepoItem])
|
||||
await featureService.migrateFeatureRepoToUserSetting([extensionRepoItem])
|
||||
expect(settingsService.updateSetting).toHaveBeenCalledWith(
|
||||
SettingName.create(SettingName.NAMES.ExtensionKey).getValue(),
|
||||
extensionKey,
|
||||
@@ -369,8 +455,7 @@ describe('FeaturesService', () => {
|
||||
const installUrl = 'http://example.com'
|
||||
crypto.base64Decode = jest.fn().mockReturnValue(installUrl)
|
||||
|
||||
const featuresService = createService()
|
||||
const result = await featuresService.downloadRemoteThirdPartyFeature(installUrl)
|
||||
const result = await featureService.downloadRemoteThirdPartyFeature(installUrl)
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
@@ -389,17 +474,14 @@ describe('FeaturesService', () => {
|
||||
const installUrl = 'http://example.com'
|
||||
crypto.base64Decode = jest.fn().mockReturnValue(installUrl)
|
||||
|
||||
const featuresService = createService()
|
||||
const result = await featuresService.downloadRemoteThirdPartyFeature(installUrl)
|
||||
const result = await featureService.downloadRemoteThirdPartyFeature(installUrl)
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('sortRolesByHierarchy', () => {
|
||||
it('should sort given roles according to role hierarchy', () => {
|
||||
const featuresService = createService()
|
||||
|
||||
const sortedRoles = featuresService.rolesBySorting([
|
||||
const sortedRoles = featureService.rolesBySorting([
|
||||
RoleName.NAMES.ProUser,
|
||||
RoleName.NAMES.CoreUser,
|
||||
RoleName.NAMES.PlusUser,
|
||||
@@ -411,50 +493,42 @@ describe('FeaturesService', () => {
|
||||
|
||||
describe('hasMinimumRole', () => {
|
||||
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 = featuresService.hasMinimumRole(RoleName.NAMES.PlusUser)
|
||||
const hasPlusUserRole = featureService.hasMinimumRole(RoleName.NAMES.PlusUser)
|
||||
|
||||
expect(hasPlusUserRole).toBe(false)
|
||||
})
|
||||
|
||||
it('should be false if plus user checks for pro role', async () => {
|
||||
const featuresService = createService()
|
||||
|
||||
sessionManager.isSignedIntoFirstPartyServer = 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)
|
||||
})
|
||||
|
||||
it('should be true if pro user checks for core user', async () => {
|
||||
const featuresService = createService()
|
||||
|
||||
sessionManager.isSignedIntoFirstPartyServer = 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)
|
||||
})
|
||||
|
||||
it('should be true if pro user checks for pro user', async () => {
|
||||
const featuresService = createService()
|
||||
|
||||
sessionManager.isSignedIntoFirstPartyServer = 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)
|
||||
})
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { MigrateFeatureRepoToUserSettingUseCase } from './UseCase/MigrateFeatureRepoToUserSetting'
|
||||
import { arraysEqual, removeFromArray, lastElement } from '@standardnotes/utils'
|
||||
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 { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
import { WebSocketsService } from '../Api/WebsocketsService'
|
||||
import { WebSocketsServiceEvent } from '../Api/WebSocketsServiceEvent'
|
||||
import { TRUSTED_CUSTOM_EXTENSIONS_HOSTS, TRUSTED_FEATURE_HOSTS } from '@Lib/Hosts'
|
||||
import { UserRolesChangedEvent } from '@standardnotes/domain-events'
|
||||
import { ExperimentalFeatures, FindNativeFeature, FeatureIdentifier } from '@standardnotes/features'
|
||||
import { ExperimentalFeatures, FindNativeFeature, NativeFeatureIdentifier } from '@standardnotes/features'
|
||||
import {
|
||||
SNFeatureRepo,
|
||||
FeatureRepoContent,
|
||||
@@ -64,7 +64,7 @@ export class FeaturesService
|
||||
{
|
||||
private onlineRoles: string[] = []
|
||||
private offlineRoles: string[] = []
|
||||
private enabledExperimentalFeatures: FeatureIdentifier[] = []
|
||||
private enabledExperimentalFeatures: string[] = []
|
||||
|
||||
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.offlineRoles = this.storage.getValue<string[]>(StorageKey.OfflineUserRoles, undefined, [])
|
||||
|
||||
this.enabledExperimentalFeatures = this.storage.getValue(StorageKey.ExperimentalFeatures, undefined, [])
|
||||
}
|
||||
|
||||
async handleEvent(event: InternalEventInterface): Promise<void> {
|
||||
if (event.type === ApiServiceEvent.MetaReceived) {
|
||||
if (!this.sync) {
|
||||
this.log('Handling events interrupted. Sync service is not yet initialized.', event)
|
||||
return
|
||||
switch (event.type) {
|
||||
case ApiServiceEvent.MetaReceived: {
|
||||
if (!this.sync) {
|
||||
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
|
||||
void this.updateOnlineRolesWithNewValues(userRoles.map((role) => role.name))
|
||||
}
|
||||
|
||||
if (event.type === ApplicationEvent.ApplicationStageChanged) {
|
||||
const stage = (event.payload as ApplicationStageChangedEventPayload).stage
|
||||
if (stage === ApplicationStage.FullSyncCompleted_13) {
|
||||
if (!this.hasFirstPartyOnlineSubscription()) {
|
||||
const offlineRepo = this.getOfflineRepo()
|
||||
|
||||
if (offlineRepo) {
|
||||
void this.downloadOfflineRoles(offlineRepo)
|
||||
case ApplicationEvent.ApplicationStageChanged: {
|
||||
const stage = (event.payload as ApplicationStageChangedEventPayload).stage
|
||||
switch (stage) {
|
||||
case ApplicationStage.StorageDecrypted_09: {
|
||||
this.initializeFromDisk()
|
||||
break
|
||||
}
|
||||
case ApplicationStage.FullSyncCompleted_13: {
|
||||
if (!this.hasFirstPartyOnlineSubscription()) {
|
||||
const offlineRepo = this.getOfflineRepo()
|
||||
if (offlineRepo) {
|
||||
void this.downloadOfflineRoles(offlineRepo)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enableExperimentalFeature(identifier: FeatureIdentifier): void {
|
||||
public enableExperimentalFeature(identifier: string): void {
|
||||
this.enabledExperimentalFeatures.push(identifier)
|
||||
|
||||
void this.storage.setValue(StorageKey.ExperimentalFeatures, this.enabledExperimentalFeatures)
|
||||
@@ -177,7 +184,7 @@ export class FeaturesService
|
||||
void this.notifyEvent(FeaturesEvent.FeaturesAvailabilityChanged)
|
||||
}
|
||||
|
||||
public disableExperimentalFeature(identifier: FeatureIdentifier): void {
|
||||
public disableExperimentalFeature(identifier: string): void {
|
||||
removeFromArray(this.enabledExperimentalFeatures, identifier)
|
||||
|
||||
void this.storage.setValue(StorageKey.ExperimentalFeatures, this.enabledExperimentalFeatures)
|
||||
@@ -195,7 +202,7 @@ export class FeaturesService
|
||||
void this.notifyEvent(FeaturesEvent.FeaturesAvailabilityChanged)
|
||||
}
|
||||
|
||||
public toggleExperimentalFeature(identifier: FeatureIdentifier): void {
|
||||
public toggleExperimentalFeature(identifier: string): void {
|
||||
if (this.isExperimentalFeatureEnabled(identifier)) {
|
||||
this.disableExperimentalFeature(identifier)
|
||||
} else {
|
||||
@@ -203,19 +210,19 @@ export class FeaturesService
|
||||
}
|
||||
}
|
||||
|
||||
public getExperimentalFeatures(): FeatureIdentifier[] {
|
||||
public getExperimentalFeatures(): string[] {
|
||||
return ExperimentalFeatures
|
||||
}
|
||||
|
||||
public isExperimentalFeature(featureId: FeatureIdentifier): boolean {
|
||||
public isExperimentalFeature(featureId: string): boolean {
|
||||
return this.getExperimentalFeatures().includes(featureId)
|
||||
}
|
||||
|
||||
public getEnabledExperimentalFeatures(): FeatureIdentifier[] {
|
||||
public getEnabledExperimentalFeatures(): string[] {
|
||||
return this.enabledExperimentalFeatures
|
||||
}
|
||||
|
||||
public isExperimentalFeatureEnabled(featureId: FeatureIdentifier): boolean {
|
||||
public isExperimentalFeatureEnabled(featureId: string): boolean {
|
||||
return this.enabledExperimentalFeatures.includes(featureId)
|
||||
}
|
||||
|
||||
@@ -302,10 +309,10 @@ export class FeaturesService
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -364,12 +371,13 @@ export class FeaturesService
|
||||
}
|
||||
|
||||
public isThirdPartyFeature(identifier: string): boolean {
|
||||
const isNativeFeature = !!FindNativeFeature(identifier as FeatureIdentifier)
|
||||
const isNativeFeature = !!FindNativeFeature(identifier)
|
||||
return !isNativeFeature
|
||||
}
|
||||
|
||||
onlineRolesIncludePaidSubscription(): boolean {
|
||||
const unpaidRoles = [RoleName.NAMES.CoreUser]
|
||||
|
||||
return this.onlineRoles.some((role) => !unpaidRoles.includes(role))
|
||||
}
|
||||
|
||||
@@ -392,7 +400,7 @@ export class FeaturesService
|
||||
}
|
||||
|
||||
public getFeatureStatus(
|
||||
featureId: FeatureIdentifier,
|
||||
featureId: NativeFeatureIdentifier | Uuid,
|
||||
options: { inContextOfItem?: DecryptedItemInterface } = {},
|
||||
): FeatureStatus {
|
||||
return this.getFeatureStatusUseCase.execute({
|
||||
|
||||
@@ -1,28 +1,23 @@
|
||||
import { FeatureIdentifier } from '@standardnotes/features'
|
||||
import { NativeFeatureIdentifier } from '@standardnotes/features'
|
||||
import { FeatureStatus, ItemManagerInterface } from '@standardnotes/services'
|
||||
import { GetFeatureStatusUseCase } from './GetFeatureStatus'
|
||||
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 { Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
describe('GetFeatureStatusUseCase', () => {
|
||||
let items: jest.Mocked<ItemManagerInterface>
|
||||
let usecase: GetFeatureStatusUseCase
|
||||
let findNativeFeature: jest.Mock<any, any>
|
||||
|
||||
beforeEach(() => {
|
||||
items = {
|
||||
getDisplayableComponents: jest.fn(),
|
||||
} as unknown as jest.Mocked<ItemManagerInterface>
|
||||
usecase = new GetFeatureStatusUseCase(items)
|
||||
;(FindNativeFeature as jest.Mock).mockReturnValue(undefined)
|
||||
findNativeFeature = jest.fn()
|
||||
usecase.findNativeFeature = findNativeFeature
|
||||
findNativeFeature.mockReturnValue(undefined)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
@@ -33,7 +28,7 @@ describe('GetFeatureStatusUseCase', () => {
|
||||
it('should return entitled for free features', () => {
|
||||
expect(
|
||||
usecase.execute({
|
||||
featureId: FeatureIdentifier.DarkTheme,
|
||||
featureId: NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.DarkTheme).getValue(),
|
||||
hasPaidAnyPartyOnlineOrOfflineSubscription: false,
|
||||
firstPartyOnlineSubscription: undefined,
|
||||
firstPartyRoles: undefined,
|
||||
@@ -44,11 +39,11 @@ describe('GetFeatureStatusUseCase', () => {
|
||||
|
||||
describe('deprecated features', () => {
|
||||
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(
|
||||
usecase.execute({
|
||||
featureId: 'deprecatedFeature',
|
||||
featureId: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
hasPaidAnyPartyOnlineOrOfflineSubscription: true,
|
||||
firstPartyOnlineSubscription: undefined,
|
||||
firstPartyRoles: undefined,
|
||||
@@ -57,11 +52,11 @@ describe('GetFeatureStatusUseCase', () => {
|
||||
})
|
||||
|
||||
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(
|
||||
usecase.execute({
|
||||
featureId: 'deprecatedFeature',
|
||||
featureId: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
hasPaidAnyPartyOnlineOrOfflineSubscription: false,
|
||||
firstPartyOnlineSubscription: undefined,
|
||||
firstPartyRoles: undefined,
|
||||
@@ -72,11 +67,11 @@ describe('GetFeatureStatusUseCase', () => {
|
||||
|
||||
describe('native features', () => {
|
||||
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(
|
||||
usecase.execute({
|
||||
featureId: 'nativeFeature',
|
||||
featureId: NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.AutobiographyTheme).getValue(),
|
||||
firstPartyOnlineSubscription: undefined,
|
||||
firstPartyRoles: undefined,
|
||||
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', () => {
|
||||
;(FindNativeFeature as jest.Mock).mockReturnValue({ deprecated: false })
|
||||
findNativeFeature.mockReturnValue({ deprecated: false })
|
||||
|
||||
expect(
|
||||
usecase.execute({
|
||||
featureId: 'nativeFeature',
|
||||
featureId: NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.AutobiographyTheme).getValue(),
|
||||
firstPartyOnlineSubscription: undefined,
|
||||
firstPartyRoles: undefined,
|
||||
hasPaidAnyPartyOnlineOrOfflineSubscription: false,
|
||||
@@ -100,11 +95,11 @@ describe('GetFeatureStatusUseCase', () => {
|
||||
})
|
||||
|
||||
it('should return NoUserSubscription for native features without subscription and roles', () => {
|
||||
;(FindNativeFeature as jest.Mock).mockReturnValue({ deprecated: false })
|
||||
findNativeFeature.mockReturnValue({ deprecated: false })
|
||||
|
||||
expect(
|
||||
usecase.execute({
|
||||
featureId: 'nativeFeature',
|
||||
featureId: NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.AutobiographyTheme).getValue(),
|
||||
firstPartyOnlineSubscription: undefined,
|
||||
firstPartyRoles: undefined,
|
||||
hasPaidAnyPartyOnlineOrOfflineSubscription: false,
|
||||
@@ -113,14 +108,14 @@ describe('GetFeatureStatusUseCase', () => {
|
||||
})
|
||||
|
||||
it('should return NotInCurrentPlan for native features with roles not in available roles', () => {
|
||||
;(FindNativeFeature as jest.Mock).mockReturnValue({
|
||||
findNativeFeature.mockReturnValue({
|
||||
deprecated: false,
|
||||
availableInRoles: ['notInRole'],
|
||||
})
|
||||
|
||||
expect(
|
||||
usecase.execute({
|
||||
featureId: 'nativeFeature',
|
||||
featureId: NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.AutobiographyTheme).getValue(),
|
||||
firstPartyOnlineSubscription: undefined,
|
||||
firstPartyRoles: { online: ['inRole'] },
|
||||
hasPaidAnyPartyOnlineOrOfflineSubscription: false,
|
||||
@@ -129,14 +124,14 @@ describe('GetFeatureStatusUseCase', () => {
|
||||
})
|
||||
|
||||
it('should return Entitled for native features with roles in available roles and active subscription', () => {
|
||||
;(FindNativeFeature as jest.Mock).mockReturnValue({
|
||||
findNativeFeature.mockReturnValue({
|
||||
deprecated: false,
|
||||
availableInRoles: ['inRole'],
|
||||
})
|
||||
|
||||
expect(
|
||||
usecase.execute({
|
||||
featureId: 'nativeFeature',
|
||||
featureId: NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.AutobiographyTheme).getValue(),
|
||||
firstPartyOnlineSubscription: {
|
||||
endsAt: new Date(Date.now() + 10000).getTime(),
|
||||
} 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', () => {
|
||||
;(FindNativeFeature as jest.Mock).mockReturnValue({
|
||||
findNativeFeature.mockReturnValue({
|
||||
deprecated: false,
|
||||
availableInRoles: ['inRole'],
|
||||
})
|
||||
|
||||
expect(
|
||||
usecase.execute({
|
||||
featureId: 'nativeFeature',
|
||||
featureId: NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.AutobiographyTheme).getValue(),
|
||||
firstPartyOnlineSubscription: {
|
||||
endsAt: new Date(Date.now() - 10000).getTime(),
|
||||
} as jest.Mocked<Subscription>,
|
||||
@@ -168,7 +163,7 @@ describe('GetFeatureStatusUseCase', () => {
|
||||
describe('third party features', () => {
|
||||
it('should return Entitled for third-party features', () => {
|
||||
const mockComponent = {
|
||||
identifier: 'thirdPartyFeature',
|
||||
uuid: '00000000-0000-0000-0000-000000000000',
|
||||
isExpired: false,
|
||||
} as unknown as jest.Mocked<ComponentInterface>
|
||||
|
||||
@@ -176,7 +171,7 @@ describe('GetFeatureStatusUseCase', () => {
|
||||
|
||||
expect(
|
||||
usecase.execute({
|
||||
featureId: 'thirdPartyFeature',
|
||||
featureId: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
hasPaidAnyPartyOnlineOrOfflineSubscription: false,
|
||||
firstPartyOnlineSubscription: undefined,
|
||||
firstPartyRoles: undefined,
|
||||
@@ -189,7 +184,7 @@ describe('GetFeatureStatusUseCase', () => {
|
||||
|
||||
expect(
|
||||
usecase.execute({
|
||||
featureId: 'nonExistingThirdPartyFeature',
|
||||
featureId: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
hasPaidAnyPartyOnlineOrOfflineSubscription: false,
|
||||
firstPartyOnlineSubscription: undefined,
|
||||
firstPartyRoles: undefined,
|
||||
@@ -199,7 +194,7 @@ describe('GetFeatureStatusUseCase', () => {
|
||||
|
||||
it('should return InCurrentPlanButExpired for expired third-party features', () => {
|
||||
const mockComponent = {
|
||||
identifier: 'thirdPartyFeature',
|
||||
uuid: '00000000-0000-0000-0000-000000000000',
|
||||
isExpired: true,
|
||||
} as unknown as jest.Mocked<ComponentInterface>
|
||||
|
||||
@@ -207,7 +202,7 @@ describe('GetFeatureStatusUseCase', () => {
|
||||
|
||||
expect(
|
||||
usecase.execute({
|
||||
featureId: 'thirdPartyFeature',
|
||||
featureId: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
hasPaidAnyPartyOnlineOrOfflineSubscription: false,
|
||||
firstPartyOnlineSubscription: undefined,
|
||||
firstPartyRoles: undefined,
|
||||
|
||||
@@ -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 { Subscription } from '@standardnotes/responses'
|
||||
import { FeatureStatus, ItemManagerInterface } from '@standardnotes/services'
|
||||
@@ -8,20 +9,19 @@ export class GetFeatureStatusUseCase {
|
||||
constructor(private items: ItemManagerInterface) {}
|
||||
|
||||
execute(dto: {
|
||||
featureId: FeatureIdentifier | string
|
||||
featureId: NativeFeatureIdentifier | Uuid
|
||||
firstPartyOnlineSubscription: Subscription | undefined
|
||||
firstPartyRoles: { online: string[] } | { offline: string[] } | undefined
|
||||
hasPaidAnyPartyOnlineOrOfflineSubscription: boolean
|
||||
inContextOfItem?: DecryptedItemInterface
|
||||
}): FeatureStatus {
|
||||
if (this.isFreeFeature(dto.featureId as FeatureIdentifier)) {
|
||||
if (this.isFreeFeature(dto.featureId)) {
|
||||
return FeatureStatus.Entitled
|
||||
}
|
||||
|
||||
const nativeFeature = FindNativeFeature(dto.featureId as FeatureIdentifier)
|
||||
|
||||
const nativeFeature = this.findNativeFeature(dto.featureId)
|
||||
if (!nativeFeature) {
|
||||
return this.getThirdPartyFeatureStatus(dto.featureId as string)
|
||||
return this.getThirdPartyFeatureStatus(dto.featureId)
|
||||
}
|
||||
|
||||
if (nativeFeature.deprecated) {
|
||||
@@ -39,6 +39,10 @@ export class GetFeatureStatusUseCase {
|
||||
})
|
||||
}
|
||||
|
||||
findNativeFeature(featureId: NativeFeatureIdentifier | Uuid): AnyFeatureDescription | undefined {
|
||||
return FindNativeFeature(featureId.value)
|
||||
}
|
||||
|
||||
private getDeprecatedNativeFeatureStatus(dto: {
|
||||
hasPaidAnyPartyOnlineOrOfflineSubscription: boolean
|
||||
nativeFeature: AnyFeatureDescription
|
||||
@@ -95,8 +99,8 @@ export class GetFeatureStatusUseCase {
|
||||
return FeatureStatus.Entitled
|
||||
}
|
||||
|
||||
private getThirdPartyFeatureStatus(featureId: string): FeatureStatus {
|
||||
const component = this.items.getDisplayableComponents().find((candidate) => candidate.identifier === featureId)
|
||||
private getThirdPartyFeatureStatus(uuid: Uuid): FeatureStatus {
|
||||
const component = this.items.getDisplayableComponents().find((candidate) => candidate.uuid === uuid.value)
|
||||
|
||||
if (!component) {
|
||||
return FeatureStatus.NoUserSubscription
|
||||
@@ -109,7 +113,9 @@ export class GetFeatureStatusUseCase {
|
||||
return FeatureStatus.Entitled
|
||||
}
|
||||
|
||||
private isFreeFeature(featureId: FeatureIdentifier) {
|
||||
return [FeatureIdentifier.DarkTheme, FeatureIdentifier.PlainEditor].includes(featureId)
|
||||
private isFreeFeature(featureId: NativeFeatureIdentifier) {
|
||||
return [NativeFeatureIdentifier.TYPES.DarkTheme, NativeFeatureIdentifier.TYPES.PlainEditor].includes(
|
||||
featureId.value,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,9 @@ import {
|
||||
UserKeyPairChangedEventData,
|
||||
InternalFeatureService,
|
||||
InternalFeature,
|
||||
ApplicationEvent,
|
||||
ApplicationStageChangedEventPayload,
|
||||
ApplicationStage,
|
||||
} from '@standardnotes/services'
|
||||
import { Base64String, PkcKeyPair, PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
import {
|
||||
@@ -112,8 +115,17 @@ export class SessionManager
|
||||
}
|
||||
|
||||
async handleEvent(event: InternalEventInterface): Promise<void> {
|
||||
if (event.type === ApiServiceEvent.SessionRefreshed) {
|
||||
this.httpService.setSession((event.payload as SessionRefreshedData).session)
|
||||
switch (event.type) {
|
||||
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)
|
||||
}
|
||||
|
||||
async initializeFromDisk(): Promise<void> {
|
||||
private async initializeFromDisk(): Promise<void> {
|
||||
this.memoizeUser(this.storage.getValue(StorageKey.User))
|
||||
|
||||
if (!this.user) {
|
||||
|
||||
@@ -145,7 +145,7 @@ describe('migrations', () => {
|
||||
content_type: ContentType.TYPES.Component,
|
||||
content: FillItemContent({
|
||||
package_info: {
|
||||
identifier: FeatureIdentifier.MarkdownProEditor,
|
||||
identifier: NativeFeatureIdentifier.TYPES.MarkdownProEditor,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
@@ -170,7 +170,7 @@ describe('migrations', () => {
|
||||
content_type: ContentType.TYPES.Component,
|
||||
content: FillItemContent({
|
||||
package_info: {
|
||||
identifier: FeatureIdentifier.DeprecatedBoldEditor,
|
||||
identifier: NativeFeatureIdentifier.TYPES.DeprecatedBoldEditor,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
|
||||
@@ -369,9 +369,9 @@ describe('app models', () => {
|
||||
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)
|
||||
expect(this.application.componentManager.editorForNote(duplicate).uniqueIdentifier).to.equal(component.uuid)
|
||||
expect(this.application.componentManager.editorForNote(duplicate).uniqueIdentifier.value).to.equal(component.uuid)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"scripts": {
|
||||
"tsc": "tsc --project tsconfig.json",
|
||||
"lint": "eslint src --ext .ts",
|
||||
"test": "jest spec"
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@standardnotes/common": "^1.50.0",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { WebApplicationInterface } from './../../WebApplication/WebApplicationInterface'
|
||||
import { FeatureIdentifier, NoteType } from '@standardnotes/features'
|
||||
import { NativeFeatureIdentifier, NoteType } from '@standardnotes/features'
|
||||
import { AegisToAuthenticatorConverter } from './AegisToAuthenticatorConverter'
|
||||
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"}]',
|
||||
)
|
||||
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', () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DecryptedTransferPayload, NoteContent } from '@standardnotes/models'
|
||||
import { readFileAsText } from '../Utils'
|
||||
import { FeatureIdentifier, NoteType } from '@standardnotes/features'
|
||||
import { NativeFeatureIdentifier, NoteType } from '@standardnotes/features'
|
||||
import { WebApplicationInterface } from '../../WebApplication/WebApplicationInterface'
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
|
||||
@@ -69,7 +69,7 @@ export class AegisToAuthenticatorConverter {
|
||||
references: [],
|
||||
...(addEditorInfo && {
|
||||
noteType: NoteType.Authentication,
|
||||
editorIdentifier: FeatureIdentifier.TokenVaultEditor,
|
||||
editorIdentifier: NativeFeatureIdentifier.TYPES.TokenVaultEditor,
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { parseFileName } from '@standardnotes/filepicker'
|
||||
import { FeatureStatus } from '@standardnotes/services'
|
||||
import { FeatureIdentifier } from '@standardnotes/features'
|
||||
import { NativeFeatureIdentifier } from '@standardnotes/features'
|
||||
import { AegisToAuthenticatorConverter } from './AegisConverter/AegisToAuthenticatorConverter'
|
||||
import { EvernoteConverter } from './EvernoteConverter/EvernoteConverter'
|
||||
import { GoogleKeepConverter } from './GoogleKeepConverter/GoogleKeepConverter'
|
||||
@@ -64,7 +64,9 @@ export class Importer {
|
||||
async getPayloadsFromFile(file: File, type: NoteImportType): Promise<DecryptedTransferPayload[]> {
|
||||
if (type === 'aegis') {
|
||||
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)]
|
||||
} else if (type === 'google-keep') {
|
||||
return [await this.googleKeepConverter.convertGoogleKeepBackupFileToNote(file, true)]
|
||||
|
||||
58
packages/ui-services/src/Theme/ActiveThemeList.spec.ts
Normal file
58
packages/ui-services/src/Theme/ActiveThemeList.spec.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
70
packages/ui-services/src/Theme/ActiveThemeList.ts
Normal file
70
packages/ui-services/src/Theme/ActiveThemeList.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
PrefKey,
|
||||
ThemeInterface,
|
||||
} from '@standardnotes/models'
|
||||
import { removeFromArray } from '@standardnotes/utils'
|
||||
import {
|
||||
InternalEventBusInterface,
|
||||
ApplicationEvent,
|
||||
@@ -16,17 +15,19 @@ import {
|
||||
PreferencesServiceEvent,
|
||||
ComponentManagerInterface,
|
||||
} from '@standardnotes/services'
|
||||
import { FeatureIdentifier, FindNativeTheme, ThemeFeatureDescription } from '@standardnotes/features'
|
||||
import { NativeFeatureIdentifier, FindNativeTheme, ThemeFeatureDescription } from '@standardnotes/features'
|
||||
import { WebApplicationInterface } from '../WebApplication/WebApplicationInterface'
|
||||
import { AbstractUIServicee } from '../Abstract/AbstractUIService'
|
||||
import { GetAllThemesUseCase } from './GetAllThemesUseCase'
|
||||
import { Uuid } from '@standardnotes/domain-core'
|
||||
import { ActiveThemeList } from './ActiveThemeList'
|
||||
|
||||
const CachedThemesKey = 'cachedThemes'
|
||||
const TimeBeforeApplyingColorScheme = 5
|
||||
const DefaultThemeIdentifier = 'Default'
|
||||
|
||||
export class ThemeManager extends AbstractUIServicee {
|
||||
private themesActiveInTheUI: string[] = []
|
||||
private themesActiveInTheUI: ActiveThemeList
|
||||
private lastUseDeviceThemeSettings = false
|
||||
|
||||
constructor(
|
||||
@@ -37,6 +38,23 @@ export class ThemeManager extends AbstractUIServicee {
|
||||
) {
|
||||
super(application, internalEventBus)
|
||||
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() {
|
||||
@@ -66,20 +84,32 @@ export class ThemeManager extends AbstractUIServicee {
|
||||
|
||||
let hasChange = false
|
||||
|
||||
const activeThemes = this.components.getActiveThemesIdentifiers()
|
||||
for (const uiActiveTheme of this.themesActiveInTheUI) {
|
||||
if (!activeThemes.includes(uiActiveTheme)) {
|
||||
this.deactivateThemeInTheUI(uiActiveTheme)
|
||||
const { features, uuids } = this.components.getActiveThemesIdentifiers()
|
||||
|
||||
const featuresList = new ActiveThemeList(this.application.items, features)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
for (const activeTheme of activeThemes) {
|
||||
if (!this.themesActiveInTheUI.includes(activeTheme)) {
|
||||
const theme =
|
||||
FindNativeTheme(activeTheme as FeatureIdentifier) ??
|
||||
this.application.items.findItem<ThemeInterface>(activeTheme)
|
||||
for (const feature of features) {
|
||||
if (!this.themesActiveInTheUI.has(feature)) {
|
||||
const theme = FindNativeTheme(feature.value)
|
||||
if (theme) {
|
||||
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) {
|
||||
const uiFeature = new UIFeature<ThemeFeatureDescription>(theme)
|
||||
this.activateTheme(uiFeature)
|
||||
@@ -99,7 +129,7 @@ export class ThemeManager extends AbstractUIServicee {
|
||||
switch (event) {
|
||||
case ApplicationEvent.SignedOut: {
|
||||
this.deactivateAllThemes()
|
||||
this.themesActiveInTheUI = []
|
||||
this.themesActiveInTheUI.clear()
|
||||
this.application?.removeValue(CachedThemesKey, StorageValueModes.Nonwrapped).catch(console.error)
|
||||
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 {
|
||||
let hasChange = false
|
||||
|
||||
for (const themeUuid of this.themesActiveInTheUI) {
|
||||
const theme = this.application.items.findItem<ThemeInterface>(themeUuid)
|
||||
|
||||
if (!theme) {
|
||||
this.deactivateThemeInTheUI(themeUuid)
|
||||
hasChange = true
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
const status = this.application.features.getFeatureStatus(theme.identifier)
|
||||
for (const theme of this.themesActiveInTheUI.asThemes()) {
|
||||
const status = this.application.features.getFeatureStatus(theme.uniqueIdentifier)
|
||||
if (status !== FeatureStatus.Entitled) {
|
||||
this.deactivateThemeInTheUI(theme.uuid)
|
||||
this.deactivateThemeInTheUI(theme.uniqueIdentifier)
|
||||
hasChange = true
|
||||
}
|
||||
}
|
||||
@@ -194,7 +202,7 @@ export class ThemeManager extends AbstractUIServicee {
|
||||
const activeThemes = this.components.getActiveThemes()
|
||||
|
||||
for (const theme of activeThemes) {
|
||||
if (!this.themesActiveInTheUI.includes(theme.uniqueIdentifier)) {
|
||||
if (!this.themesActiveInTheUI.has(theme.uniqueIdentifier)) {
|
||||
this.activateTheme(theme)
|
||||
hasChange = true
|
||||
}
|
||||
@@ -245,7 +253,7 @@ export class ThemeManager extends AbstractUIServicee {
|
||||
const preference = prefersDarkColorScheme ? PrefKey.AutoDarkThemeIdentifier : PrefKey.AutoLightThemeIdentifier
|
||||
|
||||
const preferenceDefault =
|
||||
preference === PrefKey.AutoDarkThemeIdentifier ? FeatureIdentifier.DarkTheme : DefaultThemeIdentifier
|
||||
preference === PrefKey.AutoDarkThemeIdentifier ? NativeFeatureIdentifier.TYPES.DarkTheme : DefaultThemeIdentifier
|
||||
|
||||
const usecase = new GetAllThemesUseCase(this.application.items)
|
||||
const { thirdParty, native } = usecase.execute({ excludeLayerable: false })
|
||||
@@ -289,21 +297,20 @@ export class ThemeManager extends AbstractUIServicee {
|
||||
}
|
||||
|
||||
private deactivateAllThemes() {
|
||||
const activeThemes = this.themesActiveInTheUI.slice()
|
||||
|
||||
const activeThemes = this.themesActiveInTheUI.getList()
|
||||
for (const uuid of activeThemes) {
|
||||
this.deactivateThemeInTheUI(uuid)
|
||||
}
|
||||
}
|
||||
|
||||
private activateTheme(theme: UIFeature<ThemeFeatureDescription>, skipEntitlementCheck = false) {
|
||||
if (this.themesActiveInTheUI.find((uuid) => uuid === theme.uniqueIdentifier)) {
|
||||
if (this.themesActiveInTheUI.has(theme.uniqueIdentifier)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
!skipEntitlementCheck &&
|
||||
this.application.features.getFeatureStatus(theme.featureIdentifier) !== FeatureStatus.Entitled
|
||||
this.application.features.getFeatureStatus(theme.uniqueIdentifier) !== FeatureStatus.Entitled
|
||||
) {
|
||||
return
|
||||
}
|
||||
@@ -313,14 +320,14 @@ export class ThemeManager extends AbstractUIServicee {
|
||||
return
|
||||
}
|
||||
|
||||
this.themesActiveInTheUI.push(theme.uniqueIdentifier)
|
||||
this.themesActiveInTheUI.add(theme.uniqueIdentifier)
|
||||
|
||||
const link = document.createElement('link')
|
||||
link.href = url
|
||||
link.type = 'text/css'
|
||||
link.rel = 'stylesheet'
|
||||
link.media = 'screen,print'
|
||||
link.id = theme.uniqueIdentifier
|
||||
link.id = theme.uniqueIdentifier.value
|
||||
link.onload = () => {
|
||||
this.syncThemeColorMetadata()
|
||||
|
||||
@@ -336,20 +343,20 @@ export class ThemeManager extends AbstractUIServicee {
|
||||
document.getElementsByTagName('head')[0].appendChild(link)
|
||||
}
|
||||
|
||||
private deactivateThemeInTheUI(uuid: string) {
|
||||
if (!this.themesActiveInTheUI.includes(uuid)) {
|
||||
private deactivateThemeInTheUI(id: NativeFeatureIdentifier | Uuid) {
|
||||
if (!this.themesActiveInTheUI.has(id)) {
|
||||
return
|
||||
}
|
||||
|
||||
const element = document.getElementById(uuid) as HTMLLinkElement
|
||||
const element = document.getElementById(id.value) as HTMLLinkElement
|
||||
if (element) {
|
||||
element.disabled = true
|
||||
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')
|
||||
}
|
||||
}
|
||||
@@ -373,11 +380,16 @@ export class ThemeManager extends AbstractUIServicee {
|
||||
}
|
||||
|
||||
private async cacheThemeState() {
|
||||
const themes = this.application.items.findItems<ThemeInterface>(this.themesActiveInTheUI)
|
||||
const themes = this.themesActiveInTheUI.asThemes()
|
||||
|
||||
const mapped = themes.map((theme) => {
|
||||
const payload = theme.payloadRepresentation()
|
||||
return CreateDecryptedLocalStorageContextPayload(payload)
|
||||
if (theme.isComponent) {
|
||||
const payload = theme.asComponent.payloadRepresentation()
|
||||
return CreateDecryptedLocalStorageContextPayload(payload)
|
||||
} else {
|
||||
const payload = theme.asFeatureDescription
|
||||
return payload
|
||||
}
|
||||
})
|
||||
|
||||
return this.application.setValue(CachedThemesKey, mapped, StorageValueModes.Nonwrapped)
|
||||
@@ -389,16 +401,25 @@ export class ThemeManager extends AbstractUIServicee {
|
||||
StorageValueModes.Nonwrapped,
|
||||
)
|
||||
|
||||
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 {
|
||||
if (!cachedThemes) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"prebuild": "yarn clean",
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"lint": "eslint src --ext .ts",
|
||||
"test": "jest spec"
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@standardnotes/common": "^1.50.0",
|
||||
|
||||
@@ -1,11 +1,46 @@
|
||||
import * as DOMPurifyLib from 'dompurify'
|
||||
import { JSDOM } from 'jsdom'
|
||||
import { sortByKey, withoutLastElement } from './Utils'
|
||||
import { sortByKey, withoutLastElement, compareArrayReferences } from './Utils'
|
||||
|
||||
const window = new JSDOM('').window
|
||||
const DOMPurify = DOMPurifyLib(window as never)
|
||||
|
||||
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', () => {
|
||||
const dirty = '<svg><animate onbegin=alert(1) attributeName=x dur=1s>'
|
||||
const cleaned = DOMPurify.sanitize(dirty)
|
||||
|
||||
@@ -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)))
|
||||
}
|
||||
|
||||
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) {
|
||||
if ((left && !right) || (!left && right)) {
|
||||
return false
|
||||
|
||||
@@ -6,7 +6,7 @@ import { WebApplication } from '@/Application/WebApplication'
|
||||
import {
|
||||
UIFeature,
|
||||
EditorFeatureDescription,
|
||||
FeatureIdentifier,
|
||||
NativeFeatureIdentifier,
|
||||
IframeComponentFeatureDescription,
|
||||
NoteMutator,
|
||||
NoteType,
|
||||
@@ -100,7 +100,7 @@ const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
|
||||
|
||||
setCurrentFeature(application.componentManager.editorForNote(note))
|
||||
|
||||
if (uiFeature.featureIdentifier === FeatureIdentifier.PlainEditor) {
|
||||
if (uiFeature.featureIdentifier === NativeFeatureIdentifier.TYPES.PlainEditor) {
|
||||
reloadFont(application.getPreference(PrefKey.EditorMonospaceEnabled))
|
||||
}
|
||||
},
|
||||
@@ -211,7 +211,7 @@ const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
|
||||
|
||||
return (
|
||||
<MenuRadioButtonItem
|
||||
key={menuItem.uiFeature.uniqueIdentifier}
|
||||
key={menuItem.uiFeature.uniqueIdentifier.value}
|
||||
onClick={onClickEditorItem}
|
||||
className={'flex-row-reversed py-2'}
|
||||
checked={isSelected(menuItem)}
|
||||
|
||||
@@ -141,7 +141,7 @@ const ChangeEditorMultipleMenu = ({ application, notes, setDisableClickOutside }
|
||||
}
|
||||
return (
|
||||
<MenuItem
|
||||
key={item.uiFeature.uniqueIdentifier}
|
||||
key={item.uiFeature.uniqueIdentifier.value}
|
||||
onClick={onClickEditorItem}
|
||||
className={'flex-row-reversed py-2'}
|
||||
>
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
ApplicationEvent,
|
||||
ContentType,
|
||||
DecryptedItem,
|
||||
FeatureIdentifier,
|
||||
NativeFeatureIdentifier,
|
||||
FeatureStatus,
|
||||
NoteContent,
|
||||
NoteType,
|
||||
@@ -61,7 +61,10 @@ const ClipperView = ({
|
||||
|
||||
const [user, setUser] = useState(() => application.getUser())
|
||||
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 hasSubscription = application.hasValidFirstPartySubscription()
|
||||
@@ -72,10 +75,18 @@ const ClipperView = ({
|
||||
case ApplicationEvent.SignedOut:
|
||||
case ApplicationEvent.UserRolesChanged:
|
||||
setUser(application.getUser())
|
||||
setIsEntitled(application.features.getFeatureStatus(FeatureIdentifier.Extension) === FeatureStatus.Entitled)
|
||||
setIsEntitled(
|
||||
application.features.getFeatureStatus(
|
||||
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.Clipper).getValue(),
|
||||
) === FeatureStatus.Entitled,
|
||||
)
|
||||
break
|
||||
case ApplicationEvent.FeaturesAvailabilityChanged:
|
||||
setIsEntitled(application.features.getFeatureStatus(FeatureIdentifier.Extension) === FeatureStatus.Entitled)
|
||||
setIsEntitled(
|
||||
application.features.getFeatureStatus(
|
||||
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.Clipper).getValue(),
|
||||
) === FeatureStatus.Entitled,
|
||||
)
|
||||
break
|
||||
}
|
||||
})
|
||||
@@ -212,7 +223,7 @@ const ClipperView = ({
|
||||
const note = application.items.createTemplateItem<NoteContent, SNNote>(ContentType.TYPES.Note, {
|
||||
title: clipPayload.title,
|
||||
text: editorStateJSON,
|
||||
editorIdentifier: FeatureIdentifier.SuperEditor,
|
||||
editorIdentifier: NativeFeatureIdentifier.TYPES.SuperEditor,
|
||||
noteType: NoteType.Super,
|
||||
references: [],
|
||||
})
|
||||
|
||||
@@ -168,7 +168,7 @@ const IframeFeatureView: FunctionComponent<Props> = ({ onLoad, componentViewer,
|
||||
const unregisterDesktopObserver = application
|
||||
.getDesktopService()
|
||||
?.registerUpdateObserver((updatedComponent: ComponentInterface) => {
|
||||
if (updatedComponent.uuid === uiFeature.uniqueIdentifier) {
|
||||
if (updatedComponent.uuid === uiFeature.uniqueIdentifier.value) {
|
||||
requestReload?.(componentViewer)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import {
|
||||
FeatureIdentifier,
|
||||
NativeFeatureIdentifier,
|
||||
NewNoteTitleFormat,
|
||||
PrefKey,
|
||||
EditorIdentifier,
|
||||
TagPreferences,
|
||||
isSmartView,
|
||||
isSystemView,
|
||||
SystemViewId,
|
||||
PrefDefaults,
|
||||
FeatureStatus,
|
||||
Uuid,
|
||||
} from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
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 { AnyTag } from '@/Controllers/Navigation/AnyTagType'
|
||||
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 dayjsAdvancedFormat from 'dayjs/plugin/advancedFormat'
|
||||
import dayjsUTC from 'dayjs/plugin/utc'
|
||||
@@ -24,11 +29,6 @@ import dayjsTimezone from 'dayjs/plugin/timezone'
|
||||
dayjs.extend(dayjsAdvancedFormat)
|
||||
dayjs.extend(dayjsUTC)
|
||||
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
|
||||
|
||||
@@ -57,8 +57,8 @@ const NewNotePreferences: FunctionComponent<Props> = ({
|
||||
: selectedTag.preferences
|
||||
|
||||
const [editorItems, setEditorItems] = useState<DropdownItem[]>([])
|
||||
const [defaultEditorIdentifier, setDefaultEditorIdentifier] = useState<EditorIdentifier>(
|
||||
FeatureIdentifier.PlainEditor,
|
||||
const [defaultEditorIdentifier, setDefaultEditorIdentifier] = useState<string>(
|
||||
NativeFeatureIdentifier.TYPES.PlainEditor,
|
||||
)
|
||||
const [newNoteTitleFormat, setNewNoteTitleFormat] = useState<NewNoteTitleFormat>(
|
||||
NewNoteTitleFormat.CurrentDateAndTime,
|
||||
@@ -121,14 +121,19 @@ const NewNotePreferences: FunctionComponent<Props> = ({
|
||||
|
||||
const selectEditorForNewNoteDefault = useCallback(
|
||||
(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)
|
||||
if (editorItem) {
|
||||
premiumModal.activate(editorItem.label)
|
||||
}
|
||||
return
|
||||
}
|
||||
setDefaultEditorIdentifier(value as FeatureIdentifier)
|
||||
setDefaultEditorIdentifier(value)
|
||||
|
||||
if (mode === 'global') {
|
||||
void application.setPreference(PrefKey.DefaultEditorIdentifier, value)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { WebApplication } from '@/Application/WebApplication'
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
import {
|
||||
MutatorService,
|
||||
SNComponentManager,
|
||||
SNComponent,
|
||||
SNTag,
|
||||
@@ -11,7 +10,7 @@ import {
|
||||
ItemManagerInterface,
|
||||
MutatorClientInterface,
|
||||
} from '@standardnotes/snjs'
|
||||
import { FeatureIdentifier, NoteType } from '@standardnotes/features'
|
||||
import { NativeFeatureIdentifier, NoteType } from '@standardnotes/features'
|
||||
import { NoteViewController } from './NoteViewController'
|
||||
|
||||
describe('note view controller', () => {
|
||||
@@ -40,7 +39,9 @@ describe('note view controller', () => {
|
||||
})
|
||||
|
||||
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)
|
||||
await controller.initialize()
|
||||
@@ -55,13 +56,13 @@ describe('note view controller', () => {
|
||||
it('should create notes with markdown note type', async () => {
|
||||
application.items.getDisplayableComponents = jest.fn().mockReturnValue([
|
||||
{
|
||||
identifier: FeatureIdentifier.MarkdownProEditor,
|
||||
identifier: NativeFeatureIdentifier.TYPES.MarkdownProEditor,
|
||||
} as SNComponent,
|
||||
])
|
||||
|
||||
application.componentManager.getDefaultEditorIdentifier = jest
|
||||
.fn()
|
||||
.mockReturnValue(FeatureIdentifier.MarkdownProEditor)
|
||||
.mockReturnValue(NativeFeatureIdentifier.TYPES.MarkdownProEditor)
|
||||
|
||||
const controller = new NoteViewController(application)
|
||||
await controller.initialize()
|
||||
@@ -74,7 +75,9 @@ describe('note view controller', () => {
|
||||
})
|
||||
|
||||
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 = {
|
||||
uuid: 'tag-uuid',
|
||||
|
||||
@@ -8,7 +8,7 @@ import { ElementIds } from '@/Constants/ElementIDs'
|
||||
import { StringDeleteNote, STRING_DELETE_LOCKED_ATTEMPT, STRING_DELETE_PLACEHOLDER_ATTEMPT } from '@/Constants/Strings'
|
||||
import { log, LoggingDomain } from '@/Logging'
|
||||
import { debounce, isDesktopApplication, isMobileScreen } from '@/Utils'
|
||||
import { classNames, pluralize } from '@standardnotes/utils'
|
||||
import { classNames, compareArrayReferences, pluralize } from '@standardnotes/utils'
|
||||
import {
|
||||
ApplicationEvent,
|
||||
ComponentArea,
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
EditorLineWidth,
|
||||
IframeComponentFeatureDescription,
|
||||
isUIFeatureAnIframeFeature,
|
||||
isPayloadSourceInternalChange,
|
||||
isPayloadSourceRetrieved,
|
||||
NoteType,
|
||||
PayloadEmitSource,
|
||||
@@ -94,7 +93,6 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
||||
onEditorComponentLoad?: () => void
|
||||
|
||||
private removeTrashKeyObserver?: () => void
|
||||
private removeComponentStreamObserver?: () => void
|
||||
private removeNoteStreamObserver?: () => void
|
||||
private removeComponentManagerObserver?: () => void
|
||||
private removeInnerNoteObserver?: () => void
|
||||
@@ -144,9 +142,6 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
||||
super.deinit()
|
||||
;(this.controller as unknown) = undefined
|
||||
|
||||
this.removeComponentStreamObserver?.()
|
||||
;(this.removeComponentStreamObserver as unknown) = undefined
|
||||
|
||||
this.removeNoteStreamObserver?.()
|
||||
;(this.removeNoteStreamObserver as unknown) = undefined
|
||||
|
||||
@@ -187,14 +182,19 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
||||
}
|
||||
|
||||
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)[]) {
|
||||
if (complexObjects.includes(key)) {
|
||||
continue
|
||||
}
|
||||
const prevValue = this.state[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) {
|
||||
log(LoggingDomain.NoteView, 'Rendering due to state change', key, prevValue, nextValue)
|
||||
return true
|
||||
@@ -340,7 +340,8 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
||||
|
||||
switch (eventName) {
|
||||
case ApplicationEvent.PreferencesChanged:
|
||||
this.reloadPreferences().catch(console.error)
|
||||
void this.reloadPreferences()
|
||||
void this.reloadStackComponents()
|
||||
break
|
||||
case ApplicationEvent.HighLatencySync:
|
||||
this.setState({ syncTakingTooLong: true })
|
||||
@@ -428,23 +429,6 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
||||
}
|
||||
|
||||
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 () => {
|
||||
if (!this.note) {
|
||||
return
|
||||
@@ -740,25 +724,22 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
||||
|
||||
async reloadStackComponents() {
|
||||
log(LoggingDomain.NoteView, 'Reload stack components')
|
||||
const stackComponents = sortAlphabetically(
|
||||
const enabledComponents = sortAlphabetically(
|
||||
this.application.componentManager
|
||||
.thirdPartyComponentsForArea(ComponentArea.EditorStack)
|
||||
.filter((component) => this.application.componentManager.isComponentActive(component)),
|
||||
)
|
||||
const enabledComponents = stackComponents.filter((component) => {
|
||||
return component.isExplicitlyEnabledForItem(this.note.uuid)
|
||||
})
|
||||
|
||||
const needsNewViewer = enabledComponents.filter((component) => {
|
||||
const hasExistingViewer = this.state.stackComponentViewers.find(
|
||||
(viewer) => viewer.componentUniqueIdentifier === component.uuid,
|
||||
(viewer) => viewer.componentUniqueIdentifier.value === component.uuid,
|
||||
)
|
||||
return !hasExistingViewer
|
||||
})
|
||||
|
||||
const needsDestroyViewer = this.state.stackComponentViewers.filter((viewer) => {
|
||||
const viewerComponentExistsInEnabledComponents = enabledComponents.find((component) => {
|
||||
return component.uuid === viewer.componentUniqueIdentifier
|
||||
return component.uuid === viewer.componentUniqueIdentifier.value
|
||||
})
|
||||
return !viewerComponentExistsInEnabledComponents
|
||||
})
|
||||
@@ -779,13 +760,15 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
||||
this.application.componentManager.destroyComponentViewer(viewer)
|
||||
}
|
||||
this.setState({
|
||||
availableStackComponents: stackComponents,
|
||||
availableStackComponents: enabledComponents,
|
||||
stackComponentViewers: newViewers,
|
||||
})
|
||||
}
|
||||
|
||||
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) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {
|
||||
FeatureIdentifier,
|
||||
NativeFeatureIdentifier,
|
||||
FeatureStatus,
|
||||
MuteMarketingEmailsOption,
|
||||
MuteSignInEmailsOption,
|
||||
@@ -28,7 +28,9 @@ const Email: FunctionComponent<Props> = ({ application }: Props) => {
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
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> => {
|
||||
try {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FeatureStatus, FeatureIdentifier } from '@standardnotes/snjs'
|
||||
import { FeatureStatus, NativeFeatureIdentifier } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent, useState } from 'react'
|
||||
|
||||
@@ -29,8 +29,9 @@ const SubscriptionSharing: FunctionComponent<Props> = ({ application, viewContro
|
||||
const isReadOnlySession = application.sessions.isCurrentSessionReadOnly()
|
||||
|
||||
const isSubscriptionSharingFeatureAvailable =
|
||||
application.features.getFeatureStatus(FeatureIdentifier.SubscriptionSharing) === FeatureStatus.Entitled &&
|
||||
!isReadOnlySession
|
||||
application.features.getFeatureStatus(
|
||||
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.SubscriptionSharing).getValue(),
|
||||
) === FeatureStatus.Entitled && !isReadOnlySession
|
||||
|
||||
const closeInviteDialog = () => setIsInviteDialogOpen(false)
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { usePremiumModal } from '@/Hooks/usePremiumModal'
|
||||
import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator'
|
||||
import Switch from '@/Components/Switch/Switch'
|
||||
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 { FunctionComponent, useEffect, useState } from 'react'
|
||||
import { Subtitle, Title, Text } from '@/Components/Preferences/PreferencesComponents/Content'
|
||||
@@ -50,7 +50,7 @@ const Appearance: FunctionComponent<Props> = ({ application }) => {
|
||||
label: theme.displayName as string,
|
||||
value: theme.featureIdentifier,
|
||||
icon:
|
||||
application.features.getFeatureStatus(theme.featureIdentifier) !== FeatureStatus.Entitled
|
||||
application.features.getFeatureStatus(theme.uniqueIdentifier) !== FeatureStatus.Entitled
|
||||
? PremiumFeatureIconName
|
||||
: undefined,
|
||||
}
|
||||
@@ -72,14 +72,10 @@ const Appearance: FunctionComponent<Props> = ({ application }) => {
|
||||
const toggleUseDeviceSettings = () => {
|
||||
application.setPreference(PrefKey.UseSystemColorScheme, !useDeviceSettings).catch(console.error)
|
||||
if (!application.getPreference(PrefKey.AutoLightThemeIdentifier)) {
|
||||
application
|
||||
.setPreference(PrefKey.AutoLightThemeIdentifier, autoLightTheme as FeatureIdentifier)
|
||||
.catch(console.error)
|
||||
application.setPreference(PrefKey.AutoLightThemeIdentifier, autoLightTheme).catch(console.error)
|
||||
}
|
||||
if (!application.getPreference(PrefKey.AutoDarkThemeIdentifier)) {
|
||||
application
|
||||
.setPreference(PrefKey.AutoDarkThemeIdentifier, autoDarkTheme as FeatureIdentifier)
|
||||
.catch(console.error)
|
||||
application.setPreference(PrefKey.AutoDarkThemeIdentifier, autoDarkTheme).catch(console.error)
|
||||
}
|
||||
setUseDeviceSettings(!useDeviceSettings)
|
||||
}
|
||||
@@ -90,7 +86,7 @@ const Appearance: FunctionComponent<Props> = ({ application }) => {
|
||||
premiumModal.activate(`${item.label} theme`)
|
||||
return
|
||||
}
|
||||
application.setPreference(PrefKey.AutoLightThemeIdentifier, value as FeatureIdentifier).catch(console.error)
|
||||
application.setPreference(PrefKey.AutoLightThemeIdentifier, value).catch(console.error)
|
||||
setAutoLightTheme(value)
|
||||
}
|
||||
|
||||
@@ -100,7 +96,7 @@ const Appearance: FunctionComponent<Props> = ({ application }) => {
|
||||
premiumModal.activate(`${item.label} theme`)
|
||||
return
|
||||
}
|
||||
application.setPreference(PrefKey.AutoDarkThemeIdentifier, value as FeatureIdentifier).catch(console.error)
|
||||
application.setPreference(PrefKey.AutoDarkThemeIdentifier, value).catch(console.error)
|
||||
setAutoDarkTheme(value)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Text, Title } from '@/Components/Preferences/PreferencesComponents/Cont
|
||||
import { WebApplication } from '@/Application/WebApplication'
|
||||
import {
|
||||
ApplicationEvent,
|
||||
FeatureIdentifier,
|
||||
NativeFeatureIdentifier,
|
||||
FeatureStatus,
|
||||
FindNativeFeature,
|
||||
PrefKey,
|
||||
@@ -17,7 +17,7 @@ import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator'
|
||||
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
|
||||
|
||||
type ExperimentalFeatureItem = {
|
||||
identifier: FeatureIdentifier
|
||||
identifier: string
|
||||
name: string
|
||||
description: string
|
||||
isEnabled: boolean
|
||||
@@ -55,7 +55,9 @@ const LabsPane: FunctionComponent<Props> = ({ application }) => {
|
||||
name: feature?.name ?? featureIdentifier,
|
||||
description: feature?.description ?? '',
|
||||
isEnabled: application.features.isExperimentalFeatureEnabled(featureIdentifier),
|
||||
isEntitled: application.features.getFeatureStatus(featureIdentifier) === FeatureStatus.Entitled,
|
||||
isEntitled:
|
||||
application.features.getFeatureStatus(NativeFeatureIdentifier.create(featureIdentifier).getValue()) ===
|
||||
FeatureStatus.Entitled,
|
||||
}
|
||||
})
|
||||
setExperimentalFeatures(experimentalFeatures)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FeatureIdentifier, FeatureStatus } from '@standardnotes/snjs'
|
||||
import { NativeFeatureIdentifier, FeatureStatus } from '@standardnotes/snjs'
|
||||
|
||||
import { WebApplication } from '@/Application/WebApplication'
|
||||
import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
|
||||
@@ -24,8 +24,9 @@ const Security: FunctionComponent<SecurityProps> = (props) => {
|
||||
const isNativeMobileWeb = props.application.isNativeMobileWeb()
|
||||
|
||||
const isU2FFeatureAvailable =
|
||||
props.application.features.getFeatureStatus(FeatureIdentifier.UniversalSecondFactor) === FeatureStatus.Entitled &&
|
||||
props.userProvider.getUser() !== undefined
|
||||
props.application.features.getFeatureStatus(
|
||||
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.UniversalSecondFactor).getValue(),
|
||||
) === FeatureStatus.Entitled && props.userProvider.getUser() !== undefined
|
||||
|
||||
return (
|
||||
<PreferencesPane>
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
ComponentInterface,
|
||||
UIFeature,
|
||||
ContentType,
|
||||
FeatureIdentifier,
|
||||
NativeFeatureIdentifier,
|
||||
PreferencesServiceEvent,
|
||||
ThemeFeatureDescription,
|
||||
} from '@standardnotes/snjs'
|
||||
@@ -54,7 +54,7 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = ({ quickSettingsMenuCont
|
||||
(component) =>
|
||||
!component.isTheme() &&
|
||||
[ComponentArea.EditorStack].includes(component.area) &&
|
||||
component.identifier !== FeatureIdentifier.DeprecatedFoldersComponent,
|
||||
component.identifier !== NativeFeatureIdentifier.TYPES.DeprecatedFoldersComponent,
|
||||
)
|
||||
|
||||
setEditorStackComponents(toggleableComponents)
|
||||
@@ -100,7 +100,7 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = ({ quickSettingsMenuCont
|
||||
|
||||
const toggleEditorStackComponent = useCallback(
|
||||
(component: ComponentInterface) => {
|
||||
application.componentManager.toggleComponent(component).catch(console.error)
|
||||
void application.componentManager.toggleComponent(component)
|
||||
},
|
||||
[application],
|
||||
)
|
||||
@@ -141,7 +141,7 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = ({ quickSettingsMenuCont
|
||||
Default
|
||||
</MenuRadioButtonItem>
|
||||
{themes.map((theme) => (
|
||||
<ThemesMenuButton uiFeature={theme} key={theme.uniqueIdentifier} />
|
||||
<ThemesMenuButton uiFeature={theme} key={theme.uniqueIdentifier.value} />
|
||||
))}
|
||||
<HorizontalSeparator classes="my-2" />
|
||||
<FocusModeSwitch
|
||||
|
||||
@@ -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 Icon from '@/Components/Icon/Icon'
|
||||
import { usePremiumModal } from '@/Hooks/usePremiumModal'
|
||||
@@ -26,8 +26,8 @@ const ThemesMenuButton: FunctionComponent<Props> = ({ uiFeature }) => {
|
||||
[application, uiFeature.featureIdentifier],
|
||||
)
|
||||
const isEntitledToTheme = useMemo(
|
||||
() => application.features.getFeatureStatus(uiFeature.featureIdentifier) === FeatureStatus.Entitled,
|
||||
[application, uiFeature.featureIdentifier],
|
||||
() => application.features.getFeatureStatus(uiFeature.uniqueIdentifier) === FeatureStatus.Entitled,
|
||||
[application, uiFeature.uniqueIdentifier],
|
||||
)
|
||||
const canActivateTheme = useMemo(() => isEntitledToTheme || isThirdPartyTheme, [isEntitledToTheme, isThirdPartyTheme])
|
||||
|
||||
@@ -55,10 +55,10 @@ const ThemesMenuButton: FunctionComponent<Props> = ({ uiFeature }) => {
|
||||
)
|
||||
|
||||
const isMobile = application.isNativeMobileWeb() || isMobileScreen()
|
||||
const shouldHideButton = uiFeature.featureIdentifier === FeatureIdentifier.DynamicTheme && isMobile
|
||||
const shouldHideButton = uiFeature.featureIdentifier === NativeFeatureIdentifier.TYPES.DynamicTheme && isMobile
|
||||
|
||||
const darkThemeShortcut = useMemo(() => {
|
||||
if (uiFeature.featureIdentifier === FeatureIdentifier.DarkTheme) {
|
||||
if (uiFeature.featureIdentifier === NativeFeatureIdentifier.TYPES.DarkTheme) {
|
||||
return commandService.keyboardShortcutForCommand(TOGGLE_DARK_MODE_COMMAND)
|
||||
}
|
||||
}, [commandService, uiFeature.featureIdentifier])
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FeatureIdentifier } from '@standardnotes/features'
|
||||
import { NativeFeatureIdentifier } from '@standardnotes/features'
|
||||
import { NoteType, PredicateCompoundOperator, PredicateJsonForm } from '@standardnotes/snjs'
|
||||
import { makeObservable, observable, action } from 'mobx'
|
||||
import { PredicateKeypath, PredicateKeypathTypes } from './PredicateKeypaths'
|
||||
@@ -59,7 +59,7 @@ export class CompoundPredicateBuilderController {
|
||||
this.setPredicate(index, { value: Object.values(NoteType)[0] })
|
||||
break
|
||||
case 'editorIdentifier':
|
||||
this.setPredicate(index, { value: FeatureIdentifier.PlainEditor })
|
||||
this.setPredicate(index, { value: NativeFeatureIdentifier.TYPES.PlainEditor })
|
||||
break
|
||||
case 'date':
|
||||
this.setPredicate(index, { value: '1.days.ago' })
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
isPayloadSourceRetrieved,
|
||||
PrefKey,
|
||||
PrefDefaults,
|
||||
FeatureIdentifier,
|
||||
NativeFeatureIdentifier,
|
||||
FeatureStatus,
|
||||
GetSuperNoteFeature,
|
||||
} from '@standardnotes/snjs'
|
||||
@@ -76,7 +76,12 @@ export const SuperEditor: FunctionComponent<Props> = ({
|
||||
|
||||
useEffect(() => {
|
||||
setFeatureStatus(
|
||||
application.features.getFeatureStatus(FeatureIdentifier.SuperEditor, { inContextOfItem: note.current }),
|
||||
application.features.getFeatureStatus(
|
||||
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.SuperEditor).getValue(),
|
||||
{
|
||||
inContextOfItem: note.current,
|
||||
},
|
||||
),
|
||||
)
|
||||
}, [application.features])
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { PremiumFeatureModalType } from '@/Components/PremiumFeaturesModal/Premi
|
||||
import { destroyAllObjectProperties } from '@/Utils'
|
||||
import {
|
||||
ApplicationEvent,
|
||||
FeatureIdentifier,
|
||||
NativeFeatureIdentifier,
|
||||
FeatureStatus,
|
||||
InternalEventBusInterface,
|
||||
InternalEventInterface,
|
||||
@@ -100,19 +100,25 @@ export class FeaturesController extends AbstractViewController {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { FeatureIdentifier } from '@standardnotes/snjs'
|
||||
import { ComponentArea, FindNativeFeature, GetIframeAndNativeEditors } from '@standardnotes/features'
|
||||
import {
|
||||
ComponentArea,
|
||||
FindNativeFeature,
|
||||
GetIframeAndNativeEditors,
|
||||
NativeFeatureIdentifier,
|
||||
} from '@standardnotes/features'
|
||||
import { getIconAndTintForNoteType } from './Items/Icons/getIconAndTintForNoteType'
|
||||
import { DropdownItem } from '@/Components/Dropdown/DropdownItem'
|
||||
import { WebApplicationInterface } from '@standardnotes/ui-services'
|
||||
|
||||
export type EditorOption = DropdownItem & {
|
||||
value: FeatureIdentifier
|
||||
value: string
|
||||
isLabs?: boolean
|
||||
}
|
||||
|
||||
@@ -19,6 +23,7 @@ export function getDropdownItemsForAllEditors(application: WebApplicationInterfa
|
||||
return {
|
||||
label: editor.name,
|
||||
value: editor.identifier,
|
||||
id: NativeFeatureIdentifier.create(editor.identifier).getValue(),
|
||||
...(iconType ? { icon: iconType } : null),
|
||||
...(tint ? { iconClassName: `text-accessory-tint-${tint}` } : null),
|
||||
}
|
||||
@@ -30,12 +35,11 @@ export function getDropdownItemsForAllEditors(application: WebApplicationInterfa
|
||||
.thirdPartyComponentsForArea(ComponentArea.Editor)
|
||||
.filter((component) => FindNativeFeature(component.identifier) === undefined)
|
||||
.map((editor): EditorOption => {
|
||||
const identifier = editor.package_info.identifier
|
||||
const [iconType, tint] = getIconAndTintForNoteType(editor.noteType)
|
||||
|
||||
return {
|
||||
label: editor.displayName,
|
||||
value: identifier,
|
||||
value: editor.uuid,
|
||||
...(iconType ? { icon: iconType } : null),
|
||||
...(tint ? { iconClassName: `text-accessory-tint-${tint}` } : null),
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { UIFeature, FeatureIdentifier, ThemeFeatureDescription } from '@standardnotes/snjs'
|
||||
import { UIFeature, NativeFeatureIdentifier, ThemeFeatureDescription } from '@standardnotes/snjs'
|
||||
|
||||
const isDarkModeTheme = (theme: UIFeature<ThemeFeatureDescription>) =>
|
||||
theme.featureIdentifier === FeatureIdentifier.DarkTheme
|
||||
theme.featureIdentifier === NativeFeatureIdentifier.TYPES.DarkTheme
|
||||
|
||||
export const sortThemes = (a: UIFeature<ThemeFeatureDescription>, b: UIFeature<ThemeFeatureDescription>) => {
|
||||
const aIsLayerable = a.layerable
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
GetSuperNoteFeature,
|
||||
UIFeature,
|
||||
IframeComponentFeatureDescription,
|
||||
Uuid,
|
||||
NativeFeatureIdentifier,
|
||||
} from '@standardnotes/snjs'
|
||||
import { EditorMenuGroup } from '@/Components/NotesOptions/EditorMenuGroup'
|
||||
import { EditorMenuItem } from '@/Components/NotesOptions/EditorMenuItem'
|
||||
@@ -29,7 +31,9 @@ const insertNativeEditorsInMap = (map: NoteTypeToEditorRowsMap, application: Web
|
||||
|
||||
const noteType = editorFeature.note_type
|
||||
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),
|
||||
})
|
||||
}
|
||||
@@ -52,7 +56,7 @@ const insertInstalledComponentsInMap = (map: NoteTypeToEditorRowsMap, applicatio
|
||||
|
||||
const editorItem: EditorMenuItem = {
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user