diff --git a/.eslintignore b/.eslintignore index 63f7d6e5a1..c7a22ef144 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,4 @@ node_modules +electron/dist electron/node_modules electron/renderer/dist diff --git a/.gitignore b/.gitignore index 5f7f8e30ea..5d0858b44c 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,8 @@ /.idea /keys /electron/renderer/dist +/electron/dist +/electron/node_modules /temp /wrap app.asar diff --git a/.prettierrc.json b/.prettierrc.json index afb3856712..620840934f 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -12,7 +12,7 @@ "useTabs": false, "overrides": [ { - "files": ["*.json", "electron/locale/strings-*.js"], + "files": ["*.json", "electron/locale/strings-*.ts"], "options": { "printWidth": 200 } diff --git a/electron/js/certificateUtils.js b/electron/js/certificateUtils.js deleted file mode 100644 index 2ae7ee873f..0000000000 --- a/electron/js/certificateUtils.js +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Wire - * Copyright (C) 2018 Wire Swiss GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/. - * - */ - -const crypto = require('crypto'); -const rs = require('jsrsasign'); - -const WILDCARD_CERT_FINGERPRINT = '3pHQns2wdYtN4b2MWsMguGw70gISyhBZLZDpbj+EmdU='; -const MULTIDOMAIN_CERT_FINGERPRINT = 'bORoZ2vRsPJ4WBsUdL1h3Q7C50ZaBqPwngDmDVw+wHA='; -const CERT_ALGORITHM_RSA = '2a864886f70d010101'; -// eslint-disable-next-line no-unused-vars -const PUBLIC_KEY_VERISIGN_CLASS3_G5_ROOT = - '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAryQICCl6NZ5gDKrnSztO\n3Hy8PEUcuyvg/ikC+VcIo2SFFSf18a3IMYldIugqqqZCs4/4uVW3sbdLs/6PfgdX\n7O9D22ZiFWHPYA2k2N744MNiCD1UE+tJyllUhSblK48bn+v1oZHCM0nYQ2NqUkvS\nj+hwUU3RiWl7x3D2s9wSdNt7XUtW05a/FXehsPSiJfKvHJJnGOX0BgTvkLnkAOTd\nOrUZ/wK69Dzu4IvrN4vs9Nes8vbwPa/ddZEzGR0cQMt0JBkhk9kU/qwqUseP1QRJ\n5I1jR4g8aYPL/ke9K35PxZWuDp3U0UPAZ3PjFAh+5T+fc7gzCs9dPzSHloruU+gl\nFQIDAQAB\n-----END PUBLIC KEY-----\n'; -const PUBLIC_KEY_DIGICERT_GLOBAL_ROOT_G2 = - '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQq2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5WztCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQvIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQAB\n-----END PUBLIC KEY-----'; -const pins = [ - { - publicKeyInfo: [ - { - algorithmID: CERT_ALGORITHM_RSA, - algorithmParam: null, - fingerprints: [MULTIDOMAIN_CERT_FINGERPRINT, WILDCARD_CERT_FINGERPRINT], - }, - ], - url: /^app\.wire\.com$/i, - }, - { - publicKeyInfo: [ - { - algorithmID: CERT_ALGORITHM_RSA, - algorithmParam: null, - fingerprints: [MULTIDOMAIN_CERT_FINGERPRINT, WILDCARD_CERT_FINGERPRINT], - }, - ], - url: /^(www\.)?wire\.com$/i, - }, - { - publicKeyInfo: [ - { - algorithmID: CERT_ALGORITHM_RSA, - algorithmParam: null, - fingerprints: [WILDCARD_CERT_FINGERPRINT], - }, - ], - url: /^prod-(assets|nginz-https|nginz-ssl)\.wire\.com$/i, - }, - { - issuerRootPubkeys: [PUBLIC_KEY_DIGICERT_GLOBAL_ROOT_G2], - publicKeyInfo: [], - url: /^[a-z0-9]{14,63}\.cloudfront\.net$/i, - }, -]; - -module.exports = { - hostnameShouldBePinned: hostname => pins.some(pin => pin.url.test(hostname.toLowerCase().trim())), - - verifyPinning: (hostname, certificate) => { - const {data: certData = '', issuerCert: {data: issuerCertData = ''} = {}} = certificate; - let issuerCertHex; - let publicKey; - let publicKeyBytes; - let publicKeyFingerprint; - - try { - issuerCertHex = rs.pemtohex(issuerCertData); - publicKey = rs.X509.getPublicKeyInfoPropOfCertPEM(certData); - publicKeyBytes = Buffer.from(publicKey.keyhex, 'hex').toString('binary'); - publicKeyFingerprint = crypto - .createHash('sha256') - .update(publicKeyBytes) - .digest('base64'); - } catch (error) { - console.error(`Certificate verification failed: ${error.message}`, error); - return {decoding: false}; - } - - const result = {}; - - const errorMessages = []; - - for (const pin of pins) { - const {url, publicKeyInfo = [], issuerRootPubkeys = []} = pin; - - if (url.test(hostname.toLowerCase().trim())) { - if (issuerRootPubkeys.length > 0) { - const x509 = new rs.X509(); - x509.readCertHex(issuerCertHex); - - result.verifiedIssuerRootPubkeys = issuerRootPubkeys.some(rawPublicKey => { - const x509PublicKey = rs.KEYUTIL.getKey(rawPublicKey); - return x509.verifySignature(x509PublicKey); - }); - if (!result.verifiedIssuerRootPubkeys) { - errorMessages.push( - `Issuer root public key signatures: none of "${issuerRootPubkeys.join(', ')}" could be verified.` - ); - } - } - - result.verifiedPublicKeyInfo = publicKeyInfo - .reduce((arr, pubkey) => { - const { - fingerprints: knownFingerprints = [], - algorithmID: knownAlgorithmID = '', - algorithmParam: knownAlgorithmParam = null, - } = pubkey; - - const fingerprintCheck = - knownFingerprints.length > 0 - ? knownFingerprints.some(knownFingerprint => knownFingerprint === publicKeyFingerprint) - : undefined; - const algorithmIDCheck = knownAlgorithmID === publicKey.algoid; - const algorithmParamCheck = knownAlgorithmParam === publicKey.algparam; - - if (!fingerprintCheck) { - errorMessages.push( - `Public key fingerprints: "${publicKeyFingerprint}" could not be verified with any of the known fingerprints "${knownFingerprints.join( - ', ' - )}".` - ); - } - - if (!algorithmIDCheck) { - errorMessages.push( - `Algorithm ID: "${publicKey.algoid}" could not be verified with the known ID "${knownAlgorithmID}".` - ); - } - - if (!algorithmParamCheck) { - errorMessages.push( - `Algorithm parameter: "${ - publicKey.algparam - }" could not be verified with the known parameter "${knownAlgorithmParam}".` - ); - } - - arr.push(fingerprintCheck, algorithmIDCheck, algorithmParamCheck); - - return arr; - }, []) - .every(value => Boolean(value)); - - if (errorMessages.length > 0) { - result.errorMessage = errorMessages.join('\n'); - } - - break; - } - } - - return result; - }, -}; diff --git a/electron/js/config.js b/electron/js/config.js deleted file mode 100644 index 51f310ccba..0000000000 --- a/electron/js/config.js +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Wire - * Copyright (C) 2018 Wire Swiss GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/. - * - */ - -const pkg = require('./../package.json'); - -const config = { - EMBED_DOMAINS: [ - { - allowedExternalLinks: ['www.youtube.com'], - hostname: ['www.youtube-nocookie.com'], - name: 'YouTube', - }, - { - allowedExternalLinks: ['vimeo.com', 'player.vimeo.com'], - hostname: ['player.vimeo.com'], - name: 'Vimeo', - }, - { - allowedExternalLinks: ['soundcloud.com'], - hostname: ['w.soundcloud.com'], - name: 'SoundCloud', - }, - { - allowedExternalLinks: ['www.spotify.com', 'developer.spotify.com'], - hostname: ['open.spotify.com', 'embed.spotify.com'], - name: 'Spotify', - }, - ], - - GOOGLE_CLIENT_ID: '', - GOOGLE_CLIENT_SECRET: '', - GOOGLE_SCOPES: 'https://www.googleapis.com/auth/contacts.readonly', - - LOG_FILE_NAME: 'console.log', - - NAME: pkg.productName, - - RAYGUN_API_KEY: '', - - SPELLCHECK: { - SUGGESTIONS: 4, - SUPPORTED_LANGUAGES: ['en'], - }, - - UPDATE: { - DELAY: 5 * 60 * 1000, - INTERVAL: 24 * 60 * 60 * 1000, - }, - - URL: { - LEGAL: '/legal/', - LICENSES: '/legal/licenses/', - PRIVACY: '/privacy/', - }, - - VERSION: pkg.version, - - WINDOW: { - ABOUT: { - HEIGHT: 256, - WIDTH: 304, - }, - AUTH: { - HEIGHT: 576, - WIDTH: 400, - }, - MAIN: { - DEFAULT_HEIGHT: 768, - DEFAULT_WIDTH: 1024, - MIN_HEIGHT: 512, - MIN_WIDTH: 760, - }, - }, -}; - -module.exports = config; diff --git a/electron/js/menu/TrayHandler.js b/electron/js/menu/TrayHandler.js deleted file mode 100644 index d39a26ed0f..0000000000 --- a/electron/js/menu/TrayHandler.js +++ /dev/null @@ -1,81 +0,0 @@ -const {app, Menu, nativeImage, Tray} = require('electron'); -const config = require('./../config'); -const lifecycle = require('./../lifecycle'); -const locale = require('./../../locale/locale'); -const path = require('path'); -const windowManager = require('./../window-manager'); - -function buildTrayMenu() { - const contextMenu = Menu.buildFromTemplate([ - { - click: () => windowManager.showPrimaryWindow(), - label: locale.getText('trayOpen'), - }, - { - click: async () => await lifecycle.quit(), - label: locale.getText('trayQuit'), - }, - ]); - - this.trayIcon.on('click', () => windowManager.showPrimaryWindow()); - this.trayIcon.setContextMenu(contextMenu); - this.trayIcon.setToolTip(config.NAME); -} - -function flashApplicationWindow(win, count) { - if (win.isFocused() || !count) { - win.flashFrame(false); - } else if (count > this.lastUnreadCount) { - win.flashFrame(true); - } -} - -function updateBadgeCount(count) { - app.setBadgeCount(count); - this.lastUnreadCount = count; -} - -function updateIcons(win, count) { - if (this.icons) { - const trayImage = count ? this.icons.trayWithBadge : this.icons.tray; - this.trayIcon.setImage(trayImage); - - const overlayImage = count ? this.icons.badge : null; - win.setOverlayIcon(overlayImage, locale.getText('unreadMessages')); - } -} - -class TrayHandler { - constructor() { - this.lastUnreadCount = 0; - } - - initTray(trayIcon = new Tray(nativeImage.createEmpty())) { - const IMAGE_ROOT = path.join(app.getAppPath(), 'img'); - - const iconPaths = { - badge: path.join(IMAGE_ROOT, 'taskbar.overlay.png'), - tray: path.join(IMAGE_ROOT, 'tray-icon', 'tray', 'tray.png'), - trayWithBadge: path.join(IMAGE_ROOT, 'tray-icon', 'tray-with-badge', 'tray.badge.png'), - }; - - this.icons = { - badge: nativeImage.createFromPath(iconPaths.badge), - tray: nativeImage.createFromPath(iconPaths.tray), - trayWithBadge: nativeImage.createFromPath(iconPaths.trayWithBadge), - }; - - this.trayIcon = trayIcon; - this.trayIcon.setImage(this.icons.tray); - - buildTrayMenu.call(this); - } - - showUnreadCount(win, count) { - updateIcons.call(this, win, count); - flashApplicationWindow.call(this, win, count); - updateBadgeCount.call(this, count); - } -} - -module.exports = TrayHandler; diff --git a/electron/js/preload-about.js b/electron/js/preload-about.js deleted file mode 100644 index c3068bd4c1..0000000000 --- a/electron/js/preload-about.js +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Wire - * Copyright (C) 2018 Wire Swiss GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/. - * - */ - -const {ipcRenderer} = require('electron'); -const EVENT_TYPE = require('./lib/eventType'); - -ipcRenderer.once(EVENT_TYPE.ABOUT.LOCALE_RENDER, (sender, labels) => { - for (const label in labels) { - document.querySelector(`[data-string="${label}"]`).innerHTML = labels[label]; - } -}); - -ipcRenderer.once(EVENT_TYPE.ABOUT.LOADED, (sender, details) => { - document.getElementById('name').innerHTML = details.productName; - document.getElementById('version').innerHTML = details.electronVersion || 'Development'; - - if (details.webappVersion) { - document.getElementById('webappVersion').innerHTML = details.webappVersion; - } else { - document.getElementById('webappVersion').parentNode.remove(); - } - - // Get locales - const labels = []; - for (const label of document.querySelectorAll('[data-string]')) { - labels.push(label.dataset.string); - } - ipcRenderer.send(EVENT_TYPE.ABOUT.LOCALE_VALUES, labels); -}); diff --git a/electron/js/util.js b/electron/js/util.js deleted file mode 100644 index b0fafd93f4..0000000000 --- a/electron/js/util.js +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Wire - * Copyright (C) 2018 Wire Swiss GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/. - * - */ - -const electron = require('electron'); -const url = require('url'); - -const pointInRectangle = require('./lib/pointInRect'); - -module.exports = { - capitalize: input => input.charAt(0).toUpperCase() + input.substr(1), - - isInView: win => { - const windowBounds = win.getBounds(); - const nearestWorkArea = electron.screen.getDisplayMatching(windowBounds).workArea; - - const upperLeftVisible = pointInRectangle([windowBounds.x, windowBounds.y], nearestWorkArea); - const lowerRightVisible = pointInRectangle( - [windowBounds.x + windowBounds.width, windowBounds.y + windowBounds.height], - nearestWorkArea - ); - - return upperLeftVisible || lowerRightVisible; - }, - - isMatchingHost: (_url, _baseUrl) => url.parse(_url).host === url.parse(_baseUrl).host, -}; diff --git a/electron/locale/locale.js b/electron/locale/locale.js deleted file mode 100644 index 01b4125e97..0000000000 --- a/electron/locale/locale.js +++ /dev/null @@ -1,128 +0,0 @@ -/* eslint-disable sort-keys */ -/* - * Wire - * Copyright (C) 2018 Wire Swiss GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/. - * - */ - -const app = require('electron').app || require('electron').remote.app; -const settings = require('../js/settings/ConfigurationPersistence'); -const SettingsType = require('../js/settings/SettingsType'); - -const cs = require('./strings-cs'); -const da = require('./strings-da'); -const de = require('./strings-de'); -const el = require('./strings-el'); -const en = require('./strings-en'); -const es = require('./strings-es'); -const et = require('./strings-et'); -const fi = require('./strings-fi'); -const fr = require('./strings-fr'); -const hr = require('./strings-hr'); -const hu = require('./strings-hu'); -const it = require('./strings-it'); -const lt = require('./strings-lt'); -const nl = require('./strings-nl'); -const pl = require('./strings-pl'); -const pt = require('./strings-pt'); -const ro = require('./strings-ro'); -const ru = require('./strings-ru'); -const sk = require('./strings-sk'); -const sl = require('./strings-sl'); -const tr = require('./strings-tr'); -const uk = require('./strings-uk'); - -const SUPPORTED_LANGUAGES = { - en: 'English', - cs: 'Čeština', - da: 'Dansk', - de: 'Deutsch', - el: 'Ελληνικά', - et: 'Eesti', - es: 'Español', - fr: 'Français', - hr: 'Hrvatski', - it: 'Italiano', - lt: 'Lietuvos', - hu: 'Magyar', - nl: 'Nederlands', - pl: 'Polski', - pt: 'Português do Brasil', - ro: 'Română', - ru: 'Русский', - sk: 'Slovenčina', - sl: 'Slovenščina', - fi: 'Suomi', - tr: 'Türkçe', - uk: 'Українська', -}; - -let current; - -const getSupportedLanguageKeys = () => Object.keys(SUPPORTED_LANGUAGES); - -const getCurrent = () => { - if (!current) { - // We care only about the language part and not the country (en_US, de_DE) - const defaultLocale = parseLocale(app.getLocale().substr(0, 2)); - current = settings.restore(SettingsType.LOCALE, defaultLocale); - } - return current; -}; - -const parseLocale = locale => { - const languageKeys = getSupportedLanguageKeys(); - return languageKeys.find(languageKey => languageKey === locale) || languageKeys[0]; -}; - -const getText = string_identifier => { - const strings = eval(getCurrent()); - return strings[string_identifier] || en[string_identifier] || ''; -}; - -const setLocale = locale => { - current = parseLocale(locale); - settings.save(SettingsType.LOCALE, current); -}; - -module.exports = { - cs, - da, - de, - el, - en, - es, - et, - fi, - fr, - hr, - hu, - it, - lt, - nl, - pl, - pt, - ro, - ru, - sk, - sl, - tr, - uk, - getCurrent, - getText, - setLocale, - SUPPORTED_LANGUAGES, -}; diff --git a/electron/package-lock.json b/electron/package-lock.json index aa3d3ea55d..73d7f4f788 100644 --- a/electron/package-lock.json +++ b/electron/package-lock.json @@ -3,6 +3,93 @@ "requires": true, "lockfileVersion": 1, "dependencies": { + "@types/auto-launch": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/auto-launch/-/auto-launch-5.0.0.tgz", + "integrity": "sha512-yxT2pVPPhFJNuiYX9kyAqRBA4EV7J/QeA/igZK6c4b/hS5Jn71R/Jl5J866WHNG4+r8uteLo3gojSh0sZmlqew==", + "dev": true + }, + "@types/caseless": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.1.tgz", + "integrity": "sha512-FhlMa34NHp9K5MY1Uz8yb+ZvuX0pnvn3jScRSNAb75KHGB8d3rEU6hqMs3Z2vjuytcMfRg6c5CHMc3wtYyD2/A==", + "dev": true + }, + "@types/debug": { + "version": "0.0.31", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-0.0.31.tgz", + "integrity": "sha512-LS1MCPaQKqspg7FvexuhmDbWUhE2yIJ+4AgVIyObfc06/UKZ8REgxGNjZc82wPLWmbeOm7S+gSsLgo75TanG4A==", + "dev": true + }, + "@types/file-url": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/file-url/-/file-url-2.0.0.tgz", + "integrity": "sha512-9YqUM3izkmwtCbq6ANdcHJiU2mpDvPm3WuHOcJ5ZouHw7CoYcyY/KvZm6dmYTIurfrRsmBIvHr6y1jpBjDd4Jg==", + "dev": true + }, + "@types/form-data": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-2.2.1.tgz", + "integrity": "sha512-JAMFhOaHIciYVh8fb5/83nmuO/AHwmto+Hq7a9y8FzLDcC1KCU344XDOMEmahnrTFlHjgh4L0WJFczNIX2GxnQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/fs-extra": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-5.0.4.tgz", + "integrity": "sha512-DsknoBvD8s+RFfSGjmERJ7ZOP1HI0UZRA3FSI+Zakhrc/Gy26YQsLI+m5V5DHxroHRJqCDLKJp7Hixn8zyaF7g==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/lodash": { + "version": "4.14.117", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.117.tgz", + "integrity": "sha512-xyf2m6tRbz8qQKcxYZa7PA4SllYcay+eh25DN3jmNYY6gSTL7Htc/bttVdkqj2wfJGbeWlQiX8pIyJpKU+tubw==", + "dev": true + }, + "@types/minimist": { + "version": "1.2.0", + "resolved": "http://registry.npmjs.org/@types/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY=", + "dev": true + }, + "@types/node": { + "version": "10.11.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.11.7.tgz", + "integrity": "sha512-yOxFfkN9xUFLyvWaeYj90mlqTJ41CsQzWKS3gXdOMOyPVacUsymejKxJ4/pMW7exouubuEeZLJawGgcNGYlTeg==", + "dev": true + }, + "@types/request": { + "version": "2.47.1", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.47.1.tgz", + "integrity": "sha512-TV3XLvDjQbIeVxJ1Z3oCTDk/KuYwwcNKVwz2YaT0F5u86Prgc4syDAp6P96rkTQQ4bIdh+VswQIC9zS6NjY7/g==", + "dev": true, + "requires": { + "@types/caseless": "*", + "@types/form-data": "*", + "@types/node": "*", + "@types/tough-cookie": "*" + } + }, + "@types/tough-cookie": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-2.3.3.tgz", + "integrity": "sha512-MDQLxNFRLasqS4UlkWMSACMKeSm1x4Q3TxzUC7KQUsh6RK1ZrQ0VEyE3yzXcBu+K8ejVj4wuX32eUG02yNp+YQ==", + "dev": true + }, + "@types/uuid": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.4.tgz", + "integrity": "sha512-tPIgT0GUmdJQNSHxp0X2jnpQfBSTfGxUMc/2CXBU2mnyTFVYVa2ojpoQ74w0U2yn2vw3jnC640+77lkFFpdVDw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "ajv": { "version": "5.5.2", "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", @@ -596,9 +683,9 @@ "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" }, "lodash-es": { - "version": "4.17.4", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.4.tgz", - "integrity": "sha1-3MHXVS4VCgZABzupyzHXDwMpUOc=" + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.11.tgz", + "integrity": "sha512-DHb1ub+rMjjrxqlB3H56/6MXtm1lSksDp2rA2cNWjG8mlDUYFhUj3Di2Zn5IwSU87xLv8tNIQ7sSwE/YOX/D/Q==" }, "lodash.assignin": { "version": "4.2.0", @@ -912,7 +999,7 @@ "readable-stream": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", - "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", + "integrity": "sha1-No8lEtefnUb9/HE0mueHi7weuVw=", "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -984,7 +1071,7 @@ "safe-buffer": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + "integrity": "sha1-iTMSr2myEj3vcfV4iQAWce6yyFM=" }, "safer-buffer": { "version": "2.1.2", @@ -1059,15 +1146,15 @@ "string_decoder": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "integrity": "sha1-D8Z9fBQYJd6UKC3VNr7GubzoYKs=", "requires": { "safe-buffer": "~5.1.0" } }, "symbol-observable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.1.0.tgz", - "integrity": "sha512-dQoid9tqQ+uotGhuTKEY11X4xhyYePVnqGSoSm3OGKh2E8LZ6RPULp1uXTctk33IeERlrRJYoVSBglsL05F5Uw==" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" }, "tough-cookie": { "version": "2.4.3", @@ -1143,7 +1230,7 @@ }, "winston": { "version": "git+https://github.com/wireapp/winston.git#6526c40fdf9ef4108091aac298ea954bb26493ae", - "from": "git+https://github.com/wireapp/winston.git#6526c40fdf9ef4108091aac298ea954bb26493ae", + "from": "git+https://github.com/wireapp/winston.git#2.2.0-e", "requires": { "async": "~1.0.0", "colors": "1.0.x", @@ -1156,7 +1243,7 @@ "dependencies": { "async": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async/-/async-1.0.0.tgz", + "resolved": "http://registry.npmjs.org/async/-/async-1.0.0.tgz", "integrity": "sha1-+PwEyjoTeErenhZBr5hXjPvWR6k=" } } diff --git a/electron/package.json b/electron/package.json index 32f6499024..6a2e2ecd27 100644 --- a/electron/package.json +++ b/electron/package.json @@ -27,12 +27,21 @@ }, "description": "The most secure collaboration platform.", "devDependencies": { + "@types/auto-launch": "5.0.0", + "@types/debug": "0.0.31", + "@types/file-url": "2.0.0", + "@types/fs-extra": "5.0.4", + "@types/lodash": "4.14.117", + "@types/minimist": "1.2.0", + "@types/node": "10.11.7", + "@types/request": "2.47.1", + "@types/uuid": "3.4.4", "cross-spawn": "6.0.5" }, "environment": "internal", "homepage": "https://wire.com", "license": "GPL-3.0", - "main": "main.js", + "main": "dist/main.js", "name": "wireinternal", "optionalDependencies": { "node-addressbook": "https://github.com/wireapp/node-addressbook.git#2.0.0" diff --git a/electron/renderer/src/actions/index.js b/electron/renderer/src/actions/index.js index 7747a3449e..09dafb4089 100644 --- a/electron/renderer/src/actions/index.js +++ b/electron/renderer/src/actions/index.js @@ -29,8 +29,8 @@ export const updateAccount = (id, data) => ({ type: UPDATE_ACCOUNT, }); -export const updateAccountLifecycle = (id, data) => ({ - data, +export const updateAccountLifecycle = (id, channel) => ({ + data: channel, id, type: UPDATE_ACCOUNT_LIFECYCLE, }); @@ -88,7 +88,7 @@ export const addAccountWithSession = () => { }; export const updateAccountData = (id, data) => { - return (dispatch, getState) => { + return dispatch => { const validatedAccountData = verifyObjectProperties(data, { accentID: 'Number', name: 'String', diff --git a/electron/renderer/src/components/App.jsx b/electron/renderer/src/components/App.jsx index 261b742da6..edbfa38964 100644 --- a/electron/renderer/src/components/App.jsx +++ b/electron/renderer/src/components/App.jsx @@ -33,8 +33,10 @@ const App = props => ( onKeyDown={event => { const modKeyPressed = (window.isMac && event.metaKey) || event.ctrlKey; const isValidKey = ['1', '2', '3'].includes(event.key); - if (modKeyPressed && isValidKey && props.accountIds[event.key - 1]) { - props.switchAccount(props.accountIds[event.key - 1]); + const accountIndex = parseInt(event.key, 10) - 1; + const accountId = props.accountIds[accountIndex]; + if (modKeyPressed && isValidKey && accountId) { + props.switchAccount(accountId); } }} > diff --git a/electron/renderer/src/components/Sidebar.jsx b/electron/renderer/src/components/Sidebar.jsx index 7ed4917fac..ff794a0bb7 100644 --- a/electron/renderer/src/components/Sidebar.jsx +++ b/electron/renderer/src/components/Sidebar.jsx @@ -35,8 +35,10 @@ import { import './Sidebar.css'; const centerOfEventTarget = event => { - const cRect = event.target.getBoundingClientRect(); - return [cRect.left + cRect.width / 2, cRect.top + cRect.height / 2]; + const clientRectangle = event.currentTarget.getBoundingClientRect(); + const centerX = clientRectangle.left + clientRectangle.width / 2; + const centerY = clientRectangle.top + clientRectangle.height / 2; + return {x: centerX, y: centerY}; }; const getClassName = account => { @@ -70,8 +72,10 @@ const Sidebar = ({ const isAtLeastAdmin = ['z.team.TeamRole.ROLE.OWNER', 'z.team.TeamRole.ROLE.ADMIN'].includes( account.teamRole ); + const center = centerOfEventTarget(event); connected.toggleEditAccountMenuVisibility( - ...centerOfEventTarget(event), + center.x, + center.y, account.id, account.sessionID, account.lifecycle, @@ -101,7 +105,7 @@ export default connect( currentAccentID: (accounts.find(account => account.visible) || {}).accentID, hasCreatedAccount: accounts.some(account => account.userID !== undefined), hasReachedLimitOfAccounts: accounts.length >= 3, - isAddingAccount: accounts.length && accounts.some(account => account.userID === undefined), + isAddingAccount: !!accounts.length && accounts.some(account => account.userID === undefined), isEditAccountMenuVisible: contextMenuState.isEditAccountMenuVisible, }), { diff --git a/electron/renderer/src/components/Webview.jsx b/electron/renderer/src/components/Webview.jsx index 1870807aec..978066d942 100644 --- a/electron/renderer/src/components/Webview.jsx +++ b/electron/renderer/src/components/Webview.jsx @@ -58,7 +58,7 @@ class Webview extends Component { } _focusWebview() { - if (this.props.visible) { + if (this.props.visible && this.webview) { this.webview.focus(); } } diff --git a/electron/renderer/src/components/Webviews.jsx b/electron/renderer/src/components/Webviews.jsx index a7628d0fdf..d63546020e 100644 --- a/electron/renderer/src/components/Webviews.jsx +++ b/electron/renderer/src/components/Webviews.jsx @@ -26,9 +26,9 @@ class Webviews extends Component { constructor(props) { super(props); this.state = { - canDelete: this.getCanDeletes(props.accounts), + canDelete: this._getCanDeletes(props.accounts), }; - this.getCanDeletes = this.getCanDeletes.bind(this); + this._getCanDeletes = this._getCanDeletes.bind(this); this._onUnreadCountUpdated = this._onUnreadCountUpdated.bind(this); this._onIpcMessage = this._onIpcMessage.bind(this); this._onWebviewClose = this._onWebviewClose.bind(this); @@ -36,7 +36,7 @@ class Webviews extends Component { } componentWillReceiveProps(nextProps) { - this.setState({canDelete: this.getCanDeletes(nextProps.accounts)}); + this.setState({canDelete: this._getCanDeletes(nextProps.accounts)}); } shouldComponentUpdate(nextProps, nextState) { @@ -49,7 +49,7 @@ class Webviews extends Component { return JSON.stringify(nextState.canDelete) !== JSON.stringify(this.state.canDelete); } - getCanDeletes(accounts) { + _getCanDeletes(accounts) { return accounts.reduce( (accumulator, account) => ({ ...accumulator, @@ -60,8 +60,10 @@ class Webviews extends Component { } _getEnvironmentUrl(account, forceLogin) { - const envParam = decodeURIComponent(new URL(window.location).searchParams.get('env')); - const url = new URL(envParam); + const currentLocation = new URL(window.location.href); + const envParam = currentLocation.searchParams.get('env'); + const decodedEnvParam = decodeURIComponent(envParam); + const url = new URL(decodedEnvParam); // pass account id to webview so we can access it in the preload script url.searchParams.set('id', account.id); diff --git a/electron/renderer/static/webview-preload.js b/electron/renderer/static/webview-preload.js index 21109f528d..65dffefe9a 100644 --- a/electron/renderer/static/webview-preload.js +++ b/electron/renderer/static/webview-preload.js @@ -17,12 +17,12 @@ * */ -const config = require('../../js/config'); -const environment = require('../../js/environment'); +const config = require('../../dist/js/config'); +const environment = require('../../dist/js/environment'); const fs = require('fs-extra'); const path = require('path'); const winston = require('winston'); -const EVENT_TYPE = require('../../js/lib/eventType'); +const {EVENT_TYPE} = require('../../dist/js/lib/eventType'); const {desktopCapturer, ipcRenderer, remote, webFrame} = require('electron'); const {app} = remote; @@ -150,7 +150,8 @@ const replaceGoogleAuth = () => { }; const enableFileLogging = () => { - const id = new URL(window.location).searchParams.get('id'); + const currentLocation = new URL(window.location.href); + const id = currentLocation.searchParams.get('id'); if (id) { const logFilePath = path.join(app.getPath('userData'), 'logs', id, config.LOG_FILE_NAME); @@ -194,7 +195,7 @@ process.once('loaded', () => { global.setImmediate = _setImmediate; global.desktopCapturer = desktopCapturer; global.environment = environment; - global.openGraph = require('../../js/lib/openGraph'); + global.openGraph = require('../../dist/js/lib/openGraph'); global.notification_icon = path.join(app.getAppPath(), 'img', 'notification.png'); enableFileLogging(); }); @@ -209,6 +210,6 @@ window.addEventListener('DOMContentLoaded', () => { reportWebappVersion(); // include context menu - require('../../js/menu/context'); + require('../../dist/js/menu/context'); }); }); diff --git a/electron/src/interfaces/global.ts b/electron/src/interfaces/global.ts new file mode 100644 index 0000000000..a1119dbd53 --- /dev/null +++ b/electron/src/interfaces/global.ts @@ -0,0 +1,37 @@ +/* + * Wire + * Copyright (C) 2018 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {Supportedi18nStrings} from './locale'; + +declare global { + interface Window { + isMac: boolean; + locStrings: Supportedi18nStrings; + locStringsDefault: Supportedi18nStrings; + sendBadgeCount: (count: number) => void; + sendDeleteAccount: (accountId: string, sessionId?: string) => void; + sendLogoutAccount: (accountId: string) => void; + } + + namespace NodeJS { + interface Global { + _ConfigurationPersistence: any; + } + } +} diff --git a/electron/src/interfaces/index.ts b/electron/src/interfaces/index.ts new file mode 100644 index 0000000000..973f867313 --- /dev/null +++ b/electron/src/interfaces/index.ts @@ -0,0 +1,23 @@ +/* + * Wire + * Copyright (C) 2018 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +export * from './global'; +export * from './locale'; +export * from './main'; +export * from './polyfills'; diff --git a/electron/src/interfaces/locale.ts b/electron/src/interfaces/locale.ts new file mode 100644 index 0000000000..a40197f457 --- /dev/null +++ b/electron/src/interfaces/locale.ts @@ -0,0 +1,93 @@ +/* + * Wire + * Copyright (C) 2018 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {SUPPORTED_LANGUAGES as SupportedLanguages} from '../locale/locale'; + +export type i18nLanguageIdentifier = + | 'aboutReleases' + | 'aboutUpdate' + | 'aboutVersion' + | 'aboutWebappVersion' + | 'menuAbout' + | 'menuAddPeople' + | 'menuArchive' + | 'menuBlock' + | 'menuCall' + | 'menuClose' + | 'menuConversation' + | 'menuCopy' + | 'menuCut' + | 'menuDelete' + | 'menuEdit' + | 'menuFullScreen' + | 'menuHelp' + | 'menuHideOthers' + | 'menuHideWire' + | 'menuLeave' + | 'menuLegal' + | 'menuLicense' + | 'menuLocale' + | 'menuMinimize' + | 'menuNextConversation' + | 'menuNoSuggestions' + | 'menuPaste' + | 'menuPeople' + | 'menuPing' + | 'menuPreferences' + | 'menuPreviousConversation' + | 'menuPrivacy' + | 'menuQuit' + | 'menuRedo' + | 'menuSavePictureAs' + | 'menuSelectAll' + | 'menuServices' + | 'menuSettings' + | 'menuShowAll' + | 'menuShowHide' + | 'menuSignOut' + | 'menuSpelling' + | 'menuStart' + | 'menuStartup' + | 'menuSupport' + | 'menuUnarchive' + | 'menuUndo' + | 'menuVideoCall' + | 'menuView' + | 'menuWindow' + | 'menuWireURL' + | 'restartLater' + | 'restartLocale' + | 'restartNeeded' + | 'restartNow' + | 'trayOpen' + | 'trayQuit' + | 'unreadMessages' + | 'wrapperAddAccount' + | 'wrapperCreateTeam' + | 'wrapperLogOut' + | 'wrapperManageTeam' + | 'wrapperRemoveAccount'; + +export type i18nStrings = {[identifier in i18nLanguageIdentifier]: string}; + +export type Supportedi18nStrings = Partial; + +export type Supportedi18nLanguage = keyof typeof SupportedLanguages; + +export type Supportedi18nLanguageObject = {[id in Supportedi18nLanguage]: Supportedi18nStrings}; diff --git a/electron/src/interfaces/main.ts b/electron/src/interfaces/main.ts new file mode 100644 index 0000000000..01b7a9f0e4 --- /dev/null +++ b/electron/src/interfaces/main.ts @@ -0,0 +1,61 @@ +/* + * Wire + * Copyright (C) 2018 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import * as Electron from 'electron'; + +import {i18nLanguageIdentifier} from './locale'; + +export interface ElectronMenuWithI18n extends Electron.Menu { + i18n?: i18nLanguageIdentifier; +} + +export interface ElectronMenuWithFileAndImage extends Electron.Menu { + file?: string; + image?: string; +} + +export interface ElectronMenuItemWithI18n extends Electron.MenuItemConstructorOptions { + i18n?: i18nLanguageIdentifier; + selector?: string; + submenu?: ElectronMenuItemWithI18n[] | ElectronMenuWithI18n; +} + +export interface PinningResult { + decoding?: boolean; + errorMessage?: string; + verifiedIssuerRootPubkeys?: boolean; + verifiedPublicKeyInfo?: boolean; +} + +export interface Schemata { + [version: string]: any; +} + +export type Point = [number, number]; + +export type Rectangle = { + height: number; + width: number; + x: number; + y: number; +}; + +export type SpawnCallback = (error: SpawnError | null, stdout: string) => void; + +export type SpawnError = Error & {code?: number | null; stdout?: string | null}; diff --git a/electron/src/interfaces/polyfills.ts b/electron/src/interfaces/polyfills.ts new file mode 100644 index 0000000000..c0b2a5c0a7 --- /dev/null +++ b/electron/src/interfaces/polyfills.ts @@ -0,0 +1,50 @@ +/* + * Wire + * Copyright (C) 2018 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +export interface GoogleAccessTokenResult { + access_token: string; +} + +export interface jsRsaSignPublicKey { + algoid: string; + algparam: string | null; + keyhex: string; +} + +export interface OnHeadersReceivedDetails { + responseHeaders: { + [key: string]: string[]; + }; +} + +export interface OpenGraphResult { + title: string; + type: string; + url: string; + site_name: string; + description: string; + image: { + data?: string; + url: string; + width: string; + height: string; + }; +} + +export type OnHeadersReceivedCallback = (config: OnHeadersReceivedDetails & {cancel?: boolean}) => void; diff --git a/electron/js/about.js b/electron/src/js/about.ts similarity index 66% rename from electron/js/about.js rename to electron/src/js/about.ts index f112f3e8a7..e22e01a36a 100644 --- a/electron/js/about.js +++ b/electron/src/js/about.ts @@ -17,17 +17,18 @@ * */ -const fileUrl = require('file-url'); -const path = require('path'); -const {app, BrowserWindow, ipcMain, session, shell} = require('electron'); +import {BrowserWindow, IpcMessageEvent, app, ipcMain, session, shell} from 'electron'; +import fileUrl = require('file-url'); +import * as path from 'path'; -const config = require('./config'); -const EVENT_TYPE = require('./lib/eventType'); -const pkg = require('../package.json'); -const locale = require('../locale/locale'); +import {i18nLanguageIdentifier} from '../interfaces/'; +import * as locale from '../locale/locale'; +import * as config from './config'; +import {EVENT_TYPE} from './lib/eventType'; -let aboutWindow; -let webappVersion; +const pkg: {productName: string; version: string} = require('../../package.json'); + +let webappVersion: string; // Paths const APP_PATH = app.getAppPath(); @@ -39,11 +40,13 @@ const ABOUT_WINDOW_WHITELIST = [ fileUrl(path.join(APP_PATH, 'img', 'wire.256.png')), fileUrl(path.join(APP_PATH, 'css', 'about.css')), ]; -const PRELOAD_JS = path.join(APP_PATH, 'js', 'preload-about.js'); +const PRELOAD_JS = path.join(APP_PATH, 'dist', 'js', 'preload-about.js'); -ipcMain.once(EVENT_TYPE.UI.WEBAPP_VERSION, (event, version) => (webappVersion = version)); +ipcMain.once(EVENT_TYPE.UI.WEBAPP_VERSION, (event: IpcMessageEvent, version: string) => (webappVersion = version)); const showWindow = () => { + let aboutWindow: BrowserWindow | undefined; + if (!aboutWindow) { aboutWindow = new BrowserWindow({ alwaysOnTop: true, @@ -94,19 +97,23 @@ const showWindow = () => { ); // Locales - ipcMain.on(EVENT_TYPE.ABOUT.LOCALE_VALUES, (event, labels) => { - const isExpected = event.sender.id === aboutWindow.webContents.id; - if (isExpected) { - const resultLabels = {}; - labels.forEach(label => (resultLabels[label] = locale.getText(label))); - event.sender.send(EVENT_TYPE.ABOUT.LOCALE_RENDER, resultLabels); + ipcMain.on(EVENT_TYPE.ABOUT.LOCALE_VALUES, (event: IpcMessageEvent, labels: i18nLanguageIdentifier[]) => { + if (aboutWindow) { + const isExpected = event.sender.id === aboutWindow.webContents.id; + if (isExpected) { + const resultLabels: {[index: string]: string} = {}; + labels.forEach(label => (resultLabels[label] = locale.getText(label))); + event.sender.send(EVENT_TYPE.ABOUT.LOCALE_RENDER, resultLabels); + } } }); // Close window via escape aboutWindow.webContents.on('before-input-event', (event, input) => { if (input.type === 'keyDown' && input.key === 'Escape') { - aboutWindow.close(); + if (aboutWindow) { + aboutWindow.close(); + } } }); @@ -115,17 +122,17 @@ const showWindow = () => { aboutWindow.loadURL(ABOUT_HTML); aboutWindow.webContents.on('dom-ready', () => { - aboutWindow.webContents.send(EVENT_TYPE.ABOUT.LOADED, { - electronVersion: pkg.version, - productName: pkg.productName, - webappVersion: webappVersion, - }); + if (aboutWindow) { + aboutWindow.webContents.send(EVENT_TYPE.ABOUT.LOADED, { + electronVersion: pkg.version, + productName: pkg.productName, + webappVersion: webappVersion, + }); + } }); } aboutWindow.show(); }; -module.exports = { - showWindow, -}; +export {showWindow}; diff --git a/electron/js/appInit.js b/electron/src/js/appInit.ts similarity index 88% rename from electron/js/appInit.js rename to electron/src/js/appInit.ts index b165afc4cd..2425f81b4f 100644 --- a/electron/js/appInit.js +++ b/electron/src/js/appInit.ts @@ -17,10 +17,10 @@ * */ -const {app} = require('electron'); -const environment = require('./environment'); -const minimist = require('minimist'); -const path = require('path'); +import {app} from 'electron'; +import * as minimist from 'minimist'; +import * as path from 'path'; +import * as environment from './environment'; const argv = minimist(process.argv.slice(1)); @@ -52,8 +52,4 @@ const ignoreCertificateErrors = () => { } }; -module.exports = { - fixUnityIcon, - handlePortableFlags, - ignoreCertificateErrors, -}; +export {fixUnityIcon, handlePortableFlags, ignoreCertificateErrors}; diff --git a/electron/src/js/certificateUtils.ts b/electron/src/js/certificateUtils.ts new file mode 100644 index 0000000000..a074054291 --- /dev/null +++ b/electron/src/js/certificateUtils.ts @@ -0,0 +1,171 @@ +/* + * Wire + * Copyright (C) 2018 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import * as crypto from 'crypto'; +import {PinningResult, jsRsaSignPublicKey} from '../interfaces/'; + +const rs = require('jsrsasign'); + +const WILDCARD_CERT_FINGERPRINT = '3pHQns2wdYtN4b2MWsMguGw70gISyhBZLZDpbj+EmdU='; +const MULTIDOMAIN_CERT_FINGERPRINT = 'bORoZ2vRsPJ4WBsUdL1h3Q7C50ZaBqPwngDmDVw+wHA='; +const CERT_ALGORITHM_RSA = '2a864886f70d010101'; +//@ts-ignore +const PUBLIC_KEY_VERISIGN_CLASS3_G5_ROOT = + '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAryQICCl6NZ5gDKrnSztO\n3Hy8PEUcuyvg/ikC+VcIo2SFFSf18a3IMYldIugqqqZCs4/4uVW3sbdLs/6PfgdX\n7O9D22ZiFWHPYA2k2N744MNiCD1UE+tJyllUhSblK48bn+v1oZHCM0nYQ2NqUkvS\nj+hwUU3RiWl7x3D2s9wSdNt7XUtW05a/FXehsPSiJfKvHJJnGOX0BgTvkLnkAOTd\nOrUZ/wK69Dzu4IvrN4vs9Nes8vbwPa/ddZEzGR0cQMt0JBkhk9kU/qwqUseP1QRJ\n5I1jR4g8aYPL/ke9K35PxZWuDp3U0UPAZ3PjFAh+5T+fc7gzCs9dPzSHloruU+gl\nFQIDAQAB\n-----END PUBLIC KEY-----\n'; +const PUBLIC_KEY_DIGICERT_GLOBAL_ROOT_G2 = + '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQq2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5WztCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQvIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQAB\n-----END PUBLIC KEY-----'; +const pins = [ + { + publicKeyInfo: [ + { + algorithmID: CERT_ALGORITHM_RSA, + algorithmParam: null, + fingerprints: [MULTIDOMAIN_CERT_FINGERPRINT, WILDCARD_CERT_FINGERPRINT], + }, + ], + url: /^app\.wire\.com$/i, + }, + { + publicKeyInfo: [ + { + algorithmID: CERT_ALGORITHM_RSA, + algorithmParam: null, + fingerprints: [MULTIDOMAIN_CERT_FINGERPRINT, WILDCARD_CERT_FINGERPRINT], + }, + ], + url: /^(www\.)?wire\.com$/i, + }, + { + publicKeyInfo: [ + { + algorithmID: CERT_ALGORITHM_RSA, + algorithmParam: null, + fingerprints: [WILDCARD_CERT_FINGERPRINT], + }, + ], + url: /^prod-(assets|nginz-https|nginz-ssl)\.wire\.com$/i, + }, + { + issuerRootPubkeys: [PUBLIC_KEY_DIGICERT_GLOBAL_ROOT_G2], + publicKeyInfo: [], + url: /^[a-z0-9]{14,63}\.cloudfront\.net$/i, + }, +]; + +const hostnameShouldBePinned = (hostname: string): boolean => { + return pins.some(pin => pin.url.test(hostname.toLowerCase().trim())); +}; + +const verifyPinning = (hostname: string, certificate: Electron.Certificate): PinningResult => { + const {data: certData = '', issuerCert: {data: issuerCertData = ''} = {}} = certificate; + let issuerCertHex; + let publicKey: jsRsaSignPublicKey; + let publicKeyBytes: string; + let publicKeyFingerprint: string; + + try { + issuerCertHex = rs.pemtohex(issuerCertData); + publicKey = rs.X509.getPublicKeyInfoPropOfCertPEM(certData); + publicKeyBytes = Buffer.from(publicKey.keyhex, 'hex').toString('binary'); + publicKeyFingerprint = crypto + .createHash('sha256') + .update(publicKeyBytes) + .digest('base64'); + } catch (error) { + console.error(`Certificate verification failed: ${error.message}`, error); + return {decoding: false}; + } + + const result: PinningResult = {}; + + const errorMessages: string[] = []; + + for (const pin of pins) { + const {url, publicKeyInfo = [], issuerRootPubkeys = []} = pin; + + if (url.test(hostname.toLowerCase().trim())) { + if (issuerRootPubkeys.length > 0) { + const x509 = new rs.X509(); + x509.readCertHex(issuerCertHex); + + result.verifiedIssuerRootPubkeys = issuerRootPubkeys.some(rawPublicKey => { + const x509PublicKey = rs.KEYUTIL.getKey(rawPublicKey); + return x509.verifySignature(x509PublicKey); + }); + if (!result.verifiedIssuerRootPubkeys) { + const errorMessage = `Issuer root public key signatures: none of "${issuerRootPubkeys.join( + ', ' + )}" could be verified.`; + errorMessages.push(errorMessage); + } + } + + result.verifiedPublicKeyInfo = publicKeyInfo + .reduce((arr: (boolean | undefined)[], pubkey) => { + const { + fingerprints: knownFingerprints = [], + algorithmID: knownAlgorithmID = '', + algorithmParam: knownAlgorithmParam = null, + } = pubkey; + + const fingerprintCheck = + knownFingerprints.length > 0 + ? knownFingerprints.some(knownFingerprint => knownFingerprint === publicKeyFingerprint) + : undefined; + const algorithmIDCheck = knownAlgorithmID === publicKey.algoid; + const algorithmParamCheck = knownAlgorithmParam === publicKey.algparam; + + if (!fingerprintCheck) { + const fingerprintsString = knownFingerprints.join(', '); + const errorMessage = `Public key fingerprints: "${publicKeyFingerprint}" could not be verified with any of the known fingerprints "${fingerprintsString}".`; + errorMessages.push(errorMessage); + } + + if (!algorithmIDCheck) { + const errorMessage = `Algorithm ID: "${ + publicKey.algoid + }" could not be verified with the known ID "${knownAlgorithmID}".`; + errorMessages.push(errorMessage); + } + + if (!algorithmParamCheck) { + const errorMessage = `Algorithm parameter: "${ + publicKey.algparam + }" could not be verified with the known parameter "${knownAlgorithmParam}".`; + errorMessages.push(errorMessage); + } + + arr.push(fingerprintCheck, algorithmIDCheck, algorithmParamCheck); + + return arr; + }, []) + .every(value => Boolean(value)); + + if (errorMessages.length > 0) { + result.errorMessage = errorMessages.join('\n'); + } + + break; + } + } + + return result; +}; + +export {hostnameShouldBePinned, verifyPinning}; diff --git a/electron/src/js/config.ts b/electron/src/js/config.ts new file mode 100644 index 0000000000..cb9fa7b7c2 --- /dev/null +++ b/electron/src/js/config.ts @@ -0,0 +1,103 @@ +/* + * Wire + * Copyright (C) 2018 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +const pkg: {productName: string; version: string} = require('../../package.json'); + +const EMBED_DOMAINS = [ + { + allowedExternalLinks: ['www.youtube.com'], + hostname: ['www.youtube-nocookie.com'], + name: 'YouTube', + }, + { + allowedExternalLinks: ['vimeo.com', 'player.vimeo.com'], + hostname: ['player.vimeo.com'], + name: 'Vimeo', + }, + { + allowedExternalLinks: ['soundcloud.com'], + hostname: ['w.soundcloud.com'], + name: 'SoundCloud', + }, + { + allowedExternalLinks: ['www.spotify.com', 'developer.spotify.com'], + hostname: ['open.spotify.com', 'embed.spotify.com'], + name: 'Spotify', + }, +]; + +const GOOGLE_CLIENT_ID = ''; +const GOOGLE_CLIENT_SECRET = ''; +const GOOGLE_SCOPES = 'https://www.googleapis.com/auth/contacts.readonly'; + +const LOG_FILE_NAME = 'console.log'; + +const NAME = pkg.productName; + +const RAYGUN_API_KEY = ''; + +const SPELLCHECK = { + SUGGESTIONS: 4, + SUPPORTED_LANGUAGES: ['en'], +}; + +const UPDATE = { + DELAY: 5 * 60 * 1000, + INTERVAL: 24 * 60 * 60 * 1000, +}; + +const URL = { + LEGAL: '/legal/', + LICENSES: '/legal/licenses/', + PRIVACY: '/privacy/', +}; + +const VERSION = pkg.version; + +const WINDOW = { + ABOUT: { + HEIGHT: 256, + WIDTH: 304, + }, + AUTH: { + HEIGHT: 576, + WIDTH: 400, + }, + MAIN: { + DEFAULT_HEIGHT: 768, + DEFAULT_WIDTH: 1024, + MIN_HEIGHT: 512, + MIN_WIDTH: 760, + }, +}; + +export { + EMBED_DOMAINS, + GOOGLE_CLIENT_ID, + GOOGLE_CLIENT_SECRET, + GOOGLE_SCOPES, + LOG_FILE_NAME, + NAME, + RAYGUN_API_KEY, + SPELLCHECK, + UPDATE, + URL, + VERSION, + WINDOW, +}; diff --git a/electron/js/environment.js b/electron/src/js/environment.ts similarity index 80% rename from electron/js/environment.js rename to electron/src/js/environment.ts index 1fc559e4a9..a624cfbb08 100644 --- a/electron/js/environment.js +++ b/electron/src/js/environment.ts @@ -17,11 +17,11 @@ * */ -const pkg = require('./../package.json'); -const settings = require('./settings/ConfigurationPersistence'); -const SettingsType = require('./settings/SettingsType'); +import {settings} from './settings/ConfigurationPersistence'; +import {SettingsType} from './settings/SettingsType'; +const pkg: {environment: string; updateWinUrl: string} = require('../../package.json'); -let currentEnvironment = undefined; +let currentEnvironment: string | undefined; const TYPE = { DEV: 'dev', @@ -60,11 +60,11 @@ const app = { UPDATE_URL_WIN: pkg.updateWinUrl, }; -const getEnvironment = () => { +const getEnvironment = (): string => { return currentEnvironment ? currentEnvironment : restoreEnvironment(); }; -const isProdEnvironment = () => { +const isProdEnvironment = (): boolean => { return [TYPE.INTERNAL, TYPE.PRODUCTION].includes(getEnvironment()); }; @@ -74,23 +74,23 @@ const platform = { IS_WINDOWS: process.platform === 'win32', }; -const restoreEnvironment = () => { +const restoreEnvironment = (): string => { currentEnvironment = settings.restore(SettingsType.ENV, TYPE.INTERNAL); return currentEnvironment; }; -const setEnvironment = env => { +const setEnvironment = (env: string): void => { currentEnvironment = env ? env : restoreEnvironment(); settings.save(SettingsType.ENV, currentEnvironment); }; const web = { - getAdminUrl: path => { + getAdminUrl: (path?: string): string => { const baseUrl = isProdEnvironment() ? URL_ADMIN.PRODUCTION : URL_ADMIN.STAGING; return `${baseUrl}${path ? path : ''}`; }, - getSupportUrl: path => `${URL_SUPPORT}${path ? path : ''}`, - getWebappUrl: env => { + getSupportUrl: (path?: string): string => `${URL_SUPPORT}${path ? path : ''}`, + getWebappUrl: (env?: string): string => { if (env) { return env; } @@ -114,18 +114,10 @@ const web = { return URL_WEBAPP.PRODUCTION; }, - getWebsiteUrl: path => { + getWebsiteUrl: (path?: string): string => { const baseUrl = isProdEnvironment() ? URL_WEBSITE.PRODUCTION : URL_WEBSITE.STAGING; return `${baseUrl}${path ? path : ''}`; }, }; -module.exports = { - TYPE, - URL_WEBAPP, - app, - getEnvironment, - platform, - setEnvironment, - web, -}; +export {TYPE, URL_WEBAPP, app, getEnvironment, platform, setEnvironment, web}; diff --git a/electron/js/initRaygun.js b/electron/src/js/initRaygun.ts similarity index 89% rename from electron/js/initRaygun.js rename to electron/src/js/initRaygun.ts index a629eef5dc..73bf241205 100644 --- a/electron/js/initRaygun.js +++ b/electron/src/js/initRaygun.ts @@ -17,7 +17,7 @@ * */ -const config = require('./config'); +import * as config from './config'; const raygun = require('raygun'); let raygunClient; @@ -25,12 +25,10 @@ let raygunClient; const initClient = () => { raygunClient = new raygun.Client().init({apiKey: config.RAYGUN_API_KEY}); - raygunClient.onBeforeSend(payload => { + raygunClient.onBeforeSend((payload: any) => { delete payload.details.machineName; return payload; }); }; -module.exports = { - initClient, -}; +export {initClient}; diff --git a/electron/js/lib/download.js b/electron/src/js/lib/download.ts similarity index 83% rename from electron/js/lib/download.js rename to electron/src/js/lib/download.ts index 13fb638c23..dc858c5c9f 100644 --- a/electron/js/lib/download.js +++ b/electron/src/js/lib/download.ts @@ -17,13 +17,17 @@ * */ -const fs = require('fs'); +import {dialog} from 'electron'; +import * as fs from 'fs'; + const imageType = require('image-type'); -const {dialog} = require('electron'); -module.exports = (fileName, bytes) => { +const download = (fileName: string, bytes: Uint8Array) => { return new Promise((resolve, reject) => { - const options = {}; + const options: { + defaultPath?: string; + filters?: {extensions: string[]; name: string}[]; + } = {}; const type = imageType(bytes); if (fileName) { @@ -48,3 +52,5 @@ module.exports = (fileName, bytes) => { }); }); }; + +export {download}; diff --git a/electron/js/lib/eventType.js b/electron/src/js/lib/eventType.ts similarity index 98% rename from electron/js/lib/eventType.js rename to electron/src/js/lib/eventType.ts index be2d7e595d..d31bdb7c33 100644 --- a/electron/js/lib/eventType.js +++ b/electron/src/js/lib/eventType.ts @@ -17,7 +17,7 @@ * */ -module.exports = { +const EVENT_TYPE = { ABOUT: { LOADED: 'EVENT_TYPE.ABOUT.LOADED', LOCALE_RENDER: 'EVENT_TYPE.ABOUT.LOCALE_RENDER', @@ -52,9 +52,9 @@ module.exports = { SUCCESS: 'EVENT_TYPE.GOOGLE_OAUTH.SUCCESS', }, LIFECYCLE: { - SIGN_OUT: 'EVENT_TYPE.LIFECYCLE.SIGN_OUT', SIGNED_IN: 'EVENT_TYPE.LIFECYCLE.SIGNED_IN', SIGNED_OUT: 'EVENT_TYPE.LIFECYCLE.SIGNED_OUT', + SIGN_OUT: 'EVENT_TYPE.LIFECYCLE.SIGN_OUT', UNREAD_COUNT: 'EVENT_TYPE.LIFECYCLE.UNREAD_COUNT', }, PREFERENCES: { @@ -72,3 +72,5 @@ module.exports = { UPDATE_AVAILABLE: 'EVENT_TYPE.WRAPPER.UPDATE_AVAILABLE', }, }; + +export {EVENT_TYPE}; diff --git a/electron/js/lib/googleAuth.js b/electron/src/js/lib/googleAuth.ts similarity index 59% rename from electron/js/lib/googleAuth.js rename to electron/src/js/lib/googleAuth.ts index 3c45025ca8..a0def6383b 100644 --- a/electron/js/lib/googleAuth.js +++ b/electron/src/js/lib/googleAuth.ts @@ -17,14 +17,16 @@ * */ -const {BrowserWindow} = require('electron'); +import {BrowserWindow} from 'electron'; +import * as google from 'googleapis'; +import * as qs from 'querystring'; +import * as request from 'request'; + +import {GoogleAccessTokenResult} from '../../interfaces/'; -const qs = require('querystring'); -const google = require('googleapis'); -const request = require('request'); const OAuth2 = google.auth.OAuth2; -const authorizeApp = url => { +const authorizeApp = (url: string): Promise => { return new Promise((resolve, reject) => { const win = new BrowserWindow({ title: '', @@ -34,7 +36,7 @@ const authorizeApp = url => { win.setMenuBarVisibility(false); win.loadURL(url); - win.on('closed', () => reject(new Error('User closed the window'))); + win.on('closed', () => reject(new Error('User closed the window'))); win.on('page-title-updated', () => { setImmediate(() => { @@ -55,9 +57,9 @@ const authorizeApp = url => { }); }; -const getAccessToken = (scopes, clientId, clientSecret) => { - return new Promise((resolve, reject) => { - getAuthorizationCode(scopes, clientId, clientSecret).then(code => { +const getAccessToken = (scopes: string, clientId: string, clientSecret: string): Promise => { + return getAuthorizationCode(scopes, clientId, clientSecret).then(code => { + return new Promise((resolve, reject) => { const data = qs.stringify({ client_id: clientId, client_secret: clientSecret, @@ -66,31 +68,36 @@ const getAccessToken = (scopes, clientId, clientSecret) => { redirect_uri: 'urn:ietf:wg:oauth:2.0:oob', }); - request.post( - 'https://accounts.google.com/o/oauth2/token', - { - body: data, - headers: { - Accept: 'application/json', - 'Content-Type': 'application/x-www-form-urlencoded', - }, + const requestConfig = { + body: data, + headers: { + Accept: 'application/json', + 'Content-Type': 'application/x-www-form-urlencoded', }, - (error, response, body) => (error ? reject(error) : resolve(JSON.parse(body))) - ); + }; + + const requestUrl = 'https://accounts.google.com/o/oauth2/token'; + + request.post(requestUrl, requestConfig, (error, response, body) => { + if (error) { + return reject(error); + } + + const result = JSON.parse(body) as GoogleAccessTokenResult; + return resolve(result); + }); }); }); }; -const getAuthenticationUrl = (scopes, clientId, clientSecret) => { +const getAuthenticationUrl = (scopes: string, clientId: string, clientSecret: string) => { const oauth2Client = new OAuth2(clientId, clientSecret, 'urn:ietf:wg:oauth:2.0:oob'); return oauth2Client.generateAuthUrl({scope: scopes}); }; -const getAuthorizationCode = (scopes, clientId, clientSecret) => { +const getAuthorizationCode = (scopes: string, clientId: string, clientSecret: string): Promise => { const url = getAuthenticationUrl(scopes, clientId, clientSecret); return authorizeApp(url); }; -module.exports = { - getAccessToken, -}; +export {getAccessToken}; diff --git a/electron/js/lib/openGraph.js b/electron/src/js/lib/openGraph.ts similarity index 78% rename from electron/js/lib/openGraph.js rename to electron/src/js/lib/openGraph.ts index f0e1689939..30e2604107 100644 --- a/electron/js/lib/openGraph.js +++ b/electron/src/js/lib/openGraph.ts @@ -17,21 +17,23 @@ * */ -const openGraphParse = require('open-graph').parse; -const request = require('request'); -const urlUtil = require('url'); +import * as request from 'request'; +import * as urlUtil from 'url'; +const openGraphParse = require('open-graph'); -const arrayify = (value = []) => (Array.isArray(value) ? value : [value]); +import {OpenGraphResult} from '../../interfaces/'; -const bufferToBase64 = (buffer, mimeType) => { +const arrayify = (value: T[] | T = []): T[] => (Array.isArray(value) ? value : [value]); + +const bufferToBase64 = (buffer: string, mimeType?: string): string => { const bufferBase64encoded = Buffer.from(buffer).toString('base64'); return `data:${mimeType};base64,${bufferBase64encoded}`; }; -const fetchImageAsBase64 = url => { +const fetchImageAsBase64 = (url: string): Promise => { const IMAGE_SIZE_LIMIT = 5e6; // 5MB return new Promise(resolve => { - const imageRequest = request({encoding: null, url: encodeURI(url)}, (error, response, body) => { + const imageRequest = request({encoding: null, url: encodeURI(url)}, (error, response, body: string) => { if (!error && response.statusCode === 200) { resolve(bufferToBase64(body, response.headers['content-type'])); } else { @@ -51,18 +53,18 @@ const fetchImageAsBase64 = url => { }); }; -const fetchOpenGraphData = url => { +const fetchOpenGraphData = (url: string): Promise => { const CONTENT_SIZE_LIMIT = 1e6; // ~1MB const parsedUrl = urlUtil.parse(url); const normalizedUrl = parsedUrl.protocol ? parsedUrl : urlUtil.parse(`http://${url}`); - const parseHead = body => { + const parseHead = (body: string) => { const [head] = body.match(/[\s\S]*?<\/head>/) || ['']; return openGraphParse(head); }; - return new Promise((resolve, reject) => { - const getContentRequest = request.get(urlUtil.format(normalizedUrl), (error, response, body) => { + return new Promise((resolve, reject) => { + const getContentRequest = request.get(urlUtil.format(normalizedUrl), (error, response, body: string) => { return error ? reject(error) : resolve(body); }); @@ -88,7 +90,7 @@ const fetchOpenGraphData = url => { }).then(parseHead); }; -const updateMetaDataWithImage = (meta, image) => { +const updateMetaDataWithImage = (meta: OpenGraphResult, image?: string): OpenGraphResult => { if (image) { meta.image.data = image; } else { @@ -98,7 +100,7 @@ const updateMetaDataWithImage = (meta, image) => { return meta; }; -const getOpenGraphData = (url, callback) => { +const getOpenGraphData = (url: string, callback: (error: Error | null, meta?: OpenGraphResult) => void) => { return fetchOpenGraphData(url) .then(meta => { if (meta.image && meta.image.url) { @@ -127,4 +129,4 @@ const getOpenGraphData = (url, callback) => { }); }; -module.exports = getOpenGraphData; +export {getOpenGraphData}; diff --git a/electron/js/lib/pointInRect.js b/electron/src/js/lib/pointInRect.ts similarity index 86% rename from electron/js/lib/pointInRect.js rename to electron/src/js/lib/pointInRect.ts index 03b0cc173b..9d3ffeb255 100644 --- a/electron/js/lib/pointInRect.js +++ b/electron/src/js/lib/pointInRect.ts @@ -17,10 +17,14 @@ * */ -module.exports = (point, rectangle) => { +import {Point, Rectangle} from '../../interfaces/'; + +const pointInRectangle = (point: Point, rectangle: Rectangle) => { const [x, y] = point; const xInRange = x >= rectangle.x && x <= rectangle.x + rectangle.width; const yInRange = y >= rectangle.y && y <= rectangle.y + rectangle.height; return xInRange && yInRange; }; + +export {pointInRectangle}; diff --git a/electron/js/lifecycle.js b/electron/src/js/lifecycle.ts similarity index 84% rename from electron/js/lifecycle.js rename to electron/src/js/lifecycle.ts index 10c3a996c3..7329d780cc 100644 --- a/electron/js/lifecycle.js +++ b/electron/src/js/lifecycle.ts @@ -17,11 +17,11 @@ * */ -const {app, ipcMain} = require('electron'); -const environment = require('./environment'); -const EVENT_TYPE = require('./lib/eventType'); -const settings = require('./settings/ConfigurationPersistence'); -const windowManager = require('./window-manager'); +import {app, ipcMain} from 'electron'; +import * as environment from './environment'; +import {EVENT_TYPE} from './lib/eventType'; +import {settings} from './settings/ConfigurationPersistence'; +import * as windowManager from './window-manager'; const checkForUpdate = () => { if (environment.platform.IS_WINDOWS) { @@ -69,10 +69,4 @@ const relaunch = () => { let shouldQuit = false; -module.exports = { - checkForUpdate, - checkSingleInstance, - quit, - relaunch, - shouldQuit, -}; +export {checkForUpdate, checkSingleInstance, quit, relaunch, shouldQuit}; diff --git a/electron/src/js/menu/TrayHandler.ts b/electron/src/js/menu/TrayHandler.ts new file mode 100644 index 0000000000..3cd91a9656 --- /dev/null +++ b/electron/src/js/menu/TrayHandler.ts @@ -0,0 +1,115 @@ +/* + * Wire + * Copyright (C) 2018 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {Menu, Tray, app, nativeImage} from 'electron'; +import * as path from 'path'; + +import * as locale from '../../locale/locale'; +import * as config from '../config'; +import * as lifecycle from '../lifecycle'; +import * as windowManager from '../window-manager'; + +class TrayHandler { + lastUnreadCount: number; + trayIcon?: Tray; + icons?: { + badge: nativeImage; + tray: nativeImage; + trayWithBadge: nativeImage; + }; + + constructor() { + this.lastUnreadCount = 0; + } + + initTray(trayIcon = new Tray(nativeImage.createEmpty())) { + const IMAGE_ROOT = path.join(app.getAppPath(), 'img'); + + const iconPaths = { + badge: path.join(IMAGE_ROOT, 'taskbar.overlay.png'), + tray: path.join(IMAGE_ROOT, 'tray-icon', 'tray', 'tray.png'), + trayWithBadge: path.join(IMAGE_ROOT, 'tray-icon', 'tray-with-badge', 'tray.badge.png'), + }; + + this.icons = { + badge: nativeImage.createFromPath(iconPaths.badge), + tray: nativeImage.createFromPath(iconPaths.tray), + trayWithBadge: nativeImage.createFromPath(iconPaths.trayWithBadge), + }; + + this.trayIcon = trayIcon; + this.trayIcon.setImage(this.icons.tray); + + this.buildTrayMenu(); + } + + showUnreadCount(win: Electron.BrowserWindow, count?: number) { + this.updateIcons(win, count); + this.flashApplicationWindow(win, count); + this.updateBadgeCount(count); + } + + private buildTrayMenu() { + const contextMenu = Menu.buildFromTemplate([ + { + click: () => windowManager.showPrimaryWindow(), + label: locale.getText('trayOpen'), + }, + { + click: async () => lifecycle.quit(), + label: locale.getText('trayQuit'), + }, + ]); + + if (this.trayIcon) { + this.trayIcon.on('click', () => windowManager.showPrimaryWindow()); + this.trayIcon.setContextMenu(contextMenu); + this.trayIcon.setToolTip(config.NAME); + } + } + + private flashApplicationWindow(win: Electron.BrowserWindow, count?: number) { + if (win.isFocused() || !count) { + win.flashFrame(false); + } else if (count > this.lastUnreadCount) { + win.flashFrame(true); + } + } + + private updateBadgeCount(count?: number) { + if (count) { + app.setBadgeCount(count); + this.lastUnreadCount = count; + } + } + + private updateIcons(win: Electron.BrowserWindow, count?: number) { + if (this.icons) { + const trayImage = count ? this.icons.trayWithBadge : this.icons.tray; + if (this.trayIcon) { + this.trayIcon.setImage(trayImage); + } + + const overlayImage = count ? this.icons.badge : null; + win.setOverlayIcon(overlayImage, locale.getText('unreadMessages')); + } + } +} + +export {TrayHandler}; diff --git a/electron/js/menu/context.js b/electron/src/js/menu/context.ts similarity index 77% rename from electron/js/menu/context.js rename to electron/src/js/menu/context.ts index 4f0dc72524..5f387df7a0 100644 --- a/electron/js/menu/context.js +++ b/electron/src/js/menu/context.ts @@ -17,17 +17,18 @@ * */ -const {clipboard, remote, ipcRenderer, webFrame} = require('electron'); +import {clipboard, ipcRenderer, remote, webFrame} from 'electron'; const Menu = remote.Menu; const webContents = remote.getCurrentWebContents(); -const config = require('./../config'); -const locale = require('./../../locale/locale'); -const settings = require('./../settings/ConfigurationPersistence'); -const EVENT_TYPE = require('./../lib/eventType'); -const SettingsType = require('./../settings/SettingsType'); +import {ElectronMenuWithFileAndImage} from '../../interfaces/'; +import * as locale from '../../locale/locale'; +import * as config from '../config'; +import {EVENT_TYPE} from '../lib/eventType'; +import {settings} from '../settings/ConfigurationPersistence'; +import {SettingsType} from '../settings/SettingsType'; -let textMenu; +let textMenu: Electron.Menu; /////////////////////////////////////////////////////////////////////////////// // Default @@ -46,12 +47,12 @@ const defaultMenu = Menu.buildFromTemplate([ // Text /////////////////////////////////////////////////////////////////////////////// -const selection = { +const selection: {isMisspelled: boolean; suggestions: string[]} = { isMisspelled: false, suggestions: [], }; -const textMenuTemplate = [ +const textMenuTemplate: Electron.MenuItemConstructorOptions[] = [ { label: locale.getText('menuCut'), role: 'cut', @@ -82,7 +83,7 @@ const createTextMenu = () => { if (selection.suggestions.length > 0) { for (const suggestion of selection.suggestions.reverse()) { template.unshift({ - click: menuItem => webContents.replaceMisspelling(menuItem.label), + click: (menuItem: Electron.MenuItem): void => webContents.replaceMisspelling(menuItem.label), label: suggestion, }); } @@ -101,9 +102,9 @@ const createTextMenu = () => { // Images /////////////////////////////////////////////////////////////////////////////// -const imageMenu = Menu.buildFromTemplate([ +const imageMenu: ElectronMenuWithFileAndImage = Menu.buildFromTemplate([ { - click: () => savePicture(imageMenu.file, imageMenu.image), + click: () => savePicture(imageMenu.file || '', imageMenu.image || ''), label: locale.getText('menuSavePictureAs'), }, ]); @@ -111,36 +112,41 @@ const imageMenu = Menu.buildFromTemplate([ window.addEventListener( 'contextmenu', event => { - const element = event.target; + const element = event.target as HTMLElement; copyContext = ''; if (element.nodeName === 'TEXTAREA' || element.nodeName === 'INPUT') { event.preventDefault(); + createTextMenu(); textMenu.popup(remote.getCurrentWindow()); } else if (element.classList.contains('image-element') || element.classList.contains('detail-view-image')) { event.preventDefault(); - imageMenu.image = element.src; + const elementSource = (element as HTMLImageElement).src; + imageMenu.image = elementSource; imageMenu.popup(remote.getCurrentWindow()); } else if (element.nodeName === 'A') { event.preventDefault(); - copyContext = element.href.replace(/^mailto:/, ''); + + const elementHref = (element as HTMLLinkElement).href; + copyContext = elementHref.replace(/^mailto:/, ''); defaultMenu.popup(remote.getCurrentWindow()); } else if (element.classList.contains('text')) { event.preventDefault(); + copyContext = window.getSelection().toString() || element.innerText.trim(); defaultMenu.popup(remote.getCurrentWindow()); } else { // Maybe we are in a code block _inside_ an element with the 'text' class? // Code block can consist of many tags: CODE, PRE, SPAN, etc. let parentNode = element.parentNode; - while (parentNode !== document && !parentNode.classList.contains('text')) { + while (parentNode && parentNode !== document && !(parentNode as HTMLElement).classList.contains('text')) { parentNode = parentNode.parentNode; } if (parentNode !== document) { event.preventDefault(); - copyContext = window.getSelection().toString() || parentNode.innerText.trim(); + copyContext = window.getSelection().toString() || (parentNode as HTMLElement).innerText.trim(); defaultMenu.popup(remote.getCurrentWindow()); } } @@ -148,8 +154,8 @@ window.addEventListener( false ); -const savePicture = (fileName, url) => { - fetch(url) +const savePicture = (fileName: string, url: RequestInfo) => { + return fetch(url) .then(response => response.arrayBuffer()) .then(arrayBuffer => ipcRenderer.send(EVENT_TYPE.ACTION.SAVE_PICTURE, fileName, new Uint8Array(arrayBuffer))); }; diff --git a/electron/js/menu/developer.js b/electron/src/js/menu/developer.ts similarity index 74% rename from electron/js/menu/developer.js rename to electron/src/js/menu/developer.ts index 03f2de44cb..ddbdd75aa2 100644 --- a/electron/js/menu/developer.js +++ b/electron/src/js/menu/developer.ts @@ -17,27 +17,27 @@ * */ -const {MenuItem} = require('electron'); -const config = require('./../config'); -const environment = require('./../environment'); -const util = require('./../util'); -const windowManager = require('./../window-manager'); +import * as Electron from 'electron'; +import * as config from '../config'; +import * as environment from '../environment'; +import * as util from '../util'; +import * as windowManager from '../window-manager'; const currentEnvironment = environment.getEnvironment(); const getPrimaryWindow = () => windowManager.getPrimaryWindow(); -const reloadTemplate = { +const reloadTemplate: Electron.MenuItemConstructorOptions = { click: () => getPrimaryWindow().reload(), label: 'Reload', }; -const devToolsTemplate = { +const devToolsTemplate: Electron.MenuItemConstructorOptions = { label: 'Toggle DevTools', submenu: [ { accelerator: 'Alt+CmdOrCtrl+I', - click: () => getPrimaryWindow().toggleDevTools(), + click: () => (getPrimaryWindow() as any).toggleDevTools(), label: 'Sidebar', }, { @@ -64,7 +64,7 @@ const devToolsTemplate = { ], }; -const createEnvironmentTemplate = env => { +const createEnvironmentTemplate = (env: string): Electron.MenuItemConstructorOptions => { return { checked: currentEnvironment === env, click: () => { @@ -76,23 +76,23 @@ const createEnvironmentTemplate = env => { }; }; -const versionTemplate = { +const versionTemplate: Electron.MenuItemConstructorOptions = { label: `Wire Version ${config.VERSION}`, }; -const chromeVersionTemplate = { +const chromeVersionTemplate: Electron.MenuItemConstructorOptions = { label: `Chrome Version ${process.versions.chrome}`, }; -const electronVersionTemplate = { +const electronVersionTemplate: Electron.MenuItemConstructorOptions = { label: `Electron Version ${process.versions.electron}`, }; -const separatorTemplate = { +const separatorTemplate: Electron.MenuItemConstructorOptions = { type: 'separator', }; -const menuTemplate = { +const menuTemplate: Electron.MenuItemConstructorOptions = { id: 'Developer', label: 'Developer', submenu: [ @@ -112,4 +112,6 @@ const menuTemplate = { ], }; -module.exports = new MenuItem(menuTemplate); +const menuItem = new Electron.MenuItem(menuTemplate); + +export {menuItem}; diff --git a/electron/js/menu/system.js b/electron/src/js/menu/system.ts similarity index 68% rename from electron/js/menu/system.js rename to electron/src/js/menu/system.ts index 1470a5c765..8b2e6de647 100644 --- a/electron/js/menu/system.js +++ b/electron/src/js/menu/system.ts @@ -17,20 +17,22 @@ * */ -const {dialog, Menu, shell} = require('electron'); -const autoLaunch = require('auto-launch'); -const launchCmd = process.env.APPIMAGE ? process.env.APPIMAGE : process.execPath; +import autoLaunch = require('auto-launch'); +import {Menu, dialog, shell} from 'electron'; +import * as locale from '../../locale/locale'; +import * as config from '../config'; +import * as environment from '../environment'; +import {EVENT_TYPE} from '../lib/eventType'; +import * as lifecycle from '../lifecycle'; +import {settings} from '../settings/ConfigurationPersistence'; +import {SettingsType} from '../settings/SettingsType'; +import * as windowManager from '../window-manager'; -const config = require('./../config'); -const environment = require('./../environment'); -const lifecycle = require('./../lifecycle'); -const locale = require('./../../locale/locale'); -const windowManager = require('./../window-manager'); -const settings = require('../settings/ConfigurationPersistence'); -const EVENT_TYPE = require('./../lib/eventType'); -const SettingsType = require('../settings/SettingsType'); +import {ElectronMenuItemWithI18n, Supportedi18nLanguage} from '../../interfaces/'; -let menu; +const launchCmd = process.env.APPIMAGE || process.execPath; + +let menu: Menu; const launcher = new autoLaunch({ isHidden: true, @@ -38,48 +40,50 @@ const launcher = new autoLaunch({ path: launchCmd, }); -const getPrimaryWindow = () => windowManager.getPrimaryWindow(); +const getPrimaryWindow = (): Electron.BrowserWindow => windowManager.getPrimaryWindow(); // TODO: disable menus when not in focus -const sendAction = action => { +const sendAction = (action: string): void => { const primaryWindow = getPrimaryWindow(); if (primaryWindow) { getPrimaryWindow().webContents.send(EVENT_TYPE.UI.SYSTEM_MENU, action); } }; -const separatorTemplate = { +const separatorTemplate: ElectronMenuItemWithI18n = { type: 'separator', }; -const createLanguageTemplate = languageCode => { +const createLanguageTemplate = (languageCode: Supportedi18nLanguage): ElectronMenuItemWithI18n => { return { - click: () => changeLocale(languageCode), + click: (): void => changeLocale(languageCode), label: locale.SUPPORTED_LANGUAGES[languageCode], type: 'radio', }; }; -const createLanguageSubmenu = () => { - return Object.keys(locale.SUPPORTED_LANGUAGES).map(supportedLanguage => createLanguageTemplate(supportedLanguage)); +const createLanguageSubmenu = (): ElectronMenuItemWithI18n[] => { + return Object.keys(locale.SUPPORTED_LANGUAGES).map(supportedLanguage => + createLanguageTemplate(supportedLanguage as Supportedi18nLanguage) + ); }; -const localeTemplate = { +const localeTemplate: ElectronMenuItemWithI18n = { i18n: 'menuLocale', submenu: createLanguageSubmenu(), }; -const aboutTemplate = { - click: () => menu.emit(EVENT_TYPE.ABOUT.SHOW), +const aboutTemplate: ElectronMenuItemWithI18n = { + click: (): void => (menu as any).emit(EVENT_TYPE.ABOUT.SHOW), i18n: 'menuAbout', }; -const signOutTemplate = { +const signOutTemplate: ElectronMenuItemWithI18n = { click: () => sendAction(EVENT_TYPE.ACTION.SIGN_OUT), i18n: 'menuSignOut', }; -const conversationTemplate = { +const conversationTemplate: ElectronMenuItemWithI18n = { i18n: 'menuConversation', submenu: [ { @@ -125,13 +129,13 @@ const conversationTemplate = { ], }; -const showWireTemplate = { +const showWireTemplate: ElectronMenuItemWithI18n = { accelerator: 'CmdOrCtrl+0', click: () => getPrimaryWindow().show(), label: config.NAME, }; -const toggleMenuTemplate = { +const toggleMenuTemplate: ElectronMenuItemWithI18n = { checked: settings.restore(SettingsType.SHOW_MENU_BAR, true), click: () => { const mainBrowserWindow = getPrimaryWindow(); @@ -149,7 +153,7 @@ const toggleMenuTemplate = { type: 'checkbox', }; -const toggleFullScreenTemplate = { +const toggleFullScreenTemplate: ElectronMenuItemWithI18n = { accelerator: environment.platform.IS_MAC_OS ? 'Alt+Command+F' : 'F11', click: () => { const mainBrowserWindow = getPrimaryWindow(); @@ -159,7 +163,7 @@ const toggleFullScreenTemplate = { type: 'checkbox', }; -const toggleAutoLaunchTemplate = { +const toggleAutoLaunchTemplate: ElectronMenuItemWithI18n = { checked: settings.restore(SettingsType.AUTO_LAUNCH, false), click: () => { const shouldAutoLaunch = !settings.restore(SettingsType.AUTO_LAUNCH); @@ -172,7 +176,7 @@ const toggleAutoLaunchTemplate = { const supportsSpellCheck = config.SPELLCHECK.SUPPORTED_LANGUAGES.includes(locale.getCurrent()); -const editTemplate = { +const editTemplate: ElectronMenuItemWithI18n = { i18n: 'menuEdit', submenu: [ { @@ -204,7 +208,7 @@ const editTemplate = { separatorTemplate, { checked: supportsSpellCheck && settings.restore(SettingsType.SPELL_CHECK, false), - click: event => settings.save(SettingsType.SPELL_CHECK, event.checked), + click: (menuItem: Electron.MenuItem): boolean => settings.save(SettingsType.SPELL_CHECK, menuItem.checked), enabled: supportsSpellCheck, i18n: 'menuSpelling', type: 'checkbox', @@ -212,7 +216,7 @@ const editTemplate = { ], }; -const windowTemplate = { +const windowTemplate: ElectronMenuItemWithI18n = { i18n: 'menuWindow', role: 'window', submenu: [ @@ -238,7 +242,7 @@ const windowTemplate = { ], }; -const helpTemplate = { +const helpTemplate: ElectronMenuItemWithI18n = { i18n: 'menuHelp', role: 'help', submenu: [ @@ -265,7 +269,7 @@ const helpTemplate = { ], }; -const darwinTemplate = { +const darwinTemplate: ElectronMenuItemWithI18n = { label: config.NAME, submenu: [ aboutTemplate, @@ -305,7 +309,7 @@ const darwinTemplate = { ], }; -const win32Template = { +const win32Template: ElectronMenuItemWithI18n = { label: config.NAME, submenu: [ { @@ -319,13 +323,13 @@ const win32Template = { signOutTemplate, { accelerator: 'Alt+F4', - click: async () => await lifecycle.quit(), + click: () => lifecycle.quit(), i18n: 'menuQuit', }, ], }; -const linuxTemplate = { +const linuxTemplate: ElectronMenuItemWithI18n = { label: config.NAME, submenu: [ { @@ -340,18 +344,18 @@ const linuxTemplate = { signOutTemplate, { accelerator: 'Ctrl+Q', - click: async () => await lifecycle.quit(), + click: () => lifecycle.quit(), i18n: 'menuQuit', }, ], }; -const menuTemplate = [conversationTemplate, editTemplate, windowTemplate, helpTemplate]; +const menuTemplate: ElectronMenuItemWithI18n[] = [conversationTemplate, editTemplate, windowTemplate, helpTemplate]; -const processMenu = (template, language) => { +const processMenu = (template: Iterable, language: Supportedi18nLanguage) => { for (const item of template) { if (item.submenu) { - processMenu(item.submenu, language); + processMenu(item.submenu as Iterable, language); } if (locale.SUPPORTED_LANGUAGES[language] === item.label) { @@ -364,16 +368,16 @@ const processMenu = (template, language) => { } }; -const changeLocale = language => { +const changeLocale = (language: Supportedi18nLanguage): void => { locale.setLocale(language); dialog.showMessageBox( { buttons: [ - locale[language].restartLater, - environment.platform.IS_MAC_OS ? locale[language].menuQuit : locale[language].restartNow, + locale.getText('restartLater'), + environment.platform.IS_MAC_OS ? locale.getText('menuQuit') : locale.getText('restartNow'), ], - message: locale[language].restartLocale, - title: locale[language].restartNeeded, + message: locale.getText('restartLocale'), + title: locale.getText('restartNeeded'), type: 'info', }, response => { @@ -384,37 +388,57 @@ const changeLocale = language => { ); }; -module.exports = { - createMenu: isFullScreen => { - if (environment.platform.IS_MAC_OS) { - menuTemplate.unshift(darwinTemplate); +const createMenu = (isFullScreen: boolean): Menu => { + if (!windowTemplate.submenu) { + windowTemplate.submenu = []; + } + if (!editTemplate.submenu) { + editTemplate.submenu = []; + } + if (!helpTemplate.submenu) { + helpTemplate.submenu = []; + } + + if (environment.platform.IS_MAC_OS) { + menuTemplate.unshift(darwinTemplate); + if (Array.isArray(windowTemplate.submenu)) { windowTemplate.submenu.push(separatorTemplate, showWireTemplate, separatorTemplate, toggleFullScreenTemplate); - toggleFullScreenTemplate.checked = isFullScreen; } + toggleFullScreenTemplate.checked = isFullScreen; + } - if (environment.platform.IS_WINDOWS) { - menuTemplate.unshift(win32Template); - windowTemplate.i18n = 'menuView'; + if (environment.platform.IS_WINDOWS) { + menuTemplate.unshift(win32Template); + windowTemplate.i18n = 'menuView'; + if (Array.isArray(windowTemplate.submenu)) { windowTemplate.submenu.unshift(toggleMenuTemplate, separatorTemplate); } + } - if (environment.platform.IS_LINUX) { - menuTemplate.unshift(linuxTemplate); + if (environment.platform.IS_LINUX) { + menuTemplate.unshift(linuxTemplate); + if (Array.isArray(editTemplate.submenu)) { editTemplate.submenu.push(separatorTemplate, { click: () => sendAction(EVENT_TYPE.PREFERENCES.SHOW), i18n: 'menuPreferences', }); + } + if (Array.isArray(windowTemplate.submenu)) { windowTemplate.submenu.push(separatorTemplate, toggleMenuTemplate, separatorTemplate, toggleFullScreenTemplate); - toggleFullScreenTemplate.checked = isFullScreen; } + toggleFullScreenTemplate.checked = isFullScreen; + } - if (!environment.platform.IS_MAC_OS) { + if (!environment.platform.IS_MAC_OS) { + if (Array.isArray(helpTemplate.submenu)) { helpTemplate.submenu.push(separatorTemplate, aboutTemplate); } + } - processMenu(menuTemplate, locale.getCurrent()); - menu = Menu.buildFromTemplate(menuTemplate); + processMenu(menuTemplate, locale.getCurrent()); + menu = Menu.buildFromTemplate(menuTemplate); - return menu; - }, + return menu; }; + +export {createMenu}; diff --git a/electron/src/js/preload-about.ts b/electron/src/js/preload-about.ts new file mode 100644 index 0000000000..3ae041674a --- /dev/null +++ b/electron/src/js/preload-about.ts @@ -0,0 +1,73 @@ +/* + * Wire + * Copyright (C) 2018 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {IpcMessageEvent, ipcRenderer} from 'electron'; +import {EVENT_TYPE} from './lib/eventType'; + +ipcRenderer.once(EVENT_TYPE.ABOUT.LOCALE_RENDER, (event: IpcMessageEvent, labels: string[]) => { + for (const label in labels) { + const labelElement = document.querySelector(`[data-string="${label}"]`); + if (labelElement) { + labelElement.innerHTML = labels[label]; + } + } +}); + +ipcRenderer.once( + EVENT_TYPE.ABOUT.LOADED, + ( + event: Event, + details: { + electronVersion: string; + productName: string; + webappVersion: string; + } + ) => { + const nameElement = document.getElementById('name'); + if (nameElement) { + nameElement.innerHTML = details.productName; + } + + const versionElement = document.getElementById('version'); + if (versionElement) { + versionElement.innerHTML = details.electronVersion || 'Development'; + } + + const webappVersionElement = document.getElementById('webappVersion'); + if (webappVersionElement) { + if (details.webappVersion) { + webappVersionElement.innerHTML = details.webappVersion; + } else { + if (webappVersionElement.parentNode) { + (webappVersionElement.parentNode as any).remove(); + } + } + } + + // Get locales + const labels = []; + const dataStrings = document.querySelectorAll('[data-string]'); + + for (const index in dataStrings) { + const label = dataStrings[index]; + labels.push(label.dataset.string); + } + ipcRenderer.send(EVENT_TYPE.ABOUT.LOCALE_VALUES, labels); + } +); diff --git a/electron/js/preload.js b/electron/src/js/preload.ts similarity index 63% rename from electron/js/preload.js rename to electron/src/js/preload.ts index b28cc52504..8fc8a549b3 100644 --- a/electron/js/preload.js +++ b/electron/src/js/preload.ts @@ -17,48 +17,53 @@ * */ -const {ipcRenderer, webFrame} = require('electron'); -const environment = require('./environment'); -const locale = require('../locale/locale'); -const EVENT_TYPE = require('./lib/eventType'); +import {IpcMessageEvent, WebviewTag, ipcRenderer, webFrame} from 'electron'; +import * as locale from '../locale/locale'; +import * as environment from './environment'; +import {EVENT_TYPE} from './lib/eventType'; webFrame.setZoomFactor(1.0); webFrame.setVisualZoomLevelLimits(1, 1); -window.locStrings = locale[locale.getCurrent()]; -window.locStringsDefault = locale.en; +window.locStrings = locale.LANGUAGES[locale.getCurrent()]; +window.locStringsDefault = locale.LANGUAGES.en; window.isMac = environment.platform.IS_MAC_OS; -const getSelectedWebview = () => document.querySelector('.Webview:not(.hide)'); -const getWebviewById = id => document.querySelector(`.Webview[data-accountid="${id}"]`); +const getSelectedWebview = (): WebviewTag => document.querySelector('.Webview:not(.hide)') as WebviewTag; +const getWebviewById = (id: string): WebviewTag => { + return document.querySelector(`.Webview[data-accountid="${id}"]`) as WebviewTag; +}; const subscribeToMainProcessEvents = () => { - ipcRenderer.on(EVENT_TYPE.UI.SYSTEM_MENU, (event, action) => { + ipcRenderer.on(EVENT_TYPE.UI.SYSTEM_MENU, (event: IpcMessageEvent, action: string) => { const selectedWebview = getSelectedWebview(); if (selectedWebview) { selectedWebview.send(action); } }); - ipcRenderer.on(EVENT_TYPE.WRAPPER.RELOAD, () => { - const webviews = document.querySelectorAll('webview'); - webviews.forEach(webview => webview.reload()); - }); + ipcRenderer.on( + EVENT_TYPE.WRAPPER.RELOAD, + (): void => { + const webviews = document.querySelectorAll('webview'); + webviews.forEach(webview => webview.reload()); + } + ); }; -const setupIpcInterface = () => { - window.sendBadgeCount = count => { +const setupIpcInterface = (): void => { + window.sendBadgeCount = (count: number): void => { ipcRenderer.send(EVENT_TYPE.UI.BADGE_COUNT, count); }; - window.sendDeleteAccount = (accountID, sessionID) => { + window.sendDeleteAccount = (accountID: string, sessionID?: string): void => { const accountWebview = getWebviewById(accountID); accountWebview.getWebContents().session.clearStorageData(); ipcRenderer.send(EVENT_TYPE.ACCOUNT.DELETE_DATA, accountID, sessionID); }; - window.sendLogoutAccount = accountId => { + window.sendLogoutAccount = (accountId: string): void => { const accountWebview = getWebviewById(accountId); if (accountWebview) { accountWebview.send(EVENT_TYPE.ACTION.SIGN_OUT); @@ -66,7 +71,7 @@ const setupIpcInterface = () => { }; }; -const addDragRegion = () => { +const addDragRegion = (): void => { if (environment.platform.IS_MAC_OS) { // add titlebar ghost to prevent interactions with the content while dragging const titleBar = document.createElement('div'); diff --git a/electron/js/settings/ConfigurationPersistence.js b/electron/src/js/settings/ConfigurationPersistence.ts similarity index 79% rename from electron/js/settings/ConfigurationPersistence.js rename to electron/src/js/settings/ConfigurationPersistence.ts index 92d0f83711..3300fc1e8a 100644 --- a/electron/js/settings/ConfigurationPersistence.js +++ b/electron/src/js/settings/ConfigurationPersistence.ts @@ -17,13 +17,14 @@ * */ -'use strict'; - -const debug = require('debug'); -const fs = require('fs-extra'); -const SchemaUpdater = require('./SchemaUpdater'); +import * as debug from 'debug'; +import * as fs from 'fs-extra'; +import {SchemaUpdater} from './SchemaUpdater'; class ConfigurationPersistence { + configFile: string; + debug: debug.IDebugger; + constructor() { this.configFile = SchemaUpdater.updateToVersion1(); this.debug = debug('ConfigurationPersistence'); @@ -35,19 +36,19 @@ class ConfigurationPersistence { this.debug('Init ConfigurationPersistence'); } - delete(name) { + delete(name: string): true { this.debug('Deleting %s', name); delete global._ConfigurationPersistence[name]; return true; } - save(name, value) { + save(name: string, value: T): true { this.debug('Saving %s with value "%o"', name, value); global._ConfigurationPersistence[name] = value; return true; } - restore(name, defaultValue) { + restore(name: string, defaultValue?: T): T { this.debug('Restoring %s', name); const value = global._ConfigurationPersistence[name]; return typeof value !== 'undefined' ? value : defaultValue; @@ -62,17 +63,18 @@ class ConfigurationPersistence { } } - readFromFile() { + readFromFile(): any { this.debug(`Reading config file "${this.configFile}"...`); try { return fs.readJSONSync(this.configFile); } catch (error) { + const schemataKeys = Object.keys(SchemaUpdater.SCHEMATA); // In case of an error, always use the latest schema with sensible defaults: - return SchemaUpdater.SCHEMATA[ - Object.keys(SchemaUpdater.SCHEMATA)[Object.keys(SchemaUpdater.SCHEMATA).length - 1] - ]; + return SchemaUpdater.SCHEMATA[schemataKeys[schemataKeys.length - 1]]; } } } -module.exports = new ConfigurationPersistence(); +const settings = new ConfigurationPersistence(); + +export {settings}; diff --git a/electron/js/settings/SchemaUpdater.js b/electron/src/js/settings/SchemaUpdater.ts similarity index 76% rename from electron/js/settings/SchemaUpdater.js rename to electron/src/js/settings/SchemaUpdater.ts index 7cb43beab2..8cc3e5af6e 100644 --- a/electron/js/settings/SchemaUpdater.js +++ b/electron/src/js/settings/SchemaUpdater.ts @@ -17,28 +17,28 @@ * */ -// @ts-check +import * as debug from 'debug'; +import * as Electron from 'electron'; +import * as fs from 'fs-extra'; +import * as path from 'path'; -const app = require('electron').app || require('electron').remote.app; -const debug = require('debug'); -const fs = require('fs-extra'); -const path = require('path'); -const SettingsType = require('./SettingsType'); +import {Schemata} from '../../interfaces/'; +import {SettingsType} from './SettingsType'; + +const app = Electron.app || Electron.remote.app; const debugLogger = debug('SchemaUpdate'); const defaultPathV0 = path.join(app.getPath('userData'), 'init.json'); const defaultPathV1 = path.join(app.getPath('userData'), 'config', 'init.json'); class SchemaUpdater { - static get SCHEMATA() { - return { - VERSION_1: { - configVersion: 1, - }, - }; - } + static SCHEMATA: Schemata = { + VERSION_1: { + configVersion: 1, + }, + }; - static updateToVersion1(configFileV0 = defaultPathV0, configFileV1 = defaultPathV1) { + static updateToVersion1(configFileV0: string = defaultPathV0, configFileV1: string = defaultPathV1): string { const config = SchemaUpdater.SCHEMATA.VERSION_1; if (fs.existsSync(configFileV0)) { @@ -49,7 +49,7 @@ class SchemaUpdater { debugLogger(`Could not upgrade "${configFileV0}" to "${configFileV1}": ${error.message}`, error); } - const getSetting = setting => (config.hasOwnProperty(setting) ? config[setting] : undefined); + const getSetting = (setting: string) => (config.hasOwnProperty(setting) ? config[setting] : undefined); const hasNoConfigVersion = typeof getSetting('configVersion') === 'undefined'; if (hasNoConfigVersion) { @@ -72,4 +72,4 @@ class SchemaUpdater { } } -module.exports = SchemaUpdater; +export {SchemaUpdater}; diff --git a/electron/js/settings/SettingsType.js b/electron/src/js/settings/SettingsType.ts similarity index 94% rename from electron/js/settings/SettingsType.js rename to electron/src/js/settings/SettingsType.ts index 444091aad9..ace732090a 100644 --- a/electron/js/settings/SettingsType.js +++ b/electron/src/js/settings/SettingsType.ts @@ -17,7 +17,7 @@ * */ -module.exports = { +const SettingsType = { AUTO_LAUNCH: 'shouldAutoLaunch', ENV: 'env', FULL_SCREEN: 'fullscreen', @@ -26,3 +26,5 @@ module.exports = { SPELL_CHECK: 'spelling', WINDOW_BOUNDS: 'bounds', }; + +export {SettingsType}; diff --git a/electron/js/squirrel.js b/electron/src/js/squirrel.ts similarity index 74% rename from electron/js/squirrel.js rename to electron/src/js/squirrel.ts index c12d4c3b6b..2e98601ab6 100644 --- a/electron/js/squirrel.js +++ b/electron/src/js/squirrel.ts @@ -17,16 +17,18 @@ * */ -// https://github.com/atom/atom/blob/master/src/main-process/squirrel-update.coffee +// https://github.com/atom/atom/blob/master/src/main-process/squirrel-update.js -const {app} = require('electron'); +import {app} from 'electron'; -const config = require('./config'); -const cp = require('child_process'); -const environment = require('./environment'); -const lifecycle = require('./lifecycle'); -const fs = require('fs'); -const path = require('path'); +import * as cp from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as config from './config'; +import * as environment from './environment'; +import * as lifecycle from './lifecycle'; + +import {SpawnCallback, SpawnError} from '../interfaces/'; app.setAppUserModelId(`com.squirrel.wire.${config.NAME.toLowerCase()}`); @@ -36,11 +38,19 @@ const updateDotExe = path.join(rootFolder, 'Update.exe'); const exeName = `${config.NAME}.exe`; const linkName = `${config.NAME}.lnk`; +const windowsAppData = process.env.APPDATA || ''; const taskbarLink = path.resolve( - path.join(process.env.APPDATA, 'Microsoft', 'Internet Explorer', 'Quick Launch', 'User Pinned', 'TaskBar', linkName) + windowsAppData, + 'Microsoft', + 'Internet Explorer', + 'Quick Launch', + 'User Pinned', + 'TaskBar' ); +const shortcutLink = path.resolve(taskbarLink, linkName); + const SQUIRREL_EVENT = { CREATE_SHORTCUT: '--createShortcut', INSTALL: '--squirrel-install', @@ -51,11 +61,10 @@ const SQUIRREL_EVENT = { UPDATED: '--squirrel-updated', }; -const spawn = (command, args, callback) => { - let error; +const spawn = (command: string, args: string[], callback?: SpawnCallback) => { + let error: SpawnError | null; let spawnedProcess; - let stdout; - stdout = ''; + let stdout = ''; try { spawnedProcess = cp.spawn(command, args); @@ -88,34 +97,34 @@ const spawn = (command, args, callback) => { }); }; -const spawnUpdate = (args, callback) => { +const spawnUpdate = (args: string[], callback?: SpawnCallback): void => { spawn(updateDotExe, args, callback); }; -const createStartShortcut = callback => { +const createStartShortcut = (callback?: SpawnCallback): void => { spawnUpdate([SQUIRREL_EVENT.CREATE_SHORTCUT, exeName, '-l=StartMenu'], callback); }; -const createDesktopShortcut = callback => { +const createDesktopShortcut = (callback?: SpawnCallback): void => { spawnUpdate([SQUIRREL_EVENT.CREATE_SHORTCUT, exeName, '-l=Desktop'], callback); }; -const removeShortcuts = callback => { +const removeShortcuts = (callback: (err: NodeJS.ErrnoException) => void): void => { spawnUpdate([SQUIRREL_EVENT.REMOVE_SHORTCUT, exeName, '-l=Desktop,Startup,StartMenu'], () => - fs.unlink(taskbarLink, callback) + fs.unlink(shortcutLink, callback) ); }; -const installUpdate = () => { +const installUpdate = (): void => { spawnUpdate([SQUIRREL_EVENT.UPDATE, environment.app.UPDATE_URL_WIN]); }; -const scheduleUpdate = () => { +const scheduleUpdate = (): void => { setTimeout(installUpdate, config.UPDATE.DELAY); setInterval(installUpdate, config.UPDATE.INTERVAL); }; -const handleSquirrelEvent = shouldQuit => { +const handleSquirrelEvent = (shouldQuit: boolean): boolean | void => { const [, squirrelEvent] = process.argv; switch (squirrelEvent) { @@ -151,7 +160,4 @@ const handleSquirrelEvent = shouldQuit => { scheduleUpdate(); }; -module.exports = { - handleSquirrelEvent, - installUpdate, -}; +export {handleSquirrelEvent, installUpdate}; diff --git a/electron/src/js/util.ts b/electron/src/js/util.ts new file mode 100644 index 0000000000..b51876f294 --- /dev/null +++ b/electron/src/js/util.ts @@ -0,0 +1,42 @@ +/* + * Wire + * Copyright (C) 2018 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import * as Electron from 'electron'; +import * as url from 'url'; + +import {pointInRectangle} from './lib/pointInRect'; + +const capitalize = (input: string): string => input.charAt(0).toUpperCase() + input.substr(1); + +const isInView = (win: Electron.BrowserWindow): boolean => { + const windowBounds = win.getBounds(); + const nearestWorkArea = Electron.screen.getDisplayMatching(windowBounds).workArea; + + const upperLeftVisible = pointInRectangle([windowBounds.x, windowBounds.y], nearestWorkArea); + const lowerRightVisible = pointInRectangle( + [windowBounds.x + windowBounds.width, windowBounds.y + windowBounds.height], + nearestWorkArea + ); + + return upperLeftVisible || lowerRightVisible; +}; + +const isMatchingHost = (_url: string, _baseUrl: string): boolean => url.parse(_url).host === url.parse(_baseUrl).host; + +export {capitalize, isInView, isMatchingHost}; diff --git a/electron/js/window-manager.js b/electron/src/js/window-manager.ts similarity index 69% rename from electron/js/window-manager.js rename to electron/src/js/window-manager.ts index 799de7b6da..3510131cfe 100644 --- a/electron/js/window-manager.js +++ b/electron/src/js/window-manager.ts @@ -17,17 +17,20 @@ * */ -const {BrowserWindow} = require('electron'); +import {BrowserWindow} from 'electron'; -let primaryWindowId; +let primaryWindowId: number | undefined; const getPrimaryWindow = () => { - return primaryWindowId ? BrowserWindow.fromId(primaryWindowId) : BrowserWindow.getAllWindows()[0]; + const [primaryWindow] = primaryWindowId ? [BrowserWindow.fromId(primaryWindowId)] : BrowserWindow.getAllWindows(); + return primaryWindow; }; -const setPrimaryWindowId = newPrimaryWindowId => (primaryWindowId = newPrimaryWindowId); +const setPrimaryWindowId = (newPrimaryWindowId: number): void => { + primaryWindowId = newPrimaryWindowId; +}; -const showPrimaryWindow = () => { +const showPrimaryWindow = (): void => { const browserWindow = getPrimaryWindow(); if (browserWindow) { @@ -41,8 +44,4 @@ const showPrimaryWindow = () => { } }; -module.exports = { - getPrimaryWindow, - setPrimaryWindowId, - showPrimaryWindow, -}; +export {getPrimaryWindow, setPrimaryWindowId, showPrimaryWindow}; diff --git a/electron/src/locale/locale.ts b/electron/src/locale/locale.ts new file mode 100644 index 0000000000..038459f6d9 --- /dev/null +++ b/electron/src/locale/locale.ts @@ -0,0 +1,132 @@ +/* + * Wire + * Copyright (C) 2018 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import * as Electron from 'electron'; +import {settings} from '../js/settings/ConfigurationPersistence'; +import {SettingsType} from '../js/settings/SettingsType'; + +import {Supportedi18nLanguage, Supportedi18nLanguageObject, i18nLanguageIdentifier} from '../interfaces/'; + +const cs = require('../../locale/strings-cs'); +const da = require('../../locale/strings-da'); +const de = require('../../locale/strings-de'); +const el = require('../../locale/strings-el'); +const en = require('../../locale/strings-en'); +const es = require('../../locale/strings-es'); +const et = require('../../locale/strings-et'); +const fi = require('../../locale/strings-fi'); +const fr = require('../../locale/strings-fr'); +const hr = require('../../locale/strings-hr'); +const hu = require('../../locale/strings-hu'); +const it = require('../../locale/strings-it'); +const lt = require('../../locale/strings-lt'); +const nl = require('../../locale/strings-nl'); +const pl = require('../../locale/strings-pl'); +const pt = require('../../locale/strings-pt'); +const ro = require('../../locale/strings-ro'); +const ru = require('../../locale/strings-ru'); +const sk = require('../../locale/strings-sk'); +const sl = require('../../locale/strings-sl'); +const tr = require('../../locale/strings-tr'); +const uk = require('../../locale/strings-uk'); + +const app = Electron.app || Electron.remote.app; + +const LANGUAGES: Supportedi18nLanguageObject = { + cs, + da, + de, + el, + en, + es, + et, + fi, + fr, + hr, + hu, + it, + lt, + nl, + pl, + pt, + ro, + ru, + sk, + sl, + tr, + uk, +}; + +/* tslint:disable:object-literal-sort-keys */ +const SUPPORTED_LANGUAGES = { + en: 'English', + cs: 'Čeština', + da: 'Dansk', + de: 'Deutsch', + el: 'Ελληνικά', + et: 'Eesti', + es: 'Español', + fr: 'Français', + hr: 'Hrvatski', + it: 'Italiano', + lt: 'Lietuvos', + hu: 'Magyar', + nl: 'Nederlands', + pl: 'Polski', + pt: 'Português do Brasil', + ro: 'Română', + ru: 'Русский', + sk: 'Slovenčina', + sl: 'Slovenščina', + fi: 'Suomi', + tr: 'Türkçe', + uk: 'Українська', +}; +/* tslint:enable:object-literal-sort-keys */ + +let current: Supportedi18nLanguage | undefined; + +const getSupportedLanguageKeys = (): Supportedi18nLanguage[] => + Object.keys(SUPPORTED_LANGUAGES) as Supportedi18nLanguage[]; + +const getCurrent = (): Supportedi18nLanguage => { + if (!current) { + // We care only about the language part and not the country (en_US, de_DE) + const defaultLocale = parseLocale(app.getLocale().substr(0, 2)); + current = settings.restore(SettingsType.LOCALE, defaultLocale); + } + return current; +}; + +const parseLocale = (locale: string): Supportedi18nLanguage => { + const languageKeys = getSupportedLanguageKeys(); + return languageKeys.find(languageKey => languageKey === locale) || languageKeys[0]; +}; + +const getText = (stringIdentifier: i18nLanguageIdentifier): string => { + const strings = getCurrent(); + return LANGUAGES[strings][stringIdentifier] || LANGUAGES.en[stringIdentifier] || ''; +}; + +const setLocale = (locale: string): void => { + current = parseLocale(locale); + settings.save(SettingsType.LOCALE, current); +}; + +export {getCurrent, getText, LANGUAGES, setLocale, SUPPORTED_LANGUAGES}; diff --git a/electron/main.js b/electron/src/main.ts similarity index 72% rename from electron/main.js rename to electron/src/main.ts index 1a37af7f25..0dec3d0fae 100644 --- a/electron/main.js +++ b/electron/src/main.ts @@ -18,15 +18,16 @@ */ // Modules -const debug = require('debug'); -const debugMain = debug('mainTmp'); +import * as debug from 'debug'; +import {BrowserWindow, Event, IpcMessageEvent, Menu, app, ipcMain, shell} from 'electron'; +import WindowStateKeeper = require('electron-window-state'); +import * as fs from 'fs-extra'; +import * as minimist from 'minimist'; +import * as path from 'path'; +import {URL} from 'url'; const fileUrl = require('file-url'); -const fs = require('fs-extra'); -const minimist = require('minimist'); -const WindowStateKeeper = require('electron-window-state'); -const path = require('path'); -const {BrowserWindow, Menu, app, ipcMain, shell} = require('electron'); -const {URL} = require('url'); + +const debugMain = debug('mainTmp'); // Paths const APP_PATH = app.getAppPath(); @@ -34,30 +35,33 @@ const APP_PATH = app.getAppPath(); // Local files const CERT_ERR_HTML = fileUrl(path.join(APP_PATH, 'html', 'certificate-error.html')); const LOG_DIR = path.join(app.getPath('userData'), 'logs'); -const PRELOAD_JS = path.join(APP_PATH, 'js', 'preload.js'); +const PRELOAD_JS = path.join(APP_PATH, 'dist', 'js', 'preload.js'); const WRAPPER_CSS = path.join(APP_PATH, 'css', 'wrapper.css'); // Configuration persistence -const settings = require('./js/settings/ConfigurationPersistence'); -const SettingsType = require('./js/settings/SettingsType'); +import {settings} from './js/settings/ConfigurationPersistence'; +import {SettingsType} from './js/settings/SettingsType'; // Wrapper modules -const about = require('./js/about'); -const appInit = require('./js/appInit'); -const certificateUtils = require('./js/certificateUtils'); -const config = require('./js/config'); -const developerMenu = require('./js/menu/developer'); -const download = require('./js/lib/download'); -const environment = require('./js/environment'); -const googleAuth = require('./js/lib/googleAuth'); -const initRaygun = require('./js/initRaygun'); -const lifecycle = require('./js/lifecycle'); -const locale = require('./locale/locale'); -const systemMenu = require('./js/menu/system'); -const util = require('./js/util'); -const windowManager = require('./js/window-manager'); -const TrayHandler = require('./js/menu/TrayHandler'); -const EVENT_TYPE = require('./js/lib/eventType'); +import * as about from './js/about'; +import * as appInit from './js/appInit'; +import * as certificateUtils from './js/certificateUtils'; +import * as config from './js/config'; +import * as environment from './js/environment'; +import * as initRaygun from './js/initRaygun'; +import {download} from './js/lib/download'; +import {EVENT_TYPE} from './js/lib/eventType'; +import * as googleAuth from './js/lib/googleAuth'; +import * as lifecycle from './js/lifecycle'; +import {menuItem as developerMenu} from './js/menu/developer'; +import * as systemMenu from './js/menu/system'; +import {TrayHandler} from './js/menu/TrayHandler'; +import * as util from './js/util'; +import * as windowManager from './js/window-manager'; +import * as locale from './locale/locale'; + +// Interfaces +import {OnHeadersReceivedCallback, OnHeadersReceivedDetails} from './interfaces/'; // Config const argv = minimist(process.argv.slice(1)); @@ -66,34 +70,34 @@ const BASE_URL = environment.web.getWebappUrl(argv.env); // Icon const ICON = `wire.${environment.platform.IS_WINDOWS ? 'ico' : 'png'}`; const ICON_PATH = path.join(APP_PATH, 'img', ICON); -let tray = undefined; +let tray: TrayHandler; let isFullScreen = false; let isQuitting = false; -let main; +let main: BrowserWindow; // IPC events const bindIpcEvents = () => { - ipcMain.on(EVENT_TYPE.ACTION.SAVE_PICTURE, (event, fileName, bytes) => { - download(fileName, bytes); + ipcMain.on(EVENT_TYPE.ACTION.SAVE_PICTURE, async (event: IpcMessageEvent, fileName: string, bytes: Uint8Array) => { + await download(fileName, bytes); }); ipcMain.on(EVENT_TYPE.ACTION.NOTIFICATION_CLICK, () => { windowManager.showPrimaryWindow(); }); - ipcMain.on(EVENT_TYPE.UI.BADGE_COUNT, (event, count) => { + ipcMain.on(EVENT_TYPE.UI.BADGE_COUNT, (event: IpcMessageEvent, count: number) => { tray.showUnreadCount(main, count); }); - ipcMain.on(EVENT_TYPE.GOOGLE_OAUTH.REQUEST, event => { + ipcMain.on(EVENT_TYPE.GOOGLE_OAUTH.REQUEST, (event: IpcMessageEvent) => { googleAuth .getAccessToken(config.GOOGLE_SCOPES, config.GOOGLE_CLIENT_ID, config.GOOGLE_CLIENT_SECRET) .then(code => event.sender.send('google-auth-success', code.access_token)) .catch(error => event.sender.send('google-auth-error', error)); }); - ipcMain.on(EVENT_TYPE.ACCOUNT.DELETE_DATA, (event, accountID, sessionID) => { + ipcMain.on(EVENT_TYPE.ACCOUNT.DELETE_DATA, (event: IpcMessageEvent, accountID: string, sessionID?: string) => { // delete webview partition try { if (sessionID) { @@ -119,7 +123,7 @@ const bindIpcEvents = () => { ipcMain.on(EVENT_TYPE.WRAPPER.RELAUNCH, lifecycle.relaunch); }; -const checkConfigV0FullScreen = mainWindowState => { +const checkConfigV0FullScreen = (mainWindowState: WindowStateKeeper.State) => { // if a user still has the old config version 0 and had the window maximized last time if (typeof mainWindowState.isMaximized === 'undefined' && isFullScreen === true) { main.maximize(); @@ -135,7 +139,13 @@ const initWindowStateKeeper = () => { // load version 0 full screen setting const showInFullScreen = settings.restore(SettingsType.FULL_SCREEN, 'not-set-in-v0'); - const stateKeeperOptions = { + const stateKeeperOptions: { + defaultHeight: number; + defaultWidth: number; + path: string; + fullScreen?: boolean; + maximize?: boolean; + } = { defaultHeight: loadedWindowBounds.height, defaultWidth: loadedWindowBounds.width, path: path.join(app.getPath('userData'), 'config'), @@ -151,10 +161,10 @@ const initWindowStateKeeper = () => { }; // App Windows -const showMainWindow = mainWindowState => { +const showMainWindow = (mainWindowState: WindowStateKeeper.State) => { const showMenuBar = settings.restore(SettingsType.SHOW_MENU_BAR, true); - const options = { + const options: Electron.BrowserWindowConstructorOptions = { autoHideMenuBar: !showMenuBar, backgroundColor: '#f7f8fa', height: mainWindowState.height, @@ -182,7 +192,7 @@ const showMainWindow = mainWindowState => { let baseURL = BASE_URL; baseURL += `${baseURL.includes('?') ? '&' : '?'}hl=${locale.getCurrent()}`; - main.loadURL(`file://${__dirname}/renderer/index.html?env=${encodeURIComponent(baseURL)}`); + main.loadURL(`file://${__dirname}/../renderer/index.html?env=${encodeURIComponent(baseURL)}`); if (argv.devtools) { main.webContents.openDevTools({mode: 'detach'}); @@ -207,7 +217,7 @@ const showMainWindow = mainWindowState => { event.preventDefault(); // Ensure the link does not come from a webview - if (typeof event.sender.viewInstanceId !== 'undefined') { + if (typeof (event.sender as any).viewInstanceId !== 'undefined') { debugMain('New window was created from a webview, aborting.'); return; } @@ -219,7 +229,7 @@ const showMainWindow = mainWindowState => { { urls: ['https://staging-nginz-https.zinfra.io/*'], }, - (details, callback) => { + (details: OnHeadersReceivedDetails, callback: OnHeadersReceivedCallback) => { if (environment.getEnvironment() === environment.TYPE.LOCALHOST) { // Override remote Access-Control-Allow-Origin details.responseHeaders['Access-Control-Allow-Origin'] = ['http://localhost:8080']; @@ -280,7 +290,7 @@ const handleAppEvents = () => { if (environment.app.IS_DEVELOPMENT) { appMenu.append(developerMenu); } - appMenu.on(EVENT_TYPE.ABOUT.SHOW, () => about.showWindow()); + (appMenu as any).on(EVENT_TYPE.ABOUT.SHOW, () => about.showWindow()); Menu.setApplicationMenu(appMenu); tray = new TrayHandler(); @@ -320,6 +330,8 @@ const renameLogFile = () => { }; class ElectronWrapperInit { + debug: debug.IDebugger; + constructor() { this.debug = debug('ElectronWrapperInit'); } @@ -333,7 +345,7 @@ class ElectronWrapperInit { webviewProtection() { const webviewProtectionDebug = debug('ElectronWrapperInit:webviewProtection'); - const openLinkInNewWindow = (event, _url) => { + const openLinkInNewWindow = (event: Event, _url: string) => { // Prevent default behavior event.preventDefault(); @@ -341,7 +353,7 @@ class ElectronWrapperInit { shell.openExternal(_url); }; - const willNavigateInWebview = (event, _url) => { + const willNavigateInWebview = (event: Event, _url: string) => { // Ensure navigation is to a whitelisted domain if (util.isMatchingHost(_url, BASE_URL)) { webviewProtectionDebug('Navigating inside webview. URL: %s', _url); @@ -352,7 +364,7 @@ class ElectronWrapperInit { }; app.on('web-contents-created', (webviewEvent, contents) => { - switch (contents.getType()) { + switch ((contents as any).getType()) { case 'window': contents.on('will-attach-webview', (event, webPreferences, params) => { const _url = params.src; @@ -378,30 +390,32 @@ class ElectronWrapperInit { contents.on('new-window', openLinkInNewWindow); contents.on('will-navigate', willNavigateInWebview); - contents.session.setCertificateVerifyProc((request, cb) => { - const {hostname = '', certificate = {}, verificationResult} = request; - const {hostname: hostnameInternal} = new URL(environment.URL_WEBAPP.INTERNAL); + contents.session.setCertificateVerifyProc( + (request: Electron.CertificateVerifyProcRequest, cb: (verificationResult: number) => void) => { + const {hostname, certificate, verificationResult} = request; + const {hostname: hostnameInternal} = new URL(environment.URL_WEBAPP.INTERNAL); - if (verificationResult !== 'net::OK' && hostname !== hostnameInternal) { - console.error('setCertificateVerifyProc', hostname, verificationResult); - main.loadURL(CERT_ERR_HTML); - return cb(-2); - } + if (verificationResult !== 'net::OK' && hostname !== hostnameInternal) { + console.error('setCertificateVerifyProc', hostname, verificationResult); + main.loadURL(CERT_ERR_HTML); + return cb(-2); + } - if (certificateUtils.hostnameShouldBePinned(hostname)) { - const pinningResults = certificateUtils.verifyPinning(hostname, certificate); + if (certificateUtils.hostnameShouldBePinned(hostname)) { + const pinningResults = certificateUtils.verifyPinning(hostname, certificate); - for (const result of Object.values(pinningResults)) { - if (result === false) { - console.error(`Certificate verification failed for "${hostname}":\n${pinningResults.errorMessage}`); - main.loadURL(CERT_ERR_HTML); - return cb(-2); + for (const result of Object.values(pinningResults)) { + if (result === false) { + console.error(`Certificate verification failed for "${hostname}":\n${pinningResults.errorMessage}`); + main.loadURL(CERT_ERR_HTML); + return cb(-2); + } } } - } - return cb(-3); - }); + return cb(-3); + } + ); break; } }); @@ -420,5 +434,5 @@ if (!lifecycle.shouldQuit) { bindIpcEvents(); handleAppEvents(); renameLogFile(); - new ElectronWrapperInit().run(); + new ElectronWrapperInit().run().catch(error => console.error(error)); } diff --git a/package-lock.json b/package-lock.json index 7b5b8fbc15..fdfd79c283 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2917,7 +2917,7 @@ }, "browserify-aes": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", "dev": true, "requires": { @@ -2962,7 +2962,7 @@ }, "browserify-rsa": { "version": "4.0.1", - "resolved": "http://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", "dev": true, "requires": { @@ -3643,9 +3643,9 @@ } }, "commander": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.12.2.tgz", - "integrity": "sha512-BFnaq5ZOGcDN7FlrtBT4xxkgIToalIIxwjxLWVJ8bGTpe1LroqMiqQXdA7ygc7CRvaYS+9zfPGFnJqFSayx+AA==", + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz", + "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==", "dev": true }, "commondir": { @@ -3820,7 +3820,7 @@ }, "create-hash": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", "dev": true, "requires": { @@ -3833,7 +3833,7 @@ }, "create-hmac": { "version": "1.1.7", - "resolved": "http://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", "dev": true, "requires": { @@ -4181,7 +4181,7 @@ }, "diffie-hellman": { "version": "5.0.3", - "resolved": "http://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", "dev": true, "requires": { @@ -4288,7 +4288,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "requires": { @@ -5332,7 +5332,7 @@ }, "events": { "version": "1.1.1", - "resolved": "http://registry.npmjs.org/events/-/events-1.1.1.tgz", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=", "dev": true }, @@ -5665,6 +5665,15 @@ "ms": "2.0.0" } }, + "fd-slicer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz", + "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=", + "dev": true, + "requires": { + "pend": "~1.2.0" + } + }, "mkdirp": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.0.tgz", @@ -5673,6 +5682,15 @@ "requires": { "minimist": "0.0.8" } + }, + "yauzl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz", + "integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=", + "dev": true, + "requires": { + "fd-slicer": "~1.0.1" + } } } }, @@ -5721,15 +5739,6 @@ "bser": "^2.0.0" } }, - "fd-slicer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz", - "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=", - "dev": true, - "requires": { - "pend": "~1.2.0" - } - }, "figures": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", @@ -5933,7 +5942,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "requires": { @@ -6022,7 +6031,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "requires": { @@ -6138,8 +6147,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -6160,14 +6168,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -6182,20 +6188,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -6312,8 +6315,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -6325,7 +6327,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -6340,7 +6341,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -6348,14 +6348,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.2.4", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -6374,7 +6372,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -6455,8 +6452,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -6468,7 +6464,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -6554,8 +6549,7 @@ "safe-buffer": { "version": "5.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -6591,7 +6585,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -6611,7 +6604,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -6655,14 +6647,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, @@ -7623,6 +7613,12 @@ "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", "dev": true }, + "immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=", + "dev": true + }, "import-lazy": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", @@ -9991,6 +9987,47 @@ "array-includes": "^3.0.3" } }, + "jszip": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.1.5.tgz", + "integrity": "sha512-5W8NUaFRFRqTOL7ZDDrx5qWHJyBXy6velVudIzQUSoqAAYqzSh2Z7/m0Rf1QbmQJccegD0r+YZxBjzqoBiEeJQ==", + "dev": true, + "requires": { + "core-js": "~2.3.0", + "es6-promise": "~3.0.2", + "lie": "~3.1.0", + "pako": "~1.0.2", + "readable-stream": "~2.0.6" + }, + "dependencies": { + "core-js": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.3.0.tgz", + "integrity": "sha1-+rg/uwstjchfpjbEudNMdUIMbWU=", + "dev": true + }, + "es6-promise": { + "version": "3.0.2", + "resolved": "http://registry.npmjs.org/es6-promise/-/es6-promise-3.0.2.tgz", + "integrity": "sha1-AQ1YWEI6XxGJeWZfRkhqlcbuK7Y=", + "dev": true + }, + "readable-stream": { + "version": "2.0.6", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", + "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "string_decoder": "~0.10.x", + "util-deprecate": "~1.0.1" + } + } + } + }, "just-extend": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-3.0.0.tgz", @@ -10067,6 +10104,15 @@ "type-check": "~0.3.2" } }, + "lie": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", + "integrity": "sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=", + "dev": true, + "requires": { + "immediate": "~3.0.5" + } + }, "lint-staged": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-7.3.0.tgz", @@ -10852,7 +10898,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "requires": { @@ -11180,7 +11226,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "requires": { @@ -11638,7 +11684,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "requires": { @@ -11664,7 +11710,7 @@ }, "parse-asn1": { "version": "5.1.1", - "resolved": "http://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz", "integrity": "sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw==", "dev": true, "requires": { @@ -12484,7 +12530,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "requires": { @@ -13071,7 +13117,7 @@ }, "sha.js": { "version": "2.4.11", - "resolved": "http://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", "dev": true, "requires": { @@ -13539,7 +13585,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "requires": { @@ -13594,7 +13640,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "requires": { @@ -14251,6 +14297,100 @@ "integrity": "sha512-avfPS28HmGLLc2o4elcc2EIq2FcH++Yo5YxpBZi9Yw93BCTGFthI4HPE4Rpep6vSYQaK8e69PelM44tPj+RaQg==", "dev": true }, + "tslint": { + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.11.0.tgz", + "integrity": "sha1-mPMMAurjzecAYgHkwzywi0hYHu0=", + "dev": true, + "requires": { + "babel-code-frame": "^6.22.0", + "builtin-modules": "^1.1.1", + "chalk": "^2.3.0", + "commander": "^2.12.1", + "diff": "^3.2.0", + "glob": "^7.1.1", + "js-yaml": "^3.7.0", + "minimatch": "^3.0.4", + "resolve": "^1.3.2", + "semver": "^5.3.0", + "tslib": "^1.8.0", + "tsutils": "^2.27.2" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "tslint-config-prettier": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/tslint-config-prettier/-/tslint-config-prettier-1.15.0.tgz", + "integrity": "sha512-06CgrHJxJmNYVgsmeMoa1KXzQRoOdvfkqnJth6XUkNeOz707qxN0WfxfhYwhL5kXHHbYJRby2bqAPKwThlZPhw==", + "dev": true + }, + "tslint-plugin-prettier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tslint-plugin-prettier/-/tslint-plugin-prettier-2.0.0.tgz", + "integrity": "sha512-nA8yM+1tS9dylirSajTxxFV6jCQrIMQ0Ykl//jjRgqmwwmGp3hqodG+rtr16S/OUwyQBfoFScFDK7nuHYPd4Gw==", + "dev": true, + "requires": { + "eslint-plugin-prettier": "^2.2.0", + "tslib": "^1.7.1" + }, + "dependencies": { + "eslint-plugin-prettier": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-2.7.0.tgz", + "integrity": "sha512-CStQYJgALoQBw3FsBzH0VOVDRnJ/ZimUlpLm226U8qgqYJfPOY/CPK6wyRInMxh73HSKg5wyRwdS4BVYYHwokA==", + "dev": true, + "requires": { + "fast-diff": "^1.1.1", + "jest-docblock": "^21.0.0" + } + }, + "jest-docblock": { + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-21.2.0.tgz", + "integrity": "sha512-5IZ7sY9dBAYSV+YjQ0Ovb540Ku7AO9Z5o2Cg789xj167iQuZ2cG+z0f3Uct6WeYLbU6aQiM2pCs7sZ+4dotydw==", + "dev": true + } + } + }, + "tsutils": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", + "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, "tty-browserify": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", @@ -14294,6 +14434,12 @@ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", "dev": true }, + "typescript": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.1.3.tgz", + "integrity": "sha512-+81MUSyX+BaSo+u2RbozuQk/UWx6hfG0a5gHu4ANEM4sU96XbuIyAB+rWBW1u70c6a5QuZfuYICn3s2UjuHUpA==", + "dev": true + }, "uglify-es": { "version": "3.3.9", "resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.3.9.tgz", @@ -15444,15 +15590,6 @@ "dev": true } } - }, - "yauzl": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz", - "integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=", - "dev": true, - "requires": { - "fd-slicer": "~1.0.1" - } } } } diff --git a/package.json b/package.json index 87cb5044b9..4105dc9d52 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "babel-core": "7.0.0-bridge.0", "babel-jest": "23.6.0", "babel-loader": "8.0.4", + "commander": "2.19.0", "css-loader": "1.0.0", "electron": "1.8.8", "electron-builder": "20.28.4", @@ -35,11 +36,17 @@ "grunt-gitinfo": "0.1.8", "husky": "1.1.2", "jest": "23.6.0", + "jszip": "3.1.5", "lint-staged": "7.3.0", "load-grunt-tasks": "4.0.0", "prettier": "1.14.3", + "rimraf": "2.6.2", "sinon": "7.0.0", "style-loader": "0.23.1", + "tslint": "5.11.0", + "tslint-config-prettier": "1.15.0", + "tslint-plugin-prettier": "2.0.0", + "typescript": "3.1.3", "webpack": "4.23.0", "webpack-cli": "3.1.2" }, @@ -54,6 +61,10 @@ "eslint --fix", "git add" ], + "*.ts": [ + "tslint --config tslint.json --fix", + "git add" + ], "*.{json,md,css}": [ "prettier --write", "git add" @@ -65,17 +76,24 @@ "url": "https://github.com/wireapp/wire-desktop.git" }, "scripts": { - "build:linux": "grunt linux-prod", - "build:macos": "grunt macos-prod", - "build:win": "grunt win-prod", + "build:linux": "npm run build:ts && grunt linux-prod", + "build:macos": "npm run build:ts && grunt macos-prod", + "build:ts": "npm run clear:ts && tsc", + "build:win": "npm run build:ts && grunt win-prod", "bundle:dev": "webpack", "bundle": "webpack --env.production", + "clear:ts": "rimraf electron/dist", "fix:js": "npm run test:js -- --fix", "fix:other": "npm run prettier -- --write", - "fix": "npm run fix:js && npm run fix:other", + "fix:ts": "npm run lint:ts --fix", + "fix": "npm run fix:js && npm run fix:other && npm run fix:ts", "jest": "jest", + "lint:js": "eslint -c .eslintrc.json --ignore-path .gitignore --ignore-path .eslintignore \"**/*.js\"", + "lint:other": "npm run prettier --list-different", + "lint:ts": "tslint --config tslint.json \"**/*.ts\"", + "lint": "npm run lint:js && npm run lint:other && npm run lint:ts", "postinstall": "cd electron && npm install && electron-builder install-app-deps", - "prestart": "npm run bundle:dev", + "prestart": "npm run build:ts && npm run bundle:dev", "prettier": "prettier \"**/*.{json,md,css}\"", "start:dev": "npm start -- --env=https://wire-webapp-dev.zinfra.io", "start:edge": "npm start -- --env=https://wire-webapp-edge.zinfra.io", @@ -84,10 +102,8 @@ "start:prod": "npm start -- --env=https://app.wire.com", "start:staging": "npm start -- --env=https://wire-webapp-staging.zinfra.io", "start": "npm run prestart && electron electron --inspect --devtools --enable-logging", - "test:js": "eslint -c .eslintrc.json --ignore-path .gitignore --ignore-path .eslintignore \"**/*.js\"", "test:main": "electron-mocha --reporter spec tests/main", - "test:other": "npm run prettier -- --list-different", "test:react": "jest", - "test": "npm run test:other && npm run test:js && npm run test:react && npm run test:main" + "test": "npm run lint && npm run build:ts && npm run test:react && npm run test:main" } } diff --git a/tests/certPinningTest.js b/tests/certPinningTest.js index ea44b67127..72bf868e8d 100644 --- a/tests/certPinningTest.js +++ b/tests/certPinningTest.js @@ -19,7 +19,7 @@ 'use strict'; -const certificateUtils = require('../electron/js/certificateUtils'); +const certificateUtils = require('../electron/dist/js/certificateUtils'); const https = require('https'); const assert = require('assert'); diff --git a/tests/main/TrayHandlerTest.js b/tests/main/TrayHandlerTest.js index b3205a6558..f09f28b5a3 100644 --- a/tests/main/TrayHandlerTest.js +++ b/tests/main/TrayHandlerTest.js @@ -23,7 +23,7 @@ const {app} = require('electron'); const assert = require('assert'); const path = require('path'); const sinon = require('sinon'); -const TrayHandler = require('../../electron/js/menu/TrayHandler'); +const {TrayHandler} = require('../../electron/dist/js/menu/TrayHandler'); const {BrowserWindow} = require('electron'); describe('TrayIconHandler', () => { diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000000..57e9ffcba3 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "declaration": true, + "downlevelIteration": true, + "forceConsistentCasingInFileNames": true, + "lib": ["dom", "es2016", "es2017.object"], + "module": "CommonJS", + "moduleResolution": "node", + "noEmitOnError": true, + "noImplicitReturns": true, + "noUnusedLocals": true, + "noUnusedParameters": false, + "outDir": "electron/dist", + "removeComments": true, + "rootDir": "electron/src", + "sourceMap": true, + "strict": true, + "target": "es5" + }, + "exclude": ["electron/dist", "node_modules", "electron/node_modules"] +} diff --git a/tslint.json b/tslint.json new file mode 100644 index 0000000000..0bae9ae195 --- /dev/null +++ b/tslint.json @@ -0,0 +1,36 @@ +{ + "extends": ["tslint-config-prettier", "tslint-plugin-prettier"], + "rules": { + "array-type": [true, "array"], + "ban-comma-operator": true, + "curly": true, + "jsdoc-format": [true, "check-multiline-start"], + "no-duplicate-imports": true, + "no-duplicate-switch-case": true, + "no-duplicate-variable": [true, "check-parameters"], + "no-floating-promises": true, + "no-invalid-template-strings": true, + "no-object-literal-type-assertion": true, + "no-return-await": true, + "no-sparse-arrays": true, + "ordered-imports": [ + true, + { + "named-imports-order": "lowercase-last" + } + ], + "no-this-assignment": true, + "no-unused-expression": true, + "object-literal-sort-keys": true, + "prefer-conditional-expression": true, + "prefer-const": true, + "prefer-object-spread": true, + "prefer-readonly": true, + "prefer-template": true, + "prettier": true, + "space-within-parens": [true, 0] + }, + "linterOptions": { + "exclude": ["**/bower_components/**", "**/dist/**", "**/node_modules/**"] + } +}