Typescript - Add set badge count api

This commit is contained in:
Kiran Niranjan 2018-12-01 15:16:55 +05:30
parent 8c44c04540
commit a4858f336b
11 changed files with 235 additions and 48 deletions

View File

@ -1,27 +1,33 @@
import { ipcMain } from 'electron';
import { BrowserWindow, ipcMain } from 'electron';
import { apiCmds, apiName, IApiArgs } from '../common/api-interface';
import { logger } from '../common/logger';
import { windowHandler } from './window-handler';
import { setDataUrl, showBadgeCount } from './window-utils';
const checkValidWindow = true;
/**
* Ensure events comes from a window that we have created.
* @param {EventEmitter} event node emitter event to be tested
* @return {Boolean} returns true if exists otherwise false
*/
function isValidWindow(event: Electron.Event) {
/*if (!checkValidWindow) {
function isValidWindow(event: Electron.Event): boolean {
if (!checkValidWindow) {
return true;
}*/
const result = false;
}
let result = false;
if (event && event.sender) {
// validate that event sender is from window we created
// const browserWin = BrowserWindow.fromWebContents(event.sender);
const browserWin = BrowserWindow.fromWebContents(event.sender);
// @ts-ignore
const winKey = event.sender.browserWindowOptions && event.sender.browserWindowOptions.winKey;
// result = windowMgr.hasWindow(browserWin, event.sender.id);
result = windowHandler.hasWindow(winKey, browserWin);
}
if (!result) {
// log.send(logLevels.WARN, 'invalid window try to perform action, ignoring action');
logger.warn('invalid window try to perform action, ignoring action', event.sender);
}
return result;
@ -45,22 +51,23 @@ ipcMain.on(apiName.symphonyApi, (event: Electron.Event, arg: IApiArgs) => {
if (typeof arg.isOnline === 'boolean') {
windowMgr.setIsOnline(arg.isOnline);
}
break;
case ApiCmds.setBadgeCount:
break;*/
case apiCmds.setBadgeCount:
if (typeof arg.count === 'number') {
badgeCount.show(arg.count);
console.log(arg.count);
showBadgeCount(arg.count);
}
break;
case ApiCmds.registerProtocolHandler:
/*case ApiCmds.registerProtocolHandler:
protocolHandler.setProtocolWindow(event.sender);
protocolHandler.checkProtocolAction();
break;
case ApiCmds.badgeDataUrl:
break;*/
case apiCmds.badgeDataUrl:
if (typeof arg.dataUrl === 'string' && typeof arg.count === 'number') {
badgeCount.setDataUrl(arg.dataUrl, arg.count);
setDataUrl(arg.dataUrl, arg.count);
}
break;
case ApiCmds.activate:
/*case ApiCmds.activate:
if (typeof arg.windowName === 'string') {
windowMgr.activate(arg.windowName);
}

View File

@ -7,6 +7,7 @@ import { cleanUpAppCache, createAppCacheFile } from './app-cache-handler';
import { autoLaunchInstance } from './auto-launch-controller';
import setChromeFlags from './chrome-flags';
import { config } from './config-handler';
import './main-api-handler';
import { windowHandler } from './window-handler';
const allowMultiInstance: string | boolean = getCommandLineArgs(process.argv, '--multiInstance', true) || isDevEnv;

View File

@ -3,11 +3,15 @@ import * as fs from 'fs';
import * as path from 'path';
import * as url from 'url';
import { getCommandLineArgs } from '../common/utils';
import { getCommandLineArgs, getGuid } from '../common/utils';
import { config, IConfig } from './config-handler';
import { createComponentWindow } from './window-utils';
const { buildNumber, clientVersion, version } = require('../../package.json');// tslint:disable-line:no-var-requires
const { buildNumber, clientVersion, version } = require('../../package.json'); // tslint:disable-line:no-var-requires
interface ICustomBrowserWindowConstructorOpts extends Electron.BrowserWindowConstructorOptions {
winKey: string;
}
export class WindowHandler {
@ -28,6 +32,7 @@ export class WindowHandler {
preload: path.join(__dirname, '../renderer/preload-main'),
sandbox: false,
},
winKey: getGuid(),
};
}
@ -65,15 +70,17 @@ export class WindowHandler {
return url.format(parsedUrl);
}
private readonly windowOpts: Electron.BrowserWindowConstructorOptions;
private readonly windowOpts: ICustomBrowserWindowConstructorOpts;
private readonly globalConfig: IConfig;
// Window reference
private readonly windows: object;
private mainWindow: Electron.BrowserWindow | null;
private loadingWindow: Electron.BrowserWindow | null;
private aboutAppWindow: Electron.BrowserWindow | null;
constructor(opts?: Electron.BrowserViewConstructorOptions) {
this.windowOpts = { ... WindowHandler.getMainWindowOpts(), ...opts };
this.windows = {};
this.windowOpts = { ...WindowHandler.getMainWindowOpts(), ...opts };
this.mainWindow = null;
this.loadingWindow = null;
this.aboutAppWindow = null;
@ -108,6 +115,7 @@ export class WindowHandler {
}
this.createAboutAppWindow();
});
this.addWindow(this.windowOpts.winKey, this.mainWindow);
return this.mainWindow;
}
@ -118,6 +126,16 @@ export class WindowHandler {
return this.mainWindow;
}
/**
* Checks if the window and a key has a window
* @param key {string}
* @param window {Electron.BrowserWindow}
*/
public hasWindow(key: string, window: Electron.BrowserWindow): boolean {
const browserWindow = this.windows[key];
return browserWindow && window === browserWindow;
}
/**
* Displays a loading window until the main
* application is loaded
@ -138,11 +156,20 @@ export class WindowHandler {
*/
public createAboutAppWindow() {
this.aboutAppWindow = createComponentWindow('about-app');
this.aboutAppWindow.webContents.once('did-finish-load', () => {
if (this.aboutAppWindow) {
this.aboutAppWindow.webContents.send('about-app-data', { buildNumber, clientVersion, version });
}
});
this.aboutAppWindow.webContents.once('did-finish-load', () => {
if (this.aboutAppWindow) {
this.aboutAppWindow.webContents.send('about-app-data', { buildNumber, clientVersion, version });
}
});
}
/**
* Stores information of all the window we have created
* @param key {string}
* @param browserWindow {Electron.BrowserWindow}
*/
private addWindow(key: string, browserWindow: Electron.BrowserWindow): void {
this.windows[key] = browserWindow;
}
}

View File

@ -1,6 +1,9 @@
import { BrowserWindow } from 'electron';
import { app, BrowserWindow, nativeImage } from 'electron';
import * as path from 'path';
import * as url from 'url';
import { isMac } from '../common/env';
import { logger } from '../common/logger';
import { windowHandler } from './window-handler';
/**
@ -58,4 +61,49 @@ export function preventWindowNavigation(browserWindow: Electron.BrowserWindow) {
};
browserWindow.webContents.on('will-navigate', listener);
}
/**
* Shows the badge count
* @param count {number}
*/
export function showBadgeCount(count: number): void {
if (typeof count !== 'number') {
logger.warn(`badgeCount: invalid func arg, must be a number: ${count}`);
return;
}
if (isMac) {
// too big of a number here and setBadgeCount crashes
app.setBadgeCount(Math.min(1e8, count));
return;
}
// handle ms windows...
const mainWindow = windowHandler.getMainWindow();
if (mainWindow) {
if (count > 0) {
// get badge img from renderer process, will return
// img dataUrl in setDataUrl func.
mainWindow.webContents.send('create-badge-data-url', { count });
} else {
// clear badge count icon
mainWindow.setOverlayIcon(null, '');
}
}
}
/**
* Sets the data url
* @param dataUrl
* @param count
*/
export function setDataUrl(dataUrl: string, count: number): void {
const mainWindow = windowHandler.getMainWindow();
if (mainWindow && dataUrl && count) {
const img = nativeImage.createFromDataURL(dataUrl);
// for accessibility screen readers
const desc = 'Symphony has ' + count + ' unread messages';
mainWindow.setOverlayIcon(img, desc);
}
}

View File

@ -1,22 +1,22 @@
export enum apiCmds {
isOnline,
registerLogger,
setBadgeCount,
badgeDataUrl,
activate,
registerBoundsChange,
registerProtocolHandler,
registerActivityDetection,
showNotificationSettings,
sanitize,
bringToFront,
openScreenPickerWindow,
popupMenu,
optimizeMemoryConsumption,
optimizeMemoryRegister,
setIsInMeeting,
setLocale,
keyPress,
isOnline = 'is-online',
registerLogger = 'register-logger',
setBadgeCount = 'set-badge-count',
badgeDataUrl = 'badge-data-url',
activate = 'activate',
registerBoundsChange = 'register-bounds-change',
registerProtocolHandler = 'register-protocol-handler',
registerActivityDetection = 'register-activity-detection',
showNotificationSettings = 'show-notification-settings',
sanitize = 'sanitize',
bringToFront = 'bring-to-front',
openScreenPickerWindow = 'open-screen-picker-window',
popupMenu = 'popup-menu',
optimizeMemoryConsumption = 'optimize-memory-consumption',
optimizeMemoryRegister = 'optimize-memory-register',
setIsInMeeting = 'set-is-in-meeting',
setLocale = 'set-locale',
keyPress = 'key-press',
}
export enum apiName {
@ -33,7 +33,6 @@ export interface IApiArgs {
reason: string;
sources: Electron.DesktopCapturerSource[];
id: number;
memory: Electron.ProcessMemoryInfo;
cpuUsage: Electron.CPUUsage;
isInMeeting: boolean;
locale: string;

View File

@ -179,7 +179,7 @@ const formatString = (str: string, data?: object): string => {
* @param wait
* @example const throttled = throttle(anyFunc, 500);
*/
const throttle = (func: () => void, wait: number): () => void => {
const throttle = (func: (...args) => void, wait: number): (...args) => void => {
if (wait <= 0) {
throw Error('throttle: invalid throttleTime arg, must be a number: ' + wait);
}

View File

@ -46,7 +46,7 @@ export default class AboutApp extends React.Component<{}, IState> {
}
public componentWillUnmount(): void {
ipcRenderer.removeListener('open-file-reply', this.updateState);
ipcRenderer.removeListener('about-app-data', this.updateState);
}
/**

View File

@ -2,6 +2,7 @@ import * as React from 'react';
import * as ReactDOM from 'react-dom';
import WindowsTitleBar from '../renderer/windows-title-bar';
import { SSFApi } from './ssf-api';
document.addEventListener('DOMContentLoaded', load);
@ -11,4 +12,27 @@ document.addEventListener('DOMContentLoaded', load);
function load() {
const element = React.createElement(WindowsTitleBar);
ReactDOM.render(element, document.body);
}
createAPI();
/**
* creates API exposed from electron.
*/
function createAPI() {
// iframes (and any other non-top level frames) get no api access
// http://stackoverflow.com/questions/326069/how-to-identify-if-a-webpage-is-being-loaded-inside-an-iframe-or-directly-into-t/326076
if (window.self !== window.top) {
return;
}
// note: window.open from main window (if in the same domain) will get
// api access. window.open in another domain will be opened in the default
// browser (see: handler for event 'new-window' in windowMgr.js)
//
// API exposed to renderer process.
//
// @ts-ignore
window.ssf = new SSFApi();
}

77
src/renderer/ssf-api.ts Normal file
View File

@ -0,0 +1,77 @@
import { ipcRenderer } from 'electron';
import { apiCmds, apiName } from '../common/api-interface';
import { throttle } from '../common/utils';
const local = {
ipcRenderer,
};
// Throttle func
const throttleSetBadgeCount = throttle((count) => {
console.log(count);
console.log(apiCmds.setBadgeCount);
local.ipcRenderer.send(apiName.symphonyApi, {
cmd: apiCmds.setBadgeCount,
count,
});
}, 1000);
export class SSFApi {
/**
* sets the count on the tray icon to the given number.
* @param {number} count count to be displayed
* note: count of 0 will remove the displayed count.
* note: for mac the number displayed will be 1 to infinity
* note: for windws the number displayed will be 1 to 99 and 99+
*/
public setBadgeCount(count: number): void {
console.log(count);
throttleSetBadgeCount(count);
}
}
/**
* Ipc events
*/
// Creates a data url
ipcRenderer.on('create-badge-data-url', (arg) => {
const count = arg && arg.count || 0;
// create 32 x 32 img
const radius = 16;
const canvas = document.createElement('canvas');
canvas.height = radius * 2;
canvas.width = radius * 2;
const ctx = canvas.getContext('2d');
if (ctx) {
ctx.fillStyle = 'red';
ctx.beginPath();
ctx.arc(radius, radius, radius, 0, 2 * Math.PI, false);
ctx.fill();
ctx.textAlign = 'center';
ctx.fillStyle = 'white';
const text = count > 99 ? '99+' : count.toString();
if (text.length > 2) {
ctx.font = 'bold 18px sans-serif';
ctx.fillText(text, radius, 22);
} else if (text.length > 1) {
ctx.font = 'bold 24px sans-serif';
ctx.fillText(text, radius, 24);
} else {
ctx.font = 'bold 26px sans-serif';
ctx.fillText(text, radius, 26);
}
const dataUrl = canvas.toDataURL('image/png', 1.0);
local.ipcRenderer.send(apiName.symphonyApi, {
cmd: apiCmds.badgeDataUrl,
count,
dataUrl,
});
}
});

View File

@ -104,4 +104,8 @@
width: 100%;
z-index: 3000;
bottom: 0;
}
.symphony-logo {
content: url("../src/renderer/assets/symphony-title-bar-logo.png");
}

View File

@ -86,7 +86,7 @@ export default class WindowsTitleBar extends React.Component<{}, IState> {
</button>
</div>
<div className='title-container'>
<img src='./assets/symphony-title-bar-logo.png'/>
<img className='symphony-logo'/>
<p className='title-bar-title'>{document.title || 'Symphony'}</p>
</div>
<div className='title-bar-button-container'>