summaryrefslogtreecommitdiffhomepage
path: root/gui/src/main/notification-controller.ts
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2020-06-04 13:45:06 +0200
committerOskar Nyberg <oskar@mullvad.net>2020-06-10 13:46:17 +0200
commitbfd3b02a3936451ae43867f8f3e2f986a2a2f9bc (patch)
tree3ab241e849f8f22f5bc0a8e5af226c8b30e4552e /gui/src/main/notification-controller.ts
parentd7fdccb58e8205ae26023629873922979644bfdd (diff)
downloadmullvadvpn-bfd3b02a3936451ae43867f8f3e2f986a2a2f9bc.tar.xz
mullvadvpn-bfd3b02a3936451ae43867f8f3e2f986a2a2f9bc.zip
Categorize notifications and their logic into notification definition
Diffstat (limited to 'gui/src/main/notification-controller.ts')
-rw-r--r--gui/src/main/notification-controller.ts276
1 files changed, 109 insertions, 167 deletions
diff --git a/gui/src/main/notification-controller.ts b/gui/src/main/notification-controller.ts
index 9c3cf2193c..548c20e122 100644
--- a/gui/src/main/notification-controller.ts
+++ b/gui/src/main/notification-controller.ts
@@ -1,13 +1,26 @@
-import { app, nativeImage, NativeImage, Notification, shell } from 'electron';
+import { app, nativeImage, NativeImage, Notification } from 'electron';
+import log from 'electron-log';
import os from 'os';
import path from 'path';
-import { sprintf } from 'sprintf-js';
-import config from '../config.json';
-import AccountExpiry from '../shared/account-expiry';
import { TunnelState } from '../shared/daemon-rpc-types';
-import { messages } from '../shared/gettext';
+import {
+ BlockWhenDisconnectedNotificationProvider,
+ ConnectedNotificationProvider,
+ ConnectingNotificationProvider,
+ DisconnectedNotificationProvider,
+ ErrorNotificationProvider,
+ ReconnectingNotificationProvider,
+ SystemNotification,
+ SystemNotificationProvider,
+} from '../shared/notifications/notification';
import consumePromise from '../shared/promise';
+interface NotificationControllerDelegate {
+ openLink(url: string, withAuth?: boolean): Promise<void>;
+ isWindowVisible(): boolean;
+ areSystemNotificationsEnabled(): boolean;
+}
+
export default class NotificationController {
private lastTunnelStateAnnouncement?: { body: string; notification: Notification };
private reconnecting = false;
@@ -16,7 +29,7 @@ export default class NotificationController {
private notificationTitle = process.platform === 'linux' ? app.name : '';
private notificationIcon?: NativeImage;
- constructor() {
+ constructor(private notificationControllerDelegate: NotificationControllerDelegate) {
let usePngIcon;
if (process.platform === 'linux') {
usePngIcon = true;
@@ -34,147 +47,34 @@ export default class NotificationController {
}
}
- public notifyTunnelState(tunnelState: TunnelState) {
- switch (tunnelState.state) {
- case 'connecting':
- if (!this.reconnecting) {
- const details = tunnelState.details;
- if (details && details.location && details.location.hostname) {
- const msg = sprintf(
- // TRANSLATORS: The message showed when a server is being connected to.
- // TRANSLATORS: Available placeholder:
- // TRANSLATORS: %(location) - name of the server location we're connecting to (e.g. "se-got-003")
- messages.pgettext('notifications', 'Connecting to %(location)s'),
- {
- location: details.location.hostname,
- },
- );
- this.showTunnelStateNotification(msg);
- } else {
- this.showTunnelStateNotification(messages.pgettext('notifications', 'Connecting'));
- }
- }
- break;
- case 'connected':
- {
- const details = tunnelState.details;
- if (details.location && details.location.hostname) {
- const msg = sprintf(
- // TRANSLATORS: The message showed when a server has been connected to.
- // TRANSLATORS: Available placeholder:
- // TRANSLATORS: %(location) - name of the server location we're connected to (e.g. "se-got-003")
- messages.pgettext('notifications', 'Connected to %(location)s'),
- {
- location: details.location.hostname,
- },
- );
- this.showTunnelStateNotification(msg);
- } else {
- this.showTunnelStateNotification(messages.pgettext('notifications', 'Secured'));
- }
- }
- break;
- case 'disconnected':
- this.showTunnelStateNotification(messages.pgettext('notifications', 'Unsecured'));
- break;
- case 'error':
- if (tunnelState.details.isBlocking) {
- if (
- tunnelState.details.cause.reason === 'tunnel_parameter_error' &&
- tunnelState.details.cause.details === 'no_wireguard_key'
- ) {
- this.showTunnelStateNotification(
- messages.pgettext(
- 'notifications',
- 'Blocking internet: Valid WireGuard key is missing',
- ),
- );
- } else {
- this.showTunnelStateNotification(
- messages.pgettext('notifications', 'Blocking internet'),
- );
- }
- } else {
- this.showTunnelStateNotification(
- messages.pgettext('notifications', 'Critical error (your attention is required)'),
- );
- }
- break;
- case 'disconnecting':
- switch (tunnelState.details) {
- case 'nothing':
- case 'block':
- // no-op
- break;
- case 'reconnect':
- this.showTunnelStateNotification(messages.pgettext('notifications', 'Reconnecting'));
- this.reconnecting = true;
- return;
- }
- break;
- }
+ public notifyTunnelState(tunnelState: TunnelState, blockWhenDisconnected: boolean) {
+ const notificationProviders: SystemNotificationProvider[] = [
+ new ConnectingNotificationProvider({ tunnelState, reconnecting: this.reconnecting }),
+ new ConnectedNotificationProvider(tunnelState),
+ new ReconnectingNotificationProvider(tunnelState),
+ new BlockWhenDisconnectedNotificationProvider({ tunnelState, blockWhenDisconnected }),
+ new DisconnectedNotificationProvider(tunnelState),
+ new ErrorNotificationProvider(tunnelState),
+ ];
- this.reconnecting = false;
- }
+ const notificationProvider = notificationProviders.find((notification) =>
+ notification.mayDisplay(),
+ );
- public notifyInconsistentVersion() {
- this.presentNotificationOnce('inconsistent-version', () => {
- const notification = new Notification({
- title: this.notificationTitle,
- body: messages.pgettext(
- 'notifications',
- 'Inconsistent internal version information, please restart the app',
- ),
- silent: true,
- icon: this.notificationIcon,
- });
- this.scheduleNotification(notification);
- });
- }
+ if (notificationProvider) {
+ const notification = notificationProvider.getSystemNotification();
- public notifyUnsupportedVersion(upgradeVersion: string) {
- this.presentNotificationOnce('unsupported-version', () => {
- const notification = new Notification({
- title: this.notificationTitle,
- body: sprintf(
- // TRANSLATORS: The system notification displayed to the user when the running app becomes unsupported.
- // TRANSLATORS: Available placeholder:
- // TRANSLATORS: %(version) - the newest available version of the app
- messages.pgettext(
- 'notifications',
- 'You are running an unsupported app version. Please upgrade to %(version)s now to ensure your security',
- ),
- {
- version: upgradeVersion,
- },
- ),
- silent: true,
- icon: this.notificationIcon,
- });
-
- notification.on('click', () => {
- consumePromise(shell.openExternal(config.links.download));
- });
-
- this.scheduleNotification(notification);
- });
- }
+ if (notification) {
+ this.showTunnelStateNotification(notification);
+ } else {
+ log.error(
+ `Notification providers mayDisplay() returned true but getSystemNotification() returned undefined for ${notificationProvider.constructor.name}`,
+ );
+ }
+ }
- public closeToExpiryNotification(accountExpiry: AccountExpiry) {
- const duration = accountExpiry.durationUntilExpiry();
- const notification = new Notification({
- title: this.notificationTitle,
- body: sprintf(
- // TRANSLATORS: The system notification displayed to the user when the account credit is close to expiry.
- // TRANSLATORS: Available placeholder:
- // TRANSLATORS: %(duration)s - remaining time, e.g. "2 days"
- messages.pgettext('notifications', 'Account credit expires in %(duration)s'),
- { duration },
- ),
- silent: true,
- icon: this.notificationIcon,
- });
- this.scheduleNotification(notification);
+ this.reconnecting =
+ tunnelState.state === 'disconnecting' && tunnelState.details === 'reconnect';
}
public cancelPendingNotifications() {
@@ -187,47 +87,59 @@ export default class NotificationController {
this.lastTunnelStateAnnouncement = undefined;
}
- private showTunnelStateNotification(message: string) {
- const lastAnnouncement = this.lastTunnelStateAnnouncement;
- const sameAsLastNotification = lastAnnouncement && lastAnnouncement.body === message;
+ public notify(systemNotification: SystemNotification) {
+ if (this.evaluateNotification(systemNotification)) {
+ const notification = this.createNotification(systemNotification);
+ this.addPendingNotification(notification);
+ notification.show();
- if (sameAsLastNotification) {
+ setTimeout(() => notification.close(), 4000);
+
+ return notification;
+ } else {
return;
}
+ }
- const newNotification = new Notification({
+ private createNotification(systemNotification: SystemNotification) {
+ const notification = new Notification({
title: this.notificationTitle,
- body: message,
+ body: systemNotification.message,
silent: true,
icon: this.notificationIcon,
});
- if (lastAnnouncement) {
- lastAnnouncement.notification.close();
+ if (systemNotification.action) {
+ const { withAuth, url } = systemNotification.action;
+ notification.on('click', () => {
+ consumePromise(this.notificationControllerDelegate.openLink(url, withAuth));
+ });
}
- this.lastTunnelStateAnnouncement = {
- body: message,
- notification: newNotification,
- };
-
- this.scheduleNotification(newNotification);
+ return notification;
}
- private presentNotificationOnce(notificationName: string, presentNotification: () => void) {
- const presented = this.presentedNotifications;
- if (!presented[notificationName]) {
- presented[notificationName] = true;
- presentNotification();
+ private showTunnelStateNotification(systemNotification: SystemNotification) {
+ const message = systemNotification.message;
+ const lastAnnouncement = this.lastTunnelStateAnnouncement;
+ const sameAsLastNotification = lastAnnouncement && lastAnnouncement.body === message;
+
+ if (sameAsLastNotification) {
+ return;
}
- }
- private scheduleNotification(notification: Notification) {
- this.addPendingNotification(notification);
+ if (lastAnnouncement) {
+ lastAnnouncement.notification.close();
+ }
- notification.show();
+ const newNotification = this.notify(systemNotification);
- setTimeout(() => notification.close(), 4000);
+ if (newNotification) {
+ this.lastTunnelStateAnnouncement = {
+ body: message,
+ notification: newNotification,
+ };
+ }
}
private addPendingNotification(notification: Notification) {
@@ -244,4 +156,34 @@ export default class NotificationController {
this.pendingNotifications.splice(index, 1);
}
}
+
+ private evaluateNotification(notification: SystemNotification) {
+ const suppressDueToDevelopment =
+ notification.suppressInDevelopment && process.env.NODE_ENV === 'development';
+ const suppressDueToVisibleWindow = this.notificationControllerDelegate.isWindowVisible();
+ const suppressDueToPreference =
+ !this.notificationControllerDelegate.areSystemNotificationsEnabled() &&
+ !notification.critical;
+
+ return (
+ !suppressDueToDevelopment &&
+ !suppressDueToVisibleWindow &&
+ !suppressDueToPreference &&
+ !this.suppressDueToAlreadyPresented(notification)
+ );
+ }
+
+ private suppressDueToAlreadyPresented(notification: SystemNotification) {
+ const presented = this.presentedNotifications;
+ if (notification.presentOnce?.value) {
+ if (presented[notification.presentOnce.name]) {
+ return true;
+ } else {
+ presented[notification.presentOnce.name] = true;
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
}