diff --git a/packages/api/package.json b/packages/api/package.json index 2b494706d70..aec28361964 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -27,7 +27,6 @@ }, "devDependencies": { "@types/jest": "^29.2.3", - "@types/lodash": "^4.14.189", "@typescript-eslint/eslint-plugin": "*", "eslint": "^8.27.0", "eslint-plugin-prettier": "*", diff --git a/packages/models/src/Domain/Syncable/Component/Component.ts b/packages/models/src/Domain/Syncable/Component/Component.ts index 5cb4f0b607a..4ec8042e86b 100644 --- a/packages/models/src/Domain/Syncable/Component/Component.ts +++ b/packages/models/src/Domain/Syncable/Component/Component.ts @@ -47,7 +47,7 @@ export class SNComponent extends DecryptedItem implements Comp public readonly area: ComponentArea public readonly permissions: ComponentPermission[] = [] public readonly valid_until: Date - public readonly active: boolean + public readonly legacyActive: boolean public readonly legacy_url?: string public readonly isMobileDefault: boolean @@ -69,7 +69,6 @@ export class SNComponent extends DecryptedItem implements Comp this.area = payload.content.area this.package_info = payload.content.package_info || {} this.permissions = payload.content.permissions || [] - this.active = payload.content.active ?? false this.autoupdateDisabled = payload.content.autoupdateDisabled ?? false this.disassociatedItemIds = payload.content.disassociatedItemIds || [] this.associatedItemIds = payload.content.associatedItemIds || [] @@ -85,6 +84,8 @@ export class SNComponent extends DecryptedItem implements Comp this.legacy_url = !payload.content.hosted_url ? payload.content.url : undefined this.legacyComponentData = this.payload.content.componentData || {} + + this.legacyActive = payload.content.active ?? false } /** Do not duplicate components under most circumstances. Always keep original */ diff --git a/packages/models/src/Domain/Syncable/Component/ComponentInterface.ts b/packages/models/src/Domain/Syncable/Component/ComponentInterface.ts index e2518b08a04..e9aea8f7faf 100644 --- a/packages/models/src/Domain/Syncable/Component/ComponentInterface.ts +++ b/packages/models/src/Domain/Syncable/Component/ComponentInterface.ts @@ -1,4 +1,10 @@ -import { ComponentArea, ComponentPermission, FeatureIdentifier, NoteType } from '@standardnotes/features' +import { + ComponentArea, + ComponentPermission, + FeatureIdentifier, + NoteType, + ThirdPartyFeatureDescription, +} from '@standardnotes/features' import { ComponentPackageInfo } from './PackageInfo' import { DecryptedItemInterface } from '../../Abstract/Item' import { ComponentContent } from './ComponentContent' @@ -20,8 +26,6 @@ export interface ComponentInterface extends DecryptedItemInterface { - set active(active: boolean) { - this.mutableContent.active = active - } - set isMobileDefault(isMobileDefault: boolean) { this.mutableContent.isMobileDefault = isMobileDefault } diff --git a/packages/models/src/Domain/Syncable/Theme/ThemeMutator.ts b/packages/models/src/Domain/Syncable/Theme/ThemeMutator.ts index d102c8f111a..e69cba77e8f 100644 --- a/packages/models/src/Domain/Syncable/Theme/ThemeMutator.ts +++ b/packages/models/src/Domain/Syncable/Theme/ThemeMutator.ts @@ -1,25 +1,4 @@ -import { AppDataField } from '../../Abstract/Item/Types/AppDataField' import { ComponentContent } from '../Component/ComponentContent' import { DecryptedItemMutator } from '../../Abstract/Item/Mutator/DecryptedItemMutator' -export class ThemeMutator extends DecryptedItemMutator { - setMobileRules(rules: unknown) { - this.setAppDataItem(AppDataField.MobileRules, rules) - } - - setNotAvailOnMobile(notAvailable: boolean) { - this.setAppDataItem(AppDataField.NotAvailableOnMobile, notAvailable) - } - - set local_url(local_url: string) { - this.mutableContent.local_url = local_url - } - - /** - * We must not use .active because if you set that to true, it will also - * activate that theme on desktop/web - */ - setMobileActive(active: boolean) { - this.setAppDataItem(AppDataField.MobileActive, active) - } -} +export class ThemeMutator extends DecryptedItemMutator {} diff --git a/packages/web/src/javascripts/Constants/PrefDefaults.ts b/packages/models/src/Domain/Syncable/UserPrefs/PrefDefaults.ts similarity index 77% rename from packages/web/src/javascripts/Constants/PrefDefaults.ts rename to packages/models/src/Domain/Syncable/UserPrefs/PrefDefaults.ts index 2b45447c71e..b1e27095cdd 100644 --- a/packages/web/src/javascripts/Constants/PrefDefaults.ts +++ b/packages/models/src/Domain/Syncable/UserPrefs/PrefDefaults.ts @@ -1,13 +1,10 @@ -import { - PrefKey, - CollectionSort, - NewNoteTitleFormat, - EditorLineHeight, - EditorFontSize, - EditorLineWidth, - PrefValue, -} from '@standardnotes/models' -import { FeatureIdentifier } from '@standardnotes/snjs' +import { FeatureIdentifier } from '@standardnotes/features' +import { CollectionSort } from '../../Runtime/Collection/CollectionSort' +import { EditorFontSize } from './EditorFontSize' +import { EditorLineHeight } from './EditorLineHeight' +import { EditorLineWidth } from './EditorLineWidth' +import { PrefKey, PrefValue } from './PrefKey' +import { NewNoteTitleFormat } from './NewNoteTitleFormat' export const PrefDefaults = { [PrefKey.TagsPanelWidth]: 220, @@ -45,6 +42,8 @@ export const PrefDefaults = { [PrefKey.SystemViewPreferences]: {}, [PrefKey.AuthenticatorNames]: '', [PrefKey.ComponentPreferences]: {}, + [PrefKey.ActiveThemes]: [], + [PrefKey.ActiveComponents]: [], } satisfies { [key in PrefKey]: PrefValue[key] } diff --git a/packages/models/src/Domain/Syncable/UserPrefs/PrefKey.ts b/packages/models/src/Domain/Syncable/UserPrefs/PrefKey.ts index bd8900ad873..4f151edadc4 100644 --- a/packages/models/src/Domain/Syncable/UserPrefs/PrefKey.ts +++ b/packages/models/src/Domain/Syncable/UserPrefs/PrefKey.ts @@ -44,6 +44,8 @@ export enum PrefKey { AuthenticatorNames = 'authenticatorNames', PaneGesturesEnabled = 'paneGesturesEnabled', ComponentPreferences = 'componentPreferences', + ActiveThemes = 'activeThemes', + ActiveComponents = 'activeComponents', } export type PrefValue = { @@ -82,4 +84,6 @@ export type PrefValue = { [PrefKey.AuthenticatorNames]: string [PrefKey.PaneGesturesEnabled]: boolean [PrefKey.ComponentPreferences]: AllComponentPreferences + [PrefKey.ActiveThemes]: string[] + [PrefKey.ActiveComponents]: string[] } diff --git a/packages/models/src/Domain/Syncable/UserPrefs/index.ts b/packages/models/src/Domain/Syncable/UserPrefs/index.ts index e8127fa4355..859f7e4de00 100644 --- a/packages/models/src/Domain/Syncable/UserPrefs/index.ts +++ b/packages/models/src/Domain/Syncable/UserPrefs/index.ts @@ -6,3 +6,4 @@ export * from './EditorFontSize' export * from './EditorLineWidth' export * from './NewNoteTitleFormat' export * from './ComponentPreferences' +export * from './PrefDefaults' diff --git a/packages/services/src/Domain/Application/ApplicationInterface.ts b/packages/services/src/Domain/Application/ApplicationInterface.ts index 2631b6d049a..59daedfd9df 100644 --- a/packages/services/src/Domain/Application/ApplicationInterface.ts +++ b/packages/services/src/Domain/Application/ApplicationInterface.ts @@ -1,3 +1,4 @@ +import { PreferenceServiceInterface } from './../Preferences/PreferenceServiceInterface' import { AsymmetricMessageServiceInterface } from './../AsymmetricMessage/AsymmetricMessageServiceInterface' import { SyncOptions } from './../Sync/SyncOptions' import { ImportDataReturnType } from './../Mutator/ImportDataUseCase' @@ -105,6 +106,7 @@ export interface ApplicationInterface { get challenges(): ChallengeServiceInterface get alerts(): AlertService get asymmetric(): AsymmetricMessageServiceInterface + get preferences(): PreferenceServiceInterface readonly identifier: ApplicationIdentifier readonly platform: Platform diff --git a/packages/services/src/Domain/Component/ComponentManagerInterface.ts b/packages/services/src/Domain/Component/ComponentManagerInterface.ts index 0bd12ce6864..1a791f5247f 100644 --- a/packages/services/src/Domain/Component/ComponentManagerInterface.ts +++ b/packages/services/src/Domain/Component/ComponentManagerInterface.ts @@ -6,6 +6,7 @@ import { ComponentOrNativeFeature, PermissionDialog, SNNote, + SNTheme, } from '@standardnotes/models' import { DesktopManagerInterface } from '../Device/DesktopManagerInterface' @@ -31,6 +32,7 @@ export interface ComponentManagerInterface { presentPermissionsDialog(_dialog: PermissionDialog): void legacyGetDefaultEditor(): ComponentInterface | undefined componentWithIdentifier(identifier: FeatureIdentifier | string): ComponentOrNativeFeature | undefined - toggleTheme(uuid: string): Promise + + toggleTheme(theme: SNTheme): Promise toggleComponent(component: ComponentInterface): Promise } diff --git a/packages/services/src/Domain/Preferences/PreferenceServiceInterface.ts b/packages/services/src/Domain/Preferences/PreferenceServiceInterface.ts index 78798a3972c..fe564bca666 100644 --- a/packages/services/src/Domain/Preferences/PreferenceServiceInterface.ts +++ b/packages/services/src/Domain/Preferences/PreferenceServiceInterface.ts @@ -1,8 +1,13 @@ -import { ComponentOrNativeFeature, ComponentPreferencesEntry, PrefKey, PrefValue } from '@standardnotes/models' +import { + ComponentInterface, + ComponentOrNativeFeature, + ComponentPreferencesEntry, + PrefKey, + PrefValue, + SNTheme, +} from '@standardnotes/models' import { AbstractService } from '../Service/AbstractService' -/* istanbul ignore file */ - export enum PreferencesServiceEvent { PreferencesChanged = 'PreferencesChanged', } @@ -15,4 +20,16 @@ export interface PreferenceServiceInterface extends AbstractService getComponentPreferences(component: ComponentOrNativeFeature): ComponentPreferencesEntry | undefined + + addActiveTheme(theme: SNTheme): Promise + replaceActiveTheme(theme: SNTheme): Promise + removeActiveTheme(theme: SNTheme): Promise + getActiveThemes(): SNTheme[] + getActiveThemesUuids(): string[] + isThemeActive(theme: SNTheme): boolean + + addActiveComponent(component: ComponentInterface): Promise + removeActiveComponent(component: ComponentInterface): Promise + getActiveComponents(): ComponentInterface[] + isComponentActive(component: ComponentInterface): boolean } diff --git a/packages/snjs/lib/Application/Application.ts b/packages/snjs/lib/Application/Application.ts index d74d87b2e45..ad2dcba8868 100644 --- a/packages/snjs/lib/Application/Application.ts +++ b/packages/snjs/lib/Application/Application.ts @@ -407,6 +407,10 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli return this.sharedVaultService } + public get preferences(): ExternalServices.PreferenceServiceInterface { + return this.preferencesService + } + public computePrivateUsername(username: string): Promise { return ComputePrivateUsername(this.options.crypto, username) } diff --git a/packages/snjs/lib/Migrations/Versions/2_201_6.ts b/packages/snjs/lib/Migrations/Versions/2_201_6.ts index 61886f5aad1..8f5cce7aa96 100644 --- a/packages/snjs/lib/Migrations/Versions/2_201_6.ts +++ b/packages/snjs/lib/Migrations/Versions/2_201_6.ts @@ -2,7 +2,7 @@ import { ApplicationStage } from '@standardnotes/services' import { Migration } from '@Lib/Migrations/Migration' import { ContentType } from '@standardnotes/common' import { AllComponentPreferences, ComponentInterface, PrefKey, isNativeComponent } from '@standardnotes/models' -import { Copy } from '@standardnotes/utils' +import { Copy, Uuids } from '@standardnotes/utils' export class Migration2_201_6 extends Migration { static override version(): string { @@ -12,6 +12,7 @@ export class Migration2_201_6 extends Migration { protected registerStageHandlers(): void { this.registerStageHandler(ApplicationStage.FullSyncCompleted_13, async () => { await this.migrateComponentDataToUserPreferences() + await this.migrateActiveComponentsToUserPreferences() this.markDone() }) } @@ -49,4 +50,21 @@ export class Migration2_201_6 extends Migration { await this.services.preferences.setValue(PrefKey.ComponentPreferences, mutablePreferencesValue) } + + private async migrateActiveComponentsToUserPreferences(): Promise { + const allActiveitems = [ + ...this.services.itemManager.getItems(ContentType.Component), + ...this.services.itemManager.getItems(ContentType.Theme), + ].filter((component) => component.legacyActive) + + if (allActiveitems.length === 0) { + return + } + + const activeThemes = allActiveitems.filter((component) => component.isTheme()) + const activeComponents = allActiveitems.filter((component) => !component.isTheme()) + + await this.services.preferences.setValue(PrefKey.ActiveThemes, Uuids(activeThemes)) + await this.services.preferences.setValue(PrefKey.ActiveComponents, Uuids(activeComponents)) + } } diff --git a/packages/snjs/lib/Services/Api/ApiService.ts b/packages/snjs/lib/Services/Api/ApiService.ts index 7bd602202fb..ba627bf0322 100644 --- a/packages/snjs/lib/Services/Api/ApiService.ts +++ b/packages/snjs/lib/Services/Api/ApiService.ts @@ -84,7 +84,6 @@ import { isUrlFirstParty, TRUSTED_FEATURE_HOSTS } from '@Lib/Hosts' import { Paths } from './Paths' import { DiskStorageService } from '../Storage/DiskStorageService' import { UuidString } from '../../Types/UuidString' -import merge from 'lodash/merge' import { SettingsServerInterface } from '../Settings/SettingsServerInterface' import { Strings } from '@Lib/Strings' @@ -199,9 +198,12 @@ export class SNApiService } private params(inParams: Record): HttpRequestParams { - const params = merge(inParams, { - [ApiEndpointParam.ApiVersion]: this.apiVersion, - }) + const params = { + ...inParams, + ...{ + [ApiEndpointParam.ApiVersion]: this.apiVersion, + }, + } return params } diff --git a/packages/snjs/lib/Services/ComponentManager/ComponentManager.ts b/packages/snjs/lib/Services/ComponentManager/ComponentManager.ts index bbe10a32434..cd6295c70cf 100644 --- a/packages/snjs/lib/Services/ComponentManager/ComponentManager.ts +++ b/packages/snjs/lib/Services/ComponentManager/ComponentManager.ts @@ -1,6 +1,6 @@ import { AllowedBatchStreaming } from './Types' import { SNFeaturesService } from '@Lib/Services/Features/FeaturesService' -import { ContentType, DisplayStringForContentType } from '@standardnotes/common' +import { ContentType } from '@standardnotes/common' import { ItemManager } from '@Lib/Services/Items/ItemManager' import { ActionObserver, @@ -22,8 +22,6 @@ import { getComponentOrNativeFeatureAcquiredPermissions, } from '@standardnotes/models' import { SNSyncService } from '@Lib/Services/Sync/SyncService' -import find from 'lodash/find' -import uniq from 'lodash/uniq' import { ComponentArea, ComponentAction, @@ -34,8 +32,7 @@ import { EditorFeatureDescription, GetNativeEditors, } from '@standardnotes/features' -import { Copy, filterFromArray, removeFromArray, sleep, assert } from '@standardnotes/utils' -import { UuidString } from '@Lib/Types/UuidString' +import { Copy, filterFromArray, removeFromArray, sleep, assert, uniqueArray } from '@standardnotes/utils' import { AllowedBatchContentTypes } from '@Lib/Services/ComponentManager/Types' import { ComponentViewer } from '@Lib/Services/ComponentManager/ComponentViewer' import { @@ -51,6 +48,7 @@ import { PreferenceServiceInterface, ComponentViewerItem, } from '@standardnotes/services' +import { permissionsStringForPermissions } from './permissionsStringForPermissions' const DESKTOP_URL_PREFIX = 'sn://' const LOCAL_HOST = 'localhost' @@ -297,7 +295,9 @@ export class SNComponentManager configureForDesktop(): void { this.desktopManager?.registerUpdateObserver((component: ComponentInterface) => { /* Reload theme if active */ - if (component.active && component.isTheme()) { + const activeComponents = this.preferences.getActiveComponents() + const isComponentActive = activeComponents.find((candidate) => candidate.uuid === component.uuid) + if (isComponentActive && component.isTheme()) { this.postActiveThemesToAllViewers() } }) @@ -309,12 +309,6 @@ export class SNComponentManager } } - getActiveThemes(): SNTheme[] { - return this.componentsForArea(ComponentArea.Themes).filter((theme) => { - return theme.active - }) as SNTheme[] - } - private urlForComponentOnDesktop(component: ComponentOrNativeFeature): string | undefined { assert(this.desktopManager) @@ -374,7 +368,7 @@ export class SNComponentManager } urlsForActiveThemes(): string[] { - const themes = this.getActiveThemes() + const themes = this.preferences.getActiveThemes() const urls = [] for (const theme of themes) { const url = this.urlForComponent(theme) @@ -512,7 +506,7 @@ export class SNComponentManager const params: PermissionDialog = { component: component, permissions: permissions, - permissionsString: this.permissionsStringForPermissions(permissions, component), + permissionsString: permissionsStringForPermissions(permissions, component), actionBlock: callback, callback: async (approved: boolean) => { const latestComponent = this.findComponent(component.uuid) @@ -531,7 +525,9 @@ export class SNComponentManager } else { /* Permission already exists, but content_types may have been expanded */ const contentTypes = matchingPermission.content_types || [] - matchingPermission.content_types = uniq(contentTypes.concat(permission.content_types as ContentType[])) + matchingPermission.content_types = uniqueArray( + contentTypes.concat(permission.content_types as ContentType[]), + ) } } @@ -578,9 +574,7 @@ export class SNComponentManager * Since these calls are asyncronous, multiple dialogs may be requested at the same time. * We only want to present one and trigger all callbacks based on one modal result */ - const existingDialog = find(this.permissionDialogs, { - component: component, - }) + const existingDialog = this.permissionDialogs.find((dialog) => dialog.component === component) this.permissionDialogs.push(params) if (!existingDialog) { this.presentPermissionsDialog(params) @@ -594,55 +588,37 @@ export class SNComponentManager throw 'Must override SNComponentManager.presentPermissionsDialog' } - async toggleTheme(uuid: UuidString): Promise { - this.log('Toggling theme', uuid) + async toggleTheme(theme: SNTheme): Promise { + this.log('Toggling theme', theme.uuid) - const theme = this.findComponent(uuid) as SNTheme - if (theme.active) { - await this.mutator.changeComponent(theme, (mutator) => { - mutator.active = false - }) + if (this.preferences.isThemeActive(theme)) { + await this.preferences.removeActiveTheme(theme) } else { - const activeThemes = this.getActiveThemes() - /* Activate current before deactivating others, so as not to flicker */ - await this.mutator.changeComponent(theme, (mutator) => { - mutator.active = true - }) + await this.preferences.addActiveTheme(theme) /* Deactive currently active theme(s) if new theme is not layerable */ if (!theme.isLayerable()) { await sleep(10) + + const activeThemes = this.preferences.getActiveThemes() for (const candidate of activeThemes) { if (candidate && !candidate.isLayerable()) { - await this.mutator.changeComponent(candidate, (mutator) => { - mutator.active = false - }) + await this.preferences.removeActiveTheme(candidate) } } } } - - void this.syncService.sync() } async toggleComponent(component: ComponentInterface): Promise { this.log('Toggling component', component.uuid) - const latestItem = this.itemManager.findItem(component.uuid) - if (!latestItem) { - return + if (this.preferences.isComponentActive(component)) { + await this.preferences.removeActiveComponent(component) + } else { + await this.preferences.addActiveComponent(component) } - - await this.mutator.changeComponent(latestItem, (mutator) => { - mutator.active = !(mutator.getItem() as ComponentInterface).active - }) - - void this.syncService.sync() - } - - isComponentActive(component: ComponentInterface): boolean { - return component.active } allComponentIframes(): HTMLIFrameElement[] { @@ -698,51 +674,6 @@ export class SNComponentManager return editors.filter((e) => e.legacyIsDefaultEditor())[0] } - permissionsStringForPermissions(permissions: ComponentPermission[], component: ComponentInterface): string { - if (permissions.length === 0) { - return '.' - } - - let contentTypeStrings: string[] = [] - let contextAreaStrings: string[] = [] - - permissions.forEach((permission) => { - switch (permission.name) { - case ComponentAction.StreamItems: - if (!permission.content_types) { - return - } - permission.content_types.forEach((contentType) => { - const desc = DisplayStringForContentType(contentType) - if (desc) { - contentTypeStrings.push(`${desc}s`) - } else { - contentTypeStrings.push(`items of type ${contentType}`) - } - }) - break - case ComponentAction.StreamContextItem: - { - const componentAreaMapping = { - [ComponentArea.EditorStack]: 'working note', - [ComponentArea.Editor]: 'working note', - [ComponentArea.Themes]: 'Unknown', - } - contextAreaStrings.push(componentAreaMapping[component.area]) - } - break - } - }) - - contentTypeStrings = uniq(contentTypeStrings) - contextAreaStrings = uniq(contextAreaStrings) - - if (contentTypeStrings.length === 0 && contextAreaStrings.length === 0) { - return '.' - } - return contentTypeStrings.concat(contextAreaStrings).join(', ') + '.' - } - doesEditorChangeRequireAlert( from: ComponentOrNativeFeature | undefined, to: ComponentOrNativeFeature | undefined, diff --git a/packages/snjs/lib/Services/ComponentManager/ComponentViewer.ts b/packages/snjs/lib/Services/ComponentManager/ComponentViewer.ts index 5a92461083b..406e483c74c 100644 --- a/packages/snjs/lib/Services/ComponentManager/ComponentViewer.ts +++ b/packages/snjs/lib/Services/ComponentManager/ComponentViewer.ts @@ -43,9 +43,6 @@ import { isNonNativeComponent, isNativeComponent, } from '@standardnotes/models' -import find from 'lodash/find' -import uniq from 'lodash/uniq' -import remove from 'lodash/remove' import { SNSyncService } from '@Lib/Services/Sync/SyncService' import { environmentToString, platformToString } from '@Lib/Application/Platforms' import { @@ -72,6 +69,7 @@ import { Uuids, sureSearchArray, isNotUndefined, + uniqueArray, } from '@standardnotes/utils' export class ComponentViewer implements ComponentViewerInterface { @@ -289,10 +287,12 @@ export class ComponentViewer implements ComponentViewerInterface { } if (this.streamContextItemOriginalMessage) { - if (isComponentViewerItemReadonlyItem(this.options.item)) { + const optionsItem = this.options.item + if (isComponentViewerItemReadonlyItem(optionsItem)) { return } - const matchingItem = find(nondeletedItems, { uuid: this.options.item.uuid }) + + const matchingItem = nondeletedItems.find((item) => item.uuid === optionsItem.uuid) if (matchingItem) { this.sendContextItemThroughBridge(matchingItem, source) } @@ -682,7 +682,7 @@ export class ComponentViewer implements ComponentViewerInterface { /* Check to see if additional privileges are required */ if (pendingResponseItems.length > 0) { - const requiredContentTypes = uniq( + const requiredContentTypes = uniqueArray( pendingResponseItems.map((item) => { return item.content_type }), @@ -713,7 +713,9 @@ export class ComponentViewer implements ComponentViewerInterface { } if (item.locked) { - remove(responsePayloads, { uuid: item.uuid }) + responsePayloads = responsePayloads.filter((responseItem) => { + return responseItem.uuid !== item.uuid + }) lockedCount++ if (item.content_type === ContentType.Note) { lockedNoteCount++ @@ -802,7 +804,7 @@ export class ComponentViewer implements ComponentViewerInterface { handleCreateItemsMessage(message: ComponentMessage): void { let responseItems = (message.data.item ? [message.data.item] : message.data.items) as IncomingComponentItemPayload[] - const uniqueContentTypes = uniq( + const uniqueContentTypes = uniqueArray( responseItems.map((item) => { return item.content_type }), @@ -873,7 +875,7 @@ export class ComponentViewer implements ComponentViewerInterface { const data = message.data as DeleteItemsMessageData const items = data.items.filter((item) => AllowedBatchContentTypes.includes(item.content_type)) - const requiredContentTypes = uniq(items.map((item) => item.content_type)).sort() as ContentType[] + const requiredContentTypes = uniqueArray(items.map((item) => item.content_type)).sort() as ContentType[] const requiredPermissions: ComponentPermission[] = [ { diff --git a/packages/snjs/lib/Services/ComponentManager/permissionsStringForPermissions.ts b/packages/snjs/lib/Services/ComponentManager/permissionsStringForPermissions.ts new file mode 100644 index 00000000000..c0d2a25297c --- /dev/null +++ b/packages/snjs/lib/Services/ComponentManager/permissionsStringForPermissions.ts @@ -0,0 +1,52 @@ +import { DisplayStringForContentType } from '@standardnotes/common' +import { ComponentAction, ComponentArea, ComponentPermission } from '@standardnotes/features' +import { ComponentInterface } from '@standardnotes/models' +import { uniqueArray } from '@standardnotes/utils' + +export function permissionsStringForPermissions( + permissions: ComponentPermission[], + component: ComponentInterface, +): string { + if (permissions.length === 0) { + return '.' + } + + let contentTypeStrings: string[] = [] + let contextAreaStrings: string[] = [] + + permissions.forEach((permission) => { + switch (permission.name) { + case ComponentAction.StreamItems: + if (!permission.content_types) { + return + } + permission.content_types.forEach((contentType) => { + const desc = DisplayStringForContentType(contentType) + if (desc) { + contentTypeStrings.push(`${desc}s`) + } else { + contentTypeStrings.push(`items of type ${contentType}`) + } + }) + break + case ComponentAction.StreamContextItem: + { + const componentAreaMapping = { + [ComponentArea.EditorStack]: 'working note', + [ComponentArea.Editor]: 'working note', + [ComponentArea.Themes]: 'Unknown', + } + contextAreaStrings.push(componentAreaMapping[component.area]) + } + break + } + }) + + contentTypeStrings = uniqueArray(contentTypeStrings) + contextAreaStrings = uniqueArray(contextAreaStrings) + + if (contentTypeStrings.length === 0 && contextAreaStrings.length === 0) { + return '.' + } + return contentTypeStrings.concat(contextAreaStrings).join(', ') + '.' +} diff --git a/packages/snjs/lib/Services/Features/UseCase/DownloadRemoteThirdPartyFeature.ts b/packages/snjs/lib/Services/Features/UseCase/DownloadRemoteThirdPartyFeature.ts index f8428ea5115..6f5903ab80a 100644 --- a/packages/snjs/lib/Services/Features/UseCase/DownloadRemoteThirdPartyFeature.ts +++ b/packages/snjs/lib/Services/Features/UseCase/DownloadRemoteThirdPartyFeature.ts @@ -8,7 +8,7 @@ import { FillItemContentSpecialized, } from '@standardnotes/models' import { AlertService, API_MESSAGE_FAILED_DOWNLOADING_EXTENSION, ItemManagerInterface } from '@standardnotes/services' -import { isString } from 'lodash' +import { isString } from '@standardnotes/utils' export class DownloadRemoteThirdPartyFeatureUseCase { constructor(private api: SNApiService, private items: ItemManagerInterface, private alerts: AlertService) {} diff --git a/packages/snjs/lib/Services/Preferences/PreferencesService.ts b/packages/snjs/lib/Services/Preferences/PreferencesService.ts index c8d74616473..7d4c3b336b5 100644 --- a/packages/snjs/lib/Services/Preferences/PreferencesService.ts +++ b/packages/snjs/lib/Services/Preferences/PreferencesService.ts @@ -9,6 +9,8 @@ import { AllComponentPreferences, ComponentOrNativeFeature, isNativeComponent, + SNTheme, + ComponentInterface, } from '@standardnotes/models' import { ContentType } from '@standardnotes/common' import { ItemManager } from '../Items/ItemManager' @@ -36,19 +38,19 @@ export class SNPreferencesService private removeSyncObserver?: () => void constructor( - private singletonManager: SNSingletonManager, - itemManager: ItemManager, + private singletons: SNSingletonManager, + private items: ItemManager, private mutator: MutatorClientInterface, - private syncService: SNSyncService, + private sync: SNSyncService, protected override internalEventBus: InternalEventBusInterface, ) { super(internalEventBus) - this.removeItemObserver = itemManager.addObserver(ContentType.UserPrefs, () => { + this.removeItemObserver = items.addObserver(ContentType.UserPrefs, () => { this.shouldReload = true }) - this.removeSyncObserver = syncService.addEventObserver((event) => { + this.removeSyncObserver = sync.addEventObserver((event) => { if (event === SyncEvent.SyncCompletedWithAllItemsUploaded || event === SyncEvent.LocalDataIncrementalLoad) { void this.reload() } @@ -58,7 +60,7 @@ export class SNPreferencesService override deinit(): void { this.removeItemObserver?.() this.removeSyncObserver?.() - ;(this.singletonManager as unknown) = undefined + ;(this.singletons as unknown) = undefined ;(this.mutator as unknown) = undefined super.deinit() @@ -69,7 +71,7 @@ export class SNPreferencesService if (stage === ApplicationStage.LoadedDatabase_12) { /** Try to read preferences singleton from storage */ - this.preferences = this.singletonManager.findSingleton( + this.preferences = this.singletons.findSingleton( ContentType.UserPrefs, SNUserPrefs.singletonPredicate, ) @@ -113,6 +115,70 @@ export class SNPreferencesService return preferences[preferencesLookupKey] } + async addActiveTheme(theme: SNTheme): Promise { + const activeThemes = this.getValue(PrefKey.ActiveThemes, undefined) ?? [] + + activeThemes.push(theme.uuid) + + await this.setValue(PrefKey.ActiveThemes, activeThemes) + } + + async replaceActiveTheme(theme: SNTheme): Promise { + await this.setValue(PrefKey.ActiveThemes, [theme.uuid]) + } + + async removeActiveTheme(theme: SNTheme): Promise { + const activeThemes = this.getValue(PrefKey.ActiveThemes, undefined) ?? [] + + const filteredThemes = activeThemes.filter((activeTheme) => activeTheme !== theme.uuid) + + await this.setValue(PrefKey.ActiveThemes, filteredThemes) + } + + getActiveThemes(): SNTheme[] { + const activeThemes = this.getValue(PrefKey.ActiveThemes, undefined) ?? [] + + return this.items.findItems(activeThemes) + } + + getActiveThemesUuids(): string[] { + return this.getValue(PrefKey.ActiveThemes, undefined) ?? [] + } + + isThemeActive(theme: SNTheme): boolean { + const activeThemes = this.getValue(PrefKey.ActiveThemes, undefined) ?? [] + + return activeThemes.includes(theme.uuid) + } + + async addActiveComponent(component: ComponentInterface): Promise { + const activeComponents = this.getValue(PrefKey.ActiveComponents, undefined) ?? [] + + activeComponents.push(component.uuid) + + await this.setValue(PrefKey.ActiveComponents, activeComponents) + } + + async removeActiveComponent(component: ComponentInterface): Promise { + const activeComponents = this.getValue(PrefKey.ActiveComponents, undefined) ?? [] + + const filteredComponents = activeComponents.filter((activeComponent) => activeComponent !== component.uuid) + + await this.setValue(PrefKey.ActiveComponents, filteredComponents) + } + + getActiveComponents(): ComponentInterface[] { + const activeComponents = this.getValue(PrefKey.ActiveComponents, undefined) ?? [] + + return this.items.findItems(activeComponents) + } + + isComponentActive(component: ComponentInterface): boolean { + const activeComponents = this.getValue(PrefKey.ActiveComponents, undefined) ?? [] + + return activeComponents.includes(component.uuid) + } + async setValue(key: K, value: PrefValue[K]): Promise { if (!this.preferences) { return @@ -124,7 +190,7 @@ export class SNPreferencesService void this.notifyEvent(PreferencesServiceEvent.PreferencesChanged) - void this.syncService.sync({ sourceDescription: 'PreferencesService.setValue' }) + void this.sync.sync({ sourceDescription: 'PreferencesService.setValue' }) } private async reload() { @@ -137,7 +203,7 @@ export class SNPreferencesService try { const previousRef = this.preferences - this.preferences = await this.singletonManager.findOrCreateContentTypeSingleton( + this.preferences = await this.singletons.findOrCreateContentTypeSingleton( ContentType.UserPrefs, FillItemContent({}), ) diff --git a/packages/snjs/package.json b/packages/snjs/package.json index e66855da08e..e3ff452cb90 100644 --- a/packages/snjs/package.json +++ b/packages/snjs/package.json @@ -54,7 +54,6 @@ "@types/jest": "^29.2.3", "@types/jsdom": "^20.0.1", "@types/libsodium-wrappers": "^0.7.10", - "@types/lodash": "^4.14.189", "@types/semver": "^7.3.13", "@typescript-eslint/eslint-plugin": "*", "babel-jest": "^29.3.1", @@ -70,7 +69,6 @@ "jest-environment-jsdom": "^29.3.1", "jsdom": "^20.0.2", "libsodium-wrappers": "^0.7.10", - "lodash": "^4.17.21", "nock": "^13.2.9", "otplib": "^12.0.1", "reflect-metadata": "^0.1.13", diff --git a/packages/ui-services/src/Theme/ThemeManager.ts b/packages/ui-services/src/Theme/ThemeManager.ts index 389f7d46caa..856614ddd6f 100644 --- a/packages/ui-services/src/Theme/ThemeManager.ts +++ b/packages/ui-services/src/Theme/ThemeManager.ts @@ -1,14 +1,19 @@ import { dismissToast, ToastType, addTimedToast } from '@standardnotes/toast' -import { ContentType } from '@standardnotes/common' import { CreateDecryptedLocalStorageContextPayload, LocalStorageDecryptedContextualPayload, - PayloadEmitSource, PrefKey, SNTheme, } from '@standardnotes/models' import { removeFromArray } from '@standardnotes/utils' -import { InternalEventBusInterface, ApplicationEvent, StorageValueModes, FeatureStatus } from '@standardnotes/services' +import { + InternalEventBusInterface, + ApplicationEvent, + StorageValueModes, + FeatureStatus, + PreferenceServiceInterface, + PreferencesServiceEvent, +} from '@standardnotes/services' import { FeatureIdentifier } from '@standardnotes/features' import { WebApplicationInterface } from '../WebApplication/WebApplicationInterface' import { AbstractUIServicee } from '../Abstract/AbstractUIService' @@ -18,25 +23,71 @@ const TimeBeforeApplyingColorScheme = 5 const DefaultThemeIdentifier = 'Default' export class ThemeManager extends AbstractUIServicee { - private activeThemes: string[] = [] - private unregisterDesktop?: () => void - private unregisterStream!: () => void + private themesActiveInTheUI: string[] = [] private lastUseDeviceThemeSettings = false - constructor(application: WebApplicationInterface, internalEventBus: InternalEventBusInterface) { + constructor( + application: WebApplicationInterface, + private preferences: PreferenceServiceInterface, + internalEventBus: InternalEventBusInterface, + ) { super(application, internalEventBus) this.colorSchemeEventHandler = this.colorSchemeEventHandler.bind(this) } override async onAppStart() { - this.registerObservers() + const desktopService = this.application.getDesktopService() + if (desktopService) { + this.eventDisposers.push( + desktopService.registerUpdateObserver((component) => { + if (component.isTheme()) { + if (this.preferences.isThemeActive(component as SNTheme)) { + this.deactivateThemeInTheUI(component.uuid) + setTimeout(() => { + this.activateTheme(component as SNTheme) + this.cacheThemeState().catch(console.error) + }, 10) + } + } + }), + ) + } + + this.eventDisposers.push( + this.preferences.addEventObserver(async (event) => { + if (event === PreferencesServiceEvent.PreferencesChanged) { + let hasChange = false + const activeThemes = this.preferences.getActiveThemesUuids() + for (const uiActiveTheme of this.themesActiveInTheUI) { + if (!activeThemes.includes(uiActiveTheme)) { + this.deactivateThemeInTheUI(uiActiveTheme) + hasChange = true + } + } + + for (const activeTheme of activeThemes) { + if (!this.themesActiveInTheUI.includes(activeTheme)) { + const theme = this.application.items.findItem(activeTheme) + if (theme) { + this.activateTheme(theme) + hasChange = true + } + } + } + + if (hasChange) { + this.cacheThemeState().catch(console.error) + } + } + }), + ) } override async onAppEvent(event: ApplicationEvent) { switch (event) { case ApplicationEvent.SignedOut: { this.deactivateAllThemes() - this.activeThemes = [] + this.themesActiveInTheUI = [] this.application?.removeValue(CachedThemesKey, StorageValueModes.Nonwrapped).catch(console.error) break } @@ -96,12 +147,7 @@ export class ThemeManager extends AbstractUIServicee { } override deinit() { - this.activeThemes.length = 0 - - this.unregisterDesktop?.() - this.unregisterStream() - ;(this.unregisterDesktop as unknown) = undefined - ;(this.unregisterStream as unknown) = undefined + this.themesActiveInTheUI = [] const mq = window.matchMedia('(prefers-color-scheme: dark)') if (mq.removeEventListener != undefined) { @@ -116,39 +162,34 @@ export class ThemeManager extends AbstractUIServicee { private handleFeaturesUpdated(): void { let hasChange = false - for (const themeUuid of this.activeThemes) { - const theme = this.application.items.findItem(themeUuid) as SNTheme + for (const themeUuid of this.themesActiveInTheUI) { + const theme = this.application.items.findItem(themeUuid) if (!theme) { - this.deactivateTheme(themeUuid) + this.deactivateThemeInTheUI(themeUuid) hasChange = true + continue } const status = this.application.features.getFeatureStatus(theme.identifier) if (status !== FeatureStatus.Entitled) { - if (theme.active) { - this.application.componentManager.toggleTheme(theme.uuid).catch(console.error) - } else { - this.deactivateTheme(theme.uuid) - } + this.deactivateThemeInTheUI(theme.uuid) hasChange = true } } - const activeThemes = (this.application.items.getItems(ContentType.Theme) as SNTheme[]).filter( - (theme) => theme.active, - ) + const activeThemes = this.preferences.getActiveThemes() for (const theme of activeThemes) { - if (!this.activeThemes.includes(theme.uuid)) { + if (!this.themesActiveInTheUI.includes(theme.uuid)) { this.activateTheme(theme) hasChange = true } } if (hasChange) { - this.cacheThemeState().catch(console.error) + void this.cacheThemeState() } } @@ -197,14 +238,14 @@ export class ThemeManager extends AbstractUIServicee { .getDisplayableComponents() .filter((component) => component.isTheme()) as SNTheme[] - const activeTheme = themes.find((theme) => theme.active && !theme.isLayerable()) + const activeTheme = themes.find((theme) => this.preferences.isThemeActive(theme) && !theme.isLayerable()) const activeThemeIdentifier = activeTheme ? activeTheme.identifier : DefaultThemeIdentifier const themeIdentifier = this.application.getPreference(preference, preferenceDefault) as string const toggleActiveTheme = () => { if (activeTheme) { - void this.application.componentManager.toggleTheme(activeTheme.uuid) + void this.application.componentManager.toggleTheme(activeTheme) } } @@ -213,8 +254,8 @@ export class ThemeManager extends AbstractUIServicee { toggleActiveTheme() } else { const theme = themes.find((theme) => theme.package_info.identifier === themeIdentifier) - if (theme && !theme.active) { - this.application.componentManager.toggleTheme(theme.uuid).catch(console.error) + if (theme && !this.preferences.isThemeActive(theme)) { + this.application.componentManager.toggleTheme(theme).catch(console.error) } } } @@ -233,44 +274,16 @@ export class ThemeManager extends AbstractUIServicee { } } - private registerObservers() { - this.unregisterDesktop = this.application.getDesktopService()?.registerUpdateObserver((component) => { - if (component.active && component.isTheme()) { - this.deactivateTheme(component.uuid) - setTimeout(() => { - this.activateTheme(component as SNTheme) - this.cacheThemeState().catch(console.error) - }, 10) - } - }) - - this.unregisterStream = this.application.streamItems(ContentType.Theme, ({ changed, inserted, source }) => { - const items = changed.concat(inserted) - const themes = items as SNTheme[] - for (const theme of themes) { - if (theme.active) { - this.activateTheme(theme) - } else { - this.deactivateTheme(theme.uuid) - } - } - - if (source !== PayloadEmitSource.LocalRetrieved) { - this.cacheThemeState().catch(console.error) - } - }) - } - private deactivateAllThemes() { - const activeThemes = this.activeThemes.slice() + const activeThemes = this.themesActiveInTheUI.slice() for (const uuid of activeThemes) { - this.deactivateTheme(uuid) + this.deactivateThemeInTheUI(uuid) } } private activateTheme(theme: SNTheme, skipEntitlementCheck = false) { - if (this.activeThemes.find((uuid) => uuid === theme.uuid)) { + if (this.themesActiveInTheUI.find((uuid) => uuid === theme.uuid)) { return } @@ -286,7 +299,7 @@ export class ThemeManager extends AbstractUIServicee { return } - this.activeThemes.push(theme.uuid) + this.themesActiveInTheUI.push(theme.uuid) const link = document.createElement('link') link.href = url @@ -308,6 +321,24 @@ export class ThemeManager extends AbstractUIServicee { document.getElementsByTagName('head')[0].appendChild(link) } + private deactivateThemeInTheUI(uuid: string) { + if (!this.themesActiveInTheUI.includes(uuid)) { + return + } + + const element = document.getElementById(uuid) as HTMLLinkElement + if (element) { + element.disabled = true + element.parentNode?.removeChild(element) + } + + removeFromArray(this.themesActiveInTheUI, uuid) + + if (this.themesActiveInTheUI.length === 0 && this.application.isNativeMobileWeb()) { + this.application.mobileDevice().handleThemeSchemeChange(false, '#ffffff') + } + } + private getBackgroundColor() { const bgColor = getComputedStyle(document.documentElement).getPropertyValue('--sn-stylekit-background-color').trim() return bgColor.length ? bgColor : '#ffffff' @@ -326,26 +357,8 @@ export class ThemeManager extends AbstractUIServicee { themeColorMetaElement.setAttribute('content', this.getBackgroundColor()) } - private deactivateTheme(uuid: string) { - if (!this.activeThemes.includes(uuid)) { - return - } - - const element = document.getElementById(uuid) as HTMLLinkElement - if (element) { - element.disabled = true - element.parentNode?.removeChild(element) - } - - removeFromArray(this.activeThemes, uuid) - - if (this.activeThemes.length === 0 && this.application.isNativeMobileWeb()) { - this.application.mobileDevice().handleThemeSchemeChange(false, '#ffffff') - } - } - private async cacheThemeState() { - const themes = this.application.items.findItems(this.activeThemes) as SNTheme[] + const themes = this.application.items.findItems(this.themesActiveInTheUI) as SNTheme[] const mapped = themes.map((theme) => { const payload = theme.payloadRepresentation() diff --git a/packages/web/src/javascripts/Application/WebApplication.ts b/packages/web/src/javascripts/Application/WebApplication.ts index c070f9cd116..597e4c887e8 100644 --- a/packages/web/src/javascripts/Application/WebApplication.ts +++ b/packages/web/src/javascripts/Application/WebApplication.ts @@ -24,6 +24,7 @@ import { BackupServiceInterface, InternalFeatureService, InternalFeatureServiceInterface, + PrefDefaults, } from '@standardnotes/snjs' import { makeObservable, observable } from 'mobx' import { startAuthentication, startRegistration } from '@simplewebauthn/browser' @@ -46,7 +47,6 @@ import { } from '@standardnotes/ui-services' import { MobileWebReceiver, NativeMobileEventListener } from '../NativeMobileWeb/MobileWebReceiver' import { AndroidBackHandler } from '@/NativeMobileWeb/AndroidBackHandler' -import { PrefDefaults } from '@/Constants/PrefDefaults' import { setCustomViewportHeight } from '@/setViewportHeightWithFallback' import { WebServices } from './WebServices' import { FeatureName } from '@/Controllers/FeatureName' @@ -118,7 +118,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter this.webServices = {} as WebServices this.webServices.keyboardService = new KeyboardService(platform, this.environment) this.webServices.archiveService = new ArchiveManager(this) - this.webServices.themeService = new ThemeManager(this, this.internalEventBus) + this.webServices.themeService = new ThemeManager(this, this.preferences, this.internalEventBus) this.webServices.autolockService = this.isNativeMobileWeb() ? undefined : new AutolockService(this, this.internalEventBus) diff --git a/packages/web/src/javascripts/Components/ComponentView/ComponentView.tsx b/packages/web/src/javascripts/Components/ComponentView/ComponentView.tsx index 58e26255435..9f9ce4f9087 100644 --- a/packages/web/src/javascripts/Components/ComponentView/ComponentView.tsx +++ b/packages/web/src/javascripts/Components/ComponentView/ComponentView.tsx @@ -171,7 +171,7 @@ const ComponentView: FunctionComponent = ({ onLoad, componentViewer, requ const unregisterDesktopObserver = application .getDesktopService() ?.registerUpdateObserver((updatedComponent: ComponentInterface) => { - if (isNonNativeComponent(component) && updatedComponent.uuid === component.uuid && updatedComponent.active) { + if (isNonNativeComponent(component) && updatedComponent.uuid === component.uuid) { requestReload?.(componentViewer) } }) diff --git a/packages/web/src/javascripts/Components/ContentListView/Header/DisplayOptionsMenu.tsx b/packages/web/src/javascripts/Components/ContentListView/Header/DisplayOptionsMenu.tsx index 83c2ccb25c1..a0a41ebb392 100644 --- a/packages/web/src/javascripts/Components/ContentListView/Header/DisplayOptionsMenu.tsx +++ b/packages/web/src/javascripts/Components/ContentListView/Header/DisplayOptionsMenu.tsx @@ -9,6 +9,7 @@ import { TagMutator, TagPreferences, VectorIconNameOrEmoji, + PrefDefaults, } from '@standardnotes/snjs' import { observer } from 'mobx-react-lite' import { FunctionComponent, useCallback, useEffect, useState } from 'react' @@ -16,7 +17,6 @@ import Icon from '@/Components/Icon/Icon' import Menu from '@/Components/Menu/Menu' import MenuItemSeparator from '@/Components/Menu/MenuItemSeparator' import { DisplayOptionsMenuProps } from './DisplayOptionsMenuProps' -import { PrefDefaults } from '@/Constants/PrefDefaults' import NewNotePreferences from './NewNotePreferences' import { PreferenceMode } from './PreferenceMode' import { classNames } from '@standardnotes/utils' diff --git a/packages/web/src/javascripts/Components/ContentListView/Header/NewNotePreferences.tsx b/packages/web/src/javascripts/Components/ContentListView/Header/NewNotePreferences.tsx index ce7e698a8e2..ab827224c6f 100644 --- a/packages/web/src/javascripts/Components/ContentListView/Header/NewNotePreferences.tsx +++ b/packages/web/src/javascripts/Components/ContentListView/Header/NewNotePreferences.tsx @@ -7,10 +7,10 @@ import { isSmartView, isSystemView, SystemViewId, + PrefDefaults, } from '@standardnotes/snjs' import { observer } from 'mobx-react-lite' import { ChangeEventHandler, FunctionComponent, useCallback, useEffect, useRef, useState } from 'react' -import { PrefDefaults } from '@/Constants/PrefDefaults' import Dropdown from '@/Components/Dropdown/Dropdown' import { DropdownItem } from '@/Components/Dropdown/DropdownItem' import { WebApplication } from '@/Application/WebApplication' diff --git a/packages/web/src/javascripts/Components/EditorWidthSelectionModal/EditorWidthSelectionModal.tsx b/packages/web/src/javascripts/Components/EditorWidthSelectionModal/EditorWidthSelectionModal.tsx index 1423f9e87fe..f8c9d741b6c 100644 --- a/packages/web/src/javascripts/Components/EditorWidthSelectionModal/EditorWidthSelectionModal.tsx +++ b/packages/web/src/javascripts/Components/EditorWidthSelectionModal/EditorWidthSelectionModal.tsx @@ -1,5 +1,5 @@ import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery' -import { classNames, EditorLineWidth, PrefKey, SNNote } from '@standardnotes/snjs' +import { classNames, EditorLineWidth, PrefKey, SNNote, PrefDefaults } from '@standardnotes/snjs' import { useCallback, useEffect, useMemo, useState } from 'react' import Button from '../Button/Button' import Modal, { ModalAction } from '../Modal/Modal' @@ -9,7 +9,6 @@ import { EditorMargins, EditorMaxWidths } from './EditorWidths' import { useApplication } from '../ApplicationProvider' import ModalOverlay from '../Modal/ModalOverlay' import { CHANGE_EDITOR_WIDTH_COMMAND } from '@standardnotes/ui-services' -import { PrefDefaults } from '@/Constants/PrefDefaults' import { observer } from 'mobx-react-lite' import Switch from '../Switch/Switch' diff --git a/packages/web/src/javascripts/Components/Footer/QuickSettingsButton.tsx b/packages/web/src/javascripts/Components/Footer/QuickSettingsButton.tsx index ba27ebba90d..a86e42055a4 100644 --- a/packages/web/src/javascripts/Components/Footer/QuickSettingsButton.tsx +++ b/packages/web/src/javascripts/Components/Footer/QuickSettingsButton.tsx @@ -29,7 +29,7 @@ const QuickSettingsButton = ({ application, isOpen, toggleMenu, quickSettingsMen .getDisplayableComponents() .find((theme) => theme.package_info.identifier === FeatureIdentifier.DarkTheme) as SNTheme | undefined if (darkTheme) { - void application.componentManager.toggleTheme(darkTheme.uuid) + void application.componentManager.toggleTheme(darkTheme) } }, }) diff --git a/packages/web/src/javascripts/Components/NoteView/NoteStatusIndicator.tsx b/packages/web/src/javascripts/Components/NoteView/NoteStatusIndicator.tsx index d4e41e933d4..236e9edfa7c 100644 --- a/packages/web/src/javascripts/Components/NoteView/NoteStatusIndicator.tsx +++ b/packages/web/src/javascripts/Components/NoteView/NoteStatusIndicator.tsx @@ -1,8 +1,7 @@ import { ElementIds } from '@/Constants/ElementIDs' -import { PrefDefaults } from '@/Constants/PrefDefaults' import { classNames } from '@standardnotes/utils' import { ReactNode, useCallback, useState } from 'react' -import { IconType, PrefKey } from '@standardnotes/snjs' +import { IconType, PrefKey, PrefDefaults } from '@standardnotes/snjs' import Icon from '../Icon/Icon' import { useApplication } from '../ApplicationProvider' diff --git a/packages/web/src/javascripts/Components/NoteView/NoteView.tsx b/packages/web/src/javascripts/Components/NoteView/NoteView.tsx index 12c921530cd..a843498d8ba 100644 --- a/packages/web/src/javascripts/Components/NoteView/NoteView.tsx +++ b/packages/web/src/javascripts/Components/NoteView/NoteView.tsx @@ -5,7 +5,6 @@ import NotesOptionsPanel from '@/Components/NotesOptions/NotesOptionsPanel' import PinNoteButton from '@/Components/PinNoteButton/PinNoteButton' import ProtectedItemOverlay from '@/Components/ProtectedItemOverlay/ProtectedItemOverlay' import { ElementIds } from '@/Constants/ElementIDs' -import { PrefDefaults } from '@/Constants/PrefDefaults' import { StringDeleteNote, STRING_DELETE_LOCKED_ATTEMPT, STRING_DELETE_PLACEHOLDER_ATTEMPT } from '@/Constants/Strings' import { log, LoggingDomain } from '@/Logging' import { debounce, isDesktopApplication, isMobileScreen } from '@/Utils' @@ -23,6 +22,7 @@ import { isPayloadSourceRetrieved, NoteType, PayloadEmitSource, + PrefDefaults, PrefKey, ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction, SNNote, @@ -741,7 +741,7 @@ class NoteView extends AbstractComponent { const stackComponents = sortAlphabetically( this.application.componentManager .componentsForArea(ComponentArea.EditorStack) - .filter((component) => component.active), + .filter((component) => this.application.preferences.isComponentActive(component)), ) const enabledComponents = stackComponents.filter((component) => { return component.isExplicitlyEnabledForItem(this.note.uuid) @@ -1014,6 +1014,7 @@ class NoteView extends AbstractComponent { >
{this.state.availableStackComponents.map((component) => { + const active = this.application.preferences.isComponentActive(component) return (
{ className="flex flex-grow cursor-pointer items-center justify-center [&:not(:first-child)]:ml-3" >
- {this.stackComponentExpanded(component) && component.active && } + {this.stackComponentExpanded(component) && active && } {!this.stackComponentExpanded(component) && }
diff --git a/packages/web/src/javascripts/Components/NoteView/PlainEditor/PlainEditor.tsx b/packages/web/src/javascripts/Components/NoteView/PlainEditor/PlainEditor.tsx index bee0dcfffd1..aa02de7f2a8 100644 --- a/packages/web/src/javascripts/Components/NoteView/PlainEditor/PlainEditor.tsx +++ b/packages/web/src/javascripts/Components/NoteView/PlainEditor/PlainEditor.tsx @@ -1,7 +1,6 @@ import { WebApplication } from '@/Application/WebApplication' import { usePrevious } from '@/Components/ContentListView/Calendar/usePrevious' import { ElementIds } from '@/Constants/ElementIDs' -import { PrefDefaults } from '@/Constants/PrefDefaults' import { log, LoggingDomain } from '@/Logging' import { Disposer } from '@/Types/Disposer' import { EditorEventSource } from '@/Types/EditorEventSource' @@ -14,6 +13,7 @@ import { isPayloadSourceRetrieved, PrefKey, WebAppEvent, + PrefDefaults, } from '@standardnotes/snjs' import { TAB_COMMAND } from '@standardnotes/ui-services' import { ChangeEventHandler, forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react' diff --git a/packages/web/src/javascripts/Components/Panes/usePaneGesture.ts b/packages/web/src/javascripts/Components/Panes/usePaneGesture.ts index 4d5d75e46fa..e1b3e18ef6e 100644 --- a/packages/web/src/javascripts/Components/Panes/usePaneGesture.ts +++ b/packages/web/src/javascripts/Components/Panes/usePaneGesture.ts @@ -2,8 +2,7 @@ import { useStateRef } from '@/Hooks/useStateRef' import { useEffect, useRef, useState } from 'react' import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery' import { useApplication } from '../ApplicationProvider' -import { ApplicationEvent, PrefKey } from '@standardnotes/snjs' -import { PrefDefaults } from '@/Constants/PrefDefaults' +import { ApplicationEvent, PrefKey, PrefDefaults } from '@standardnotes/snjs' function getScrollParent(node: HTMLElement | null): HTMLElement | null { if (!node) { diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Appearance.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Appearance.tsx index 962cc2624c1..2f6f81b8d09 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Appearance.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Appearance.tsx @@ -13,6 +13,7 @@ import { FindNativeFeature, FeatureStatus, naturalSort, + PrefDefaults, } from '@standardnotes/snjs' import { observer } from 'mobx-react-lite' import { FunctionComponent, useEffect, useState } from 'react' @@ -21,7 +22,6 @@ import PreferencesPane from '../PreferencesComponents/PreferencesPane' import PreferencesGroup from '../PreferencesComponents/PreferencesGroup' import PreferencesSegment from '../PreferencesComponents/PreferencesSegment' import { PremiumFeatureIconName } from '@/Components/Icon/PremiumFeatureIcon' -import { PrefDefaults } from '@/Constants/PrefDefaults' import EditorAppearance from './Appearance/EditorAppearance' type Props = { diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Appearance/EditorAppearance.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Appearance/EditorAppearance.tsx index b555a0109e4..cf4c56d00e8 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Appearance/EditorAppearance.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Appearance/EditorAppearance.tsx @@ -3,8 +3,14 @@ import Dropdown from '@/Components/Dropdown/Dropdown' import Icon from '@/Components/Icon/Icon' import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator' import Switch from '@/Components/Switch/Switch' -import { PrefDefaults } from '@/Constants/PrefDefaults' -import { ApplicationEvent, EditorFontSize, EditorLineHeight, EditorLineWidth, PrefKey } from '@standardnotes/snjs' +import { + ApplicationEvent, + EditorFontSize, + EditorLineHeight, + EditorLineWidth, + PrefKey, + PrefDefaults, +} from '@standardnotes/snjs' import { useCallback, useEffect, useMemo, useState } from 'react' import { Subtitle, Title, Text } from '../../PreferencesComponents/Content' import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup' diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/General/Advanced/Packages/Types/AnyPackageType.ts b/packages/web/src/javascripts/Components/Preferences/Panes/General/Advanced/Packages/Types/AnyPackageType.ts index 31a23e1ae7a..16251f25546 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/General/Advanced/Packages/Types/AnyPackageType.ts +++ b/packages/web/src/javascripts/Components/Preferences/Panes/General/Advanced/Packages/Types/AnyPackageType.ts @@ -1,3 +1,3 @@ -import { SNActionsExtension, SNComponent, SNTheme } from '@standardnotes/snjs' +import { ComponentInterface, SNActionsExtension, SNTheme } from '@standardnotes/snjs' -export type AnyPackageType = SNComponent | SNTheme | SNActionsExtension +export type AnyPackageType = ComponentInterface | SNTheme | SNActionsExtension diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/General/Defaults.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/General/Defaults.tsx index 9c0c2d161be..d40436441f6 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/General/Defaults.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/General/Defaults.tsx @@ -1,4 +1,4 @@ -import { PrefKey, Platform } from '@standardnotes/snjs' +import { PrefKey, Platform, PrefDefaults } from '@standardnotes/snjs' import { Subtitle, Text, Title } from '@/Components/Preferences/PreferencesComponents/Content' import { WebApplication } from '@/Application/WebApplication' import { FunctionComponent, useState } from 'react' @@ -6,7 +6,6 @@ import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator' import Switch from '@/Components/Switch/Switch' import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup' import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment' -import { PrefDefaults } from '@/Constants/PrefDefaults' type Props = { application: WebApplication diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/General/Labs/Labs.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/General/Labs/Labs.tsx index 35b6dc50a8c..409da2b4b6d 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/General/Labs/Labs.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/General/Labs/Labs.tsx @@ -1,6 +1,13 @@ import { Text, Title } from '@/Components/Preferences/PreferencesComponents/Content' import { WebApplication } from '@/Application/WebApplication' -import { ApplicationEvent, FeatureIdentifier, FeatureStatus, FindNativeFeature, PrefKey } from '@standardnotes/snjs' +import { + ApplicationEvent, + FeatureIdentifier, + FeatureStatus, + FindNativeFeature, + PrefKey, + PrefDefaults, +} from '@standardnotes/snjs' import { Fragment, FunctionComponent, useCallback, useEffect, useState } from 'react' import { usePremiumModal } from '@/Hooks/usePremiumModal' import PreferencesGroup from '../../../PreferencesComponents/PreferencesGroup' @@ -8,7 +15,6 @@ import PreferencesSegment from '../../../PreferencesComponents/PreferencesSegmen import LabsFeature from './LabsFeature' import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator' import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery' -import { PrefDefaults } from '@/Constants/PrefDefaults' type ExperimentalFeatureItem = { identifier: FeatureIdentifier diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/General/Tools.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/General/Tools.tsx index ac175608d31..8d52c492fd0 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/General/Tools.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/General/Tools.tsx @@ -1,12 +1,11 @@ import Switch from '@/Components/Switch/Switch' import { Subtitle, Text, Title } from '@/Components/Preferences/PreferencesComponents/Content' import { WebApplication } from '@/Application/WebApplication' -import { PrefKey } from '@standardnotes/snjs' +import { PrefKey, PrefDefaults } from '@standardnotes/snjs' import { observer } from 'mobx-react-lite' import { FunctionComponent, useState } from 'react' import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup' import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment' -import { PrefDefaults } from '@/Constants/PrefDefaults' type Props = { application: WebApplication diff --git a/packages/web/src/javascripts/Components/QuickSettingsMenu/QuickSettingsMenu.tsx b/packages/web/src/javascripts/Components/QuickSettingsMenu/QuickSettingsMenu.tsx index 2aa0036b0a5..e1fe4147bd6 100644 --- a/packages/web/src/javascripts/Components/QuickSettingsMenu/QuickSettingsMenu.tsx +++ b/packages/web/src/javascripts/Components/QuickSettingsMenu/QuickSettingsMenu.tsx @@ -1,5 +1,13 @@ import { WebApplication } from '@/Application/WebApplication' -import { ComponentArea, ContentType, FeatureIdentifier, GetFeatures, SNComponent } from '@standardnotes/snjs' +import { + ComponentArea, + ComponentInterface, + ContentType, + FeatureIdentifier, + GetFeatures, + SNComponent, + SNTheme, +} from '@standardnotes/snjs' import { observer } from 'mobx-react-lite' import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react' import Icon from '@/Components/Icon/Icon' @@ -25,7 +33,9 @@ const QuickSettingsMenu: FunctionComponent = ({ application, quickSet const [themes, setThemes] = useState([]) const [toggleableComponents, setToggleableComponents] = useState([]) - const defaultThemeOn = !themes.map((item) => item?.component).find((theme) => theme?.active && !theme.isLayerable()) + const activeThemes = application.preferences.getActiveThemes() + const hasNonLayerableActiveTheme = activeThemes.find((theme) => !theme.isLayerable()) + const defaultThemeOn = !hasNonLayerableActiveTheme const prefsButtonRef = useRef(null) const defaultThemeButtonRef = useRef(null) @@ -100,9 +110,9 @@ const QuickSettingsMenu: FunctionComponent = ({ application, quickSet }, []) const toggleComponent = useCallback( - (component: SNComponent) => { + (component: ComponentInterface) => { if (component.isTheme()) { - application.componentManager.toggleTheme(component.uuid).catch(console.error) + application.componentManager.toggleTheme(component as SNTheme).catch(console.error) } else { application.componentManager.toggleComponent(component).catch(console.error) } @@ -111,11 +121,11 @@ const QuickSettingsMenu: FunctionComponent = ({ application, quickSet ) const deactivateAnyNonLayerableTheme = useCallback(() => { - const activeTheme = themes.map((item) => item.component).find((theme) => theme?.active && !theme.isLayerable()) - if (activeTheme) { - application.componentManager.toggleTheme(activeTheme.uuid).catch(console.error) + const nonLayerableActiveTheme = application.preferences.getActiveThemes().find((theme) => !theme.isLayerable()) + if (nonLayerableActiveTheme) { + void application.componentManager.toggleTheme(nonLayerableActiveTheme) } - }, [application, themes]) + }, [application]) const toggleDefaultTheme = useCallback(() => { deactivateAnyNonLayerableTheme() @@ -131,7 +141,7 @@ const QuickSettingsMenu: FunctionComponent = ({ application, quickSet onChange={() => { toggleComponent(component) }} - checked={component.active} + checked={application.preferences.isComponentActive(component)} key={component.uuid} > diff --git a/packages/web/src/javascripts/Components/QuickSettingsMenu/ThemesMenuButton.tsx b/packages/web/src/javascripts/Components/QuickSettingsMenu/ThemesMenuButton.tsx index a5cdcd85f75..4f7f43669a4 100644 --- a/packages/web/src/javascripts/Components/QuickSettingsMenu/ThemesMenuButton.tsx +++ b/packages/web/src/javascripts/Components/QuickSettingsMenu/ThemesMenuButton.tsx @@ -35,10 +35,10 @@ const ThemesMenuButton: FunctionComponent = ({ application, item }) => { const toggleTheme = useCallback(() => { if (item.component && canActivateTheme) { const isThemeLayerable = item.component.isLayerable() - const themeIsLayerableOrNotActive = isThemeLayerable || !item.component.active + const themeIsLayerableOrNotActive = isThemeLayerable || !application.preferences.isThemeActive(item.component) if (themeIsLayerableOrNotActive) { - application.componentManager.toggleTheme(item.component.uuid).catch(console.error) + application.componentManager.toggleTheme(item.component).catch(console.error) } } else { premiumModal.activate(`${item.name} theme`) @@ -66,16 +66,18 @@ const ThemesMenuButton: FunctionComponent = ({ application, item }) => { return null } + const themeActive = item.component ? application.preferences.isThemeActive(item.component) : false + return item.component?.isLayerable() ? ( - toggleTheme()}> + toggleTheme()}> {!canActivateTheme && ( )} {item.name} ) : ( - - {item.name} + + {item.name} {darkThemeShortcut && } {item.component && canActivateTheme ? (
(preference: Key) { diff --git a/packages/web/src/javascripts/Utils/NoteExportUtils.ts b/packages/web/src/javascripts/Utils/NoteExportUtils.ts index d0eae08942f..9966b313d3a 100644 --- a/packages/web/src/javascripts/Utils/NoteExportUtils.ts +++ b/packages/web/src/javascripts/Utils/NoteExportUtils.ts @@ -1,6 +1,5 @@ import { HeadlessSuperConverter } from '@/Components/SuperEditor/Tools/HeadlessSuperConverter' -import { PrefDefaults } from '@/Constants/PrefDefaults' -import { NoteType, PrefKey, SNNote, getComponentOrNativeFeatureFileType } from '@standardnotes/snjs' +import { NoteType, PrefKey, SNNote, getComponentOrNativeFeatureFileType, PrefDefaults } from '@standardnotes/snjs' import { WebApplicationInterface } from '@standardnotes/ui-services' export const getNoteFormat = (application: WebApplicationInterface, note: SNNote) => {