From 9815796cca3d16dd911525ed405268092c755c9e Mon Sep 17 00:00:00 2001 From: Vishwas Shashidhar Date: Wed, 4 Nov 2020 16:56:10 +0530 Subject: [PATCH 1/7] SDA-2170: set context isolation to true by default (#1104) --- src/app/window-handler.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app/window-handler.ts b/src/app/window-handler.ts index 58ef42a5..8b577bd4 100644 --- a/src/app/window-handler.ts +++ b/src/app/window-handler.ts @@ -125,7 +125,10 @@ export class WindowHandler { const { disableThrottling } = config.getCloudConfigFields(['disableThrottling']) as any; this.windows = {}; - this.contextIsolation = this.globalConfig.contextIsolation || false; + this.contextIsolation = true; + if (this.globalConfig.contextIsolation !== undefined) { + this.contextIsolation = this.globalConfig.contextIsolation; + } this.backgroundThrottling = (customFlags.disableThrottling !== CloudConfigDataTypes.ENABLED || disableThrottling !== CloudConfigDataTypes.ENABLED); this.isCustomTitleBar = isWindowsOS && this.config.isCustomTitleBar === CloudConfigDataTypes.ENABLED; this.windowOpts = { From 46a253923b1f763443d9552a5a175734c6a5b1e9 Mon Sep 17 00:00:00 2001 From: Mattias Gustavsson Date: Wed, 4 Nov 2020 14:04:13 +0100 Subject: [PATCH 2/7] SDA-2593 Disabled cancel button in progress dialog --- installer/win/WixSharpInstaller/ProgressDialog.Designer.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/installer/win/WixSharpInstaller/ProgressDialog.Designer.cs b/installer/win/WixSharpInstaller/ProgressDialog.Designer.cs index c74fb379..0295f7c3 100644 --- a/installer/win/WixSharpInstaller/ProgressDialog.Designer.cs +++ b/installer/win/WixSharpInstaller/ProgressDialog.Designer.cs @@ -170,6 +170,7 @@ namespace Symphony this.cancel.Text = "[WixUICancel]"; this.cancel.UseVisualStyleBackColor = true; this.cancel.Click += new System.EventHandler(this.cancel_Click); + this.cancel.Enabled = false; // // bottomBorder // From 198d891c9407d0bd7793b991d6f1e294c5871223 Mon Sep 17 00:00:00 2001 From: Vishwas Shashidhar Date: Fri, 6 Nov 2020 12:13:29 +0530 Subject: [PATCH 3/7] feat: SDA-2456: add screen annotate tool (#1103) * SDA-2456: snipping tool component - Create a react component - Stylise the component - Add functionality to open a new window - Add event against the `Done` button and upload snippet * SDA-2456: snipping annotate tool - Added icons for undo, redo & clear - Made the snipping tool window & the image container dynamic * SDA-2456: make image dynamic * SDA-2456: add snapshot tests * SDA-2456: add the height & width properties to the image container * SDA-2456: update snipping tool snapshot * SDA-2456: add support for button selected border * SDA-2456: close existing annotate window * SDA-2456 Changed from class to functional component * SDA-2456: remove outdated tests * SDA-2456: fix image dimension issue * SDA-2456: address PR comments by Kiran Co-authored-by: psjostrom --- .vscode/launch.json | 19 +- .vscode/settings.json | 6 + package.json | 1 + spec/__snapshots__/snippingTool.spec.ts.snap | 65 + spec/snippingTool.spec.ts | 20 + src/app/screen-snippet-handler.ts | 441 ++- src/app/window-handler.ts | 3047 ++++++++++------- src/app/window-utils.ts | 14 +- src/locale/en-US.json | 3 +- src/locale/en.json | 3 +- src/locale/fr-FR.json | 4 +- src/locale/fr.json | 3 +- src/locale/ja-JP.json | 3 +- src/locale/ja.json | 3 +- src/renderer/assets/snip-draw.svg | 4 + src/renderer/assets/snip-erase.svg | 4 + src/renderer/assets/snip-highlight.svg | 4 + src/renderer/assets/snip-redo.png | Bin 0 -> 9234 bytes src/renderer/assets/snip-redo.svg | 4 + src/renderer/assets/snip-undo.png | Bin 0 -> 10084 bytes src/renderer/assets/snip-undo.svg | 4 + src/renderer/components/about-app.tsx | 4 +- src/renderer/components/basic-auth.tsx | 28 +- src/renderer/components/loading-screen.tsx | 8 +- src/renderer/components/notification-comp.tsx | 48 +- .../components/notification-settings.tsx | 4 +- src/renderer/components/screen-picker.tsx | 22 +- .../components/screen-sharing-indicator.tsx | 6 +- src/renderer/components/snipping-tool.tsx | 124 + src/renderer/preload-component.ts | 15 +- src/renderer/styles/snipping-tool.less | 182 + src/renderer/styles/theme.less | 11 +- tslint.json | 2 +- 33 files changed, 2545 insertions(+), 1561 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 spec/__snapshots__/snippingTool.spec.ts.snap create mode 100644 spec/snippingTool.spec.ts create mode 100644 src/renderer/assets/snip-draw.svg create mode 100644 src/renderer/assets/snip-erase.svg create mode 100644 src/renderer/assets/snip-highlight.svg create mode 100644 src/renderer/assets/snip-redo.png create mode 100644 src/renderer/assets/snip-redo.svg create mode 100644 src/renderer/assets/snip-undo.png create mode 100644 src/renderer/assets/snip-undo.svg create mode 100644 src/renderer/components/snipping-tool.tsx create mode 100644 src/renderer/styles/snipping-tool.less diff --git a/.vscode/launch.json b/.vscode/launch.json index 007f1cff..27de8dd5 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,7 +10,9 @@ "windows": { "runtimeExecutable": "${workspaceFolder}/node_modules/electron/dist/Electron.exe" }, - "args": ["."], + "args": [ + "." + ], "env": { "ELECTRON_DEBUGGING": "true", "ELECTRON_DEV": "true" @@ -30,7 +32,10 @@ "windows": { "runtimeExecutable": "${workspaceFolder}/node_modules/electron/dist/Electron.exe" }, - "args": [".", "--url=https://corporate.symphony.com"], + "args": [ + ".", + "--url=https://corporate.symphony.com" + ], "env": { "ELECTRON_DEBUGGING": "true", "ELECTRON_DEV": "true" @@ -50,7 +55,10 @@ "windows": { "runtimeExecutable": "${workspaceFolder}/node_modules/electron/dist/Electron.exe" }, - "args": [".", "--url=https://local-dev.symphony.com:9090"], + "args": [ + ".", + "--url=https://local-dev.symphony.com:9090" + ], "env": { "ELECTRON_DEBUGGING": "true", "ELECTRON_DEV": "true" @@ -70,7 +78,10 @@ "windows": { "runtimeExecutable": "${workspaceFolder}/node_modules/electron/dist/Electron.exe" }, - "args": [".", "--url=${workspaceFolder}/src/demo/index.html"], + "args": [ + ".", + "--url=file://${workspaceFolder}/src/demo/index.html" + ], "env": { "ELECTRON_DEBUGGING": "true", "ELECTRON_DEV": "true" diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..3539469d --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "npm.packageManager": "npm", + "typescript.tsdk": "./node_modules/typescript/lib", + "editor.formatOnSave": true, + "jestrunner.configPath": "jest-config.json", +} diff --git a/package.json b/package.json index f1f573eb..31508552 100644 --- a/package.json +++ b/package.json @@ -159,6 +159,7 @@ "electron-spellchecker": "git+https://github.com/symphonyoss/electron-spellchecker.git#v2.0.4", "ffi-napi": "3.0.0", "filesize": "6.1.0", + "image-size": "^0.9.3", "react": "16.13.0", "react-dom": "16.13.0", "ref-napi": "1.4.3", diff --git a/spec/__snapshots__/snippingTool.spec.ts.snap b/spec/__snapshots__/snippingTool.spec.ts.snap new file mode 100644 index 00000000..ec9ce78a --- /dev/null +++ b/spec/__snapshots__/snippingTool.spec.ts.snap @@ -0,0 +1,65 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Snipping Tool should render correctly 1`] = ` +
+
+
+ + + +
+
+ +
+
+
+ Screen snippet +
+
+ +
+
+`; diff --git a/spec/snippingTool.spec.ts b/spec/snippingTool.spec.ts new file mode 100644 index 00000000..43bf1cb4 --- /dev/null +++ b/spec/snippingTool.spec.ts @@ -0,0 +1,20 @@ +import { shallow } from 'enzyme'; +import * as React from 'react'; +import SnippingTool from '../src/renderer/components/snipping-tool'; +import { ipcRenderer } from './__mocks__/electron'; + +describe('Snipping Tool', () => { + const snippingToolLabel = 'snipping-tool-data'; + const onLabelEvent = 'on'; + + it('should render correctly', () => { + const wrapper = shallow(React.createElement(SnippingTool)); + expect(wrapper).toMatchSnapshot(); + }); + + it('should call `snipping-tool-data` event when component is mounted', () => { + const spy = jest.spyOn(ipcRenderer, onLabelEvent); + shallow(React.createElement(SnippingTool)); + expect(spy).toBeCalledWith(snippingToolLabel, expect.any(Function)); + }); +}); diff --git a/src/app/screen-snippet-handler.ts b/src/app/screen-snippet-handler.ts index 0c40a0fb..65df8f18 100644 --- a/src/app/screen-snippet-handler.ts +++ b/src/app/screen-snippet-handler.ts @@ -1,12 +1,19 @@ -import { app, BrowserWindow } from 'electron'; +import { app, BrowserWindow, ipcMain } from 'electron'; import * as fs from 'fs'; +import sizeOf from 'image-size'; import * as os from 'os'; import * as path from 'path'; import { ChildProcess, ExecException, execFile } from 'child_process'; import * as util from 'util'; import { IScreenSnippet } from '../common/api-interface'; -import { isDevEnv, isElectronQA, isLinux, isMac, isWindowsOS } from '../common/env'; +import { + isDevEnv, + isElectronQA, + isLinux, + isMac, + isWindowsOS, +} from '../common/env'; import { i18n } from '../common/i18n'; import { logger } from '../common/logger'; import { updateAlwaysOnTop } from './window-actions'; @@ -16,187 +23,279 @@ import { windowExists } from './window-utils'; const readFile = util.promisify(fs.readFile); class ScreenSnippet { - private readonly tempDir: string; - private readonly captureUtil: string; - private outputFileName: string | undefined; - private captureUtilArgs: ReadonlyArray | undefined; - private child: ChildProcess | undefined; - private focusedWindow: BrowserWindow | null = null; - private shouldUpdateAlwaysOnTop: boolean = false; + private readonly tempDir: string; + private readonly captureUtil: string; + private outputFileName: string | undefined; + private captureUtilArgs: ReadonlyArray | undefined; + private child: ChildProcess | undefined; + private focusedWindow: BrowserWindow | null = null; + private shouldUpdateAlwaysOnTop: boolean = false; - constructor() { - if (isElectronQA) { - this.tempDir = os.tmpdir(); - } else { - this.tempDir = path.join(app.getPath('userData'), 'temp'); - if (!fs.existsSync(this.tempDir)) { - fs.mkdirSync(this.tempDir); - } - } - this.captureUtil = isMac ? '/usr/sbin/screencapture' : isDevEnv - ? path.join(__dirname, - '../../../node_modules/screen-snippet/ScreenSnippet.exe') - : path.join(path.dirname(app.getPath('exe')), 'ScreenSnippet.exe'); - - if (isLinux) { - this.captureUtil = '/usr/bin/gnome-screenshot'; - } + constructor() { + if (isElectronQA) { + this.tempDir = os.tmpdir(); + } else { + this.tempDir = path.join(app.getPath('userData'), 'temp'); + if (!fs.existsSync(this.tempDir)) { + fs.mkdirSync(this.tempDir); + } } + this.captureUtil = isMac + ? '/usr/sbin/screencapture' + : isDevEnv + ? path.join( + __dirname, + '../../../node_modules/screen-snippet/ScreenSnippet.exe', + ) + : path.join(path.dirname(app.getPath('exe')), 'ScreenSnippet.exe'); - /** - * Captures a user selected portion of the monitor and returns jpeg image - * encoded in base64 format. - * - * @param webContents {Electron.webContents} - */ - public async capture(webContents: Electron.webContents) { - const mainWindow = windowHandler.getMainWindow(); - if (mainWindow && windowExists(mainWindow) && isWindowsOS) { - this.shouldUpdateAlwaysOnTop = mainWindow.isAlwaysOnTop(); - if (this.shouldUpdateAlwaysOnTop) { - await updateAlwaysOnTop(false, false, false); - } - } - logger.info(`screen-snippet-handler: Starting screen capture!`); - this.outputFileName = path.join(this.tempDir, 'symphonyImage-' + Date.now() + '.png'); - if (isMac) { - this.captureUtilArgs = [ '-i', '-s', '-t', 'png', this.outputFileName ]; - } else if (isWindowsOS) { - if (windowHandler.isMana) { - this.captureUtilArgs = [ '--no-annotate', this.outputFileName, i18n.getLocale() ]; - } else { - this.captureUtilArgs = [ this.outputFileName, i18n.getLocale() ]; - } - } else if (isLinux) { - this.captureUtilArgs = ['-a', '-f', this.outputFileName]; - } else { - this.captureUtilArgs = []; - } - this.focusedWindow = BrowserWindow.getFocusedWindow(); - - logger.info(`screen-snippet-handler: Capturing snippet with file ${this.outputFileName} and args ${this.captureUtilArgs}!`); - - // only allow one screen capture at a time. - if (this.child) { - logger.info(`screen-snippet-handler: Child screen capture exists, killing it and keeping only 1 instance!`); - this.killChildProcess(); - } - try { - await this.execCmd(this.captureUtil, this.captureUtilArgs); - const { message, data, type }: IScreenSnippet = await this.convertFileToData(); - logger.info(`screen-snippet-handler: Snippet captured! Sending data to SFE`); - webContents.send('screen-snippet-data', { message, data, type }); - await this.verifyAndUpdateAlwaysOnTop(); - } catch (error) { - await this.verifyAndUpdateAlwaysOnTop(); - logger.error(`screen-snippet-handler: screen capture failed with error: ${error}!`); - } + if (isLinux) { + this.captureUtil = '/usr/bin/gnome-screenshot'; } + } - /** - * Cancels a screen capture and closes the snippet window - */ - public async cancelCapture() { - if (!isWindowsOS) { - return; - } - logger.info(`screen-snippet-handler: Cancel screen capture!`); - this.focusedWindow = BrowserWindow.getFocusedWindow(); - - try { - await this.execCmd(this.captureUtil, []); - await this.verifyAndUpdateAlwaysOnTop(); - } catch (error) { - await this.verifyAndUpdateAlwaysOnTop(); - logger.error(`screen-snippet-handler: screen capture cancel failed with error: ${error}!`); - } + /** + * Captures a user selected portion of the monitor and returns jpeg image + * encoded in base64 format. + * + * @param webContents {Electron.webContents} + */ + public async capture(webContents: Electron.webContents) { + const mainWindow = windowHandler.getMainWindow(); + if (mainWindow && windowExists(mainWindow) && isWindowsOS) { + this.shouldUpdateAlwaysOnTop = mainWindow.isAlwaysOnTop(); + if (this.shouldUpdateAlwaysOnTop) { + await updateAlwaysOnTop(false, false, false); + } } - - /** - * Kills the child process when the application is reloaded - */ - public killChildProcess(): void { - if (this.child && typeof this.child.kill === 'function') { - this.child.kill(); - } + logger.info(`screen-snippet-handler: Starting screen capture!`); + this.outputFileName = path.join( + this.tempDir, + 'symphonyImage-' + Date.now() + '.png', + ); + if (isMac) { + this.captureUtilArgs = ['-i', '-s', '-t', 'png', this.outputFileName]; + } else if (isWindowsOS) { + if (windowHandler.isMana) { + this.captureUtilArgs = [ + '--no-annotate', + this.outputFileName, + i18n.getLocale(), + ]; + } else { + this.captureUtilArgs = [this.outputFileName, i18n.getLocale()]; + } + } else if (isLinux) { + this.captureUtilArgs = ['-a', '-f', this.outputFileName]; + } else { + this.captureUtilArgs = []; } + this.focusedWindow = BrowserWindow.getFocusedWindow(); - /** - * Executes the given command via a child process - * - * Windows: uses custom built windows screen capture tool - * Mac OSX: uses built-in screencapture tool which has been - * available since OSX ver 10.2. - * - * @param captureUtil {string} - * @param captureUtilArgs {captureUtilArgs} - * @example execCmd('-i -s', '/user/desktop/symphonyImage-1544025391698.png') - */ - private execCmd(captureUtil: string, captureUtilArgs: ReadonlyArray): Promise { - logger.info(`screen-snippet-handlers: execCmd ${captureUtil} ${captureUtilArgs}`); - return new Promise((resolve, reject) => { - return this.child = execFile(captureUtil, captureUtilArgs, (error: ExecException | null) => { - if (error && error.killed) { - // processs was killed, just resolve with no data. - return reject(error); - } - resolve(); - }); + logger.info( + `screen-snippet-handler: Capturing snippet with file ${this.outputFileName} and args ${this.captureUtilArgs}!`, + ); + + // only allow one screen capture at a time. + if (this.child) { + logger.info( + `screen-snippet-handler: Child screen capture exists, killing it and keeping only 1 instance!`, + ); + this.killChildProcess(); + } + try { + await this.execCmd(this.captureUtil, this.captureUtilArgs); + if (windowHandler.isMana) { + windowHandler.closeSnippingToolWindow(); + const dimensions = this.getImageSize(); + windowHandler.createSnippingToolWindow(this.outputFileName, dimensions); + this.uploadSnippet(webContents); + return; + } + const { + message, + data, + type, + }: IScreenSnippet = await this.convertFileToData(); + logger.info( + `screen-snippet-handler: Snippet captured! Sending data to SFE`, + ); + webContents.send('screen-snippet-data', { message, data, type }); + await this.verifyAndUpdateAlwaysOnTop(); + } catch (error) { + await this.verifyAndUpdateAlwaysOnTop(); + logger.error( + `screen-snippet-handler: screen capture failed with error: ${error}!`, + ); + } + } + + /** + * Cancels a screen capture and closes the snippet window + */ + public async cancelCapture() { + if (!isWindowsOS) { + return; + } + logger.info(`screen-snippet-handler: Cancel screen capture!`); + this.focusedWindow = BrowserWindow.getFocusedWindow(); + + try { + await this.execCmd(this.captureUtil, []); + await this.verifyAndUpdateAlwaysOnTop(); + } catch (error) { + await this.verifyAndUpdateAlwaysOnTop(); + logger.error( + `screen-snippet-handler: screen capture cancel failed with error: ${error}!`, + ); + } + } + + /** + * Kills the child process when the application is reloaded + */ + public killChildProcess(): void { + if (this.child && typeof this.child.kill === 'function') { + this.child.kill(); + } + } + + /** + * Executes the given command via a child process + * + * Windows: uses custom built windows screen capture tool + * Mac OSX: uses built-in screencapture tool which has been + * available since OSX ver 10.2. + * + * @param captureUtil {string} + * @param captureUtilArgs {captureUtilArgs} + * @example execCmd('-i -s', '/user/desktop/symphonyImage-1544025391698.png') + */ + private execCmd( + captureUtil: string, + captureUtilArgs: ReadonlyArray, + ): Promise { + logger.info( + `screen-snippet-handlers: execCmd ${captureUtil} ${captureUtilArgs}`, + ); + return new Promise((resolve, reject) => { + return (this.child = execFile( + captureUtil, + captureUtilArgs, + (error: ExecException | null) => { + if (error && error.killed) { + // processs was killed, just resolve with no data. + return reject(error); + } + resolve(); + }, + )); + }); + } + + /** + * Converts the temporary stored file into base64 + * and removes the temp file + * + * @return Promise { message, data, type } + */ + private async convertFileToData(): Promise { + try { + if (!this.outputFileName) { + logger.info( + `screen-snippet-handler: screen capture failed! output file doesn't exist!`, + ); + return { message: 'output file name is required', type: 'ERROR' }; + } + const data = await readFile(this.outputFileName); + if (!data) { + logger.info( + `screen-snippet-handler: screen capture failed! data doesn't exist!`, + ); + return { message: `no file data provided`, type: 'ERROR' }; + } + // convert binary data to base64 encoded string + const output = Buffer.from(data).toString('base64'); + return { message: 'success', data: output, type: 'image/png;base64' }; + } catch (error) { + // no such file exists or user likely aborted + // creating snippet. also include any error when + // creating child process. + return error && error.code === 'ENOENT' + ? { message: `file does not exist`, type: 'ERROR' } + : { message: `${error}`, type: 'ERROR' }; + } finally { + if (this.focusedWindow && windowExists(this.focusedWindow)) { + this.focusedWindow.moveTop(); + } + // remove tmp file (async) + if (this.outputFileName) { + fs.unlink(this.outputFileName, (removeErr) => { + logger.info( + `screen-snippet-handler: cleaning up temp snippet file: ${this.outputFileName}!`, + ); + if (removeErr) { + logger.error( + `screen-snippet-handler: error removing temp snippet file: ${this.outputFileName}, err: ${removeErr}`, + ); + } }); + } + } + } + + /** + * Verify and updates always on top + */ + private async verifyAndUpdateAlwaysOnTop(): Promise { + if (this.shouldUpdateAlwaysOnTop) { + await updateAlwaysOnTop(true, false, false); + this.shouldUpdateAlwaysOnTop = false; + } + } + + /** + * Gets the height & width of an image + */ + private getImageSize(): + | { + height: number | undefined; + width: number | undefined; + } + | undefined { + if (!this.outputFileName) { + return undefined; } - /** - * Converts the temporary stored file into base64 - * and removes the temp file - * - * @return Promise { message, data, type } - */ - private async convertFileToData(): Promise { - try { - if (!this.outputFileName) { - logger.info(`screen-snippet-handler: screen capture failed! output file doesn't exist!`); - return { message: 'output file name is required', type: 'ERROR' }; - } - const data = await readFile(this.outputFileName); - if (!data) { - logger.info(`screen-snippet-handler: screen capture failed! data doesn't exist!`); - return { message: `no file data provided`, type: 'ERROR' }; - } - // convert binary data to base64 encoded string - const output = Buffer.from(data).toString('base64'); - return { message: 'success', data: output, type: 'image/png;base64' }; - } catch (error) { - // no such file exists or user likely aborted - // creating snippet. also include any error when - // creating child process. - return error && error.code === 'ENOENT' - ? { message: `file does not exist`, type: 'ERROR' } - : { message: `${error}`, type: 'ERROR' }; - } finally { - if (this.focusedWindow && windowExists(this.focusedWindow)) { - this.focusedWindow.moveTop(); - } - // remove tmp file (async) - if (this.outputFileName) { - fs.unlink(this.outputFileName, (removeErr) => { - logger.info(`screen-snippet-handler: cleaning up temp snippet file: ${this.outputFileName}!`); - if (removeErr) { - logger.error(`screen-snippet-handler: error removing temp snippet file: ${this.outputFileName}, err: ${removeErr}`); - } - }); - } - } - } + const dimensions = sizeOf(this.outputFileName); + return { + height: dimensions.height, + width: dimensions.width, + }; + } - /** - * Verify and updates always on top - */ - private async verifyAndUpdateAlwaysOnTop(): Promise { - if (this.shouldUpdateAlwaysOnTop) { - await updateAlwaysOnTop(true, false, false); - this.shouldUpdateAlwaysOnTop = false; - } - } + /** + * Uploads a screen snippet + * @param webContents A browser window's web contents object + */ + private uploadSnippet(webContents: Electron.webContents) { + ipcMain.once('upload-snippet', async (_event, snipImage: string) => { + windowHandler.closeSnippingToolWindow(); + if (snipImage) { + this.outputFileName = snipImage; + } + const { + message, + data, + type, + }: IScreenSnippet = await this.convertFileToData(); + logger.info( + `screen-snippet-handler: Snippet captured! Sending data to SFE`, + ); + webContents.send('screen-snippet-data', { message, data, type }); + await this.verifyAndUpdateAlwaysOnTop(); + }); + } } const screenSnippet = new ScreenSnippet(); diff --git a/src/app/window-handler.ts b/src/app/window-handler.ts index 8b577bd4..77c6e762 100644 --- a/src/app/window-handler.ts +++ b/src/app/window-handler.ts @@ -1,20 +1,26 @@ import { ChildProcess, ExecException, execFile } from 'child_process'; import * as electron from 'electron'; import { - app, - BrowserWindow, - BrowserWindowConstructorOptions, - crashReporter, - DesktopCapturerSource, - globalShortcut, - ipcMain, + app, + BrowserWindow, + BrowserWindowConstructorOptions, + crashReporter, + DesktopCapturerSource, + globalShortcut, + ipcMain, } from 'electron'; import * as fs from 'fs'; import * as path from 'path'; import { format, parse } from 'url'; import { apiName, WindowTypes } from '../common/api-interface'; -import { isDevEnv, isLinux, isMac, isNodeEnv, isWindowsOS } from '../common/env'; +import { + isDevEnv, + isLinux, + isMac, + isNodeEnv, + isWindowsOS, +} from '../common/env'; import { i18n, LocaleType } from '../common/i18n'; import { logger } from '../common/logger'; import { getCommandLineArgs, getGuid } from '../common/utils'; @@ -22,41 +28,56 @@ import { notification } from '../renderer/notification'; import { cleanAppCacheOnCrash } from './app-cache-handler'; import { AppMenu } from './app-menu'; import { handleChildWindow } from './child-window-handler'; -import { CloudConfigDataTypes, config, IConfig, IGlobalConfig } from './config-handler'; +import { + CloudConfigDataTypes, + config, + IConfig, + IGlobalConfig, +} from './config-handler'; import { SpellChecker } from './spell-check-handler'; import { checkIfBuildExpired } from './ttl-handler'; import { versionHandler } from './version-handler'; -import { handlePermissionRequests, monitorWindowActions, onConsoleMessages } from './window-actions'; import { - createComponentWindow, - didVerifyAndRestoreWindow, - getBounds, getWindowByName, - handleCertificateProxyVerification, - handleDownloadManager, - injectStyles, - isSymphonyReachable, - monitorNetworkInterception, - preventWindowNavigation, - reloadWindow, - windowExists, + handlePermissionRequests, + monitorWindowActions, + onConsoleMessages, +} from './window-actions'; +import { + createComponentWindow, + didVerifyAndRestoreWindow, + getBounds, + getWindowByName, + handleCertificateProxyVerification, + handleDownloadManager, + injectStyles, + isSymphonyReachable, + monitorNetworkInterception, + preventWindowNavigation, + reloadWindow, + windowExists, } from './window-utils'; -const windowSize: string | null = getCommandLineArgs(process.argv, '--window-size', false); +const windowSize: string | null = getCommandLineArgs( + process.argv, + '--window-size', + false, +); enum ClientSwitchType { - CLIENT_1_5 = 'CLIENT_1_5', - CLIENT_2_0 = 'CLIENT_2_0', - CLIENT_2_0_DAILY = 'CLIENT_2_0_DAILY', + CLIENT_1_5 = 'CLIENT_1_5', + CLIENT_2_0 = 'CLIENT_2_0', + CLIENT_2_0_DAILY = 'CLIENT_2_0_DAILY', } -interface ICustomBrowserWindowConstructorOpts extends Electron.BrowserWindowConstructorOptions { - winKey: string; +interface ICustomBrowserWindowConstructorOpts + extends Electron.BrowserWindowConstructorOptions { + winKey: string; } export interface ICustomBrowserWindow extends Electron.BrowserWindow { - winName: string; - notificationData?: object; - origin?: string; + winName: string; + notificationData?: object; + origin?: string; } // Default window width & height @@ -64,1320 +85,1742 @@ let DEFAULT_WIDTH: number = 900; let DEFAULT_HEIGHT: number = 900; export class WindowHandler { + /** + * Verifies if the url is valid and + * forcefully appends https if not present + * + * @param configURL {string} + */ + private static getValidUrl(configURL: string): string { + const parsedUrl = parse(configURL); - /** - * Verifies if the url is valid and - * forcefully appends https if not present - * - * @param configURL {string} - */ - private static getValidUrl(configURL: string): string { - const parsedUrl = parse(configURL); + if (!parsedUrl.protocol || parsedUrl.protocol !== 'https') { + parsedUrl.protocol = 'https:'; + parsedUrl.slashes = true; + } + return format(parsedUrl); + } - if (!parsedUrl.protocol || parsedUrl.protocol !== 'https') { - parsedUrl.protocol = 'https:'; - parsedUrl.slashes = true; - } - return format(parsedUrl); + public appMenu: AppMenu | null; + public isAutoReload: boolean; + public isOnline: boolean; + public url: string | undefined; + public startUrl!: string; + public isMana: boolean = false; + public willQuitApp: boolean = false; + public spellchecker: SpellChecker | undefined; + public isCustomTitleBar: boolean; + public isWebPageLoading: boolean = true; + public screenShareIndicatorFrameUtil: string; + public shouldShowWelcomeScreen: boolean = false; + + private readonly defaultPodUrl: string = 'https://[POD].symphony.com'; + private readonly contextIsolation: boolean; + private readonly backgroundThrottling: boolean; + private readonly windowOpts: ICustomBrowserWindowConstructorOpts; + private readonly globalConfig: IGlobalConfig; + private readonly userConfig: IConfig; + private readonly config: IConfig; + // Window reference + private readonly windows: object; + + private loadFailError: string | undefined; + private mainWindow: ICustomBrowserWindow | null = null; + private aboutAppWindow: Electron.BrowserWindow | null = null; + private screenPickerWindow: Electron.BrowserWindow | null = null; + private screenSharingIndicatorWindow: Electron.BrowserWindow | null = null; + private screenSharingFrameWindow: Electron.BrowserWindow | null = null; + private basicAuthWindow: Electron.BrowserWindow | null = null; + private notificationSettingsWindow: Electron.BrowserWindow | null = null; + private snippingToolWindow: Electron.BrowserWindow | null = null; + + constructor(opts?: Electron.BrowserViewConstructorOptions) { + // Use these variables only on initial setup + this.config = config.getConfigFields([ + 'isCustomTitleBar', + 'mainWinPos', + 'minimizeOnClose', + 'notificationSettings', + 'alwaysOnTop', + 'locale', + 'customFlags', + 'clientSwitch', + 'enableRendererLogs', + ]); + logger.info( + `window-handler: main windows initialized with following config data`, + this.config, + ); + + this.globalConfig = config.getGlobalConfigFields([ + 'url', + 'contextIsolation', + 'contextOriginUrl', + ]); + this.userConfig = config.getUserConfigFields(['url']); + + const { customFlags } = this.config; + const { disableThrottling } = config.getCloudConfigFields([ + 'disableThrottling', + ]) as any; + + this.windows = {}; + this.contextIsolation = true; + if (this.globalConfig.contextIsolation !== undefined) { + this.contextIsolation = this.globalConfig.contextIsolation; + } + this.backgroundThrottling = + customFlags.disableThrottling !== CloudConfigDataTypes.ENABLED || + disableThrottling !== CloudConfigDataTypes.ENABLED; + this.isCustomTitleBar = + isWindowsOS && + this.config.isCustomTitleBar === CloudConfigDataTypes.ENABLED; + this.windowOpts = { + ...this.getWindowOpts( + { + alwaysOnTop: + this.config.alwaysOnTop === CloudConfigDataTypes.ENABLED || false, + frame: !this.isCustomTitleBar, + minHeight: 300, + minWidth: 300, + title: 'Symphony', + }, + { + preload: path.join(__dirname, '../renderer/_preload-main.js'), + }, + ), + ...opts, + }; + this.isAutoReload = false; + this.isOnline = true; + + this.screenShareIndicatorFrameUtil = ''; + if (isWindowsOS) { + this.screenShareIndicatorFrameUtil = isDevEnv + ? path.join( + __dirname, + '../../../node_modules/screen-share-indicator-frame/ScreenShareIndicatorFrame.exe', + ) + : path.join( + path.dirname(app.getPath('exe')), + 'ScreenShareIndicatorFrame.exe', + ); + } else if (isMac) { + this.screenShareIndicatorFrameUtil = isDevEnv + ? path.join( + __dirname, + '../../../node_modules/screen-share-indicator-frame/SymphonyScreenShareIndicator', + ) + : path.join( + path.dirname(app.getPath('exe')), + '../node_modules/screen-share-indicator-frame/SymphonyScreenShareIndicator', + ); } - public appMenu: AppMenu | null; - public isAutoReload: boolean; - public isOnline: boolean; - public url: string | undefined; - public startUrl!: string; - public isMana: boolean = false; - public willQuitApp: boolean = false; - public spellchecker: SpellChecker | undefined; - public isCustomTitleBar: boolean; - public isWebPageLoading: boolean = true; - public screenShareIndicatorFrameUtil: string; - public shouldShowWelcomeScreen: boolean = false; + this.appMenu = null; + const locale: LocaleType = (this.config.locale || + app.getLocale()) as LocaleType; + i18n.setLocale(locale); - private readonly defaultPodUrl: string = 'https://[POD].symphony.com'; - private readonly contextIsolation: boolean; - private readonly backgroundThrottling: boolean; - private readonly windowOpts: ICustomBrowserWindowConstructorOpts; - private readonly globalConfig: IGlobalConfig; - private readonly userConfig: IConfig; - private readonly config: IConfig; - // Window reference - private readonly windows: object; + try { + const extra = { + podUrl: this.userConfig.url + ? this.userConfig.url + : this.globalConfig.url, + process: 'main', + }; + const defaultOpts = { + uploadToServer: false, + companyName: 'Symphony', + submitURL: '', + }; + crashReporter.start({ ...defaultOpts, extra }); + } catch (e) { + throw new Error('failed to init crash report'); + } + } - private loadFailError: string | undefined; - private mainWindow: ICustomBrowserWindow | null = null; - private aboutAppWindow: Electron.BrowserWindow | null = null; - private screenPickerWindow: Electron.BrowserWindow | null = null; - private screenSharingIndicatorWindow: Electron.BrowserWindow | null = null; - private screenSharingFrameWindow: Electron.BrowserWindow | null = null; - private basicAuthWindow: Electron.BrowserWindow | null = null; - private notificationSettingsWindow: Electron.BrowserWindow | null = null; + /** + * Starting point of the app + */ + public async createApplication() { + this.spellchecker = new SpellChecker(); + logger.info( + `window-handler: initialized spellchecker module with locale ${this.spellchecker.locale}`, + ); - constructor(opts?: Electron.BrowserViewConstructorOptions) { - // Use these variables only on initial setup - this.config = config.getConfigFields(['isCustomTitleBar', 'mainWinPos', 'minimizeOnClose', 'notificationSettings', 'alwaysOnTop', 'locale', 'customFlags', 'clientSwitch', 'enableRendererLogs']); - logger.info(`window-handler: main windows initialized with following config data`, this.config); + logger.info( + 'window-handler: createApplication mainWinPos: ' + + JSON.stringify(this.config.mainWinPos), + ); - this.globalConfig = config.getGlobalConfigFields(['url', 'contextIsolation', 'contextOriginUrl']); - this.userConfig = config.getUserConfigFields(['url']); + let { isFullScreen, isMaximized } = this.config.mainWinPos + ? this.config.mainWinPos + : { isFullScreen: false, isMaximized: false }; - const { customFlags } = this.config; - const { disableThrottling } = config.getCloudConfigFields(['disableThrottling']) as any; + this.url = WindowHandler.getValidUrl( + this.userConfig.url ? this.userConfig.url : this.globalConfig.url, + ); + logger.info(`window-handler: setting url ${this.url} from config file!`); - this.windows = {}; - this.contextIsolation = true; - if (this.globalConfig.contextIsolation !== undefined) { - this.contextIsolation = this.globalConfig.contextIsolation; - } - this.backgroundThrottling = (customFlags.disableThrottling !== CloudConfigDataTypes.ENABLED || disableThrottling !== CloudConfigDataTypes.ENABLED); - this.isCustomTitleBar = isWindowsOS && this.config.isCustomTitleBar === CloudConfigDataTypes.ENABLED; - this.windowOpts = { - ...this.getWindowOpts({ - alwaysOnTop: this.config.alwaysOnTop === CloudConfigDataTypes.ENABLED || false, - frame: !this.isCustomTitleBar, - minHeight: 300, - minWidth: 300, - title: 'Symphony', - }, { - preload: path.join(__dirname, '../renderer/_preload-main.js'), - }), ...opts, - }; - this.isAutoReload = false; - this.isOnline = true; + if ( + config.isFirstTimeLaunch() && + this.globalConfig.url.indexOf('https://my.symphony.com') >= 0 + ) { + this.shouldShowWelcomeScreen = true; + this.url = this.defaultPodUrl; + isMaximized = false; + isFullScreen = false; + DEFAULT_HEIGHT = 333; + DEFAULT_WIDTH = 542; + this.windowOpts.resizable = false; + this.windowOpts.maximizable = false; + this.windowOpts.fullscreenable = false; - this.screenShareIndicatorFrameUtil = ''; - if (isWindowsOS) { - this.screenShareIndicatorFrameUtil = isDevEnv - ? path.join(__dirname, - '../../../node_modules/screen-share-indicator-frame/ScreenShareIndicatorFrame.exe') - : path.join(path.dirname(app.getPath('exe')), 'ScreenShareIndicatorFrame.exe'); - } else if (isMac) { - this.screenShareIndicatorFrameUtil = isDevEnv - ? path.join(__dirname, - '../../../node_modules/screen-share-indicator-frame/SymphonyScreenShareIndicator') - : path.join(path.dirname(app.getPath('exe')), '../node_modules/screen-share-indicator-frame/SymphonyScreenShareIndicator'); - } + if (this.config.mainWinPos && this.config.mainWinPos.height) { + this.config.mainWinPos.height = DEFAULT_HEIGHT; + } - this.appMenu = null; - const locale: LocaleType = (this.config.locale || app.getLocale()) as LocaleType; - i18n.setLocale(locale); + if (this.config.mainWinPos && this.config.mainWinPos.width) { + this.config.mainWinPos.width = DEFAULT_WIDTH; + } - try { - const extra = { podUrl: this.userConfig.url ? this.userConfig.url : this.globalConfig.url, process: 'main' }; - const defaultOpts = { uploadToServer: false, companyName: 'Symphony', submitURL: '' }; - crashReporter.start({ ...defaultOpts, extra }); - } catch (e) { - throw new Error('failed to init crash report'); - } + if (this.config.mainWinPos && this.config.mainWinPos.x) { + this.config.mainWinPos.x = undefined; + } + if (this.config.mainWinPos && this.config.mainWinPos.y) { + this.config.mainWinPos.y = undefined; + } } - /** - * Starting point of the app - */ - public async createApplication() { - - this.spellchecker = new SpellChecker(); - logger.info(`window-handler: initialized spellchecker module with locale ${this.spellchecker.locale}`); - - logger.info('window-handler: createApplication mainWinPos: ' + JSON.stringify(this.config.mainWinPos)); - - let { isFullScreen, isMaximized } = this.config.mainWinPos ? this.config.mainWinPos : { isFullScreen: false, isMaximized: false }; - - this.url = WindowHandler.getValidUrl(this.userConfig.url ? this.userConfig.url : this.globalConfig.url); - logger.info(`window-handler: setting url ${this.url} from config file!`); - - if (config.isFirstTimeLaunch() && this.globalConfig.url.indexOf('https://my.symphony.com') >= 0) { - this.shouldShowWelcomeScreen = true; - this.url = this.defaultPodUrl; - isMaximized = false; - isFullScreen = false; - DEFAULT_HEIGHT = 333; - DEFAULT_WIDTH = 542; - this.windowOpts.resizable = false; - this.windowOpts.maximizable = false; - this.windowOpts.fullscreenable = false; - - if (this.config.mainWinPos && this.config.mainWinPos.height) { - this.config.mainWinPos.height = DEFAULT_HEIGHT; - } - - if (this.config.mainWinPos && this.config.mainWinPos.width) { - this.config.mainWinPos.width = DEFAULT_WIDTH; - } - - if (this.config.mainWinPos && this.config.mainWinPos.x) { - this.config.mainWinPos.x = undefined; - } - - if (this.config.mainWinPos && this.config.mainWinPos.y) { - this.config.mainWinPos.y = undefined; - } - } - - logger.info('window-handler: windowSize: ' + JSON.stringify(windowSize)); - if (windowSize) { - const args = windowSize.split('='); - const sizes = args[1].split(','); - logger.info('window-handler: windowSize: args: ' + JSON.stringify(args)); - logger.info('window-handler: windowSize: sizes: ' + JSON.stringify(sizes)); - DEFAULT_WIDTH = Number(sizes[0]); - DEFAULT_HEIGHT = Number(sizes[1]); - if (this.config.mainWinPos) { - this.config.mainWinPos.width = DEFAULT_WIDTH; - this.config.mainWinPos.height = DEFAULT_HEIGHT; - } - } - - // set window opts with additional config - this.mainWindow = new BrowserWindow({ - ...this.windowOpts, ...getBounds(this.config.mainWinPos, DEFAULT_WIDTH, DEFAULT_HEIGHT), - }) as ICustomBrowserWindow; - - if (isWindowsOS) { - // SDA-1720 when Symphony on secondary screen is wider than main screen, the window on secondary is clapmed to the width of main screen - // Only happens on windows (BrowserWindow) - this.mainWindow.setBounds(getBounds(this.config.mainWinPos, DEFAULT_WIDTH, DEFAULT_HEIGHT) as Electron.Rectangle); - } - - logger.info('window-handler: this.mainWindow.getBounds: ' + JSON.stringify(this.mainWindow.getBounds())); - - this.mainWindow.winName = apiName.mainWindowName; - // Get url to load from cmd line or from global config file - const urlFromCmd = getCommandLineArgs(process.argv, '--url=', false); - - if (urlFromCmd) { - const commandLineUrl = urlFromCmd.substr(6); - logger.info(`window-handler: trying to set url ${commandLineUrl} from command line.`); - const { podWhitelist } = config.getConfigFields(['podWhitelist']); - logger.info(`window-handler: checking pod whitelist.`); - if (podWhitelist.length > 0) { - logger.info(`window-handler: pod whitelist is not empty ${podWhitelist}`); - if (podWhitelist.includes(commandLineUrl)) { - logger.info(`window-handler: url from command line is whitelisted in the config file.`); - logger.info(`window-handler: setting ${commandLineUrl} from the command line as the main window url.`); - this.url = commandLineUrl; - this.shouldShowWelcomeScreen = false; - isMaximized = true; - isFullScreen = false; - this.mainWindow.resizable = true; - this.mainWindow.maximizable = true; - this.mainWindow.fullScreenable = true; - } else { - logger.info(`window-handler: url ${commandLineUrl} from command line is NOT WHITELISTED in the config file.`); - } - } else { - logger.info(`window-handler: setting ${commandLineUrl} from the command line as the main window url since pod whitelist is empty.`); - this.url = commandLineUrl; - this.shouldShowWelcomeScreen = false; - isMaximized = true; - isFullScreen = false; - this.mainWindow.resizable = true; - this.mainWindow.maximizable = true; - this.mainWindow.fullScreenable = true; - } - } - - if (isMaximized) { - this.mainWindow.maximize(); - logger.info(`window-handler: window is maximized!`); - } - - if (isFullScreen) { - logger.info(`window-handler: window is in full screen!`); - this.mainWindow.setFullScreen(true); - } - - this.startUrl = this.url; - if (this.shouldShowWelcomeScreen) { - this.handleWelcomeScreen(); - } - - cleanAppCacheOnCrash(this.mainWindow); - // loads the main window with url from config/cmd line - this.mainWindow.loadURL(this.url); - // check for build expiry in case of test builds - this.checkExpiry(this.mainWindow); - // update version info from server - this.updateVersionInfo(); - // need this for postMessage origin - this.mainWindow.origin = this.globalConfig.contextOriginUrl || this.url; - - // Event needed to hide native menu bar on Windows 10 as we use custom menu bar - this.mainWindow.webContents.once('did-start-loading', () => { - logger.info(`window-handler: main window web contents started loading!`); - if ((this.config.isCustomTitleBar === CloudConfigDataTypes.ENABLED && isWindowsOS) && this.mainWindow && windowExists(this.mainWindow)) { - this.mainWindow.setMenuBarVisibility(false); - } - // monitors network connection and - // displays error banner on failure - monitorNetworkInterception(this.url || this.userConfig.url || this.globalConfig.url); - }); - - this.mainWindow.webContents.on('did-finish-load', async () => { - logger.info(`window-handler: main window web contents finished loading!`); - // early exit if the window has already been destroyed - if (!this.mainWindow || !windowExists(this.mainWindow)) { - logger.info(`window-handler: main window web contents destroyed already! exiting`); - return; - } - this.url = this.mainWindow.webContents.getURL(); - logger.info('window-handler: did-finish-load, url: ' + this.url); - const manaPath = 'client-bff'; - if (this.url.includes(manaPath)) { - this.isMana = true; - } else { - this.isMana = false; - } - logger.info('window-handler: isMana: ' + this.isMana); - - // Injects custom title bar and snack bar css into the webContents - await injectStyles(this.mainWindow, this.isCustomTitleBar); - - this.mainWindow.webContents.send('page-load', { - isWindowsOS, - locale: i18n.getLocale(), - resources: i18n.loadedResources, - enableCustomTitleBar: this.isCustomTitleBar, - isMainWindow: true, - }); - this.appMenu = new AppMenu(); - const { permissions } = config.getConfigFields(['permissions']); - this.mainWindow.webContents.send('is-screen-share-enabled', permissions.media); - }); - - this.mainWindow.webContents.on('did-fail-load', (_event, errorCode, errorDesc, validatedURL) => { - logger.error(`window-handler: Failed to load ${validatedURL}, with an error: ${errorCode}::${errorDesc}`); - this.loadFailError = errorDesc; - }); - - this.mainWindow.webContents.on('did-stop-loading', async () => { - if (this.mainWindow && windowExists(this.mainWindow)) { - this.mainWindow.webContents.send('page-load-failed', { - locale: i18n.getLocale(), - resources: i18n.loadedResources, - }); - const href = await this.mainWindow.webContents.executeJavaScript('document.location.href'); - try { - if (href === 'data:text/html,chromewebdata' || href === 'chrome-error://chromewebdata/') { - if (this.mainWindow && windowExists(this.mainWindow)) { - this.mainWindow.webContents.insertCSS(fs.readFileSync(path.join(__dirname, '..', '/renderer/styles/network-error.css'), 'utf8').toString()); - this.mainWindow.webContents.send('network-error', { error: this.loadFailError }); - isSymphonyReachable(this.mainWindow, this.url || this.userConfig.url || this.globalConfig.url); - } - } - } catch (error) { - logger.error(`window-handler: Could not read document.location`, error); - } - } - }); - - this.mainWindow.webContents.on('crashed', async (_event: Event, killed: boolean) => { - if (killed) { - logger.info(`window-handler: main window crashed (killed)!`); - return; - } - logger.info(`window-handler: main window crashed!`); - const { response } = await electron.dialog.showMessageBox({ - type: 'error', - title: i18n.t('Renderer Process Crashed')(), - message: i18n.t('Oops! Looks like we have had a crash. Please reload or close this window.')(), - buttons: ['Reload', 'Close'], - }); - if (!this.mainWindow || !windowExists(this.mainWindow)) { - return; - } - response === 0 ? this.mainWindow.reload() : this.mainWindow.close(); - }); - - // Handle main window close - this.mainWindow.on('close', (event) => { - if (!this.mainWindow || !windowExists(this.mainWindow)) { - return; - } - - if (this.willQuitApp) { - logger.info(`window-handler: app is quitting, destroying all windows!`); - if (this.mainWindow && this.mainWindow.webContents.isDevToolsOpened()) { - this.mainWindow.webContents.closeDevTools(); - } - return this.destroyAllWindows(); - } - - const { minimizeOnClose } = config.getConfigFields(['minimizeOnClose']); - if (minimizeOnClose === CloudConfigDataTypes.ENABLED) { - event.preventDefault(); - this.mainWindow.minimize(); - return; - } - - if (isMac) { - event.preventDefault(); - this.mainWindow.hide(); - return; - } - - app.quit(); - }); - - this.mainWindow.once('closed', () => { - logger.info(`window-handler: main window closed, destroying all windows!`); - if (isWindowsOS || isMac) { - this.execCmd(this.screenShareIndicatorFrameUtil, []); - } - this.closeAllWindow(); - this.destroyAllWindows(); - }); - - // Reloads the Symphony - ipcMain.on('reload-symphony', () => { - if (this.mainWindow && windowExists(this.mainWindow)) { - this.mainWindow.loadURL(this.url || this.globalConfig.url); - } - }); - - // Certificate verification proxy - if (!isDevEnv) { - this.mainWindow.webContents.session.setCertificateVerifyProc(handleCertificateProxyVerification); - } - - // Register global shortcuts - this.registerGlobalShortcuts(); - - // Validate window navigation - preventWindowNavigation(this.mainWindow, false); - - // Handle media/other permissions - handlePermissionRequests(this.mainWindow.webContents); - - // Start monitoring window actions - monitorWindowActions(this.mainWindow); - - // Download manager - this.mainWindow.webContents.session.on('will-download', handleDownloadManager); - - // store window ref - this.addWindow(this.windowOpts.winKey, this.mainWindow); - - // Handle pop-outs window - handleChildWindow(this.mainWindow.webContents); - - if (this.config.enableRendererLogs) { - this.mainWindow.webContents.on('console-message', onConsoleMessages); - } - - return this.mainWindow; + logger.info('window-handler: windowSize: ' + JSON.stringify(windowSize)); + if (windowSize) { + const args = windowSize.split('='); + const sizes = args[1].split(','); + logger.info('window-handler: windowSize: args: ' + JSON.stringify(args)); + logger.info( + 'window-handler: windowSize: sizes: ' + JSON.stringify(sizes), + ); + DEFAULT_WIDTH = Number(sizes[0]); + DEFAULT_HEIGHT = Number(sizes[1]); + if (this.config.mainWinPos) { + this.config.mainWinPos.width = DEFAULT_WIDTH; + this.config.mainWinPos.height = DEFAULT_HEIGHT; + } } - /** - * Handles the use case of showing - * welcome screen for first time installs - */ - public handleWelcomeScreen() { + // set window opts with additional config + this.mainWindow = new BrowserWindow({ + ...this.windowOpts, + ...getBounds(this.config.mainWinPos, DEFAULT_WIDTH, DEFAULT_HEIGHT), + }) as ICustomBrowserWindow; - if (!this.url || !this.mainWindow) { - return; - } - - if (this.url.startsWith(this.defaultPodUrl)) { - this.url = format({ - pathname: require.resolve('../renderer/react-window.html'), - protocol: 'file', - query: { - componentName: 'welcome', - locale: i18n.getLocale(), - }, - slashes: true, - }); - } - - this.mainWindow.webContents.on('did-finish-load', () => { - if (!this.url || !this.mainWindow) { - return; - } - if (this.url.indexOf('welcome')) { - this.mainWindow.webContents.send('page-load-welcome', { - locale: i18n.getLocale(), - resource: i18n.loadedResources, - }); - const userConfigUrl = this.userConfig.url - && this.userConfig.url.indexOf('/login/sso/initsso') > -1 ? - this.userConfig.url.slice(0, this.userConfig.url.indexOf('/login/sso/initsso')) - : this.userConfig.url; - - this.mainWindow.webContents.send('welcome', { - url: userConfigUrl || this.startUrl, - message: '', - urlValid: !!userConfigUrl, - sso: false, - }); - } - }); - - ipcMain.on('set-pod-url', async (_event, newPodUrl: string) => { - await config.updateUserConfig({ url: newPodUrl }); - app.relaunch(); - app.exit(); - }); + if (isWindowsOS) { + // SDA-1720 when Symphony on secondary screen is wider than main screen, the window on secondary is clapmed to the width of main screen + // Only happens on windows (BrowserWindow) + this.mainWindow.setBounds( + getBounds( + this.config.mainWinPos, + DEFAULT_WIDTH, + DEFAULT_HEIGHT, + ) as Electron.Rectangle, + ); } - /** - * Gets the main window - */ - public getMainWindow(): ICustomBrowserWindow | null { - return this.mainWindow; - } - - /** - * Gets all the window that we have created - * - * @return {Electron.BrowserWindow} - * - */ - public getAllWindows(): object { - return this.windows; - } - - /** - * Closes the window from an event emitted by the render processes - * - * @param windowType {WindowTypes} - * @param winKey {string} - Unique ID assigned to the window - */ - public closeWindow(windowType: WindowTypes, winKey?: string): void { - logger.info(`window-handler: closing window type ${windowType} with key ${winKey}!`); - switch (windowType) { - case 'screen-picker': - if (this.screenPickerWindow && windowExists(this.screenPickerWindow)) { - this.screenPickerWindow.close(); - } - break; - case 'screen-sharing-indicator': - if (winKey) { - const browserWindow = this.windows[winKey]; - - if (browserWindow && windowExists(browserWindow)) { - browserWindow.destroy(); - } - } - if (isWindowsOS || isMac) { - const timeoutValue = 300; - setTimeout(() => this.execCmd(this.screenShareIndicatorFrameUtil, []), timeoutValue); - } else { - if (this.screenSharingFrameWindow && windowExists(this.screenSharingFrameWindow)) { - this.screenSharingFrameWindow.close(); - } - } - break; - case 'notification-settings': - if (this.notificationSettingsWindow && windowExists(this.notificationSettingsWindow)) { - this.notificationSettingsWindow.close(); - } - break; - default: - break; - } - } - - /** - * Finds all the child window and closes it - */ - public async closeAllWindow(): Promise { - const browserWindows = BrowserWindow.getAllWindows(); - await notification.cleanUp(); - if (browserWindows && browserWindows.length) { - browserWindows.forEach((win) => { - const browserWindow = win as ICustomBrowserWindow; - if (browserWindow && windowExists(browserWindow)) { - // Closes only child windows - if (browserWindow.winName !== apiName.mainWindowName && browserWindow.winName !== apiName.notificationWindowName) { - if (browserWindow.closable) { - browserWindow.close(); - } else { - browserWindow.destroy(); - } - } - } - }); - } - } - - /** - * Sets is auto reload when the application - * is auto reloaded for optimizing memory - * - * @param shouldAutoReload {boolean} - */ - public setIsAutoReload(shouldAutoReload: boolean): void { - this.isAutoReload = shouldAutoReload; - } - - /** - * Checks if the window and a key has a window - * - * @param key {string} - * @param window {Electron.BrowserWindow} - */ - public hasWindow(key: string, window: Electron.BrowserWindow): boolean { - const browserWindow = this.windows[key]; - return browserWindow && window === browserWindow; - } - - /** - * Move window to the same screen as main window - */ - public moveWindow(windowToMove: BrowserWindow, fixedYPosition?: number) { - if (this.mainWindow && windowExists(this.mainWindow)) { - const display = electron.screen.getDisplayMatching(this.mainWindow.getBounds()); - - logger.info('window-handler: moveWindow, display: ' + JSON.stringify(display.workArea)); - logger.info('window-handler: moveWindow, windowToMove: ' + JSON.stringify(windowToMove.getBounds())); - - if (display.workArea.width < windowToMove.getBounds().width) { - windowToMove.setSize(display.workArea.width, windowToMove.getBounds().height); - } - - if (display.workArea.height < windowToMove.getBounds().height) { - windowToMove.setSize(windowToMove.getBounds().width, display.workArea.height); - } - - let positionX = Math.trunc(display.workArea.x + display.workArea.width / 2 - windowToMove.getBounds().width / 2); - if (positionX < display.workArea.x) { - positionX = display.workArea.x; - } - - let positionY; - if (fixedYPosition) { - positionY = Math.trunc(display.workArea.y + fixedYPosition); - } else { - // Center the window in y-axis - positionY = Math.trunc(display.workArea.y + display.workArea.height / 2 - windowToMove.getBounds().height / 2); - } - - if (positionY < display.workArea.y) { - positionY = display.workArea.y; - } - - logger.info('window-handler: moveWindow, positionX: ' + positionX); - logger.info('window-handler: moveWindow, positionY: ' + positionY); - - windowToMove.setPosition(positionX, positionY); - // Because of a bug for windows10 we need to call setPosition twice - windowToMove.setPosition(positionX, positionY); - } - } - - /** - * Creates a about app window - */ - public createAboutAppWindow(windowName: string): void { - - // This prevents creating multiple instances of the - // about window - if (didVerifyAndRestoreWindow(this.aboutAppWindow)) { - return; - } - - const selectedParentWindow = getWindowByName(windowName); - - const opts: BrowserWindowConstructorOptions = this.getWindowOpts({ - width: 440, - height: 315, - modal: true, - alwaysOnTop: isMac, - resizable: false, - fullscreenable: false, - }, { - devTools: false, - }); - - if (this.mainWindow && windowExists(this.mainWindow) && this.mainWindow.isAlwaysOnTop()) { - opts.alwaysOnTop = true; - } - - if (isWindowsOS && selectedParentWindow) { - opts.parent = selectedParentWindow; - } - - this.aboutAppWindow = createComponentWindow('about-app', opts); - this.moveWindow(this.aboutAppWindow); - this.aboutAppWindow.setVisibleOnAllWorkspaces(true); - - this.aboutAppWindow.webContents.once('did-finish-load', async () => { - let client = ''; - if (this.url && this.url.startsWith('https://corporate.symphony.com')) { - const manaPath = 'client-bff'; - const daily = 'daily'; - if (this.url.includes(manaPath)) { - if (this.url.includes(daily)) { - client = 'Symphony 2.0 - Daily'; - } else { - client = 'Symphony 2.0'; - } - } else { - client = 'Symphony Classic'; - } - } - const ABOUT_SYMPHONY_NAMESPACE = 'AboutSymphony'; - const versionLocalised = i18n.t('Version', ABOUT_SYMPHONY_NAMESPACE)(); - const { hostname } = parse(this.url || this.globalConfig.url); - const userConfig = config.userConfig; - const globalConfig = config.globalConfig; - const cloudConfig = config.cloudConfig; - const filteredConfig = config.filteredCloudConfig; - const finalConfig = { ...globalConfig, ...userConfig, ...filteredConfig }; - const aboutInfo = { - userConfig, - globalConfig, - cloudConfig, - finalConfig, - hostname, - versionLocalised, - ...versionHandler.versionInfo, - client, - }; - if (this.aboutAppWindow && windowExists(this.aboutAppWindow)) { - this.aboutAppWindow.webContents.send('about-app-data', aboutInfo); - } - }); - } - - /** - * Creates a screen picker window - * - * @param window - * @param sources - * @param id - */ - public createScreenPickerWindow(window: Electron.WebContents, sources: DesktopCapturerSource[], id: number): void { - - if (this.screenPickerWindow && windowExists(this.screenPickerWindow)) { - this.screenPickerWindow.close(); - } - - const opts: ICustomBrowserWindowConstructorOpts = this.getWindowOpts({ - alwaysOnTop: true, - autoHideMenuBar: true, - frame: false, - modal: true, - height: isMac ? 519 : 523, - width: 580, - show: false, - fullscreenable: false, - }, { - devTools: false, - }); - const focusedWindow = BrowserWindow.getFocusedWindow(); - if (focusedWindow && windowExists(focusedWindow) && isWindowsOS) { - opts.parent = focusedWindow; - } - - this.screenPickerWindow = createComponentWindow('screen-picker', opts); - this.moveWindow(this.screenPickerWindow); - this.screenPickerWindow.webContents.once('did-finish-load', () => { - if (!this.screenPickerWindow || !windowExists(this.screenPickerWindow)) { - return; - } - - this.screenPickerWindow.webContents.setZoomFactor(1); - this.screenPickerWindow.webContents.setVisualZoomLevelLimits(1, 1); - - this.screenPickerWindow.webContents.send('screen-picker-data', { sources, id }); - this.addWindow(opts.winKey, this.screenPickerWindow); - }); - ipcMain.once('screen-source-selected', (_event, source) => { - const displays = electron.screen.getAllDisplays(); - logger.info('window-utils: displays.length: ' + displays.length); - for (let i = 0, len = displays.length; i < len; i++) { - logger.info('window-utils: display[' + i + ']: ' + JSON.stringify(displays[i])); - } - - if (source != null) { - logger.info(`window-handler: screen-source-selected`, source, id); - if (isWindowsOS || isMac) { - const type = source.id.split(':')[0]; - if (type === 'window') { - const hwnd = source.id.split(':')[1]; - this.execCmd(this.screenShareIndicatorFrameUtil, [hwnd]); - } else if (isMac && type === 'screen') { - const dispId = source.id.split(':')[1]; - this.execCmd(this.screenShareIndicatorFrameUtil, [dispId]); - } else if (isWindowsOS && type === 'screen') { - logger.info('window-handler: source.display_id: ' + source.display_id); - if (source.display_id !== '') { - this.execCmd(this.screenShareIndicatorFrameUtil, [source.display_id]); - } else { - const dispId = source.id.split(':')[1]; - const clampedDispId = Math.min(dispId, displays.length - 1); - const keyId = 'id'; - logger.info('window-utils: dispId: ' + dispId); - logger.info('window-utils: clampedDispId: ' + clampedDispId); - logger.info('window-utils: displays [' + clampedDispId + '] [id]: ' + displays[clampedDispId][keyId]); - this.execCmd(this.screenShareIndicatorFrameUtil, [displays[clampedDispId][keyId].toString()]); - } - } - } - } - - window.send('start-share' + id, source); - if (this.screenPickerWindow && windowExists(this.screenPickerWindow)) { - this.screenPickerWindow.close(); - } - }); - this.screenPickerWindow.once('closed', () => { - this.removeWindow(opts.winKey); - this.screenPickerWindow = null; - }); - } - - /** - * Creates a Basic auth window whenever the network - * requires authentications - * - * Invoked by app.on('login') - * - * @param window - * @param hostname - * @param isMultipleTries - * @param clearSettings - * @param callback - */ - public createBasicAuthWindow(window: ICustomBrowserWindow, hostname: string, isMultipleTries: boolean, clearSettings, callback): void { - const opts = this.getWindowOpts({ - width: 360, - height: isMac ? 270 : 295, - show: false, - modal: true, - autoHideMenuBar: true, - resizable: false, - }, { - devTools: false, - }); - opts.parent = window; - this.basicAuthWindow = createComponentWindow('basic-auth', opts); - this.moveWindow(this.basicAuthWindow); - this.basicAuthWindow.setVisibleOnAllWorkspaces(true); - this.basicAuthWindow.webContents.once('did-finish-load', () => { - if (!this.basicAuthWindow || !windowExists(this.basicAuthWindow)) { - return; - } - this.basicAuthWindow.webContents.send('basic-auth-data', { hostname, isValidCredentials: isMultipleTries }); - }); - const closeBasicAuth = (_event, shouldClearSettings = true) => { - if (shouldClearSettings) { - clearSettings(); - } - if (this.basicAuthWindow && windowExists(this.basicAuthWindow)) { - this.basicAuthWindow.close(); - this.basicAuthWindow = null; - } - }; - - const login = (_event, arg) => { - const { username, password } = arg; - callback(username, password); - closeBasicAuth(null, false); - }; - - this.basicAuthWindow.once('close', () => { - ipcMain.removeListener('basic-auth-closed', closeBasicAuth); - ipcMain.removeListener('basic-auth-login', login); - }); - - ipcMain.once('basic-auth-closed', closeBasicAuth); - ipcMain.once('basic-auth-login', login); - } - - /** - * Creates and displays notification settings window - * - * @param windowName - */ - public createNotificationSettingsWindow(windowName: string): void { - const opts = this.getWindowOpts({ - width: 460, - height: 360, - show: false, - modal: true, - minimizable: false, - maximizable: false, - fullscreenable: false, - autoHideMenuBar: true, - }, { - devTools: false, - }); - // This prevents creating multiple instances of the - // notification configuration window - if (didVerifyAndRestoreWindow(this.notificationSettingsWindow)) { - return; - } - const selectedParentWindow = getWindowByName(windowName); - - if (selectedParentWindow) { - opts.parent = selectedParentWindow; - } - - this.notificationSettingsWindow = createComponentWindow('notification-settings', opts); - this.moveWindow(this.notificationSettingsWindow); - this.notificationSettingsWindow.setVisibleOnAllWorkspaces(true); - this.notificationSettingsWindow.webContents.on('did-finish-load', () => { - if (this.notificationSettingsWindow && windowExists(this.notificationSettingsWindow)) { - let screens: Electron.Display[] = []; - if (app.isReady()) { - screens = electron.screen.getAllDisplays(); - } - const { position, display } = config.getConfigFields(['notificationSettings']).notificationSettings; - this.notificationSettingsWindow.webContents.send('notification-settings-data', { screens, position, display }); - } - }); - - this.addWindow(opts.winKey, this.notificationSettingsWindow); - - ipcMain.once('notification-settings-update', async (_event, args) => { - const { display, position } = args; - try { - await config.updateUserConfig({ notificationSettings: { display, position } }); - } catch (e) { - logger.error(`NotificationSettings: Could not update user config file error`, e); - } - if (this.notificationSettingsWindow && windowExists(this.notificationSettingsWindow)) { - this.notificationSettingsWindow.close(); - } - // Update latest notification settings from config - notification.updateNotificationSettings(); - }); - - this.notificationSettingsWindow.once('closed', () => { - this.removeWindow(opts.winKey); - this.notificationSettingsWindow = null; - }); - } - - /** - * 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: string, - ): void { - const indicatorScreen = - (displayId && electron.screen.getAllDisplays().filter((d) => - displayId.includes(d.id.toString()))[0]) || electron.screen.getPrimaryDisplay(); - - const topPositionOfIndicatorScreen = 16; - - const screenRect = indicatorScreen.workArea; - // Set stream id as winKey to link stream to the window - let opts = { - ...this.getWindowOpts({ - width: 592, - height: 48, - show: false, - modal: true, - frame: false, - focusable: true, - transparent: true, - autoHideMenuBar: true, - resizable: false, - alwaysOnTop: true, - fullscreenable: false, - titleBarStyle: 'customButtonsOnHover', - minimizable: false, - maximizable: false, - title: 'Screen Sharing Indicator - Symphony', - closable: false, - }, { - devTools: false, - }), ...{ winKey: streamId }, - }; - if (opts.width && opts.height) { - opts = Object.assign({}, opts, { - x: screenRect.x + Math.round((screenRect.width - opts.width) / 2), - y: screenRect.y + topPositionOfIndicatorScreen, - }); - } - - logger.info('window-handler: createScreenSharingIndicatorWindow, displayId: ' + displayId); - if (displayId !== '') { - if (isLinux) { - const displays = electron.screen.getAllDisplays(); - displays.forEach((element) => { - logger.info('window-handler: element.id.toString(): ' + element.id.toString()); - if (displayId === element.id.toString()) { - logger.info(`window-handler: element:`, element); - this.createScreenSharingFrameWindow('screen-sharing-frame', - element.workArea.width, - element.workArea.height, - element.workArea.x, - element.workArea.y); - } - }); - } - } - - this.screenSharingIndicatorWindow = createComponentWindow('screen-sharing-indicator', opts); - this.moveWindow(this.screenSharingIndicatorWindow, topPositionOfIndicatorScreen); - this.screenSharingIndicatorWindow.setVisibleOnAllWorkspaces(true); - this.screenSharingIndicatorWindow.setSkipTaskbar(true); - this.screenSharingIndicatorWindow.setAlwaysOnTop(true, 'screen-saver'); - this.screenSharingIndicatorWindow.webContents.once('did-finish-load', () => { - if (!this.screenSharingIndicatorWindow || !windowExists(this.screenSharingIndicatorWindow)) { - return; - } - this.screenSharingIndicatorWindow.webContents.setZoomFactor(1); - this.screenSharingIndicatorWindow.webContents.setVisualZoomLevelLimits(1, 1); - 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); - } - - /** - * Creates a screen-sharing frame around the shared area - */ - public createScreenSharingFrameWindow(windowName: string, frameWidth: number, frameHeight: number, framePositionX: number, framePositionY: number): void { - - // This prevents creating multiple instances of the - // about window - if (didVerifyAndRestoreWindow(this.screenSharingFrameWindow)) { - return; - } - - const selectedParentWindow = getWindowByName(windowName); - - const opts: BrowserWindowConstructorOptions = this.getWindowOpts({ - width: frameWidth, - height: frameHeight, - frame: false, - transparent: true, - alwaysOnTop: true, - }, { - devTools: false, - }); - - if (this.mainWindow && windowExists(this.mainWindow) && this.mainWindow.isAlwaysOnTop()) { - opts.alwaysOnTop = true; - } - - if (isWindowsOS && selectedParentWindow) { - opts.parent = selectedParentWindow; - } - - this.screenSharingFrameWindow = createComponentWindow('screen-sharing-frame', opts); - - const area = this.screenSharingFrameWindow.getBounds(); - area.x = framePositionX; - area.y = framePositionY; - this.screenSharingFrameWindow.setBounds(area); - - this.screenSharingFrameWindow.setIgnoreMouseEvents(true); - this.screenSharingFrameWindow.setVisibleOnAllWorkspaces(true); - } - - /** - * Update version info on the about app window and more info window - */ - public async updateVersionInfo() { - await versionHandler.getClientVersion(true, this.url); - this.setAboutPanel(); - } - - /** - * Opens an external url in the system's default browser - * - * @param urlToOpen - */ - public openUrlInDefaultBrowser(urlToOpen) { - if (urlToOpen) { - electron.shell.openExternal(urlToOpen); - logger.info(`window-handler: opened url ${urlToOpen} in the default browser!`); - } - } - - /** - * Stores information of all the window we have created - * - * @param key {string} - * @param browserWindow {Electron.BrowserWindow} - */ - public addWindow(key: string, browserWindow: Electron.BrowserWindow): void { - this.windows[key] = browserWindow; - } - - /** - * Removes the window reference - * - * @param key {string} - */ - public removeWindow(key: string): void { - delete this.windows[key]; - } - - /** - * Executes the given command via a child process - * - * @param util {string} - * @param utilArgs {ReadonlyArray} - */ - public execCmd(util: string, utilArgs: ReadonlyArray): Promise { - logger.info(`window handler: execCmd: util: ${util} utilArgs: ${utilArgs}`); - return new Promise((resolve, reject) => { - return execFile(util, utilArgs, (error: ExecException | null) => { - if (error) { - logger.info(`window handler: execCmd: error: ${error}`); - } - if (error && error.killed) { - // processs was killed, just resolve with no data. - return reject(error); - } - resolve(); - }); - }); - } - - /** - * Sets the about panel details for macOS - */ - private setAboutPanel() { - if (!isMac) { - return; - } - const appName = app.getName(); - const copyright = `Copyright \xA9 ${new Date().getFullYear()} ${appName}`; - app.setAboutPanelOptions({ - applicationName: appName, - applicationVersion: versionHandler.versionInfo.clientVersion, - version: versionHandler.versionInfo.buildNumber, - copyright, - }); - - } - - /** - * Registers keyboard shortcuts or devtools - */ - private registerGlobalShortcuts(): void { - logger.info(`window-handler: registering global shortcuts!`); - globalShortcut.register(isMac ? 'Cmd+Alt+I' : 'Ctrl+Shift+I', this.onRegisterDevtools); - globalShortcut.register('CmdOrCtrl+R', this.onReload); - - // Hack to switch between Client 1.5, Mana-stable and Mana-daily - if (this.url && this.url.startsWith('https://corporate.symphony.com')) { - globalShortcut.register(isMac ? 'Cmd+Alt+1' : 'Ctrl+Shift+1', () => this.switchClient(ClientSwitchType.CLIENT_1_5)); - globalShortcut.register(isMac ? 'Cmd+Alt+2' : 'Ctrl+Shift+2', () => this.switchClient(ClientSwitchType.CLIENT_2_0)); - globalShortcut.register(isMac ? 'Cmd+Alt+3' : 'Ctrl+Shift+3', () => this.switchClient(ClientSwitchType.CLIENT_2_0_DAILY)); + logger.info( + 'window-handler: this.mainWindow.getBounds: ' + + JSON.stringify(this.mainWindow.getBounds()), + ); + + this.mainWindow.winName = apiName.mainWindowName; + // Get url to load from cmd line or from global config file + const urlFromCmd = getCommandLineArgs(process.argv, '--url=', false); + + if (urlFromCmd) { + const commandLineUrl = urlFromCmd.substr(6); + logger.info( + `window-handler: trying to set url ${commandLineUrl} from command line.`, + ); + const { podWhitelist } = config.getConfigFields(['podWhitelist']); + logger.info(`window-handler: checking pod whitelist.`); + if (podWhitelist.length > 0) { + logger.info( + `window-handler: pod whitelist is not empty ${podWhitelist}`, + ); + if (podWhitelist.includes(commandLineUrl)) { + logger.info( + `window-handler: url from command line is whitelisted in the config file.`, + ); + logger.info( + `window-handler: setting ${commandLineUrl} from the command line as the main window url.`, + ); + this.url = commandLineUrl; + this.shouldShowWelcomeScreen = false; + isMaximized = true; + isFullScreen = false; + this.mainWindow.resizable = true; + this.mainWindow.maximizable = true; + this.mainWindow.fullScreenable = true; } else { - logger.info('Switch between clients not supported for this POD-url'); + logger.info( + `window-handler: url ${commandLineUrl} from command line is NOT WHITELISTED in the config file.`, + ); } + } else { + logger.info( + `window-handler: setting ${commandLineUrl} from the command line as the main window url since pod whitelist is empty.`, + ); + this.url = commandLineUrl; + this.shouldShowWelcomeScreen = false; + isMaximized = true; + isFullScreen = false; + this.mainWindow.resizable = true; + this.mainWindow.maximizable = true; + this.mainWindow.fullScreenable = true; + } + } - if (isMac) { - globalShortcut.register('CmdOrCtrl+Plus', this.onZoomIn); - globalShortcut.register('CmdOrCtrl+=', this.onZoomIn); - } + if (isMaximized) { + this.mainWindow.maximize(); + logger.info(`window-handler: window is maximized!`); + } - app.on('browser-window-focus', () => { - globalShortcut.register(isMac ? 'Cmd+Alt+I' : 'Ctrl+Shift+I', this.onRegisterDevtools); - globalShortcut.register('CmdOrCtrl+R', this.onReload); - if (isMac) { - globalShortcut.register('CmdOrCtrl+Plus', this.onZoomIn); - globalShortcut.register('CmdOrCtrl+=', this.onZoomIn); - } - if (this.url && this.url.startsWith('https://corporate.symphony.com')) { - globalShortcut.register(isMac ? 'Cmd+Alt+1' : 'Ctrl+Shift+1', () => this.switchClient(ClientSwitchType.CLIENT_1_5)); - globalShortcut.register(isMac ? 'Cmd+Alt+2' : 'Ctrl+Shift+2', () => this.switchClient(ClientSwitchType.CLIENT_2_0)); - globalShortcut.register(isMac ? 'Cmd+Alt+3' : 'Ctrl+Shift+3', () => this.switchClient(ClientSwitchType.CLIENT_2_0_DAILY)); - } else { - logger.info('Switch between clients not supported for this POD-url'); - } + if (isFullScreen) { + logger.info(`window-handler: window is in full screen!`); + this.mainWindow.setFullScreen(true); + } + + this.startUrl = this.url; + if (this.shouldShowWelcomeScreen) { + this.handleWelcomeScreen(); + } + + cleanAppCacheOnCrash(this.mainWindow); + // loads the main window with url from config/cmd line + this.mainWindow.loadURL(this.url); + // check for build expiry in case of test builds + this.checkExpiry(this.mainWindow); + // update version info from server + this.updateVersionInfo(); + // need this for postMessage origin + this.mainWindow.origin = this.globalConfig.contextOriginUrl || this.url; + + // Event needed to hide native menu bar on Windows 10 as we use custom menu bar + this.mainWindow.webContents.once('did-start-loading', () => { + logger.info(`window-handler: main window web contents started loading!`); + if ( + this.config.isCustomTitleBar === CloudConfigDataTypes.ENABLED && + isWindowsOS && + this.mainWindow && + windowExists(this.mainWindow) + ) { + this.mainWindow.setMenuBarVisibility(false); + } + // monitors network connection and + // displays error banner on failure + monitorNetworkInterception( + this.url || this.userConfig.url || this.globalConfig.url, + ); + }); + + this.mainWindow.webContents.on('did-finish-load', async () => { + logger.info(`window-handler: main window web contents finished loading!`); + // early exit if the window has already been destroyed + if (!this.mainWindow || !windowExists(this.mainWindow)) { + logger.info( + `window-handler: main window web contents destroyed already! exiting`, + ); + return; + } + this.url = this.mainWindow.webContents.getURL(); + logger.info('window-handler: did-finish-load, url: ' + this.url); + const manaPath = 'client-bff'; + if (this.url.includes(manaPath)) { + this.isMana = true; + } else { + this.isMana = false; + } + logger.info('window-handler: isMana: ' + this.isMana); + + // Injects custom title bar and snack bar css into the webContents + await injectStyles(this.mainWindow, this.isCustomTitleBar); + + this.mainWindow.webContents.send('page-load', { + isWindowsOS, + locale: i18n.getLocale(), + resources: i18n.loadedResources, + enableCustomTitleBar: this.isCustomTitleBar, + isMainWindow: true, + }); + this.appMenu = new AppMenu(); + const { permissions } = config.getConfigFields(['permissions']); + this.mainWindow.webContents.send( + 'is-screen-share-enabled', + permissions.media, + ); + }); + + this.mainWindow.webContents.on( + 'did-fail-load', + (_event, errorCode, errorDesc, validatedURL) => { + logger.error( + `window-handler: Failed to load ${validatedURL}, with an error: ${errorCode}::${errorDesc}`, + ); + this.loadFailError = errorDesc; + }, + ); + + this.mainWindow.webContents.on('did-stop-loading', async () => { + if (this.mainWindow && windowExists(this.mainWindow)) { + this.mainWindow.webContents.send('page-load-failed', { + locale: i18n.getLocale(), + resources: i18n.loadedResources, }); - - app.on('browser-window-blur', () => { - globalShortcut.unregister(isMac ? 'Cmd+Alt+I' : 'Ctrl+Shift+I'); - globalShortcut.unregister('CmdOrCtrl+R'); - if (isMac) { - globalShortcut.unregister('CmdOrCtrl+Plus'); - globalShortcut.unregister('CmdOrCtrl+='); - } - // Unregister shortcuts related to client switch - if (this.url && this.url.startsWith('https://corporate.symphony.com')) { - globalShortcut.unregister(isMac ? 'Cmd+Alt+1' : 'Ctrl+Shift+1'); - globalShortcut.unregister(isMac ? 'Cmd+Alt+2' : 'Ctrl+Shift+2'); - globalShortcut.unregister(isMac ? 'Cmd+Alt+3' : 'Ctrl+Shift+3'); - } - }); - } - - /** - * Verifies and toggle devtool based on global config settings - * else displays a dialog - */ - private onRegisterDevtools(): void { - const focusedWindow = BrowserWindow.getFocusedWindow(); - if (!focusedWindow || !windowExists(focusedWindow)) { - return; - } - const { devToolsEnabled } = config.getConfigFields(['devToolsEnabled']); - if (devToolsEnabled) { - focusedWindow.webContents.toggleDevTools(); - return; - } - focusedWindow.webContents.closeDevTools(); - logger.info(`window-handler: dev tools disabled by admin, not opening it for the user!`); - } - - /** - * Reloads the window based on the window type - */ - private onReload(): void { - const focusedWindow = BrowserWindow.getFocusedWindow(); - if (!focusedWindow || !windowExists(focusedWindow)) { - return; - } - reloadWindow(focusedWindow as ICustomBrowserWindow); - } - - /** - * This is a workarround untill we have a - * fix on the electron framework - * https://github.com/electron/electron/issues/15496 - */ - private onZoomIn(): void { - const focusedWindow = BrowserWindow.getFocusedWindow(); - if (!focusedWindow || !windowExists(focusedWindow)) { - return; - } - - if (focusedWindow.getTitle() === 'Screen Sharing Indicator - Symphony') { - return; - } - - // electron/lib/browser/api/menu-item-roles.js row 159 - const currentZoomLevel = focusedWindow.webContents.getZoomLevel(); - focusedWindow.webContents.setZoomLevel(currentZoomLevel + 0.5); - } - - /** - * Switch between clients 1.5, 2.0 and 2.0 daily - * @param clientSwitch client switch you want to switch to. - */ - private async switchClient(clientSwitch: ClientSwitchType): Promise { - logger.info(`window handler: switch to client ${clientSwitch}`); - - if (!this.mainWindow || !windowExists(this.mainWindow)) { - logger.info(`window-handler: switch client - main window web contents destroyed already! exiting`); - return; - } + const href = await this.mainWindow.webContents.executeJavaScript( + 'document.location.href', + ); try { - if (!this.url) { - this.url = this.globalConfig.url; + if ( + href === 'data:text/html,chromewebdata' || + href === 'chrome-error://chromewebdata/' + ) { + if (this.mainWindow && windowExists(this.mainWindow)) { + this.mainWindow.webContents.insertCSS( + fs + .readFileSync( + path.join( + __dirname, + '..', + '/renderer/styles/network-error.css', + ), + 'utf8', + ) + .toString(), + ); + this.mainWindow.webContents.send('network-error', { + error: this.loadFailError, + }); + isSymphonyReachable( + this.mainWindow, + this.url || this.userConfig.url || this.globalConfig.url, + ); } - const parsedUrl = parse(this.url); - const manaPath = 'client-bff'; - const manaChannel = 'daily'; - const csrfToken = await this.mainWindow.webContents.executeJavaScript(`localStorage.getItem('x-km-csrf-token')`); - switch (clientSwitch) { - case ClientSwitchType.CLIENT_1_5: - this.url = this.startUrl + `?x-km-csrf-token=${csrfToken}`; - break; - case ClientSwitchType.CLIENT_2_0: - this.url = `https://${parsedUrl.hostname}/${manaPath}/index.html?x-km-csrf-token=${csrfToken}`; - break; - case ClientSwitchType.CLIENT_2_0_DAILY: - this.url = `https://${parsedUrl.hostname}/${manaPath}/${manaChannel}/index.html?x-km-csrf-token=${csrfToken}`; - break; - default: - this.url = this.globalConfig.url + `?x-km-csrf-token=${csrfToken}`; + } + } catch (error) { + logger.error( + `window-handler: Could not read document.location`, + error, + ); + } + } + }); + + this.mainWindow.webContents.on( + 'crashed', + async (_event: Event, killed: boolean) => { + if (killed) { + logger.info(`window-handler: main window crashed (killed)!`); + return; + } + logger.info(`window-handler: main window crashed!`); + const { response } = await electron.dialog.showMessageBox({ + type: 'error', + title: i18n.t('Renderer Process Crashed')(), + message: i18n.t( + 'Oops! Looks like we have had a crash. Please reload or close this window.', + )(), + buttons: ['Reload', 'Close'], + }); + if (!this.mainWindow || !windowExists(this.mainWindow)) { + return; + } + response === 0 ? this.mainWindow.reload() : this.mainWindow.close(); + }, + ); + + // Handle main window close + this.mainWindow.on('close', (event) => { + if (!this.mainWindow || !windowExists(this.mainWindow)) { + return; + } + + if (this.willQuitApp) { + logger.info(`window-handler: app is quitting, destroying all windows!`); + if (this.mainWindow && this.mainWindow.webContents.isDevToolsOpened()) { + this.mainWindow.webContents.closeDevTools(); + } + return this.destroyAllWindows(); + } + + const { minimizeOnClose } = config.getConfigFields(['minimizeOnClose']); + if (minimizeOnClose === CloudConfigDataTypes.ENABLED) { + event.preventDefault(); + this.mainWindow.minimize(); + return; + } + + if (isMac) { + event.preventDefault(); + this.mainWindow.hide(); + return; + } + + app.quit(); + }); + + this.mainWindow.once('closed', () => { + logger.info( + `window-handler: main window closed, destroying all windows!`, + ); + if (isWindowsOS || isMac) { + this.execCmd(this.screenShareIndicatorFrameUtil, []); + } + this.closeAllWindow(); + this.destroyAllWindows(); + }); + + // Reloads the Symphony + ipcMain.on('reload-symphony', () => { + if (this.mainWindow && windowExists(this.mainWindow)) { + this.mainWindow.loadURL(this.url || this.globalConfig.url); + } + }); + + // Certificate verification proxy + if (!isDevEnv) { + this.mainWindow.webContents.session.setCertificateVerifyProc( + handleCertificateProxyVerification, + ); + } + + // Register global shortcuts + this.registerGlobalShortcuts(); + + // Validate window navigation + preventWindowNavigation(this.mainWindow, false); + + // Handle media/other permissions + handlePermissionRequests(this.mainWindow.webContents); + + // Start monitoring window actions + monitorWindowActions(this.mainWindow); + + // Download manager + this.mainWindow.webContents.session.on( + 'will-download', + handleDownloadManager, + ); + + // store window ref + this.addWindow(this.windowOpts.winKey, this.mainWindow); + + // Handle pop-outs window + handleChildWindow(this.mainWindow.webContents); + + if (this.config.enableRendererLogs) { + this.mainWindow.webContents.on('console-message', onConsoleMessages); + } + + return this.mainWindow; + } + + /** + * Handles the use case of showing + * welcome screen for first time installs + */ + public handleWelcomeScreen() { + if (!this.url || !this.mainWindow) { + return; + } + + if (this.url.startsWith(this.defaultPodUrl)) { + this.url = format({ + pathname: require.resolve('../renderer/react-window.html'), + protocol: 'file', + query: { + componentName: 'welcome', + locale: i18n.getLocale(), + }, + slashes: true, + }); + } + + this.mainWindow.webContents.on('did-finish-load', () => { + if (!this.url || !this.mainWindow) { + return; + } + if (this.url.indexOf('welcome')) { + this.mainWindow.webContents.send('page-load-welcome', { + locale: i18n.getLocale(), + resource: i18n.loadedResources, + }); + const userConfigUrl = + this.userConfig.url && + this.userConfig.url.indexOf('/login/sso/initsso') > -1 + ? this.userConfig.url.slice( + 0, + this.userConfig.url.indexOf('/login/sso/initsso'), + ) + : this.userConfig.url; + + this.mainWindow.webContents.send('welcome', { + url: userConfigUrl || this.startUrl, + message: '', + urlValid: !!userConfigUrl, + sso: false, + }); + } + }); + + ipcMain.on('set-pod-url', async (_event, newPodUrl: string) => { + await config.updateUserConfig({ url: newPodUrl }); + app.relaunch(); + app.exit(); + }); + } + + /** + * Gets the main window + */ + public getMainWindow(): ICustomBrowserWindow | null { + return this.mainWindow; + } + + /** + * Gets all the window that we have created + * + * @return {Electron.BrowserWindow} + * + */ + public getAllWindows(): object { + return this.windows; + } + + /** + * Closes the window from an event emitted by the render processes + * + * @param windowType {WindowTypes} + * @param winKey {string} - Unique ID assigned to the window + */ + public closeWindow(windowType: WindowTypes, winKey?: string): void { + logger.info( + `window-handler: closing window type ${windowType} with key ${winKey}!`, + ); + switch (windowType) { + case 'screen-picker': + if (this.screenPickerWindow && windowExists(this.screenPickerWindow)) { + this.screenPickerWindow.close(); + } + break; + case 'screen-sharing-indicator': + if (winKey) { + const browserWindow = this.windows[winKey]; + + if (browserWindow && windowExists(browserWindow)) { + browserWindow.destroy(); + } + } + if (isWindowsOS || isMac) { + const timeoutValue = 300; + setTimeout( + () => this.execCmd(this.screenShareIndicatorFrameUtil, []), + timeoutValue, + ); + } else { + if ( + this.screenSharingFrameWindow && + windowExists(this.screenSharingFrameWindow) + ) { + this.screenSharingFrameWindow.close(); + } + } + break; + case 'notification-settings': + if ( + this.notificationSettingsWindow && + windowExists(this.notificationSettingsWindow) + ) { + this.notificationSettingsWindow.close(); + } + break; + default: + break; + } + } + + /** + * Finds all the child window and closes it + */ + public async closeAllWindow(): Promise { + const browserWindows = BrowserWindow.getAllWindows(); + await notification.cleanUp(); + if (browserWindows && browserWindows.length) { + browserWindows.forEach((win) => { + const browserWindow = win as ICustomBrowserWindow; + if (browserWindow && windowExists(browserWindow)) { + // Closes only child windows + if ( + browserWindow.winName !== apiName.mainWindowName && + browserWindow.winName !== apiName.notificationWindowName + ) { + if (browserWindow.closable) { + browserWindow.close(); + } else { + browserWindow.destroy(); } - this.execCmd(this.screenShareIndicatorFrameUtil, []); - await this.mainWindow.loadURL(this.url); - } catch (e) { - logger.error(`window-handler: failed to switch client because of error ${e}`); + } } + }); + } + } + + /** + * Sets is auto reload when the application + * is auto reloaded for optimizing memory + * + * @param shouldAutoReload {boolean} + */ + public setIsAutoReload(shouldAutoReload: boolean): void { + this.isAutoReload = shouldAutoReload; + } + + /** + * Checks if the window and a key has a window + * + * @param key {string} + * @param window {Electron.BrowserWindow} + */ + public hasWindow(key: string, window: Electron.BrowserWindow): boolean { + const browserWindow = this.windows[key]; + return browserWindow && window === browserWindow; + } + + /** + * Move window to the same screen as main window + */ + public moveWindow(windowToMove: BrowserWindow, fixedYPosition?: number) { + if (this.mainWindow && windowExists(this.mainWindow)) { + const display = electron.screen.getDisplayMatching( + this.mainWindow.getBounds(), + ); + + logger.info( + 'window-handler: moveWindow, display: ' + + JSON.stringify(display.workArea), + ); + logger.info( + 'window-handler: moveWindow, windowToMove: ' + + JSON.stringify(windowToMove.getBounds()), + ); + + if (display.workArea.width < windowToMove.getBounds().width) { + windowToMove.setSize( + display.workArea.width, + windowToMove.getBounds().height, + ); + } + + if (display.workArea.height < windowToMove.getBounds().height) { + windowToMove.setSize( + windowToMove.getBounds().width, + display.workArea.height, + ); + } + + let positionX = Math.trunc( + display.workArea.x + + display.workArea.width / 2 - + windowToMove.getBounds().width / 2, + ); + if (positionX < display.workArea.x) { + positionX = display.workArea.x; + } + + let positionY; + if (fixedYPosition) { + positionY = Math.trunc(display.workArea.y + fixedYPosition); + } else { + // Center the window in y-axis + positionY = Math.trunc( + display.workArea.y + + display.workArea.height / 2 - + windowToMove.getBounds().height / 2, + ); + } + + if (positionY < display.workArea.y) { + positionY = display.workArea.y; + } + + logger.info('window-handler: moveWindow, positionX: ' + positionX); + logger.info('window-handler: moveWindow, positionY: ' + positionY); + + windowToMove.setPosition(positionX, positionY); + // Because of a bug for windows10 we need to call setPosition twice + windowToMove.setPosition(positionX, positionY); + } + } + + /** + * Creates a about app window + */ + public createAboutAppWindow(windowName: string): void { + // This prevents creating multiple instances of the + // about window + if (didVerifyAndRestoreWindow(this.aboutAppWindow)) { + return; } - /** - * Cleans up reference - */ - private destroyAllWindows(): void { - for (const key in this.windows) { - if (Object.prototype.hasOwnProperty.call(this.windows, key)) { - const winKey = this.windows[key]; - this.removeWindow(winKey); + const selectedParentWindow = getWindowByName(windowName); + + const opts: BrowserWindowConstructorOptions = this.getWindowOpts( + { + width: 440, + height: 315, + modal: true, + alwaysOnTop: isMac, + resizable: false, + fullscreenable: false, + }, + { + devTools: false, + }, + ); + + if ( + this.mainWindow && + windowExists(this.mainWindow) && + this.mainWindow.isAlwaysOnTop() + ) { + opts.alwaysOnTop = true; + } + + if (isWindowsOS && selectedParentWindow) { + opts.parent = selectedParentWindow; + } + + this.aboutAppWindow = createComponentWindow('about-app', opts); + this.moveWindow(this.aboutAppWindow); + this.aboutAppWindow.setVisibleOnAllWorkspaces(true); + + this.aboutAppWindow.webContents.once('did-finish-load', async () => { + let client = ''; + if (this.url && this.url.startsWith('https://corporate.symphony.com')) { + const manaPath = 'client-bff'; + const daily = 'daily'; + if (this.url.includes(manaPath)) { + if (this.url.includes(daily)) { + client = 'Symphony 2.0 - Daily'; + } else { + client = 'Symphony 2.0'; + } + } else { + client = 'Symphony Classic'; + } + } + const ABOUT_SYMPHONY_NAMESPACE = 'AboutSymphony'; + const versionLocalised = i18n.t('Version', ABOUT_SYMPHONY_NAMESPACE)(); + const { hostname } = parse(this.url || this.globalConfig.url); + const userConfig = config.userConfig; + const globalConfig = config.globalConfig; + const cloudConfig = config.cloudConfig; + const filteredConfig = config.filteredCloudConfig; + const finalConfig = { + ...globalConfig, + ...userConfig, + ...filteredConfig, + }; + const aboutInfo = { + userConfig, + globalConfig, + cloudConfig, + finalConfig, + hostname, + versionLocalised, + ...versionHandler.versionInfo, + client, + }; + if (this.aboutAppWindow && windowExists(this.aboutAppWindow)) { + this.aboutAppWindow.webContents.send('about-app-data', aboutInfo); + } + }); + } + + /** + * Creates the snipping tool window + */ + public createSnippingToolWindow( + snipImage: string, + dimensions?: { + height: number | undefined; + width: number | undefined; + }, + ): void { + // Prevents creating multiple instances + if (didVerifyAndRestoreWindow(this.snippingToolWindow)) { + return; + } + + const parentWindow = BrowserWindow.getFocusedWindow(); + const MIN_HEIGHT = 312; + const MIN_WIDTH = 320; + const CONTAINER_HEIGHT = 124; + + let windowHeight = dimensions?.height + ? dimensions.height + CONTAINER_HEIGHT + : 600; + let windowWidth = dimensions?.width || 800; + + if (dimensions && dimensions.height && dimensions.height < MIN_HEIGHT) { + windowHeight = MIN_HEIGHT + CONTAINER_HEIGHT; + } + + if (dimensions && dimensions.width && dimensions.width < MIN_WIDTH) { + windowWidth = MIN_WIDTH; + } + + const opts: ICustomBrowserWindowConstructorOpts = this.getWindowOpts( + { + width: windowWidth, + height: windowHeight, + modal: false, + alwaysOnTop: false, + resizable: false, + fullscreenable: false, + }, + { + devTools: false, + }, + ); + + if ( + this.mainWindow && + windowExists(this.mainWindow) && + this.mainWindow.isAlwaysOnTop() + ) { + opts.alwaysOnTop = true; + } + + if (isWindowsOS && parentWindow) { + opts.parent = parentWindow; + } + + this.snippingToolWindow = createComponentWindow('snipping-tool', opts); + this.moveWindow(this.snippingToolWindow); + this.snippingToolWindow.setVisibleOnAllWorkspaces(true); + + this.snippingToolWindow.webContents.once('did-finish-load', async () => { + const snippingToolInfo = { + snipImage, + height: dimensions?.height, + width: dimensions?.width, + }; + if (this.snippingToolWindow && windowExists(this.snippingToolWindow)) { + this.snippingToolWindow.webContents.send( + 'snipping-tool-data', + snippingToolInfo, + ); + } + }); + + this.snippingToolWindow.once('closed', () => { + this.removeWindow(opts.winKey); + this.screenPickerWindow = null; + }); + } + + /** + * Closes the snipping tool window + */ + public closeSnippingToolWindow() { + if (this.snippingToolWindow && windowExists(this.snippingToolWindow)) { + this.snippingToolWindow.close(); + } + } + + /** + * Creates a screen picker window + * + * @param window + * @param sources + * @param id + */ + public createScreenPickerWindow( + window: Electron.WebContents, + sources: DesktopCapturerSource[], + id: number, + ): void { + if (this.screenPickerWindow && windowExists(this.screenPickerWindow)) { + this.screenPickerWindow.close(); + } + + const opts: ICustomBrowserWindowConstructorOpts = this.getWindowOpts( + { + alwaysOnTop: true, + autoHideMenuBar: true, + frame: false, + modal: true, + height: isMac ? 519 : 523, + width: 580, + show: false, + fullscreenable: false, + }, + { + devTools: false, + }, + ); + const focusedWindow = BrowserWindow.getFocusedWindow(); + if (focusedWindow && windowExists(focusedWindow) && isWindowsOS) { + opts.parent = focusedWindow; + } + + this.screenPickerWindow = createComponentWindow('screen-picker', opts); + this.moveWindow(this.screenPickerWindow); + this.screenPickerWindow.webContents.once('did-finish-load', () => { + if (!this.screenPickerWindow || !windowExists(this.screenPickerWindow)) { + return; + } + + this.screenPickerWindow.webContents.setZoomFactor(1); + this.screenPickerWindow.webContents.setVisualZoomLevelLimits(1, 1); + + this.screenPickerWindow.webContents.send('screen-picker-data', { + sources, + id, + }); + this.addWindow(opts.winKey, this.screenPickerWindow); + }); + ipcMain.once('screen-source-selected', (_event, source) => { + const displays = electron.screen.getAllDisplays(); + logger.info('window-utils: displays.length: ' + displays.length); + for (let i = 0, len = displays.length; i < len; i++) { + logger.info( + 'window-utils: display[' + i + ']: ' + JSON.stringify(displays[i]), + ); + } + + if (source != null) { + logger.info(`window-handler: screen-source-selected`, source, id); + if (isWindowsOS || isMac) { + const type = source.id.split(':')[0]; + if (type === 'window') { + const hwnd = source.id.split(':')[1]; + this.execCmd(this.screenShareIndicatorFrameUtil, [hwnd]); + } else if (isMac && type === 'screen') { + const dispId = source.id.split(':')[1]; + this.execCmd(this.screenShareIndicatorFrameUtil, [dispId]); + } else if (isWindowsOS && type === 'screen') { + logger.info( + 'window-handler: source.display_id: ' + source.display_id, + ); + if (source.display_id !== '') { + this.execCmd(this.screenShareIndicatorFrameUtil, [ + source.display_id, + ]); + } else { + const dispId = source.id.split(':')[1]; + const clampedDispId = Math.min(dispId, displays.length - 1); + const keyId = 'id'; + logger.info('window-utils: dispId: ' + dispId); + logger.info('window-utils: clampedDispId: ' + clampedDispId); + logger.info( + 'window-utils: displays [' + + clampedDispId + + '] [id]: ' + + displays[clampedDispId][keyId], + ); + this.execCmd(this.screenShareIndicatorFrameUtil, [ + displays[clampedDispId][keyId].toString(), + ]); } + } } - this.mainWindow = null; + } + + window.send('start-share' + id, source); + if (this.screenPickerWindow && windowExists(this.screenPickerWindow)) { + this.screenPickerWindow.close(); + } + }); + this.screenPickerWindow.once('closed', () => { + this.removeWindow(opts.winKey); + this.screenPickerWindow = null; + }); + } + + /** + * Creates a Basic auth window whenever the network + * requires authentications + * + * Invoked by app.on('login') + * + * @param window + * @param hostname + * @param isMultipleTries + * @param clearSettings + * @param callback + */ + public createBasicAuthWindow( + window: ICustomBrowserWindow, + hostname: string, + isMultipleTries: boolean, + clearSettings, + callback, + ): void { + const opts = this.getWindowOpts( + { + width: 360, + height: isMac ? 270 : 295, + show: false, + modal: true, + autoHideMenuBar: true, + resizable: false, + }, + { + devTools: false, + }, + ); + opts.parent = window; + this.basicAuthWindow = createComponentWindow('basic-auth', opts); + this.moveWindow(this.basicAuthWindow); + this.basicAuthWindow.setVisibleOnAllWorkspaces(true); + this.basicAuthWindow.webContents.once('did-finish-load', () => { + if (!this.basicAuthWindow || !windowExists(this.basicAuthWindow)) { + return; + } + this.basicAuthWindow.webContents.send('basic-auth-data', { + hostname, + isValidCredentials: isMultipleTries, + }); + }); + const closeBasicAuth = (_event, shouldClearSettings = true) => { + if (shouldClearSettings) { + clearSettings(); + } + if (this.basicAuthWindow && windowExists(this.basicAuthWindow)) { + this.basicAuthWindow.close(); + this.basicAuthWindow = null; + } + }; + + const login = (_event, arg) => { + const { username, password } = arg; + callback(username, password); + closeBasicAuth(null, false); + }; + + this.basicAuthWindow.once('close', () => { + ipcMain.removeListener('basic-auth-closed', closeBasicAuth); + ipcMain.removeListener('basic-auth-login', login); + }); + + ipcMain.once('basic-auth-closed', closeBasicAuth); + ipcMain.once('basic-auth-login', login); + } + + /** + * Creates and displays notification settings window + * + * @param windowName + */ + public createNotificationSettingsWindow(windowName: string): void { + const opts = this.getWindowOpts( + { + width: 460, + height: 360, + show: false, + modal: true, + minimizable: false, + maximizable: false, + fullscreenable: false, + autoHideMenuBar: true, + }, + { + devTools: false, + }, + ); + // This prevents creating multiple instances of the + // notification configuration window + if (didVerifyAndRestoreWindow(this.notificationSettingsWindow)) { + return; + } + const selectedParentWindow = getWindowByName(windowName); + + if (selectedParentWindow) { + opts.parent = selectedParentWindow; } - /** - * Check if build is expired and show an error message - * @param browserWindow Focused window instance - */ - private async checkExpiry(browserWindow: BrowserWindow) { - logger.info(`window handler: calling ttl handler to check for build expiry!`); - const buildExpired = checkIfBuildExpired(); - if (!buildExpired) { - logger.info(`window handler: build not expired, proceeding further!`); - return; + this.notificationSettingsWindow = createComponentWindow( + 'notification-settings', + opts, + ); + this.moveWindow(this.notificationSettingsWindow); + this.notificationSettingsWindow.setVisibleOnAllWorkspaces(true); + this.notificationSettingsWindow.webContents.on('did-finish-load', () => { + if ( + this.notificationSettingsWindow && + windowExists(this.notificationSettingsWindow) + ) { + let screens: Electron.Display[] = []; + if (app.isReady()) { + screens = electron.screen.getAllDisplays(); } - logger.info(`window handler: build expired, will inform the user and quit the app!`); + const { position, display } = config.getConfigFields([ + 'notificationSettings', + ]).notificationSettings; + this.notificationSettingsWindow.webContents.send( + 'notification-settings-data', + { screens, position, display }, + ); + } + }); - const options = { - type: 'error', - title: i18n.t('Build expired')(), - message: i18n.t('Sorry, this is a test build and it has expired. Please contact your administrator to get a production build.')(), - buttons: [i18n.t('Quit')()], - cancelId: 0, - }; + this.addWindow(opts.winKey, this.notificationSettingsWindow); - const { response } = await electron.dialog.showMessageBox(browserWindow, options); - if (response === 0) { - electron.app.exit(); - } + ipcMain.once('notification-settings-update', async (_event, args) => { + const { display, position } = args; + try { + await config.updateUserConfig({ + notificationSettings: { display, position }, + }); + } catch (e) { + logger.error( + `NotificationSettings: Could not update user config file error`, + e, + ); + } + if ( + this.notificationSettingsWindow && + windowExists(this.notificationSettingsWindow) + ) { + this.notificationSettingsWindow.close(); + } + // Update latest notification settings from config + notification.updateNotificationSettings(); + }); + + this.notificationSettingsWindow.once('closed', () => { + this.removeWindow(opts.winKey); + this.notificationSettingsWindow = null; + }); + } + + /** + * 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: string, + ): void { + const indicatorScreen = + (displayId && + electron.screen + .getAllDisplays() + .filter((d) => displayId.includes(d.id.toString()))[0]) || + electron.screen.getPrimaryDisplay(); + + const topPositionOfIndicatorScreen = 16; + + const screenRect = indicatorScreen.workArea; + // Set stream id as winKey to link stream to the window + let opts = { + ...this.getWindowOpts( + { + width: 592, + height: 48, + show: false, + modal: true, + frame: false, + focusable: true, + transparent: true, + autoHideMenuBar: true, + resizable: false, + alwaysOnTop: true, + fullscreenable: false, + titleBarStyle: 'customButtonsOnHover', + minimizable: false, + maximizable: false, + title: 'Screen Sharing Indicator - Symphony', + closable: false, + }, + { + devTools: false, + }, + ), + ...{ winKey: streamId }, + }; + if (opts.width && opts.height) { + opts = Object.assign({}, opts, { + x: screenRect.x + Math.round((screenRect.width - opts.width) / 2), + y: screenRect.y + topPositionOfIndicatorScreen, + }); } - /** - * Returns constructor opts for the browser window - * - * @param windowOpts {Electron.BrowserWindowConstructorOptions} - * @param webPreferences {Electron.WebPreferences} - */ - private getWindowOpts(windowOpts: Electron.BrowserWindowConstructorOptions, webPreferences: Electron.WebPreferences): ICustomBrowserWindowConstructorOpts { - const defaultPreferencesOpts = { - ...{ - sandbox: !isNodeEnv, - nodeIntegration: isNodeEnv, - contextIsolation: isNodeEnv ? false : this.contextIsolation, - backgroundThrottling: this.backgroundThrottling, - }, ...webPreferences, - }; - const defaultWindowOpts = { - alwaysOnTop: false, - webPreferences: defaultPreferencesOpts, - winKey: getGuid(), - }; - - return { ...defaultWindowOpts, ...windowOpts }; + logger.info( + 'window-handler: createScreenSharingIndicatorWindow, displayId: ' + + displayId, + ); + if (displayId !== '') { + if (isLinux) { + const displays = electron.screen.getAllDisplays(); + displays.forEach((element) => { + logger.info( + 'window-handler: element.id.toString(): ' + element.id.toString(), + ); + if (displayId === element.id.toString()) { + logger.info(`window-handler: element:`, element); + this.createScreenSharingFrameWindow( + 'screen-sharing-frame', + element.workArea.width, + element.workArea.height, + element.workArea.x, + element.workArea.y, + ); + } + }); + } } + + this.screenSharingIndicatorWindow = createComponentWindow( + 'screen-sharing-indicator', + opts, + ); + this.moveWindow( + this.screenSharingIndicatorWindow, + topPositionOfIndicatorScreen, + ); + this.screenSharingIndicatorWindow.setVisibleOnAllWorkspaces(true); + this.screenSharingIndicatorWindow.setSkipTaskbar(true); + this.screenSharingIndicatorWindow.setAlwaysOnTop(true, 'screen-saver'); + this.screenSharingIndicatorWindow.webContents.once( + 'did-finish-load', + () => { + if ( + !this.screenSharingIndicatorWindow || + !windowExists(this.screenSharingIndicatorWindow) + ) { + return; + } + this.screenSharingIndicatorWindow.webContents.setZoomFactor(1); + this.screenSharingIndicatorWindow.webContents.setVisualZoomLevelLimits( + 1, + 1, + ); + 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); + } + + /** + * Creates a screen-sharing frame around the shared area + */ + public createScreenSharingFrameWindow( + windowName: string, + frameWidth: number, + frameHeight: number, + framePositionX: number, + framePositionY: number, + ): void { + // This prevents creating multiple instances of the + // about window + if (didVerifyAndRestoreWindow(this.screenSharingFrameWindow)) { + return; + } + + const selectedParentWindow = getWindowByName(windowName); + + const opts: BrowserWindowConstructorOptions = this.getWindowOpts( + { + width: frameWidth, + height: frameHeight, + frame: false, + transparent: true, + alwaysOnTop: true, + }, + { + devTools: false, + }, + ); + + if ( + this.mainWindow && + windowExists(this.mainWindow) && + this.mainWindow.isAlwaysOnTop() + ) { + opts.alwaysOnTop = true; + } + + if (isWindowsOS && selectedParentWindow) { + opts.parent = selectedParentWindow; + } + + this.screenSharingFrameWindow = createComponentWindow( + 'screen-sharing-frame', + opts, + ); + + const area = this.screenSharingFrameWindow.getBounds(); + area.x = framePositionX; + area.y = framePositionY; + this.screenSharingFrameWindow.setBounds(area); + + this.screenSharingFrameWindow.setIgnoreMouseEvents(true); + this.screenSharingFrameWindow.setVisibleOnAllWorkspaces(true); + } + + /** + * Update version info on the about app window and more info window + */ + public async updateVersionInfo() { + await versionHandler.getClientVersion(true, this.url); + this.setAboutPanel(); + } + + /** + * Opens an external url in the system's default browser + * + * @param urlToOpen + */ + public openUrlInDefaultBrowser(urlToOpen) { + if (urlToOpen) { + electron.shell.openExternal(urlToOpen); + logger.info( + `window-handler: opened url ${urlToOpen} in the default browser!`, + ); + } + } + + /** + * Stores information of all the window we have created + * + * @param key {string} + * @param browserWindow {Electron.BrowserWindow} + */ + public addWindow(key: string, browserWindow: Electron.BrowserWindow): void { + this.windows[key] = browserWindow; + } + + /** + * Removes the window reference + * + * @param key {string} + */ + public removeWindow(key: string): void { + delete this.windows[key]; + } + + /** + * Executes the given command via a child process + * + * @param util {string} + * @param utilArgs {ReadonlyArray} + */ + public execCmd( + util: string, + utilArgs: ReadonlyArray, + ): Promise { + logger.info(`window handler: execCmd: util: ${util} utilArgs: ${utilArgs}`); + return new Promise((resolve, reject) => { + return execFile(util, utilArgs, (error: ExecException | null) => { + if (error) { + logger.info(`window handler: execCmd: error: ${error}`); + } + if (error && error.killed) { + // processs was killed, just resolve with no data. + return reject(error); + } + resolve(); + }); + }); + } + + /** + * Sets the about panel details for macOS + */ + private setAboutPanel() { + if (!isMac) { + return; + } + const appName = app.getName(); + const copyright = `Copyright \xA9 ${new Date().getFullYear()} ${appName}`; + app.setAboutPanelOptions({ + applicationName: appName, + applicationVersion: versionHandler.versionInfo.clientVersion, + version: versionHandler.versionInfo.buildNumber, + copyright, + }); + } + + /** + * Registers keyboard shortcuts or devtools + */ + private registerGlobalShortcuts(): void { + logger.info(`window-handler: registering global shortcuts!`); + globalShortcut.register( + isMac ? 'Cmd+Alt+I' : 'Ctrl+Shift+I', + this.onRegisterDevtools, + ); + globalShortcut.register('CmdOrCtrl+R', this.onReload); + + // Hack to switch between Client 1.5, Mana-stable and Mana-daily + if (this.url && this.url.startsWith('https://corporate.symphony.com')) { + globalShortcut.register(isMac ? 'Cmd+Alt+1' : 'Ctrl+Shift+1', () => + this.switchClient(ClientSwitchType.CLIENT_1_5), + ); + globalShortcut.register(isMac ? 'Cmd+Alt+2' : 'Ctrl+Shift+2', () => + this.switchClient(ClientSwitchType.CLIENT_2_0), + ); + globalShortcut.register(isMac ? 'Cmd+Alt+3' : 'Ctrl+Shift+3', () => + this.switchClient(ClientSwitchType.CLIENT_2_0_DAILY), + ); + } else { + logger.info('Switch between clients not supported for this POD-url'); + } + + if (isMac) { + globalShortcut.register('CmdOrCtrl+Plus', this.onZoomIn); + globalShortcut.register('CmdOrCtrl+=', this.onZoomIn); + } + + app.on('browser-window-focus', () => { + globalShortcut.register( + isMac ? 'Cmd+Alt+I' : 'Ctrl+Shift+I', + this.onRegisterDevtools, + ); + globalShortcut.register('CmdOrCtrl+R', this.onReload); + if (isMac) { + globalShortcut.register('CmdOrCtrl+Plus', this.onZoomIn); + globalShortcut.register('CmdOrCtrl+=', this.onZoomIn); + } + if (this.url && this.url.startsWith('https://corporate.symphony.com')) { + globalShortcut.register(isMac ? 'Cmd+Alt+1' : 'Ctrl+Shift+1', () => + this.switchClient(ClientSwitchType.CLIENT_1_5), + ); + globalShortcut.register(isMac ? 'Cmd+Alt+2' : 'Ctrl+Shift+2', () => + this.switchClient(ClientSwitchType.CLIENT_2_0), + ); + globalShortcut.register(isMac ? 'Cmd+Alt+3' : 'Ctrl+Shift+3', () => + this.switchClient(ClientSwitchType.CLIENT_2_0_DAILY), + ); + } else { + logger.info('Switch between clients not supported for this POD-url'); + } + }); + + app.on('browser-window-blur', () => { + globalShortcut.unregister(isMac ? 'Cmd+Alt+I' : 'Ctrl+Shift+I'); + globalShortcut.unregister('CmdOrCtrl+R'); + if (isMac) { + globalShortcut.unregister('CmdOrCtrl+Plus'); + globalShortcut.unregister('CmdOrCtrl+='); + } + // Unregister shortcuts related to client switch + if (this.url && this.url.startsWith('https://corporate.symphony.com')) { + globalShortcut.unregister(isMac ? 'Cmd+Alt+1' : 'Ctrl+Shift+1'); + globalShortcut.unregister(isMac ? 'Cmd+Alt+2' : 'Ctrl+Shift+2'); + globalShortcut.unregister(isMac ? 'Cmd+Alt+3' : 'Ctrl+Shift+3'); + } + }); + } + + /** + * Verifies and toggle devtool based on global config settings + * else displays a dialog + */ + private onRegisterDevtools(): void { + const focusedWindow = BrowserWindow.getFocusedWindow(); + if (!focusedWindow || !windowExists(focusedWindow)) { + return; + } + const { devToolsEnabled } = config.getConfigFields(['devToolsEnabled']); + if (devToolsEnabled) { + focusedWindow.webContents.toggleDevTools(); + return; + } + focusedWindow.webContents.closeDevTools(); + logger.info( + `window-handler: dev tools disabled by admin, not opening it for the user!`, + ); + } + + /** + * Reloads the window based on the window type + */ + private onReload(): void { + const focusedWindow = BrowserWindow.getFocusedWindow(); + if (!focusedWindow || !windowExists(focusedWindow)) { + return; + } + reloadWindow(focusedWindow as ICustomBrowserWindow); + } + + /** + * This is a workarround untill we have a + * fix on the electron framework + * https://github.com/electron/electron/issues/15496 + */ + private onZoomIn(): void { + const focusedWindow = BrowserWindow.getFocusedWindow(); + if (!focusedWindow || !windowExists(focusedWindow)) { + return; + } + + if (focusedWindow.getTitle() === 'Screen Sharing Indicator - Symphony') { + return; + } + + // electron/lib/browser/api/menu-item-roles.js row 159 + const currentZoomLevel = focusedWindow.webContents.getZoomLevel(); + focusedWindow.webContents.setZoomLevel(currentZoomLevel + 0.5); + } + + /** + * Switch between clients 1.5, 2.0 and 2.0 daily + * @param clientSwitch client switch you want to switch to. + */ + private async switchClient(clientSwitch: ClientSwitchType): Promise { + logger.info(`window handler: switch to client ${clientSwitch}`); + + if (!this.mainWindow || !windowExists(this.mainWindow)) { + logger.info( + `window-handler: switch client - main window web contents destroyed already! exiting`, + ); + return; + } + try { + if (!this.url) { + this.url = this.globalConfig.url; + } + const parsedUrl = parse(this.url); + const manaPath = 'client-bff'; + const manaChannel = 'daily'; + const csrfToken = await this.mainWindow.webContents.executeJavaScript( + `localStorage.getItem('x-km-csrf-token')`, + ); + switch (clientSwitch) { + case ClientSwitchType.CLIENT_1_5: + this.url = this.startUrl + `?x-km-csrf-token=${csrfToken}`; + break; + case ClientSwitchType.CLIENT_2_0: + this.url = `https://${parsedUrl.hostname}/${manaPath}/index.html?x-km-csrf-token=${csrfToken}`; + break; + case ClientSwitchType.CLIENT_2_0_DAILY: + this.url = `https://${parsedUrl.hostname}/${manaPath}/${manaChannel}/index.html?x-km-csrf-token=${csrfToken}`; + break; + default: + this.url = this.globalConfig.url + `?x-km-csrf-token=${csrfToken}`; + } + this.execCmd(this.screenShareIndicatorFrameUtil, []); + await this.mainWindow.loadURL(this.url); + } catch (e) { + logger.error( + `window-handler: failed to switch client because of error ${e}`, + ); + } + } + + /** + * Cleans up reference + */ + private destroyAllWindows(): void { + for (const key in this.windows) { + if (Object.prototype.hasOwnProperty.call(this.windows, key)) { + const winKey = this.windows[key]; + this.removeWindow(winKey); + } + } + this.mainWindow = null; + } + + /** + * Check if build is expired and show an error message + * @param browserWindow Focused window instance + */ + private async checkExpiry(browserWindow: BrowserWindow) { + logger.info( + `window handler: calling ttl handler to check for build expiry!`, + ); + const buildExpired = checkIfBuildExpired(); + if (!buildExpired) { + logger.info(`window handler: build not expired, proceeding further!`); + return; + } + logger.info( + `window handler: build expired, will inform the user and quit the app!`, + ); + + const options = { + type: 'error', + title: i18n.t('Build expired')(), + message: i18n.t( + 'Sorry, this is a test build and it has expired. Please contact your administrator to get a production build.', + )(), + buttons: [i18n.t('Quit')()], + cancelId: 0, + }; + + const { response } = await electron.dialog.showMessageBox( + browserWindow, + options, + ); + if (response === 0) { + electron.app.exit(); + } + } + + /** + * Returns constructor opts for the browser window + * + * @param windowOpts {Electron.BrowserWindowConstructorOptions} + * @param webPreferences {Electron.WebPreferences} + */ + private getWindowOpts( + windowOpts: Electron.BrowserWindowConstructorOptions, + webPreferences: Electron.WebPreferences, + ): ICustomBrowserWindowConstructorOpts { + const defaultPreferencesOpts = { + ...{ + sandbox: !isNodeEnv, + nodeIntegration: isNodeEnv, + contextIsolation: isNodeEnv ? false : this.contextIsolation, + backgroundThrottling: this.backgroundThrottling, + }, + ...webPreferences, + }; + const defaultWindowOpts = { + alwaysOnTop: false, + webPreferences: defaultPreferencesOpts, + winKey: getGuid(), + }; + + return { ...defaultWindowOpts, ...windowOpts }; + } } const windowHandler = new WindowHandler(); diff --git a/src/app/window-utils.ts b/src/app/window-utils.ts index 7e35983f..a7fe7ebb 100644 --- a/src/app/window-utils.ts +++ b/src/app/window-utils.ts @@ -32,7 +32,7 @@ enum styleNames { } const checkValidWindow = true; -const { ctWhitelist } = config.getConfigFields([ 'ctWhitelist' ]); +const { ctWhitelist } = config.getConfigFields(['ctWhitelist']); // Network status check variables const networkStatusCheckInterval = 10 * 1000; @@ -76,7 +76,7 @@ export const preventWindowNavigation = (browserWindow: BrowserWindow, isPopOutWi if (browserWindow && windowExists(browserWindow)) { const response = await electron.dialog.showMessageBox(browserWindow, { type: 'warning', - buttons: [ 'OK' ], + buttons: ['OK'], title: i18n.t('Not Allowed')(), message: `${i18n.t(`Sorry, you are not allowed to access this website`)()} (${winUrl}), ${i18n.t('please contact your administrator for more details')()}`, }); @@ -317,7 +317,7 @@ export const getBounds = (winPos: ICustomRectangle | Electron.Rectangle | undefi const displays = electron.screen.getAllDisplays(); for (let i = 0, len = displays.length; i < len; i++) { - const bounds = displays[ i ].bounds; + const bounds = displays[i].bounds; logger.info('window-utils: getBounds, bounds: ' + JSON.stringify(bounds)); if (winPos.x >= bounds.x && winPos.y >= bounds.y && ((winPos.x + winPos.width) <= (bounds.x + bounds.width)) && @@ -659,11 +659,11 @@ export const monitorNetworkInterception = (url: string) => { logger.info('window-utils: monitoring network interception for url', podUrl); // Filter applied w.r.t pod url - const filter = { urls: [ podUrl + '*' ] }; + const filter = { urls: [podUrl + '*'] }; if (mainWindow && windowExists(mainWindow)) { isNetworkMonitorInitialized = true; - mainWindow.webContents.session.webRequest.onErrorOccurred(filter,async (details) => { + mainWindow.webContents.session.webRequest.onErrorOccurred(filter, async (details) => { if (!mainWindow || !windowExists(mainWindow)) { return; } @@ -671,8 +671,8 @@ export const monitorNetworkInterception = (url: string) => { const isMana = manaUrl && manaUrl.includes('client-bff'); if (!isMana && windowHandler.isWebPageLoading && (details.error === 'net::ERR_INTERNET_DISCONNECTED' - || details.error === 'net::ERR_NETWORK_CHANGED' - || details.error === 'net::ERR_NAME_NOT_RESOLVED')) { + || details.error === 'net::ERR_NETWORK_CHANGED' + || details.error === 'net::ERR_NAME_NOT_RESOLVED')) { logger.error(`window-utils: URL failed to load`, details); mainWindow.webContents.send('show-banner', { show: true, bannerType: 'error', url: podUrl }); diff --git a/src/locale/en-US.json b/src/locale/en-US.json index c0ae724d..96a46f8a 100644 --- a/src/locale/en-US.json +++ b/src/locale/en-US.json @@ -158,7 +158,8 @@ "Erase": "Erase", "Highlight": "Highlight", "Pen": "Pen", - "Snipping Tool": "Snipping Tool" + "Snipping Tool": "Snipping Tool", + "Clear": "Clear" }, "Select All": "Select All", "Services": "Services", diff --git a/src/locale/en.json b/src/locale/en.json index c0ae724d..96a46f8a 100644 --- a/src/locale/en.json +++ b/src/locale/en.json @@ -158,7 +158,8 @@ "Erase": "Erase", "Highlight": "Highlight", "Pen": "Pen", - "Snipping Tool": "Snipping Tool" + "Snipping Tool": "Snipping Tool", + "Clear": "Clear" }, "Select All": "Select All", "Services": "Services", diff --git a/src/locale/fr-FR.json b/src/locale/fr-FR.json index 4f160c83..b5e28781 100644 --- a/src/locale/fr-FR.json +++ b/src/locale/fr-FR.json @@ -31,7 +31,6 @@ "Build expired": "Construit expiré", "Cancel": "Annuler", "Certificate Error": "Erreur de certificat", - "Changing GPU settings requires Symphony to relaunch.": "La modification des paramètres du GPU nécessite la relance de Symphony.", "Clear cache and Reload": "Vider le cache et rafraîchir Symphony", "Close": "Fermer", "ContextMenu": { @@ -159,7 +158,8 @@ "Erase": "Effacer", "Highlight": "Surligner", "Pen": "Stylo", - "Snipping Tool": "Outil Capture" + "Snipping Tool": "Outil Capture", + "Clear": "Claire" }, "Select All": "Tout sélectionner", "Services": "Services", diff --git a/src/locale/fr.json b/src/locale/fr.json index d269de4d..b5e28781 100644 --- a/src/locale/fr.json +++ b/src/locale/fr.json @@ -158,7 +158,8 @@ "Erase": "Effacer", "Highlight": "Surligner", "Pen": "Stylo", - "Snipping Tool": "Outil Capture" + "Snipping Tool": "Outil Capture", + "Clear": "Claire" }, "Select All": "Tout sélectionner", "Services": "Services", diff --git a/src/locale/ja-JP.json b/src/locale/ja-JP.json index bd439e1a..5c620a1f 100644 --- a/src/locale/ja-JP.json +++ b/src/locale/ja-JP.json @@ -158,7 +158,8 @@ "Erase": "消去", "Highlight": "強調", "Pen": "ペン", - "Snipping Tool": "切り取りツール" + "Snipping Tool": "切り取りツール", + "Clear": "晴れ" }, "Select All": "すべてを選択", "Services": "サービス", diff --git a/src/locale/ja.json b/src/locale/ja.json index bd439e1a..5c620a1f 100644 --- a/src/locale/ja.json +++ b/src/locale/ja.json @@ -158,7 +158,8 @@ "Erase": "消去", "Highlight": "強調", "Pen": "ペン", - "Snipping Tool": "切り取りツール" + "Snipping Tool": "切り取りツール", + "Clear": "晴れ" }, "Select All": "すべてを選択", "Services": "サービス", diff --git a/src/renderer/assets/snip-draw.svg b/src/renderer/assets/snip-draw.svg new file mode 100644 index 00000000..e35968fd --- /dev/null +++ b/src/renderer/assets/snip-draw.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/renderer/assets/snip-erase.svg b/src/renderer/assets/snip-erase.svg new file mode 100644 index 00000000..12e08afb --- /dev/null +++ b/src/renderer/assets/snip-erase.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/renderer/assets/snip-highlight.svg b/src/renderer/assets/snip-highlight.svg new file mode 100644 index 00000000..d237193c --- /dev/null +++ b/src/renderer/assets/snip-highlight.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/renderer/assets/snip-redo.png b/src/renderer/assets/snip-redo.png new file mode 100644 index 0000000000000000000000000000000000000000..aa3a1e3505312e1e4156da829d4e414570d9253c GIT binary patch literal 9234 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 literal 0 HcmV?d00001 diff --git a/src/renderer/assets/snip-redo.svg b/src/renderer/assets/snip-redo.svg new file mode 100644 index 00000000..cfe98f26 --- /dev/null +++ b/src/renderer/assets/snip-redo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/renderer/assets/snip-undo.png b/src/renderer/assets/snip-undo.png new file mode 100644 index 0000000000000000000000000000000000000000..2396fbfc1860bf02480f5b46edc23235ab9f1994 GIT binary patch literal 10084 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 XRQaXIVb3EU#ewtMhG%om5bpmEsCvqO literal 0 HcmV?d00001 diff --git a/src/renderer/assets/snip-undo.svg b/src/renderer/assets/snip-undo.svg new file mode 100644 index 00000000..2a5b9755 --- /dev/null +++ b/src/renderer/assets/snip-undo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/renderer/components/about-app.tsx b/src/renderer/components/about-app.tsx index c2b68031..45d1b648 100644 --- a/src/renderer/components/about-app.tsx +++ b/src/renderer/components/about-app.tsx @@ -133,12 +133,12 @@ export default class AboutApp extends React.Component<{}, IState> { public copy(): void { const data = this.state; if (data) { - remote.clipboard.write({ text: JSON.stringify(data, null, 4) }, 'clipboard' ); + remote.clipboard.write({ text: JSON.stringify(data, null, 4) }, 'clipboard'); } } /** - * Sets the About app state + * Sets the component state * * @param _event * @param data {Object} { buildNumber, clientVersion, version } diff --git a/src/renderer/components/basic-auth.tsx b/src/renderer/components/basic-auth.tsx index e140e543..4e23b1ba 100644 --- a/src/renderer/components/basic-auth.tsx +++ b/src/renderer/components/basic-auth.tsx @@ -55,18 +55,18 @@ export default class BasicAuth extends React.Component<{}, IState> {
- - - - - - - - + + + + + + + +
{i18n.t('User name:', BASIC_AUTH_NAMESPACE)()} - -
{i18n.t('Password:', BASIC_AUTH_NAMESPACE)()} - -
{i18n.t('User name:', BASIC_AUTH_NAMESPACE)()} + +
{i18n.t('Password:', BASIC_AUTH_NAMESPACE)()} + +
@@ -89,7 +89,7 @@ export default class BasicAuth extends React.Component<{}, IState> { */ private change(event): void { this.setState({ - [ (event.target as any).id ]: (event.target as any).value, + [(event.target as any).id]: (event.target as any).value, } as IState); } @@ -111,7 +111,7 @@ export default class BasicAuth extends React.Component<{}, IState> { } /** - * Sets the About app state + * Sets the component state * * @param _event * @param data {Object} { hostname, isValidCredentials } diff --git a/src/renderer/components/loading-screen.tsx b/src/renderer/components/loading-screen.tsx index 70bd520b..dab0d3ca 100644 --- a/src/renderer/components/loading-screen.tsx +++ b/src/renderer/components/loading-screen.tsx @@ -50,11 +50,11 @@ export default class LoadingScreen extends React.Component<{}, IState> { return (
- + {appName} - +
@@ -78,7 +78,7 @@ export default class LoadingScreen extends React.Component<{}, IState> { private renderErrorContent(error: string): JSX.Element { return (
- + {i18n.t('Problem connecting to Symphony')()}
{error}
@@ -104,7 +104,7 @@ export default class LoadingScreen extends React.Component<{}, IState> { } /** - * Sets the About app state + * Sets the component state * * @param _event * @param data {Object} diff --git a/src/renderer/components/notification-comp.tsx b/src/renderer/components/notification-comp.tsx index de99cae8..0ca5d967 100644 --- a/src/renderer/components/notification-comp.tsx +++ b/src/renderer/components/notification-comp.tsx @@ -4,7 +4,7 @@ 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); -const darkTheme = [ '#e23030', '#b5616a', '#ab8ead', '#ebc875', '#a3be77', '#58c6ff', '#ebab58' ]; +const darkTheme = ['#e23030', '#b5616a', '#ab8ead', '#ebc875', '#a3be77', '#58c6ff', '#ebab58']; type Theme = '' | 'light' | 'dark'; interface IState { @@ -77,29 +77,29 @@ export default class NotificationComp extends React.Component<{}, IState> { return (
- {isExternal ?
: null} + {isExternal ?
: null}
- - - - + + + + - - + + - - + + @@ -115,8 +115,8 @@ export default class NotificationComp extends React.Component<{}, IState> { {this.renderProfile(icon, image, title)}
- - + +
@@ -134,10 +134,10 @@ export default class NotificationComp extends React.Component<{}, IState> { return (
- - - - + + + +
); @@ -155,7 +155,7 @@ export default class NotificationComp extends React.Component<{}, IState> { if (icon || image) { return (
- user profile picture + user profile picture
); } @@ -223,7 +223,7 @@ export default class NotificationComp extends React.Component<{}, IState> { } /** - * Sets the About app state + * Sets the component state * * @param _event * @param data {Object} diff --git a/src/renderer/components/notification-settings.tsx b/src/renderer/components/notification-settings.tsx index 1bdd479a..5b094a22 100644 --- a/src/renderer/components/notification-settings.tsx +++ b/src/renderer/components/notification-settings.tsx @@ -118,7 +118,7 @@ export default class NotificationSettings extends React.Component<{}, IState> { type='radio' name='position' checked={this.state.position === id} - value={id}/> + value={id} />
); } @@ -174,7 +174,7 @@ export default class NotificationSettings extends React.Component<{}, IState> { } /** - * Sets the About app state + * Sets the component state * * @param _event * @param data {Object} { buildNumber, clientVersion, version } diff --git a/src/renderer/components/screen-picker.tsx b/src/renderer/components/screen-picker.tsx index 31cb11fd..bba66f54 100644 --- a/src/renderer/components/screen-picker.tsx +++ b/src/renderer/components/screen-picker.tsx @@ -148,7 +148,7 @@ export default class ScreenPicker extends React.Component<{}, IState> { id={source.id} onClick={() => this.eventHandlers.onSelect(source)}>
- thumbnail image + thumbnail image
{screenName}
, @@ -161,7 +161,7 @@ export default class ScreenPicker extends React.Component<{}, IState> { id={source.id} onClick={() => this.eventHandlers.onSelect(source)}>
- thumbnail image + thumbnail image
{source.name}
, @@ -173,8 +173,8 @@ export default class ScreenPicker extends React.Component<{}, IState> { if (!this.isScreensAvailable && !this.isApplicationsAvailable) { return (
- - {i18n.t('No screens or applications are currently available.', SCREEN_PICKER_NAMESPACE)()} + + {i18n.t('No screens or applications are currently available.', SCREEN_PICKER_NAMESPACE)()}
); @@ -205,7 +205,7 @@ export default class ScreenPicker extends React.Component<{}, IState> { onChange={this.eventHandlers.onToggle('screens')} />, , @@ -218,7 +218,7 @@ export default class ScreenPicker extends React.Component<{}, IState> { onChange={this.eventHandlers.onToggle('applications')} />, , @@ -235,7 +235,7 @@ export default class ScreenPicker extends React.Component<{}, IState> { onChange={this.eventHandlers.onToggle('screens')} />, , @@ -252,7 +252,7 @@ export default class ScreenPicker extends React.Component<{}, IState> { onChange={this.eventHandlers.onToggle('applications')} />, , @@ -372,14 +372,14 @@ export default class ScreenPicker extends React.Component<{}, IState> { // Find the next item to be selected const nextIndex = (this.currentIndex + index + sources.length) % sources.length; - if (sources[ nextIndex ] && sources[ nextIndex ].id) { + if (sources[nextIndex] && sources[nextIndex].id) { // Updates the selected source - this.setState({ selectedSource: sources[ nextIndex ] }); + this.setState({ selectedSource: sources[nextIndex] }); } } /** - * Sets the About app state + * Sets the component state * * @param _event * @param data {Object} diff --git a/src/renderer/components/screen-sharing-indicator.tsx b/src/renderer/components/screen-sharing-indicator.tsx index 4cecdc57..c748e864 100644 --- a/src/renderer/components/screen-sharing-indicator.tsx +++ b/src/renderer/components/screen-sharing-indicator.tsx @@ -1,5 +1,5 @@ import classNames from 'classnames'; -import { ipcRenderer, remote } from 'electron'; +import { ipcRenderer, remote } from 'electron'; import * as React from 'react'; import { apiCmds, apiName } from '../../common/api-interface'; @@ -40,7 +40,7 @@ export default class ScreenSharingIndicator extends React.Component<{}, IState> return (
- {(i18n.t(`You are sharing your screen on {appName}`, namespace)({ appName: remote.app.getName()})).replace(remote.app.getName(), '')} + {(i18n.t(`You are sharing your screen on {appName}`, namespace)({ appName: remote.app.getName() })).replace(remote.app.getName(), '')}  {remote.app.getName()} @@ -82,7 +82,7 @@ export default class ScreenSharingIndicator extends React.Component<{}, IState> } /** - * Sets the About app state + * Sets the component state * * @param _event * @param data {Object} { buildNumber, clientVersion, version } diff --git a/src/renderer/components/snipping-tool.tsx b/src/renderer/components/snipping-tool.tsx new file mode 100644 index 00000000..5e677ff2 --- /dev/null +++ b/src/renderer/components/snipping-tool.tsx @@ -0,0 +1,124 @@ +import { ipcRenderer } from 'electron'; +import * as React from 'react'; +import { i18n } from '../../common/i18n-preload'; + +const { useState, useEffect } = React; + +interface IProps { + drawEnabled: boolean; + highlightEnabled: boolean; + eraseEnabled: boolean; +} + +const SNIPPING_TOOL_NAMESPACE = 'ScreenSnippet'; + +const SnippingTool: React.FunctionComponent = ({drawEnabled, highlightEnabled, eraseEnabled}) => { + + const [screenSnippet, setScreenSnippet] = useState('Screen-Snippet'); + const [imageDimensions, setImageDimensions] = useState({height: 600, width: 800}); + + const getSnipImageData = (_event, {snipImage, height, width}) => { + setScreenSnippet(snipImage); + setImageDimensions({height, width}); + }; + + ipcRenderer.on('snipping-tool-data', getSnipImageData); + + useEffect(() => { + return () => { + ipcRenderer.removeListener('snipping-tool-data', getSnipImageData); + }; + }, []); + + const usePen = () => { + // setTool("pen"); + // setShouldRenderPenColorPicker(shouldRenderPenColorPicker => !shouldRenderPenColorPicker); + // setShouldRenderHighlightColorPicker(false); + }; + + const useHighlight = () => { + // setTool("highlight"); + // setShouldRenderHighlightColorPicker(shouldRenderHighlightColorPicker => !shouldRenderHighlightColorPicker); + // setShouldRenderPenColorPicker(false); + }; + + const useEraser = () => { + // setTool("eraser"); + }; + + const clear = () => { + // const updPaths = [...paths]; + // updPaths.map((p) => { + // p.shouldShow = false; + // return p; + // }); + // setPaths(updPaths); + }; + + const done = () => { + ipcRenderer.send('upload-snippet', screenSnippet); + }; + + return ( +
+
+
+ + + +
+
+ +
+
+ +
+ {i18n.t('Screen +
+ +
+ +
+
+ ); +}; + +export default SnippingTool; diff --git a/src/renderer/preload-component.ts b/src/renderer/preload-component.ts index 7bf89c56..0a4b0372 100644 --- a/src/renderer/preload-component.ts +++ b/src/renderer/preload-component.ts @@ -10,6 +10,7 @@ import NotificationSettings from './components/notification-settings'; import ScreenPicker from './components/screen-picker'; import ScreenSharingFrame from './components/screen-sharing-frame'; import ScreenSharingIndicator from './components/screen-sharing-indicator'; +import SnippingTool from './components/snipping-tool'; import Welcome from './components/welcome'; const enum components { @@ -21,6 +22,7 @@ const enum components { notification = 'notification-comp', notificationSettings = 'notification-settings', welcome = 'welcome', + snippingTool = 'snipping-tool', } const loadStyle = (style) => { @@ -47,26 +49,31 @@ const load = () => { break; case components.screenPicker: loadStyle(components.screenPicker); - document.title = 'Screen Picker - Symphony'; + document.title = i18n.t('Screen Picker - Symphony')(); component = ScreenPicker; break; case components.screenSharingIndicator: loadStyle(components.screenSharingIndicator); - document.title = 'Screen Sharing Indicator - Symphony'; + document.title = i18n.t('Screen Sharing Indicator - Symphony')(); component = ScreenSharingIndicator; break; case components.screenSharingFrame: loadStyle(components.screenSharingFrame); component = ScreenSharingFrame; break; + case components.snippingTool: + loadStyle(components.snippingTool); + document.title = i18n.t('Symphony')(); + component = SnippingTool; + break; case components.basicAuth: loadStyle(components.basicAuth); - document.title = 'Basic Authentication - Symphony'; + document.title = i18n.t('Basic Authentication - Symphony')(); component = BasicAuth; break; case components.notification: loadStyle(components.notification); - document.title = 'Notification - Symphony'; + document.title = i18n.t('Notification - Symphony')(); component = NotificationComp; break; case components.notificationSettings: diff --git a/src/renderer/styles/snipping-tool.less b/src/renderer/styles/snipping-tool.less new file mode 100644 index 00000000..66664507 --- /dev/null +++ b/src/renderer/styles/snipping-tool.less @@ -0,0 +1,182 @@ +@import 'theme'; + +@white: rgb(255, 255, 255, 1); +@version-text-color: rgb(47, 47, 47, 1); +@text-padding: 10px; + +::-webkit-scrollbar { + display: none; +} + +body { + background-color: white; + margin: 0; + height: 100%; + width: 100%; +} + +.SnippingTool:lang(ja-JP) { + font-family: @font-family-ja; + + h4 { + margin: 3px 0; + } + + .SnippingTool-symphony-section { + padding-left: 10px; + } +} + +.SnippingTool:lang(fr-FR) { + .SnippingTool-symphony-section { + padding-left: 10px; + } +} + +.SnippingTool { + display: flex; + flex-direction: column; + font-family: @font-family; + + header { + display: flex; + flex-direction: row; + justify-content: center; + text-align: center; + padding: 4px 0; + max-height: 48px; + + .ActionButton { + margin-left: 24px; + background: white; + border-radius: 4px; + cursor: pointer; + padding: 4px 0; + border: none; + + &:first-child { + margin-left: 0; + } + + img { + width: 24px; + height: 24px; + } + } + + .ActionButtonSelected { + margin-left: 24px; + background: white; + border-radius: 4px; + cursor: pointer; + padding: 4px 0; + border: 2px solid #008eff; + box-sizing: border-box; + border-radius: 4px; + + &:first-child { + margin-left: 0; + } + + img { + width: 24px; + height: 24px; + } + } + + .ClearButton { + display: block; + padding: 4px 10px; + border: 2px solid #7c7f86; + border-radius: 16px; + color: #7c7f86; + font-weight: 600; + font-size: 12px; + line-height: 16px; + margin-left: 24px; + text-transform: uppercase; + cursor: pointer; + background-color: #ffffff; + } + + .DrawActions { + display: flex; + flex-direction: row; + justify-content: center; + text-align: center; + } + + .ClearActions { + display: flex; + flex-direction: row; + justify-content: center; + text-align: center; + position: absolute; + right: 24px; + align-items: center; + + .ActionButton { + img { + width: 16px; + } + } + } + } + + main { + text-align: center; + margin: 0; + background: linear-gradient( + 0deg, + rgba(255, 255, 255, 0.96), + rgba(255, 255, 255, 0.96) + ), + #525760; + + .SnippetImage { + width: 100%; + height: 100%; + object-fit: contain; + } + } + + footer { + display: flex; + justify-content: flex-end; + padding: 2px 32px 4px 0; + max-height: 72px; + + button { + box-shadow: none; + border: none; + border-radius: 16px; + font-size: 0.75rem; + font-weight: 600; + text-align: center; + padding: 8px 24px; + display: inline-block; + text-decoration: none; + line-height: 16px; + background-color: #008eff; + color: rgba(255, 255, 255, 0.96); + cursor: pointer; + text-transform: uppercase; + margin: 10px 30px 4px 0; + + &:focus { + box-shadow: 0 0 10px rgba(61, 162, 253, 1); + outline: none; + } + } + } + + @media only screen and (max-width: 400px) { + header { + .ClearActions { + position: relative; + right: auto; + margin-left: 24px; + } + } + } +} diff --git a/src/renderer/styles/theme.less b/src/renderer/styles/theme.less index b55023f6..1b9dc8fd 100644 --- a/src/renderer/styles/theme.less +++ b/src/renderer/styles/theme.less @@ -1,5 +1,6 @@ -@font-family: "Segoe UI", "Helvetica Neue", "Verdana", "Arial", sans-serif; -@font-family-ja: "ヒラギノ角ゴ Pro W3", "Hiragino Kaku Gothic Pro", Meiryo, "MS Pゴシック", "MS PGothic", sans-serif; -@text-color-primary: #FFFFFF; -@text-color-primary-dark: #2F3237; -@text-color-secondary: #6A707C; +@font-family: 'Segoe UI', 'Helvetica Neue', 'Verdana', 'Arial', sans-serif; +@font-family-ja: 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, + 'MS Pゴシック', 'MS PGothic', sans-serif; +@text-color-primary: #ffffff; +@text-color-primary-dark: #2f3237; +@text-color-secondary: #6a707c; diff --git a/tslint.json b/tslint.json index 029d45f9..1ca56f61 100644 --- a/tslint.json +++ b/tslint.json @@ -26,7 +26,7 @@ "no-trailing-whitespace": true, "no-duplicate-variable": true, "no-var-keyword": true, - "variable-name": [true, "ban-keywords", "check-format", "allow-leading-underscore"], + "variable-name": [true, "ban-keywords", "check-format", "allow-leading-underscore", "allow-pascal-case"], "no-empty": true, "no-unused-expression": true, "no-use-before-declare": true, From fd0ef8593cd8eab8880a40d742d67830cf448742 Mon Sep 17 00:00:00 2001 From: Johan Kwarnmark Date: Fri, 6 Nov 2020 16:55:18 +0100 Subject: [PATCH 4/7] sda-2657 remove redframe and screen sharing indicator when reloading --- src/app/app-menu.ts | 42 ++++++++++++++++++++--------------------- src/app/window-utils.ts | 2 ++ 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/src/app/app-menu.ts b/src/app/app-menu.ts index 5e7beb16..cd23d9b2 100644 --- a/src/app/app-menu.ts +++ b/src/app/app-menu.ts @@ -65,7 +65,7 @@ let { let initialAnalyticsSent = false; const menuItemsArray = Object.keys(menuSections) - .map((key) => menuSections[ key ]) + .map((key) => menuSections[key]) .filter((value) => isMac ? true : value !== menuSections.about); @@ -82,7 +82,7 @@ export class AppMenu { constructor() { this.menuList = []; this.locale = i18n.getLocale(); - this.menuItemConfigFields = [ 'minimizeOnClose', 'launchOnStartup', 'alwaysOnTop', 'bringToFront', 'memoryRefresh', 'isCustomTitleBar', 'devToolsEnabled' ]; + this.menuItemConfigFields = ['minimizeOnClose', 'launchOnStartup', 'alwaysOnTop', 'bringToFront', 'memoryRefresh', 'isCustomTitleBar', 'devToolsEnabled']; this.cloudConfig = config.getFilteredCloudConfigFields(this.menuItemConfigFields); this.disableGpu = config.getConfigFields(['disableGpu']).disableGpu; this.enableRendererLogs = config.getConfigFields(['enableRendererLogs']).enableRendererLogs; @@ -107,12 +107,12 @@ export class AppMenu { this.updateGlobals(); this.menuList = menuItemsArray.reduce((map: Electron.MenuItemConstructorOptions, key: string) => { - map[ key ] = this.buildMenuKey(key); + map[key] = this.buildMenuKey(key); return map; }, this.menuList || {}); const template = Object.keys(this.menuList) - .map((key) => this.menuList[ key ]); + .map((key) => this.menuList[key]); this.menu = Menu.buildFromTemplate(template); logger.info(`app-menu: built menu from the provided template`); @@ -262,17 +262,17 @@ export class AppMenu { logger.info(`app-menu: building view menu`); return { label: i18n.t('View')(), - submenu: [ { + submenu: [{ accelerator: 'CmdOrCtrl+R', click: (_item, focusedWindow) => focusedWindow ? reloadWindow(focusedWindow as ICustomBrowserWindow) : null, label: i18n.t('Reload')(), }, - this.buildSeparator(), - this.assignRoleOrLabel({ role: 'resetZoom', label: i18n.t('Actual Size')() }), - this.assignRoleOrLabel({ role: 'zoomIn', label: i18n.t('Zoom In')() }), - this.assignRoleOrLabel({ role: 'zoomOut', label: i18n.t('Zoom Out')() }), - this.buildSeparator(), - this.assignRoleOrLabel({ role: 'togglefullscreen', label: i18n.t('Toggle Full Screen')() }), + this.buildSeparator(), + this.assignRoleOrLabel({ role: 'resetZoom', label: i18n.t('Actual Size')() }), + this.assignRoleOrLabel({ role: 'zoomIn', label: i18n.t('Zoom In')() }), + this.assignRoleOrLabel({ role: 'zoomOut', label: i18n.t('Zoom Out')() }), + this.buildSeparator(), + this.assignRoleOrLabel({ role: 'togglefullscreen', label: i18n.t('Toggle Full Screen')() }), ], }; } @@ -378,7 +378,7 @@ export class AppMenu { const defaultSession = session.defaultSession; if (defaultSession) { await defaultSession.clearCache(); - focusedWindow.reload(); + reloadWindow(focusedWindow as ICustomBrowserWindow); } } }, @@ -430,7 +430,7 @@ export class AppMenu { label: i18n.t('Help')(), role: 'help', submenu: - [ { + [{ click: () => shell.openExternal(i18n.t('Help Url')()), label: i18n.t('Symphony Help')(), }, { @@ -438,7 +438,7 @@ export class AppMenu { label: i18n.t('Learn More')(), }, { label: i18n.t('Troubleshooting')(), - submenu: [ { + submenu: [{ click: async () => exportLogs(), label: showLogsLabel, }, { @@ -464,7 +464,7 @@ export class AppMenu { : i18n.t('Disable GPU')(), click: () => { gpuRestartDialog(!this.disableGpu); - }, + }, }, { label: i18n.t('Enable Renderer Logs')(), @@ -482,7 +482,7 @@ export class AppMenu { await config.updateUserConfig({ enableRendererLogs }); logger.info('New value for enableRendererLogs: ' + this.enableRendererLogs); }, - } ], + }], }, { label: i18n.t('About Symphony')(), visible: isWindowsOS || isLinux, @@ -490,7 +490,7 @@ export class AppMenu { const windowName = focusedWindow ? (focusedWindow as ICustomBrowserWindow).winName : ''; windowHandler.createAboutAppWindow(windowName); }, - } ], + }], }; } @@ -517,13 +517,13 @@ export class AppMenu { } if (isMac) { - return label ? { role, label, accelerator: role ? macAccelerator[ role ] : '' } - : { role, accelerator: role ? macAccelerator[ role ] : '' }; + return label ? { role, label, accelerator: role ? macAccelerator[role] : '' } + : { role, accelerator: role ? macAccelerator[role] : '' }; } if (isWindowsOS) { - return label ? { role, label, accelerator: role ? windowsAccelerator[ role ] : '' } - : { role, accelerator: role ? windowsAccelerator[ role ] : '' }; + return label ? { role, label, accelerator: role ? windowsAccelerator[role] : '' } + : { role, accelerator: role ? windowsAccelerator[role] : '' }; } return label ? { role, label } : { role }; diff --git a/src/app/window-utils.ts b/src/app/window-utils.ts index a7fe7ebb..6d706a77 100644 --- a/src/app/window-utils.ts +++ b/src/app/window-utils.ts @@ -570,6 +570,8 @@ export const reloadWindow = (browserWindow: ICustomBrowserWindow) => { logger.info(`window-utils: reloading the main window`); browserWindow.reload(); + windowHandler.closeAllWindow(); + windowHandler.execCmd(windowHandler.screenShareIndicatorFrameUtil, []); return; From 87de146c07fb6ea654e3c0ac396a2184149ba42a Mon Sep 17 00:00:00 2001 From: psjostrom Date: Fri, 6 Nov 2020 17:17:48 +0100 Subject: [PATCH 5/7] SDA-2402 Work on state for annotate and adding color picker --- .vscode/launch.json | 52 +++- .vscode/tasks.json | 10 + package.json | 2 + src/renderer/components/color-picker-pill.tsx | 66 +++++ src/renderer/components/snipping-tool.tsx | 256 ++++++++++++++---- src/renderer/styles/snipping-tool.less | 69 +++-- 6 files changed, 382 insertions(+), 73 deletions(-) create mode 100644 .vscode/tasks.json create mode 100644 src/renderer/components/color-picker-pill.tsx diff --git a/.vscode/launch.json b/.vscode/launch.json index 27de8dd5..736e2733 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -47,7 +47,31 @@ ] }, { - "name": "mana", + "name": "build corp", + "type": "node", + "request": "launch", + "cwd": "${workspaceFolder}", + "runtimeExecutable": "${workspaceFolder}/node_modules/electron/dist/Electron.app/Contents/MacOS/Electron", + "windows": { + "runtimeExecutable": "${workspaceFolder}/node_modules/electron/dist/Electron.exe" + }, + "preLaunchTask": "build", + "args": [ + ".", + "--url=https://corporate.symphony.com" + ], + "env": { + "ELECTRON_DEBUGGING": "true", + "ELECTRON_DEV": "true" + }, + "outputCapture": "std", + "sourceMaps": true, + "outFiles": [ + "${workspaceFolder}/lib/**/*.js" + ] + }, + { + "name": "mana local", "type": "node", "request": "launch", "cwd": "${workspaceFolder}", @@ -68,7 +92,31 @@ "outFiles": [ "${workspaceFolder}/lib/**/*.js" ] - } + }, + { + "name": "build mana local", + "type": "node", + "request": "launch", + "cwd": "${workspaceFolder}", + "runtimeExecutable": "${workspaceFolder}/node_modules/electron/dist/Electron.app/Contents/MacOS/Electron", + "windows": { + "runtimeExecutable": "${workspaceFolder}/node_modules/electron/dist/Electron.exe" + }, + "preLaunchTask": "build", + "args": [ + ".", + "--url=https://local-dev.symphony.com:9090" + ], + "env": { + "ELECTRON_DEBUGGING": "true", + "ELECTRON_DEV": "true" + }, + "outputCapture": "std", + "sourceMaps": true, + "outFiles": [ + "${workspaceFolder}/lib/**/*.js" + ] + }, { "name": "demo", "type": "node", diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..62e9bd7d --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,10 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "type": "shell", + "command": "npx run-s compile browserify" + } + ] + } \ No newline at end of file diff --git a/package.json b/package.json index 31508552..37ff9504 100644 --- a/package.json +++ b/package.json @@ -150,6 +150,7 @@ "typescript": "3.9.7" }, "dependencies": { + "@types/lazy-brush": "^1.0.0", "archiver": "3.1.1", "async.map": "0.5.2", "classnames": "2.2.6", @@ -160,6 +161,7 @@ "ffi-napi": "3.0.0", "filesize": "6.1.0", "image-size": "^0.9.3", + "lazy-brush": "^1.0.1", "react": "16.13.0", "react-dom": "16.13.0", "ref-napi": "1.4.3", diff --git a/src/renderer/components/color-picker-pill.tsx b/src/renderer/components/color-picker-pill.tsx new file mode 100644 index 00000000..111a614a --- /dev/null +++ b/src/renderer/components/color-picker-pill.tsx @@ -0,0 +1,66 @@ +import * as React from 'react'; + +export interface IColorPickerPillProps { + availableColors: IColor[]; + onChange: (color: string) => void; +} + +export interface IColor { + rgbaColor: string; // Should be provided as a rgba string i.e. 'rgba(255, 0, 0, 0.3)' + outline?: string; // Should be provided as a rgba string i.e. 'rgba(255, 0, 0, 0.3)' + chosen?: boolean; +} + +const ColorPickerPill = (props: IColorPickerPillProps) => { + const getChosenColor = (colors: IColor[]) => { + return colors.find((color) => color.chosen === true); + }; + const chosenColor = getChosenColor(props.availableColors); + + const ColorDot = (color: IColor) => { + const isChosenColor = color === chosenColor; + const hasOutline = !!color.outline; + const border = 'solid 1px ' + color.outline; + + const getWidthAndHeight = () => { + if (isChosenColor) { + return hasOutline ? '22px' : '24px'; + } + return hasOutline ? '6px' : '8px'; + }; + + const widthAndHeight = getWidthAndHeight(); + + const chooseColor = () => { + props.onChange(color.rgbaColor); + }; + + return ( +
+
+
+ ); + }; + + return ( +
+ {props.availableColors.map((color) => ColorDot(color))} +
+ ); +}; + +export default ColorPickerPill; diff --git a/src/renderer/components/snipping-tool.tsx b/src/renderer/components/snipping-tool.tsx index 5e677ff2..3fa1bdbd 100644 --- a/src/renderer/components/snipping-tool.tsx +++ b/src/renderer/components/snipping-tool.tsx @@ -1,25 +1,81 @@ import { ipcRenderer } from 'electron'; import * as React from 'react'; import { i18n } from '../../common/i18n-preload'; +import ColorPickerPill, { IColor } from './color-picker-pill'; -const { useState, useEffect } = React; +const { useState, useCallback, useRef, useEffect } = React; -interface IProps { - drawEnabled: boolean; - highlightEnabled: boolean; - eraseEnabled: boolean; +enum Tool { + pen = 'PEN', + highlight = 'HIGHLIGHT', + eraser = 'ERASER', } +export interface IPath { + points: IPoint[]; + color: string; + strokeWidth: number; + shouldShow: boolean; + key: string; +} + +export interface IPoint { + x: number; + y: number; +} + +export interface ISvgPath { + svgPath: string; + key: string; + strokeWidth: number; + color: string; + shouldShow: boolean; +} + +const availablePenColors: IColor[] = [ + { rgbaColor: 'rgba(0, 0, 40, 1)' }, + { rgbaColor: 'rgba(0, 142, 255, 1)' }, + { rgbaColor: 'rgba(38, 196, 58, 1)' }, + { rgbaColor: 'rgba(246, 178, 2, 1)' }, + { rgbaColor: 'rgba(255, 255, 255, 1)', outline: 'rgba(0, 0, 0, 1)' }, +]; +const availableHighlightColors: IColor[] = [ + { rgbaColor: 'rgba(0, 142, 255, 0.64)' }, + { rgbaColor: 'rgba(38, 196, 58, 0.64)' }, + { rgbaColor: 'rgba(246, 178, 2, 0.64)' }, + { rgbaColor: 'rgba(233, 0, 0, 0.64)' }, +]; const SNIPPING_TOOL_NAMESPACE = 'ScreenSnippet'; -const SnippingTool: React.FunctionComponent = ({drawEnabled, highlightEnabled, eraseEnabled}) => { +const SnippingTool = () => { + // State and ref preparation functions const [screenSnippet, setScreenSnippet] = useState('Screen-Snippet'); - const [imageDimensions, setImageDimensions] = useState({height: 600, width: 800}); + const [imageDimensions, setImageDimensions] = useState({ + height: 600, + width: 800, + }); + const [paths, setPaths] = useState([]); + const [chosenTool, setChosenTool] = useState(Tool.pen); + const [annotateAreaLocation, setAnnotateAreaLocation] = useState({ + left: 0, + top: 0, + }); + const [penColor, setPenColor] = useState('rgba(0, 142, 255, 1)'); + const [highlightColor, setHighlightColor] = useState( + 'rgba(0, 142, 255, 0.64)', + ); + const [ + shouldRenderHighlightColorPicker, + setShouldRenderHighlightColorPicker, + ] = useState(false); + const [shouldRenderPenColorPicker, setShouldRenderPenColorPicker] = useState( + false, + ); - const getSnipImageData = (_event, {snipImage, height, width}) => { + const getSnipImageData = ({ }, { snipImage, height, width }) => { setScreenSnippet(snipImage); - setImageDimensions({height, width}); + setImageDimensions({ height, width }); }; ipcRenderer.on('snipping-tool-data', getSnipImageData); @@ -30,32 +86,114 @@ const SnippingTool: React.FunctionComponent = ({drawEnabled, highlightEn }; }, []); + const annotateRef = useCallback((domNode) => { + if (domNode) { + setAnnotateAreaLocation(domNode.getBoundingClientRect()); + } + }, []); + + // Hook that alerts clicks outside of the passed ref + const useClickOutsideExaminer = ( + colorPickerRf: React.RefObject, + penRf: React.RefObject, + highlightRf: React.RefObject, + ) => { + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + !colorPickerRf?.current?.contains(event.target as Node) && + !penRf?.current?.contains(event.target as Node) && + !highlightRf?.current?.contains(event.target as Node) + ) { + setShouldRenderHighlightColorPicker(false); + setShouldRenderPenColorPicker(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [colorPickerRf, penRf, highlightRf]); + }; + + const colorPickerRef = useRef(null); + const penRef = useRef(null); + const highlightRef = useRef(null); + useClickOutsideExaminer(colorPickerRef, penRef, highlightRef); + + // State mutating functions + + const penColorChosen = (color: string) => { + setPenColor(color); + setShouldRenderPenColorPicker(false); + }; + + const highlightColorChosen = (color: string) => { + setHighlightColor(color); + setShouldRenderHighlightColorPicker(false); + }; + const usePen = () => { - // setTool("pen"); - // setShouldRenderPenColorPicker(shouldRenderPenColorPicker => !shouldRenderPenColorPicker); - // setShouldRenderHighlightColorPicker(false); + setChosenTool(Tool.pen); + setShouldRenderPenColorPicker(!shouldRenderPenColorPicker); + setShouldRenderHighlightColorPicker(false); }; const useHighlight = () => { - // setTool("highlight"); - // setShouldRenderHighlightColorPicker(shouldRenderHighlightColorPicker => !shouldRenderHighlightColorPicker); - // setShouldRenderPenColorPicker(false); + setChosenTool(Tool.highlight); + setShouldRenderHighlightColorPicker(!shouldRenderHighlightColorPicker); + setShouldRenderPenColorPicker(false); }; const useEraser = () => { - // setTool("eraser"); + setChosenTool(Tool.eraser); }; const clear = () => { - // const updPaths = [...paths]; - // updPaths.map((p) => { - // p.shouldShow = false; - // return p; - // }); - // setPaths(updPaths); + const updPaths = [...paths]; + updPaths.map((p) => { + p.shouldShow = false; + return p; + }); + setPaths(updPaths); }; - const done = () => { + // Utility functions + + const getMousePosition = (e: React.MouseEvent) => { + return { + x: e.pageX - annotateAreaLocation.left, + y: e.pageY - annotateAreaLocation.top, + }; + }; + + const markChosenColor = (colors: IColor[], chosenColor: string) => { + return colors.map((color) => { + if (color.rgbaColor === chosenColor) { + return { ...color, chosen: true }; + } else { + return color; + } + }); + }; + + const getBorderStyle = (tool: Tool) => { + if (chosenTool !== tool) { + return undefined; + } + if (chosenTool === Tool.pen) { + return { border: '2px solid ' + penColor }; + } else if (chosenTool === Tool.highlight) { + return { border: '2px solid ' + highlightColor }; + } else if (chosenTool === Tool.eraser) { + return { border: '2px solid #008EFF' }; + } + return undefined; + }; + + const done = (e) => { + getMousePosition(e); ipcRenderer.send('upload-snippet', screenSnippet); }; @@ -64,52 +202,72 @@ const SnippingTool: React.FunctionComponent = ({drawEnabled, highlightEn
-
-
+ ; + + { + shouldRenderPenColorPicker && ( +
+
+ +
+
+ ) + } + { + shouldRenderHighlightColorPicker && ( +
+
+ +
+
+ ) + }
- {i18n.t('Screen +
+ {i18n.t('Screen +
@@ -117,7 +275,7 @@ const SnippingTool: React.FunctionComponent = ({drawEnabled, highlightEn {i18n.t('Done', SNIPPING_TOOL_NAMESPACE)()}
-
+
); }; diff --git a/src/renderer/styles/snipping-tool.less b/src/renderer/styles/snipping-tool.less index 66664507..1681c01f 100644 --- a/src/renderer/styles/snipping-tool.less +++ b/src/renderer/styles/snipping-tool.less @@ -47,40 +47,27 @@ body { max-height: 48px; .ActionButton { - margin-left: 24px; + width: 24px; + height: 24px; + margin-left: 15px; background: white; border-radius: 4px; cursor: pointer; padding: 4px 0; border: none; - - &:first-child { - margin-left: 0; - } - - img { - width: 24px; - height: 24px; - } - } - - .ActionButtonSelected { - margin-left: 24px; - background: white; - border-radius: 4px; - cursor: pointer; - padding: 4px 0; - border: 2px solid #008eff; box-sizing: border-box; border-radius: 4px; + display: flex; + justify-content: center; + align-items: center; + border: 2px solid white; &:first-child { margin-left: 0; } - img { - width: 24px; - height: 24px; + &:focus { + outline: none; } } @@ -180,3 +167,41 @@ body { } } } + +.colorPicker { + display: flex; + flex-direction: row; + margin: 4px; + align-items: center; + justify-content: center; + width: fit-content; + height: 40px; + background: #FFFFFF; + box-shadow: 0px 4px 16px rgba(15, 27, 36, 0.14), 0px 4px 8px rgba(15, 27, 36, 0.26); + border-radius: 24px; + } + +.enclosingCircle { + width: 24px; + height: 24px; + background: #FFFFFF; + border-radius: 50%; + flex: none; + order: 0; + flex-grow: 0; + margin: 8px; + cursor: pointer; + align-items: center; + justify-content: center; + display: flex; +} + +.colorDot { + border-radius: 50%; + flex: none; + order: 0; + flex-grow: 0; + width: 8px; + height: 8px; + background: #000028; +} \ No newline at end of file From 153973862a10ddc4e7f252e64b0181c5dcdd6fe7 Mon Sep 17 00:00:00 2001 From: psjostrom Date: Mon, 9 Nov 2020 09:15:07 +0100 Subject: [PATCH 6/7] SDA-2402 Updated snapshot test --- spec/__snapshots__/snippingTool.spec.ts.snap | 22 +++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/spec/__snapshots__/snippingTool.spec.ts.snap b/spec/__snapshots__/snippingTool.spec.ts.snap index ec9ce78a..d31bedbb 100644 --- a/spec/__snapshots__/snippingTool.spec.ts.snap +++ b/spec/__snapshots__/snippingTool.spec.ts.snap @@ -12,6 +12,11 @@ exports[`Snipping Tool should render correctly 1`] = `
+ ;
- Screen snippet +
+ Screen snippet +