From bfbf9ab8ceb6f1ecd3a0690bce3b5d1c5c52e84c Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Sat, 17 Feb 2024 14:23:37 +0530 Subject: [PATCH] feat: Themes and appeareance settings are now local to your device and not synced (#2847) --- .../Domain/Syncable/UserPrefs/PrefDefaults.ts | 10 +- .../src/Domain/Syncable/UserPrefs/PrefKey.ts | 20 +-- .../src/Domain/Event/ApplicationEvent.ts | 1 + .../src/Domain/Preferences/LocalPrefKey.ts | 15 +++ .../Preferences/PreferenceServiceInterface.ts | 10 ++ .../src/Domain/Storage/StorageKeys.ts | 1 + packages/services/src/Domain/index.ts | 1 + packages/snjs/lib/Application/Application.ts | 9 +- .../Application/Dependencies/Dependencies.ts | 2 + .../snjs/lib/Migrations/Versions/2_202_1.ts | 2 +- .../snjs/lib/Migrations/Versions/2_208_0.ts | 55 ++++++++ .../snjs/lib/Migrations/Versions/index.ts | 3 + .../ComponentManager/ComponentManager.ts | 29 +++-- .../Preferences/PreferencesService.ts | 26 ++++ .../ui-services/src/Theme/ThemeManager.ts | 121 +++++++++--------- .../Preferences/Panes/Appearance.tsx | 33 ++--- .../QuickSettingsMenu/QuickSettingsMenu.tsx | 2 +- .../src/javascripts/Hooks/usePreference.tsx | 27 +++- 18 files changed, 250 insertions(+), 117 deletions(-) create mode 100644 packages/services/src/Domain/Preferences/LocalPrefKey.ts create mode 100644 packages/snjs/lib/Migrations/Versions/2_208_0.ts diff --git a/packages/models/src/Domain/Syncable/UserPrefs/PrefDefaults.ts b/packages/models/src/Domain/Syncable/UserPrefs/PrefDefaults.ts index 4c6b1d507..15b9f01f4 100644 --- a/packages/models/src/Domain/Syncable/UserPrefs/PrefDefaults.ts +++ b/packages/models/src/Domain/Syncable/UserPrefs/PrefDefaults.ts @@ -27,10 +27,10 @@ export const PrefDefaults = { [PrefKey.NotesHideDate]: false, [PrefKey.NotesHideTags]: false, [PrefKey.NotesHideEditorIcon]: false, - [PrefKey.UseSystemColorScheme]: false, - [PrefKey.UseTranslucentUI]: true, - [PrefKey.AutoLightThemeIdentifier]: 'Default', - [PrefKey.AutoDarkThemeIdentifier]: NativeFeatureIdentifier.TYPES.DarkTheme, + [PrefKey.DEPRECATED_UseSystemColorScheme]: false, + [PrefKey.DEPRECATED_UseTranslucentUI]: true, + [PrefKey.DEPRECATED_AutoLightThemeIdentifier]: 'Default', + [PrefKey.DEPRECATED_AutoDarkThemeIdentifier]: NativeFeatureIdentifier.TYPES.DarkTheme, [PrefKey.NoteAddToParentFolders]: true, [PrefKey.NewNoteTitleFormat]: NewNoteTitleFormat.CurrentDateAndTime, [PrefKey.CustomNoteTitleFormat]: 'YYYY-MM-DD [at] hh:mm A', @@ -46,7 +46,7 @@ export const PrefDefaults = { [PrefKey.SystemViewPreferences]: {}, [PrefKey.AuthenticatorNames]: '', [PrefKey.ComponentPreferences]: {}, - [PrefKey.ActiveThemes]: [], + [PrefKey.DEPRECATED_ActiveThemes]: [], [PrefKey.ActiveComponents]: [], [PrefKey.AlwaysShowSuperToolbar]: true, [PrefKey.AddImportsToTag]: true, diff --git a/packages/models/src/Domain/Syncable/UserPrefs/PrefKey.ts b/packages/models/src/Domain/Syncable/UserPrefs/PrefKey.ts index 8fc7ef3b7..2c4edbfc7 100644 --- a/packages/models/src/Domain/Syncable/UserPrefs/PrefKey.ts +++ b/packages/models/src/Domain/Syncable/UserPrefs/PrefKey.ts @@ -28,10 +28,6 @@ export enum PrefKey { NotesHideDate = 'hideDate', NotesHideTags = 'hideTags', NotesHideEditorIcon = 'hideEditorIcon', - UseSystemColorScheme = 'useSystemColorScheme', - UseTranslucentUI = 'useTranslucentUI', - AutoLightThemeIdentifier = 'autoLightThemeIdentifier', - AutoDarkThemeIdentifier = 'autoDarkThemeIdentifier', NoteAddToParentFolders = 'noteAddToParentFolders', NewNoteTitleFormat = 'newNoteTitleFormat', CustomNoteTitleFormat = 'customNoteTitleFormat', @@ -47,12 +43,16 @@ export enum PrefKey { AuthenticatorNames = 'authenticatorNames', PaneGesturesEnabled = 'paneGesturesEnabled', ComponentPreferences = 'componentPreferences', - ActiveThemes = 'activeThemes', ActiveComponents = 'activeComponents', AlwaysShowSuperToolbar = 'alwaysShowSuperToolbar', AddImportsToTag = 'addImportsToTag', AlwaysCreateNewTagForImports = 'alwaysCreateNewTagForImports', ExistingTagForImports = 'existingTagForImports', + DEPRECATED_ActiveThemes = 'activeThemes', + DEPRECATED_UseSystemColorScheme = 'useSystemColorScheme', + DEPRECATED_UseTranslucentUI = 'useTranslucentUI', + DEPRECATED_AutoLightThemeIdentifier = 'autoLightThemeIdentifier', + DEPRECATED_AutoDarkThemeIdentifier = 'autoDarkThemeIdentifier', } export type PrefValue = { @@ -73,10 +73,11 @@ export type PrefValue = { [PrefKey.NotesHideDate]: boolean [PrefKey.NotesHideTags]: boolean [PrefKey.NotesHideEditorIcon]: boolean - [PrefKey.UseSystemColorScheme]: boolean - [PrefKey.UseTranslucentUI]: boolean - [PrefKey.AutoLightThemeIdentifier]: string - [PrefKey.AutoDarkThemeIdentifier]: string + [PrefKey.DEPRECATED_ActiveThemes]: string[] + [PrefKey.DEPRECATED_UseSystemColorScheme]: boolean + [PrefKey.DEPRECATED_UseTranslucentUI]: boolean + [PrefKey.DEPRECATED_AutoLightThemeIdentifier]: string + [PrefKey.DEPRECATED_AutoDarkThemeIdentifier]: string [PrefKey.NoteAddToParentFolders]: boolean [PrefKey.NewNoteTitleFormat]: NewNoteTitleFormat [PrefKey.CustomNoteTitleFormat]: string @@ -95,7 +96,6 @@ export type PrefValue = { [PrefKey.AuthenticatorNames]: string [PrefKey.PaneGesturesEnabled]: boolean [PrefKey.ComponentPreferences]: AllComponentPreferences - [PrefKey.ActiveThemes]: string[] [PrefKey.ActiveComponents]: string[] [PrefKey.AlwaysShowSuperToolbar]: boolean [PrefKey.AddImportsToTag]: boolean diff --git a/packages/services/src/Domain/Event/ApplicationEvent.ts b/packages/services/src/Domain/Event/ApplicationEvent.ts index 82c63b6fd..c41cb2257 100644 --- a/packages/services/src/Domain/Event/ApplicationEvent.ts +++ b/packages/services/src/Domain/Event/ApplicationEvent.ts @@ -49,6 +49,7 @@ export enum ApplicationEvent { /** When StorageService is ready (but NOT yet decrypted) to start servicing read/write requests */ StorageReady = 'Application:StorageReady', PreferencesChanged = 'Application:PreferencesChanged', + LocalPreferencesChanged = 'Application:LocalPreferencesChanged', UnprotectedSessionBegan = 'Application:UnprotectedSessionBegan', UserRolesChanged = 'Application:UserRolesChanged', FeaturesAvailabilityChanged = 'Application:FeaturesAvailabilityChanged', diff --git a/packages/services/src/Domain/Preferences/LocalPrefKey.ts b/packages/services/src/Domain/Preferences/LocalPrefKey.ts new file mode 100644 index 000000000..6ac3e4769 --- /dev/null +++ b/packages/services/src/Domain/Preferences/LocalPrefKey.ts @@ -0,0 +1,15 @@ +export enum LocalPrefKey { + ActiveThemes = 'activeThemes', + UseSystemColorScheme = 'useSystemColorScheme', + UseTranslucentUI = 'useTranslucentUI', + AutoLightThemeIdentifier = 'autoLightThemeIdentifier', + AutoDarkThemeIdentifier = 'autoDarkThemeIdentifier', +} + +export type LocalPrefValue = { + [LocalPrefKey.ActiveThemes]: string[] + [LocalPrefKey.UseSystemColorScheme]: boolean + [LocalPrefKey.UseTranslucentUI]: boolean + [LocalPrefKey.AutoLightThemeIdentifier]: string + [LocalPrefKey.AutoDarkThemeIdentifier]: string +} diff --git a/packages/services/src/Domain/Preferences/PreferenceServiceInterface.ts b/packages/services/src/Domain/Preferences/PreferenceServiceInterface.ts index cf3be4846..f2793d4e3 100644 --- a/packages/services/src/Domain/Preferences/PreferenceServiceInterface.ts +++ b/packages/services/src/Domain/Preferences/PreferenceServiceInterface.ts @@ -1,7 +1,9 @@ import { PrefKey, PrefValue } from '@standardnotes/models' import { AbstractService } from '../Service/AbstractService' +import { LocalPrefKey, LocalPrefValue } from './LocalPrefKey' export enum PreferencesServiceEvent { + LocalPreferencesChanged = 'LocalPreferencesChanged', PreferencesChanged = 'PreferencesChanged', } @@ -10,7 +12,15 @@ export interface PreferenceServiceInterface extends AbstractService(key: K, defaultValue?: PrefValue[K]): PrefValue[K] | undefined getValue(key: K, defaultValue: PrefValue[K] | undefined): PrefValue[K] | undefined + getLocalValue(key: K, defaultValue: LocalPrefValue[K]): LocalPrefValue[K] + getLocalValue(key: K, defaultValue?: LocalPrefValue[K]): LocalPrefValue[K] | undefined + getLocalValue( + key: K, + defaultValue: LocalPrefValue[K] | undefined, + ): LocalPrefValue[K] | undefined + setValue(key: K, value: PrefValue[K]): Promise /** Set value without triggering sync or event notifications */ setValueDetached(key: K, value: PrefValue[K]): Promise + setLocalValue(key: K, value: LocalPrefValue[K]): void } diff --git a/packages/services/src/Domain/Storage/StorageKeys.ts b/packages/services/src/Domain/Storage/StorageKeys.ts index 7ce6cd28b..3d0d2fc78 100644 --- a/packages/services/src/Domain/Storage/StorageKeys.ts +++ b/packages/services/src/Domain/Storage/StorageKeys.ts @@ -50,6 +50,7 @@ export enum StorageKey { FileBackupsLocation = 'file_backups_location', VaultSelectionOptions = 'vault_selection_options', Subscription = 'subscription', + LocalPreferences = 'local_preferences', } export enum NonwrappedStorageKey { diff --git a/packages/services/src/Domain/index.ts b/packages/services/src/Domain/index.ts index 53d78c9b1..d5eda0a86 100644 --- a/packages/services/src/Domain/index.ts +++ b/packages/services/src/Domain/index.ts @@ -134,6 +134,7 @@ export * from './KeySystem/KeySystemKeyManager' export * from './Mfa/MfaServiceInterface' export * from './Mutator/MutatorClientInterface' export * from './Payloads/PayloadManagerInterface' +export * from './Preferences/LocalPrefKey' export * from './Preferences/PreferenceId' export * from './Preferences/PreferenceServiceInterface' export * from './Protection/MobileUnlockTiming' diff --git a/packages/snjs/lib/Application/Application.ts b/packages/snjs/lib/Application/Application.ts index 0ac889ec8..857ddb85d 100644 --- a/packages/snjs/lib/Application/Application.ts +++ b/packages/snjs/lib/Application/Application.ts @@ -81,6 +81,7 @@ import { CreateDecryptedBackupFile, CreateEncryptedBackupFile, WebSocketsService, + PreferencesServiceEvent, } from '@standardnotes/services' import { SNNote, @@ -326,8 +327,12 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli const preferencesService = this.dependencies.get(TYPES.PreferencesService) this.serviceObservers.push( - preferencesService.addEventObserver(() => { - void this.notifyEvent(ApplicationEvent.PreferencesChanged) + preferencesService.addEventObserver((event) => { + if (event === PreferencesServiceEvent.PreferencesChanged) { + void this.notifyEvent(ApplicationEvent.PreferencesChanged) + } else if (event === PreferencesServiceEvent.LocalPreferencesChanged) { + void this.notifyEvent(ApplicationEvent.LocalPreferencesChanged) + } }), ) diff --git a/packages/snjs/lib/Application/Dependencies/Dependencies.ts b/packages/snjs/lib/Application/Dependencies/Dependencies.ts index ca0288a2b..7f5815742 100644 --- a/packages/snjs/lib/Application/Dependencies/Dependencies.ts +++ b/packages/snjs/lib/Application/Dependencies/Dependencies.ts @@ -147,6 +147,7 @@ import { DesignateSurvivor, SyncBackoffService, SyncBackoffServiceInterface, + StorageServiceInterface, } from '@standardnotes/services' import { ItemManager } from '../../Services/Items/ItemManager' import { PayloadManager } from '../../Services/Payloads/PayloadManager' @@ -1282,6 +1283,7 @@ export class Dependencies { this.get(TYPES.ItemManager), this.get(TYPES.MutatorService), this.get(TYPES.SyncService), + this.get(TYPES.DiskStorageService), this.get(TYPES.InternalEventBus), ) }) diff --git a/packages/snjs/lib/Migrations/Versions/2_202_1.ts b/packages/snjs/lib/Migrations/Versions/2_202_1.ts index 19ce8b5f9..23aa64ac1 100644 --- a/packages/snjs/lib/Migrations/Versions/2_202_1.ts +++ b/packages/snjs/lib/Migrations/Versions/2_202_1.ts @@ -66,7 +66,7 @@ export class Migration2_202_1 extends Migration { const activeThemes = allActiveitems.filter((component) => component.isTheme()) const activeComponents = allActiveitems.filter((component) => !component.isTheme()) - await this.services.preferences.setValueDetached(PrefKey.ActiveThemes, Uuids(activeThemes)) + await this.services.preferences.setValueDetached(PrefKey.DEPRECATED_ActiveThemes, Uuids(activeThemes)) await this.services.preferences.setValueDetached(PrefKey.ActiveComponents, Uuids(activeComponents)) } } diff --git a/packages/snjs/lib/Migrations/Versions/2_208_0.ts b/packages/snjs/lib/Migrations/Versions/2_208_0.ts new file mode 100644 index 000000000..9987e53bc --- /dev/null +++ b/packages/snjs/lib/Migrations/Versions/2_208_0.ts @@ -0,0 +1,55 @@ +import { LocalPrefKey, ApplicationStage } from '@standardnotes/services' +import { Migration } from '@Lib/Migrations/Migration' +import { PrefDefaults, PrefKey } from '@standardnotes/models' + +export class Migration2_208_0 extends Migration { + static override version(): string { + return '2.208.0' + } + + protected registerStageHandlers(): void { + this.registerStageHandler(ApplicationStage.FullSyncCompleted_13, async () => { + await this.migrateSyncedPreferencesToLocal() + + this.markDone() + }) + } + + private async migrateSyncedPreferencesToLocal(): Promise { + this.services.preferences.setLocalValue( + LocalPrefKey.ActiveThemes, + this.services.preferences.getValue( + PrefKey.DEPRECATED_ActiveThemes, + PrefDefaults[PrefKey.DEPRECATED_ActiveThemes], + ), + ) + this.services.preferences.setLocalValue( + LocalPrefKey.UseSystemColorScheme, + this.services.preferences.getValue( + PrefKey.DEPRECATED_UseSystemColorScheme, + PrefDefaults[PrefKey.DEPRECATED_UseSystemColorScheme], + ), + ) + this.services.preferences.setLocalValue( + LocalPrefKey.AutoLightThemeIdentifier, + this.services.preferences.getValue( + PrefKey.DEPRECATED_AutoLightThemeIdentifier, + PrefDefaults[PrefKey.DEPRECATED_AutoLightThemeIdentifier], + ), + ) + this.services.preferences.setLocalValue( + LocalPrefKey.AutoDarkThemeIdentifier, + this.services.preferences.getValue( + PrefKey.DEPRECATED_AutoDarkThemeIdentifier, + PrefDefaults[PrefKey.DEPRECATED_AutoDarkThemeIdentifier], + ), + ) + this.services.preferences.setLocalValue( + LocalPrefKey.UseTranslucentUI, + this.services.preferences.getValue( + PrefKey.DEPRECATED_UseTranslucentUI, + PrefDefaults[PrefKey.DEPRECATED_UseTranslucentUI], + ), + ) + } +} diff --git a/packages/snjs/lib/Migrations/Versions/index.ts b/packages/snjs/lib/Migrations/Versions/index.ts index 610025c7c..51f9cc292 100644 --- a/packages/snjs/lib/Migrations/Versions/index.ts +++ b/packages/snjs/lib/Migrations/Versions/index.ts @@ -6,6 +6,7 @@ import { Migration2_42_0 } from './2_42_0' import { Migration2_167_6 } from './2_167_6' import { Migration2_168_6 } from './2_168_6' import { Migration2_202_1 } from './2_202_1' +import { Migration2_208_0 } from './2_208_0' export const MigrationClasses = [ Migration2_0_15, @@ -16,6 +17,7 @@ export const MigrationClasses = [ Migration2_167_6, Migration2_168_6, Migration2_202_1, + Migration2_208_0, ] export { @@ -27,4 +29,5 @@ export { Migration2_167_6, Migration2_168_6, Migration2_202_1, + Migration2_208_0, } diff --git a/packages/snjs/lib/Services/ComponentManager/ComponentManager.ts b/packages/snjs/lib/Services/ComponentManager/ComponentManager.ts index 0741c0a53..87f532d04 100644 --- a/packages/snjs/lib/Services/ComponentManager/ComponentManager.ts +++ b/packages/snjs/lib/Services/ComponentManager/ComponentManager.ts @@ -48,6 +48,7 @@ import { ItemManagerInterface, SyncServiceInterface, FeatureStatus, + LocalPrefKey, } from '@standardnotes/services' import { GetFeatureUrl } from './UseCase/GetFeatureUrl' import { ComponentManagerEventData } from './ComponentManagerEventData' @@ -393,7 +394,7 @@ export class ComponentManager this.logger.info('Toggling theme', uiFeature.uniqueIdentifier) if (this.isThemeActive(uiFeature)) { - await this.removeActiveTheme(uiFeature) + this.removeActiveTheme(uiFeature) return } @@ -403,7 +404,7 @@ export class ComponentManager } /* Activate current before deactivating others, so as not to flicker */ - await this.addActiveTheme(uiFeature) + this.addActiveTheme(uiFeature) /* Deactive currently active theme(s) if new theme is not layerable */ if (!uiFeature.layerable) { @@ -416,7 +417,7 @@ export class ComponentManager } if (!candidate.layerable) { - await this.removeActiveTheme(candidate) + this.removeActiveTheme(candidate) } } } @@ -453,7 +454,7 @@ export class ComponentManager const features: NativeFeatureIdentifier[] = [] const uuids: Uuid[] = [] - const strings = this.preferences.getValue(PrefKey.ActiveThemes, undefined) ?? [] + const strings = this.preferences.getLocalValue(LocalPrefKey.ActiveThemes, []) for (const string of strings) { const nativeIdentifier = NativeFeatureIdentifier.create(string) if (!nativeIdentifier.isFailed()) { @@ -534,24 +535,24 @@ export class ComponentManager return preferences[preferencesLookupKey] } - async addActiveTheme(theme: UIFeature): Promise { - const activeThemes = (this.preferences.getValue(PrefKey.ActiveThemes, undefined) ?? []).slice() + addActiveTheme(theme: UIFeature) { + const activeThemes = this.preferences.getLocalValue(LocalPrefKey.ActiveThemes, []).slice() activeThemes.push(theme.uniqueIdentifier.value) - await this.preferences.setValue(PrefKey.ActiveThemes, activeThemes) + this.preferences.setLocalValue(LocalPrefKey.ActiveThemes, activeThemes) } - async replaceActiveTheme(theme: UIFeature): Promise { - await this.preferences.setValue(PrefKey.ActiveThemes, [theme.uniqueIdentifier.value]) + replaceActiveTheme(theme: UIFeature) { + this.preferences.setLocalValue(LocalPrefKey.ActiveThemes, [theme.uniqueIdentifier.value]) } - async removeActiveTheme(theme: UIFeature): Promise { - const activeThemes = this.preferences.getValue(PrefKey.ActiveThemes, undefined) ?? [] + removeActiveTheme(theme: UIFeature) { + const activeThemes = this.preferences.getLocalValue(LocalPrefKey.ActiveThemes, []) const filteredThemes = activeThemes.filter((activeTheme) => activeTheme !== theme.uniqueIdentifier.value) - await this.preferences.setValue(PrefKey.ActiveThemes, filteredThemes) + this.preferences.setLocalValue(LocalPrefKey.ActiveThemes, filteredThemes) } isThemeActive(theme: UIFeature): boolean { @@ -559,13 +560,13 @@ export class ComponentManager return false } - const activeThemes = this.preferences.getValue(PrefKey.ActiveThemes, undefined) ?? [] + const activeThemes = this.preferences.getLocalValue(LocalPrefKey.ActiveThemes, []) return activeThemes.includes(theme.uniqueIdentifier.value) } async addActiveComponent(component: ComponentInterface): Promise { - const activeComponents = (this.preferences.getValue(PrefKey.ActiveComponents, undefined) ?? []).slice() + const activeComponents = this.preferences.getValue(PrefKey.ActiveComponents, []).slice() activeComponents.push(component.uuid) diff --git a/packages/snjs/lib/Services/Preferences/PreferencesService.ts b/packages/snjs/lib/Services/Preferences/PreferencesService.ts index f86c6929b..f73dbd66f 100644 --- a/packages/snjs/lib/Services/Preferences/PreferencesService.ts +++ b/packages/snjs/lib/Services/Preferences/PreferencesService.ts @@ -14,6 +14,10 @@ import { InternalEventInterface, ApplicationEvent, ApplicationStageChangedEventPayload, + StorageServiceInterface, + StorageKey, + LocalPrefKey, + LocalPrefValue, } from '@standardnotes/services' import { ContentType } from '@standardnotes/domain-core' @@ -24,6 +28,7 @@ export class PreferencesService private shouldReload = true private reloading = false private preferences?: SNUserPrefs + private localPreferences: { [key in LocalPrefKey]?: LocalPrefValue[key] } = {} private removeItemObserver?: () => void private removeSyncObserver?: () => void @@ -32,6 +37,7 @@ export class PreferencesService items: ItemManager, private mutator: MutatorClientInterface, private sync: SyncService, + private storage: StorageServiceInterface, protected override internalEventBus: InternalEventBusInterface, ) { super(internalEventBus) @@ -69,16 +75,36 @@ export class PreferencesService if (this.preferences) { void this.notifyEvent(PreferencesServiceEvent.PreferencesChanged) } + } else if (stage === ApplicationStage.StorageDecrypted_09) { + this.localPreferences = this.storage.getValue(StorageKey.LocalPreferences) ?? {} + void this.notifyEvent(PreferencesServiceEvent.LocalPreferencesChanged) } } } + getLocalValue( + key: K, + defaultValue: LocalPrefValue[K] | undefined, + ): LocalPrefValue[K] | undefined + getLocalValue(key: K, defaultValue: LocalPrefValue[K]): LocalPrefValue[K] + getLocalValue(key: K, defaultValue?: LocalPrefValue[K]): LocalPrefValue[K] | undefined { + return this.localPreferences[key] ?? defaultValue + } + getValue(key: K, defaultValue: PrefValue[K] | undefined): PrefValue[K] | undefined getValue(key: K, defaultValue: PrefValue[K]): PrefValue[K] getValue(key: K, defaultValue?: PrefValue[K]): PrefValue[K] | undefined { return this.preferences?.getPref(key) ?? defaultValue } + setLocalValue(key: K, value: LocalPrefValue[K]): void { + this.localPreferences[key] = value + + this.storage.setValue(StorageKey.LocalPreferences, this.localPreferences) + + void this.notifyEvent(PreferencesServiceEvent.LocalPreferencesChanged) + } + async setValue(key: K, value: PrefValue[K]): Promise { await this.setValueDetached(key, value) diff --git a/packages/ui-services/src/Theme/ThemeManager.ts b/packages/ui-services/src/Theme/ThemeManager.ts index 45063671b..a36bd0bcc 100644 --- a/packages/ui-services/src/Theme/ThemeManager.ts +++ b/packages/ui-services/src/Theme/ThemeManager.ts @@ -2,7 +2,6 @@ import { UIFeature, CreateDecryptedLocalStorageContextPayload, LocalStorageDecryptedContextualPayload, - PrefKey, PrefDefaults, ComponentInterface, } from '@standardnotes/models' @@ -12,8 +11,8 @@ import { StorageValueModes, FeatureStatus, PreferenceServiceInterface, - PreferencesServiceEvent, ComponentManagerInterface, + LocalPrefKey, } from '@standardnotes/services' import { NativeFeatureIdentifier, FindNativeTheme, ThemeFeatureDescription } from '@standardnotes/features' import { WebApplicationInterface } from '../WebApplication/WebApplicationInterface' @@ -75,56 +74,6 @@ export class ThemeManager extends AbstractUIService { }), ) } - - this.eventDisposers.push( - this.preferences.addEventObserver(async (event) => { - if (event !== PreferencesServiceEvent.PreferencesChanged) { - return - } - - this.toggleTranslucentUIColors() - - let hasChange = false - - 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 feature of features) { - if (!this.themesActiveInTheUI.has(feature)) { - const theme = FindNativeTheme(feature.value) - if (theme) { - const uiFeature = new UIFeature(theme) - this.activateTheme(uiFeature) - hasChange = true - } - } - } - - for (const uuid of uuids) { - if (!this.themesActiveInTheUI.has(uuid)) { - const theme = this.application.items.findItem(uuid.value) - if (theme) { - const uiFeature = new UIFeature(theme) - this.activateTheme(uiFeature) - hasChange = true - } - } - } - - if (hasChange) { - this.cacheThemeState().catch(console.error) - } - }), - ) } override async onAppEvent(event: ApplicationEvent) { @@ -154,15 +103,15 @@ export class ThemeManager extends AbstractUIService { } break } - case ApplicationEvent.PreferencesChanged: { - void this.handlePreferencesChangeEvent() + case ApplicationEvent.LocalPreferencesChanged: { + void this.handleLocalPreferencesChangeEvent() break } } } async handleMobileColorSchemeChangeEvent() { - const useDeviceThemeSettings = this.application.getPreference(PrefKey.UseSystemColorScheme, false) + const useDeviceThemeSettings = this.preferences.getLocalValue(LocalPrefKey.UseSystemColorScheme, false) if (useDeviceThemeSettings) { const prefersDarkColorScheme = (await this.application.mobileDevice.getColorScheme()) === 'dark' @@ -170,10 +119,54 @@ export class ThemeManager extends AbstractUIService { } } - private async handlePreferencesChangeEvent() { + private handleThemeStateChange() { + let hasChange = false + + 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 feature of features) { + if (!this.themesActiveInTheUI.has(feature)) { + const theme = FindNativeTheme(feature.value) + if (theme) { + const uiFeature = new UIFeature(theme) + this.activateTheme(uiFeature) + hasChange = true + } + } + } + + for (const uuid of uuids) { + if (!this.themesActiveInTheUI.has(uuid)) { + const theme = this.application.items.findItem(uuid.value) + if (theme) { + const uiFeature = new UIFeature(theme) + this.activateTheme(uiFeature) + hasChange = true + } + } + } + + if (hasChange) { + this.cacheThemeState().catch(console.error) + } + } + + private async handleLocalPreferencesChangeEvent() { + this.handleThemeStateChange() + this.toggleTranslucentUIColors() - const useDeviceThemeSettings = this.application.getPreference(PrefKey.UseSystemColorScheme, false) + const useDeviceThemeSettings = this.preferences.getLocalValue(LocalPrefKey.UseSystemColorScheme, false) const hasPreferenceChanged = useDeviceThemeSettings !== this.lastUseDeviceThemeSettings @@ -218,7 +211,7 @@ export class ThemeManager extends AbstractUIService { } private colorSchemeEventHandler(event: MediaQueryListEvent) { - const shouldChangeTheme = this.application.getPreference(PrefKey.UseSystemColorScheme, false) + const shouldChangeTheme = this.preferences.getLocalValue(LocalPrefKey.UseSystemColorScheme, false) if (shouldChangeTheme) { this.setThemeAsPerColorScheme(event.matches) @@ -226,10 +219,14 @@ export class ThemeManager extends AbstractUIService { } private setThemeAsPerColorScheme(prefersDarkColorScheme: boolean) { - const preference = prefersDarkColorScheme ? PrefKey.AutoDarkThemeIdentifier : PrefKey.AutoLightThemeIdentifier + const preference = prefersDarkColorScheme + ? LocalPrefKey.AutoDarkThemeIdentifier + : LocalPrefKey.AutoLightThemeIdentifier const preferenceDefault = - preference === PrefKey.AutoDarkThemeIdentifier ? NativeFeatureIdentifier.TYPES.DarkTheme : DefaultThemeIdentifier + preference === LocalPrefKey.AutoDarkThemeIdentifier + ? NativeFeatureIdentifier.TYPES.DarkTheme + : DefaultThemeIdentifier const usecase = new GetAllThemesUseCase(this.application.items) const { thirdParty, native } = usecase.execute({ excludeLayerable: false }) @@ -237,7 +234,7 @@ export class ThemeManager extends AbstractUIService { const activeTheme = themes.find((theme) => this.components.isThemeActive(theme) && !theme.layerable) - const themeIdentifier = this.preferences.getValue(preference, preferenceDefault) + const themeIdentifier = this.preferences.getLocalValue(preference, preferenceDefault) const toggleActiveTheme = () => { if (activeTheme) { @@ -337,7 +334,7 @@ export class ThemeManager extends AbstractUIService { } private shouldUseTranslucentUI() { - return this.application.getPreference(PrefKey.UseTranslucentUI, PrefDefaults[PrefKey.UseTranslucentUI]) + return this.preferences.getLocalValue(LocalPrefKey.UseTranslucentUI, PrefDefaults[LocalPrefKey.UseTranslucentUI]) } private toggleTranslucentUIColors() { diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Appearance.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Appearance.tsx index 5d2962b41..30a4d0c77 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Appearance.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Appearance.tsx @@ -4,7 +4,7 @@ import { usePremiumModal } from '@/Hooks/usePremiumModal' import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator' import Switch from '@/Components/Switch/Switch' import { WebApplication } from '@/Application/WebApplication' -import { PrefKey, FeatureStatus, naturalSort, PrefDefaults } from '@standardnotes/snjs' +import { FeatureStatus, naturalSort, LocalPrefKey } from '@standardnotes/snjs' import { observer } from 'mobx-react-lite' import { FunctionComponent, useEffect, useState } from 'react' import { Subtitle, Title, Text } from '@/Components/Preferences/PreferencesComponents/Content' @@ -14,7 +14,7 @@ import PreferencesSegment from '../PreferencesComponents/PreferencesSegment' import { PremiumFeatureIconName } from '@/Components/Icon/PremiumFeatureIcon' import EditorAppearance from './Appearance/EditorAppearance' import { GetAllThemesUseCase } from '@standardnotes/ui-services' -import usePreference from '@/Hooks/usePreference' +import { useLocalPreference } from '@/Hooks/usePreference' type Props = { application: WebApplication @@ -24,19 +24,14 @@ const Appearance: FunctionComponent = ({ application }) => { const premiumModal = usePremiumModal() const [themeItems, setThemeItems] = useState([]) - const [autoLightTheme, setAutoLightTheme] = useState(() => - application.getPreference(PrefKey.AutoLightThemeIdentifier, PrefDefaults[PrefKey.AutoLightThemeIdentifier]), - ) - const [autoDarkTheme, setAutoDarkTheme] = useState(() => - application.getPreference(PrefKey.AutoDarkThemeIdentifier, PrefDefaults[PrefKey.AutoDarkThemeIdentifier]), - ) - const [useDeviceSettings, setUseDeviceSettings] = useState(() => - application.getPreference(PrefKey.UseSystemColorScheme, PrefDefaults[PrefKey.UseSystemColorScheme]), - ) - const useTranslucentUI = usePreference(PrefKey.UseTranslucentUI) + const [autoLightTheme, setAutoLightTheme] = useLocalPreference(LocalPrefKey.AutoLightThemeIdentifier) + const [autoDarkTheme, setAutoDarkTheme] = useLocalPreference(LocalPrefKey.AutoDarkThemeIdentifier) + const [useDeviceSettings, setUseDeviceSettings] = useLocalPreference(LocalPrefKey.UseSystemColorScheme) + + const [useTranslucentUI, setUseTranslucentUI] = useLocalPreference(LocalPrefKey.UseTranslucentUI) const toggleTranslucentUI = () => { - application.setPreference(PrefKey.UseTranslucentUI, !useTranslucentUI).catch(console.error) + setUseTranslucentUI(!useTranslucentUI) } useEffect(() => { @@ -76,12 +71,12 @@ const Appearance: FunctionComponent = ({ application }) => { }, [application]) const toggleUseDeviceSettings = () => { - application.setPreference(PrefKey.UseSystemColorScheme, !useDeviceSettings).catch(console.error) - if (!application.getPreference(PrefKey.AutoLightThemeIdentifier)) { - application.setPreference(PrefKey.AutoLightThemeIdentifier, autoLightTheme).catch(console.error) + setUseDeviceSettings(!useDeviceSettings) + if (!application.preferences.getLocalValue(LocalPrefKey.AutoLightThemeIdentifier)) { + setAutoLightTheme(autoLightTheme) } - if (!application.getPreference(PrefKey.AutoDarkThemeIdentifier)) { - application.setPreference(PrefKey.AutoDarkThemeIdentifier, autoDarkTheme).catch(console.error) + if (!application.preferences.getLocalValue(LocalPrefKey.AutoDarkThemeIdentifier)) { + setAutoDarkTheme(autoDarkTheme) } setUseDeviceSettings(!useDeviceSettings) } @@ -92,7 +87,6 @@ const Appearance: FunctionComponent = ({ application }) => { premiumModal.activate(`${item.label} theme`) return } - application.setPreference(PrefKey.AutoLightThemeIdentifier, value).catch(console.error) setAutoLightTheme(value) } @@ -102,7 +96,6 @@ const Appearance: FunctionComponent = ({ application }) => { premiumModal.activate(`${item.label} theme`) return } - application.setPreference(PrefKey.AutoDarkThemeIdentifier, value).catch(console.error) setAutoDarkTheme(value) } diff --git a/packages/web/src/javascripts/Components/QuickSettingsMenu/QuickSettingsMenu.tsx b/packages/web/src/javascripts/Components/QuickSettingsMenu/QuickSettingsMenu.tsx index 72c946894..645a907fe 100644 --- a/packages/web/src/javascripts/Components/QuickSettingsMenu/QuickSettingsMenu.tsx +++ b/packages/web/src/javascripts/Components/QuickSettingsMenu/QuickSettingsMenu.tsx @@ -76,7 +76,7 @@ const QuickSettingsMenu: FunctionComponent = ({ closeMenu }) => { useEffect(() => { return application.preferences.addEventObserver((event) => { - if (event === PreferencesServiceEvent.PreferencesChanged) { + if (event === PreferencesServiceEvent.LocalPreferencesChanged) { reloadThemes() } }) diff --git a/packages/web/src/javascripts/Hooks/usePreference.tsx b/packages/web/src/javascripts/Hooks/usePreference.tsx index 62a598305..9402aab9e 100644 --- a/packages/web/src/javascripts/Hooks/usePreference.tsx +++ b/packages/web/src/javascripts/Hooks/usePreference.tsx @@ -1,6 +1,29 @@ import { useApplication } from '@/Components/ApplicationProvider' -import { ApplicationEvent, PrefKey, PrefDefaults } from '@standardnotes/snjs' -import { useEffect, useState } from 'react' +import { ApplicationEvent, PrefKey, PrefDefaults, LocalPrefKey, LocalPrefValue } from '@standardnotes/snjs' +import { useCallback, useEffect, useState } from 'react' + +export function useLocalPreference(preference: Key) { + const application = useApplication() + + const [value, setValue] = useState(application.preferences.getLocalValue(preference, PrefDefaults[preference])) + + const setNewValue = useCallback( + (newValue: LocalPrefValue[Key]) => { + application.preferences.setLocalValue(preference, newValue) + }, + [application, preference], + ) + + useEffect(() => { + return application.addEventObserver(async () => { + const latestValue = application.preferences.getLocalValue(preference, PrefDefaults[preference]) + + setValue(latestValue) + }, ApplicationEvent.LocalPreferencesChanged) + }, [application, preference]) + + return [value, setNewValue] as const +} export default function usePreference(preference: Key) { const application = useApplication()