diff options
| author | Oskar Nyberg <oskar@mullvad.net> | 2020-06-04 13:45:06 +0200 |
|---|---|---|
| committer | Oskar Nyberg <oskar@mullvad.net> | 2020-06-10 13:46:17 +0200 |
| commit | bfd3b02a3936451ae43867f8f3e2f986a2a2f9bc (patch) | |
| tree | 3ab241e849f8f22f5bc0a8e5af226c8b30e4552e /gui/src/main/notification-controller.ts | |
| parent | d7fdccb58e8205ae26023629873922979644bfdd (diff) | |
| download | mullvadvpn-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.ts | 276 |
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; + } + } } |
