From a4858f336b30dc2e11883c32a64abac6c76bd177 Mon Sep 17 00:00:00 2001 From: Kiran Niranjan Date: Sat, 1 Dec 2018 15:16:55 +0530 Subject: [PATCH] Typescript - Add set badge count api --- src/browser/main-api-handler.ts | 39 ++++++++------- src/browser/main.ts | 1 + src/browser/window-handler.ts | 45 +++++++++++++---- src/browser/window-utils.ts | 50 ++++++++++++++++++- src/common/api-interface.ts | 37 +++++++------- src/common/utils.ts | 2 +- src/renderer/about-app.tsx | 2 +- src/renderer/preload-main.ts | 24 ++++++++++ src/renderer/ssf-api.ts | 77 ++++++++++++++++++++++++++++++ src/renderer/styles/title-bar.css | 4 ++ src/renderer/windows-title-bar.tsx | 2 +- 11 files changed, 235 insertions(+), 48 deletions(-) create mode 100644 src/renderer/ssf-api.ts diff --git a/src/browser/main-api-handler.ts b/src/browser/main-api-handler.ts index b8a1c437..6e935c91 100644 --- a/src/browser/main-api-handler.ts +++ b/src/browser/main-api-handler.ts @@ -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); } diff --git a/src/browser/main.ts b/src/browser/main.ts index 29483f97..aacf87c9 100644 --- a/src/browser/main.ts +++ b/src/browser/main.ts @@ -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; diff --git a/src/browser/window-handler.ts b/src/browser/window-handler.ts index 39fbec2e..a4d730e8 100644 --- a/src/browser/window-handler.ts +++ b/src/browser/window-handler.ts @@ -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; } } diff --git a/src/browser/window-utils.ts b/src/browser/window-utils.ts index 984f2a0d..6c0bf5d4 100644 --- a/src/browser/window-utils.ts +++ b/src/browser/window-utils.ts @@ -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); + } } \ No newline at end of file diff --git a/src/common/api-interface.ts b/src/common/api-interface.ts index 89923b0e..36c16585 100644 --- a/src/common/api-interface.ts +++ b/src/common/api-interface.ts @@ -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; diff --git a/src/common/utils.ts b/src/common/utils.ts index c642eddf..19656953 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -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); } diff --git a/src/renderer/about-app.tsx b/src/renderer/about-app.tsx index fbc54c6a..91f978d7 100644 --- a/src/renderer/about-app.tsx +++ b/src/renderer/about-app.tsx @@ -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); } /** diff --git a/src/renderer/preload-main.ts b/src/renderer/preload-main.ts index c996f468..6f05c92e 100644 --- a/src/renderer/preload-main.ts +++ b/src/renderer/preload-main.ts @@ -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(); } \ No newline at end of file diff --git a/src/renderer/ssf-api.ts b/src/renderer/ssf-api.ts new file mode 100644 index 00000000..77e53fd4 --- /dev/null +++ b/src/renderer/ssf-api.ts @@ -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, + }); + } +}); \ No newline at end of file diff --git a/src/renderer/styles/title-bar.css b/src/renderer/styles/title-bar.css index 4848bdfb..7d7d16ee 100644 --- a/src/renderer/styles/title-bar.css +++ b/src/renderer/styles/title-bar.css @@ -104,4 +104,8 @@ width: 100%; z-index: 3000; bottom: 0; +} + +.symphony-logo { + content: url("../src/renderer/assets/symphony-title-bar-logo.png"); } \ No newline at end of file diff --git a/src/renderer/windows-title-bar.tsx b/src/renderer/windows-title-bar.tsx index 3444d95f..def6610e 100644 --- a/src/renderer/windows-title-bar.tsx +++ b/src/renderer/windows-title-bar.tsx @@ -86,7 +86,7 @@ export default class WindowsTitleBar extends React.Component<{}, IState> {
- +

{document.title || 'Symphony'}