diff options
Diffstat (limited to 'gui/src')
| -rw-r--r-- | gui/src/main/index.ts | 6 | ||||
| -rw-r--r-- | gui/src/main/notification-controller.ts | 101 | ||||
| -rw-r--r-- | gui/src/main/tray-icon-controller.ts | 41 | ||||
| -rw-r--r-- | gui/src/main/user-interface.ts | 14 |
4 files changed, 118 insertions, 44 deletions
diff --git a/gui/src/main/index.ts b/gui/src/main/index.ts index 51ce0667ac..7181f863bf 100644 --- a/gui/src/main/index.ts +++ b/gui/src/main/index.ts @@ -941,6 +941,7 @@ class ApplicationMain return shell.openExternal(url); } }; + public showNotificationIcon = (value: boolean) => this.userInterface?.showNotificationIcon(value); // NotificationSender public notify = (notification: SystemNotification) => { @@ -954,7 +955,8 @@ class ApplicationMain this.notificationController.closeNotificationsInCategory(category); // UserInterfaceDelegate - public closeActiveNotifications = () => this.notificationController.closeActiveNotifications(); + public dismissActiveNotifications = () => + this.notificationController.dismissActiveNotifications(); public isUnpinnedWindow = () => this.settings.gui.unpinnedWindow; public updateAccountData = () => this.account.updateAccountData(); public getAccountData = () => this.account.accountData; @@ -984,7 +986,7 @@ class ApplicationMain // SettingsDelegate public handleMonochromaticIconChange = (value: boolean) => - this.userInterface?.setUseMonochromaticTrayIcon(value) ?? Promise.resolve(); + this.userInterface?.setMonochromaticIcon(value) ?? Promise.resolve(); public handleUnpinnedWindowChange = () => void this.userInterface?.recreateWindow( this.account.isLoggedIn(), diff --git a/gui/src/main/notification-controller.ts b/gui/src/main/notification-controller.ts index 4acd988a23..6fbe6d4f19 100644 --- a/gui/src/main/notification-controller.ts +++ b/gui/src/main/notification-controller.ts @@ -33,13 +33,24 @@ export interface NotificationSender { export interface NotificationControllerDelegate { openApp(): void; openLink(url: string, withAuth?: boolean): Promise<void>; + showNotificationIcon(value: boolean): void; +} + +enum NotificationSuppressReason { + development, + windowVisible, + preference, + alreadyPresented, } export default class NotificationController { private reconnecting = false; + private presentedNotifications: { [key: string]: boolean } = {}; private activeNotifications: Set<Notification> = new Set(); + private dismissedNotifications: Set<SystemNotification> = new Set(); private throttledNotifications: Map<SystemNotification, Scheduler> = new Map(); + private notificationTitle = process.platform === 'linux' ? app.name : ''; private notificationIcon?: NativeImage; @@ -105,16 +116,32 @@ export default class NotificationController { tunnelState.state === 'disconnecting' && tunnelState.details === 'reconnect'; } - public closeActiveNotifications() { - this.activeNotifications.forEach((notification) => notification.notification.close()); + // Closes still relevant notifications but still lets them affect notification dot in tray icon. + public dismissActiveNotifications() { + this.activeNotifications.forEach((notification) => { + notification.notification.close(); + }); + this.updateNotificationIcon(); } - public closeNotificationsInCategory(category: SystemNotificationCategory) { + public closeNotificationsInCategory( + category: SystemNotificationCategory, + severity?: SystemNotificationSeverityType, + ) { this.activeNotifications.forEach((notification) => { if (notification.specification.category === category) { notification.notification.close(); } }); + this.dismissedNotifications.forEach((notification) => { + if ( + notification.category === category && + (severity === undefined || severity >= notification.severity) + ) { + this.dismissedNotifications.delete(notification); + } + }); + this.updateNotificationIcon(); } public notify( @@ -122,7 +149,20 @@ export default class NotificationController { windowVisible: boolean, infoNotificationsEnabled: boolean, ) { - if (!this.evaluateNotification(systemNotification, windowVisible, infoNotificationsEnabled)) { + const notificationSuppressReason = this.evaluateNotification( + systemNotification, + windowVisible, + infoNotificationsEnabled, + ); + if (notificationSuppressReason !== undefined) { + if ( + notificationSuppressReason === NotificationSuppressReason.preference || + notificationSuppressReason === NotificationSuppressReason.windowVisible + ) { + this.dismissedNotifications.add(systemNotification); + this.updateNotificationIcon(); + } + return; } @@ -152,7 +192,7 @@ export default class NotificationController { private notifyImpl(systemNotification: SystemNotification): Notification { // Remove notifications in the same category if specified if (systemNotification.category !== undefined) { - this.closeNotificationsInCategory(systemNotification.category); + this.closeNotificationsInCategory(systemNotification.category, systemNotification.severity); } const notification = this.createNotification(systemNotification); @@ -207,27 +247,52 @@ export default class NotificationController { } private addActiveNotification(notification: Notification) { - notification.notification.on('close', () => this.activeNotifications.delete(notification)); + notification.notification.on('close', () => { + this.dismissedNotifications.add({ ...notification.specification }); + this.activeNotifications.delete(notification); + this.updateNotificationIcon(); + }); this.activeNotifications.add(notification); + this.updateNotificationIcon(); + } + + private updateNotificationIcon() { + for (const notification of this.activeNotifications) { + if (notification.specification.severity >= SystemNotificationSeverityType.medium) { + this.notificationControllerDelegate.showNotificationIcon(true); + return; + } + } + + for (const notification of this.dismissedNotifications) { + if (notification.severity >= SystemNotificationSeverityType.medium) { + this.notificationControllerDelegate.showNotificationIcon(true); + return; + } + } + + this.notificationControllerDelegate.showNotificationIcon(false); } private evaluateNotification( notification: SystemNotification, isWindowVisible: boolean, areSystemNotificationsEnabled: boolean, - ) { - const suppressDueToDevelopment = - notification.suppressInDevelopment && process.env.NODE_ENV === 'development'; - const suppressDueToVisibleWindow = isWindowVisible; - const suppressDueToPreference = - !areSystemNotificationsEnabled && notification.severity > SystemNotificationSeverityType.info; + ): NotificationSuppressReason | undefined { + if (notification.suppressInDevelopment && process.env.NODE_ENV === 'development') { + return NotificationSuppressReason.development; + } else if (isWindowVisible) { + return NotificationSuppressReason.windowVisible; + } else if ( + !areSystemNotificationsEnabled && + notification.severity >= SystemNotificationSeverityType.low + ) { + return NotificationSuppressReason.preference; + } else if (this.suppressDueToAlreadyPresented(notification)) { + return NotificationSuppressReason.alreadyPresented; + } - return ( - !suppressDueToDevelopment && - !suppressDueToVisibleWindow && - !suppressDueToPreference && - !this.suppressDueToAlreadyPresented(notification) - ); + return undefined; } private suppressDueToAlreadyPresented(notification: SystemNotification) { diff --git a/gui/src/main/tray-icon-controller.ts b/gui/src/main/tray-icon-controller.ts index 2fd2449f9f..e71d6cb276 100644 --- a/gui/src/main/tray-icon-controller.ts +++ b/gui/src/main/tray-icon-controller.ts @@ -17,7 +17,8 @@ export default class TrayIconController { constructor( private tray: Tray, private iconTypeValue: TrayIconType, - private useMonochromaticIconValue: boolean, + private monochromaticIcon: boolean, + private notificationIcon: boolean, ) { void this.init(); } @@ -47,11 +48,16 @@ export default class TrayIconController { } } - public async setUseMonochromaticIcon(useMonochromaticIcon: boolean) { - this.useMonochromaticIconValue = useMonochromaticIcon; + public async setMonochromaticIcon(monochromaticIcon: boolean) { + this.monochromaticIcon = monochromaticIcon; await this.updateTheme(); } + public showNotificationIcon(notificationIcon: boolean) { + this.notificationIcon = notificationIcon; + void this.updateTheme(); + } + public animateToIcon(type: TrayIconType) { if (this.iconTypeValue === type || !this.animation) { return; @@ -85,22 +91,21 @@ export default class TrayIconController { }; private async loadImages(): Promise<NativeImage[]> { - switch (process.platform) { - case 'darwin': - return this.useMonochromaticIconValue - ? this.loadImageSet('Template') - : this.loadImageSet(''); - case 'win32': - if (this.useMonochromaticIconValue) { + const notificationIcon = this.notificationIcon ? '_notification' : ''; + if (this.monochromaticIcon) { + switch (process.platform) { + case 'darwin': + return this.loadImageSet(`${notificationIcon}Template`); + case 'win32': return (await this.getSystemUsesLightTheme()) - ? this.loadImageSet('_black') - : this.loadImageSet('_white'); - } else { - return this.loadImageSet(''); - } - case 'linux': - default: - return this.useMonochromaticIconValue ? this.loadImageSet('_white') : this.loadImageSet(''); + ? this.loadImageSet(`_black${notificationIcon}`) + : this.loadImageSet(`_white${notificationIcon}`); + case 'linux': + default: + return this.loadImageSet(`_white${notificationIcon}`); + } + } else { + return this.loadImageSet(notificationIcon); } } diff --git a/gui/src/main/user-interface.ts b/gui/src/main/user-interface.ts index 7013397baa..bc239cec2c 100644 --- a/gui/src/main/user-interface.ts +++ b/gui/src/main/user-interface.ts @@ -25,7 +25,7 @@ import WindowController, { WindowControllerDelegate } from './window-controller' const execAsync = promisify(exec); export interface UserInterfaceDelegate { - closeActiveNotifications(): void; + dismissActiveNotifications(): void; updateAccountData(): void; connectTunnel(): void; reconnectTunnel(): void; @@ -90,7 +90,7 @@ export default class UserInterface implements WindowControllerDelegate { monochromaticIcon: boolean, ) { const iconType = this.trayIconType(tunnelState, blockWhenDisconnected); - this.trayIconController = new TrayIconController(this.tray, iconType, monochromaticIcon); + this.trayIconController = new TrayIconController(this.tray, iconType, monochromaticIcon, false); } public async initializeWindow(isLoggedIn: boolean, tunnelState: TunnelState) { @@ -184,9 +184,11 @@ export default class UserInterface implements WindowControllerDelegate { public reloadWindow = () => this.windowController.window?.reload(); public isWindowVisible = () => this.windowController.isVisible(); public showWindow = () => this.windowController.show(); - public updateTrayTheme = () => this.trayIconController?.updateTheme(); - public setUseMonochromaticTrayIcon = (value: boolean) => - this.trayIconController?.setUseMonochromaticIcon(value); + public updateTrayTheme = () => this.trayIconController?.updateTheme() ?? Promise.resolve(); + public setMonochromaticIcon = (value: boolean) => + this.trayIconController?.setMonochromaticIcon(value); + public showNotificationIcon = (value: boolean) => + this.trayIconController?.showNotificationIcon(value); public setWindowIcon = (icon: string) => this.windowController.window?.setIcon(icon); public updateTrayIcon(tunnelState: TunnelState, blockWhenDisconnected: boolean) { @@ -318,7 +320,7 @@ export default class UserInterface implements WindowControllerDelegate { this.blurNavigationResetScheduler.cancel(); // cancel notifications when window appears - this.delegate.closeActiveNotifications(); + this.delegate.dismissActiveNotifications(); const accountData = this.delegate.getAccountData(); if (!accountData || closeToExpiry(accountData.expiry, 4) || hasExpired(accountData.expiry)) { |
