diff --git a/apps/desktop/src/app/accounts/settings.component.ts b/apps/desktop/src/app/accounts/settings.component.ts index bf1102ab54..4e6af9bff4 100644 --- a/apps/desktop/src/app/accounts/settings.component.ts +++ b/apps/desktop/src/app/accounts/settings.component.ts @@ -120,8 +120,8 @@ export class SettingsComponent implements OnInit { private domainSettingsService: DomainSettingsService, private dialogService: DialogService, private userVerificationService: UserVerificationServiceAbstraction, - private biometricStateService: BiometricStateService, private desktopSettingsService: DesktopSettingsService, + private biometricStateService: BiometricStateService, ) { const isMac = this.platformUtilsService.getDevice() === DeviceType.MacOsDesktop; @@ -253,12 +253,12 @@ export class SettingsComponent implements OnInit { clearClipboard: await firstValueFrom(this.autofillSettingsService.clearClipboardDelay$), minimizeOnCopyToClipboard: await this.stateService.getMinimizeOnCopyToClipboard(), enableFavicons: await firstValueFrom(this.domainSettingsService.showFavicons$), - enableTray: await this.stateService.getEnableTray(), - enableMinToTray: await this.stateService.getEnableMinimizeToTray(), - enableCloseToTray: await this.stateService.getEnableCloseToTray(), - startToTray: await this.stateService.getEnableStartToTray(), - openAtLogin: await this.stateService.getOpenAtLogin(), - alwaysShowDock: await this.stateService.getAlwaysShowDock(), + enableTray: await firstValueFrom(this.desktopSettingsService.trayEnabled$), + enableMinToTray: await firstValueFrom(this.desktopSettingsService.minimizeToTray$), + enableCloseToTray: await firstValueFrom(this.desktopSettingsService.closeToTray$), + startToTray: await firstValueFrom(this.desktopSettingsService.startToTray$), + openAtLogin: await firstValueFrom(this.desktopSettingsService.openAtLogin$), + alwaysShowDock: await firstValueFrom(this.desktopSettingsService.alwaysShowDock$), enableBrowserIntegration: await this.stateService.getEnableBrowserIntegration(), enableBrowserIntegrationFingerprint: await this.stateService.getEnableBrowserIntegrationFingerprint(), @@ -507,16 +507,16 @@ export class SettingsComponent implements OnInit { } async saveMinToTray() { - await this.stateService.setEnableMinimizeToTray(this.form.value.enableMinToTray); + await this.desktopSettingsService.setMinimizeToTray(this.form.value.enableMinToTray); } async saveCloseToTray() { if (this.requireEnableTray) { this.form.controls.enableTray.setValue(true); - await this.stateService.setEnableTray(this.form.value.enableTray); + await this.desktopSettingsService.setTrayEnabled(this.form.value.enableTray); } - await this.stateService.setEnableCloseToTray(this.form.value.enableCloseToTray); + await this.desktopSettingsService.setCloseToTray(this.form.value.enableCloseToTray); } async saveTray() { @@ -533,9 +533,9 @@ export class SettingsComponent implements OnInit { if (confirm) { this.form.controls.startToTray.setValue(false, { emitEvent: false }); - await this.stateService.setEnableStartToTray(this.form.value.startToTray); + await this.desktopSettingsService.setStartToTray(this.form.value.startToTray); this.form.controls.enableCloseToTray.setValue(false, { emitEvent: false }); - await this.stateService.setEnableCloseToTray(this.form.value.enableCloseToTray); + await this.desktopSettingsService.setCloseToTray(this.form.value.enableCloseToTray); } else { this.form.controls.enableTray.setValue(true); } @@ -543,17 +543,18 @@ export class SettingsComponent implements OnInit { return; } - await this.stateService.setEnableTray(this.form.value.enableTray); + await this.desktopSettingsService.setTrayEnabled(this.form.value.enableTray); + // TODO: Ideally the DesktopSettingsService.trayEnabled$ could be subscribed to instead of using messaging. this.messagingService.send(this.form.value.enableTray ? "showTray" : "removeTray"); } async saveStartToTray() { if (this.requireEnableTray) { this.form.controls.enableTray.setValue(true); - await this.stateService.setEnableTray(this.form.value.enableTray); + await this.desktopSettingsService.setTrayEnabled(this.form.value.enableTray); } - await this.stateService.setEnableStartToTray(this.form.value.startToTray); + await this.desktopSettingsService.setStartToTray(this.form.value.startToTray); } async saveLocale() { @@ -573,13 +574,12 @@ export class SettingsComponent implements OnInit { } async saveAlwaysShowDock() { - await this.stateService.setAlwaysShowDock(this.form.value.alwaysShowDock); + await this.desktopSettingsService.setAlwaysShowDock(this.form.value.alwaysShowDock); } async saveOpenAtLogin() { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.stateService.setOpenAtLogin(this.form.value.openAtLogin); + await this.desktopSettingsService.setOpenAtLogin(this.form.value.openAtLogin); + // TODO: Ideally DesktopSettingsService.openAtLogin$ could be subscribed to directly rather than sending a message this.messagingService.send( this.form.value.openAtLogin ? "addOpenAtLogin" : "removeOpenAtLogin", ); diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index b7ba2faf79..ee36d49dd9 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -57,8 +57,8 @@ export class Main { environmentService: DefaultEnvironmentService; mainCryptoFunctionService: MainCryptoFunctionService; desktopCredentialStorageListener: DesktopCredentialStorageListener; - migrationRunner: MigrationRunner; desktopSettingsService: DesktopSettingsService; + migrationRunner: MigrationRunner; tokenService: TokenServiceAbstraction; windowMain: WindowMain; @@ -179,6 +179,8 @@ export class Main { false, // Do not use disk caching because this will get out of sync with the renderer service ); + this.desktopSettingsService = new DesktopSettingsService(stateProvider); + const biometricStateService = new DefaultBiometricStateService(stateProvider); this.windowMain = new WindowMain( @@ -186,13 +188,13 @@ export class Main { biometricStateService, this.logService, this.storageService, + this.desktopSettingsService, (arg) => this.processDeepLink(arg), (win) => this.trayMain.setupWindowListeners(win), ); - this.messagingMain = new MessagingMain(this, this.stateService); + this.messagingMain = new MessagingMain(this, this.stateService, this.desktopSettingsService); this.updaterMain = new UpdaterMain(this.i18nService, this.windowMain); - this.trayMain = new TrayMain(this.windowMain, this.i18nService, this.stateService); - this.desktopSettingsService = new DesktopSettingsService(stateProvider); + this.trayMain = new TrayMain(this.windowMain, this.i18nService, this.desktopSettingsService); this.messagingService = new ElectronMainMessagingService(this.windowMain, (message) => { this.messagingMain.onMessage(message); @@ -244,7 +246,7 @@ export class Main { await this.toggleHardwareAcceleration(); await this.windowMain.init(); await this.i18nService.init(); - this.messagingMain.init(); + await this.messagingMain.init(); // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises this.menuMain.init(); @@ -256,10 +258,8 @@ export class Main { click: () => this.messagingService.send("lockVault"), }, ]); - if (await this.stateService.getEnableStartToTray()) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.trayMain.hideToTray(); + if (await firstValueFrom(this.desktopSettingsService.startToTray$)) { + await this.trayMain.hideToTray(); } this.powerMonitorMain.init(); await this.updaterMain.init(); diff --git a/apps/desktop/src/main/messaging.main.ts b/apps/desktop/src/main/messaging.main.ts index 44184e6f84..cc67e312b5 100644 --- a/apps/desktop/src/main/messaging.main.ts +++ b/apps/desktop/src/main/messaging.main.ts @@ -6,6 +6,7 @@ import { app, ipcMain } from "electron"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Main } from "../main"; +import { DesktopSettingsService } from "../platform/services/desktop-settings.service"; import { MenuUpdateRequest } from "./menu/menu.updater"; @@ -17,19 +18,16 @@ export class MessagingMain { constructor( private main: Main, private stateService: StateService, + private desktopSettingsService: DesktopSettingsService, ) {} - init() { + async init() { this.scheduleNextSync(); if (process.platform === "linux") { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.stateService.setOpenAtLogin(fs.existsSync(this.linuxStartupFile())); + await this.desktopSettingsService.setOpenAtLogin(fs.existsSync(this.linuxStartupFile())); } else { const loginSettings = app.getLoginItemSettings(); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.stateService.setOpenAtLogin(loginSettings.openAtLogin); + await this.desktopSettingsService.setOpenAtLogin(loginSettings.openAtLogin); } ipcMain.on("messagingService", async (event: any, message: any) => this.onMessage(message)); } diff --git a/apps/desktop/src/main/tray.main.ts b/apps/desktop/src/main/tray.main.ts index 3f8ad4eedf..948c48f519 100644 --- a/apps/desktop/src/main/tray.main.ts +++ b/apps/desktop/src/main/tray.main.ts @@ -1,9 +1,11 @@ import * as path from "path"; import { app, BrowserWindow, Menu, MenuItemConstructorOptions, nativeImage, Tray } from "electron"; +import { firstValueFrom } from "rxjs"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; + +import { DesktopSettingsService } from "../platform/services/desktop-settings.service"; import { WindowMain } from "./window.main"; @@ -18,7 +20,7 @@ export class TrayMain { constructor( private windowMain: WindowMain, private i18nService: I18nService, - private stateService: StateService, + private desktopSettingsService: DesktopSettingsService, ) { if (process.platform === "win32") { this.icon = path.join(__dirname, "/images/icon.ico"); @@ -54,14 +56,14 @@ export class TrayMain { } this.contextMenu = Menu.buildFromTemplate(menuItemOptions); - if (await this.stateService.getEnableTray()) { + if (await firstValueFrom(this.desktopSettingsService.trayEnabled$)) { this.showTray(); } } setupWindowListeners(win: BrowserWindow) { win.on("minimize", async (e: Event) => { - if (await this.stateService.getEnableMinimizeToTray()) { + if (await firstValueFrom(this.desktopSettingsService.minimizeToTray$)) { e.preventDefault(); // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises @@ -70,7 +72,7 @@ export class TrayMain { }); win.on("close", async (e: Event) => { - if (await this.stateService.getEnableCloseToTray()) { + if (await firstValueFrom(this.desktopSettingsService.closeToTray$)) { if (!this.windowMain.isQuitting) { e.preventDefault(); // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. @@ -81,7 +83,7 @@ export class TrayMain { }); win.on("show", async () => { - const enableTray = await this.stateService.getEnableTray(); + const enableTray = await firstValueFrom(this.desktopSettingsService.trayEnabled$); if (!enableTray) { setTimeout(() => this.removeTray(false), 100); } @@ -106,7 +108,7 @@ export class TrayMain { if (this.windowMain.win != null) { this.windowMain.win.hide(); } - if (this.isDarwin() && !(await this.stateService.getAlwaysShowDock())) { + if (this.isDarwin() && !(await firstValueFrom(this.desktopSettingsService.alwaysShowDock$))) { this.hideDock(); } } @@ -176,7 +178,7 @@ export class TrayMain { } if (this.windowMain.win.isVisible()) { this.windowMain.win.hide(); - if (this.isDarwin() && !(await this.stateService.getAlwaysShowDock())) { + if (this.isDarwin() && !(await firstValueFrom(this.desktopSettingsService.alwaysShowDock$))) { this.hideDock(); } } else { diff --git a/apps/desktop/src/main/window.main.ts b/apps/desktop/src/main/window.main.ts index 9771006c8a..64b4bc48d2 100644 --- a/apps/desktop/src/main/window.main.ts +++ b/apps/desktop/src/main/window.main.ts @@ -3,13 +3,15 @@ import * as path from "path"; import * as url from "url"; import { app, BrowserWindow, ipcMain, nativeTheme, screen, session } from "electron"; +import { firstValueFrom } from "rxjs"; -import { WindowState } from "@bitwarden/common/models/domain/window-state"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; +import { WindowState } from "../platform/models/domain/window-state"; +import { DesktopSettingsService } from "../platform/services/desktop-settings.service"; import { cleanUserAgent, isDev, @@ -40,6 +42,7 @@ export class WindowMain { private biometricStateService: BiometricStateService, private logService: LogService, private storageService: AbstractStorageService, + private desktopSettingsService: DesktopSettingsService, private argvCallback: (argv: string[]) => void = null, private createWindowCallback: (win: BrowserWindow) => void, ) {} @@ -121,7 +124,7 @@ export class WindowMain { app.on("activate", async () => { // On OS X it's common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. - if (this.win === null) { + if (this.win == null) { await this.createWindow(); } else { // Show the window when clicking on Dock icon @@ -141,7 +144,7 @@ export class WindowMain { this.defaultWidth, this.defaultHeight, ); - this.enableAlwaysOnTop = await this.stateService.getEnableAlwaysOnTop(); + this.enableAlwaysOnTop = await firstValueFrom(this.desktopSettingsService.alwaysOnTop$); this.session = session.fromPartition("persist:bitwarden", { cache: false }); @@ -265,7 +268,7 @@ export class WindowMain { async toggleAlwaysOnTop() { this.enableAlwaysOnTop = !this.win.isAlwaysOnTop(); this.win.setAlwaysOnTop(this.enableAlwaysOnTop); - await this.stateService.setEnableAlwaysOnTop(this.enableAlwaysOnTop); + await this.desktopSettingsService.setAlwaysOnTop(this.enableAlwaysOnTop); } private windowStateChangeHandler(configKey: string, win: BrowserWindow) { @@ -284,7 +287,7 @@ export class WindowMain { const bounds = win.getBounds(); if (this.windowStates[configKey] == null) { - this.windowStates[configKey] = await this.stateService.getWindow(); + this.windowStates[configKey] = await firstValueFrom(this.desktopSettingsService.window$); if (this.windowStates[configKey] == null) { this.windowStates[configKey] = {}; } @@ -304,14 +307,14 @@ export class WindowMain { this.windowStates[configKey].zoomFactor = win.webContents.zoomFactor; } - await this.stateService.setWindow(this.windowStates[configKey]); + await this.desktopSettingsService.setWindow(this.windowStates[configKey]); } catch (e) { this.logService.error(e); } } private async getWindowState(defaultWidth: number, defaultHeight: number) { - const state = await this.stateService.getWindow(); + const state = await firstValueFrom(this.desktopSettingsService.window$); const isValid = state != null && (this.stateHasBounds(state) || state.isMaximized); let displayBounds: Electron.Rectangle = null; diff --git a/libs/common/src/models/domain/window-state.ts b/apps/desktop/src/platform/models/domain/window-state.ts similarity index 89% rename from libs/common/src/models/domain/window-state.ts rename to apps/desktop/src/platform/models/domain/window-state.ts index 5bd8e86eff..aba1cdb470 100644 --- a/libs/common/src/models/domain/window-state.ts +++ b/apps/desktop/src/platform/models/domain/window-state.ts @@ -4,7 +4,7 @@ export class WindowState { isMaximized?: boolean; // TODO: displayBounds is an Electron.Rectangle. // We need to establish some kind of client-specific global state, similar to the way we already extend a base Account. - displayBounds: any; + displayBounds: Electron.Rectangle; x?: number; y?: number; zoomFactor?: number; diff --git a/apps/desktop/src/platform/services/desktop-settings.service.ts b/apps/desktop/src/platform/services/desktop-settings.service.ts index 791e5a2c5d..d967e5fb1d 100644 --- a/apps/desktop/src/platform/services/desktop-settings.service.ts +++ b/apps/desktop/src/platform/services/desktop-settings.service.ts @@ -1,4 +1,4 @@ -import { map } from "rxjs"; +import { Observable, map } from "rxjs"; import { DESKTOP_SETTINGS_DISK, @@ -6,6 +6,8 @@ import { StateProvider, } from "@bitwarden/common/platform/state"; +import { WindowState } from "../models/domain/window-state"; + export const HARDWARE_ACCELERATION = new KeyDefinition( DESKTOP_SETTINGS_DISK, "hardwareAcceleration", @@ -14,13 +16,165 @@ export const HARDWARE_ACCELERATION = new KeyDefinition( }, ); +const WINDOW_KEY = new KeyDefinition(DESKTOP_SETTINGS_DISK, "window", { + deserializer: (s) => s, +}); + +const CLOSE_TO_TRAY_KEY = new KeyDefinition(DESKTOP_SETTINGS_DISK, "closeToTray", { + deserializer: (b) => b, +}); + +const MINIMIZE_TO_TRAY_KEY = new KeyDefinition(DESKTOP_SETTINGS_DISK, "minimizeToTray", { + deserializer: (b) => b, +}); + +const START_TO_TRAY_KEY = new KeyDefinition(DESKTOP_SETTINGS_DISK, "startToTray", { + deserializer: (b) => b, +}); + +const TRAY_ENABLED_KEY = new KeyDefinition(DESKTOP_SETTINGS_DISK, "trayEnabled", { + deserializer: (b) => b, +}); + +const OPEN_AT_LOGIN_KEY = new KeyDefinition(DESKTOP_SETTINGS_DISK, "openAtLogin", { + deserializer: (b) => b, +}); + +const ALWAYS_SHOW_DOCK_KEY = new KeyDefinition(DESKTOP_SETTINGS_DISK, "alwaysShowDock", { + deserializer: (b) => b, +}); + +const ALWAYS_ON_TOP_KEY = new KeyDefinition(DESKTOP_SETTINGS_DISK, "alwaysOnTop", { + deserializer: (b) => b, +}); + +/** + * Various settings for controlling application behavior specific to the desktop client. + */ export class DesktopSettingsService { private hwState = this.stateProvider.getGlobal(HARDWARE_ACCELERATION); hardwareAcceleration$ = this.hwState.state$.pipe(map((v) => v ?? true)); - constructor(private stateProvider: StateProvider) {} + private readonly windowState = this.stateProvider.getGlobal(WINDOW_KEY); + + private readonly closeToTrayState = this.stateProvider.getGlobal(CLOSE_TO_TRAY_KEY); + /** + * Tha applications setting for whether or not to close the application into the system tray. + */ + closeToTray$ = this.closeToTrayState.state$.pipe(map((value) => value ?? false)); + + private readonly minimizeToTrayState = this.stateProvider.getGlobal(MINIMIZE_TO_TRAY_KEY); + /** + * The application setting for whether or not to minimize the applicaiton into the system tray. + */ + minimizeToTray$ = this.minimizeToTrayState.state$.pipe(map((value) => value ?? false)); + + private readonly startToTrayState = this.stateProvider.getGlobal(START_TO_TRAY_KEY); + /** + * The application setting for whether or not to start the application into the system tray. + */ + startToTray$ = this.startToTrayState.state$.pipe(map((value) => value ?? false)); + + private readonly trayEnabledState = this.stateProvider.getGlobal(TRAY_ENABLED_KEY); + /** + * Whether or not the system tray has been enabled. + */ + trayEnabled$ = this.trayEnabledState.state$.pipe(map((value) => value ?? false)); + + private readonly openAtLoginState = this.stateProvider.getGlobal(OPEN_AT_LOGIN_KEY); + /** + * The application setting for whether or not the application should open at system login. + */ + openAtLogin$ = this.openAtLoginState.state$.pipe(map((value) => value ?? false)); + + private readonly alwaysShowDockState = this.stateProvider.getGlobal(ALWAYS_SHOW_DOCK_KEY); + /** + * The application setting for whether or not the application should show up in the dock. + */ + alwaysShowDock$ = this.alwaysShowDockState.state$.pipe(map((value) => value ?? false)); + + private readonly alwaysOnTopState = this.stateProvider.getGlobal(ALWAYS_ON_TOP_KEY); + + alwaysOnTop$ = this.alwaysOnTopState.state$.pipe(map((value) => value ?? false)); + + constructor(private stateProvider: StateProvider) { + this.window$ = this.windowState.state$.pipe( + map((window) => + window != null && Object.keys(window).length > 0 ? window : new WindowState(), + ), + ); + } async setHardwareAcceleration(enabled: boolean) { await this.hwState.update(() => enabled); } + + /** + * The applications current window state. + */ + window$: Observable; + + /** + * Updates the window state of the application so that the application can reopen in the same place as it was closed from. + * @param windowState The window state to set. + */ + async setWindow(windowState: WindowState) { + await this.windowState.update(() => windowState); + } + + /** + * Sets the setting for whether or not the application should go into the system tray when closed. + * @param value `true` if the application should go into the system tray when closed, `false` if it should not. + */ + async setCloseToTray(value: boolean) { + await this.closeToTrayState.update(() => value); + } + + /** + * Sets the setting for whether or not the application should go into the tray when minimized. + * @param value `true` if the application should minimize into the system tray, `false` if it should not. + */ + async setMinimizeToTray(value: boolean) { + await this.minimizeToTrayState.update(() => value); + } + + /** + * Sets the setting for whether or not the application should be started into the system tray. + * @param value `true` if the application should be started to the tray`, `false` if it should not. + */ + async setStartToTray(value: boolean) { + await this.startToTrayState.update(() => value); + } + + /** + * Sets the setting for whether or not the application be shown in the system tray. + * @param value `true` if the application should show in the tray, `false` if it should not. + */ + async setTrayEnabled(value: boolean) { + await this.trayEnabledState.update(() => value); + } + + /** + * Sets the setting for whether or not the application should open at login of the computer. + * @param value `true` if the application should open at login, `false` if it should not. + */ + async setOpenAtLogin(value: boolean) { + await this.openAtLoginState.update(() => value); + } + + /** + * Sets the setting for whether or not the application should be shown in the dock. + * @param value `true` if the application should should in the dock, `false` if it should not. + */ + async setAlwaysShowDock(value: boolean) { + await this.alwaysShowDockState.update(() => value); + } + + /** + * Sets the setting for whether or not the application should stay on top of all other windows. + * @param value `true` if the application should stay on top, `false` if it should not. + */ + async setAlwaysOnTop(value: boolean) { + await this.alwaysOnTopState.update(() => value); + } } diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index e0228ee062..29a6752e65 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -4,7 +4,6 @@ import { AdminAuthRequestStorable } from "../../auth/models/domain/admin-auth-re import { ForceSetPasswordReason } from "../../auth/models/domain/force-set-password-reason"; import { KdfConfig } from "../../auth/models/domain/kdf-config"; import { BiometricKey } from "../../auth/types/biometric-key"; -import { WindowState } from "../../models/domain/window-state"; import { GeneratorOptions } from "../../tools/generator/generator-options"; import { GeneratedPasswordHistory, PasswordGeneratorOptions } from "../../tools/generator/password"; import { UsernameGeneratorOptions } from "../../tools/generator/username"; @@ -52,8 +51,6 @@ export abstract class StateService { getAddEditCipherInfo: (options?: StorageOptions) => Promise; setAddEditCipherInfo: (value: AddEditCipherInfo, options?: StorageOptions) => Promise; - getAlwaysShowDock: (options?: StorageOptions) => Promise; - setAlwaysShowDock: (value: boolean, options?: StorageOptions) => Promise; getBiometricFingerprintValidated: (options?: StorageOptions) => Promise; setBiometricFingerprintValidated: (value: boolean, options?: StorageOptions) => Promise; getConvertAccountToKeyConnector: (options?: StorageOptions) => Promise; @@ -184,8 +181,6 @@ export abstract class StateService { setEmail: (value: string, options?: StorageOptions) => Promise; getEmailVerified: (options?: StorageOptions) => Promise; setEmailVerified: (value: boolean, options?: StorageOptions) => Promise; - getEnableAlwaysOnTop: (options?: StorageOptions) => Promise; - setEnableAlwaysOnTop: (value: boolean, options?: StorageOptions) => Promise; getEnableBrowserIntegration: (options?: StorageOptions) => Promise; setEnableBrowserIntegration: (value: boolean, options?: StorageOptions) => Promise; getEnableBrowserIntegrationFingerprint: (options?: StorageOptions) => Promise; @@ -193,19 +188,11 @@ export abstract class StateService { value: boolean, options?: StorageOptions, ) => Promise; - getEnableCloseToTray: (options?: StorageOptions) => Promise; - setEnableCloseToTray: (value: boolean, options?: StorageOptions) => Promise; getEnableDuckDuckGoBrowserIntegration: (options?: StorageOptions) => Promise; setEnableDuckDuckGoBrowserIntegration: ( value: boolean, options?: StorageOptions, ) => Promise; - getEnableMinimizeToTray: (options?: StorageOptions) => Promise; - setEnableMinimizeToTray: (value: boolean, options?: StorageOptions) => Promise; - getEnableStartToTray: (options?: StorageOptions) => Promise; - setEnableStartToTray: (value: boolean, options?: StorageOptions) => Promise; - getEnableTray: (options?: StorageOptions) => Promise; - setEnableTray: (value: boolean, options?: StorageOptions) => Promise; getEncryptedCiphers: (options?: StorageOptions) => Promise<{ [id: string]: CipherData }>; setEncryptedCiphers: ( value: { [id: string]: CipherData }, @@ -261,12 +248,8 @@ export abstract class StateService { ) => Promise; getLocale: (options?: StorageOptions) => Promise; setLocale: (value: string, options?: StorageOptions) => Promise; - getMainWindowSize: (options?: StorageOptions) => Promise; - setMainWindowSize: (value: number, options?: StorageOptions) => Promise; getMinimizeOnCopyToClipboard: (options?: StorageOptions) => Promise; setMinimizeOnCopyToClipboard: (value: boolean, options?: StorageOptions) => Promise; - getOpenAtLogin: (options?: StorageOptions) => Promise; - setOpenAtLogin: (value: boolean, options?: StorageOptions) => Promise; getOrganizationInvitation: (options?: StorageOptions) => Promise; setOrganizationInvitation: (value: any, options?: StorageOptions) => Promise; getPasswordGenerationOptions: (options?: StorageOptions) => Promise; @@ -302,8 +285,6 @@ export abstract class StateService { setVaultTimeoutAction: (value: string, options?: StorageOptions) => Promise; getApproveLoginRequests: (options?: StorageOptions) => Promise; setApproveLoginRequests: (value: boolean, options?: StorageOptions) => Promise; - getWindow: () => Promise; - setWindow: (value: WindowState) => Promise; /** * @deprecated Do not call this directly, use ConfigService */ diff --git a/libs/common/src/platform/models/domain/account.ts b/libs/common/src/platform/models/domain/account.ts index c8d903cfea..2657467ae6 100644 --- a/libs/common/src/platform/models/domain/account.ts +++ b/libs/common/src/platform/models/domain/account.ts @@ -186,7 +186,6 @@ export class AccountProfile { export class AccountSettings { defaultUriMatch?: UriMatchStrategySetting; disableGa?: boolean; - enableAlwaysOnTop?: boolean; enableBiometric?: boolean; minimizeOnCopyToClipboard?: boolean; passwordGenerationOptions?: PasswordGeneratorOptions; diff --git a/libs/common/src/platform/models/domain/global-state.ts b/libs/common/src/platform/models/domain/global-state.ts index b27bac3bd4..b5a12d0555 100644 --- a/libs/common/src/platform/models/domain/global-state.ts +++ b/libs/common/src/platform/models/domain/global-state.ts @@ -1,26 +1,17 @@ -import { WindowState } from "../../../models/domain/window-state"; import { ThemeType } from "../../enums"; export class GlobalState { - enableAlwaysOnTop?: boolean; installedVersion?: string; locale?: string; organizationInvitation?: any; rememberedEmail?: string; theme?: ThemeType = ThemeType.System; - window?: WindowState = new WindowState(); twoFactorToken?: string; biometricFingerprintValidated?: boolean; vaultTimeout?: number; vaultTimeoutAction?: string; loginRedirect?: any; mainWindowSize?: number; - enableTray?: boolean; - enableMinimizeToTray?: boolean; - enableCloseToTray?: boolean; - enableStartToTray?: boolean; - openAtLogin?: boolean; - alwaysShowDock?: boolean; enableBrowserIntegration?: boolean; enableBrowserIntegrationFingerprint?: boolean; enableDuckDuckGoBrowserIntegration?: boolean; diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index fc548b562e..3fef978506 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -8,7 +8,6 @@ import { AdminAuthRequestStorable } from "../../auth/models/domain/admin-auth-re import { ForceSetPasswordReason } from "../../auth/models/domain/force-set-password-reason"; import { KdfConfig } from "../../auth/models/domain/kdf-config"; import { BiometricKey } from "../../auth/types/biometric-key"; -import { WindowState } from "../../models/domain/window-state"; import { GeneratorOptions } from "../../tools/generator/generator-options"; import { GeneratedPasswordHistory, PasswordGeneratorOptions } from "../../tools/generator/password"; import { UsernameGeneratorOptions } from "../../tools/generator/username"; @@ -277,24 +276,6 @@ export class StateService< ); } - async getAlwaysShowDock(options?: StorageOptions): Promise { - return ( - (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) - ?.alwaysShowDock ?? false - ); - } - - async setAlwaysShowDock(value: boolean, options?: StorageOptions): Promise { - const globals = await this.getGlobals( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - globals.alwaysShowDock = value; - await this.saveGlobals( - globals, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - async getBiometricFingerprintValidated(options?: StorageOptions): Promise { return ( (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) @@ -847,36 +828,6 @@ export class StateService< ); } - async getEnableAlwaysOnTop(options?: StorageOptions): Promise { - const accountPreference = ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.settings?.enableAlwaysOnTop; - const globalPreference = ( - await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.enableAlwaysOnTop; - return accountPreference ?? globalPreference ?? false; - } - - async setEnableAlwaysOnTop(value: boolean, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.settings.enableAlwaysOnTop = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - - const globals = await this.getGlobals( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - globals.enableAlwaysOnTop = value; - await this.saveGlobals( - globals, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - async getEnableBrowserIntegration(options?: StorageOptions): Promise { return ( (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) @@ -916,24 +867,6 @@ export class StateService< ); } - async getEnableCloseToTray(options?: StorageOptions): Promise { - return ( - (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) - ?.enableCloseToTray ?? false - ); - } - - async setEnableCloseToTray(value: boolean, options?: StorageOptions): Promise { - const globals = await this.getGlobals( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - globals.enableCloseToTray = value; - await this.saveGlobals( - globals, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - async getEnableDuckDuckGoBrowserIntegration(options?: StorageOptions): Promise { return ( (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) @@ -955,60 +888,6 @@ export class StateService< ); } - async getEnableMinimizeToTray(options?: StorageOptions): Promise { - return ( - (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) - ?.enableMinimizeToTray ?? false - ); - } - - async setEnableMinimizeToTray(value: boolean, options?: StorageOptions): Promise { - const globals = await this.getGlobals( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - globals.enableMinimizeToTray = value; - await this.saveGlobals( - globals, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getEnableStartToTray(options?: StorageOptions): Promise { - return ( - (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) - ?.enableStartToTray ?? false - ); - } - - async setEnableStartToTray(value: boolean, options?: StorageOptions): Promise { - const globals = await this.getGlobals( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - globals.enableStartToTray = value; - await this.saveGlobals( - globals, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getEnableTray(options?: StorageOptions): Promise { - return ( - (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) - ?.enableTray ?? false - ); - } - - async setEnableTray(value: boolean, options?: StorageOptions): Promise { - const globals = await this.getGlobals( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - globals.enableTray = value; - await this.saveGlobals( - globals, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - @withPrototypeForObjectValues(CipherData) async getEncryptedCiphers(options?: StorageOptions): Promise<{ [id: string]: CipherData }> { return ( @@ -1309,23 +1188,6 @@ export class StateService< ); } - async getMainWindowSize(options?: StorageOptions): Promise { - return ( - await this.getGlobals(this.reconcileOptions(options, await this.defaultInMemoryOptions())) - )?.mainWindowSize; - } - - async setMainWindowSize(value: number, options?: StorageOptions): Promise { - const globals = await this.getGlobals( - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - globals.mainWindowSize = value; - await this.saveGlobals( - globals, - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - } - async getMinimizeOnCopyToClipboard(options?: StorageOptions): Promise { return ( (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) @@ -1344,24 +1206,6 @@ export class StateService< ); } - async getOpenAtLogin(options?: StorageOptions): Promise { - return ( - (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) - ?.openAtLogin ?? false - ); - } - - async setOpenAtLogin(value: boolean, options?: StorageOptions): Promise { - const globals = await this.getGlobals( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - globals.openAtLogin = value; - await this.saveGlobals( - globals, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - async getOrganizationInvitation(options?: StorageOptions): Promise { return ( await this.getGlobals(this.reconcileOptions(options, await this.defaultInMemoryOptions())) @@ -1571,24 +1415,6 @@ export class StateService< ); } - async getWindow(): Promise { - const globals = await this.getGlobals(await this.defaultOnDiskOptions()); - return globals?.window != null && Object.keys(globals.window).length > 0 - ? globals.window - : new WindowState(); - } - - async setWindow(value: WindowState, options?: StorageOptions): Promise { - const globals = await this.getGlobals( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - globals.window = value; - return await this.saveGlobals( - globals, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - async setServerConfig(value: ServerConfigData, options?: StorageOptions): Promise { const account = await this.getAccount( this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), diff --git a/libs/common/src/state-migrations/migrate.ts b/libs/common/src/state-migrations/migrate.ts index 35a53c2341..3c03854780 100644 --- a/libs/common/src/state-migrations/migrate.ts +++ b/libs/common/src/state-migrations/migrate.ts @@ -42,6 +42,7 @@ import { AutoConfirmFingerPrintsMigrator } from "./migrations/43-move-auto-confi import { UserDecryptionOptionsMigrator } from "./migrations/44-move-user-decryption-options-to-state-provider"; import { MergeEnvironmentState } from "./migrations/45-merge-environment-state"; import { DeleteBiometricPromptCancelledData } from "./migrations/46-delete-orphaned-biometric-prompt-data"; +import { MoveDesktopSettingsMigrator } from "./migrations/47-move-desktop-settings"; import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys"; import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key"; import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account"; @@ -50,7 +51,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting import { MinVersionMigrator } from "./migrations/min-version"; export const MIN_VERSION = 3; -export const CURRENT_VERSION = 46; +export const CURRENT_VERSION = 47; export type MinVersion = typeof MIN_VERSION; export function createMigrationBuilder() { @@ -98,7 +99,8 @@ export function createMigrationBuilder() { .with(AutoConfirmFingerPrintsMigrator, 42, 43) .with(UserDecryptionOptionsMigrator, 43, 44) .with(MergeEnvironmentState, 44, 45) - .with(DeleteBiometricPromptCancelledData, 45, CURRENT_VERSION); + .with(DeleteBiometricPromptCancelledData, 45, 46) + .with(MoveDesktopSettingsMigrator, 46, CURRENT_VERSION); } export async function currentVersion( diff --git a/libs/common/src/state-migrations/migration-helper.spec.ts b/libs/common/src/state-migrations/migration-helper.spec.ts index 3bcf99b2b6..e929877b63 100644 --- a/libs/common/src/state-migrations/migration-helper.spec.ts +++ b/libs/common/src/state-migrations/migration-helper.spec.ts @@ -178,12 +178,9 @@ export function mockMigrationHelper( return mockHelper; } -// TODO: Use const generic for TUsers in TypeScript 5.0 so consumers don't have to `as const` themselves export type InitialDataHint = { /** * A string array of the users id who are authenticated - * - * NOTE: It's recommended to as const this string array so you get type help defining the users data */ authenticatedAccounts?: TUsers; /** @@ -282,10 +279,9 @@ function expectInjectedData( * @param initalData The data to start with * @returns State after your migration has ran. */ -// TODO: Use const generic for TUsers in TypeScript 5.0 so consumers don't have to `as const` themselves export async function runMigrator< TMigrator extends Migrator, - TUsers extends readonly string[] = string[], + const TUsers extends readonly string[], >( migrator: TMigrator, initalData?: InitialDataHint, diff --git a/libs/common/src/state-migrations/migrations/47-move-desktop-settings.spec.ts b/libs/common/src/state-migrations/migrations/47-move-desktop-settings.spec.ts new file mode 100644 index 0000000000..41080d024e --- /dev/null +++ b/libs/common/src/state-migrations/migrations/47-move-desktop-settings.spec.ts @@ -0,0 +1,116 @@ +import { runMigrator } from "../migration-helper.spec"; + +import { MoveDesktopSettingsMigrator } from "./47-move-desktop-settings"; + +describe("MoveDesktopSettings", () => { + const sut = new MoveDesktopSettingsMigrator(46, 47); + + it("can migrate truthy values", async () => { + const output = await runMigrator(sut, { + authenticatedAccounts: ["user1"], + global: { + window: { + width: 400, + height: 400, + displayBounds: { + height: 200, + width: 200, + x: 200, + y: 200, + }, + }, + enableAlwaysOnTop: true, + enableCloseToTray: true, + enableMinimizeToTray: true, + enableStartToTray: true, + enableTray: true, + openAtLogin: true, + alwaysShowDock: true, + }, + user1: { + settings: { + enableAlwaysOnTop: true, + }, + }, + }); + + expect(output).toEqual({ + authenticatedAccounts: ["user1"], + global: {}, + global_desktopSettings_window: { + width: 400, + height: 400, + displayBounds: { + height: 200, + width: 200, + x: 200, + y: 200, + }, + }, + global_desktopSettings_closeToTray: true, + global_desktopSettings_minimizeToTray: true, + global_desktopSettings_startToTray: true, + global_desktopSettings_trayEnabled: true, + global_desktopSettings_openAtLogin: true, + global_desktopSettings_alwaysShowDock: true, + global_desktopSettings_alwaysOnTop: true, + user1: { + settings: {}, + }, + }); + }); + + it("can migrate falsey values", async () => { + const output = await runMigrator(sut, { + authenticatedAccounts: ["user1"], + global: { + window: null, + enableCloseToTray: false, + enableMinimizeToTray: false, + enableStartToTray: false, + enableTray: false, + openAtLogin: false, + alwaysShowDock: false, + enableAlwaysOnTop: false, + }, + user1: { + settings: { + enableAlwaysOnTop: false, + }, + }, + }); + + expect(output).toEqual({ + authenticatedAccounts: ["user1"], + global: {}, + global_desktopSettings_window: null, + global_desktopSettings_closeToTray: false, + global_desktopSettings_minimizeToTray: false, + global_desktopSettings_startToTray: false, + global_desktopSettings_trayEnabled: false, + global_desktopSettings_openAtLogin: false, + global_desktopSettings_alwaysShowDock: false, + global_desktopSettings_alwaysOnTop: false, + user1: { + settings: {}, + }, + }); + }); + + it("can migrate even if none of our values are found", async () => { + // + const output = await runMigrator(sut, { + authenticatedAccounts: ["user1"] as const, + global: { + anotherSetting: "", + }, + }); + + expect(output).toEqual({ + authenticatedAccounts: ["user1"] as const, + global: { + anotherSetting: "", + }, + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/47-move-desktop-settings.ts b/libs/common/src/state-migrations/migrations/47-move-desktop-settings.ts new file mode 100644 index 0000000000..f6f3ebdfc2 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/47-move-desktop-settings.ts @@ -0,0 +1,128 @@ +import { KeyDefinitionLike, MigrationHelper, StateDefinitionLike } from "../migration-helper"; +import { IRREVERSIBLE, Migrator } from "../migrator"; + +type ExpectedGlobalType = { + window?: object; + enableTray?: boolean; + enableMinimizeToTray?: boolean; + enableCloseToTray?: boolean; + enableStartToTray?: boolean; + openAtLogin?: boolean; + alwaysShowDock?: boolean; + enableAlwaysOnTop?: boolean; +}; + +type ExpectedAccountType = { + settings?: { + enableAlwaysOnTop?: boolean; + }; +}; + +const DESKTOP_SETTINGS_STATE: StateDefinitionLike = { name: "desktopSettings" }; + +const WINDOW_KEY: KeyDefinitionLike = { key: "window", stateDefinition: DESKTOP_SETTINGS_STATE }; + +const CLOSE_TO_TRAY_KEY: KeyDefinitionLike = { + key: "closeToTray", + stateDefinition: DESKTOP_SETTINGS_STATE, +}; +const MINIMIZE_TO_TRAY_KEY: KeyDefinitionLike = { + key: "minimizeToTray", + stateDefinition: DESKTOP_SETTINGS_STATE, +}; +const START_TO_TRAY_KEY: KeyDefinitionLike = { + key: "startToTray", + stateDefinition: DESKTOP_SETTINGS_STATE, +}; +const TRAY_ENABLED_KEY: KeyDefinitionLike = { + key: "trayEnabled", + stateDefinition: DESKTOP_SETTINGS_STATE, +}; +const OPEN_AT_LOGIN_KEY: KeyDefinitionLike = { + key: "openAtLogin", + stateDefinition: DESKTOP_SETTINGS_STATE, +}; +const ALWAYS_SHOW_DOCK_KEY: KeyDefinitionLike = { + key: "alwaysShowDock", + stateDefinition: DESKTOP_SETTINGS_STATE, +}; + +const ALWAYS_ON_TOP_KEY: KeyDefinitionLike = { + key: "alwaysOnTop", + stateDefinition: DESKTOP_SETTINGS_STATE, +}; + +export class MoveDesktopSettingsMigrator extends Migrator<46, 47> { + async migrate(helper: MigrationHelper): Promise { + const legacyGlobal = await helper.get("global"); + + let updatedGlobal = false; + if (legacyGlobal?.window !== undefined) { + await helper.setToGlobal(WINDOW_KEY, legacyGlobal.window); + updatedGlobal = true; + delete legacyGlobal.window; + } + + if (legacyGlobal?.enableCloseToTray != null) { + await helper.setToGlobal(CLOSE_TO_TRAY_KEY, legacyGlobal.enableCloseToTray); + updatedGlobal = true; + delete legacyGlobal.enableCloseToTray; + } + + if (legacyGlobal?.enableMinimizeToTray != null) { + await helper.setToGlobal(MINIMIZE_TO_TRAY_KEY, legacyGlobal.enableMinimizeToTray); + updatedGlobal = true; + delete legacyGlobal.enableMinimizeToTray; + } + + if (legacyGlobal?.enableStartToTray != null) { + await helper.setToGlobal(START_TO_TRAY_KEY, legacyGlobal.enableStartToTray); + updatedGlobal = true; + delete legacyGlobal.enableStartToTray; + } + + if (legacyGlobal?.enableTray != null) { + await helper.setToGlobal(TRAY_ENABLED_KEY, legacyGlobal.enableTray); + updatedGlobal = true; + delete legacyGlobal.enableTray; + } + + if (legacyGlobal?.openAtLogin != null) { + await helper.setToGlobal(OPEN_AT_LOGIN_KEY, legacyGlobal.openAtLogin); + updatedGlobal = true; + delete legacyGlobal.openAtLogin; + } + + if (legacyGlobal?.alwaysShowDock != null) { + await helper.setToGlobal(ALWAYS_SHOW_DOCK_KEY, legacyGlobal.alwaysShowDock); + updatedGlobal = true; + delete legacyGlobal.alwaysShowDock; + } + + if (legacyGlobal?.enableAlwaysOnTop != null) { + await helper.setToGlobal(ALWAYS_ON_TOP_KEY, legacyGlobal.enableAlwaysOnTop); + updatedGlobal = true; + delete legacyGlobal.enableAlwaysOnTop; + } + + if (updatedGlobal) { + await helper.set("global", legacyGlobal); + } + + async function migrateAccount(userId: string, account: ExpectedAccountType) { + // We only migrate the global setting for this, if we find it on the account object + // just delete it. + if (account?.settings?.enableAlwaysOnTop != null) { + delete account.settings.enableAlwaysOnTop; + await helper.set(userId, account); + } + } + + const accounts = await helper.getAccounts(); + await Promise.all(accounts.map(({ userId, account }) => migrateAccount(userId, account))); + } + + rollback(helper: MigrationHelper): Promise { + throw IRREVERSIBLE; + } +}