From e7f4470d9cd8d47a26d63382192926689146f458 Mon Sep 17 00:00:00 2001 From: Kiran Niranjan Date: Wed, 20 Oct 2021 13:10:58 +0530 Subject: [PATCH] SDA-2547 (Upgrade electron version to 14.0.1) (#1267) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * SDA-2547 - Upgrade electron version to 14.0.1 * SDA-2547 - refactor and fix unit tests * SDA-2555 - Move custom title bar away from remote module * SDA-2555 - Update API new-window to setWindowOpenHandler and fix issues * SDA-2555 - Arrange imports * SDA-2555 - Fix unit tests * SDA-3387 - Fixed reload, native notification issues & finally removed the SFE CSS injection 🎉 * SDA-2547 - Fix fullscreen state on Windows * SDA-2552 - Update version info * SDA-2548 - Fix media permission * SDA-2547 - Get app name from package.json --- docs/development/DEV_SETUP.md | 6 +- package-lock.json | 476 +++++++++++++----- package.json | 6 +- spec/__mocks__/electron.ts | 49 ++ .../windowsTitleBar.spec.ts.snap | 4 +- spec/aboutApp.spec.ts | 14 +- spec/childWindowHandle.spec.ts | 58 +-- spec/windowsTitleBar.spec.ts | 141 ++---- src/app/child-window-handler.ts | 176 ++++--- src/app/crash-handler.ts | 4 +- src/app/main-api-handler.ts | 99 +++- src/app/main-event-handler.ts | 92 ++++ src/app/main.ts | 8 +- .../notifications/electron-notification.ts | 27 +- src/app/notifications/notification-helper.ts | 7 +- src/app/window-actions.ts | 18 +- src/app/window-handler.ts | 277 ++++++---- src/app/window-utils.ts | 231 ++++++++- src/common/api-interface.ts | 24 +- src/demo/index.html | 113 ++++- src/renderer/app-bridge.ts | 40 +- src/renderer/components/about-app.tsx | 15 +- src/renderer/components/loading-screen.tsx | 4 +- .../components/screen-sharing-indicator.tsx | 10 +- src/renderer/components/snack-bar.ts | 16 +- src/renderer/components/windows-title-bar.tsx | 121 +---- src/renderer/desktop-capturer.ts | 32 +- ...hendler.ts => notification-ssf-handler.ts} | 78 +-- src/renderer/preload-component.ts | 6 + src/renderer/preload-main.ts | 56 +-- src/renderer/ssf-api.ts | 40 +- 31 files changed, 1487 insertions(+), 761 deletions(-) create mode 100644 src/app/main-event-handler.ts rename src/renderer/{notification-ssf-hendler.ts => notification-ssf-handler.ts} (52%) diff --git a/docs/development/DEV_SETUP.md b/docs/development/DEV_SETUP.md index bc6e1e5d..35bdcd9a 100644 --- a/docs/development/DEV_SETUP.md +++ b/docs/development/DEV_SETUP.md @@ -1,7 +1,7 @@ ## Prerequisites ### Windows -- NodeJS version >= 12.x.y (corresponds to electron 9.x.y) +- NodeJS version >= 14.x.y (corresponds to electron 14.x.y) - Microsoft Visual Studio 2017 Community or Paid (C++ and .NET/C# development tools) - Python >= 2.7.1 - Dot Net 3.5 SP1 or later @@ -13,7 +13,7 @@ ### Mac - Xcode command line tools. Or better, Xcode latest version -- NodeJS version >= 12.x.y (corresponds to electron 9.x.y) +- NodeJS version >= 14.x.y (corresponds to electron 14.x.y) - [Sudre Packages](http://s.sudre.free.fr/Software/Packages/about.html) #### Notes @@ -35,7 +35,7 @@ npm install npm run prebuild # Run against a POD -cross-env ELECTRON_DEV=true electron . --url=https://corporate.symphony.com +npm run dev -- --url=https://corporate.symphony.com # Run the demo app npm run demo diff --git a/package-lock.json b/package-lock.json index 5610bb5e..3b3bef5b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "symphony", - "version": "9.3.0", + "version": "14.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1163,9 +1163,9 @@ } }, "@electron/get": { - "version": "1.12.4", - "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/@electron/get/-/get-1.12.4.tgz", - "integrity": "sha1-pZcRE/wb+PoSqHidwgFSpzWfBqs=", + "version": "1.13.0", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/@electron/get/-/get-1.13.0.tgz", + "integrity": "sha1-lca8r/T5pQXqRnkkJPRR7+qJIow=", "dev": true, "requires": { "debug": "^4.1.1", @@ -1179,6 +1179,12 @@ "sumchecker": "^3.0.1" } }, + "@electron/remote": { + "version": "1.2.2", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/@electron/remote/-/remote-1.2.2.tgz", + "integrity": "sha1-TDkKLmad9Hr5c8Ce7BBhYqKWwyM=", + "dev": true + }, "@felixrieseberg/spellchecker": { "version": "4.0.12", "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/@felixrieseberg/spellchecker/-/spellchecker-4.0.12.tgz", @@ -1673,9 +1679,9 @@ } }, "@types/cacheable-request": { - "version": "6.0.1", - "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/@types/cacheable-request/-/cacheable-request-6.0.1.tgz", - "integrity": "sha1-XSLz3e0f06hMC761A5p0GcLJGXY=", + "version": "6.0.2", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/@types/cacheable-request/-/cacheable-request-6.0.2.tgz", + "integrity": "sha1-wyTaAZfeCpiiMSFWU2riYkKf9rk=", "dev": true, "requires": { "@types/http-cache-semantics": "*", @@ -1755,9 +1761,9 @@ } }, "@types/http-cache-semantics": { - "version": "4.0.0", - "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/@types/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz", - "integrity": "sha1-kUB3lzaqJlVjXudW4kZ9eHz+iio=", + "version": "4.0.1", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", + "integrity": "sha1-Dqe2FJaQK5WJDcTDoRa2DLja6BI=", "dev": true }, "@types/istanbul-lib-coverage": { @@ -1792,9 +1798,9 @@ "dev": true }, "@types/keyv": { - "version": "3.1.1", - "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/@types/keyv/-/keyv-3.1.1.tgz", - "integrity": "sha1-5FpFMk/KnatxarEjDuJJyftSz6c=", + "version": "3.1.3", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/@types/keyv/-/keyv-3.1.3.tgz", + "integrity": "sha1-HJquMocuwfINza7omp87qI9GXkE=", "dev": true, "requires": { "@types/node": "*" @@ -1851,9 +1857,9 @@ "dev": true }, "@types/puppeteer": { - "version": "5.4.3", - "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/@types/puppeteer/-/puppeteer-5.4.3.tgz", - "integrity": "sha1-zcqEqndR13RI2KR32/oK8fEUhfI=", + "version": "5.4.4", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/@types/puppeteer/-/puppeteer-5.4.4.tgz", + "integrity": "sha1-6Sq+zMT0YgfD4bOJNKEka+CAzNA=", "dev": true, "requires": { "@types/node": "*" @@ -1959,9 +1965,9 @@ "dev": true }, "@types/yauzl": { - "version": "2.9.1", - "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/@types/yauzl/-/yauzl-2.9.1.tgz", - "integrity": "sha1-0Q9p+fUi7vPPmOMK+2hKHh7JI68=", + "version": "2.9.2", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/@types/yauzl/-/yauzl-2.9.2.tgz", + "integrity": "sha1-xI5dVq/xREQJ45+hZLC01FUqe3o=", "dev": true, "optional": true, "requires": { @@ -1992,15 +1998,15 @@ }, "dependencies": { "ansi-regex": { - "version": "5.0.0", - "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha1-OIU59VF5vzkznIGvMKZU1p+Hy3U=", + "version": "5.0.1", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha1-CCyyyJyf6GWaMRpTvWpNxTAdswQ=", "dev": true }, "chalk": { - "version": "4.1.0", - "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha1-ThSHCmGNni7dl92DRf2dncMVZGo=", + "version": "4.1.2", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha1-qsTit3NKdAhnrrFr8CqtVWoeegE=", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -2008,12 +2014,12 @@ } }, "strip-ansi": { - "version": "6.0.0", - "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha1-CxVx3XZpzNTz4G4U7x7tJiJa5TI=", + "version": "6.0.1", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha1-nibGPTD1NEPpSJSVshBdN7Z6hdk=", "dev": true, "requires": { - "ansi-regex": "^5.0.0" + "ansi-regex": "^5.0.1" } } } @@ -3525,9 +3531,9 @@ "dev": true }, "boolean": { - "version": "3.0.2", - "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/boolean/-/boolean-3.0.2.tgz", - "integrity": "sha1-3xuqGLaisOcIQEdeHZPsj+dbJXA=", + "version": "3.1.4", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/boolean/-/boolean-3.1.4.tgz", + "integrity": "sha1-9RovtYOKmeBvm27B7bZ03mcCZDU=", "dev": true, "optional": true }, @@ -4121,9 +4127,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001203", - "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/caniuse-lite/-/caniuse-lite-1.0.30001203.tgz", - "integrity": "sha1-p6NN8ho4fZ3v/NVsAAuM9atUBYA=", + "version": "1.0.30001265", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/caniuse-lite/-/caniuse-lite-1.0.30001265.tgz", + "integrity": "sha1-BhPJ5ski5CJ5Lm/O/fmjr+7k+MM=", "dev": true }, "capture-exit": { @@ -4732,9 +4738,9 @@ } }, "config-chain": { - "version": "1.1.12", - "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/config-chain/-/config-chain-1.1.12.tgz", - "integrity": "sha1-D96NCRIA616AjK8l/mGMAvSOTvo=", + "version": "1.1.13", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha1-+tB5Wqamza/57Rto6d/5Q3LCMvQ=", "dev": true, "optional": true, "requires": { @@ -5566,9 +5572,9 @@ "dev": true }, "detect-node": { - "version": "2.0.5", - "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/detect-node/-/detect-node-2.0.5.tgz", - "integrity": "sha1-nScKp+qlrwtyxMnZuBTn9M5zi3k=", + "version": "2.1.0", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha1-yccHdaScPQO8LAbZpzvlUPl4+LE=", "dev": true, "optional": true }, @@ -5930,14 +5936,22 @@ } }, "electron": { - "version": "9.4.1", - "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/electron/-/electron-9.4.1.tgz", - "integrity": "sha1-YqKq5M2T8bVteUpHVBUFpxZUF3o=", + "version": "14.1.1", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/electron/-/electron-14.1.1.tgz", + "integrity": "sha1-ZDcm/h/UrXf7s6dbIRAF7QE1dIU=", "dev": true, "requires": { "@electron/get": "^1.0.1", - "@types/node": "^12.0.12", + "@types/node": "^14.6.2", "extract-zip": "^1.0.3" + }, + "dependencies": { + "@types/node": { + "version": "14.17.19", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/@types/node/-/node-14.17.19.tgz", + "integrity": "sha1-c0HprBtddI16PdwEM27VNqb5HDE=", + "dev": true + } } }, "electron-builder": { @@ -6235,12 +6249,12 @@ } }, "electron-chromedriver": { - "version": "9.0.0", - "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/electron-chromedriver/-/electron-chromedriver-9.0.0.tgz", - "integrity": "sha1-x2Kf5rlyEUDzo4AUT5mWDCvDtcE=", + "version": "13.0.0", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/electron-chromedriver/-/electron-chromedriver-13.0.0.tgz", + "integrity": "sha1-pVOvd0MhWsRj4eQODbFNSlQu92I=", "dev": true, "requires": { - "@electron/get": "^1.12.2", + "@electron/get": "^1.12.4", "extract-zip": "^2.0.0" }, "dependencies": { @@ -6480,6 +6494,15 @@ "path-exists": "^3.0.0" } }, + "node-abi": { + "version": "2.30.1", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/node-abi/-/node-abi-2.30.1.tgz", + "integrity": "sha1-xDfUsf4OKFqvKQ1FtF1Nev7axM8=", + "dev": true, + "requires": { + "semver": "^5.4.1" + } + }, "p-locate": { "version": "3.0.0", "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/p-locate/-/p-locate-3.0.0.tgz", @@ -6504,6 +6527,12 @@ "tslib": "^1.9.0" } }, + "semver": { + "version": "5.7.1", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/semver/-/semver-5.7.1.tgz", + "integrity": "sha1-qVT5Ma66UI0we78Gnv8MAclhFvc=", + "dev": true + }, "spawn-rx": { "version": "3.0.0", "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/spawn-rx/-/spawn-rx-3.0.0.tgz", @@ -6589,8 +6618,8 @@ } }, "electron-spellchecker": { - "version": "git+https://github.com/symphonyoss/electron-spellchecker.git#8628a0e62660bc23da969fc19c9e9b39eb54be5a", - "from": "git+https://github.com/symphonyoss/electron-spellchecker.git#v2.3.2", + "version": "git+ssh://git@github.com/symphonyoss/electron-spellchecker.git#8628a0e62660bc23da969fc19c9e9b39eb54be5a", + "from": "electron-spellchecker@git+https://github.com/symphonyoss/electron-spellchecker.git#v2.3.2", "requires": { "@aabuhijleh/electron-remote": "^1.4.0", "@felixrieseberg/spellchecker": "^4.0.12", @@ -8415,9 +8444,9 @@ } }, "global-agent": { - "version": "2.1.12", - "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/global-agent/-/global-agent-2.1.12.tgz", - "integrity": "sha1-5K44Ercxqegcv4Jfk3fvRQqOQZU=", + "version": "2.2.0", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/global-agent/-/global-agent-2.2.0.tgz", + "integrity": "sha1-VmMxsGRua/eUKaFod2hcSh+/dtw=", "dev": true, "optional": true, "requires": { @@ -8431,9 +8460,9 @@ }, "dependencies": { "core-js": { - "version": "3.9.1", - "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/core-js/-/core-js-3.9.1.tgz", - "integrity": "sha1-zsjeWT246yqF/7Db3rMSy25UYK4=", + "version": "3.18.1", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/core-js/-/core-js-3.18.1.tgz", + "integrity": "sha1-KJ1L4s4AhdQPwSRMCxpUwARUYi8=", "dev": true, "optional": true }, @@ -8465,9 +8494,9 @@ } }, "semver": { - "version": "7.3.4", - "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/semver/-/semver-7.3.4.tgz", - "integrity": "sha1-J6qn0uTKdkUvmNOt0JOnLJQ+3Jc=", + "version": "7.3.5", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/semver/-/semver-7.3.5.tgz", + "integrity": "sha1-C2Ich5NI2JmOSw5L6Us/EuYBjvc=", "dev": true, "optional": true, "requires": { @@ -11414,13 +11443,13 @@ } }, "lighthouse-logger": { - "version": "1.2.0", - "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/lighthouse-logger/-/lighthouse-logger-1.2.0.tgz", - "integrity": "sha1-t21Wk16cE36GoEdB9rubJ3bohso=", + "version": "1.3.0", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/lighthouse-logger/-/lighthouse-logger-1.3.0.tgz", + "integrity": "sha1-umMD5zkwfE7uGPCCSVJOfa/VENs=", "dev": true, "requires": { - "debug": "^2.6.8", - "marky": "^1.2.0" + "debug": "^2.6.9", + "marky": "^1.2.2" }, "dependencies": { "debug": { @@ -11755,9 +11784,9 @@ } }, "marky": { - "version": "1.2.1", - "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/marky/-/marky-1.2.1.tgz", - "integrity": "sha1-o/z4L/01d1a4uK/+yf2/OjDcGwI=", + "version": "1.2.2", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/marky/-/marky-1.2.2.tgz", + "integrity": "sha1-RFZ2W03jB6E9JjppsMeb8ibmgyM=", "dev": true }, "matchdep": { @@ -12348,18 +12377,36 @@ } }, "node-abi": { - "version": "2.21.0", - "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/node-abi/-/node-abi-2.21.0.tgz", - "integrity": "sha1-wtyeutb09T1uqbUx57j6rYEEHUg=", + "version": "3.2.0", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/node-abi/-/node-abi-3.2.0.tgz", + "integrity": "sha1-yOxodPgItNpfvVbpUGOQzmWxUqI=", "dev": true, "requires": { - "semver": "^5.4.1" + "semver": "^7.3.5" }, "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha1-bW/mVw69lqr5D8rR2vo7JWbbOpQ=", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, "semver": { - "version": "5.7.1", - "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/semver/-/semver-5.7.1.tgz", - "integrity": "sha1-qVT5Ma66UI0we78Gnv8MAclhFvc=", + "version": "7.3.5", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/semver/-/semver-7.3.5.tgz", + "integrity": "sha1-C2Ich5NI2JmOSw5L6Us/EuYBjvc=", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha1-m7knkNnA7/7GO+c1GeEaNQGaOnI=", "dev": true } } @@ -12370,10 +12417,37 @@ "integrity": "sha1-gTJeCiEXeJwBKNq2Xn448HzroWE=" }, "node-fetch": { - "version": "2.6.1", - "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha1-BFvTI2Mfdu0uK1VXM5RBa2OaAFI=", - "dev": true + "version": "2.6.5", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/node-fetch/-/node-fetch-2.6.5.tgz", + "integrity": "sha1-QnNVN9fwgKfl94tsVJtxRr4XQv0=", + "dev": true, + "requires": { + "whatwg-url": "^5.0.0" + }, + "dependencies": { + "tr46": { + "version": "0.0.3", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", + "dev": true + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=", + "dev": true + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "dev": true, + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + } + } }, "node-gyp": { "version": "6.1.0", @@ -13584,6 +13658,23 @@ "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0", "which-pm-runs": "^1.0.0" + }, + "dependencies": { + "node-abi": { + "version": "2.30.1", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/node-abi/-/node-abi-2.30.1.tgz", + "integrity": "sha1-xDfUsf4OKFqvKQ1FtF1Nev7axM8=", + "dev": true, + "requires": { + "semver": "^5.4.1" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/semver/-/semver-5.7.1.tgz", + "integrity": "sha1-qVT5Ma66UI0we78Gnv8MAclhFvc=", + "dev": true + } } }, "prelude-ls": { @@ -14593,9 +14684,9 @@ } }, "resolve-alpn": { - "version": "1.0.0", - "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/resolve-alpn/-/resolve-alpn-1.0.0.tgz", - "integrity": "sha1-dFrWCz1q/0tKSOAbjAvccJWeDow=", + "version": "1.2.1", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha1-t629rDVGqq7CC0Xn2CZZJwcnJvk=", "dev": true }, "resolve-cwd": { @@ -14648,9 +14739,9 @@ } }, "resq": { - "version": "1.10.0", - "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/resq/-/resq-1.10.0.tgz", - "integrity": "sha1-QLXjUV/5hGaOa2t8JAHygrCAQuo=", + "version": "1.10.1", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/resq/-/resq-1.10.1.tgz", + "integrity": "sha1-wF0bOAgBbM7sTUhc6zday0lWX1M=", "dev": true, "requires": { "fast-deep-equal": "^2.0.1" @@ -14752,6 +14843,23 @@ "nan": "^2.14.0", "node-abi": "^2.13.0", "prebuild-install": "^5.3.3" + }, + "dependencies": { + "node-abi": { + "version": "2.30.1", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/node-abi/-/node-abi-2.30.1.tgz", + "integrity": "sha1-xDfUsf4OKFqvKQ1FtF1Nev7axM8=", + "dev": true, + "requires": { + "semver": "^5.4.1" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/semver/-/semver-5.7.1.tgz", + "integrity": "sha1-qVT5Ma66UI0we78Gnv8MAclhFvc=", + "dev": true + } } }, "rst-selector-parser": { @@ -15042,16 +15150,16 @@ } }, "screen-share-indicator-frame": { - "version": "git+https://github.com/symphonyoss/ScreenShareIndicatorFrame.git#e943ec141899d8cf7301f4bd3f91a1434e1ceb10", - "from": "screen-share-indicator-frame@git+https://github.com/symphonyoss/ScreenShareIndicatorFrame.git#e943ec141899d8cf7301f4bd3f91a1434e1ceb10", + "version": "git+ssh://git@github.com/symphonyoss/ScreenShareIndicatorFrame.git#e943ec141899d8cf7301f4bd3f91a1434e1ceb10", + "from": "screen-share-indicator-frame@git+https://github.com/symphonyoss/ScreenShareIndicatorFrame.git#v1.4.10", "optional": true, "requires": { "run-script-os": "1.0.7" } }, "screen-snippet": { - "version": "git+https://github.com/symphonyoss/ScreenSnippet2.git#889aedbd3ecf16320a387967aaee0e7ca992d717", - "from": "screen-snippet@git+https://github.com/symphonyoss/ScreenSnippet2.git#889aedbd3ecf16320a387967aaee0e7ca992d717", + "version": "git+ssh://git@github.com/symphonyoss/ScreenSnippet2.git#889aedbd3ecf16320a387967aaee0e7ca992d717", + "from": "screen-snippet@git+https://github.com/symphonyoss/ScreenSnippet2.git#v2.4.0", "optional": true }, "semver": { @@ -15527,16 +15635,140 @@ "dev": true }, "spectron": { - "version": "11.1.0", - "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/spectron/-/spectron-11.1.0.tgz", - "integrity": "sha1-7k8RyQV/bXkJTy1ETrpVXMvmOWU=", + "version": "15.0.0", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/spectron/-/spectron-15.0.0.tgz", + "integrity": "sha1-nA4lSyvj8HJagbg0MJK50BCEOcc=", "dev": true, "requires": { + "@electron/remote": "^1.1.0", "dev-null": "^0.1.1", - "electron-chromedriver": "^9.0.0", - "request": "^2.87.0", - "split": "^1.0.0", - "webdriverio": "^6.1.20" + "electron-chromedriver": "^13.0.0", + "got": "^11.8.0", + "split": "^1.0.1", + "webdriverio": "^6.9.1" + }, + "dependencies": { + "@sindresorhus/is": { + "version": "4.2.0", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/@sindresorhus/is/-/is-4.2.0.tgz", + "integrity": "sha1-Znv8YYaufJ4LRaCJYMVRQ3F24co=", + "dev": true + }, + "@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha1-tKkUu2LnwnLU5Zif5EQPgSqx2Ac=", + "dev": true, + "requires": { + "defer-to-connect": "^2.0.0" + } + }, + "cacheable-request": { + "version": "7.0.2", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/cacheable-request/-/cacheable-request-7.0.2.tgz", + "integrity": "sha1-6g0LiJNkolhUdXMByhKy2nf5HSc=", + "dev": true, + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + } + }, + "decompress-response": { + "version": "6.0.0", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha1-yjh2Et234QS9FthaqwDV7PCcZvw=", + "dev": true, + "requires": { + "mimic-response": "^3.1.0" + } + }, + "defer-to-connect": { + "version": "2.0.1", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha1-gBa9tBQ+RjK3ejRJxiNid95SBYc=", + "dev": true + }, + "get-stream": { + "version": "5.2.0", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha1-SWaheV7lrOZecGxLe+txJX1uItM=", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "got": { + "version": "11.8.2", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/got/-/got-11.8.2.tgz", + "integrity": "sha1-ers5Weoowx81dvFXbB7/ziPzNZk=", + "dev": true, + "requires": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.1", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + } + }, + "json-buffer": { + "version": "3.0.1", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha1-kziAKjDTtmBfvgYT4JQAjKjAWhM=", + "dev": true + }, + "keyv": { + "version": "4.0.3", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/keyv/-/keyv-4.0.3.tgz", + "integrity": "sha1-TzqpjeJUgDyvzSiWc0EI2qNeQlQ=", + "dev": true, + "requires": { + "json-buffer": "3.0.1" + } + }, + "lowercase-keys": { + "version": "2.0.0", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha1-JgPni3tLAAbLyi+8yKMgJVislHk=", + "dev": true + }, + "mimic-response": { + "version": "3.1.0", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha1-LR1Zr5wbEpgVrMwsRqAipc4fo8k=", + "dev": true + }, + "normalize-url": { + "version": "6.1.0", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha1-QNCIW1Nd7/4/MUe+yHfQX+TFZoo=", + "dev": true + }, + "p-cancelable": { + "version": "2.1.1", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha1-qrf71BZYL6MqPbSYWcEiSHxe0s8=", + "dev": true + }, + "responselike": { + "version": "2.0.0", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/responselike/-/responselike-2.0.0.tgz", + "integrity": "sha1-JjkbzDF091D5p56sxAoSpcQtdyM=", + "dev": true, + "requires": { + "lowercase-keys": "^2.0.0" + } + } } }, "split": { @@ -16853,9 +17085,9 @@ "dev": true }, "ua-parser-js": { - "version": "0.7.24", - "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/ua-parser-js/-/ua-parser-js-0.7.24.tgz", - "integrity": "sha1-jT7OpG7U8fHWPsJfF9hWgQXcAnw=", + "version": "0.7.28", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/ua-parser-js/-/ua-parser-js-0.7.28.tgz", + "integrity": "sha1-i6BOZT81ziECOcZGYWhb+RId7DE=", "dev": true }, "uglify-js": { @@ -17447,24 +17679,24 @@ }, "dependencies": { "@sindresorhus/is": { - "version": "4.0.0", - "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/@sindresorhus/is/-/is-4.0.0.tgz", - "integrity": "sha1-L/Z06WEbRbUoiW2CDT16gS3i8OQ=", + "version": "4.2.0", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/@sindresorhus/is/-/is-4.2.0.tgz", + "integrity": "sha1-Znv8YYaufJ4LRaCJYMVRQ3F24co=", "dev": true }, "@szmarczak/http-timer": { - "version": "4.0.5", - "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/@szmarczak/http-timer/-/http-timer-4.0.5.tgz", - "integrity": "sha1-v71QIR6d+lG6B9pYoUzf0zMgUVI=", + "version": "4.0.6", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha1-tKkUu2LnwnLU5Zif5EQPgSqx2Ac=", "dev": true, "requires": { "defer-to-connect": "^2.0.0" } }, "cacheable-request": { - "version": "7.0.1", - "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/cacheable-request/-/cacheable-request-7.0.1.tgz", - "integrity": "sha1-BiAxwoViMngu1pSiV/o12pOUKlg=", + "version": "7.0.2", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/cacheable-request/-/cacheable-request-7.0.2.tgz", + "integrity": "sha1-6g0LiJNkolhUdXMByhKy2nf5HSc=", "dev": true, "requires": { "clone-response": "^1.0.2", @@ -17472,7 +17704,7 @@ "http-cache-semantics": "^4.0.0", "keyv": "^4.0.0", "lowercase-keys": "^2.0.0", - "normalize-url": "^4.1.0", + "normalize-url": "^6.0.1", "responselike": "^2.0.0" } }, @@ -17546,10 +17778,16 @@ "integrity": "sha1-LR1Zr5wbEpgVrMwsRqAipc4fo8k=", "dev": true }, + "normalize-url": { + "version": "6.1.0", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha1-QNCIW1Nd7/4/MUe+yHfQX+TFZoo=", + "dev": true + }, "p-cancelable": { - "version": "2.1.0", - "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/p-cancelable/-/p-cancelable-2.1.0.tgz", - "integrity": "sha1-TVHDuR9IPQKg0wB2UyH8o5PXWN0=", + "version": "2.1.1", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha1-qrf71BZYL6MqPbSYWcEiSHxe0s8=", "dev": true }, "responselike": { @@ -17610,19 +17848,19 @@ } }, "async": { - "version": "3.2.0", - "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/async/-/async-3.2.0.tgz", - "integrity": "sha1-s6JoXF67ZB094C0WEALGD8n4VyA=", + "version": "3.2.1", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/async/-/async-3.2.1.tgz", + "integrity": "sha1-0ydOxm0QekdHakxJE2qs2wBmX8g=", "dev": true }, "compress-commons": { - "version": "4.1.0", - "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/compress-commons/-/compress-commons-4.1.0.tgz", - "integrity": "sha1-Jex6RSiFLM0dRBp9Q1PNDs4RNxs=", + "version": "4.1.1", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/compress-commons/-/compress-commons-4.1.1.tgz", + "integrity": "sha1-3yoJp+0XRHZCutEKhcyaGeXEKn0=", "dev": true, "requires": { "buffer-crc32": "^0.2.13", - "crc32-stream": "^4.0.1", + "crc32-stream": "^4.0.2", "normalize-path": "^3.0.0", "readable-stream": "^3.6.0" } diff --git a/package.json b/package.json index c5019e6a..aec54548 100644 --- a/package.json +++ b/package.json @@ -126,7 +126,7 @@ "browserify": "16.5.1", "cross-env": "5.2.0", "del": "3.0.0", - "electron": "9.4.1", + "electron": "14.1.1", "electron-builder": "22.7.0", "electron-builder-squirrel-windows": "20.38.3", "electron-icon-maker": "0.0.4", @@ -144,13 +144,13 @@ "jest-html-reporter": "3.0.0", "less": "3.8.1", "ncp": "2.0.0", - "node-abi": "^2.17.0", + "node-abi": "^3.2.0", "npm-run-all": "4.1.5", "prettier": "2.2.1", "pretty-quick": "^3.1.0", "robotjs": "0.6.0", "run-script-os": "1.0.7", - "spectron": "^11.0.0", + "spectron": "^15.0.0", "ts-jest": "25.3.0", "tslint": "5.11.0", "tslint-config-prettier": "^1.18.0", diff --git a/spec/__mocks__/electron.ts b/spec/__mocks__/electron.ts index ce1f878e..f8379263 100644 --- a/spec/__mocks__/electron.ts +++ b/spec/__mocks__/electron.ts @@ -31,6 +31,7 @@ interface ILoginItemSettings { } interface IIpcMain { on(event: any, cb: any): void; + handle(event: any, cb: any): Promise; send(event: any, cb: any): void; } interface IIpcRenderer { @@ -40,6 +41,14 @@ interface IIpcRenderer { removeListener(eventName: any, cb: any): void; once(eventName: any, cb: any): void; } +interface IWebContents { + setWindowOpenHandler(details: any): any; + sendSync(event: any, cb: any): any; + on(eventName: any, cb: any): void; + send(event: any, ...cb: any[]): void; + removeListener(eventName: any, cb: any): void; + once(eventName: any, cb: any): void; +} interface IPowerMonitor { getSystemIdleTime(): void; } @@ -92,6 +101,10 @@ export const ipcMain: IIpcMain = { on: (event, cb) => { ipcEmitter.on(event, cb); }, + handle: (event, cb) => { + ipcEmitter.on(event, cb); + return Promise.resolve(); + }, send: (event, args) => { const senderEvent = { sender: { @@ -141,6 +154,42 @@ export const ipcRenderer: IIpcRenderer = { }, }; +export const webContents: IWebContents = { + setWindowOpenHandler: (_details: {}) => { + return { action: 'allow' }; + }, + sendSync: (event, args) => { + const listeners = ipcEmitter.listeners(event); + if (listeners.length > 0) { + const listener = listeners[0]; + const eventArg = {}; + listener(eventArg, args); + return eventArg; + } + return null; + }, + send: (event, ...args) => { + const senderEvent = { + sender: { + send: (eventSend, ...arg) => { + ipcEmitter.emit(eventSend, ...arg); + }, + }, + preventDefault: jest.fn(), + }; + ipcEmitter.emit(event, senderEvent, ...args); + }, + on: (eventName, cb) => { + ipcEmitter.on(eventName, cb); + }, + removeListener: (eventName, cb) => { + ipcEmitter.removeListener(eventName, cb); + }, + once: (eventName, cb) => { + ipcEmitter.on(eventName, cb); + }, +}; + export const shell = { openExternal: jest.fn(), }; diff --git a/spec/__snapshots__/windowsTitleBar.spec.ts.snap b/spec/__snapshots__/windowsTitleBar.spec.ts.snap index 06a66aff..9953f599 100644 --- a/spec/__snapshots__/windowsTitleBar.spec.ts.snap +++ b/spec/__snapshots__/windowsTitleBar.spec.ts.snap @@ -119,7 +119,7 @@ exports[`windows title bar should render correctly 1`] = ` onClick={[Function]} onContextMenu={[Function]} onMouseDown={[Function]} - title="Maximize" + title="Restore" > diff --git a/spec/aboutApp.spec.ts b/spec/aboutApp.spec.ts index f315c081..84dbd268 100644 --- a/spec/aboutApp.spec.ts +++ b/spec/aboutApp.spec.ts @@ -1,7 +1,8 @@ import { shallow } from 'enzyme'; import * as React from 'react'; +import { apiCmds } from '../src/common/api-interface'; import AboutApp from '../src/renderer/components/about-app'; -import { ipcRenderer, remote } from './__mocks__/electron'; +import { ipcRenderer } from './__mocks__/electron'; describe('about app', () => { const aboutAppDataLabel = 'about-app-data'; @@ -32,6 +33,7 @@ describe('about app', () => { swiftSearchSupportedVersion: 'N/A', }; const onLabelEvent = 'on'; + const ipcSendEvent = 'send'; const removeListenerLabelEvent = 'removeListener'; it('should render correctly', () => { @@ -62,12 +64,16 @@ describe('about app', () => { }); it('should copy the correct data on to clipboard', () => { - const spyMount = jest.spyOn(remote.clipboard, 'write'); + const spyIpc = jest.spyOn(ipcRenderer, ipcSendEvent); const wrapper = shallow(React.createElement(AboutApp)); ipcRenderer.send('about-app-data', aboutDataMock); const copyButtonSelector = `button.AboutApp-copy-button[title="Copy all the version information in this page"]`; wrapper.find(copyButtonSelector).simulate('click'); - const expectedData = { text: JSON.stringify(aboutDataMock, null, 4) }; - expect(spyMount).toBeCalledWith(expectedData, 'clipboard'); + const expectedData = { + cmd: apiCmds.aboutAppClipBoardData, + clipboard: aboutDataMock, + clipboardType: 'clipboard', + }; + expect(spyIpc).toBeCalledWith('symphony-api', expectedData); }); }); diff --git a/spec/childWindowHandle.spec.ts b/spec/childWindowHandle.spec.ts index 131e2928..47e398b3 100644 --- a/spec/childWindowHandle.spec.ts +++ b/spec/childWindowHandle.spec.ts @@ -1,8 +1,6 @@ import { handleChildWindow } from '../src/app/child-window-handler'; -import { config } from '../src/app/config-handler'; -import { windowHandler } from '../src/app/window-handler'; -import { injectStyles } from '../src/app/window-utils'; -import { ipcRenderer } from './__mocks__/electron'; +import { webContents } from './__mocks__/electron'; +import anything = jasmine.anything; const getMainWindow = { isDestroyed: jest.fn(() => false), @@ -69,52 +67,16 @@ jest.mock('../src/common/logger', () => { }); describe('child window handle', () => { - const frameName = {}; - const disposition = 'new-window'; - const newWinOptions = { - webPreferences: jest.fn(), - webContents: { ...ipcRenderer, ...getMainWindow, webContents: ipcRenderer }, - }; + it('should set open window handler', () => { + const spy = jest.spyOn(webContents, 'setWindowOpenHandler'); - it('should call `did-start-loading` correctly on WindowOS', () => { - const newWinUrl = 'about:blank'; - const args = [newWinUrl, frameName, disposition, newWinOptions]; - const spy = jest.spyOn(getMainWindow, 'setMenuBarVisibility'); - handleChildWindow(ipcRenderer as any); - ipcRenderer.send('new-window', ...args); - ipcRenderer.send('did-start-loading'); - expect(spy).toBeCalledWith(false); + handleChildWindow(webContents as any); + expect(spy).toBeCalledWith(expect.any(Function)); }); - it('should call `did-finish-load` correctly on WindowOS', () => { - config.getGlobalConfigFields = jest.fn(() => { - return { - url: 'https://foundation-dev.symphony.com', - }; - }); - const newWinUrl = 'about:blank'; - const args = [newWinUrl, frameName, disposition, newWinOptions]; - const spy = jest.spyOn(newWinOptions.webContents.webContents, 'send'); - handleChildWindow(ipcRenderer as any); - ipcRenderer.send('new-window', ...args); - ipcRenderer.send('did-finish-load'); - expect(spy).lastCalledWith('page-load', { - enableCustomTitleBar: false, - isMainWindow: false, - isWindowsOS: true, - locale: 'en-US', - origin: 'https://foundation-dev.symphony.com', - resources: {}, - }); - expect(injectStyles).toBeCalled(); - }); - - it('should call `windowHandler.openUrlInDefaultBrowser` when url in invalid', () => { - const newWinUrl = 'invalid'; - const args = [newWinUrl, frameName, disposition, newWinOptions]; - const spy = jest.spyOn(windowHandler, 'openUrlInDefaultBrowser'); - handleChildWindow(ipcRenderer as any); - ipcRenderer.send('new-window', ...args); - expect(spy).not.toBeCalledWith('invalid'); + it('should trigger did-create-window', () => { + const spy = jest.spyOn(webContents, 'on'); + handleChildWindow(webContents as any); + expect(spy).toBeCalledWith('did-create-window', anything()); }); }); diff --git a/spec/windowsTitleBar.spec.ts b/spec/windowsTitleBar.spec.ts index a2d5ffc6..342a68ba 100644 --- a/spec/windowsTitleBar.spec.ts +++ b/spec/windowsTitleBar.spec.ts @@ -1,7 +1,8 @@ import { shallow } from 'enzyme'; import * as React from 'react'; +import { apiCmds } from '../src/common/api-interface'; import WindowsTitleBar from '../src/renderer/components/windows-title-bar'; -import { ipcRenderer, remote } from './__mocks__/electron'; +import { ipcRenderer } from './__mocks__/electron'; // @ts-ignore global.MutationObserver = jest.fn().mockImplementation(() => ({ @@ -9,33 +10,11 @@ global.MutationObserver = jest.fn().mockImplementation(() => ({ disconnect: jest.fn(), })); +// TODO: Fix tests describe('windows title bar', () => { - beforeEach(() => { - // state initial - jest.spyOn(remote, 'getCurrentWindow').mockImplementation(() => { - return { - isFullScreen: jest.fn(() => { - return false; - }), - isMaximized: jest.fn(() => { - return false; - }), - on: jest.fn(), - removeListener: jest.fn(), - isDestroyed: jest.fn(() => { - return false; - }), - close: jest.fn(), - maximize: jest.fn(), - minimize: jest.fn(), - unmaximize: jest.fn(), - setFullScreen: jest.fn(), - }; - }); - }); - - const getCurrentWindowFnLabel = 'getCurrentWindow'; const onEventLabel = 'on'; + const sendEventLabel = 'send'; + const apiName = 'symphony-api'; const maximizeEventLabel = 'maximize'; const unmaximizeEventLabel = 'unmaximize'; const enterFullScreenEventLabel = 'enter-full-screen'; @@ -47,28 +26,19 @@ describe('windows title bar', () => { }); it('should mount correctly', () => { + const spy = jest.spyOn(ipcRenderer, onEventLabel); const wrapper = shallow(React.createElement(WindowsTitleBar)); const instance: any = wrapper.instance(); - const window = instance.window; - const spy = jest.spyOn(remote, getCurrentWindowFnLabel); - const spyWindow = jest.spyOn(window, onEventLabel); + instance.updateState({ isMaximized: false }); expect(spy).toBeCalled(); - expect(spyWindow).nthCalledWith( - 1, - maximizeEventLabel, - expect.any(Function), - ); - expect(spyWindow).nthCalledWith( - 2, - unmaximizeEventLabel, - expect.any(Function), - ); - expect(spyWindow).nthCalledWith( + expect(spy).nthCalledWith(1, maximizeEventLabel, expect.any(Function)); + expect(spy).nthCalledWith(2, unmaximizeEventLabel, expect.any(Function)); + expect(spy).nthCalledWith( 3, enterFullScreenEventLabel, expect.any(Function), ); - expect(spyWindow).nthCalledWith( + expect(spy).nthCalledWith( 4, leaveFullScreenEventLabel, expect.any(Function), @@ -76,27 +46,27 @@ describe('windows title bar', () => { }); it('should call `close` correctly', () => { - const fnLabel = 'close'; const titleLabel = 'Close'; const wrapper = shallow(React.createElement(WindowsTitleBar)); const customSelector = `button.title-bar-button[title="${titleLabel}"]`; - const instance: any = wrapper.instance(); - const window = instance.window; - const spy = jest.spyOn(window, fnLabel); + const cmd = { + cmd: apiCmds.closeMainWindow, + }; + const spy = jest.spyOn(ipcRenderer, sendEventLabel); wrapper.find(customSelector).simulate('click'); - expect(spy).toBeCalled(); + expect(spy).toBeCalledWith(apiName, cmd); }); it('should call `minimize` correctly', () => { - const fnLabel = 'minimize'; const titleLabel = 'Minimize'; const wrapper = shallow(React.createElement(WindowsTitleBar)); const customSelector = `button.title-bar-button[title="${titleLabel}"]`; - const instance: any = wrapper.instance(); - const window = instance.window; - const spy = jest.spyOn(window, fnLabel); + const spy = jest.spyOn(ipcRenderer, sendEventLabel); + const cmd = { + cmd: apiCmds.minimizeMainWindow, + }; wrapper.find(customSelector).simulate('click'); - expect(spy).toBeCalled(); + expect(spy).toBeCalledWith(apiName, cmd); }); it('should call `showMenu` correctly', () => { @@ -132,82 +102,47 @@ describe('windows title bar', () => { expect(spy).lastCalledWith(expect.any(Function)); }); - describe('componentDidMount event', () => { - beforeEach(() => { - document.body.innerHTML = `
`; - }); - - it('should call `componentDidMount` when isFullScreen', () => { - const spy = jest.spyOn(document.body.style, 'removeProperty'); - const expectedValue = 'margin-top'; - // changing state before componentDidMount - jest.spyOn(remote, 'getCurrentWindow').mockImplementation(() => { - return { - isFullScreen: jest.fn(() => { - return true; - }), - isMaximized: jest.fn(() => { - return false; - }), - on: jest.fn(), - removeListener: jest.fn(), - isDestroyed: jest.fn(() => { - return false; - }), - close: jest.fn(), - maximize: jest.fn(), - minimize: jest.fn(), - unmaximize: jest.fn(), - setFullScreen: jest.fn(), - }; - }); - shallow(React.createElement(WindowsTitleBar)); - expect(spy).toBeCalledWith(expectedValue); - }); - }); - describe('maximize functions', () => { it('should call `unmaximize` correctly when is not full screen', () => { const titleLabel = 'Restore'; - const unmaximizeFn = 'unmaximize'; + const cmd = { + cmd: apiCmds.unmaximizeMainWindow, + }; const customSelector = `button.title-bar-button[title="${titleLabel}"]`; const wrapper = shallow(React.createElement(WindowsTitleBar)); - const instance: any = wrapper.instance(); - const window = instance.window; - const spy = jest.spyOn(window, unmaximizeFn); + const spy = jest.spyOn(ipcRenderer, sendEventLabel); wrapper.setState({ isMaximized: true }); wrapper.find(customSelector).simulate('click'); - expect(spy).toBeCalled(); + expect(spy).toBeCalledWith(apiName, cmd); }); it('should call `unmaximize` correctly when is full screen', () => { - const windowSpyFn = 'setFullScreen'; const titleLabel = 'Restore'; const customSelector = `button.title-bar-button[title="${titleLabel}"]`; const wrapper = shallow(React.createElement(WindowsTitleBar)); - const instance: any = wrapper.instance(); - const window = instance.window; - const spy = jest.spyOn(window, windowSpyFn); - window.isFullScreen = jest.fn(() => { - return true; - }); + const cmd = { + cmd: apiCmds.unmaximizeMainWindow, + }; + const spy = jest.spyOn(ipcRenderer, sendEventLabel); wrapper.setState({ isMaximized: true }); wrapper.find(customSelector).simulate('click'); - expect(spy).toBeCalledWith(false); + expect(spy).toBeCalledWith(apiName, cmd); }); it('should call maximize correctly when it is not in full screen', () => { const titleLabel = 'Maximize'; - const maximizeFn = 'maximize'; const expectedState = { isMaximized: true }; const customSelector = `button.title-bar-button[title="${titleLabel}"]`; const wrapper = shallow(React.createElement(WindowsTitleBar)); - const instance: any = wrapper.instance(); - const window = instance.window; - const spyWindow = jest.spyOn(window, maximizeFn); + wrapper.setState({ isMaximized: false }); + const spy = jest.spyOn(ipcRenderer, sendEventLabel); const spyState = jest.spyOn(wrapper, 'setState'); wrapper.find(customSelector).simulate('click'); - expect(spyWindow).toBeCalled(); + const cmd = { + cmd: apiCmds.maximizeMainWindow, + }; + expect(spy).toBeCalled(); + expect(spy).toBeCalledWith(apiName, cmd); expect(spyState).lastCalledWith(expectedState); }); }); diff --git a/src/app/child-window-handler.ts b/src/app/child-window-handler.ts index fad042cb..e47a0d7c 100644 --- a/src/app/child-window-handler.ts +++ b/src/app/child-window-handler.ts @@ -1,4 +1,11 @@ -import { BrowserWindow, crashReporter, WebContents } from 'electron'; +import { + BrowserWindow, + BrowserWindowConstructorOptions, + crashReporter, + DidCreateWindowDetails, + HandlerDetails, + WebContents, +} from 'electron'; import { parse as parseQuerystring } from 'querystring'; import { format, parse, Url } from 'url'; @@ -9,6 +16,7 @@ import { getGuid } from '../common/utils'; import { whitelistHandler } from '../common/whitelist-handler'; import { config } from './config-handler'; import crashHandler from './crash-handler'; +import { mainEvents } from './main-event-handler'; import { handlePermissionRequests, monitorWindowActions, @@ -16,10 +24,13 @@ import { removeWindowEventListener, sendInitialBoundChanges, } from './window-actions'; -import { ICustomBrowserWindow, windowHandler } from './window-handler'; +import { + ICustomBrowserWindow, + ICustomBrowserWindowConstructorOpts, + windowHandler, +} from './window-handler'; import { getBounds, - // handleCertificateProxyVerification, injectStyles, preventWindowNavigation, } from './window-utils'; @@ -30,6 +41,8 @@ const DEFAULT_POP_OUT_HEIGHT = 600; const MIN_WIDTH = 300; const MIN_HEIGHT = 300; +const CHILD_WINDOW_EVENTS = ['enter-full-screen', 'leave-full-screen']; + /** * Verifies protocol for a new url to check if it is http or https * @param url URL to be verified @@ -82,38 +95,39 @@ const getParsedUrl = (url: string): Url => { export const handleChildWindow = (webContents: WebContents): void => { const childWindow = ( - event, - newWinUrl, - frameName, - disposition, - newWinOptions, - ): void => { - logger.info(`child-window-handler: trying to create new child window for url: ${newWinUrl}, - frame name: ${frameName || undefined}, disposition: ${disposition}`); + details: HandlerDetails, + ): + | { action: 'deny' } + | { + action: 'allow'; + overrideBrowserWindowOptions?: BrowserWindowConstructorOptions; + } => { + logger.info(`child-window-handler: trying to create new child window for url: ${ + details.url + }, + frame name: ${details.frameName || undefined}, disposition: ${ + details.disposition + }`); const mainWindow = windowHandler.getMainWindow(); if (!mainWindow || mainWindow.isDestroyed()) { logger.info( `child-window-handler: main window is not available / destroyed, not creating child window!`, ); - return; + return { + action: 'deny', + }; } if (!windowHandler.url) { logger.info( `child-window-handler: we don't have a valid url, not creating child window!`, ); - return; + return { + action: 'deny', + }; } - if (!newWinOptions.webPreferences) { - newWinOptions.webPreferences = {}; - } - - Object.assign(newWinOptions.webPreferences, webContents); - - // need this to extract other parameters - const newWinParsedUrl = getParsedUrl(newWinUrl); - - const newWinUrlData = whitelistHandler.parseDomain(newWinUrl); + const newWinOptions = windowHandler.getMainWindowOpts(); + const newWinUrlData = whitelistHandler.parseDomain(details.url); const mainWinUrlData = whitelistHandler.parseDomain(windowHandler.url); const newWinDomainName = `${newWinUrlData.domain}${newWinUrlData.tld}`; @@ -130,24 +144,70 @@ export const handleChildWindow = (webContents: WebContents): void => { // otherwise open in default browser. if ( (newWinDomainName === mainWinDomainName || - emptyUrlString.includes(newWinUrl)) && - frameName !== '' && - dispositionWhitelist.includes(disposition) + emptyUrlString.includes(details.url)) && + details.frameName !== '' && + dispositionWhitelist.includes(details.disposition) ) { logger.info( - `child-window-handler: opening pop-out window for ${newWinUrl}`, + `child-window-handler: opening pop-out window for ${details.url}`, ); - - const newWinKey = getGuid(); - if (!frameName) { + if (!details.frameName) { logger.info( - `child-window-handler: frame name missing! not opening the url ${newWinUrl}`, + `child-window-handler: frame name missing! not opening the url ${details.url}`, + ); + return { + action: 'deny', + }; + } + return { + action: 'allow', + // override child window options + overrideBrowserWindowOptions: { ...newWinOptions, ...{ frame: true } }, + }; + } else { + if (details.url && details.url.length > 2083) { + logger.info( + `child-window-handler: new window url length is greater than 2083, not performing any action!`, + ); + return { + action: 'deny', + }; + } + if (!verifyProtocolForNewUrl(details.url)) { + logger.info( + `child-window-handler: new window url protocol is not valid, not performing any action!`, + ); + return { + action: 'deny', + }; + } + logger.info(`child-window-handler: new window url is ${details.url} which is not of the same host / protocol, + so opening it in the default app!`); + windowHandler.openUrlInDefaultBrowser(details.url); + return { action: 'deny' }; + } + }; + + webContents.setWindowOpenHandler(childWindow); + + webContents.on( + 'did-create-window', + (browserWindow: BrowserWindow, details: DidCreateWindowDetails) => { + const newWinOptions = details.options as ICustomBrowserWindowConstructorOpts; + const width = newWinOptions.width || DEFAULT_POP_OUT_WIDTH; + const height = newWinOptions.height || DEFAULT_POP_OUT_HEIGHT; + const newWinKey = getGuid(); + + const mainWindow = windowHandler.getMainWindow(); + if (!mainWindow || mainWindow.isDestroyed()) { + logger.info( + 'child-window-handler: main window is not available / destroyed, not creating child window!', ); return; } - const width = newWinOptions.width || DEFAULT_POP_OUT_WIDTH; - const height = newWinOptions.height || DEFAULT_POP_OUT_HEIGHT; + // need this to extract other parameters + const newWinParsedUrl = getParsedUrl(details.url); // try getting x and y position from query parameters const query = @@ -184,8 +244,9 @@ export const handleChildWindow = (webContents: WebContents): void => { newWinOptions.frame = true; newWinOptions.winKey = newWinKey; newWinOptions.fullscreen = false; + newWinOptions.fullscreenable = true; - const childWebContents: WebContents = newWinOptions.webContents; + const childWebContents: WebContents = browserWindow.webContents; // Event needed to hide native menu bar childWebContents.once('did-start-loading', () => { const browserWin = BrowserWindow.fromWebContents( @@ -203,7 +264,7 @@ export const handleChildWindow = (webContents: WebContents): void => { childWebContents.once('did-finish-load', async () => { logger.info( - `child-window-handler: child window content loaded for url ${newWinUrl}!`, + `child-window-handler: child window content loaded for url ${details.url}!`, ); const browserWin: ICustomBrowserWindow = BrowserWindow.fromWebContents( childWebContents, @@ -221,12 +282,11 @@ export const handleChildWindow = (webContents: WebContents): void => { locale: i18n.getLocale(), resources: i18n.loadedResources, origin: url, - enableCustomTitleBar: false, isMainWindow: false, }); // Inserts css on to the window - await injectStyles(browserWin, false); - browserWin.winName = frameName; + await injectStyles(browserWin.webContents, false); + browserWin.winName = details.frameName; browserWin.setAlwaysOnTop(mainWindow.isAlwaysOnTop()); logger.info( `child-window-handler: setting always on top for child window? ${mainWindow.isAlwaysOnTop()}!`, @@ -251,7 +311,12 @@ export const handleChildWindow = (webContents: WebContents): void => { // Remove all attached event listeners browserWin.on('close', () => { logger.info( - `child-window-handler: close event occurred for window with url ${newWinUrl}!`, + `child-window-handler: close event occurred for window with url ${details.url}!`, + ); + // Subscribe events for main view - snack bar + mainEvents.unsubscribeMultipleEvents( + CHILD_WINDOW_EVENTS, + browserWin.webContents, ); removeWindowEventListener(browserWin); }); @@ -268,11 +333,6 @@ export const handleChildWindow = (webContents: WebContents): void => { // validate link and create a child window or open in browser handleChildWindow(browserWin.webContents); - // Certificate verification proxy - // if (!isDevEnv) { - // browserWin.webContents.session.setCertificateVerifyProc(handleCertificateProxyVerification); - // } - // Updates media permissions for preload context const { permissions } = config.getConfigFields(['permissions']); browserWin.webContents.send( @@ -280,25 +340,13 @@ export const handleChildWindow = (webContents: WebContents): void => { permissions.media, ); } + + // Subscribe events for main view - snack bar + mainEvents.subscribeMultipleEvents( + CHILD_WINDOW_EVENTS, + browserWin.webContents, + ); }); - } else { - event.preventDefault(); - if (newWinUrl && newWinUrl.length > 2083) { - logger.info( - `child-window-handler: new window url length is greater than 2083, not performing any action!`, - ); - return; - } - if (!verifyProtocolForNewUrl(newWinUrl)) { - logger.info( - `child-window-handler: new window url protocol is not valid, not performing any action!`, - ); - return; - } - logger.info(`child-window-handler: new window url is ${newWinUrl} which is not of the same host / protocol, - so opening it in the default app!`); - windowHandler.openUrlInDefaultBrowser(newWinUrl); - } - }; - webContents.on('new-window', childWindow); + }, + ); }; diff --git a/src/app/crash-handler.ts b/src/app/crash-handler.ts index 313aa002..021d5f2f 100644 --- a/src/app/crash-handler.ts +++ b/src/app/crash-handler.ts @@ -1,4 +1,4 @@ -import { app, crashReporter, Details, dialog } from 'electron'; +import { app, crashReporter, dialog, RenderProcessGoneDetails } from 'electron'; import { i18n } from '../common/i18n'; import { logger } from '../common/logger'; import { @@ -89,7 +89,7 @@ class CrashHandler { public handleRendererCrash(browserWindow: ICustomBrowserWindow) { browserWindow.webContents.on( 'render-process-gone', - async (_event: Event, details: Details) => { + async (_event: Event, details: RenderProcessGoneDetails) => { logger.info(`crash-handler: Renderer process for ${browserWindow.winName} crashed. Reason is ${details.reason}`); const eventData: ICrashData = { diff --git a/src/app/main-api-handler.ts b/src/app/main-api-handler.ts index ce3956ae..70fef95c 100644 --- a/src/app/main-api-handler.ts +++ b/src/app/main-api-handler.ts @@ -1,11 +1,17 @@ -import { BrowserWindow, ipcMain } from 'electron'; +import { + BrowserWindow, + clipboard, + dialog, + ipcMain, + systemPreferences, +} from 'electron'; import { apiCmds, apiName, IApiArgs, INotificationData, } from '../common/api-interface'; -import { LocaleType } from '../common/i18n'; +import { i18n, LocaleType } from '../common/i18n'; import { logger } from '../common/logger'; import { activityDetection } from './activity-detection'; import { analytics } from './analytics-handler'; @@ -22,6 +28,7 @@ import { activate, handleKeyPress } from './window-actions'; import { ICustomBrowserWindow, windowHandler } from './window-handler'; import { downloadManagerAction, + isValidView, isValidWindow, sanitize, setDataUrl, @@ -40,7 +47,12 @@ import { ipcMain.on( apiName.symphonyApi, async (event: Electron.IpcMainEvent, arg: IApiArgs) => { - if (!isValidWindow(BrowserWindow.fromWebContents(event.sender))) { + if ( + !( + isValidWindow(BrowserWindow.fromWebContents(event.sender)) || + isValidView(event.sender) + ) + ) { logger.error( `main-api-handler: invalid window try to perform action, ignoring action`, arg.cmd, @@ -295,8 +307,89 @@ ipcMain.on( case apiCmds.autoUpdate: autoUpdate.update(arg.filename); break; + case apiCmds.aboutAppClipBoardData: + if (arg.clipboard && arg.clipboardType) { + clipboard.write( + { text: JSON.stringify(arg.clipboard, null, 4) }, + arg.clipboardType, + ); + } + break; + case apiCmds.closeMainWindow: + windowHandler.getMainWindow()?.close(); + break; + case apiCmds.minimizeMainWindow: + windowHandler.getMainWindow()?.minimize(); + break; + case apiCmds.maximizeMainWindow: + windowHandler.getMainWindow()?.maximize(); + break; + case apiCmds.unmaximizeMainWindow: + const mainWindow = windowHandler.getMainWindow(); + if (mainWindow && windowExists(mainWindow)) { + mainWindow.isFullScreen() + ? mainWindow.setFullScreen(false) + : mainWindow.unmaximize(); + } + break; default: break; } }, ); + +ipcMain.handle( + apiName.symphonyApi, + async (event: Electron.IpcMainInvokeEvent, arg: IApiArgs) => { + if ( + !( + isValidWindow(BrowserWindow.fromWebContents(event.sender)) || + isValidView(event.sender) + ) + ) { + logger.error( + `main-api-handler: invalid window try to perform action, ignoring action`, + arg.cmd, + ); + return; + } + + if (!arg) { + return; + } + + switch (arg.cmd) { + case apiCmds.getCurrentOriginUrl: + return windowHandler.getMainWindow()?.origin; + case apiCmds.isAeroGlassEnabled: + return systemPreferences.isAeroGlassEnabled(); + case apiCmds.showScreenSharePermissionDialog: { + const focusedWindow = BrowserWindow.getFocusedWindow(); + if (focusedWindow && !focusedWindow.isDestroyed()) { + await dialog.showMessageBox(focusedWindow, { + message: `${i18n.t( + 'Your administrator has disabled sharing your screen. Please contact your admin for help', + 'Permissions', + )()}`, + title: `${i18n.t('Permission Denied')()}!`, + type: 'error', + }); + return; + } + return; + } + case apiCmds.getMediaAccessStatus: + const camera = systemPreferences.getMediaAccessStatus('camera'); + const microphone = systemPreferences.getMediaAccessStatus('microphone'); + const screen = systemPreferences.getMediaAccessStatus('screen'); + return { + camera, + microphone, + screen, + }; + default: + break; + } + return; + }, +); diff --git a/src/app/main-event-handler.ts b/src/app/main-event-handler.ts new file mode 100644 index 00000000..affad702 --- /dev/null +++ b/src/app/main-event-handler.ts @@ -0,0 +1,92 @@ +import { WebContents } from 'electron'; + +export class MainProcessEvents { + private registeredWebContents: Map> = new Map< + string, + Set + >(); + + /** + * Broadcasts events to all the registered webContents + * @param eventName + * @param args + */ + public publish(eventName: string, args?: any[]) { + const allWebContests = this.registeredWebContents.get(eventName); + if (!allWebContests) { + return; + } + allWebContests.forEach((w) => { + if (w && !w.isDestroyed()) { + w.send(eventName, args); + } + }); + } + + /** + * Subscribe multiple events for the webContents + * @param eventNames + * @param webContents + */ + public subscribeMultipleEvents( + eventNames: string[], + webContents: WebContents, + ) { + eventNames.forEach((e) => this.subscribe(e, webContents)); + } + + /** + * Unsubscribe multiple events for the webContents + * @param eventNames + * @param webContents + */ + public unsubscribeMultipleEvents( + eventNames: string[], + webContents: WebContents, + ) { + eventNames.forEach((e) => this.unsubscribe(e, webContents)); + } + + /** + * Subscribe event for the webContents + * @param eventName + * @param webContents + */ + public subscribe(eventName: string, webContents: WebContents) { + if (!webContents || webContents.isDestroyed()) { + return; + } + const registeredWebContents = this.registeredWebContents.get(eventName); + if (registeredWebContents) { + const isRegistered = registeredWebContents.has(webContents); + if (isRegistered) { + return; + } + registeredWebContents.add(webContents); + return; + } + this.registeredWebContents.set(eventName, new Set()); + this.registeredWebContents.get(eventName)!.add(webContents); + } + + /** + * Unsubscribe an event from a specific webContents + * @param eventName + * @param webContents + */ + public unsubscribe(eventName: string, webContents: WebContents) { + if (!webContents || webContents.isDestroyed()) { + return; + } + const registeredWebContents = this.registeredWebContents.get(eventName); + if (registeredWebContents) { + if (registeredWebContents.has(webContents)) { + registeredWebContents.delete(webContents); + } + } + } +} + +const mainEvents = new MainProcessEvents(); + +export { mainEvents }; diff --git a/src/app/main.ts b/src/app/main.ts index 15063e10..697037cd 100644 --- a/src/app/main.ts +++ b/src/app/main.ts @@ -2,7 +2,7 @@ import { app, systemPreferences } from 'electron'; import * as electronDownloader from 'electron-dl'; import * as shellPath from 'shell-path'; -import { isDevEnv, isElectronQA, isLinux, isMac } from '../common/env'; +import { isDevEnv, isLinux, isMac } from '../common/env'; import { logger } from '../common/logger'; import { getCommandLineArgs } from '../common/utils'; import { cleanUpAppCache, createAppCacheFile } from './app-cache-handler'; @@ -60,12 +60,6 @@ electronDownloader(); handlePerformanceSettings(); setChromeFlags(); -// Need this to prevent blank pop-out from 8.x versions -// Refer - SDA-1877 - https://github.com/electron/electron/issues/18397 -if (!isElectronQA) { - app.allowRendererProcessReuse = true; -} - // Electron sets the default protocol if (!isDevEnv) { const { userDataPath } = config.getConfigFields(['userDataPath']); diff --git a/src/app/notifications/electron-notification.ts b/src/app/notifications/electron-notification.ts index bb484500..44688ea4 100644 --- a/src/app/notifications/electron-notification.ts +++ b/src/app/notifications/electron-notification.ts @@ -19,26 +19,11 @@ export class ElectronNotification extends Notification { this.callback = callback; this.options = options; - this.once('click', this.onClick); - this.once('reply', this.onReply); - } - - /** - * Notification on click handler - * @param _event - * @private - */ - private onClick(_event: Event) { - this.callback(NotificationActions.notificationClicked, this.options); - } - - /** - * Notification reply handler - * @param _event - * @param reply - * @private - */ - private onReply(_event: Event, reply: string) { - this.callback(NotificationActions.notificationReply, this.options, reply); + this.once('click', (_event) => { + this.callback(NotificationActions.notificationClicked, this.options); + }); + this.once('reply', (_event, reply) => { + this.callback(NotificationActions.notificationReply, this.options, reply); + }); } } diff --git a/src/app/notifications/notification-helper.ts b/src/app/notifications/notification-helper.ts index cacac82c..bb0cce64 100644 --- a/src/app/notifications/notification-helper.ts +++ b/src/app/notifications/notification-helper.ts @@ -6,7 +6,6 @@ import { import { isWindowsOS } from '../../common/env'; import { notification } from '../../renderer/notification'; import { windowHandler } from '../window-handler'; -import { windowExists } from '../window-utils'; import { ElectronNotification } from './electron-notification'; class NotificationHelper { @@ -80,9 +79,9 @@ class NotificationHelper { data: ElectronNotificationData, notificationData: ElectronNotificationData, ) { - const mainWindow = windowHandler.getMainWindow(); - if (mainWindow && windowExists(mainWindow) && mainWindow.webContents) { - mainWindow.webContents.send('notification-actions', { + const mainWebContents = windowHandler.getMainWebContents(); + if (mainWebContents && !mainWebContents.isDestroyed()) { + mainWebContents.send('notification-actions', { event, data, notificationData, diff --git a/src/app/window-actions.ts b/src/app/window-actions.ts index 3cb0b96a..d40aeef3 100644 --- a/src/app/window-actions.ts +++ b/src/app/window-actions.ts @@ -12,6 +12,7 @@ import { logger } from '../common/logger'; import { throttle } from '../common/utils'; import { notification } from '../renderer/notification'; import { CloudConfigDataTypes, config } from './config-handler'; +import { mainEvents } from './main-event-handler'; import { ICustomBrowserWindow, windowHandler } from './window-handler'; import { showPopupMenu, windowExists } from './window-utils'; @@ -97,10 +98,11 @@ const windowMaximized = async (): Promise => { } }; -const throttledWindowChanges = throttle(async () => { +const throttledWindowChanges = throttle(async (eventName) => { await saveWindowSettings(); await windowMaximized(); notification.moveNotificationToTop(); + mainEvents.publish(eventName); }, 1000); const throttledWindowRestore = throttle(async () => { @@ -303,14 +305,18 @@ export const monitorWindowActions = (window: BrowserWindow): void => { eventNames.forEach((event: string) => { if (window) { // @ts-ignore - window.on(event, throttledWindowChanges); + window.on(event, () => throttledWindowChanges(event)); } }); - window.on('enter-full-screen', throttledWindowChanges); - window.on('maximize', throttledWindowChanges); + window.on('enter-full-screen', () => + throttledWindowChanges('enter-full-screen'), + ); + window.on('maximize', () => throttledWindowChanges('maximize')); - window.on('leave-full-screen', throttledWindowChanges); - window.on('unmaximize', throttledWindowChanges); + window.on('leave-full-screen', () => + throttledWindowChanges('leave-full-screen'), + ); + window.on('unmaximize', () => throttledWindowChanges('unmaximize')); if ((window as ICustomBrowserWindow).winName === apiName.mainWindowName) { window.on('restore', throttledWindowRestore); diff --git a/src/app/window-handler.ts b/src/app/window-handler.ts index d4c0a4a8..5b31a600 100644 --- a/src/app/window-handler.ts +++ b/src/app/window-handler.ts @@ -8,8 +8,10 @@ import { dialog, globalShortcut, ipcMain, + RenderProcessGoneDetails, screen, shell, + WebContents, } from 'electron'; import * as fs from 'fs'; import * as path from 'path'; @@ -41,6 +43,7 @@ import { IGlobalConfig, } from './config-handler'; import crashHandler from './crash-handler'; +import { mainEvents } from './main-event-handler'; import { SpellChecker } from './spell-check-handler'; import { checkIfBuildExpired } from './ttl-handler'; import { versionHandler } from './version-handler'; @@ -58,10 +61,12 @@ import { handleDownloadManager, injectStyles, isSymphonyReachable, + loadBrowserViews, monitorNetworkInterception, preventWindowNavigation, reloadWindow, resetZoomLevel, + viewExists, windowExists, zoomIn, zoomOut, @@ -79,7 +84,9 @@ enum ClientSwitchType { CLIENT_2_0_DAILY = 'CLIENT_2_0_DAILY', } -interface ICustomBrowserWindowConstructorOpts +const MAIN_WEB_CONTENTS_EVENTS = ['enter-full-screen', 'leave-full-screen']; + +export interface ICustomBrowserWindowConstructorOpts extends Electron.BrowserWindowConstructorOptions { winKey: string; } @@ -90,9 +97,15 @@ export interface ICustomBrowserWindow extends Electron.BrowserWindow { origin?: string; } +export interface ICustomBrowserView extends Electron.BrowserView { + winName: string; + notificationData?: object; + origin?: string; +} + // Default window width & height -let DEFAULT_WIDTH: number = 900; -let DEFAULT_HEIGHT: number = 900; +export let DEFAULT_WIDTH: number = 900; +export let DEFAULT_HEIGHT: number = 900; // Timeout on restarting SDA in case it's stuck const LISTEN_TIMEOUT: number = 25 * 1000; @@ -113,6 +126,9 @@ export class WindowHandler { } return format(parsedUrl); } + public mainView: ICustomBrowserView | null; + public titleBarView: ICustomBrowserView | null; + public mainWebContents: WebContents | undefined; public appMenu: AppMenu | null; public isAutoReload: boolean; public isOnline: boolean; @@ -233,6 +249,8 @@ export class WindowHandler { } this.appMenu = null; + this.mainView = null; + this.titleBarView = null; const locale: LocaleType = (this.config.locale || app.getLocale()) as LocaleType; i18n.setLocale(locale); @@ -387,8 +405,24 @@ export class WindowHandler { cleanAppCacheOnCrash(this.mainWindow); // loads the main window with url from config/cmd line logger.info(`Loading main window with url ${this.url}`); - const userAgent = this.getUserAgent(this.mainWindow); - this.mainWindow.loadURL(this.url, { userAgent }); + const userAgent = this.getUserAgent(this.mainWindow.webContents); + + if ( + this.config.isCustomTitleBar === CloudConfigDataTypes.ENABLED && + isWindowsOS && + this.mainWindow && + windowExists(this.mainWindow) + ) { + this.mainWebContents = await loadBrowserViews( + this.mainWindow, + this.url, + userAgent, + ); + } else { + await this.mainWindow.loadURL(this.url, { userAgent }); + this.mainWebContents = this.mainWindow.webContents; + } + // check for build expiry in case of test builds this.checkExpiry(this.mainWindow); // update version info from server @@ -397,20 +431,12 @@ export class WindowHandler { 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', () => { + this.mainView?.webContents.once('did-start-loading', () => { logger.info( - `window-handler: main window web contents started loading for url ${this.mainWindow?.webContents.getURL()}!`, + `window-handler: main window web contents started loading for url ${this.mainView?.webContents.getURL()}!`, ); this.finishedLoading = false; this.listenForLoad(); - 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( @@ -441,50 +467,55 @@ export class WindowHandler { logger.info(`window-handler: Main Window ready to show: ${event}`); }); - this.mainWindow.webContents.on('did-finish-load', async () => { + this.mainWebContents.on('did-finish-load', async () => { // reset to false when the client reloads this.isMana = false; 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)) { + if (!this.mainWebContents || this.mainWebContents.isDestroyed()) { logger.info( `window-handler: main window web contents destroyed already! exiting`, ); return; } this.finishedLoading = true; - this.url = this.mainWindow.webContents.getURL(); - if (this.url.indexOf('about:blank') === 0) { + this.url = this.mainWebContents?.getURL(); + if (this.url?.indexOf('about:blank') === 0) { logger.info( `Looks like about:blank got loaded which may lead to blank screen`, ); logger.info(`Reloading the app to check if it resolves the issue`); const url = this.userConfig.url || this.globalConfig.url; - const userAgent = this.getUserAgent(this.mainWindow); - await this.mainWindow.loadURL(url, { userAgent }); + const userAgent = this.getUserAgent(this.mainWebContents); + await this.mainWebContents?.loadURL(url, { userAgent }); return; } logger.info('window-handler: did-finish-load, url: ' + this.url); - // Injects custom title bar and snack bar css into the webContents - await injectStyles(this.mainWindow, this.isCustomTitleBar); + if (this.mainWebContents && !this.mainWebContents.isDestroyed()) { + // Injects custom title bar and snack bar css into the webContents + await injectStyles(this.mainWebContents, this.isCustomTitleBar); + this.mainWebContents.send('page-load', { + isWindowsOS, + locale: i18n.getLocale(), + resources: i18n.loadedResources, + isMainWindow: true, + }); - 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.appMenu = new AppMenu(); + + const { permissions } = config.getConfigFields(['permissions']); + this.mainWebContents.send('is-screen-share-enabled', permissions.media); + + // Subscribe events for main view - snack bar + mainEvents.subscribeMultipleEvents( + MAIN_WEB_CONTENTS_EVENTS, + this.mainWebContents, + ); + } }); - this.mainWindow.webContents.on( + this.mainWebContents.on( 'did-fail-load', (_event, errorCode, errorDesc, validatedURL) => { logger.error( @@ -494,13 +525,13 @@ export class WindowHandler { }, ); - this.mainWindow.webContents.on('did-stop-loading', async () => { + this.mainWebContents.on('did-stop-loading', async () => { if (this.mainWindow && windowExists(this.mainWindow)) { - this.mainWindow.webContents.send('page-load-failed', { + this.mainWebContents?.send('page-load-failed', { locale: i18n.getLocale(), resources: i18n.loadedResources, }); - const href = await this.mainWindow.webContents.executeJavaScript( + const href = await this.mainWebContents?.executeJavaScript( 'document.location.href', ); try { @@ -509,7 +540,7 @@ export class WindowHandler { href === 'chrome-error://chromewebdata/' ) { if (this.mainWindow && windowExists(this.mainWindow)) { - this.mainWindow.webContents.insertCSS( + this.mainWebContents?.insertCSS( fs .readFileSync( path.join( @@ -521,7 +552,7 @@ export class WindowHandler { ) .toString(), ); - this.mainWindow.webContents.send('network-error', { + this.mainWebContents?.send('network-error', { error: this.loadFailError, }); isSymphonyReachable( @@ -537,12 +568,14 @@ export class WindowHandler { ); } } + // Register dev tools on initial launch + this.registerGlobalShortcuts(); }); - this.mainWindow.webContents.on( - 'crashed', - async (_event: Event, killed: boolean) => { - if (killed) { + this.mainWebContents.on( + 'render-process-gone', + async (_event: Event, details: RenderProcessGoneDetails) => { + if (details.reason === 'killed') { logger.info(`window-handler: main window crashed (killed)!`); return; } @@ -581,8 +614,12 @@ export class WindowHandler { if (this.willQuitApp) { logger.info(`window-handler: app is quitting, destroying all windows!`); - if (this.mainWindow && this.mainWindow.webContents.isDevToolsOpened()) { - this.mainWindow.webContents.closeDevTools(); + if ( + this.mainWindow && + !this.mainWebContents?.isDestroyed() && + this.mainWebContents?.isDevToolsOpened() + ) { + this.mainWebContents?.closeDevTools(); } return this.destroyAllWindows(); } @@ -629,7 +666,7 @@ export class WindowHandler { // Certificate verification proxy if (!isDevEnv) { - this.mainWindow.webContents.session.setCertificateVerifyProc( + this.mainWebContents.session.setCertificateVerifyProc( handleCertificateProxyVerification, ); } @@ -646,25 +683,22 @@ export class WindowHandler { preventWindowNavigation(this.mainWindow, false); // Handle media/other permissions - handlePermissionRequests(this.mainWindow.webContents); + handlePermissionRequests(this.mainWebContents); // Start monitoring window actions monitorWindowActions(this.mainWindow); // Download manager - this.mainWindow.webContents.session.on( - 'will-download', - handleDownloadManager, - ); + this.mainWebContents.session.on('will-download', handleDownloadManager); // store window ref this.addWindow(this.windowOpts.winKey, this.mainWindow); // Handle pop-outs window - handleChildWindow(this.mainWindow.webContents); + handleChildWindow(this.mainWebContents); if (this.config.enableRendererLogs) { - this.mainWindow.webContents.on('console-message', onConsoleMessages); + this.mainWebContents.on('console-message', onConsoleMessages); } return this.mainWindow; @@ -697,13 +731,12 @@ export class WindowHandler { } logger.info(`finished loading welcome screen.`); if (this.url.indexOf('welcome')) { - const ssoValue = + const ssoValue = !!( this.userConfig.url && this.userConfig.url.indexOf('/login/sso/initsso') > -1 - ? true - : false; + ); - this.mainWindow.webContents.send('page-load-welcome', { + this.mainWindow.webContents?.send('page-load-welcome', { locale: i18n.getLocale(), resource: i18n.loadedResources, }); @@ -716,7 +749,7 @@ export class WindowHandler { ) : this.userConfig.url; - this.mainWindow.webContents.send('welcome', { + this.mainWindow.webContents?.send('welcome', { url: userConfigUrl || this.startUrl, message: '', urlValid: !!userConfigUrl, @@ -739,6 +772,20 @@ export class WindowHandler { return this.mainWindow; } + /** + * Gets the main browser webContents + */ + public getMainWebContents(): WebContents | undefined { + return this.mainWebContents; + } + + /** + * Gets the main browser view + */ + public getMainView(): ICustomBrowserView | null { + return this.mainView; + } + /** * Gets all the window that we have created * @@ -749,6 +796,33 @@ export class WindowHandler { return this.windows; } + /** + * Gets the main window opts + * + * @return ICustomBrowserWindowConstructorOpts + */ + public getMainWindowOpts(): ICustomBrowserWindowConstructorOpts { + return this.windowOpts; + } + + /** + * Sets the title bar view + * + * @param mainView + */ + public setMainView(mainView: ICustomBrowserView): void { + this.mainView = mainView; + } + + /** + * Sets the title bar view + * + * @param titleBarView + */ + public setTitleBarView(titleBarView: ICustomBrowserView): void { + this.titleBarView = titleBarView; + } + /** * Closes the window from an event emitted by the render processes * @@ -837,12 +911,27 @@ export class WindowHandler { /** * 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; + public hasWindow(window: Electron.BrowserWindow): boolean { + for (const key in this.windows) { + if (this.windows[key] === window) { + return true; + } + } + return this.aboutAppWindow === window; + } + + /** + * Checks if the window and a key has a window + * + * @param webContents {Electron.webContents} + */ + public hasView(webContents: Electron.webContents): boolean { + return ( + webContents === this.mainView?.webContents || + webContents === this.titleBarView?.webContents + ); } /** @@ -1756,15 +1845,15 @@ export class WindowHandler { /** * Reloads symphony in case of network failures */ - public reloadSymphony() { - if (this.mainWindow && windowExists(this.mainWindow)) { + public async reloadSymphony() { + if (this.mainWebContents && !this.mainWebContents.isDestroyed()) { // If the client is fully loaded, upon network interruption, load that if (this.isLoggedIn) { logger.info( `window-utils: user has logged in, getting back to Symphony app`, ); - const userAgent = this.getUserAgent(this.mainWindow); - this.mainWindow.loadURL( + const userAgent = this.getUserAgent(this.mainWebContents); + await this.mainWebContents.loadURL( this.url || this.userConfig.url || this.globalConfig.url, { userAgent }, ); @@ -1774,10 +1863,13 @@ export class WindowHandler { logger.info( `window-utils: user hasn't logged in yet, loading login page again`, ); - const userAgent = this.getUserAgent(this.mainWindow); - this.mainWindow.loadURL(this.userConfig.url || this.globalConfig.url, { - userAgent, - }); + const userAgent = this.getUserAgent(this.mainWebContents); + await this.mainWebContents.loadURL( + this.userConfig.url || this.globalConfig.url, + { + userAgent, + }, + ); } } @@ -1788,16 +1880,16 @@ export class WindowHandler { setTimeout(async () => { if (!this.finishedLoading) { logger.info(`window-handler: Pod load failed on launch`); - if (this.mainWindow && windowExists(this.mainWindow)) { - const webContentsUrl = this.mainWindow.webContents.getURL(); + if (this.mainWebContents && !this.mainWebContents.isDestroyed()) { + const webContentsUrl = this.mainWebContents?.getURL(); logger.info( `window-handler: Current main window url is ${webContentsUrl}.`, ); const reloadUrl = webContentsUrl || this.userConfig.url || this.globalConfig.url; logger.info(`window-handler: Trying to reload ${reloadUrl}.`); - const userAgent = this.getUserAgent(this.mainWindow); - await this.mainWindow.loadURL(reloadUrl, { userAgent }); + const userAgent = this.getUserAgent(this.mainWebContents); + await this.mainWebContents.loadURL(reloadUrl, { userAgent }); return; } logger.error( @@ -1829,9 +1921,8 @@ export class WindowHandler { */ private registerGlobalShortcuts(): void { logger.info('window-handler: register global shortcuts!'); - globalShortcut.register( - isMac ? 'Cmd+Alt+I' : 'Ctrl+Shift+I', - this.onRegisterDevtools, + globalShortcut.register(isMac ? 'Cmd+Alt+I' : 'Ctrl+Shift+I', () => + this.onRegisterDevtools(), ); globalShortcut.register('CmdOrCtrl+R', this.onReload); @@ -1895,6 +1986,16 @@ export class WindowHandler { } const { devToolsEnabled } = config.getConfigFields(['devToolsEnabled']); if (devToolsEnabled) { + if ( + this.mainWindow && + windowExists(this.mainWindow) && + focusedWindow === this.mainWindow + ) { + if (this.mainView && viewExists(this.mainView)) { + this.mainWebContents?.toggleDevTools(); + return; + } + } focusedWindow.webContents.toggleDevTools(); return; } @@ -1922,7 +2023,7 @@ export class WindowHandler { private async switchClient(clientSwitch: ClientSwitchType): Promise { logger.info(`window handler: switch to client ${clientSwitch}`); - if (!this.mainWindow || !windowExists(this.mainWindow)) { + if (!this.mainWebContents || this.mainWebContents.isDestroyed()) { logger.info( `window-handler: switch client - main window web contents destroyed already! exiting`, ); @@ -1933,7 +2034,7 @@ export class WindowHandler { this.url = this.globalConfig.url; } const parsedUrl = parse(this.url); - const csrfToken = await this.mainWindow.webContents.executeJavaScript( + const csrfToken = await this.mainWebContents?.executeJavaScript( `localStorage.getItem('x-km-csrf-token')`, ); switch (clientSwitch) { @@ -1949,9 +2050,9 @@ export class WindowHandler { default: this.url = this.globalConfig.url + `?x-km-csrf-token=${csrfToken}`; } - this.execCmd(this.screenShareIndicatorFrameUtil, []); - const userAgent = this.getUserAgent(this.mainWindow); - await this.mainWindow.loadURL(this.url, { userAgent }); + await this.execCmd(this.screenShareIndicatorFrameUtil, []); + const userAgent = this.getUserAgent(this.mainWebContents); + await this.mainWebContents.loadURL(this.url, { userAgent }); } catch (e) { logger.error( `window-handler: failed to switch client because of error ${e}`, @@ -2038,12 +2139,12 @@ export class WindowHandler { * getUserAgent retrieves current window user-agent and updates it * depending on global config setup * Electron user-agent is removed due to Microsoft Azure not supporting SSO if found - cf SDA-3201 - * @param mainWindow + * @param webContents * @returns updated user-agents */ - private getUserAgent(mainWindow: ICustomBrowserWindow): string { + private getUserAgent(webContents: WebContents): string { const doOverrideUserAgents = !!this.globalConfig.overrideUserAgent; - let userAgent = mainWindow.webContents.getUserAgent(); + let userAgent = webContents.getUserAgent(); if (doOverrideUserAgents) { const electronUserAgentRegex = /Electron/; userAgent = userAgent.replace(electronUserAgentRegex, 'ElectronSymphony'); diff --git a/src/app/window-utils.ts b/src/app/window-utils.ts index 329f4daa..d9d87d5e 100644 --- a/src/app/window-utils.ts +++ b/src/app/window-utils.ts @@ -1,10 +1,12 @@ import { app, + BrowserView, BrowserWindow, dialog, nativeImage, screen, shell, + WebContents, } from 'electron'; import electron = require('electron'); import fetch from 'electron-fetch'; @@ -14,7 +16,13 @@ import * as path from 'path'; import { format, parse } from 'url'; import { apiName } from '../common/api-interface'; -import { isDevEnv, isLinux, isMac, isNodeEnv } from '../common/env'; +import { + isDevEnv, + isLinux, + isMac, + isNodeEnv, + isWindowsOS, +} from '../common/env'; import { i18n, LocaleType } from '../common/i18n'; import { logger } from '../common/logger'; import { getGuid } from '../common/utils'; @@ -30,9 +38,16 @@ import { downloadHandler, IDownloadItem } from './download-handler'; import { memoryMonitor } from './memory-monitor'; import { screenSnippet } from './screen-snippet-handler'; import { updateAlwaysOnTop } from './window-actions'; -import { ICustomBrowserWindow, windowHandler } from './window-handler'; +import { + DEFAULT_HEIGHT, + DEFAULT_WIDTH, + ICustomBrowserView, + ICustomBrowserWindow, + windowHandler, +} from './window-handler'; import { notification } from '../renderer/notification'; +import { mainEvents } from './main-event-handler'; interface IStyles { name: styleNames; content: string; @@ -45,7 +60,10 @@ enum styleNames { } const checkValidWindow = true; -const { ctWhitelist } = config.getConfigFields(['ctWhitelist']); +const { ctWhitelist, mainWinPos } = config.getConfigFields([ + 'ctWhitelist', + 'mainWinPos', +]); // Network status check variables const networkStatusCheckInterval = 10 * 1000; @@ -55,6 +73,13 @@ let isNetworkMonitorInitialized = false; const styles: IStyles[] = []; const DOWNLOAD_MANAGER_NAMESPACE = 'DownloadManager'; +const TITLE_BAR_EVENTS = [ + 'maximize', + 'unmaximize', + 'enter-full-screen', + 'leave-full-screen', +]; + /** * Checks if window is valid and exists * @@ -64,6 +89,17 @@ const DOWNLOAD_MANAGER_NAMESPACE = 'DownloadManager'; export const windowExists = (window: BrowserWindow): boolean => !!window && typeof window.isDestroyed === 'function' && !window.isDestroyed(); +/** + * Checks if view is valid and exists + * + * @param view {BrowserView} + * @return boolean + */ +export const viewExists = (view: BrowserView): boolean => + !!view && + typeof view.webContents.isDestroyed === 'function' && + !view.webContents.isDestroyed(); + /** * Prevents window from navigating * @@ -265,9 +301,31 @@ export const isValidWindow = ( } let result: boolean = false; if (browserWin && !browserWin.isDestroyed()) { - // @ts-ignore - const winKey = browserWin.webContents.browserWindowOptions.winKey; - result = windowHandler.hasWindow(winKey, browserWin); + result = windowHandler.hasWindow(browserWin); + } + + if (!result) { + logger.warn( + `window-utils: invalid window try to perform action, ignoring action`, + ); + } + + return result; +}; + +/** + * Ensure events comes from a view that we have created. + * + * @return {Boolean} returns true if exists otherwise false + * @param webContents + */ +export const isValidView = (webContents: Electron.webContents): boolean => { + if (!checkValidWindow) { + return true; + } + let result: boolean = false; + if (webContents && !webContents.isDestroyed()) { + result = windowHandler.hasView(webContents); } if (!result) { @@ -502,22 +560,22 @@ export const handleDownloadManager = ( /** * Inserts css in to the window * - * @param window {BrowserWindow} + * @param mainWebContents {WebContents} */ -const readAndInsertCSS = async (window): Promise => { - if (window && windowExists(window)) { - return styles.map(({ content }) => window.webContents.insertCSS(content)); +const readAndInsertCSS = async (mainWebContents): Promise => { + if (mainWebContents && !mainWebContents.isDestroyed()) { + return styles.map(({ content }) => mainWebContents.insertCSS(content)); } }; /** * Inserts all the required css on to the specified windows * - * @param mainWindow {BrowserWindow} + * @param mainView {WebContents} * @param isCustomTitleBar {boolean} - whether custom title bar enabled */ export const injectStyles = async ( - mainWindow: BrowserWindow, + mainView: WebContents, isCustomTitleBar: boolean, ): Promise => { if (isCustomTitleBar) { @@ -578,7 +636,7 @@ export const injectStyles = async ( }); } - await readAndInsertCSS(mainWindow); + await readAndInsertCSS(mainView); return; }; @@ -638,12 +696,12 @@ export const isSymphonyReachable = ( const podUrl = `${protocol}//${hostname}`; logger.info(`window-utils: checking to see if pod ${podUrl} is reachable!`); fetch(podUrl, { method: 'GET' }) - .then((rsp) => { + .then(async (rsp) => { if (rsp.status === 200 && windowHandler.isOnline) { logger.info( `window-utils: pod ${podUrl} is reachable, loading main window!`, ); - windowHandler.reloadSymphony(); + await windowHandler.reloadSymphony(); if (networkStatusCheckIntervalId) { clearInterval(networkStatusCheckIntervalId); networkStatusCheckIntervalId = null; @@ -673,11 +731,15 @@ export const reloadWindow = (browserWindow: ICustomBrowserWindow) => { } const windowName = browserWindow.winName; - const mainWindow = windowHandler.getMainWindow(); + const mainWebContents = windowHandler.getMainWebContents(); // reload the main window - if (windowName === apiName.mainWindowName) { + if ( + windowName === apiName.mainWindowName && + mainWebContents && + !mainWebContents.isDestroyed() + ) { logger.info(`window-utils: reloading the main window`); - browserWindow.reload(); + mainWebContents.reload(); windowHandler.closeAllWindows(); @@ -685,11 +747,12 @@ export const reloadWindow = (browserWindow: ICustomBrowserWindow) => { return; } + // Send an event to SFE that restarts the pop-out window - if (mainWindow && windowExists(mainWindow)) { + if (mainWebContents && !mainWebContents.isDestroyed()) { logger.info(`window-handler: reloading the window`, { windowName }); const bounds = browserWindow.getBounds(); - mainWindow.webContents.send('restart-floater', { windowName, bounds }); + mainWebContents.send('restart-floater', { windowName, bounds }); } }; @@ -903,3 +966,131 @@ export const monitorNetworkInterception = (url: string) => { ); } }; + +export const loadBrowserViews = async ( + mainWindow: BrowserWindow, + url: string, + userAgent: string, +): Promise => { + mainWindow.setMenuBarVisibility(false); + + const titleBarView = new BrowserView({ + webPreferences: { + sandbox: !isNodeEnv, + nodeIntegration: isNodeEnv, + preload: path.join(__dirname, '../renderer/_preload-component.js'), + devTools: isDevEnv, + }, + }) as ICustomBrowserView; + const mainView = new BrowserView({ + ...windowHandler.getMainWindowOpts(), + ...getBounds(mainWinPos, DEFAULT_WIDTH, DEFAULT_HEIGHT), + }) as ICustomBrowserView; + + mainWindow.addBrowserView(titleBarView); + mainWindow.addBrowserView(mainView); + + const titleBarWindowUrl = format({ + pathname: require.resolve('../renderer/react-window.html'), + protocol: 'file', + query: { + componentName: 'title-bar', + locale: i18n.getLocale(), + }, + slashes: true, + }); + titleBarView.webContents.once('did-finish-load', () => { + if (!titleBarView || titleBarView.webContents.isDestroyed()) { + return; + } + titleBarView?.webContents.send('page-load', { + isWindowsOS, + locale: i18n.getLocale(), + resource: i18n.loadedResources, + isMainWindow: true, + }); + mainEvents.subscribeMultipleEvents( + TITLE_BAR_EVENTS, + titleBarView.webContents, + ); + + mainWindow?.on('enter-full-screen', () => { + if (!titleBarView || !viewExists(titleBarView)) { + return; + } + const titleBarBounds = titleBarView.getBounds(); + titleBarView.setBounds({ ...titleBarBounds, ...{ height: 0 } }); + + if ( + !mainView || + !viewExists(mainView) || + !mainWindow || + !windowExists(mainWindow) + ) { + return; + } + const mainWindowBounds = mainWindow.getBounds(); + const mainViewBounds = mainView.getBounds(); + mainView.setBounds({ + width: mainWindowBounds.width, + height: mainViewBounds.height, + x: 0, + y: 0, + }); + }); + mainWindow?.on('leave-full-screen', () => { + if (!titleBarView || !viewExists(titleBarView)) { + return; + } + const titleBarBounds = titleBarView.getBounds(); + titleBarView.setBounds({ ...titleBarBounds, ...{ height: 32 } }); + + if ( + !mainView || + !viewExists(mainView) || + !mainWindow || + !windowExists(mainWindow) + ) { + return; + } + const mainWindowBounds = mainWindow.getBounds(); + mainView.setBounds({ + width: mainWindowBounds.width, + height: mainWindowBounds.height, + x: mainWindowBounds.x, + y: 32, + }); + }); + if (mainWindow?.isMaximized()) { + mainEvents.publish('maximize'); + } + if (mainWindow?.isFullScreen()) { + mainEvents.publish('enter-full-screen'); + } + }); + await titleBarView.webContents.loadURL(titleBarWindowUrl); + titleBarView.setBounds({ + ...mainWindow.getBounds(), + ...{ x: 0, y: 0, height: 32 }, + }); + titleBarView.setAutoResize({ + vertical: false, + horizontal: true, + width: true, + height: false, + }); + + await mainView.webContents.loadURL(url, { userAgent }); + mainView.setBounds({ ...mainWindow.getBounds(), ...{ y: 32 } }); + mainView.setAutoResize({ + horizontal: true, + vertical: false, + width: true, + height: false, + }); + + windowHandler.setMainView(mainView); + windowHandler.setTitleBarView(mainView); + + return mainView.webContents; +}; diff --git a/src/common/api-interface.ts b/src/common/api-interface.ts index e7a15e87..64400741 100644 --- a/src/common/api-interface.ts +++ b/src/common/api-interface.ts @@ -48,6 +48,15 @@ export enum apiCmds { closeAllWrapperWindows = 'close-all-windows', setZoomLevel = 'set-zoom-level', autoUpdate = 'auto-update', + aboutAppClipBoardData = 'about-app-clip-board-data', + closeMainWindow = 'close-main-window', + minimizeMainWindow = 'minimize-main-window', + maximizeMainWindow = 'maximize-main-window', + unmaximizeMainWindow = 'unmaximize-main-window', + getCurrentOriginUrl = 'get-current-origin-url', + isAeroGlassEnabled = 'is-aero-glass-enabled', + showScreenSharePermissionDialog = 'show-screen-share-permission-dialog', + getMediaAccessStatus = 'get-media-access-status', } export enum apiName { @@ -89,6 +98,10 @@ export interface IApiArgs { theme: Themes; zoomLevel: number; filename: string; + clipboard: string; + clipboardType: 'clipboard' | 'selection'; + requestId: number; + mediaStatus: IMediaPermission; } export type Themes = 'light' | 'dark'; @@ -195,9 +208,14 @@ export interface IDownloadManager { } export interface IMediaPermission { - camera: string; - microphone: string; - screen: string; + camera: 'not-determined' | 'granted' | 'denied' | 'restricted' | 'unknown'; + microphone: + | 'not-determined' + | 'granted' + | 'denied' + | 'restricted' + | 'unknown'; + screen: 'not-determined' | 'granted' | 'denied' | 'restricted' | 'unknown'; } export interface ILogMsg { diff --git a/src/demo/index.html b/src/demo/index.html index 4fbfee3c..0d3227f5 100644 --- a/src/demo/index.html +++ b/src/demo/index.html @@ -45,7 +45,7 @@ - +

Symphony Electron API Demo

@@ -331,6 +331,8 @@ openConfigWin.addEventListener('click', function () { if (window.ssf) { ssf.showNotificationSettings(); + } else if (window.manaSSF) { + window.manaSSF.showNotificationSettings(); } else { postMessage(apiCmds.showNotificationSettings); } @@ -431,6 +433,8 @@ badgeCount++; if (window.ssf) { ssf.setBadgeCount(badgeCount); + } else if (window.manaSSF) { + window.manaSSF.setBadgeCount(badgeCount); } else { postMessage(apiCmds.setBadgeCount, badgeCount); } @@ -441,6 +445,8 @@ badgeCount = 0; if (window.ssf) { ssf.setBadgeCount(0); + } else if (window.manaSSF) { + window.manaSSF.setBadgeCount(0); } else { postMessage(apiCmds.setBadgeCount, 0); } @@ -448,21 +454,23 @@ const snippetButton = document.getElementById('snippet'); snippetButton.addEventListener('click', () => { - if (window.ssf) { - const gotSnippet = (rsp) => { - if (rsp) { - if (rsp.type && rsp.type === 'ERROR') { - // called when some error occurs during capture - alert('error getting snippet' + rsp.message); - } else if (rsp.data && rsp.type === 'image/png;base64') { - const dataUrl = 'data:' + rsp.type + ',' + rsp.data; - const img = document.getElementById('snippet-img'); - img.src = dataUrl; - } + const gotSnippet = (rsp) => { + if (rsp) { + if (rsp.type && rsp.type === 'ERROR') { + // called when some error occurs during capture + alert('error getting snippet' + rsp.message); + } else if (rsp.data && rsp.type === 'image/png;base64') { + const dataUrl = 'data:' + rsp.type + ',' + rsp.data; + const img = document.getElementById('snippet-img'); + img.src = dataUrl; } - }; + } + }; + if (window.ssf) { const screenSnippet = new window.ssf.ScreenSnippet(); screenSnippet.capture().then(gotSnippet).catch(gotSnippet); + } else if (window.manaSSF) { + window.manaSSF.openScreenSnippet(gotSnippet); } else { postMessage(apiCmds.openScreenSnippet); } @@ -473,6 +481,8 @@ if (window.ssf) { const screenSnippet = new window.ssf.ScreenSnippet(); screenSnippet.cancelCapture(); + } else if (window.manaSSF) { + window.manaSSF.closeScreenSnippet(); } else { postMessage(apiCmds.closeScreenSnippet); } @@ -504,6 +514,8 @@ front.addEventListener('click', () => { if (window.ssf) { window.ssf.activate(win.name); + } else if (window.manaSSF) { + window.manaSSF.activate(win.name); } else { postMessage(apiCmds.activate, win.name); } @@ -515,6 +527,8 @@ frontWithoutFocus.addEventListener('click', () => { if (window.ssf) { window.ssf.bringToFront(win.name, 'notification'); + } else if (window.manaSSF) { + window.manaSSF.bringToFront(win.name, 'notification'); } else { postMessage(apiCmds.bringToFront, { windowName: win.name, @@ -540,6 +554,15 @@ searchApiVer.innerText = versionInfo.searchApiVer; cpuArch.innerText = versionInfo.cpuArch; }); + } else if (window.manaSSF) { + window.manaSSF.getVersionInfo().then((versionInfo) => { + apiVersionInfo.innerText = versionInfo.apiVer; + containerIdentifier.innerText = versionInfo.containerIdentifier; + version.innerText = versionInfo.containerVer; + buildNumber.innerText = versionInfo.buildNumber; + searchApiVer.innerText = versionInfo.searchApiVer; + cpuArch.innerText = versionInfo.cpuArch; + }); } else { postRequest(apiCmds.getVersionInfo, null, { successCallback: (versionInfo) => { @@ -767,6 +790,8 @@ */ if (window.ssf) { window.ssf.registerRestartFloater(onRestartFloater); + } else if (window.manaSSF) { + window.manaSSF.registerRestartFloater(onRestartFloater); } else { postMessage(apiCmds.registerRestartFloater); } @@ -780,6 +805,9 @@ if (window.ssf) { window.ssf.registerActivityDetection(5000, activityCallback); startActivityInterval(); + } else if (window.manaSSF) { + window.manaSSF.registerActivityDetection(5000, activityCallback); + startActivityInterval(); } else { postMessage(apiCmds.registerActivityDetection, 5000); startActivityInterval(); @@ -850,6 +878,16 @@ microphonePermission.innerText = mediaPermission.microphone; screenPermission.innerText = mediaPermission.screen; }); + } else if (window.manaSSF) { + window.manaSSF.checkMediaPermission().then((mediaPermission) => { + console.log( + 'check-media-permission, mediaPermission: ' + + JSON.stringify(mediaPermission), + ); + cameraPermission.innerText = mediaPermission.camera; + microphonePermission.innerText = mediaPermission.microphone; + screenPermission.innerText = mediaPermission.screen; + }); } else { postRequest(apiCmds.checkMediaPermission, null, { successCallback: (mediaPermission) => { @@ -902,6 +940,32 @@ ); }, ); + } else if (window.manaSSF) { + window.manaSSF.getMediaSource( + { types: ['window', 'screen'] }, + function (error, source) { + if (error) throw error; + navigator.webkitGetUserMedia( + { + audio: false, + video: { + mandatory: { + chromeMediaSource: 'desktop', + chromeMediaSourceId: source.id, + minWidth: 1280, + maxWidth: 1280, + minHeight: 720, + maxHeight: 720, + }, + }, + }, + (stream) => { + handleStream(stream, source.display_id); + }, + handleError, + ); + }, + ); } else { const successHandler = (data) => { const constraints = { @@ -956,6 +1020,21 @@ } }, ); + } else if (window.manaSSF) { + window.manaSSF.showScreenSharingIndicator( + { + stream, + displayId, + }, + (event) => { + console.log('screen-sharing-indicator callback', event); + if (event.type === 'stopRequested') { + stream.getVideoTracks().forEach((t) => t.stop()); + document.getElementById('video').srcObject = null; + window.manaSSF.closeScreenSharingIndicator(stream.id); + } + }, + ); } else { const success = (data) => { if (data.error) { @@ -1052,6 +1131,8 @@ const id = downloadedItem.id; if (window.ssf) { window.ssf.openDownloadedItem(id); + } else if (window.manaSSF) { + window.manaSSF.openDownloadedItem(id); } else { postMessage(apiCmds.openDownloadedItem, id); } @@ -1066,6 +1147,8 @@ const id = downloadedItem.id; if (window.ssf) { window.ssf.showDownloadedItem(id); + } else if (window.manaSSF) { + window.manaSSF.showDownloadedItem(id); } else { postMessage(apiCmds.showDownloadedItem, id); } @@ -1077,6 +1160,8 @@ downloadedItem = undefined; if (window.ssf) { window.ssf.clearDownloadedItems(); + } else if (window.manaSSF) { + window.manaSSF.clearDownloadedItems(); } else { postMessage(apiCmds.clearDownloadedItems); } @@ -1085,6 +1170,8 @@ document.getElementById('restart-app').addEventListener('click', () => { if (window.ssf) { window.ssf.restartApp(); + } else if (window.manaSSF) { + window.manaSSF.restartApp(); } else { postMessage(apiCmds.restartApp); } diff --git a/src/renderer/app-bridge.ts b/src/renderer/app-bridge.ts index e7e37dd1..15e1f8dc 100644 --- a/src/renderer/app-bridge.ts +++ b/src/renderer/app-bridge.ts @@ -1,8 +1,8 @@ -import { remote } from 'electron'; - +import { ipcRenderer } from 'electron'; import { IAnalyticsData } from '../app/analytics-handler'; import { apiCmds, + apiName, IBoundsChange, ILogMsg, INotificationData, @@ -20,12 +20,12 @@ import { import { SSFApi } from './ssf-api'; const ssf = new SSFApi(); -const notification = remote.require('../renderer/notification').notification; let ssInstance: any; try { - const SSAPIBridge = remote.require('swift-search').SSAPIBridge; - ssInstance = new SSAPIBridge(); + // TODO: remove remote module + /*const SSAPIBridge = remote.require('swift-search').SSAPIBridge; + ssInstance = new SSAPIBridge();*/ } catch (e) { ssInstance = null; console.warn( @@ -47,7 +47,7 @@ export class AppBridge { return event.source && event.source === window; } - public origin: string; + public origin: string = ''; private readonly callbackHandlers = { onMessage: (event) => this.handleMessage(event), @@ -80,14 +80,22 @@ export class AppBridge { constructor() { // starts with corporate pod and // will be updated with the global config url - const currentWindow = remote.getCurrentWindow(); - // @ts-ignore - this.origin = currentWindow.origin || ''; - // this.origin = '*'; // DEMO-APP: Comment this line back in only to test demo-app - DO NOT COMMIT - if (ssInstance && typeof ssInstance.setBroadcastMessage === 'function') { - ssInstance.setBroadcastMessage(this.broadcastMessage); - } - window.addEventListener('message', this.callbackHandlers.onMessage); + ipcRenderer + .invoke(apiName.symphonyApi, { + cmd: apiCmds.getCurrentOriginUrl, + }) + .then((origin) => { + this.origin = origin; + // this.origin = '*'; // DEMO-APP: Comment this line back in only to test demo-app - DO NOT COMMIT + if ( + ssInstance && + typeof ssInstance.setBroadcastMessage === 'function' + ) { + ssInstance.setBroadcastMessage(this.broadcastMessage); + } + window.addEventListener('message', this.callbackHandlers.onMessage); + }) // tslint:disable-next-line:no-console + .catch((reason) => console.error(reason)); } /** @@ -200,13 +208,13 @@ export class AppBridge { ); break; case apiCmds.notification: - notification.showNotification( + ssf.showNotification( data as INotificationData, this.callbackHandlers.onNotificationCallback, ); break; case apiCmds.closeNotification: - await notification.hideNotification(data as number); + await ssf.closeNotification(data as number); break; case apiCmds.showNotificationSettings: ssf.showNotificationSettings(data); diff --git a/src/renderer/components/about-app.tsx b/src/renderer/components/about-app.tsx index 1e900e0f..1437b3ce 100644 --- a/src/renderer/components/about-app.tsx +++ b/src/renderer/components/about-app.tsx @@ -1,5 +1,7 @@ -import { ipcRenderer, remote } from 'electron'; +import { ipcRenderer } from 'electron'; import * as React from 'react'; +import { productName } from '../../../package.json'; +import { apiCmds, apiName } from '../../common/api-interface'; import { i18n } from '../../common/i18n-preload'; interface IState { userConfig: object; @@ -86,7 +88,7 @@ export default class AboutApp extends React.Component<{}, IState> { client, } = this.state; - const appName = remote.app.getName() || 'Symphony'; + const appName = productName || 'Symphony'; const copyright = `\xA9 ${new Date().getFullYear()} ${appName}`; const podVersion = `${clientVersion} (${buildNumber})`; const sdaVersionBuild = `${sdaVersion} (${sdaBuildNumber})`; @@ -165,10 +167,11 @@ export default class AboutApp extends React.Component<{}, IState> { const { clientVersion, ...rest } = this.state; const data = { ...{ sbeVersion: clientVersion }, ...rest }; if (data) { - remote.clipboard.write( - { text: JSON.stringify(data, null, 4) }, - 'clipboard', - ); + ipcRenderer.send(apiName.symphonyApi, { + cmd: apiCmds.aboutAppClipBoardData, + clipboard: data, + clipboardType: 'clipboard', + }); } } diff --git a/src/renderer/components/loading-screen.tsx b/src/renderer/components/loading-screen.tsx index 82fa412f..688738a2 100644 --- a/src/renderer/components/loading-screen.tsx +++ b/src/renderer/components/loading-screen.tsx @@ -1,4 +1,4 @@ -import { ipcRenderer, remote } from 'electron'; +import { ipcRenderer } from 'electron'; import * as React from 'react'; import { i18n } from '../../common/i18n-preload'; @@ -47,7 +47,7 @@ export default class LoadingScreen extends React.Component<{}, IState> { */ public render(): JSX.Element { const { error } = this.state; - const appName = remote.app.getName() || 'Symphony'; + const appName = 'Symphony'; if (error) { return this.renderErrorContent(error); diff --git a/src/renderer/components/screen-sharing-indicator.tsx b/src/renderer/components/screen-sharing-indicator.tsx index dacfe461..d575111c 100644 --- a/src/renderer/components/screen-sharing-indicator.tsx +++ b/src/renderer/components/screen-sharing-indicator.tsx @@ -1,6 +1,7 @@ import classNames from 'classnames'; -import { ipcRenderer, remote } from 'electron'; +import { ipcRenderer } from 'electron'; import * as React from 'react'; +import { productName } from '../../../package.json'; import { apiCmds, apiName } from '../../common/api-interface'; import { isMac } from '../../common/env'; @@ -40,6 +41,7 @@ export default class ScreenSharingIndicator extends React.Component< public render(): JSX.Element { const { id } = this.state; const namespace = 'ScreenSharingIndicator'; + const appName = productName || 'Symphony'; return (

@@ -48,9 +50,9 @@ export default class ScreenSharingIndicator extends React.Component< .t( `You are sharing your screen on {appName}`, namespace, - )({ appName: remote.app.getName() }) - .replace(remote.app.getName(), '')} -  {remote.app.getName()} + )({ appName }) + .replace(appName, '')} +  {appName}