diff --git a/spec/mainApiHandler.spec.ts b/spec/mainApiHandler.spec.ts index 91f56389..a54aaf64 100644 --- a/spec/mainApiHandler.spec.ts +++ b/spec/mainApiHandler.spec.ts @@ -107,6 +107,15 @@ jest.mock('../src/app/download-handler', () => { }; }); +jest.mock('../src/app/notifications/notification-helper', () => { + return { + notificationHelper: { + showNotification: jest.fn(), + closeNotification: jest.fn(), + }, + }; +}); + jest.mock('../src/common/i18n'); describe('main api handler', () => { diff --git a/src/app/main-api-handler.ts b/src/app/main-api-handler.ts index 491f8e5e..501586a8 100644 --- a/src/app/main-api-handler.ts +++ b/src/app/main-api-handler.ts @@ -1,6 +1,6 @@ import { BrowserWindow, ipcMain } from 'electron'; -import { apiCmds, apiName, IApiArgs } from '../common/api-interface'; +import { apiCmds, apiName, IApiArgs, INotificationData } from '../common/api-interface'; import { LocaleType } from '../common/i18n'; import { logger } from '../common/logger'; import { activityDetection } from './activity-detection'; @@ -9,6 +9,7 @@ import appStateHandler from './app-state-handler'; import { CloudConfigDataTypes, config, ICloudConfig } from './config-handler'; import { downloadHandler } from './download-handler'; import { memoryMonitor } from './memory-monitor'; +import notificationHelper from './notifications/notification-helper'; import { protocolHandler } from './protocol-handler'; import { finalizeLogExports, registerLogRetriever } from './reports-handler'; import { screenSnippet } from './screen-snippet-handler'; @@ -208,6 +209,17 @@ ipcMain.on(apiName.symphonyApi, async (event: Electron.IpcMainEvent, arg: IApiAr logger.info('window-handler: isMana: ' + windowHandler.isMana); } break; + case apiCmds.showNotification: + if (typeof arg.notificationOpts === 'object') { + const opts = arg.notificationOpts as INotificationData; + notificationHelper.showNotification(opts); + } + break; + case apiCmds.closeNotification: + if (typeof arg.notificationId === 'number') { + await notificationHelper.closeNotification(arg.notificationId); + } + break; default: break; } diff --git a/src/app/notifications/electron-notification.ts b/src/app/notifications/electron-notification.ts new file mode 100644 index 00000000..122e4c8c --- /dev/null +++ b/src/app/notifications/electron-notification.ts @@ -0,0 +1,40 @@ +import { Notification, NotificationConstructorOptions } from 'electron'; + +import { ElectronNotificationData, INotificationData, NotificationActions } from '../../common/api-interface'; + +export class ElectronNotification extends Notification { + private callback: ( + actionType: NotificationActions, + data: INotificationData, + notificationData?: ElectronNotificationData, + ) => void; + private options: INotificationData; + + constructor(options: INotificationData, callback) { + super(options as NotificationConstructorOptions); + this.callback = callback; + this.options = options; + + this.once('click', this.onClick); + this.once('reply', this.onReply); + } + + /** + * Notification on click handler + * @param _event + * @private + */ + private onClick(_event: Event) { + this.callback(NotificationActions.notificationClicked, this.options); + } + + /** + * Notification reply handler + * @param _event + * @param reply + * @private + */ + private onReply(_event: Event, reply: string) { + this.callback(NotificationActions.notificationReply, this.options, reply); + } +} diff --git a/src/app/notifications/notification-helper.ts b/src/app/notifications/notification-helper.ts new file mode 100644 index 00000000..a7a9923b --- /dev/null +++ b/src/app/notifications/notification-helper.ts @@ -0,0 +1,78 @@ +import { ElectronNotificationData, INotificationData, NotificationActions } from '../../common/api-interface'; +import { isWindowsOS } from '../../common/env'; +import { notification } from '../../renderer/notification'; +import { windowHandler } from '../window-handler'; +import { windowExists } from '../window-utils'; +import { ElectronNotification } from './electron-notification'; + +class NotificationHelper { + private electronNotification: Map; + + constructor() { + this.electronNotification = new Map(); + } + + /** + * Displays Electron/HTML notification based on the + * isElectronNotification flag + * + * @param options {INotificationData} + */ + public showNotification(options: INotificationData) { + if (options.isElectronNotification) { + // MacOS: Electron notification only supports static image path + options.icon = this.getIcon(options); + const electronToast = new ElectronNotification(options, this.notificationCallback); + this.electronNotification.set(options.id, electronToast); + electronToast.show(); + return; + } + notification.showNotification(options, this.notificationCallback); + } + + /** + * Closes a specific notification by id + * + * @param id {number} - unique id assigned to a specific notification + */ + public async closeNotification(id: number) { + if (this.electronNotification.has(id)) { + const electronNotification = this.electronNotification.get(id); + if (electronNotification) { + electronNotification.close(); + } + return; + } + await notification.hideNotification(id); + } + + /** + * Sends the notification actions event to the web client + * + * @param event {NotificationActions} + * @param data {ElectronNotificationData} + * @param notificationData {ElectronNotificationData} + */ + public notificationCallback( + event: NotificationActions, + data: ElectronNotificationData, + notificationData: ElectronNotificationData, + ) { + const mainWindow = windowHandler.getMainWindow(); + if (mainWindow && windowExists(mainWindow) && mainWindow.webContents) { + mainWindow.webContents.send('notification-actions', { event, data, notificationData }); + } + } + + /** + * Return the correct icon based on platform + * @param options + * @private + */ + private getIcon(options: INotificationData): string | undefined { + return isWindowsOS ? options.icon : undefined; + } +} + +const notificationHelper = new NotificationHelper(); +export default notificationHelper; diff --git a/src/common/api-interface.ts b/src/common/api-interface.ts index 835d15d3..c2340d6c 100644 --- a/src/common/api-interface.ts +++ b/src/common/api-interface.ts @@ -44,6 +44,7 @@ export enum apiCmds { clearDownloadedItems = 'clear-downloaded-items', restartApp = 'restart-app', setIsMana = 'set-is-mana', + showNotification = 'show-notification', } export enum apiName { @@ -80,6 +81,8 @@ export interface IApiArgs { logs: ILogs; cloudConfig: object; isMana: boolean; + notificationOpts: object; + notificationId: number; } export type WindowTypes = 'screen-picker' | 'screen-sharing-indicator' | 'notification-settings'; @@ -126,7 +129,7 @@ export interface INotificationData { title: string; body: string; image: string; - icon: string; + icon?: string; flash: boolean; color: string; tag: string; @@ -135,11 +138,14 @@ export interface INotificationData { displayTime: number; isExternal: boolean; theme: Theme; + isElectronNotification?: boolean; + callback?: () => void; } export enum NotificationActions { notificationClicked = 'notification-clicked', notificationClosed = 'notification-closed', + notificationReply = 'notification-reply', } /** @@ -204,3 +210,7 @@ export interface IRestartFloaterData { windowName: string; bounds: Electron.Rectangle; } + +export type Reply = string; +export type ElectronNotificationData = Reply | object; +export type NotificationActionCallback = (event: NotificationActions, data: INotificationData) => void; diff --git a/src/demo/index.html b/src/demo/index.html index c7d0ef4f..0ff95062 100644 --- a/src/demo/index.html +++ b/src/demo/index.html @@ -53,6 +53,13 @@

Notifications:

+

+

+ + +

+
+

Reply content will display here

@@ -77,6 +84,10 @@

+

+ + +

Change theme: