From 13e82bac009c15544143bf7eebff1f215f7dbd35 Mon Sep 17 00:00:00 2001 From: Kiran Niranjan Date: Tue, 19 Mar 2019 16:22:39 +0530 Subject: [PATCH] Merge TS context isolation branch onto Typescript master branch (#598) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Typescript 🎉 * Typescript 🎉 (logger, get-guid, string-format and throttle) * Refactor typescript code * consolidate all the utility functions to one file * refactor protocol handler feature * Typescript: Add code documentation Add pre-commit hooks * Typescript: Fix logger formatting * Typescript: Add support for react * Typescript: Completed about app * Typescript: Completed about app * Typescript: Completed about app * Typescript - Fix issues with about-app and add login to convert less to css * Typescript - Fix loading screen * Typescript - Add custom title bar * Typescript - Add method to get locale * Typescript - Add logic to clean up old logs * Typescript - Add set badge count api * Typescript - Complete application menu * Typescript - Add logic to translate menu items * Typescript - freeze window.ssf api * Typescript - Handle popup menu on alt key press * Typescript - Completed activity detection * Typescript - Completed screen snippet * Typescript - Add login to close screen snippet * Typescript - Completed window actions & snackbar, Updated i18n module * Typescript - Completed native crypto implementation & fixed bugs * Typescript - Completed Desktop capturer & screen picker implementation * Typescript - Optimize window actions * Typescript - Add support for child window * Typescript - fix pop url validation issue & browserify preload * Typescript - Completed context menu implementation and fixed screen snippet * Typescript - Completed screen sharing indicator and fixed i18n usage issue * Typescript - Fix i18n locale setting issue * Typescript - Completed download manager * Typescript - Completed Basic auth * Typescript - Network connectivity dialog * Typescript - Handle certificate error * Typescript - Add translation for certificate error dialog buttons * Typescript - Add gulp tasks to compile less, typescript and copy files * Typescript - Fix some issues with custom title bar, loading screen & screen snippet * Typescript - Remove ES2015 lib * :typescript: - Do not inject custom title bar for mac * :typescript: - Fix screen sharing indicator text and format string * Typescript - Fix esc to full screen * Typescript - handle multiple/single instance of the client and add safety checks * Typescript - Refactor code base * Typescript - Optimize window validation and fix screen picker issue * Typescript - Optimize protocol handler * typescript: logger unit test * typescript: activityDetection unit test (#560) * ELECTRON-1022 - Create app bridge that communicates between renderer and preload via postMessage * ELECTRON-1024 - Add support for screen share and screen sharing indicator * config unit test (#566) * ELECTRON-1024 - Fix screen sharing indicator close issue * ELECTRON-1022 - Bump Symphony version to 5.0.0 (1.55) * fixing jest coverage output report (#575) * protocol handle unit test (#576) * Typescript - Remove unwanted checks in protocol handler and add test cases * added more tests to increase coverage to 100 for protocol handler * Typescript download manager unit test (#579) * adding enzyme * download manager unit test * Typescript - Completed notification workflow * about app unit test * Typescript - Fix notification styles * fixing Compiler error: Generic type ReactElement (#583) * fix app path on windows (#580) * basic auth unit test (#582) * screen picker unit test (#587) * screen picker unit test * screen sharing indicator unit test * loading screen unit test (#588) * improving snapshot using snapshotSerializers to remove unnecessary things (#596) * Typescript - Enforce braces for if/for/do/while statements. * Typescript - Fix Lint issues and Unit test * Typescript - Enable eofline (Ensure the file ends with a newline.) * Typescript - Update logger logic and format * Typescript - Provide option for user to set custom log path * Typescript - Fix eofline in css files * Typescript - ignore spec from compiling and remove unwanted rebuild command --- demo/index.html | 25 +- gulpfile.js | 4 +- package.json | 6 +- spec/screenSharingIndicator.spec.ts | 2 + src/app/activity-detection.ts | 6 +- src/app/app-cache-handler.ts | 2 +- src/app/auto-launch-controller.ts | 2 +- src/app/child-window-handler.ts | 14 +- src/app/chrome-flags.ts | 2 +- src/app/config-handler.ts | 2 +- src/app/crypto-handler.ts | 4 +- src/app/dialog-handler.ts | 10 +- src/app/main-api-handler.ts | 27 +- src/app/main.ts | 6 +- src/app/protocol-handler.ts | 2 +- src/app/reports-handler.ts | 2 +- src/app/screen-snippet-handler.ts | 10 +- src/app/spell-check-handler.ts | 2 +- src/app/window-actions.ts | 41 ++- src/app/window-handler.ts | 189 ++++++----- src/app/window-utils.ts | 34 +- src/common/animation-queue.ts | 50 +++ src/common/api-interface.ts | 34 +- src/common/env.ts | 2 +- src/common/i18n-preload.ts | 2 +- src/common/i18n.ts | 2 +- src/common/logger.ts | 28 +- src/common/utils.ts | 30 +- src/renderer/app-bridge.ts | 196 +++++++++++ src/renderer/assets/symphony-logo-black.png | Bin 0 -> 1768 bytes src/renderer/assets/symphony-logo-white.png | Bin 0 -> 1796 bytes src/renderer/components/about-app.tsx | 2 +- src/renderer/components/download-manager.tsx | 4 +- src/renderer/components/loading-screen.tsx | 2 +- src/renderer/components/more-info.tsx | 2 +- src/renderer/components/notification-comp.tsx | 108 ++++++ src/renderer/components/screen-picker.tsx | 6 +- .../components/screen-sharing-indicator.tsx | 6 +- src/renderer/components/snack-bar.tsx | 2 +- src/renderer/desktop-capturer.ts | 31 +- src/renderer/notification-handler.ts | 223 ++++++++++++ src/renderer/notification.ts | 321 ++++++++++++++++++ src/renderer/preload-component.ts | 8 +- src/renderer/preload-main.ts | 10 +- src/renderer/ssf-api.ts | 247 ++++++++++++-- src/renderer/styles/notification-comp.less | 109 ++++++ .../styles/screen-sharing-indicator.less | 1 - tsconfig.json | 5 +- tslint.json | 6 +- 49 files changed, 1577 insertions(+), 252 deletions(-) create mode 100644 src/common/animation-queue.ts create mode 100644 src/renderer/app-bridge.ts create mode 100644 src/renderer/assets/symphony-logo-black.png create mode 100644 src/renderer/assets/symphony-logo-white.png create mode 100644 src/renderer/components/notification-comp.tsx create mode 100644 src/renderer/notification-handler.ts create mode 100644 src/renderer/notification.ts create mode 100644 src/renderer/styles/notification-comp.less diff --git a/demo/index.html b/demo/index.html index 5599c2af..2b6197c2 100644 --- a/demo/index.html +++ b/demo/index.html @@ -132,7 +132,9 @@ num++; - var notf = new ssf.Notification(title, { + var notf = { + id: num, + title, body: (body + ' num=' + num + ' tag=' + tag), image: imageUrl, flash: shouldFlash, @@ -142,24 +144,11 @@ hello: 'hello word' }, tag: tag, - company: company - }); - - notf.addEventListener('click', onclick); - function onclick(event) { - event.target.close(); - alert('notification clicked: ' + event.target.data.hello); - } - - notf.addEventListener('close', onclose); - function onclose() { - alert('notification closed'); + company: company, + method: 'notification', }; - notf.addEventListener('error', onerror); - function onerror(event) { - alert('error=' + event.result); - }; + window.postMessage({ method: 'notification', data: notf }, '*'); }); var badgeCount = 0; @@ -292,5 +281,7 @@ document.location.reload(); }); + window.addEventListener('message', (event) => console.log(event)); + diff --git a/gulpfile.js b/gulpfile.js index 4cd62bc1..9a118e0b 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -12,7 +12,7 @@ gulp.task('clean', function() { }); gulp.task('compile', function() { - return gulp.src(['src/**/*.ts']) + return gulp.src(['src/**/*.ts', 'src/**/*.tsx']) .pipe(tsc({ project: './tsconfig.json' })) .pipe(gulp.dest('lib/')) }); @@ -35,4 +35,4 @@ gulp.task('copy', function () { }).pipe(gulp.dest('lib/src')) }); -gulp.task('build', gulp.series('clean', 'compile', 'less', 'copy')); \ No newline at end of file +gulp.task('build', gulp.series('clean', 'compile', 'less', 'copy')); diff --git a/package.json b/package.json index dace4d18..d0b1488f 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "Symphony", "productName": "Symphony", - "version": "4.5.0", - "clientVersion": "1.55.0", + "version": "5.0.0", + "clientVersion": "1.55", "buildNumber": "0", "description": "Symphony desktop app (Foundation ODP)", "author": "Symphony", @@ -16,7 +16,7 @@ "browserify-preload": "browserify -o lib/src/renderer/_preload-main.js -x electron --insert-global-vars=__filename,__dirname lib/src/renderer/preload-main.js", "rebuild": "electron-rebuild -f", "dev": "npm run prebuild && cross-env ELECTRON_DEV=true electron .", - "test": "npm run lint && npm rebuild --build-from-source && cross-env ELECTRON_QA=true jest --config jest.unit.config.json --runInBand && npm run rebuild", + "test": "npm run lint && cross-env ELECTRON_QA=true jest --config jest.unit.config.json --runInBand", "demo-win": "npm run prebuild && cross-env ELECTRON_DEV=true electron . --url=file:///demo/index.html", "demo-mac": "npm run prebuild && cross-env ELECTRON_DEV=true electron . --url=file://$(pwd)/demo/index.html", "unpacked-mac": "npm run prebuild && npm run test && build --mac --dir", diff --git a/spec/screenSharingIndicator.spec.ts b/spec/screenSharingIndicator.spec.ts index 6cd0a234..8fb535f4 100644 --- a/spec/screenSharingIndicator.spec.ts +++ b/spec/screenSharingIndicator.spec.ts @@ -30,9 +30,11 @@ describe('screen sharing indicator', () => { const closeIpcRendererMock = { cmd: 'close-window', windowType: 'screen-sharing-indicator', + winKey: 'id-123', }; const spy = jest.spyOn(ipcRenderer, sendEventLabel); const wrapper = shallow(React.createElement(ScreenSharingIndicator)); + wrapper.setState({ streamId: 'id-123' }); wrapper.find(customSelector).simulate('click'); expect(spy).lastCalledWith(symphonyAPIEventLabel, closeIpcRendererMock); }); diff --git a/src/app/activity-detection.ts b/src/app/activity-detection.ts index 4cdddb5c..07cca7ea 100644 --- a/src/app/activity-detection.ts +++ b/src/app/activity-detection.ts @@ -46,7 +46,9 @@ class ActivityDetection { const idleTimeInMillis = idleTime * 1000; if (idleTimeInMillis < this.idleThreshold) { this.sendActivity(idleTimeInMillis); - if (this.timer) clearInterval(this.timer); + if (this.timer) { + clearInterval(this.timer); + } this.timer = undefined; logger.info(`activity-detection: activity occurred`); return; @@ -78,4 +80,4 @@ class ActivityDetection { const activityDetection = new ActivityDetection(); -export { activityDetection }; \ No newline at end of file +export { activityDetection }; diff --git a/src/app/app-cache-handler.ts b/src/app/app-cache-handler.ts index 8b35e81f..60157a9a 100644 --- a/src/app/app-cache-handler.ts +++ b/src/app/app-cache-handler.ts @@ -22,4 +22,4 @@ export const cleanUpAppCache = async (): Promise => { */ export const createAppCacheFile = (): void => { fs.writeFileSync(cacheCheckFilePath, ''); -}; \ No newline at end of file +}; diff --git a/src/app/auto-launch-controller.ts b/src/app/auto-launch-controller.ts index d60460f2..f7605189 100644 --- a/src/app/auto-launch-controller.ts +++ b/src/app/auto-launch-controller.ts @@ -111,4 +111,4 @@ const autoLaunchInstance = new AutoLaunchController(props); export { autoLaunchInstance, -}; \ No newline at end of file +}; diff --git a/src/app/child-window-handler.ts b/src/app/child-window-handler.ts index bb43de4c..6aabd033 100644 --- a/src/app/child-window-handler.ts +++ b/src/app/child-window-handler.ts @@ -33,8 +33,12 @@ const getParsedUrl = (configURL: string): Url => { export const handleChildWindow = (webContents: WebContents): void => { const childWindow = (event, newWinUrl, frameName, disposition, newWinOptions): void => { const mainWindow = windowHandler.getMainWindow(); - if (!mainWindow || mainWindow.isDestroyed()) return; - if (!windowHandler.url) return; + if (!mainWindow || mainWindow.isDestroyed()) { + return; + } + if (!windowHandler.url) { + return; + } if (!newWinOptions.webPreferences) { newWinOptions.webPreferences = {}; @@ -104,7 +108,9 @@ export const handleChildWindow = (webContents: WebContents): void => { childWebContents.once('did-finish-load', async () => { const browserWin: ICustomBrowserWindow = BrowserWindow.fromWebContents(childWebContents) as ICustomBrowserWindow; - if (!browserWin) return; + if (!browserWin) { + return; + } windowHandler.addWindow(newWinKey, browserWin); browserWin.webContents.send('page-load', { isWindowsOS }); // Inserts css on to the window @@ -129,4 +135,4 @@ export const handleChildWindow = (webContents: WebContents): void => { } }; webContents.on('new-window', childWindow); -}; \ No newline at end of file +}; diff --git a/src/app/chrome-flags.ts b/src/app/chrome-flags.ts index 0e6b2e7f..79ba61c5 100644 --- a/src/app/chrome-flags.ts +++ b/src/app/chrome-flags.ts @@ -63,4 +63,4 @@ export const setChromeFlags = () => { } } } -}; \ No newline at end of file +}; diff --git a/src/app/config-handler.ts b/src/app/config-handler.ts index 5f6138ae..c5d80a90 100644 --- a/src/app/config-handler.ts +++ b/src/app/config-handler.ts @@ -224,4 +224,4 @@ const config = new Config(); export { config, -}; \ No newline at end of file +}; diff --git a/src/app/crypto-handler.ts b/src/app/crypto-handler.ts index ecde2d81..296fbaf3 100644 --- a/src/app/crypto-handler.ts +++ b/src/app/crypto-handler.ts @@ -10,7 +10,7 @@ import { logger } from '../common/logger'; const TAG_LENGTH = 16; const arch = process.arch === 'ia32'; const winLibraryPath = isDevEnv ? path.join(__dirname, '..', '..', 'library') : path.join(execPath, 'library'); -const macLibraryPath = isDevEnv ? path.join(__dirname, '..', '..', 'library') : path.join(execPath, '..', 'library'); +const macLibraryPath = isDevEnv ? path.join(__dirname, '..', '..', '..', 'library') : path.join(execPath, '..', 'library'); const cryptoLibPath = isMac ? path.join(macLibraryPath, 'cryptoLib.dylib') : @@ -133,4 +133,4 @@ class CryptoLibrary implements ICryptoLib { const cryptoLibrary = new CryptoLibrary(); -export { cryptoLibrary }; \ No newline at end of file +export { cryptoLibrary }; diff --git a/src/app/dialog-handler.ts b/src/app/dialog-handler.ts index 7a1f89ec..475a2eca 100644 --- a/src/app/dialog-handler.ts +++ b/src/app/dialog-handler.ts @@ -96,8 +96,12 @@ electron.app.on('certificate-error', (event, webContents, url, error, _certifica */ export const showLoadFailure = (browserWindow: Electron.BrowserWindow, url: string, errorDesc: string, errorCode: number, retryCallback: () => void, showDialog: boolean): void => { let message = url ? `${i18n.t('Error loading URL')()}:\n${url}` : i18n.t('Error loading window')(); - if (errorDesc) message += `\n\n${errorDesc}`; - if (errorCode) message += `\n\nError Code: ${errorCode}`; + if (errorDesc) { + message += `\n\n${errorDesc}`; + } + if (errorCode) { + message += `\n\nError Code: ${errorCode}`; + } // async handle of user input const response = (buttonId: number): void => { @@ -132,4 +136,4 @@ export const showLoadFailure = (browserWindow: Electron.BrowserWindow, url: stri export const showNetworkConnectivityError = (browserWindow: Electron.BrowserWindow, url: string = '', retryCallback: () => void): void => { const errorDesc = i18n.t('Network connectivity has been lost. Check your internet connection.')(); showLoadFailure(browserWindow, url, errorDesc, 0, retryCallback, true); -}; \ No newline at end of file +}; diff --git a/src/app/main-api-handler.ts b/src/app/main-api-handler.ts index eae5bfa2..b218627d 100644 --- a/src/app/main-api-handler.ts +++ b/src/app/main-api-handler.ts @@ -80,7 +80,9 @@ ipcMain.on(apiName.symphonyApi, (event: Electron.Event, arg: IApiArgs) => { // validates the user bring to front config and activates the wrapper if (typeof arg.reason === 'string' && arg.reason === 'notification') { const shouldBringToFront = config.getConfigFields([ 'bringToFront' ]); - if (shouldBringToFront) activate(arg.windowName, false); + if (shouldBringToFront) { + activate(arg.windowName, false); + } } break; case apiCmds.openScreenPickerWindow: @@ -95,21 +97,6 @@ ipcMain.on(apiName.symphonyApi, (event: Electron.Event, arg: IApiArgs) => { } break; } - /*case ApiCmds.optimizeMemoryConsumption: - if (typeof arg.memory === 'object' - && typeof arg.cpuUsage === 'object' - && typeof arg.memory.workingSetSize === 'number') { - setPreloadMemoryInfo(arg.memory, arg.cpuUsage); - } - break; - case ApiCmds.optimizeMemoryRegister: - setPreloadWindow(event.sender); - break; - case ApiCmds.setIsInMeeting: - if (typeof arg.isInMeeting === 'boolean') { - setIsInMeeting(arg.isInMeeting); - } - break;*/ case apiCmds.setLocale: if (typeof arg.locale === 'string') { updateLocale(arg.locale as LocaleType); @@ -124,12 +111,12 @@ ipcMain.on(apiName.symphonyApi, (event: Electron.Event, arg: IApiArgs) => { screenSnippet.capture(event.sender); break; case apiCmds.closeWindow: - windowHandler.closeWindow(arg.windowType); + windowHandler.closeWindow(arg.windowType, arg.winKey); break; case apiCmds.openScreenSharingIndicator: - const { displayId, id } = arg; - if (typeof displayId === 'string' && typeof id === 'number') { - windowHandler.createScreenSharingIndicatorWindow(event.sender, displayId, id); + const { displayId, id, streamId } = arg; + if (typeof displayId === 'string' && typeof id === 'number' && typeof streamId === 'string') { + windowHandler.createScreenSharingIndicatorWindow(event.sender, displayId, id, streamId); } break; case apiCmds.downloadManagerAction: diff --git a/src/app/main.ts b/src/app/main.ts index 3c72bceb..1d5792e7 100644 --- a/src/app/main.ts +++ b/src/app/main.ts @@ -66,7 +66,9 @@ if (!allowMultiInstance) { // Someone tried to run a second instance, we should focus our window. const mainWindow = windowHandler.getMainWindow(); if (mainWindow && !mainWindow.isDestroyed()) { - if (isMac) return mainWindow.show(); + if (isMac) { + return mainWindow.show(); + } if (mainWindow.isMinimized()) { mainWindow.restore(); } @@ -117,4 +119,4 @@ app.on('activate', () => { * * This event is emitted only on macOS at this moment */ -app.on('open-url', (_event, url) => protocolHandler.sendProtocol(url)); \ No newline at end of file +app.on('open-url', (_event, url) => protocolHandler.sendProtocol(url)); diff --git a/src/app/protocol-handler.ts b/src/app/protocol-handler.ts index c198ad95..d03f29ba 100644 --- a/src/app/protocol-handler.ts +++ b/src/app/protocol-handler.ts @@ -69,4 +69,4 @@ class ProtocolHandler { const protocolHandler = new ProtocolHandler(); -export { protocolHandler }; \ No newline at end of file +export { protocolHandler }; diff --git a/src/app/reports-handler.ts b/src/app/reports-handler.ts index 0c71db32..b366fe85 100644 --- a/src/app/reports-handler.ts +++ b/src/app/reports-handler.ts @@ -131,4 +131,4 @@ export const exportCrashDumps = (): void => { }); } }); -}; \ No newline at end of file +}; diff --git a/src/app/screen-snippet-handler.ts b/src/app/screen-snippet-handler.ts index f4ecb057..0719dcfe 100644 --- a/src/app/screen-snippet-handler.ts +++ b/src/app/screen-snippet-handler.ts @@ -53,7 +53,9 @@ class ScreenSnippet { updateAlwaysOnTop(false, false); } // only allow one screen capture at a time. - if (this.child) this.child.kill(); + if (this.child) { + this.child.kill(); + } try { await this.execCmd(this.captureUtil, this.captureUtilArgs); const { message, data, type }: IScreenSnippet = await this.convertFileToData(); @@ -86,7 +88,9 @@ class ScreenSnippet { private execCmd(captureUtil: string, captureUtilArgs: ReadonlyArray): Promise { return new Promise((resolve, reject) => { return this.child = execFile(captureUtil, captureUtilArgs, (error: ExecException | null) => { - if (this.isAlwaysOnTop) updateAlwaysOnTop(true, false); + if (this.isAlwaysOnTop) { + updateAlwaysOnTop(true, false); + } if (error && error.killed) { // processs was killed, just resolve with no data. return reject(error); @@ -140,4 +144,4 @@ class ScreenSnippet { const screenSnippet = new ScreenSnippet(); -export { screenSnippet }; \ No newline at end of file +export { screenSnippet }; diff --git a/src/app/spell-check-handler.ts b/src/app/spell-check-handler.ts index 96da10cf..034204d8 100644 --- a/src/app/spell-check-handler.ts +++ b/src/app/spell-check-handler.ts @@ -99,4 +99,4 @@ export class SpellChecker { } return menu; } -} \ No newline at end of file +} diff --git a/src/app/window-actions.ts b/src/app/window-actions.ts index e55b2d5c..e399a2f2 100644 --- a/src/app/window-actions.ts +++ b/src/app/window-actions.ts @@ -1,7 +1,7 @@ import { BrowserWindow } from 'electron'; import { apiName, IBoundsChange, KeyCodes } from '../common/api-interface'; -import { isWindowsOS } from '../common/env'; +import { isMac, isWindowsOS } from '../common/env'; import { throttle } from '../common/utils'; import { config } from './config-handler'; import { ICustomBrowserWindow, windowHandler } from './window-handler'; @@ -14,7 +14,7 @@ export const saveWindowSettings = (): void => { const [ x, y ] = browserWindow.getPosition(); const [ width, height ] = browserWindow.getSize(); if (x && y && width && height) { - browserWindow.webContents.send('boundChanges', { x, y, width, height, windowName: browserWindow.winName } as IBoundsChange); + browserWindow.webContents.send('boundsChange', { x, y, width, height, windowName: browserWindow.winName } as IBoundsChange); if (browserWindow.winName === apiName.mainWindowName) { const isMaximized = browserWindow.isMaximized(); @@ -54,19 +54,26 @@ export const throttledWindowChanges = throttle(saveWindowSettings, 1000); export const activate = (windowName: string, shouldFocus: boolean = true): void => { // Electron-136: don't activate when the app is reloaded programmatically - if (windowHandler.isAutoReload) return; + if (windowHandler.isAutoReload) { + return; + } const windows = windowHandler.getAllWindows(); for (const key in windows) { - if (windows.hasOwnProperty(key)) { + if (Object.prototype.hasOwnProperty.call(windows, key)) { const window = windows[ key ]; if (window && !window.isDestroyed() && window.winName === windowName) { // Bring the window to the top without focusing // Flash task bar icon in Windows for windows if (!shouldFocus) { - window.moveTop(); - return isWindowsOS ? window.flashFrame(true) : null; + return isMac ? window.showInactive() : window.flashFrame(true); + } + + // Note: On window just focusing will preserve window snapped state + // Hiding the window and just calling the focus() won't display the window + if (isWindowsOS) { + return window.isMinimized() ? window.restore() : window.focus(); } return window.isMinimized() ? window.restore() : window.show(); @@ -130,11 +137,15 @@ export const handleKeyPress = (key: number): void => { * @param window {BrowserWindow} */ export const monitorWindowActions = (window: BrowserWindow): void => { - if (!window || window.isDestroyed()) return; + if (!window || window.isDestroyed()) { + return; + } const eventNames = [ 'move', 'resize', 'maximize', 'unmaximize' ]; eventNames.forEach((event: string) => { - // @ts-ignore - if (window) window.on(event, throttledWindowChanges); + if (window) { + // @ts-ignore + window.on(event, throttledWindowChanges); + } }); window.on('enter-full-screen', enterFullScreen); window.on('leave-full-screen', leaveFullScreen); @@ -146,12 +157,16 @@ export const monitorWindowActions = (window: BrowserWindow): void => { * @param window */ export const removeWindowEventListener = (window: BrowserWindow): void => { - if (!window || window.isDestroyed()) return; + if (!window || window.isDestroyed()) { + return; + } const eventNames = [ 'move', 'resize', 'maximize', 'unmaximize' ]; eventNames.forEach((event: string) => { - // @ts-ignore - if (window) window.removeListener(event, throttledWindowChanges); + if (window) { + // @ts-ignore + window.removeListener(event, throttledWindowChanges); + } }); window.removeListener('enter-full-screen', enterFullScreen); window.removeListener('leave-full-screen', leaveFullScreen); -}; \ No newline at end of file +}; diff --git a/src/app/window-handler.ts b/src/app/window-handler.ts index e395e3fa..33edc89d 100644 --- a/src/app/window-handler.ts +++ b/src/app/window-handler.ts @@ -14,7 +14,7 @@ import { handleChildWindow } from './child-window-handler'; import { config, IConfig } from './config-handler'; import { showNetworkConnectivityError } from './dialog-handler'; import { monitorWindowActions } from './window-actions'; -import { createComponentWindow, getBounds, handleDownloadManager, injectStyles } from './window-utils'; +import { createComponentWindow, getBounds, handleDownloadManager, injectStyles, windowExists } from './window-utils'; interface ICustomBrowserWindowConstructorOpts extends Electron.BrowserWindowConstructorOptions { winKey: string; @@ -189,7 +189,7 @@ export class WindowHandler { // Event needed to hide native menu bar on Windows 10 as we use custom menu bar this.mainWindow.webContents.once('did-start-loading', () => { - if ((this.config.isCustomTitleBar || isWindowsOS) && this.mainWindow && this.windowExists(this.mainWindow)) { + if ((this.config.isCustomTitleBar || isWindowsOS) && this.mainWindow && windowExists(this.mainWindow)) { this.mainWindow.setMenuBarVisibility(false); } }); @@ -204,25 +204,36 @@ export class WindowHandler { // Displays a dialog if network connectivity has been lost const retry = () => { - if (!this.mainWindow) return; - if (!this.isOnline) showNetworkConnectivityError(this.mainWindow, this.url, retry); + if (!this.mainWindow) { + return; + } + if (!this.isOnline) { + showNetworkConnectivityError(this.mainWindow, this.url, retry); + } this.mainWindow.webContents.reload(); }; - if (!this.isOnline && this.mainWindow) showNetworkConnectivityError(this.mainWindow, this.url, retry); + if (!this.isOnline && this.mainWindow) { + showNetworkConnectivityError(this.mainWindow, this.url, retry); + } // early exit if the window has already been destroyed - if (!this.mainWindow || !this.windowExists(this.mainWindow)) return; + if (!this.mainWindow || !windowExists(this.mainWindow)) { + return; + } this.url = this.mainWindow.webContents.getURL(); // Injects custom title bar css into the webContents // only for Window and if it is enabled await injectStyles(this.mainWindow, this.isCustomTitleBarAndWindowOS); - if (this.isCustomTitleBarAndWindowOS) this.mainWindow.webContents.send('initiate-custom-title-bar'); + if (this.isCustomTitleBarAndWindowOS) { + this.mainWindow.webContents.send('initiate-custom-title-bar'); + } this.mainWindow.webContents.send('page-load', { isWindowsOS, locale: i18n.getLocale(), resources: i18n.loadedResources, + origin: this.globalConfig.url, }); this.appMenu = new AppMenu(); @@ -239,9 +250,13 @@ export class WindowHandler { // Handle main window close this.mainWindow.on('close', (event) => { - if (!this.mainWindow || !this.windowExists(this.mainWindow)) return; + if (!this.mainWindow || !windowExists(this.mainWindow)) { + return; + } - if (this.willQuitApp) return this.destroyAllWindow(); + if (this.willQuitApp) { + return this.destroyAllWindow(); + } if (this.config.minimizeOnClose) { event.preventDefault(); @@ -285,16 +300,23 @@ export class WindowHandler { /** * Closes the window from an event emitted by the render processes * - * @param windowType + * @param windowType {WindowTypes} + * @param winKey {string} - Unique ID assigned to the window */ - public closeWindow(windowType: WindowTypes): void { + public closeWindow(windowType: WindowTypes, winKey?: string): void { switch (windowType) { case 'screen-picker': - if (this.screenPickerWindow && this.windowExists(this.screenPickerWindow)) this.screenPickerWindow.close(); + if (this.screenPickerWindow && windowExists(this.screenPickerWindow)) { + this.screenPickerWindow.close(); + } break; case 'screen-sharing-indicator': - if (this.screenSharingIndicatorWindow - && this.windowExists(this.screenSharingIndicatorWindow)) this.screenSharingIndicatorWindow.close(); + if (winKey) { + const browserWindow = this.windows[ winKey ]; + if (browserWindow && windowExists(browserWindow)) { + browserWindow.close(); + } + } break; default: break; @@ -329,7 +351,9 @@ export class WindowHandler { public showLoadingScreen(): void { this.loadingWindow = createComponentWindow('loading-screen', WindowHandler.getLoadingWindowOpts()); this.loadingWindow.webContents.once('did-finish-load', () => { - if (!this.loadingWindow || !this.windowExists(this.loadingWindow)) return; + if (!this.loadingWindow || !windowExists(this.loadingWindow)) { + return; + } this.loadingWindow.webContents.send('data'); }); @@ -342,7 +366,9 @@ export class WindowHandler { public createAboutAppWindow(): void { this.aboutAppWindow = createComponentWindow('about-app'); this.aboutAppWindow.webContents.once('did-finish-load', () => { - if (!this.aboutAppWindow || !this.windowExists(this.aboutAppWindow)) return; + if (!this.aboutAppWindow || !windowExists(this.aboutAppWindow)) { + return; + } this.aboutAppWindow.webContents.send('about-app-data', { buildNumber, clientVersion, version }); }); } @@ -353,7 +379,9 @@ export class WindowHandler { public createMoreInfoWindow(): void { this.moreInfoWindow = createComponentWindow('more-info'); this.moreInfoWindow.webContents.once('did-finish-load', () => { - if (!this.moreInfoWindow || !this.windowExists(this.moreInfoWindow)) return; + if (!this.moreInfoWindow || !windowExists(this.moreInfoWindow)) { + return; + } this.moreInfoWindow.webContents.send('more-info-data'); }); } @@ -367,18 +395,22 @@ export class WindowHandler { */ public createScreenPickerWindow(window: Electron.WebContents, sources: DesktopCapturerSource[], id: number): void { - if (this.screenPickerWindow && this.windowExists(this.screenPickerWindow)) this.screenPickerWindow.close(); + if (this.screenPickerWindow && windowExists(this.screenPickerWindow)) { + this.screenPickerWindow.close(); + } const opts = WindowHandler.getScreenPickerWindowOpts(); this.screenPickerWindow = createComponentWindow('screen-picker', opts); this.screenPickerWindow.webContents.once('did-finish-load', () => { - if (!this.screenPickerWindow || !this.windowExists(this.screenPickerWindow)) return; + if (!this.screenPickerWindow || !windowExists(this.screenPickerWindow)) { + return; + } this.screenPickerWindow.webContents.send('screen-picker-data', { sources, id }); this.addWindow(opts.winKey, this.screenPickerWindow); }); ipcMain.once('screen-source-selected', (_event, source) => { window.send('start-share' + id, source); - if (this.screenPickerWindow && this.windowExists(this.screenPickerWindow)) { + if (this.screenPickerWindow && windowExists(this.screenPickerWindow)) { this.screenPickerWindow.close(); } }); @@ -388,50 +420,6 @@ export class WindowHandler { }); } - /** - * Creates a screen sharing indicator whenever uses start - * sharing the screen - * - * @param screenSharingWebContents {Electron.webContents} - * @param displayId {string} - * @param id {number} - */ - public createScreenSharingIndicatorWindow(screenSharingWebContents: Electron.webContents, displayId: string, id: number): void { - - if (this.screenSharingIndicatorWindow - && this.windowExists(this.screenSharingIndicatorWindow)) this.screenSharingIndicatorWindow.close(); - - const indicatorScreen = - (displayId && electron.screen.getAllDisplays().filter((d) => - displayId.includes(d.id.toString()))[ 0 ]) || electron.screen.getPrimaryDisplay(); - - const screenRect = indicatorScreen.workArea; - let opts = WindowHandler.getScreenSharingIndicatorOpts(); - if (opts.width && opts.height) { - opts = Object.assign({}, opts, { - x: screenRect.x + Math.round((screenRect.width - opts.width) / 2), - y: screenRect.y + screenRect.height - opts.height, - }); - } - this.screenSharingIndicatorWindow = createComponentWindow('screen-sharing-indicator', opts); - this.screenSharingIndicatorWindow.setVisibleOnAllWorkspaces(true); - this.screenSharingIndicatorWindow.webContents.once('did-finish-load', () => { - if (!this.screenSharingIndicatorWindow || !this.windowExists(this.screenSharingIndicatorWindow)) return; - this.screenSharingIndicatorWindow.webContents.send('screen-sharing-indicator-data', { id }); - }); - const stopScreenSharing = (_event, indicatorId) => { - if (id === indicatorId) { - screenSharingWebContents.send('screen-sharing-stopped', id); - } - }; - - this.screenSharingIndicatorWindow.once('close', () => { - ipcMain.removeListener('stop-screen-sharing', stopScreenSharing); - }); - - ipcMain.once('stop-screen-sharing', stopScreenSharing); - } - /** * Creates a Basic auth window whenever the network * requires authentications @@ -450,12 +438,16 @@ export class WindowHandler { this.basicAuthWindow = createComponentWindow('basic-auth', opts); this.basicAuthWindow.setVisibleOnAllWorkspaces(true); this.basicAuthWindow.webContents.once('did-finish-load', () => { - if (!this.basicAuthWindow || !this.windowExists(this.basicAuthWindow)) return; + if (!this.basicAuthWindow || !windowExists(this.basicAuthWindow)) { + return; + } this.basicAuthWindow.webContents.send('basic-auth-data', { hostname, isValidCredentials: isMultipleTries }); }); const closeBasicAuth = (shouldClearSettings = true) => { - if (shouldClearSettings) clearSettings(); - if (this.basicAuthWindow && !this.windowExists(this.basicAuthWindow)) { + if (shouldClearSettings) { + clearSettings(); + } + if (this.basicAuthWindow && !windowExists(this.basicAuthWindow)) { this.basicAuthWindow.close(); this.basicAuthWindow = null; } @@ -476,6 +468,58 @@ export class WindowHandler { ipcMain.once('basic-auth-login', login); } + /** + * Creates a screen sharing indicator whenever uses start + * sharing the screen + * + * @param screenSharingWebContents {Electron.webContents} + * @param displayId {string} - current display id + * @param id {number} - postMessage request id + * @param streamId {string} - MediaStream id + */ + public createScreenSharingIndicatorWindow( + screenSharingWebContents: Electron.webContents, + displayId: string, + id: number, + streamId, + ): void { + const indicatorScreen = + (displayId && electron.screen.getAllDisplays().filter((d) => + displayId.includes(d.id.toString()))[ 0 ]) || electron.screen.getPrimaryDisplay(); + + const screenRect = indicatorScreen.workArea; + // Set stream id as winKey to link stream to the window + let opts = { ...WindowHandler.getScreenSharingIndicatorOpts(), ...{ winKey: streamId } }; + if (opts.width && opts.height) { + opts = Object.assign({}, opts, { + x: screenRect.x + Math.round((screenRect.width - opts.width) / 2), + y: screenRect.y + screenRect.height - opts.height, + }); + } + this.screenSharingIndicatorWindow = createComponentWindow('screen-sharing-indicator', opts); + this.screenSharingIndicatorWindow.setVisibleOnAllWorkspaces(true); + this.screenSharingIndicatorWindow.webContents.once('did-finish-load', () => { + if (!this.screenSharingIndicatorWindow || !windowExists(this.screenSharingIndicatorWindow)) { + return; + } + this.screenSharingIndicatorWindow.webContents.send('screen-sharing-indicator-data', { id, streamId }); + }); + const stopScreenSharing = (_event, indicatorId) => { + if (id === indicatorId) { + screenSharingWebContents.send('screen-sharing-stopped', id); + } + }; + + this.addWindow(opts.winKey, this.screenSharingIndicatorWindow); + + this.screenSharingIndicatorWindow.once('close', () => { + this.removeWindow(streamId); + ipcMain.removeListener('stop-screen-sharing', stopScreenSharing); + }); + + ipcMain.once('stop-screen-sharing', stopScreenSharing); + } + /** * Opens an external url in the system's default browser * @@ -519,14 +563,6 @@ export class WindowHandler { this.mainWindow = null; } - /** - * Checks if window is valid and exists - * - * @param window - * @return boolean - */ - private windowExists = (window: BrowserWindow): boolean => !!window && typeof window.isDestroyed === 'function' && !window.isDestroyed(); - /** * Main window opts */ @@ -542,6 +578,7 @@ export class WindowHandler { nodeIntegration: false, preload: path.join(__dirname, '../renderer/_preload-main.js'), sandbox: true, + contextIsolation: true, }, winKey: getGuid(), }; @@ -550,4 +587,4 @@ export class WindowHandler { const windowHandler = new WindowHandler(); -export { windowHandler }; \ No newline at end of file +export { windowHandler }; diff --git a/src/app/window-utils.ts b/src/app/window-utils.ts index 6d3e0785..878a7a5e 100644 --- a/src/app/window-utils.ts +++ b/src/app/window-utils.ts @@ -29,7 +29,9 @@ export const preventWindowNavigation = (browserWindow: Electron.BrowserWindow, i if (browserWindow.isDestroyed() || browserWindow.webContents.isDestroyed() - || winUrl === browserWindow.webContents.getURL()) return; + || winUrl === browserWindow.webContents.getURL()) { + return; + } e.preventDefault(); }; @@ -46,10 +48,12 @@ export const preventWindowNavigation = (browserWindow: Electron.BrowserWindow, i * * @param componentName * @param opts + * @param shouldFocus {boolean} */ export const createComponentWindow = ( componentName: string, opts?: Electron.BrowserWindowConstructorOptions, + shouldFocus: boolean = true, ): BrowserWindow => { const options: Electron.BrowserWindowConstructorOptions = { @@ -67,9 +71,13 @@ export const createComponentWindow = ( }; const browserWindow: ICustomBrowserWindow = new BrowserWindow(options) as ICustomBrowserWindow; - browserWindow.on('ready-to-show', () => browserWindow.show()); + if (shouldFocus) { + browserWindow.once('ready-to-show', () => browserWindow.show()); + } browserWindow.webContents.once('did-finish-load', () => { - if (!browserWindow || browserWindow.isDestroyed()) return; + if (!browserWindow || browserWindow.isDestroyed()) { + return; + } browserWindow.webContents.send('set-locale-resource', { locale: i18n.getLocale(), resource: i18n.loadedResources }); }); browserWindow.setMenu(null as any); @@ -169,7 +177,9 @@ export const updateLocale = (locale: LocaleType): void => { // sets the new locale i18n.setLocale(locale); const appMenu = windowHandler.appMenu; - if (appMenu) appMenu.update(locale); + if (appMenu) { + appMenu.update(locale); + } }; /** @@ -181,7 +191,9 @@ export const showPopupMenu = (opts: Electron.PopupOptions): void => { const { x, y } = mainWindow.isFullScreen() ? { x: 0, y: 0 } : { x: 10, y: -20 }; const popupOpts = { window: mainWindow, x, y }; const appMenu = windowHandler.appMenu; - if (appMenu) appMenu.popupMenu({ ...popupOpts, ...opts }); + if (appMenu) { + appMenu.popupMenu({ ...popupOpts, ...opts }); + } } }; @@ -219,7 +231,9 @@ export const sanitize = (windowName: string): void => { * @return {x?: Number, y?: Number, width: Number, height: Number} */ export const getBounds = (winPos: Electron.Rectangle, defaultWidth: number, defaultHeight: number): Partial => { - if (!winPos) return { width: defaultWidth, height: defaultHeight }; + if (!winPos) { + return { width: defaultWidth, height: defaultHeight }; + } const displays = electron.screen.getAllDisplays(); for (let i = 0, len = displays.length; i < len; i++) { @@ -327,3 +341,11 @@ export const injectStyles = async (mainWindow: BrowserWindow, isCustomTitleBarAn return await readAndInsertCSS(mainWindow, paths); }; + +/** + * Checks if window is valid and exists + * + * @param window {BrowserWindow} + * @return boolean + */ +export const windowExists = (window: BrowserWindow): boolean => !!window && typeof window.isDestroyed === 'function' && !window.isDestroyed(); diff --git a/src/common/animation-queue.ts b/src/common/animation-queue.ts new file mode 100644 index 00000000..9d9c071e --- /dev/null +++ b/src/common/animation-queue.ts @@ -0,0 +1,50 @@ +import { logger } from './logger'; + +export class AnimationQueue { + private queue: any[] = []; + private running: boolean = false; + + constructor() { + this.animate = this.animate.bind(this); + } + + /** + * Pushes each animation to a queue + * + * @param object + */ + public push(object) { + if (this.running) { + this.queue.push(object); + } else { + this.running = true; + setTimeout(() => this.animate(object), 0); + } + } + + /** + * Animates an animation that is part of the queue + * @param object + */ + public async animate(object): Promise { + try { + await object.func.apply(null, object.args); + } catch (err) { + logger.error(`animationQueue: encountered an error: ${err} with stack trace: ${err.stack}`); + } finally { + if (this.queue.length > 0) { + // Run next animation + this.animate.call(this, this.queue.shift()); + } else { + this.running = false; + } + } + } + + /** + * Clears the queue + */ + public clear() { + this.queue = []; + } +} diff --git a/src/common/api-interface.ts b/src/common/api-interface.ts index fffff606..b96628e8 100644 --- a/src/common/api-interface.ts +++ b/src/common/api-interface.ts @@ -1,5 +1,6 @@ export enum apiCmds { isOnline = 'is-online', + getVersionInfo = 'get-version-info', registerLogger = 'register-logger', setBadgeCount = 'set-badge-count', badgeDataUrl = 'badge-data-url', @@ -20,7 +21,11 @@ export enum apiCmds { keyPress = 'key-press', closeWindow = 'close-window', openScreenSharingIndicator = 'open-screen-sharing-indicator', + closeScreenSharingIndicator = 'close-screen-sharing-indicator', downloadManagerAction = 'download-manager-action', + getMediaSource = 'get-media-source', + notification = 'notification', + closeNotification = 'close-notification', } export enum apiName { @@ -43,6 +48,8 @@ export interface IApiArgs { locale: string; keyCode: number; windowType: WindowTypes; + winKey: string; + streamId: string; displayId: string; path: string; type: string; @@ -50,13 +57,6 @@ export interface IApiArgs { export type WindowTypes = 'screen-picker' | 'screen-sharing-indicator'; -/** - * Activity detection - */ -export interface IActivityDetection { - idleTime: number; -} - export interface IBadgeCount { count: number; } @@ -80,10 +80,28 @@ export interface IBoundsChange extends Electron.Rectangle { */ export interface IScreenSharingIndicator { type: string; + requestId: number; reason?: string; } export enum KeyCodes { Esc = 27, Alt = 18, -} \ No newline at end of file +} + +export interface IVersionInfo { + containerIdentifier: string; + containerVer: string; + buildNumber: string; + apiVer: string; + searchApiVer: string; +} + +export interface ILogMsg { + level: LogLevel; + details: any; + showInConsole: boolean; + startTime: number; +} + +export type LogLevel = 'error' | 'warn' | 'info' | 'verbose' | 'debug' | 'silly'; diff --git a/src/common/env.ts b/src/common/env.ts index d12a6f5f..77bdcb18 100644 --- a/src/common/env.ts +++ b/src/common/env.ts @@ -5,4 +5,4 @@ export const isElectronQA = !!process.env.ELECTRON_QA; export const isMac = (process.platform === 'darwin'); export const isWindowsOS = (process.platform === 'win32'); -export const isNodeEnv = !!process.env.NODE_ENV; \ No newline at end of file +export const isNodeEnv = !!process.env.NODE_ENV; diff --git a/src/common/i18n-preload.ts b/src/common/i18n-preload.ts index c5e79337..52755464 100644 --- a/src/common/i18n-preload.ts +++ b/src/common/i18n-preload.ts @@ -85,4 +85,4 @@ class Translation { const i18n = new Translation(); -export { i18n }; \ No newline at end of file +export { i18n }; diff --git a/src/common/i18n.ts b/src/common/i18n.ts index fdf1ca80..84af7e38 100644 --- a/src/common/i18n.ts +++ b/src/common/i18n.ts @@ -82,4 +82,4 @@ class Translation { const i18n = new Translation(); -export { i18n }; \ No newline at end of file +export { i18n }; diff --git a/src/common/logger.ts b/src/common/logger.ts index 45da66b5..9bf00a97 100644 --- a/src/common/logger.ts +++ b/src/common/logger.ts @@ -33,13 +33,19 @@ class Logger { this.loggerWindow = null; this.logQueue = []; + // If the user has specified a custom log path use it. + const customLogPathArg = getCommandLineArgs(process.argv, '--logPath=', false); + const customLogsFolder = customLogPathArg && customLogPathArg.substring(customLogPathArg.indexOf('=') + 1); + if (customLogsFolder && fs.existsSync(customLogsFolder)) { + app.setPath('logs', customLogsFolder); + } + this.logPath = app.getPath('logs'); if (!isElectronQA) { - transports.file.file = path.join(this.logPath, 'app.log'); + transports.file.file = path.join(this.logPath, `app_${Date.now()}.log`); transports.file.level = 'debug'; - transports.file.format = '{h}:{i}:{s}:{ms} {text}'; - transports.file.maxSize = 10 * 1024 * 1024; + transports.file.format = '{y}-{m}-{d} {h}:{i}:{s}:{ms} {z} | {level} | {text}'; transports.file.appName = 'Symphony'; } @@ -136,9 +142,15 @@ class Logger { if (this.loggerWindow) { const logMsgs: IClientLogMsg = {}; - if (this.logQueue.length) logMsgs.msgs = this.logQueue; - if (this.desiredLogLevel) logMsgs.logLevel = this.desiredLogLevel; - if (Object.keys(logMsgs).length) this.loggerWindow.send('log', logMsgs); + if (this.logQueue.length) { + logMsgs.msgs = this.logQueue; + } + if (this.desiredLogLevel) { + logMsgs.logLevel = this.desiredLogLevel; + } + if (Object.keys(logMsgs).length) { + this.loggerWindow.send('log', logMsgs); + } } } @@ -198,7 +210,7 @@ class Logger { } if (this.loggerWindow) { - this.loggerWindow.send('log', { msgs: [ logMsg ] }); + this.loggerWindow.send('log', { msgs: [ logMsg ], logLevel: this.desiredLogLevel, showInConsole: this.showInConsole }); } else { this.logQueue.push(logMsg); // don't store more than 100 msgs. keep most recent log msgs. @@ -230,4 +242,4 @@ class Logger { const logger = new Logger(); -export { logger }; \ No newline at end of file +export { logger }; diff --git a/src/common/utils.ts b/src/common/utils.ts index 1e96e484..5a102751 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -60,8 +60,12 @@ export const compareVersions = (v1: string, v2: string): number => { const n1 = parseInt(s1[i] || '0', 10); const n2 = parseInt(s2[i] || '0', 10); - if (n1 > n2) return 1; - if (n2 > n1) return -1; + if (n1 > n2) { + return 1; + } + if (n2 > n1) { + return -1; + } } if ([s1[2], s2[2]].every(patch.test.bind(patch))) { @@ -71,11 +75,19 @@ export const compareVersions = (v1: string, v2: string): number => { const p2 = patch.exec(s2[2])[1].split('.').map(tryParse); for (let k = 0; k < Math.max(p1.length, p2.length); k++) { - if (p1[k] === undefined || typeof p2[k] === 'string' && typeof p1[k] === 'number') return -1; - if (p2[k] === undefined || typeof p1[k] === 'string' && typeof p2[k] === 'number') return 1; + if (p1[k] === undefined || typeof p2[k] === 'string' && typeof p1[k] === 'number') { + return -1; + } + if (p2[k] === undefined || typeof p1[k] === 'string' && typeof p2[k] === 'number') { + return 1; + } - if (p1[k] > p2[k]) return 1; - if (p2[k] > p1[k]) return -1; + if (p1[k] > p2[k]) { + return 1; + } + if (p2[k] > p1[k]) { + return -1; + } } } else if ([s1[2], s2[2]].some(patch.test.bind(patch))) { return patch.test(s1[2]) ? -1 : 1; @@ -179,7 +191,9 @@ export const throttle = (func: (...args) => void, wait: number): (...args) => vo */ export const formatString = (str: string, data?: object): string => { - if (!str || !data) return str; + if (!str || !data) { + return str; + } for (const key in data) { if (Object.prototype.hasOwnProperty.call(data, key)) { @@ -193,4 +207,4 @@ export const formatString = (str: string, data?: object): string => { } } return str; -}; \ No newline at end of file +}; diff --git a/src/renderer/app-bridge.ts b/src/renderer/app-bridge.ts new file mode 100644 index 00000000..088e98ad --- /dev/null +++ b/src/renderer/app-bridge.ts @@ -0,0 +1,196 @@ +import { DesktopCapturerSource, remote } from 'electron'; + +import { + apiCmds, + IBoundsChange, + ILogMsg, + IScreenSharingIndicator, + IScreenSnippet, + LogLevel, +} from '../common/api-interface'; +import { IScreenSourceError } from './desktop-capturer'; +import { SSFApi } from './ssf-api'; + +const ssf = new SSFApi(); +const notification = remote.require('../renderer/notification').notification; + +export default class AppBridge { + + /** + * Validates the incoming postMessage + * events based on the host name + * + * @param event + */ + private static isValidEvent(event): boolean { + if (!event) { + return false; + } + return event.source && event.source === window; + } + + public origin: string; + + private readonly callbackHandlers = { + onMessage: (event) => this.handleMessage(event), + onActivityCallback: (idleTime: number) => this.activityCallback(idleTime), + onScreenSnippetCallback: (arg: IScreenSnippet) => this.screenSnippetCallback(arg), + onRegisterBoundsChangeCallback: (arg: IBoundsChange) => this.registerBoundsChangeCallback(arg), + onRegisterLoggerCallback: (msg: ILogMsg, logLevel: LogLevel, showInConsole: boolean) => + this.registerLoggerCallback(msg, logLevel, showInConsole), + onRegisterProtocolHandlerCallback: (uri: string) => this.protocolHandlerCallback(uri), + onScreenSharingIndicatorCallback: (arg: IScreenSharingIndicator) => this.screenSharingIndicatorCallback(arg), + onMediaSourceCallback: ( + requestId: number | undefined, + error: IScreenSourceError | null, + source: DesktopCapturerSource | undefined, + ): void => this.gotMediaSource(requestId, error, source), + onNotificationCallback: (event, data) => this.notificationCallback(event, data), + }; + + constructor() { + // starts with corporate pod and + // will be updated with the global config url + this.origin = 'https://corporate.symphony.com'; + window.addEventListener('message', this.callbackHandlers.onMessage); + } + + /** + * Switch case that validates and handle + * incoming messages from postMessage + * + * @param event + */ + private handleMessage(event): void { + if (!AppBridge.isValidEvent(event)) { + return; + } + + const { method, data } = event.data; + switch (method) { + case apiCmds.getVersionInfo: + this.broadcastMessage('get-version-info-callback', ssf.getVersionInfo()); + break; + case apiCmds.activate: + ssf.activate(data); + break; + case apiCmds.bringToFront: + const { windowName, reason } = data; + ssf.bringToFront(windowName, reason); + break; + case apiCmds.setBadgeCount: + if (typeof data === 'number') { + ssf.setBadgeCount(data); + } + break; + case apiCmds.setLocale: + if (typeof data === 'string') { + ssf.setLocale(data); + } + break; + case apiCmds.registerActivityDetection: + ssf.registerActivityDetection(data, this.callbackHandlers.onActivityCallback); + break; + case apiCmds.openScreenSnippet: + ssf.openScreenSnippet(this.callbackHandlers.onScreenSnippetCallback); + break; + case apiCmds.registerBoundsChange: + ssf.registerBoundsChange(this.callbackHandlers.onRegisterBoundsChangeCallback); + break; + case apiCmds.registerLogger: + ssf.registerLogger(this.callbackHandlers.onRegisterLoggerCallback); + break; + case apiCmds.registerProtocolHandler: + ssf.registerProtocolHandler(this.callbackHandlers.onRegisterProtocolHandlerCallback); + break; + case apiCmds.openScreenSharingIndicator: + ssf.showScreenSharingIndicator(data, this.callbackHandlers.onScreenSharingIndicatorCallback); + break; + case apiCmds.closeScreenSharingIndicator: + ssf.closeScreenSharingIndicator(data.streamId); + break; + case apiCmds.getMediaSource: + ssf.getMediaSource(data, this.callbackHandlers.onMediaSourceCallback); + break; + case apiCmds.notification: + notification.showNotification(data, this.callbackHandlers.onNotificationCallback); + break; + case apiCmds.closeNotification: + notification.hideNotification(data); + break; + } + } + + /** + * Broadcast user activity + * @param idleTime {number} - system idle tick + */ + private activityCallback = (idleTime: number): void => this.broadcastMessage('activity-callback', idleTime); + + /** + * Broadcast snippet data + * @param arg {IScreenSnippet} + */ + private screenSnippetCallback = (arg: IScreenSnippet): void => this.broadcastMessage('screen-snippet-callback', arg); + + /** + * Broadcast bound changes + * @param arg {IBoundsChange} + */ + private registerBoundsChangeCallback = (arg: IBoundsChange): void => this.broadcastMessage('bound-changes-callback', arg); + + /** + * Broadcast logs + * @param msg {ILogMsg} + * @param logLevel {LogLevel} + * @param showInConsole {boolean} + */ + private registerLoggerCallback(msg: ILogMsg, logLevel: LogLevel, showInConsole: boolean): void { + this.broadcastMessage('logger-callback', { msg, logLevel, showInConsole }); + } + + /** + * Broadcast protocol uri + * @param uri {string} + */ + private protocolHandlerCallback = (uri: string): void => this.broadcastMessage('protocol-callback', uri); + + /** + * Broadcast event that stops screen sharing + * @param arg {IScreenSharingIndicator} + */ + private screenSharingIndicatorCallback(arg: IScreenSharingIndicator): void { + this.broadcastMessage('screen-sharing-indicator-callback', arg); + } + + /** + * Broadcast the user selected source + * @param requestId {number} + * @param error {Error} + * @param source {DesktopCapturerSource} + */ + private gotMediaSource(requestId: number | undefined, error: IScreenSourceError | null, source: DesktopCapturerSource | undefined): void { + this.broadcastMessage('media-source-callback', { requestId, source, error }); + } + + /** + * Broadcast notification events + * + * @param event {string} + * @param data {Object} + */ + private notificationCallback(event, data) { + this.broadcastMessage(event, data); + } + + /** + * Method that broadcast messages to a specific origin via postMessage + * + * @param method {string} + * @param data {any} + */ + private broadcastMessage(method: string, data: any): void { + window.postMessage({ method, data }, this.origin); + } + +} diff --git a/src/renderer/assets/symphony-logo-black.png b/src/renderer/assets/symphony-logo-black.png new file mode 100644 index 0000000000000000000000000000000000000000..8ba73198eb7bac6f09794d954670a9763008e387 GIT binary patch literal 1768 zcmVP)@ydYN$h{lSMrO^6H2rcsO)E_4CTx)nD#>#*lK-DWSD}?|trl z`Ml@LifC?h(%DO=gIP2t=}gi&Z9=G~#vq+T_P11U#{O;tpe!&X5K%hAWdv+~lIxlU6YBdNzgwL-n z?mvVDB>*U~6{<-9$nU$B&*;ON#&rnPGyt=lKxZ0ElVt^9s*wVVhP>BJXHb_wO#^Ul z34n*s@v;IiV}4u}fKVfg+-esP6|KO-eLKoU={BE)sqUcZl|CCjLK4#W%A!l$M0yP1EivZ?k)ccW! zH4KqGrppF^RQM5gJjM=&8Bu(wVKM$PHAJMq+lbmy@dFFywr0uu9c;ouUi|1305b&g zCV@wq^A5frTKX;Z4+0Ad+`@B4>3nID1vXK2?JB;j!}Mm%&uyzdQ6cwtp!f(D=))R4 zT>yHxn9|M&5nGqN{fW;lO8`887+0`BvYb5tFu|sNX7O2s)?6z=H2JqWna8u8Z{!@hn}789_!V``?OXN_j3-`P&&mLjrKb1V2mXsrcB+ zqF&srWM}43et^a2h5$Uq=NnCve*y*NGhSq@cLCj`?8k!j5nDMaDac@N`{{gzzjuIk zQ5Nx`{FY(1A>OYO`DL2?4Hk2VK$jHA(_yhC6M!#J&o(B%k1U305P_=CFV_jG{#P?ve)|LW5Vo-0#Fu8pQwF}arnmaMTp$6^saQ{g`Zr{av`$YsNXOMM!SuW5Q)!w$q zd!9ql0o+2(xd#BOEN)QHj7oC*o~Zt2Xu`Qoio1_frh$!>9ZVKQF^Luoel>SJ-74*uF{UuekOQSxM&wsDpbYklm8abVhY{mAA>ZI9EJo=}z7KRiwZKW*=NYz76v|LX+aXa;+5ZCq{_!8X^&jl?OR~yuh^ql<(sD9{}zG?p7-R z3oJ4h|3BqHiaQ00)3v|g{?{cPA(O816kv{-`5&8DfUXNrgu4L;J!hV zg@R(J5qszroN-z=IjHafN^fZFxVpm+vGZpdJwY00=n;Icwz5kqP@|)>CI=+Otjc;W z^xhj%MyPVQxoTHgTo(WWWvxkbV!&Lj%E73qxO%rYxG5%RGfr6>Y=eO?*Kh&u3O(E( zTDI&y8!HXf0S**cV}WYI%3r^PW5|1U@qMqx_E=-T7g!)z9*L`Wg4A9F@^#~LocQyuj$Z$C+fAB+Dm!-(08XYxcNG9)eY~rDrvMPF6dnl6 zM((l#K%FI-4u7_3?RAz^dFmNh|9Gmigo^XV>b@-}SOx&7IqHe*Hk1-tp0EA4L3!gp0R{kzWc#B99SHCM0000< KMNUMnLSTX{Bsp&Y literal 0 HcmV?d00001 diff --git a/src/renderer/assets/symphony-logo-white.png b/src/renderer/assets/symphony-logo-white.png new file mode 100644 index 0000000000000000000000000000000000000000..1a48419221975f777f181fcb6d1eaec908daca02 GIT binary patch literal 1796 zcmV+f2mAPmP)jE-pjE+Y)ic03{TlB$o`pR9q(F zT&fHa5q9r@3&gk1rkbj0OGoFj31bc*kY5f612 zb@d4V^o?oSYNg^l!r*0QzKa3i3D<7Tc}BPf7X<+AqPV0M03a8LY{h^k8Ne(U)Ci;_ z0QhKYK}`b?*JOZ8zEAPsQUR#!0!WV|o4fxR07Mx@XuA7!94{FF4qTmz!S85W4K@mZ zwD`Uu{at|eXnRdfGl1QoXL!#T!7us~05A%KV)uI}qJncQrpa1>gC9}3hw6}zo!H%2 z;rprW;MW)c5HGGIVI8T^N$_vi1^03hbb^w+8~_lwCZKx)3Wu^EgAuM`nj0`CIM+Jc zXE2<6_6~tA0=>hE(@miJ;~H!6#Qj?%09bX28-ogT_u;eKI+7LapmiK-Hel%UvslaC zgaNk{c|qfyLHda#v2&_PCQ5y&DTs(0J~9V0Zz8_W6;W3O$PW>9X|lQa?iLg4l)Mkv zxw{&>-$iUVR$$-{2(2v|C>t>HR#yj1AUeozR-hm)<_8#2211N`Mmpn5bbnxA{0uu! zyZn=xgG~v*=%Eq-Y4;COD{2oQsFo)t%n@8vAnf4|v$C-Y`fGK8X?}4opcklFy2=Jk ztS8}^pkjcR=XW%rIM+lg&}%_Kro=6{VR>grV;3%T)ExUA;~&HtcZ%T$u%MR<0E`)_ zei*>qW6T6Vj1=);1JyUHp!-8rpcAJLzb0nhYoyu}Vu4r0fEyjZgIf?2)|k+TTXb{A zhM3PQ0xBo?DRn>Gbb-w}5HodDBF3B~J3jzG6UMsrHl;@LH%gPTTjY$}p%r$gkx4vN z(*yH!4Y4Z9?TUlS>5jY^Spba7O^Brx?xlSDS5CCu;Odv;Zx>CEG|LPWPZQ+!RTXsz zJ(D`pBf6_hi>i`jCDtJ4uJP47?SRGZ5rn2Dx*xl>kaK&@!YKLx05>S=HXLw&Ry%kS zQ$nz$S3v)mcNT#zH@Dkt$jAU#>9tAPVTlM$T3`XAH~1Yt6Ay-1Lm(9~-&MK2#u3Y` zX8^jk0LZw-T9U>@<0*n>Rw|F-nnY)>nZ_I6NpeO=pM#< zaE%y2_eEOF5uk7|e#i-i9mE2Dgg%fy%g47OduUEE&j$c{RRf@|ThPu^MJDwMoJ=!> zgWv&R^x+Epu0Y>;-%d}ln`G#-REged^c<7Ca`;=t2NtHs4ot`jqmHE4Cz-`^N9hIZ zc`C{T!iKk|Yl$ss9sq7hd#b79fSpY%W+is7dC9mp#=OPv z=6FEw0|AIobFHCG09fPqOvFfz;crb+eDA{e2xC|(lW3Z9Lq&XMMLC9eN+edK1@(lH zrqKaFexMm^7(RqiL)mbt+?_%4?&U~LQkm}+*AtpB34qxHxH0aZA*BYHO&Lm?W(NRT z1dt!7nk6d3rNhs3i=3RQae>`l$-9iXjsTGbl`H-pgB2%4|I~!3x-lSvP}x>AoST+C zD7-|W(zZxpv%KHKy(FskSd$0wGpo{$*99O1+9M7`x`s<*J)J;CxgWkyuJEg9POyVn z!9mk?4BsKp0swkGL2E13HCzfzKH|qt^Y_T-2k`aMSK=U;2)LbTufXK}fJYcTM?j>0 z+)_iZ_{i}a!^LgOx=01AaLxt5KiKgDy0CkVjNk#ZooPLfU9ZwrK8p}ybeD$nbCZ*d z=$P&hT#viT;U&HzvqFlZ;Pf~8gd@W__J|+8&hts*QoV`t4!L_G)%=`VuaO$ps@;`Z zWKXt=vPJ>ONr~J8?CJm@#giH0NzixYT zMYeFA*;ix$yU+pB-2HL@uvB*be*vTW { private updateState(_event, data): void { this.setState(data as IState); } -} \ No newline at end of file +} diff --git a/src/renderer/components/download-manager.tsx b/src/renderer/components/download-manager.tsx index b7fed733..091ac79c 100644 --- a/src/renderer/components/download-manager.tsx +++ b/src/renderer/components/download-manager.tsx @@ -77,7 +77,9 @@ export default class DownloadManager extends React.Component<{}, IManagerState> * @param item */ private mapItems(item: IDownloadManager): JSX.Element | undefined { - if (!item) return; + if (!item) { + return; + } const { _id, total, fileName }: IDownloadManager = item; const fileDisplayName = this.getFileDisplayName(fileName); diff --git a/src/renderer/components/loading-screen.tsx b/src/renderer/components/loading-screen.tsx index 2507e8ed..2bc4eaf3 100644 --- a/src/renderer/components/loading-screen.tsx +++ b/src/renderer/components/loading-screen.tsx @@ -23,4 +23,4 @@ export default class LoadingScreen extends React.PureComponent { ); } -} \ No newline at end of file +} diff --git a/src/renderer/components/more-info.tsx b/src/renderer/components/more-info.tsx index a15fd960..abb061a2 100644 --- a/src/renderer/components/more-info.tsx +++ b/src/renderer/components/more-info.tsx @@ -27,4 +27,4 @@ export default class MoreInfo extends React.Component<{}, {}> { ); } -} \ No newline at end of file +} diff --git a/src/renderer/components/notification-comp.tsx b/src/renderer/components/notification-comp.tsx new file mode 100644 index 00000000..9764a121 --- /dev/null +++ b/src/renderer/components/notification-comp.tsx @@ -0,0 +1,108 @@ +import classNames from 'classnames'; +import { ipcRenderer } from 'electron'; +import * as React from 'react'; + +import { i18n } from '../../common/i18n-preload'; + +const whiteColorRegExp = new RegExp(/^(?:white|#fff(?:fff)?|rgba?\(\s*255\s*,\s*255\s*,\s*255\s*(?:,\s*1\s*)?\))$/i); + +interface IState { + title: string; + company: string; + body: string; + image: string; + id: number; + color: string; +} + +type mouseEventButton = React.MouseEvent; + +export default class NotificationComp extends React.Component<{}, IState> { + + private readonly eventHandlers = { + onClose: (winKey) => (_event: mouseEventButton) => this.close(winKey), + onClick: (data) => (_event: mouseEventButton) => this.click(data), + }; + + constructor(props) { + super(props); + this.state = { + title: '', + company: 'Symphony', + body: '', + image: '', + id: 0, + color: '', + }; + this.updateState = this.updateState.bind(this); + } + + public componentDidMount(): void { + ipcRenderer.on('notification-data', this.updateState); + } + + public componentWillUnmount(): void { + ipcRenderer.removeListener('notification-data', this.updateState); + } + + /** + * Renders the custom title bar + */ + public render(): JSX.Element { + const { title, company, body, image, id, color } = this.state; + const isLightTheme = color ? color.match(whiteColorRegExp) : true; + + const theme = classNames({ light: isLightTheme, dark: !isLightTheme }); + const bgColor = { backgroundColor: color || '#ffffff' }; + + return ( +
+
+ symphony logo +
+
+ {title} + {company} + {body} +
+
+ user profile picture +
+
+ + + + +
+
+ ); + } + + /** + * Invoked when the notification window is clicked + * + * @param id {number} + */ + private click(id: number) { + ipcRenderer.send('notification-clicked', id); + } + + /** + * Closes the notification + * + * @param id {number} + */ + private close(id: number) { + ipcRenderer.send('close-notification', id); + } + + /** + * Sets the About app state + * + * @param _event + * @param data {Object} + */ + private updateState(_event, data): void { + this.setState(data as IState); + } +} diff --git a/src/renderer/components/screen-picker.tsx b/src/renderer/components/screen-picker.tsx index 23e48140..56ce53ec 100644 --- a/src/renderer/components/screen-picker.tsx +++ b/src/renderer/components/screen-picker.tsx @@ -62,7 +62,9 @@ export default class ScreenPicker extends React.Component<{}, IState> { public componentDidMount(): void { ipcRenderer.on('screen-picker-data', this.updateState); document.addEventListener('keyup', this.handleKeyUpPress, true); - if (isWindowsOS) document.body.classList.add('ScreenPicker-window-border'); + if (isWindowsOS) { + document.body.classList.add('ScreenPicker-window-border'); + } } public componentWillUnmount(): void { @@ -370,4 +372,4 @@ export default class ScreenPicker extends React.Component<{}, IState> { private updateState(_event, data): void { this.setState(data as IState); } -} \ No newline at end of file +} diff --git a/src/renderer/components/screen-sharing-indicator.tsx b/src/renderer/components/screen-sharing-indicator.tsx index 61bb4f1e..1d44e98b 100644 --- a/src/renderer/components/screen-sharing-indicator.tsx +++ b/src/renderer/components/screen-sharing-indicator.tsx @@ -8,6 +8,7 @@ import { i18n } from '../../common/i18n-preload'; interface IState { id: number; + streamId: string; } type mouseEventButton = React.MouseEvent; @@ -25,6 +26,7 @@ export default class ScreenSharingIndicator extends React.Component<{}, IState> super(props); this.state = { id: 0, + streamId: '', }; this.updateState = this.updateState.bind(this); } @@ -70,9 +72,11 @@ export default class ScreenSharingIndicator extends React.Component<{}, IState> * Closes the screen sharing indicator window */ private close(): void { + const { streamId } = this.state; ipcRenderer.send(apiName.symphonyApi, { cmd: apiCmds.closeWindow, windowType: 'screen-sharing-indicator', + winKey: streamId, }); } @@ -85,4 +89,4 @@ export default class ScreenSharingIndicator extends React.Component<{}, IState> private updateState(_event, data): void { this.setState(data as IState); } -} \ No newline at end of file +} diff --git a/src/renderer/components/snack-bar.tsx b/src/renderer/components/snack-bar.tsx index eb21b221..e68bd29f 100644 --- a/src/renderer/components/snack-bar.tsx +++ b/src/renderer/components/snack-bar.tsx @@ -66,4 +66,4 @@ export default class SnackBar extends React.Component<{}, IState> { ) :
; } -} \ No newline at end of file +} diff --git a/src/renderer/desktop-capturer.ts b/src/renderer/desktop-capturer.ts index 568c6df0..b3fca0d3 100644 --- a/src/renderer/desktop-capturer.ts +++ b/src/renderer/desktop-capturer.ts @@ -17,7 +17,16 @@ let nextId = 0; let isScreenShareEnabled = true; let screenShareArgv: string; -type CallbackType = (error: Error | null, source?: DesktopCapturerSource) => DesktopCapturerSource | Error; +export interface ICustomSourcesOptions extends SourcesOptions { + requestId?: number; +} + +export interface IScreenSourceError { + name: string; + message: string; +} + +export type CallbackType = (requestId: number | undefined, error: IScreenSourceError | null, source?: DesktopCapturerSource) => void; const getNextId = () => ++nextId; /** @@ -25,7 +34,7 @@ const getNextId = () => ++nextId; * @param options |options.type| can not be empty and has to include 'window' or 'screen'. * @returns {boolean} */ -const isValid = (options: SourcesOptions) => { +const isValid = (options: ICustomSourcesOptions) => { return ((options !== null ? options.types : undefined) !== null) && Array.isArray(options.types); }; @@ -36,19 +45,19 @@ const isValid = (options: SourcesOptions) => { * @param callback {CallbackType} * @returns {*} */ -export const getSource = (options: SourcesOptions, callback: CallbackType) => { +export const getSource = (options: ICustomSourcesOptions, callback: CallbackType) => { let captureWindow; let captureScreen; let id; const sourcesOpts: string[] = []; + const { requestId, ...updatedOptions } = options; if (!isValid(options)) { - callback(new Error('Invalid options')); + callback(requestId, { name: 'Invalid options', message: 'Invalid options' }); return; } captureWindow = includes.call(options.types, 'window'); captureScreen = includes.call(options.types, 'screen'); - const updatedOptions = options; if (!updatedOptions.thumbnailSize) { updatedOptions.thumbnailSize = { height: 150, @@ -83,7 +92,7 @@ export const getSource = (options: SourcesOptions, callback: CallbackType) => { title: `${i18n.t('Permission Denied')()}!`, type: 'error', }); - callback(new Error('Permission Denied')); + callback(requestId, { name: 'Permission Denied', message: 'Permission Denied' }); return; } } @@ -97,11 +106,11 @@ export const getSource = (options: SourcesOptions, callback: CallbackType) => { const filteredSource: DesktopCapturerSource[] = sources.filter((source) => source.name === title); if (Array.isArray(filteredSource) && filteredSource.length > 0) { - return callback(null, filteredSource[ 0 ]); + return callback(requestId, null, filteredSource[ 0 ]); } if (sources.length > 0) { - return callback(null, sources[ 0 ]); + return callback(requestId, null, sources[ 0 ]); } } @@ -122,9 +131,9 @@ export const getSource = (options: SourcesOptions, callback: CallbackType) => { // Cleaning up the event listener to prevent memory leaks if (!source) { ipcRenderer.removeListener('start-share' + id, successCallback); - return callback(new Error('User Cancelled')); + return callback(requestId, { name: 'User Cancelled', message: 'User Cancelled' }); } - return callback(null, source); + return callback(requestId, null, source); }; ipcRenderer.once('start-share' + id, successCallback); return null; @@ -143,4 +152,4 @@ ipcRenderer.on('is-screen-share-enabled', (_event, screenShare) => { if (typeof screenShare === 'boolean' && screenShare) { isScreenShareEnabled = true; } -}); \ No newline at end of file +}); diff --git a/src/renderer/notification-handler.ts b/src/renderer/notification-handler.ts new file mode 100644 index 00000000..f32b0c21 --- /dev/null +++ b/src/renderer/notification-handler.ts @@ -0,0 +1,223 @@ +import * as asyncMap from 'async.map'; +import * as electron from 'electron'; + +import { windowExists } from '../app/window-utils'; +import { isMac } from '../common/env'; + +interface ISettings { + startCorner: startCorner; + height: number; + width: number; + totalHeight: number; + totalWidth: number; + corner: ICorner; + firstPos: ICorner; + maxVisibleNotifications: number; + animationSteps: number; + animationStepMs: number; +} + +interface ICorner { + x: number; + y: number; +} + +type startCorner = 'upper-right' | 'upper-left' | 'lower-right' | 'lower-left'; + +export default class NotificationHandler { + public settings: ISettings; + public nextInsertPos: ICorner = { x: 0, y: 0 }; + + private readonly eventHandlers = { + onSetup: () => this.setupNotificationPosition(), + }; + + private externalDisplay: Electron.Display | undefined; + private displayId: string = ''; + + constructor(opts) { + this.settings = opts as ISettings; + this.setupNotificationPosition(); + + electron.screen.on('display-added', this.eventHandlers.onSetup); + electron.screen.on('display-removed', this.eventHandlers.onSetup); + electron.screen.on('display-metrics-changed', this.eventHandlers.onSetup); + } + + /** + * Sets the position of the notification window + * + * @param window {BrowserWindow} + * @param x {number} + * @param y {number} + */ + public setWindowPosition(window: Electron.BrowserWindow, x: number = 0, y: number = 0) { + if (window && !window.isDestroyed()) { + window.setPosition(parseInt(String(x), 10), parseInt(String(y), 10)); + } + } + + /** + * Initializes / resets the notification positional values + */ + public setupNotificationPosition() { + // This feature only applies to windows + if (isMac) { + return; + } + const screens = electron.screen.getAllDisplays(); + if (screens && screens.length >= 0) { + this.externalDisplay = screens.find((screen) => { + const screenId = screen.id.toString(); + return screenId === this.displayId; + }); + } + + const display = this.externalDisplay || electron.screen.getPrimaryDisplay(); + this.settings.corner.x = display.workArea.x; + this.settings.corner.y = display.workArea.y; + + // update corner x/y based on corner of screen where notification should appear + const workAreaWidth = display.workAreaSize.width; + const workAreaHeight = display.workAreaSize.height; + switch (this.settings.startCorner) { + case 'upper-right': + this.settings.corner.x += workAreaWidth; + break; + case 'lower-right': + this.settings.corner.x += workAreaWidth; + this.settings.corner.y += workAreaHeight; + break; + case 'lower-left': + this.settings.corner.y += workAreaHeight; + break; + case 'upper-left': + default: + // no change needed + break; + } + this.calculateDimensions(); + // Maximum amount of Notifications we can show: + this.settings.maxVisibleNotifications = Math.floor(display.workAreaSize.height / this.settings.totalHeight); + } + + /** + * Find next possible insert position (on top) + */ + public calcNextInsertPos(activeNotificationLength) { + if (activeNotificationLength < this.settings.maxVisibleNotifications) { + switch (this.settings.startCorner) { + case 'upper-right': + case 'upper-left': + this.nextInsertPos.y = this.settings.corner.y + (this.settings.totalHeight * activeNotificationLength); + break; + + default: + case 'lower-right': + case 'lower-left': + this.nextInsertPos.y = this.settings.corner.y - (this.settings.totalHeight * (activeNotificationLength + 1)); + break; + } + } + } + + /** + * Moves the notification by one step + * + * @param startPos {number} + * @param activeNotifications {ICustomBrowserWindow[]} + */ + public moveNotificationDown(startPos, activeNotifications) { + if (startPos >= activeNotifications || startPos === -1) { + return; + } + // Build array with index of affected notifications + const notificationPosArray: number[] = []; + for (let i = startPos; i < activeNotifications.length; i++) { + notificationPosArray.push(i); + } + asyncMap(notificationPosArray, (i, done) => { + // Get notification to move + const notificationWindow = activeNotifications[i]; + + // Calc new y position + let newY; + switch (this.settings.startCorner) { + case 'upper-right': + case 'upper-left': + newY = this.settings.corner.y + (this.settings.totalHeight * i); + break; + default: + case 'lower-right': + case 'lower-left': + newY = this.settings.corner.y - (this.settings.totalHeight * (i + 1)); + break; + } + + if (!windowExists(notificationWindow)) { + return; + } + + // Get startPos, calc step size and start animationInterval + const startY = notificationWindow.getPosition()[1]; + const step = (newY - startY) / this.settings.animationSteps; + let curStep = 1; + const animationInterval = setInterval(() => { + // Abort condition + if (curStep === this.settings.animationSteps) { + this.setWindowPosition(notificationWindow, this.settings.firstPos.x, newY); + clearInterval(animationInterval); + done(null, 'done'); + return; + } + // Move one step down + this.setWindowPosition(notificationWindow, this.settings.firstPos.x, startY + curStep * step); + curStep++; + }, this.settings.animationStepMs); + }); + } + + /** + * Calculates the first and next notification insert position + */ + private calculateDimensions() { + const vertSpace = 8; + + // Calc totalHeight & totalWidth + this.settings.totalHeight = this.settings.height + vertSpace; + this.settings.totalWidth = this.settings.width; + + let firstPosX; + let firstPosY; + switch (this.settings.startCorner) { + case 'upper-right': + firstPosX = this.settings.corner.x - this.settings.totalWidth; + firstPosY = this.settings.corner.y; + break; + case 'lower-right': + firstPosX = this.settings.corner.x - this.settings.totalWidth; + firstPosY = this.settings.corner.y - this.settings.totalHeight; + break; + case 'lower-left': + firstPosX = this.settings.corner.x; + firstPosY = this.settings.corner.y - this.settings.totalHeight; + break; + case 'upper-left': + default: + firstPosX = this.settings.corner.x; + firstPosY = this.settings.corner.y; + break; + } + + // Calc pos of first notification: + this.settings.firstPos = { + x: firstPosX, + y: firstPosY, + }; + + // Set nextInsertPos + this.nextInsertPos.x = this.settings.firstPos.x; + this.nextInsertPos.y = this.settings.firstPos.y; + } + +} diff --git a/src/renderer/notification.ts b/src/renderer/notification.ts new file mode 100644 index 00000000..2b37b54a --- /dev/null +++ b/src/renderer/notification.ts @@ -0,0 +1,321 @@ +import { ipcMain } from 'electron'; + +import { createComponentWindow, windowExists } from '../app/window-utils'; +import { AnimationQueue } from '../common/animation-queue'; +import { logger } from '../common/logger'; +import NotificationHandler from './notification-handler'; + +// const MAX_QUEUE_SIZE = 30; +const CLEAN_UP_INTERVAL = 60 * 100; +const animationQueue = new AnimationQueue(); + +interface ICustomBrowserWindow extends Electron.BrowserWindow { + notificationData: INotificationData; + displayTimer: NodeJS.Timer; + clientId: number; +} + +interface INotificationData { + id: number; + title: string; + text: string; + image: string; + flash: boolean; + color: string; + tag: string; + sticky: boolean; + company: string; + displayTime: number; +} + +type startCorner = 'upper-right' | 'upper-left' | 'lower-right' | 'lower-left'; + +const notificationSettings = { + startCorner: 'upper-right' as startCorner, + 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), + }; + private readonly activeNotifications: Electron.BrowserWindow[] = []; + private readonly inactiveWindows: Electron.BrowserWindow[] = []; + private readonly notificationQueue: INotificationData[] = []; + private readonly notificationCallbacks: any[] = []; + private cleanUpTimer: NodeJS.Timer; + + constructor(opts) { + super(opts); + ipcMain.on('close-notification', (_event, windowId) => { + this.hideNotification(windowId); + }); + + ipcMain.on('notification-clicked', (_event, windowId) => { + this.notificationClicked(windowId); + }); + 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 { + + 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; + 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 { + 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; + } + + /** + * Waits for window to load and resolves + * + * @param window + * @param data + */ + private didFinishLoad(window, data) { + return new Promise((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() { + logger.info('active notification', this.activeNotifications.length); + logger.info('inactive notification', this.inactiveWindows.length); + if (this.inactiveWindows.length > 0) { + logger.info('cleaning up inactive notification windows', { inactiveNotification: this.inactiveWindows.length }); + this.inactiveWindows.forEach((window) => { + if (windowExists(window)) { + window.close(); + } + }); + logger.info(`Cleaned up inactive notification windows`, { inactiveNotification: this.inactiveWindows.length }); + } + } + + /** + * 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, +}; diff --git a/src/renderer/preload-component.ts b/src/renderer/preload-component.ts index 974637a8..eea7170d 100644 --- a/src/renderer/preload-component.ts +++ b/src/renderer/preload-component.ts @@ -7,6 +7,7 @@ import AboutBox from './components/about-app'; import BasicAuth from './components/basic-auth'; import LoadingScreen from './components/loading-screen'; import MoreInfo from './components/more-info'; +import NotificationComp from './components/notification-comp'; import ScreenPicker from './components/screen-picker'; import ScreenSharingIndicator from './components/screen-sharing-indicator'; @@ -17,6 +18,7 @@ const enum components { screenPicker = 'screen-picker', screenSharingIndicator = 'screen-sharing-indicator', basicAuth = 'basic-auth', + notification = 'notification-comp', } const loadStyle = (style) => { @@ -60,6 +62,10 @@ const load = () => { loadStyle(components.basicAuth); component = BasicAuth; break; + case components.notification: + loadStyle(components.notification); + component = NotificationComp; + break; } const element = React.createElement(component); ReactDOM.render(element, document.getElementById('Root')); @@ -70,4 +76,4 @@ document.addEventListener('DOMContentLoaded', load); ipcRenderer.on('set-locale-resource', (_event, data) => { const { locale, resource } = data; i18n.setResource(locale, resource); -}); \ No newline at end of file +}); diff --git a/src/renderer/preload-main.ts b/src/renderer/preload-main.ts index 31e09093..937f9c3d 100644 --- a/src/renderer/preload-main.ts +++ b/src/renderer/preload-main.ts @@ -3,6 +3,7 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { i18n } from '../common/i18n-preload'; +import AppBridge from './app-bridge'; import DownloadManager from './components/download-manager'; import SnackBar from './components/snack-bar'; import WindowsTitleBar from './components/windows-title-bar'; @@ -13,6 +14,7 @@ interface ISSFWindow extends Window { } const ssfWindow: ISSFWindow = window; +const appBridge = new AppBridge(); /** * creates API exposed from electron. @@ -39,7 +41,11 @@ const createAPI = () => { createAPI(); // When the window is completely loaded -ipcRenderer.on('page-load', (_event, { locale, resources }) => { +ipcRenderer.on('page-load', (_event, { locale, resources, origin }) => { + // origin for postMessage targetOrigin communication + if (origin) { + appBridge.origin = origin; + } i18n.setResource(locale, resources); @@ -63,4 +69,4 @@ ipcRenderer.on('initiate-custom-title-bar', () => { const div = document.createElement( 'div' ); document.body.appendChild(div); ReactDOM.render(element, div); -}); \ No newline at end of file +}); diff --git a/src/renderer/ssf-api.ts b/src/renderer/ssf-api.ts index bcfd78b6..255a6a9e 100644 --- a/src/renderer/ssf-api.ts +++ b/src/renderer/ssf-api.ts @@ -1,12 +1,17 @@ import { ipcRenderer, remote } from 'electron'; +import { buildNumber } from '../../package.json'; import { apiCmds, apiName, - IActivityDetection, IBadgeCount, - IBoundsChange, IScreenSharingIndicator, - IScreenSnippet, KeyCodes, + IBoundsChange, + ILogMsg, + IScreenSharingIndicator, + IScreenSnippet, + IVersionInfo, + KeyCodes, + LogLevel, } from '../common/api-interface'; import { i18n, LocaleType } from '../common/i18n-preload'; import { throttle } from '../common/utils'; @@ -14,19 +19,20 @@ import { getSource } from './desktop-capturer'; let isAltKey: boolean = false; let isMenuOpen: boolean = false; -let nextId = 0; interface ICryptoLib { AESGCMEncrypt: (name: string, base64IV: string, base64AAD: string, base64Key: string, base64In: string) => string | null; AESGCMDecrypt: (base64IV: string, base64AAD: string, base64Key: string, base64In: string) => string | null; } -interface ILocalObject { +export interface ILocalObject { ipcRenderer; - activityDetectionCallback?: (arg: IActivityDetection) => void; + logger?: (msg: ILogMsg, logLevel: LogLevel, showInConsole: boolean) => void; + activityDetectionCallback?: (arg: number) => void; screenSnippetCallback?: (arg: IScreenSnippet) => void; boundsChangeCallback?: (arg: IBoundsChange) => void; screenSharingIndicatorCallback?: (arg: IScreenSharingIndicator) => void; + protocolActionCallback?: (arg: string) => void; } const local: ILocalObject = { @@ -48,6 +54,29 @@ const throttledSetLocale = throttle((locale) => { }); }, 1000); +const throttledActivate = throttle((windowName) => { + local.ipcRenderer.send(apiName.symphonyApi, { + cmd: apiCmds.activate, + windowName, + }); +}, 1000); + +const throttledBringToFront = throttle((windowName, reason) => { + local.ipcRenderer.send(apiName.symphonyApi, { + cmd: apiCmds.bringToFront, + windowName, + reason, + }); +}, 1000); + +const throttledCloseScreenShareIndicator = throttle((streamId) => { + ipcRenderer.send(apiName.symphonyApi, { + cmd: apiCmds.closeWindow, + windowType: 'screen-sharing-indicator', + winKey: streamId, + }); +}, 1000); + let cryptoLib: ICryptoLib | null; try { cryptoLib = remote.require('../app/crypto-handler.js').cryptoLibrary; @@ -75,6 +104,45 @@ export class SSFApi { */ public getMediaSource = getSource; + /** + * Brings window forward and gives focus. + * + * @param {String} windowName - Name of window. Note: main window name is 'main' + */ + public activate(windowName) { + if (typeof windowName === 'string') { + throttledActivate(windowName); + } + } + + /** + * Brings window forward and gives focus. + * + * @param {String} windowName Name of window. Note: main window name is 'main' + * @param {String} reason, The reason for which the window is to be activated + */ + public bringToFront(windowName, reason) { + if (typeof windowName === 'string') { + throttledBringToFront(windowName, reason); + } + } + + /** + * Method that returns various version info + */ + public getVersionInfo(): IVersionInfo { + const appName = remote.app.getName(); + const appVer = remote.app.getVersion(); + + return { + containerIdentifier: appName, + containerVer: appVer, + buildNumber, + apiVer: '2.0.0', + searchApiVer: '3.0.0', + }; + } + /** * Allows JS to register a activity detector that can be used by electron main process. * @@ -82,7 +150,7 @@ export class SSFApi { * @param {Object} activityDetectionCallback - function that can be called accepting * @example registerActivityDetection(40000, func) */ - public registerActivityDetection(period: number, activityDetectionCallback: Partial): void { + public registerActivityDetection(period: number, activityDetectionCallback: (arg: number) => void): void { if (typeof activityDetectionCallback === 'function') { local.activityDetectionCallback = activityDetectionCallback; @@ -101,18 +169,64 @@ export class SSFApi { * only one window can register for bounds change. * @param {Function} callback Function invoked when bounds changes. */ - public registerBoundsChange(callback: () => void): void { + public registerBoundsChange(callback: (arg: IBoundsChange) => void): void { if (typeof callback === 'function') { local.boundsChangeCallback = callback; } } + /** + * Allows JS to register a logger that can be used by electron main process. + * @param {Object} logger function that can be called accepting + * object: { + * logLevel: 'ERROR'|'CONFLICT'|'WARN'|'ACTION'|'INFO'|'DEBUG', + * logDetails: String + * } + */ + public registerLogger(logger) { + if (typeof logger === 'function') { + local.logger = logger; + + // only main window can register + local.ipcRenderer.send(apiName.symphonyApi, { + cmd: apiCmds.registerLogger, + }); + } + } + + /** + * Allows JS to register a protocol handler that can be used by the + * electron main process. + * + * @param protocolHandler {Function} callback will be called when app is + * invoked with registered protocol (e.g., symphony). The callback + * receives a single string argument: full uri that the app was + * invoked with e.g., symphony://?streamId=xyz123&streamType=chatroom + * + * Note: this function should only be called after client app is fully + * able for protocolHandler callback to be invoked. It is possible + * the app was started using protocol handler, in this case as soon as + * this registration func is invoked then the protocolHandler callback + * will be immediately called. + */ + public registerProtocolHandler(protocolHandler) { + if (typeof protocolHandler === 'function') { + + local.protocolActionCallback = protocolHandler; + + local.ipcRenderer.send(apiName.symphonyApi, { + cmd: apiCmds.registerProtocolHandler, + }); + + } + } + /** * Allow user to capture portion of screen * * @param screenSnippetCallback {function} */ - public openScreenSnippet(screenSnippetCallback: Partial): void { + public openScreenSnippet(screenSnippetCallback: (arg: IScreenSnippet) => void): void { if (typeof screenSnippetCallback === 'function') { local.screenSnippetCallback = screenSnippetCallback; @@ -160,35 +274,40 @@ export class SSFApi { * - 'stopRequested' - user clicked "Stop Sharing" button. */ public showScreenSharingIndicator(options, callback): void { - const { stream, displayId } = options; + const { displayId, requestId, streamId } = options; if (typeof callback === 'function') { - if (!stream || !stream.active || stream.getVideoTracks().length !== 1) { - callback({ type: 'error', reason: 'bad stream' }); - return; - } - if (displayId && typeof (displayId) !== 'string') { - callback({ type: 'error', reason: 'bad displayId' }); - return; - } - local.screenSharingIndicatorCallback = callback; - const id = ++nextId; ipcRenderer.send(apiName.symphonyApi, { cmd: apiCmds.openScreenSharingIndicator, - displayId: options.displayId, - id, + displayId, + id: requestId, + streamId, }); } } + /** + * Closes the screen sharing indicator + */ + public closeScreenSharingIndicator(winKey: string): void { + throttledCloseScreenShareIndicator(winKey); + } + } /** * Ipc events */ -// Creates a data url +/** + * An event triggered by the main process + * to construct a canvas for the Windows badge count image + * + * @param {IBadgeCount} arg { + * count: number + * } + */ local.ipcRenderer.on('create-badge-data-url', (_event: Event, arg: IBadgeCount) => { const count = arg && arg.count || 0; @@ -228,19 +347,47 @@ local.ipcRenderer.on('create-badge-data-url', (_event: Event, arg: IBadgeCount) } }); +/** + * An event triggered by the main process + * when the snippet is complete + * + * @param {IScreenSnippet} arg { + * message: string, + * data: base64, + * type: 'ERROR' | 'image/jpg;base64', + * } + */ local.ipcRenderer.on('screen-snippet-data', (_event: Event, arg: IScreenSnippet) => { if (typeof arg === 'object' && typeof local.screenSnippetCallback === 'function') { local.screenSnippetCallback(arg); } }); -local.ipcRenderer.on('activity', (_event: Event, arg: IActivityDetection) => { - if (typeof arg === 'object' && typeof local.activityDetectionCallback === 'function') { - local.activityDetectionCallback(arg); +/** + * An event triggered by the main process + * for ever few minutes if the user is active + * + * @param {number} idleTime - current system idle tick + */ +local.ipcRenderer.on('activity', (_event: Event, idleTime: number) => { + if (typeof idleTime === 'number' && typeof local.activityDetectionCallback === 'function') { + local.activityDetectionCallback(idleTime); } }); -// listen for notifications that some window size/position has changed +/** + * An event triggered by the main process + * Whenever some Window position or dimension changes + * + * @param {IBoundsChange} arg { + * x: number, + * y: number, + * height: number, + * width: number, + * windowName: string + * } + * + */ local.ipcRenderer.on('boundsChange', (_event, arg: IBoundsChange): void => { const { x, y, height, width, windowName } = arg; if (x && y && height && width && windowName && typeof local.boundsChangeCallback === 'function') { @@ -254,14 +401,40 @@ local.ipcRenderer.on('boundsChange', (_event, arg: IBoundsChange): void => { } }); -local.ipcRenderer.on('screen-sharing-stopped', () => { +/** + * An event triggered by the main process + * when the screen sharing has been stopper + */ +local.ipcRenderer.on('screen-sharing-stopped', (_event, id) => { if (typeof local.screenSharingIndicatorCallback === 'function') { - local.screenSharingIndicatorCallback({ type: 'stopRequested' }); - // closes the screen sharing indicator - ipcRenderer.send(apiName.symphonyApi, { - cmd: apiCmds.closeWindow, - windowType: 'screen-sharing-indicator', - }); + local.screenSharingIndicatorCallback({ type: 'stopRequested', requestId: id }); + } +}); + +/** + * An event triggered by the main process + * for send logs on to web app + * + * @param {object} arg { + * msgs: ILogMsg[], + * logLevel: LogLevel, + * showInConsole: boolean + * } + * + */ +local.ipcRenderer.on('log', (_event, arg) => { + if (arg && local.logger) { + local.logger(arg.msgs || [], arg.logLevel, arg.showInConsole); + } +}); + +/** + * An event triggered by the main process for processing protocol urls + * @param {String} arg - the protocol url + */ +local.ipcRenderer.on('protocol-action', (_event, arg: string) => { + if (typeof local.protocolActionCallback === 'function' && typeof arg === 'string') { + local.protocolActionCallback(arg); } }); @@ -284,7 +457,7 @@ const updateOnlineStatus = (): void => { }; // Handle key down events -const throttledKeyDown = throttle( (event) => { +const throttledKeyDown = throttle((event) => { isAltKey = event.keyCode === KeyCodes.Alt; if (event.keyCode === KeyCodes.Esc) { local.ipcRenderer.send(apiName.symphonyApi, { @@ -295,7 +468,7 @@ const throttledKeyDown = throttle( (event) => { }, 500); // Handle key up events -const throttledKeyUp = throttle( (event) => { +const throttledKeyUp = throttle((event) => { if (isAltKey && (event.keyCode === KeyCodes.Alt || KeyCodes.Esc)) { isMenuOpen = !isMenuOpen; } @@ -323,4 +496,4 @@ window.addEventListener('offline', updateOnlineStatus, false); window.addEventListener('online', updateOnlineStatus, false); window.addEventListener('keyup', throttledKeyUp, true); window.addEventListener('keydown', throttledKeyDown, true); -window.addEventListener('mousedown', throttleMouseDown, { capture: true }); \ No newline at end of file +window.addEventListener('mousedown', throttleMouseDown, { capture: true }); diff --git a/src/renderer/styles/notification-comp.less b/src/renderer/styles/notification-comp.less new file mode 100644 index 00000000..ac704127 --- /dev/null +++ b/src/renderer/styles/notification-comp.less @@ -0,0 +1,109 @@ +@font-family: "Segoe UI", "Helvetica Neue", "Verdana", "Arial", sans-serif; + +.light { + --text-color: #4a4a4a; + --logo-bg: url('../assets/symphony-logo-black.png'); +} +.dark { + --text-color: #ffffff; + --logo-bg: url('../assets/symphony-logo-white.png'); +} + +body { + margin: 0; + overflow: hidden; + -webkit-user-select: none; + font-family: @font-family; +} + +.container { + width: 380px; + height: 100px; + display: flex; + justify-content: center; + background-color: #ffffff; + overflow: hidden; + position: relative; + line-height: 15px; + box-sizing: border-box; + border-radius: 5px; +} + +.header { + width: 245px; + min-width: 230px; + margin: auto; + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.user-profile-pic-container { + align-items: center; + display: flex; +} + +.user-profile-pic { + height: 43px; + border-radius: 4px; + width: 43px; +} + +.close { + width: 16px; + height: 80px; + display: flex; + margin: auto; + opacity: 0.54; + font-size: 12px; + color: #CCC; + cursor: pointer; +} + +.title { + font-family: @font-family; + font-size: 14px; + font-weight: 700; + color: var(--text-color); + overflow: hidden; + display: -webkit-box; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; +} + +.company { + font-family: @font-family; + font-size: 11px; + overflow: hidden; + filter: brightness(70%); + display: -webkit-box; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; +} + +.message { + font-family: @font-family; + width: 100%; + overflow-wrap: break-word; + font-size: 12px; + margin-top: 5px; + overflow: hidden; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + cursor: default; + text-overflow: ellipsis; + color: var(--text-color); +} + +.logo-container { + display: flex; + align-items: center; +} + +.logo { + margin-left: 5px; + opacity: 0.6; + width: 43px; + content: var(--logo-bg); +} diff --git a/src/renderer/styles/screen-sharing-indicator.less b/src/renderer/styles/screen-sharing-indicator.less index d3fbb4b0..a132f97a 100644 --- a/src/renderer/styles/screen-sharing-indicator.less +++ b/src/renderer/styles/screen-sharing-indicator.less @@ -83,4 +83,3 @@ body { } } - diff --git a/tsconfig.json b/tsconfig.json index cf81168b..6981c740 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -30,6 +30,7 @@ "exclude": [ "node_modules", "lib", - "tests" + "tests", + "spec" ] -} \ No newline at end of file +} diff --git a/tslint.json b/tslint.json index 737df0b3..029d45f9 100644 --- a/tslint.json +++ b/tslint.json @@ -8,8 +8,8 @@ ] }, "rules": { - "curly": false, - "eofline": false, + "curly": true, + "eofline": true, "align": [ true, "parameters" @@ -73,4 +73,4 @@ ], "completed-docs": [true, "functions", "methods"] } -} \ No newline at end of file +}