mirror of
https://github.com/finos/SymphonyElectron.git
synced 2025-02-25 18:55:29 -06:00
Merge branch 'master' into SDA-2764
This commit is contained in:
commit
f0ca21344a
@ -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');
|
jest.mock('../src/common/i18n');
|
||||||
|
|
||||||
describe('main api handler', () => {
|
describe('main api handler', () => {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { BrowserWindow, ipcMain } from 'electron';
|
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 { LocaleType } from '../common/i18n';
|
||||||
import { logger } from '../common/logger';
|
import { logger } from '../common/logger';
|
||||||
import { activityDetection } from './activity-detection';
|
import { activityDetection } from './activity-detection';
|
||||||
@ -9,6 +9,7 @@ import appStateHandler from './app-state-handler';
|
|||||||
import { CloudConfigDataTypes, config, ICloudConfig } from './config-handler';
|
import { CloudConfigDataTypes, config, ICloudConfig } from './config-handler';
|
||||||
import { downloadHandler } from './download-handler';
|
import { downloadHandler } from './download-handler';
|
||||||
import { memoryMonitor } from './memory-monitor';
|
import { memoryMonitor } from './memory-monitor';
|
||||||
|
import notificationHelper from './notifications/notification-helper';
|
||||||
import { protocolHandler } from './protocol-handler';
|
import { protocolHandler } from './protocol-handler';
|
||||||
import { finalizeLogExports, registerLogRetriever } from './reports-handler';
|
import { finalizeLogExports, registerLogRetriever } from './reports-handler';
|
||||||
import { screenSnippet } from './screen-snippet-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);
|
logger.info('window-handler: isMana: ' + windowHandler.isMana);
|
||||||
}
|
}
|
||||||
break;
|
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:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
40
src/app/notifications/electron-notification.ts
Normal file
40
src/app/notifications/electron-notification.ts
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
78
src/app/notifications/notification-helper.ts
Normal file
78
src/app/notifications/notification-helper.ts
Normal file
@ -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<number, ElectronNotification>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.electronNotification = new Map<number, ElectronNotification>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
@ -44,6 +44,7 @@ export enum apiCmds {
|
|||||||
clearDownloadedItems = 'clear-downloaded-items',
|
clearDownloadedItems = 'clear-downloaded-items',
|
||||||
restartApp = 'restart-app',
|
restartApp = 'restart-app',
|
||||||
setIsMana = 'set-is-mana',
|
setIsMana = 'set-is-mana',
|
||||||
|
showNotification = 'show-notification',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum apiName {
|
export enum apiName {
|
||||||
@ -80,6 +81,8 @@ export interface IApiArgs {
|
|||||||
logs: ILogs;
|
logs: ILogs;
|
||||||
cloudConfig: object;
|
cloudConfig: object;
|
||||||
isMana: boolean;
|
isMana: boolean;
|
||||||
|
notificationOpts: object;
|
||||||
|
notificationId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WindowTypes = 'screen-picker' | 'screen-sharing-indicator' | 'notification-settings';
|
export type WindowTypes = 'screen-picker' | 'screen-sharing-indicator' | 'notification-settings';
|
||||||
@ -126,7 +129,7 @@ export interface INotificationData {
|
|||||||
title: string;
|
title: string;
|
||||||
body: string;
|
body: string;
|
||||||
image: string;
|
image: string;
|
||||||
icon: string;
|
icon?: string;
|
||||||
flash: boolean;
|
flash: boolean;
|
||||||
color: string;
|
color: string;
|
||||||
tag: string;
|
tag: string;
|
||||||
@ -135,11 +138,14 @@ export interface INotificationData {
|
|||||||
displayTime: number;
|
displayTime: number;
|
||||||
isExternal: boolean;
|
isExternal: boolean;
|
||||||
theme: Theme;
|
theme: Theme;
|
||||||
|
isElectronNotification?: boolean;
|
||||||
|
callback?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum NotificationActions {
|
export enum NotificationActions {
|
||||||
notificationClicked = 'notification-clicked',
|
notificationClicked = 'notification-clicked',
|
||||||
notificationClosed = 'notification-closed',
|
notificationClosed = 'notification-closed',
|
||||||
|
notificationReply = 'notification-reply',
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -204,3 +210,7 @@ export interface IRestartFloaterData {
|
|||||||
windowName: string;
|
windowName: string;
|
||||||
bounds: Electron.Rectangle;
|
bounds: Electron.Rectangle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Reply = string;
|
||||||
|
export type ElectronNotificationData = Reply | object;
|
||||||
|
export type NotificationActionCallback = (event: NotificationActions, data: INotificationData) => void;
|
||||||
|
@ -53,6 +53,13 @@
|
|||||||
<hr>
|
<hr>
|
||||||
<p>Notifications:<p>
|
<p>Notifications:<p>
|
||||||
<button id='notf'>show notification</button>
|
<button id='notf'>show notification</button>
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
<button id='closeNotification'>close notification</button>
|
||||||
|
<input type='number' id='notificationTag' value='Notification Tag to close'/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<p id='reply'>Reply content will display here</p>
|
||||||
<p>
|
<p>
|
||||||
<label for='title'>title:</label>
|
<label for='title'>title:</label>
|
||||||
<input type='text' id='title' value='Notification Demo'/>
|
<input type='text' id='title' value='Notification Demo'/>
|
||||||
@ -77,6 +84,10 @@
|
|||||||
<label for='isExternal'>external:</label>
|
<label for='isExternal'>external:</label>
|
||||||
<input type='checkbox' id='isExternal'/>
|
<input type='checkbox' id='isExternal'/>
|
||||||
</p>
|
</p>
|
||||||
|
<p>
|
||||||
|
<label for='isNativeNotification'>Native Electron Notification:</label>
|
||||||
|
<input type='checkbox' id='isNativeNotification'/>
|
||||||
|
</p>
|
||||||
<p>Change theme:<p>
|
<p>Change theme:<p>
|
||||||
<select id="select-theme" name="theme">
|
<select id="select-theme" name="theme">
|
||||||
<option value="">select</option>
|
<option value="">select</option>
|
||||||
@ -267,6 +278,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
var notfEl = document.getElementById('notf');
|
var notfEl = document.getElementById('notf');
|
||||||
|
var closeNotificationEl = document.getElementById('closeNotification');
|
||||||
var num = 0;
|
var num = 0;
|
||||||
|
|
||||||
// note: notification will close when clicked
|
// note: notification will close when clicked
|
||||||
@ -280,6 +292,7 @@
|
|||||||
var color = document.getElementById('color').value;
|
var color = document.getElementById('color').value;
|
||||||
var tag = document.getElementById('tag').value;
|
var tag = document.getElementById('tag').value;
|
||||||
var company = document.getElementById('company').value;
|
var company = document.getElementById('company').value;
|
||||||
|
var isNativeNotification = document.getElementById('isNativeNotification').checked;
|
||||||
|
|
||||||
num++;
|
num++;
|
||||||
|
|
||||||
@ -298,6 +311,8 @@
|
|||||||
hello: `Notification with id ${num} clicked')`
|
hello: `Notification with id ${num} clicked')`
|
||||||
},
|
},
|
||||||
tag: tag,
|
tag: tag,
|
||||||
|
isElectronNotification: isNativeNotification,
|
||||||
|
hasReply: true,
|
||||||
method: 'notification',
|
method: 'notification',
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -323,7 +338,9 @@
|
|||||||
};
|
};
|
||||||
ssfNotificationHandler.addEventListener('error', onerror);
|
ssfNotificationHandler.addEventListener('error', onerror);
|
||||||
} else if (window.manaSSF) {
|
} else if (window.manaSSF) {
|
||||||
const callback = () => { console.log('notification clicked') };
|
const callback = (event, data) => {
|
||||||
|
document.getElementById('reply').innerText = data.notificationData;
|
||||||
|
};
|
||||||
window.manaSSF.showNotification(notf, callback);
|
window.manaSSF.showNotification(notf, callback);
|
||||||
} else {
|
} else {
|
||||||
window.postMessage({ method: 'notification', data: notf }, '*');
|
window.postMessage({ method: 'notification', data: notf }, '*');
|
||||||
@ -331,6 +348,13 @@
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
closeNotificationEl.addEventListener('click', () => {
|
||||||
|
if (window.manaSSF) {
|
||||||
|
const id = document.getElementById('notificationTag').value;
|
||||||
|
window.manaSSF.closeNotification(parseInt(id));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Badge Count
|
* Badge Count
|
||||||
*/
|
*/
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { contextBridge, ipcRenderer, remote, webFrame } from 'electron';
|
import { contextBridge, ipcRenderer, webFrame } from 'electron';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import * as ReactDOM from 'react-dom';
|
import * as ReactDOM from 'react-dom';
|
||||||
import { apiCmds, apiName } from '../common/api-interface';
|
import { apiCmds, apiName } from '../common/api-interface';
|
||||||
@ -22,7 +22,6 @@ const minMemoryFetchInterval = 4 * 60 * 60 * 1000;
|
|||||||
const maxMemoryFetchInterval = 12 * 60 * 60 * 1000;
|
const maxMemoryFetchInterval = 12 * 60 * 60 * 1000;
|
||||||
const snackBar = new SnackBar();
|
const snackBar = new SnackBar();
|
||||||
const banner = new MessageBanner();
|
const banner = new MessageBanner();
|
||||||
const notification = remote.require('../renderer/notification').notification;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* creates API exposed from electron.
|
* creates API exposed from electron.
|
||||||
@ -83,8 +82,8 @@ if (ssfWindow.ssf) {
|
|||||||
registerRestartFloater: ssfWindow.ssf.registerRestartFloater,
|
registerRestartFloater: ssfWindow.ssf.registerRestartFloater,
|
||||||
setCloudConfig: ssfWindow.ssf.setCloudConfig,
|
setCloudConfig: ssfWindow.ssf.setCloudConfig,
|
||||||
checkMediaPermission: ssfWindow.ssf.checkMediaPermission,
|
checkMediaPermission: ssfWindow.ssf.checkMediaPermission,
|
||||||
showNotification: notification.showNotification,
|
showNotification: ssfWindow.ssf.showNotification,
|
||||||
closeNotification: notification.hideNotification,
|
closeNotification: ssfWindow.ssf.closeNotification,
|
||||||
restartApp: ssfWindow.ssf.restartApp,
|
restartApp: ssfWindow.ssf.restartApp,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import {
|
|||||||
ICPUUsage,
|
ICPUUsage,
|
||||||
ILogMsg,
|
ILogMsg,
|
||||||
IMediaPermission,
|
IMediaPermission,
|
||||||
|
INotificationData,
|
||||||
IRestartFloaterData,
|
IRestartFloaterData,
|
||||||
IScreenSharingIndicator,
|
IScreenSharingIndicator,
|
||||||
IScreenSharingIndicatorOptions,
|
IScreenSharingIndicatorOptions,
|
||||||
@ -18,6 +19,7 @@ import {
|
|||||||
IVersionInfo,
|
IVersionInfo,
|
||||||
KeyCodes,
|
KeyCodes,
|
||||||
LogLevel,
|
LogLevel,
|
||||||
|
NotificationActionCallback,
|
||||||
} from '../common/api-interface';
|
} from '../common/api-interface';
|
||||||
import { i18n, LocaleType } from '../common/i18n-preload';
|
import { i18n, LocaleType } from '../common/i18n-preload';
|
||||||
import { throttle } from '../common/utils';
|
import { throttle } from '../common/utils';
|
||||||
@ -51,6 +53,8 @@ const local: ILocalObject = {
|
|||||||
ipcRenderer,
|
ipcRenderer,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const notificationActionCallbacks = new Map<number, NotificationActionCallback>();
|
||||||
|
|
||||||
// Throttle func
|
// Throttle func
|
||||||
const throttledSetBadgeCount = throttle((count) => {
|
const throttledSetBadgeCount = throttle((count) => {
|
||||||
local.ipcRenderer.send(apiName.symphonyApi, {
|
local.ipcRenderer.send(apiName.symphonyApi, {
|
||||||
@ -217,7 +221,7 @@ export class SSFApi {
|
|||||||
containerIdentifier: appName,
|
containerIdentifier: appName,
|
||||||
containerVer: appVer,
|
containerVer: appVer,
|
||||||
buildNumber,
|
buildNumber,
|
||||||
apiVer: '2.0.0',
|
apiVer: '3.0.0',
|
||||||
cpuArch,
|
cpuArch,
|
||||||
// Only need to bump if there are any breaking changes.
|
// Only need to bump if there are any breaking changes.
|
||||||
searchApiVer: searchAPIVersion,
|
searchApiVer: searchAPIVersion,
|
||||||
@ -590,6 +594,39 @@ export class SSFApi {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a notification from the main process
|
||||||
|
* @param notificationOpts {INotificationData}
|
||||||
|
* @param notificationCallback {NotificationActionCallback}
|
||||||
|
*/
|
||||||
|
public showNotification(notificationOpts: INotificationData, notificationCallback: NotificationActionCallback): void {
|
||||||
|
// Store callbacks based on notification id so,
|
||||||
|
// we can use this to trigger on notification action
|
||||||
|
if (typeof notificationOpts.id === 'number') {
|
||||||
|
notificationActionCallbacks.set(notificationOpts.id, notificationCallback);
|
||||||
|
}
|
||||||
|
// ipc does not support sending Functions, Promises, Symbols, WeakMaps,
|
||||||
|
// or WeakSets will throw an exception
|
||||||
|
if (notificationOpts.callback) {
|
||||||
|
delete notificationOpts.callback;
|
||||||
|
}
|
||||||
|
ipcRenderer.send(apiName.symphonyApi, {
|
||||||
|
cmd: apiCmds.showNotification,
|
||||||
|
notificationOpts,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes a specific notification based on id
|
||||||
|
* @param notificationId {number} Id of a notification
|
||||||
|
*/
|
||||||
|
public closeNotification(notificationId: number): void {
|
||||||
|
ipcRenderer.send(apiName.symphonyApi, {
|
||||||
|
cmd: apiCmds.closeNotification,
|
||||||
|
notificationId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -770,6 +807,19 @@ local.ipcRenderer.on('restart-floater', (_event, { windowName, bounds }: IRestar
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An event triggered by the main process on notification actions
|
||||||
|
* @param {INotificationData}
|
||||||
|
*/
|
||||||
|
local.ipcRenderer.on('notification-actions', (_event, args) => {
|
||||||
|
const callback = notificationActionCallbacks.get(args.data.id);
|
||||||
|
const data = args.data;
|
||||||
|
data.notificationData = args.notificationData;
|
||||||
|
if (args && callback) {
|
||||||
|
callback(args.event, data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Invoked whenever the app is reloaded/navigated
|
// Invoked whenever the app is reloaded/navigated
|
||||||
const sanitize = (): void => {
|
const sanitize = (): void => {
|
||||||
if (window.name === apiName.mainWindowName) {
|
if (window.name === apiName.mainWindowName) {
|
||||||
|
Loading…
Reference in New Issue
Block a user