2019-04-05 00:20:54 -05:00
|
|
|
import { app, ipcMain } from 'electron';
|
2019-03-19 05:52:39 -05:00
|
|
|
|
2019-03-25 03:23:10 -05:00
|
|
|
import { config } from '../app/config-handler';
|
2019-03-19 05:52:39 -05:00
|
|
|
import { createComponentWindow, windowExists } from '../app/window-utils';
|
|
|
|
import { AnimationQueue } from '../common/animation-queue';
|
2019-04-05 00:20:54 -05:00
|
|
|
import { apiName, INotificationData } from '../common/api-interface';
|
2019-03-19 05:52:39 -05:00
|
|
|
import { logger } from '../common/logger';
|
|
|
|
import NotificationHandler from './notification-handler';
|
|
|
|
|
|
|
|
// const MAX_QUEUE_SIZE = 30;
|
2019-03-25 03:23:10 -05:00
|
|
|
const CLEAN_UP_INTERVAL = 60 * 1000; // Closes inactive notification
|
2019-03-19 05:52:39 -05:00
|
|
|
const animationQueue = new AnimationQueue();
|
|
|
|
|
|
|
|
interface ICustomBrowserWindow extends Electron.BrowserWindow {
|
2019-04-05 00:20:54 -05:00
|
|
|
winName: string;
|
2019-03-19 05:52:39 -05:00
|
|
|
notificationData: INotificationData;
|
|
|
|
displayTimer: NodeJS.Timer;
|
|
|
|
clientId: number;
|
|
|
|
}
|
|
|
|
|
|
|
|
type startCorner = 'upper-right' | 'upper-left' | 'lower-right' | 'lower-left';
|
|
|
|
|
|
|
|
const notificationSettings = {
|
|
|
|
startCorner: 'upper-right' as startCorner,
|
2019-03-25 03:23:10 -05:00
|
|
|
display: '',
|
2019-03-19 05:52:39 -05:00
|
|
|
width: 380,
|
|
|
|
height: 100,
|
|
|
|
totalHeight: 0,
|
|
|
|
totalWidth: 0,
|
|
|
|
corner: {
|
|
|
|
x: 0,
|
|
|
|
y: 0,
|
|
|
|
},
|
|
|
|
firstPos: {
|
|
|
|
x: 0,
|
|
|
|
y: 0,
|
|
|
|
},
|
|
|
|
templatePath: '',
|
|
|
|
maxVisibleNotifications: 6,
|
|
|
|
borderRadius: 5,
|
|
|
|
displayTime: 5000,
|
|
|
|
animationSteps: 5,
|
|
|
|
animationStepMs: 5,
|
|
|
|
logging: true,
|
|
|
|
};
|
|
|
|
|
|
|
|
class Notification extends NotificationHandler {
|
|
|
|
|
|
|
|
private readonly funcHandlers = {
|
|
|
|
onCleanUpInactiveNotification: () => this.cleanUpInactiveNotification(),
|
|
|
|
onCreateNotificationWindow: (data: INotificationData) => this.createNotificationWindow(data),
|
|
|
|
};
|
2019-04-05 00:20:54 -05:00
|
|
|
private activeNotifications: Electron.BrowserWindow[] = [];
|
|
|
|
private inactiveWindows: Electron.BrowserWindow[] = [];
|
2019-03-19 05:52:39 -05:00
|
|
|
private cleanUpTimer: NodeJS.Timer;
|
2019-04-05 00:20:54 -05:00
|
|
|
private notificationQueue: INotificationData[] = [];
|
|
|
|
|
|
|
|
private readonly notificationCallbacks: any[] = [];
|
2019-03-19 05:52:39 -05:00
|
|
|
|
|
|
|
constructor(opts) {
|
|
|
|
super(opts);
|
|
|
|
ipcMain.on('close-notification', (_event, windowId) => {
|
|
|
|
this.hideNotification(windowId);
|
|
|
|
});
|
|
|
|
|
|
|
|
ipcMain.on('notification-clicked', (_event, windowId) => {
|
|
|
|
this.notificationClicked(windowId);
|
|
|
|
});
|
2019-03-25 03:23:10 -05:00
|
|
|
// Update latest notification settings from config
|
2019-04-05 00:20:54 -05:00
|
|
|
app.on('ready', () => this.updateNotificationSettings());
|
2019-03-19 05:52:39 -05:00
|
|
|
this.cleanUpTimer = setInterval(this.funcHandlers.onCleanUpInactiveNotification, CLEAN_UP_INTERVAL);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Displays a new notification
|
|
|
|
*
|
|
|
|
* @param data
|
|
|
|
* @param callback
|
|
|
|
*/
|
|
|
|
public showNotification(data: INotificationData, callback): void {
|
|
|
|
clearInterval(this.cleanUpTimer);
|
|
|
|
animationQueue.push({
|
|
|
|
func: this.funcHandlers.onCreateNotificationWindow,
|
|
|
|
args: [ data ],
|
|
|
|
});
|
|
|
|
this.notificationCallbacks[ data.id ] = callback;
|
|
|
|
this.cleanUpTimer = setInterval(this.funcHandlers.onCleanUpInactiveNotification, CLEAN_UP_INTERVAL);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates a new notification window
|
|
|
|
*
|
|
|
|
* @param data
|
|
|
|
*/
|
|
|
|
public async createNotificationWindow(data): Promise<ICustomBrowserWindow | undefined> {
|
|
|
|
|
2019-03-25 03:23:10 -05:00
|
|
|
// TODO: Handle MAX_QUEUE_SIZE
|
2019-03-19 05:52:39 -05:00
|
|
|
if (data.tag) {
|
|
|
|
for (let i = 0; i < this.notificationQueue.length; i++) {
|
|
|
|
if (this.notificationQueue[ i ].tag === data.tag) {
|
|
|
|
this.notificationQueue[ i ] = data;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const window of this.activeNotifications) {
|
|
|
|
const notificationWin = window as ICustomBrowserWindow;
|
|
|
|
if (window && notificationWin.notificationData.tag === data.tag) {
|
|
|
|
this.setNotificationContent(notificationWin, data);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Checks if number of active notification displayed is greater than or equal to the
|
|
|
|
// max displayable notification and queues them
|
|
|
|
if (this.activeNotifications.length >= this.settings.maxVisibleNotifications) {
|
|
|
|
this.notificationQueue.push(data);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Checks for the cashed window and use them
|
|
|
|
if (this.inactiveWindows.length > 0) {
|
|
|
|
const inactiveWin = this.inactiveWindows[0] as ICustomBrowserWindow;
|
|
|
|
if (windowExists(inactiveWin)) {
|
|
|
|
this.inactiveWindows.splice(0, 1);
|
|
|
|
this.renderNotification(inactiveWin, data);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const notificationWindow = createComponentWindow(
|
|
|
|
'notification-comp',
|
|
|
|
this.getNotificationOpts(),
|
|
|
|
false,
|
|
|
|
) as ICustomBrowserWindow;
|
|
|
|
|
|
|
|
notificationWindow.notificationData = data;
|
2019-04-05 00:20:54 -05:00
|
|
|
notificationWindow.winName = apiName.notificationWindowName;
|
2019-03-19 05:52:39 -05:00
|
|
|
notificationWindow.once('closed', () => {
|
|
|
|
const activeWindowIndex = this.activeNotifications.indexOf(notificationWindow);
|
|
|
|
const inactiveWindowIndex = this.inactiveWindows.indexOf(notificationWindow);
|
|
|
|
|
|
|
|
if (activeWindowIndex !== -1) {
|
|
|
|
this.activeNotifications.splice(activeWindowIndex, 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (inactiveWindowIndex !== -1) {
|
|
|
|
this.inactiveWindows.splice(inactiveWindowIndex, 1);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return await this.didFinishLoad(notificationWindow, data);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sets the notification contents
|
|
|
|
*
|
|
|
|
* @param notificationWindow
|
|
|
|
* @param data {INotificationData}
|
|
|
|
*/
|
|
|
|
public setNotificationContent(notificationWindow: ICustomBrowserWindow, data: INotificationData): void {
|
|
|
|
notificationWindow.clientId = data.id;
|
|
|
|
const displayTime = data.displayTime ? data.displayTime : notificationSettings.displayTime;
|
|
|
|
let timeoutId;
|
|
|
|
|
|
|
|
if (!data.sticky) {
|
|
|
|
timeoutId = setTimeout(async () => {
|
|
|
|
await this.hideNotification(notificationWindow.clientId);
|
|
|
|
}, displayTime);
|
|
|
|
notificationWindow.displayTimer = timeoutId;
|
|
|
|
}
|
|
|
|
|
|
|
|
notificationWindow.webContents.send('notification-data', data);
|
|
|
|
notificationWindow.showInactive();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Hides the notification window
|
|
|
|
*
|
|
|
|
* @param clientId
|
|
|
|
*/
|
|
|
|
public async hideNotification(clientId: number): Promise<void> {
|
|
|
|
const browserWindow = this.getNotificationWindow(clientId);
|
|
|
|
if (browserWindow && windowExists(browserWindow)) {
|
|
|
|
// send empty to reset the state
|
|
|
|
const pos = this.activeNotifications.indexOf(browserWindow);
|
|
|
|
this.activeNotifications.splice(pos, 1);
|
|
|
|
|
|
|
|
if (this.inactiveWindows.length < this.settings.maxVisibleNotifications || 5) {
|
|
|
|
this.inactiveWindows.push(browserWindow);
|
|
|
|
browserWindow.hide();
|
|
|
|
} else {
|
|
|
|
browserWindow.close();
|
|
|
|
}
|
|
|
|
|
|
|
|
this.moveNotificationDown(pos, this.activeNotifications);
|
|
|
|
|
|
|
|
if (this.notificationQueue.length > 0 && this.activeNotifications.length < this.settings.maxVisibleNotifications) {
|
|
|
|
const notificationData = this.notificationQueue[0];
|
|
|
|
this.notificationQueue.splice(0, 1);
|
|
|
|
animationQueue.push({
|
|
|
|
func: this.funcHandlers.onCreateNotificationWindow,
|
|
|
|
args: [ notificationData ],
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles notification click
|
|
|
|
*
|
|
|
|
* @param clientId {number}
|
|
|
|
*/
|
|
|
|
public notificationClicked(clientId): void {
|
|
|
|
const browserWindow = this.getNotificationWindow(clientId);
|
|
|
|
if (browserWindow && windowExists(browserWindow) && browserWindow.notificationData) {
|
|
|
|
const data = browserWindow.notificationData;
|
|
|
|
const callback = this.notificationCallbacks[ clientId ];
|
|
|
|
if (typeof callback === 'function') {
|
|
|
|
this.notificationCallbacks[ clientId ]('notification-clicked', data);
|
|
|
|
}
|
|
|
|
this.hideNotification(clientId);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the notification based on the client id
|
|
|
|
*
|
|
|
|
* @param clientId {number}
|
|
|
|
*/
|
|
|
|
public getNotificationWindow(clientId: number): ICustomBrowserWindow | undefined {
|
|
|
|
const index: number = this.activeNotifications.findIndex((win) => {
|
|
|
|
const notificationWindow = win as ICustomBrowserWindow;
|
|
|
|
return notificationWindow.clientId === clientId;
|
|
|
|
});
|
|
|
|
if (index === -1) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
return this.activeNotifications[ index ] as ICustomBrowserWindow;
|
|
|
|
}
|
|
|
|
|
2019-03-25 03:23:10 -05:00
|
|
|
/**
|
|
|
|
* Update latest notification settings from config
|
|
|
|
*/
|
|
|
|
public updateNotificationSettings(): void {
|
|
|
|
const { display, position } = config.getConfigFields([ 'notificationSettings' ]).notificationSettings;
|
|
|
|
this.settings.displayId = display;
|
|
|
|
this.settings.startCorner = position as startCorner;
|
|
|
|
|
|
|
|
// recalculate notification position
|
|
|
|
this.setupNotificationPosition();
|
|
|
|
this.moveNotificationDown(0, this.activeNotifications);
|
|
|
|
}
|
|
|
|
|
2019-04-05 00:20:54 -05:00
|
|
|
/**
|
|
|
|
* Closes all the notification windows and resets some configurations
|
|
|
|
*/
|
|
|
|
public async cleanUp(): Promise<void> {
|
|
|
|
animationQueue.clear();
|
|
|
|
this.notificationQueue = [];
|
|
|
|
const activeNotificationWindows = Object.assign([], this.activeNotifications);
|
|
|
|
const inactiveNotificationWindows = Object.assign([], this.inactiveWindows);
|
|
|
|
for (const activeWindow of activeNotificationWindows) {
|
|
|
|
if (activeWindow && windowExists(activeWindow)) {
|
|
|
|
await this.hideNotification((activeWindow as ICustomBrowserWindow).clientId);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for (const inactiveWindow of inactiveNotificationWindows) {
|
|
|
|
if (inactiveWindow && windowExists(inactiveWindow)) {
|
|
|
|
inactiveWindow.close();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
this.activeNotifications = [];
|
|
|
|
this.inactiveWindows = [];
|
|
|
|
}
|
|
|
|
|
2019-03-19 05:52:39 -05:00
|
|
|
/**
|
|
|
|
* Waits for window to load and resolves
|
|
|
|
*
|
|
|
|
* @param window
|
|
|
|
* @param data
|
|
|
|
*/
|
|
|
|
private didFinishLoad(window, data) {
|
|
|
|
return new Promise<ICustomBrowserWindow>((resolve) => {
|
|
|
|
window.webContents.once('did-finish-load', () => {
|
|
|
|
if (windowExists(window)) {
|
|
|
|
this.renderNotification(window, data);
|
|
|
|
}
|
|
|
|
return resolve(window);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Calculates all the required attributes and displays the notification
|
|
|
|
*
|
|
|
|
* @param notificationWindow {BrowserWindow}
|
|
|
|
* @param data {INotificationData}
|
|
|
|
*/
|
|
|
|
private renderNotification(notificationWindow, data): void {
|
|
|
|
this.calcNextInsertPos(this.activeNotifications.length);
|
|
|
|
this.setWindowPosition(notificationWindow, this.nextInsertPos.x, this.nextInsertPos.y);
|
|
|
|
this.setNotificationContent(notificationWindow, { ...data, windowId: notificationWindow.id });
|
|
|
|
this.activeNotifications.push(notificationWindow);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Closes the active notification after certain period
|
|
|
|
*/
|
|
|
|
private cleanUpInactiveNotification() {
|
|
|
|
if (this.inactiveWindows.length > 0) {
|
2019-05-17 06:06:50 -05:00
|
|
|
logger.info('notification: cleaning up inactive notification windows', {inactiveNotification: this.inactiveWindows.length});
|
2019-03-19 05:52:39 -05:00
|
|
|
this.inactiveWindows.forEach((window) => {
|
|
|
|
if (windowExists(window)) {
|
|
|
|
window.close();
|
|
|
|
}
|
|
|
|
});
|
2019-05-17 06:06:50 -05:00
|
|
|
logger.info(`notification: cleaned up inactive notification windows`, {inactiveNotification: this.inactiveWindows.length});
|
2019-03-19 05:52:39 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* notification window opts
|
|
|
|
*/
|
|
|
|
private getNotificationOpts(): Electron.BrowserWindowConstructorOptions {
|
|
|
|
return {
|
|
|
|
width: 380,
|
|
|
|
height: 100,
|
|
|
|
alwaysOnTop: true,
|
|
|
|
skipTaskbar: true,
|
|
|
|
resizable: false,
|
|
|
|
show: false,
|
|
|
|
frame: false,
|
|
|
|
transparent: true,
|
|
|
|
acceptFirstMouse: true,
|
|
|
|
webPreferences: {
|
|
|
|
sandbox: true,
|
|
|
|
nodeIntegration: false,
|
|
|
|
devTools: true,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const notification = new Notification(notificationSettings);
|
|
|
|
|
|
|
|
export {
|
|
|
|
notification,
|
|
|
|
};
|