summaryrefslogtreecommitdiffhomepage
path: root/gui/src
diff options
context:
space:
mode:
Diffstat (limited to 'gui/src')
-rw-r--r--gui/src/main/index.ts6
-rw-r--r--gui/src/main/notification-controller.ts101
-rw-r--r--gui/src/main/tray-icon-controller.ts41
-rw-r--r--gui/src/main/user-interface.ts14
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)) {