[PM-38565] Replace tray settings with "Keep Bitwarden running in the background" (#21044)

* Implement new settings behavior

* Copy changes

* Migrate setting

* Fix build

* Fix
This commit is contained in:
Bernd Schoolmann 2026-06-16 01:17:52 +09:00 committed by GitHub
parent a2c9e34784
commit dde73675e8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 262 additions and 441 deletions

View File

@ -100,34 +100,15 @@
</bit-tab>
<bit-tab [label]="'appPreferences' | i18n">
<bit-form-control>
<input type="checkbox" bitCheckbox formControlName="enableTray" (change)="saveTray()" />
<bit-label>{{ enableTrayText }}</bit-label>
<bit-hint>{{ enableTrayDescText }}</bit-hint>
</bit-form-control>
@if (showMinToTray) {
<bit-form-control>
<input
type="checkbox"
bitCheckbox
formControlName="enableMinToTray"
(change)="saveMinToTray()"
/>
<bit-label>{{ enableMinToTrayText }}</bit-label>
<bit-hint>{{ enableMinToTrayDescText }}</bit-hint>
</bit-form-control>
}
<bit-form-control>
<input
type="checkbox"
bitCheckbox
formControlName="enableCloseToTray"
(change)="saveCloseToTray()"
formControlName="runInBackground"
(change)="saveRunInBackground()"
/>
<bit-label>{{ enableCloseToTrayText }}</bit-label>
<bit-hint>{{ enableCloseToTrayDescText }}</bit-hint>
<bit-label>{{ runInBackgroundText }}</bit-label>
<bit-hint>{{ runInBackgroundDescText }}</bit-hint>
</bit-form-control>
@if (showOpenAtLoginOption) {
@ -143,19 +124,6 @@
</bit-form-control>
}
@if (showAlwaysShowDock) {
<bit-form-control>
<input
type="checkbox"
bitCheckbox
formControlName="alwaysShowDock"
(change)="saveAlwaysShowDock()"
/>
<bit-label>{{ "alwaysShowDock" | i18n }}</bit-label>
<bit-hint>{{ "alwaysShowDockDesc" | i18n }}</bit-hint>
</bit-form-control>
}
<bit-form-control>
<input
type="checkbox"

View File

@ -190,11 +190,8 @@ describe("SettingsDialogComponent", () => {
biometricStateService.promptAutomatically$ = of(false);
autofillSettingsServiceAbstraction.clearClipboardDelay$ = of(null);
desktopSettingsService.minimizeOnCopy$ = of(false);
desktopSettingsService.trayEnabled$ = of(false);
desktopSettingsService.minimizeToTray$ = of(false);
desktopSettingsService.closeToTray$ = of(false);
desktopSettingsService.runInBackground$ = of(false);
desktopSettingsService.openAtLogin$ = of(false);
desktopSettingsService.alwaysShowDock$ = of(false);
desktopSettingsService.browserIntegrationEnabled$ = of(false);
desktopSettingsService.hardwareAcceleration$ = of(false);
desktopSettingsService.sshAgentEnabled$ = of(false);

View File

@ -136,20 +136,13 @@ export class SettingsDialogComponent implements OnInit {
protected readonly isWindows: boolean;
protected readonly isLinux: boolean;
protected readonly isMac: boolean;
protected readonly requireEnableTray: boolean;
protected readonly showOpenAtLoginOption: boolean;
protected readonly showDuckDuckGoIntegrationOption: boolean;
protected readonly enableTrayText: string;
protected readonly enableTrayDescText: string;
protected readonly enableMinToTrayText: string;
protected readonly enableMinToTrayDescText: string;
protected readonly enableCloseToTrayText: string;
protected readonly enableCloseToTrayDescText: string;
protected readonly runInBackgroundText: string;
protected readonly runInBackgroundDescText: string;
protected readonly supportsBiometric = signal(false);
protected readonly showEnableAutotype = signal(false);
protected readonly showMinToTray: boolean;
protected readonly showAlwaysShowDock: boolean;
private readonly activeAccount = toSignal(this.accountService.activeAccount$, {
requireSync: true,
});
@ -182,11 +175,8 @@ export class SettingsDialogComponent implements OnInit {
minimizeOnCopyToClipboard: false,
enableFavicons: false,
// App Settings
enableTray: false,
enableMinToTray: false,
enableCloseToTray: false,
runInBackground: false,
openAtLogin: false,
alwaysShowDock: false,
enableBrowserIntegration: false,
enableHardwareAcceleration: true,
enableSshAgent: false,
@ -206,23 +196,11 @@ export class SettingsDialogComponent implements OnInit {
this.isMac = this.platformUtilsService.getDevice() === DeviceType.MacOsDesktop;
this.isLinux = this.platformUtilsService.getDevice() === DeviceType.LinuxDesktop;
this.isWindows = this.platformUtilsService.getDevice() === DeviceType.WindowsDesktop;
this.showMinToTray = !this.isLinux;
this.showAlwaysShowDock = this.isMac;
// Workaround to avoid ghosting trays https://github.com/electron/electron/issues/17622
this.requireEnableTray = this.platformUtilsService.getDevice() === DeviceType.LinuxDesktop;
const trayKey = this.isMac ? "enableMenuBar" : "enableTray";
this.enableTrayText = this.i18nService.t(trayKey);
this.enableTrayDescText = this.i18nService.t(trayKey + "Desc");
const minToTrayKey = this.isMac ? "enableMinToMenuBar" : "enableMinToTray";
this.enableMinToTrayText = this.i18nService.t(minToTrayKey);
this.enableMinToTrayDescText = this.i18nService.t(minToTrayKey + "Desc");
const closeToTrayKey = this.isMac ? "enableCloseToMenuBar" : "enableCloseToTray";
this.enableCloseToTrayText = this.i18nService.t(closeToTrayKey);
this.enableCloseToTrayDescText = this.i18nService.t(closeToTrayKey + "Desc");
this.runInBackgroundText = this.i18nService.t("runInBackground");
this.runInBackgroundDescText = this.i18nService.t(
this.isMac ? "runInBackgroundDescMac" : "runInBackgroundDesc",
);
this.showOpenAtLoginOption = this.showAutostartSetting();
@ -294,11 +272,8 @@ export class SettingsDialogComponent implements OnInit {
clearClipboard: await firstValueFrom(this.autofillSettingsService.clearClipboardDelay$),
minimizeOnCopyToClipboard: await firstValueFrom(this.desktopSettingsService.minimizeOnCopy$),
enableFavicons: await firstValueFrom(this.domainSettingsService.showFavicons$),
enableTray: await firstValueFrom(this.desktopSettingsService.trayEnabled$),
enableMinToTray: await firstValueFrom(this.desktopSettingsService.minimizeToTray$),
enableCloseToTray: await firstValueFrom(this.desktopSettingsService.closeToTray$),
runInBackground: await firstValueFrom(this.desktopSettingsService.runInBackground$),
openAtLogin: await firstValueFrom(this.desktopSettingsService.openAtLogin$),
alwaysShowDock: await firstValueFrom(this.desktopSettingsService.alwaysShowDock$),
enableBrowserIntegration: await firstValueFrom(
this.desktopSettingsService.browserIntegrationEnabled$,
),
@ -556,44 +531,8 @@ export class SettingsDialogComponent implements OnInit {
this.messagingService.send("refreshCiphers");
}
protected async saveMinToTray() {
await this.desktopSettingsService.setMinimizeToTray(this.form.value.enableMinToTray);
}
protected async saveCloseToTray() {
if (this.requireEnableTray) {
this.form.controls.enableTray.setValue(true);
await this.desktopSettingsService.setTrayEnabled(this.form.value.enableTray);
}
await this.desktopSettingsService.setCloseToTray(this.form.value.enableCloseToTray);
}
protected async saveTray() {
if (
this.requireEnableTray &&
!this.form.value.enableTray &&
this.form.value.enableCloseToTray
) {
const confirm = await this.dialogService.openSimpleDialog({
title: { key: "confirmTrayTitle" },
content: { key: "confirmTrayDesc" },
type: "warning",
});
if (confirm) {
this.form.controls.enableCloseToTray.setValue(false, { emitEvent: false });
await this.desktopSettingsService.setCloseToTray(this.form.value.enableCloseToTray);
} else {
this.form.controls.enableTray.setValue(true);
}
return;
}
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");
protected async saveRunInBackground() {
await this.desktopSettingsService.setRunInBackground(this.form.value.runInBackground);
}
private async saveLocale(newValue: string) {
@ -615,10 +554,6 @@ export class SettingsDialogComponent implements OnInit {
await this.autofillSettingsService.setClearClipboardDelay(newValue);
}
protected async saveAlwaysShowDock() {
await this.desktopSettingsService.setAlwaysShowDock(this.form.value.alwaysShowDock);
}
private showAutostartSetting(): boolean {
// Windows store does not support autostart
// Dev mode should not show auto-start, because it would result in an empty electron window starting on login

View File

@ -183,51 +183,19 @@
<ng-container *ngIf="showAppPreferences">
<div class="form-group">
<div class="checkbox">
<label for="enableTray">
<label for="runInBackground">
<input
id="enableTray"
id="runInBackground"
type="checkbox"
aria-describedby="enableTrayHelp"
formControlName="enableTray"
(change)="saveTray()"
aria-describedby="runInBackgroundHelp"
formControlName="runInBackground"
(change)="saveRunInBackground()"
/>
{{ enableTrayText }}
{{ runInBackgroundText }}
</label>
</div>
<small id="enableTrayHelp" class="help-block">{{ enableTrayDescText }}</small>
</div>
<div class="form-group" *ngIf="showMinToTray">
<div class="checkbox">
<label for="enableMinToTray">
<input
id="enableMinToTray"
type="checkbox"
aria-describedby="enableMinToTrayHelp"
formControlName="enableMinToTray"
(change)="saveMinToTray()"
/>
{{ enableMinToTrayText }}
</label>
</div>
<small id="enableMinToTrayHelp" class="help-block">{{
enableMinToTrayDescText
}}</small>
</div>
<div class="form-group">
<div class="checkbox">
<label for="enableCloseToTray">
<input
id="enableCloseToTray"
type="checkbox"
aria-describedby="enableCloseToTrayHelp"
formControlName="enableCloseToTray"
(change)="saveCloseToTray()"
/>
{{ enableCloseToTrayText }}
</label>
</div>
<small id="enableCloseToTrayHelp" class="help-block">{{
enableCloseToTrayDescText
<small id="runInBackgroundHelp" class="help-block">{{
runInBackgroundDescText
}}</small>
</div>
<div class="form-group" *ngIf="showOpenAtLoginOption">
@ -247,23 +215,6 @@
"openAtLoginDesc" | i18n
}}</small>
</div>
<div class="form-group" *ngIf="showAlwaysShowDock">
<div class="checkbox">
<label for="alwaysShowDock">
<input
id="alwaysShowDock"
type="checkbox"
aria-describedby="alwaysShowDockHelp"
formControlName="alwaysShowDock"
(change)="saveAlwaysShowDock()"
/>
{{ "alwaysShowDock" | i18n }}
</label>
</div>
<small id="alwaysShowDockHelp" class="help-block">{{
"alwaysShowDockDesc" | i18n
}}</small>
</div>
<div class="form-group" *ngIf="showEnableAutotype">
<div class="checkbox">
<label for="enableAutotype">

View File

@ -171,11 +171,8 @@ describe("SettingsComponent", () => {
biometricStateService.promptAutomatically$ = of(false);
autofillSettingsServiceAbstraction.clearClipboardDelay$ = of(null);
desktopSettingsService.minimizeOnCopy$ = of(false);
desktopSettingsService.trayEnabled$ = of(false);
desktopSettingsService.minimizeToTray$ = of(false);
desktopSettingsService.closeToTray$ = of(false);
desktopSettingsService.runInBackground$ = of(false);
desktopSettingsService.openAtLogin$ = of(false);
desktopSettingsService.alwaysShowDock$ = of(false);
desktopSettingsService.browserIntegrationEnabled$ = of(false);
desktopSettingsService.hardwareAcceleration$ = of(false);
desktopSettingsService.sshAgentEnabled$ = of(false);

View File

@ -99,15 +99,12 @@ import { NativeMessagingManifestService } from "../services/native-messaging-man
],
})
export class SettingsComponent implements OnInit, OnDestroy {
showMinToTray = false;
localeOptions: any[];
themeOptions: any[];
clearClipboardOptions: any[];
sshAgentPromptBehaviorOptions: any[];
supportsBiometric: boolean;
private timerId: any;
showAlwaysShowDock = false;
requireEnableTray = false;
showDuckDuckGoIntegrationOption = false;
showEnableAutotype = false;
autotypeShortcut: string;
@ -116,12 +113,8 @@ export class SettingsComponent implements OnInit, OnDestroy {
isLinux: boolean;
isMac: boolean;
enableTrayText: string;
enableTrayDescText: string;
enableMinToTrayText: string;
enableMinToTrayDescText: string;
enableCloseToTrayText: string;
enableCloseToTrayDescText: string;
runInBackgroundText: string;
runInBackgroundDescText: string;
showSecurity = true;
showAccountPreferences = true;
@ -146,11 +139,8 @@ export class SettingsComponent implements OnInit, OnDestroy {
minimizeOnCopyToClipboard: false,
enableFavicons: false,
// App Settings
enableTray: false,
enableMinToTray: false,
enableCloseToTray: false,
runInBackground: false,
openAtLogin: false,
alwaysShowDock: false,
enableHardwareAcceleration: true,
enableSshAgent: false,
sshAgentPromptBehavior: SshAgentPromptType.Always,
@ -199,20 +189,10 @@ export class SettingsComponent implements OnInit, OnDestroy {
this.isLinux = this.platformUtilsService.getDevice() === DeviceType.LinuxDesktop;
this.isWindows = this.platformUtilsService.getDevice() === DeviceType.WindowsDesktop;
// Workaround to avoid ghosting trays https://github.com/electron/electron/issues/17622
this.requireEnableTray = this.platformUtilsService.getDevice() === DeviceType.LinuxDesktop;
const trayKey = this.isMac ? "enableMenuBar" : "enableTray";
this.enableTrayText = this.i18nService.t(trayKey);
this.enableTrayDescText = this.i18nService.t(trayKey + "Desc");
const minToTrayKey = this.isMac ? "enableMinToMenuBar" : "enableMinToTray";
this.enableMinToTrayText = this.i18nService.t(minToTrayKey);
this.enableMinToTrayDescText = this.i18nService.t(minToTrayKey + "Desc");
const closeToTrayKey = this.isMac ? "enableCloseToMenuBar" : "enableCloseToTray";
this.enableCloseToTrayText = this.i18nService.t(closeToTrayKey);
this.enableCloseToTrayDescText = this.i18nService.t(closeToTrayKey + "Desc");
this.runInBackgroundText = this.i18nService.t("runInBackground");
this.runInBackgroundDescText = this.i18nService.t(
this.isMac ? "runInBackgroundDescMac" : "runInBackgroundDesc",
);
this.showOpenAtLoginOption = this.showAutostartSetting();
@ -302,11 +282,8 @@ export class SettingsComponent implements OnInit, OnDestroy {
clearClipboard: await firstValueFrom(this.autofillSettingsService.clearClipboardDelay$),
minimizeOnCopyToClipboard: await firstValueFrom(this.desktopSettingsService.minimizeOnCopy$),
enableFavicons: await firstValueFrom(this.domainSettingsService.showFavicons$),
enableTray: await firstValueFrom(this.desktopSettingsService.trayEnabled$),
enableMinToTray: await firstValueFrom(this.desktopSettingsService.minimizeToTray$),
enableCloseToTray: await firstValueFrom(this.desktopSettingsService.closeToTray$),
runInBackground: await firstValueFrom(this.desktopSettingsService.runInBackground$),
openAtLogin: await firstValueFrom(this.desktopSettingsService.openAtLogin$),
alwaysShowDock: await firstValueFrom(this.desktopSettingsService.alwaysShowDock$),
enableDuckDuckGoBrowserIntegration: await firstValueFrom(
this.desktopAutofillSettingsService.enableDuckDuckGoBrowserIntegration$,
),
@ -327,10 +304,6 @@ export class SettingsComponent implements OnInit, OnDestroy {
};
this.form.setValue(initialValues, { emitEvent: false });
// Non-form values
this.showMinToTray = this.platformUtilsService.getDevice() !== DeviceType.LinuxDesktop;
this.showAlwaysShowDock = this.platformUtilsService.getDevice() === DeviceType.MacOsDesktop;
if (isWindows) {
this.billingAccountProfileStateService
.hasPremiumFromAnySource$(activeAccount.id)
@ -535,44 +508,8 @@ export class SettingsComponent implements OnInit, OnDestroy {
this.messagingService.send("refreshCiphers");
}
async saveMinToTray() {
await this.desktopSettingsService.setMinimizeToTray(this.form.value.enableMinToTray);
}
async saveCloseToTray() {
if (this.requireEnableTray) {
this.form.controls.enableTray.setValue(true);
await this.desktopSettingsService.setTrayEnabled(this.form.value.enableTray);
}
await this.desktopSettingsService.setCloseToTray(this.form.value.enableCloseToTray);
}
async saveTray() {
if (
this.requireEnableTray &&
!this.form.value.enableTray &&
this.form.value.enableCloseToTray
) {
const confirm = await this.dialogService.openSimpleDialog({
title: { key: "confirmTrayTitle" },
content: { key: "confirmTrayDesc" },
type: "warning",
});
if (confirm) {
this.form.controls.enableCloseToTray.setValue(false, { emitEvent: false });
await this.desktopSettingsService.setCloseToTray(this.form.value.enableCloseToTray);
} else {
this.form.controls.enableTray.setValue(true);
}
return;
}
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 saveRunInBackground() {
await this.desktopSettingsService.setRunInBackground(this.form.value.runInBackground);
}
async saveLocale() {
@ -594,10 +531,6 @@ export class SettingsComponent implements OnInit, OnDestroy {
await this.autofillSettingsService.setClearClipboardDelay(this.form.value.clearClipboard);
}
async saveAlwaysShowDock() {
await this.desktopSettingsService.setAlwaysShowDock(this.form.value.alwaysShowDock);
}
private showAutostartSetting(): boolean {
// Windows store does not support autostart
// Dev mode should not show auto-start, because it would result in an empty electron window starting on login

View File

@ -1680,35 +1680,14 @@
"showIconsChangePasswordUrls": {
"message": "Show website icons and retrieve change password URLs"
},
"enableMinToTray": {
"message": "Minimize to tray icon"
"runInBackground": {
"message": "Keep Bitwarden running in the background"
},
"enableMinToTrayDesc": {
"message": "When minimizing the window, show an icon in the system tray instead."
"runInBackgroundDesc": {
"message": "When you close the window, Bitwarden is accessible from the tray. Recommended when using the SSH agent or browser integration."
},
"enableMinToMenuBar": {
"message": "Minimize to menu bar"
},
"enableMinToMenuBarDesc": {
"message": "When minimizing the window, show an icon in the menu bar instead."
},
"enableCloseToTray": {
"message": "Close to tray icon"
},
"enableCloseToTrayDesc": {
"message": "When closing the window, show an icon in the system tray instead."
},
"enableCloseToMenuBar": {
"message": "Close to menu bar"
},
"enableCloseToMenuBarDesc": {
"message": "When closing the window, show an icon in the menu bar instead."
},
"enableTray": {
"message": "Show tray icon"
},
"enableTrayDesc": {
"message": "Always show an icon in the system tray."
"runInBackgroundDescMac": {
"message": "When you close the window, Bitwarden is accessible from the menu bar. Recommended when using the SSH agent or browser integration."
},
"openAtLogin": {
"message": "Start automatically on login"
@ -1716,18 +1695,6 @@
"openAtLoginDesc": {
"message": "Start the Bitwarden desktop application automatically on login."
},
"alwaysShowDock": {
"message": "Always show in the Dock"
},
"alwaysShowDockDesc": {
"message": "Show the Bitwarden icon in the Dock even when minimized to the menu bar."
},
"confirmTrayTitle": {
"message": "Confirm hiding tray"
},
"confirmTrayDesc": {
"message": "Turning off this setting will also turn off all other tray related settings."
},
"language": {
"message": "Language"
},
@ -2129,9 +2096,9 @@
"exit": {
"message": "Exit"
},
"showHide": {
"message": "Show / Hide",
"description": "Text for a button that toggles the visibility of the window. Shows the window when it is hidden or hides the window if it is currently open."
"show": {
"message": "Show",
"description": "Tray menu item that brings the Bitwarden window to the foreground."
},
"hideToTray": {
"message": "Hide to tray"
@ -2335,12 +2302,6 @@
"preferences": {
"message": "Preferences"
},
"enableMenuBar": {
"message": "Show menu bar icon"
},
"enableMenuBarDesc": {
"message": "Always show an icon in the menu bar."
},
"hideToMenuBar": {
"message": "Hide to menu bar"
},

View File

@ -238,6 +238,7 @@ export class Main {
this.shell,
(arg) => this.processDeepLink(arg),
(win) => this.trayMain.setupWindowListeners(win),
() => this.trayMain.restoreFromTray(),
);
this.biometricsService = new MainBiometricsService(
@ -388,9 +389,11 @@ export class Main {
},
]);
// Autostart should always start to tray. Any auto-start mechanism must provide this flag.
if (isAutostart) {
await this.trayMain.hideToTray();
// Autostart starts to tray. Any auto-start mechanism must provide this flag.
// Only hide to tray when running in the background is enabled; otherwise there
// would be a hidden window with no tray icon to bring it back.
if (isAutostart && (await firstValueFrom(this.desktopSettingsService.runInBackground$))) {
this.trayMain.hideToTray();
}
this.powerMonitorMain.init();

View File

@ -68,15 +68,7 @@ export class MessagingMain {
}
}
break;
case "showTray":
this.main.trayMain.showTray();
break;
case "removeTray":
this.main.trayMain.removeTray();
break;
case "hideToTray":
// 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.main.trayMain.hideToTray();
break;
case "addOpenAtLogin":
@ -86,7 +78,7 @@ export class MessagingMain {
this.removeOpenAtLogin();
break;
case "setFocus":
this.setFocus();
await this.setFocus();
break;
case "getWindowIsFocused":
this.windowIsFocused();
@ -218,8 +210,8 @@ export class MessagingMain {
return path.join(app.getPath("home"), ".config", "autostart", "bitwarden.desktop");
}
private setFocus() {
this.main.trayMain.restoreFromTray();
private async setFocus() {
await this.main.trayMain.restoreFromTray();
this.main.windowMain.win.focusOnWebView();
}

View File

@ -48,8 +48,8 @@ export class TrayMain {
const menuItemOptions: MenuItemConstructorOptions[] = [
{
label: this.i18nService.t("showHide"),
click: () => this.toggleWindow(),
label: this.i18nService.t("show"),
click: () => this.restoreFromTray(),
},
{
visible: isDev(),
@ -73,39 +73,36 @@ export class TrayMain {
}
this.contextMenu = Menu.buildFromTemplate(menuItemOptions);
if (await firstValueFrom(this.desktopSettingsService.trayEnabled$)) {
this.showTray();
}
// The tray icon is shown only while "keep running in the background" is enabled.
// React to the setting so toggling it shows/removes the tray without a restart.
this.desktopSettingsService.runInBackground$.subscribe((runInBackground) => {
if (runInBackground) {
this.showTray();
} else {
this.removeTray(false);
}
});
}
setupWindowListeners(win: BrowserWindow) {
win.on("minimize", async () => {
if (await firstValueFrom(this.desktopSettingsService.minimizeToTray$)) {
// 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.hideToTray();
}
});
win.on("restore", async () => {
await this.biometricService.setShouldAutopromptNow(true);
});
win.on("close", async (e: Event) => {
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.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.hideToTray();
}
if (this.windowMain.isQuitting) {
return;
}
});
win.on("show", async () => {
const enableTray = await firstValueFrom(this.desktopSettingsService.trayEnabled$);
if (!enableTray) {
setTimeout(() => this.removeTray(false), 100);
if (await firstValueFrom(this.desktopSettingsService.runInBackground$)) {
// Keep running in the background: closing the window hides it to the tray.
e.preventDefault();
this.hideToTray();
} else {
// Not running in the background: closing the window quits the application.
// Setting isQuitting ensures macOS also quits via the window-all-closed handler.
this.windowMain.isQuitting = true;
}
});
}
@ -123,21 +120,36 @@ export class TrayMain {
}
}
async hideToTray() {
hideToTray() {
this.showTray();
if (this.windowMain.win != null) {
this.windowMain.win.hide();
}
if (this.isDarwin() && !(await firstValueFrom(this.desktopSettingsService.alwaysShowDock$))) {
if (this.isDarwin()) {
this.hideDock();
}
}
restoreFromTray() {
if (this.windowMain.win == null || !this.windowMain.win.isVisible()) {
// 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.toggleWindow();
/**
* Brings the application to the foreground. Recreates the window if it was destroyed
* (macOS), unminimizes it if minimized, then shows and focuses it. This never hides
* the window - foregrounding is not a toggle.
*/
async restoreFromTray() {
if (this.windowMain.win == null) {
// On macOS, closing the window via the red button destroys the BrowserWindow instance.
await this.windowMain.createWindow();
this.windowMain.win.show();
} else {
if (this.windowMain.win.isMinimized()) {
this.windowMain.win.restore();
}
this.windowMain.show();
this.windowMain.win.focus();
}
if (this.isDarwin()) {
this.showDock();
}
}
@ -148,7 +160,7 @@ export class TrayMain {
this.tray = new Tray(this.icon);
this.tray.setToolTip(this.appName);
this.tray.on("click", () => this.toggleWindow());
this.tray.on("click", () => this.restoreFromTray());
this.tray.on("right-click", () => this.tray.popUpContextMenu(this.contextMenu));
if (this.pressedIcon != null) {
@ -183,32 +195,6 @@ export class TrayMain {
return process.platform === "linux";
}
private async toggleWindow() {
if (this.windowMain.win == null) {
if (this.isDarwin()) {
// On MacOS, closing the window via the red button destroys the BrowserWindow instance.
// 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.windowMain.createWindow().then(() => {
this.windowMain.win.show();
this.showDock();
});
}
return;
}
if (this.windowMain.win.isVisible()) {
this.windowMain.win.hide();
if (this.isDarwin() && !(await firstValueFrom(this.desktopSettingsService.alwaysShowDock$))) {
this.hideDock();
}
} else {
this.windowMain.show();
if (this.isDarwin()) {
this.showDock();
}
}
}
private closeWindow() {
this.windowMain.isQuitting = true;
if (this.windowMain.win != null) {

View File

@ -52,6 +52,7 @@ export class WindowMain {
private shell: SafeShell,
private argvCallback: (argv: string[]) => void = null,
private createWindowCallback: (win: BrowserWindow) => void,
private focusWindowCallback: () => Promise<void> | void = null,
) {}
init(show: boolean = true): Promise<any> {
@ -134,8 +135,11 @@ export class WindowMain {
return;
} else {
app.on("second-instance", (event, argv, workingDirectory) => {
// Someone tried to run a second instance, we should focus our window.
if (this.win != null) {
// Someone tried to run a second instance (e.g. launching from the desktop
// icon or terminal while already running). Always bring us to the foreground.
if (this.focusWindowCallback != null) {
void this.focusWindowCallback();
} else if (this.win != null) {
if (this.win.isMinimized() || !this.win.isVisible()) {
this.win.show();
}
@ -211,7 +215,9 @@ 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.focusWindowCallback != null) {
await this.focusWindowCallback();
} else if (this.win == null) {
await this.createWindow();
} else {
// Show the window when clicking on Dock icon

View File

@ -34,15 +34,7 @@ const WINDOW_KEY = new KeyDefinition<WindowState | null>(DESKTOP_SETTINGS_DISK,
deserializer: (s) => s,
});
const CLOSE_TO_TRAY_KEY = new KeyDefinition<boolean>(DESKTOP_SETTINGS_DISK, "closeToTray", {
deserializer: (b) => b,
});
const MINIMIZE_TO_TRAY_KEY = new KeyDefinition<boolean>(DESKTOP_SETTINGS_DISK, "minimizeToTray", {
deserializer: (b) => b,
});
const TRAY_ENABLED_KEY = new KeyDefinition<boolean>(DESKTOP_SETTINGS_DISK, "trayEnabled", {
const RUN_IN_BACKGROUND_KEY = new KeyDefinition<boolean>(DESKTOP_SETTINGS_DISK, "runInBackground", {
deserializer: (b) => b,
});
@ -50,10 +42,6 @@ const OPEN_AT_LOGIN_KEY = new KeyDefinition<boolean>(DESKTOP_SETTINGS_DISK, "ope
deserializer: (b) => b,
});
const ALWAYS_SHOW_DOCK_KEY = new KeyDefinition<boolean>(DESKTOP_SETTINGS_DISK, "alwaysShowDock", {
deserializer: (b) => b,
});
const ALWAYS_ON_TOP_KEY = new KeyDefinition<boolean>(DESKTOP_SETTINGS_DISK, "alwaysOnTop", {
deserializer: (b) => b,
});
@ -105,23 +93,16 @@ export class DesktopSettingsService {
private readonly windowState = this.stateProvider.getGlobal(WINDOW_KEY);
private readonly closeToTrayState = this.stateProvider.getGlobal(CLOSE_TO_TRAY_KEY);
private readonly runInBackground = this.stateProvider.getGlobal(RUN_IN_BACKGROUND_KEY);
/**
* The applications setting for whether or not to close the application into the system tray.
* The application setting for whether or not Bitwarden should keep running in the background.
*
* When enabled, the system tray icon is shown and closing the window hides the
* application to the tray (keeping background features such as the SSH agent and
* biometric unlock available) instead of quitting. When disabled, no tray is shown
* and closing the window quits the application.
*/
closeToTray$ = this.closeToTrayState.state$.pipe(map((v) => v ?? !isDev()));
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(Boolean));
private readonly trayEnabledState = this.stateProvider.getGlobal(TRAY_ENABLED_KEY);
/**
* Whether or not the system tray has been enabled.
*/
trayEnabled$ = this.trayEnabledState.state$.pipe(map((v) => v ?? !isDev()));
runInBackground$ = this.runInBackground.state$.pipe(map((v) => v ?? !isDev()));
private readonly openAtLoginState = this.stateProvider.getGlobal(OPEN_AT_LOGIN_KEY);
/**
@ -129,12 +110,6 @@ export class DesktopSettingsService {
*/
openAtLogin$ = this.openAtLoginState.state$.pipe(map((v) => v ?? !isDev()));
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(Boolean));
private readonly alwaysOnTopState = this.stateProvider.getGlobal(ALWAYS_ON_TOP_KEY);
alwaysOnTop$ = this.alwaysOnTopState.state$.pipe(map(Boolean));
@ -210,27 +185,12 @@ export class DesktopSettingsService {
}
/**
* 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.
* Sets the setting for whether or not Bitwarden should keep running in the background.
* @param value `true` if the application should show the tray and hide to it when the window
* is closed, `false` if closing the window should quit the application.
*/
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 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);
async setRunInBackground(value: boolean) {
await this.runInBackground.update(() => value);
}
/**
@ -241,14 +201,6 @@ export class DesktopSettingsService {
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 show 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.

View File

@ -77,11 +77,12 @@ import { ClearClipboardDelayToStringMigrator } from "./migrations/77-clear-clipb
import { MigrateSsoRequiredCache } from "./migrations/78-migrate-sso-required-cache";
import { InitializeFeatureFlagOverridesMigrator } from "./migrations/79-initialize-feature-flag-overrides";
import { MoveStateVersionMigrator } from "./migrations/8-move-state-version";
import { ConsolidateTrayToRunInBackground } from "./migrations/80-consolidate-tray-to-run-in-background";
import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-settings-to-global";
import { MinVersionMigrator } from "./migrations/min-version";
export const MIN_VERSION = 3;
export const CURRENT_VERSION = 79;
export const CURRENT_VERSION = 80;
export type MinVersion = typeof MIN_VERSION;
export function createMigrationBuilder() {
@ -162,7 +163,8 @@ export function createMigrationBuilder() {
.with(MigratePopupWidthOptions, 75, 76)
.with(ClearClipboardDelayToStringMigrator, 76, 77)
.with(MigrateSsoRequiredCache, 77, 78)
.with(InitializeFeatureFlagOverridesMigrator, 78, CURRENT_VERSION);
.with(InitializeFeatureFlagOverridesMigrator, 78, 79)
.with(ConsolidateTrayToRunInBackground, 79, CURRENT_VERSION);
}
export async function currentVersion(

View File

@ -0,0 +1,76 @@
import { runMigrator } from "../migration-helper.spec";
import { IRREVERSIBLE } from "../migrator";
import { ConsolidateTrayToRunInBackground } from "./80-consolidate-tray-to-run-in-background";
describe("ConsolidateTrayToRunInBackground", () => {
const sut = new ConsolidateTrayToRunInBackground(79, 80);
describe("migrate", () => {
it("keeps run in background enabled when closeToTray was enabled", async () => {
const output = await runMigrator(sut, {
global_desktopSettings_closeToTray: true,
global_desktopSettings_trayEnabled: false,
});
expect(output).toEqual({
global_desktopSettings_runInBackground: true,
});
});
it("enables run in background when only trayEnabled was set", async () => {
const output = await runMigrator(sut, {
global_desktopSettings_closeToTray: false,
global_desktopSettings_trayEnabled: true,
});
expect(output).toEqual({
global_desktopSettings_runInBackground: true,
});
});
it("disables run in background when both legacy settings were disabled", async () => {
const output = await runMigrator(sut, {
global_desktopSettings_closeToTray: false,
global_desktopSettings_trayEnabled: false,
});
expect(output).toEqual({
global_desktopSettings_runInBackground: false,
});
});
it("removes the orphaned minimizeToTray and alwaysShowDock keys", async () => {
const output = await runMigrator(sut, {
global_desktopSettings_closeToTray: true,
global_desktopSettings_trayEnabled: true,
global_desktopSettings_minimizeToTray: true,
global_desktopSettings_alwaysShowDock: true,
});
expect(output).toEqual({
global_desktopSettings_runInBackground: true,
});
});
it("does not write runInBackground when neither legacy setting was present", async () => {
const output = await runMigrator(sut, {
global_desktopSettings_minimizeToTray: true,
});
expect(output).toEqual({});
});
it("does nothing when no desktop settings are present", async () => {
const output = await runMigrator(sut, {});
expect(output).toEqual({});
});
});
describe("rollback", () => {
it("is irreversible", async () => {
await expect(runMigrator(sut, {}, "rollback")).rejects.toThrow(IRREVERSIBLE);
});
});
});

View File

@ -0,0 +1,62 @@
import { KeyDefinitionLike, MigrationHelper, StateDefinitionLike } from "../migration-helper";
import { IRREVERSIBLE, Migrator } from "../migrator";
const DESKTOP_SETTINGS_STATE: StateDefinitionLike = { name: "desktopSettings" };
const CLOSE_TO_TRAY_KEY: KeyDefinitionLike = {
key: "closeToTray",
stateDefinition: DESKTOP_SETTINGS_STATE,
};
const RUN_IN_BACKGROUND_KEY: KeyDefinitionLike = {
key: "runInBackground",
stateDefinition: DESKTOP_SETTINGS_STATE,
};
const TRAY_ENABLED_KEY: KeyDefinitionLike = {
key: "trayEnabled",
stateDefinition: DESKTOP_SETTINGS_STATE,
};
const MINIMIZE_TO_TRAY_KEY: KeyDefinitionLike = {
key: "minimizeToTray",
stateDefinition: DESKTOP_SETTINGS_STATE,
};
const ALWAYS_SHOW_DOCK_KEY: KeyDefinitionLike = {
key: "alwaysShowDock",
stateDefinition: DESKTOP_SETTINGS_STATE,
};
/**
* Consolidates the legacy `trayEnabled`, `minimizeToTray`, and `closeToTray` desktop settings
* into a single "run in the background" setting, stored under the new `runInBackground` key.
* `runInBackground = closeToTray || trayEnabled`. The now-unused `closeToTray`, `trayEnabled`,
* `minimizeToTray`, and `alwaysShowDock` keys are removed.
*/
export class ConsolidateTrayToRunInBackground extends Migrator<79, 80> {
async migrate(helper: MigrationHelper): Promise<void> {
const closeToTray = await helper.getFromGlobal<boolean>(CLOSE_TO_TRAY_KEY);
const trayEnabled = await helper.getFromGlobal<boolean>(TRAY_ENABLED_KEY);
// Only write a consolidated value when at least one legacy value existed, so users who
// never customized these keep falling through to the `!isDev()` default in the service.
if (closeToTray != null || trayEnabled != null) {
await helper.setToGlobal(RUN_IN_BACKGROUND_KEY, Boolean(closeToTray) || Boolean(trayEnabled));
}
// Remove orphaned keys (guarded so absent keys aren't touched).
if (closeToTray != null) {
await helper.removeFromGlobal(CLOSE_TO_TRAY_KEY);
}
if (trayEnabled != null) {
await helper.removeFromGlobal(TRAY_ENABLED_KEY);
}
if ((await helper.getFromGlobal<boolean>(MINIMIZE_TO_TRAY_KEY)) != null) {
await helper.removeFromGlobal(MINIMIZE_TO_TRAY_KEY);
}
if ((await helper.getFromGlobal<boolean>(ALWAYS_SHOW_DOCK_KEY)) != null) {
await helper.removeFromGlobal(ALWAYS_SHOW_DOCK_KEY);
}
}
rollback(helper: MigrationHelper): Promise<void> {
throw IRREVERSIBLE;
}
}