Compare commits

..
119 changed files with 1226 additions and 732 deletions
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.26.32](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
**Note:** Version bump only for package @standardnotes/api
## [1.26.31](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
**Note:** Version bump only for package @standardnotes/api
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/api",
"version": "1.26.31",
"version": "1.26.32",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.1.116](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
**Note:** Version bump only for package @standardnotes/clipper
## [1.1.115](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
**Note:** Version bump only for package @standardnotes/clipper
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@standardnotes/clipper",
"description": "Web clipper browser extension for Standard Notes",
"version": "1.1.115",
"version": "1.1.116",
"private": true,
"scripts": {
"build-mv2": "yarn clean && webpack --config ./webpack.config.prod.js",
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.108.47](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
**Note:** Version bump only for package @standardnotes/desktop
## [3.108.46](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
### Reverts
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@standardnotes/desktop",
"main": "./app/dist/index.js",
"version": "3.108.46",
"version": "3.108.47",
"license": "AGPL-3.0-or-later",
"author": "Standard Notes.",
"private": true,
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.21.55](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
**Note:** Version bump only for package @standardnotes/encryption
## [1.21.54](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
**Note:** Version bump only for package @standardnotes/encryption
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/encryption",
"version": "1.21.54",
"version": "1.21.55",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+6
View File
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.59.11](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
### Bug Fixes
* Fixes issue where lock screen would not use previously active theme ([#2372](https://github.com/standardnotes/app/issues/2372)) ([d268c02](https://github.com/standardnotes/app/commit/d268c02ab31beb5e2fd9e6547610f9a4dd61bed4))
## [1.59.10](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-17)
**Note:** Version bump only for package @standardnotes/features
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/features",
"version": "1.59.10",
"version": "1.59.11",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -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],
},
+8 -8
View File
@@ -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 -2
View File
@@ -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'
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.28.64](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
**Note:** Version bump only for package @standardnotes/filepicker
## [1.28.63](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
**Note:** Version bump only for package @standardnotes/filepicker
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/filepicker",
"version": "1.28.63",
"version": "1.28.64",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.16.10](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
**Note:** Version bump only for package @standardnotes/files
## [1.16.9](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
**Note:** Version bump only for package @standardnotes/files
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/files",
"version": "1.16.9",
"version": "1.16.10",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.56.27](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
**Note:** Version bump only for package @standardnotes/mobile
## [3.56.26](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
**Note:** Version bump only for package @standardnotes/mobile
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/mobile",
"version": "3.56.26",
"version": "3.56.27",
"author": "Standard Notes.",
"private": true,
"license": "AGPL-3.0-or-later",
+6
View File
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.46.14](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
### Bug Fixes
* Fixes issue where lock screen would not use previously active theme ([#2372](https://github.com/standardnotes/app/issues/2372)) ([d268c02](https://github.com/standardnotes/app/commit/d268c02ab31beb5e2fd9e6547610f9a4dd61bed4))
## [1.46.13](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
**Note:** Version bump only for package @standardnotes/models
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/models",
"version": "1.46.13",
"version": "1.46.14",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -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>>
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.4.399](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.398](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
**Note:** Version bump only for package @standardnotes/releases
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/releases",
"version": "1.4.398",
"version": "1.4.399",
"license": "AGPL-3.0-or-later",
"main": "dist/releases.json",
"types": "dist/index.d.ts",
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.13.32](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
**Note:** Version bump only for package @standardnotes/responses
## [1.13.31](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-23)
**Note:** Version bump only for package @standardnotes/responses
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/responses",
"version": "1.13.31",
"version": "1.13.32",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+6
View File
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.63.12](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
### Bug Fixes
* Fixes issue where lock screen would not use previously active theme ([#2372](https://github.com/standardnotes/app/issues/2372)) ([d268c02](https://github.com/standardnotes/app/commit/d268c02ab31beb5e2fd9e6547610f9a4dd61bed4))
## [1.63.11](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
**Note:** Version bump only for package @standardnotes/services
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/services",
"version": "1.63.11",
"version": "1.63.12",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -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()
+6
View File
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.202.16](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
### Bug Fixes
* Fixes issue where lock screen would not use previously active theme ([#2372](https://github.com/standardnotes/app/issues/2372)) ([d268c02](https://github.com/standardnotes/app/commit/d268c02ab31beb5e2fd9e6547610f9a4dd61bed4))
## [2.202.15](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
**Note:** Version bump only for package @standardnotes/snjs
+2 -4
View File
@@ -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)
})
})
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/snjs",
"version": "2.202.15",
"version": "2.202.16",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+6
View File
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.28.14](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
### Bug Fixes
* Fixes issue where lock screen would not use previously active theme ([#2372](https://github.com/standardnotes/app/issues/2372)) ([d268c02](https://github.com/standardnotes/app/commit/d268c02ab31beb5e2fd9e6547610f9a4dd61bed4))
## [1.28.13](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
**Note:** Version bump only for package @standardnotes/ui-services
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/ui-services",
"version": "1.28.13",
"version": "1.28.14",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -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":"[email protected]","secret":"TESTMAILTESTMAILTESTMAILTESTMAIL","notes":"Some note"},{"service":"Some Service","account":"[email protected]","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,
}),
},
}
+4 -2
View File
@@ -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)]
@@ -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)
})
})
@@ -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
}
}
+84 -63
View File
@@ -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
}
}
+6
View File
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.17.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
### Bug Fixes
* Fixes issue where lock screen would not use previously active theme ([#2372](https://github.com/standardnotes/app/issues/2372)) ([d268c02](https://github.com/standardnotes/app/commit/d268c02ab31beb5e2fd9e6547610f9a4dd61bed4))
## [1.17.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-12)
**Note:** Version bump only for package @standardnotes/utils
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/utils",
"version": "1.17.6",
"version": "1.17.7",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -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",
+36 -1
View File
@@ -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)
+4
View File
@@ -223,6 +223,10 @@ export function arrayByDifference<T>(array: T[], subtract: T[]): T[] {
return array.filter((x) => !subtract.includes(x)).concat(subtract.filter((x) => !array.includes(x)))
}
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
View File
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.167.14](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
### Bug Fixes
* Fixes issue where lock screen would not use previously active theme ([#2372](https://github.com/standardnotes/app/issues/2372)) ([d268c02](https://github.com/standardnotes/app/commit/d268c02ab31beb5e2fd9e6547610f9a4dd61bed4))
## [3.167.13](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
**Note:** Version bump only for package @standardnotes/web
+14
View File
@@ -1,5 +1,19 @@
{
"versions": [
{
"version": "3.167.14",
"title": "[3.167.14](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)",
"date": null,
"body": "### Bug Fixes\n\n* Fixes issue where lock screen would not use previously active theme ([#2372](https://github.com/standardnotes/app/issues/2372)) ([d268c02](https://github.com/standardnotes/app/commit/d268c02ab31beb5e2fd9e6547610f9a4dd61bed4))",
"parsed": {
"_": [
"Fixes issue where lock screen would not use previously active theme (#2372) (d268c02)"
],
"Bug Fixes": [
"Fixes issue where lock screen would not use previously active theme (#2372) (d268c02)"
]
}
},
{
"version": "3.167.13",
"title": "[3.167.13](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/web",
"version": "3.167.13",
"version": "3.167.14",
"license": "AGPL-3.0-or-later",
"main": "dist/app.js",
"author": "Standard Notes",
@@ -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)}

Some files were not shown because too many files have changed in this diff Show More