From 15bc264fd5b1e70442036f3c984ce61b4f3c34f2 Mon Sep 17 00:00:00 2001 From: Yuya Ochiai Date: Tue, 7 Aug 2018 23:47:43 +0900 Subject: [PATCH 01/18] Keep autostart config in sync with config.json --- src/browser/components/SettingsPage.jsx | 38 ++------------------ src/common/config/defaultPreferences.js | 1 + src/main.js | 10 ++++++ src/main/AutoLauncher.js | 48 +++++++++++++++++++++++++ 4 files changed, 61 insertions(+), 36 deletions(-) create mode 100644 src/main/AutoLauncher.js diff --git a/src/browser/components/SettingsPage.jsx b/src/browser/components/SettingsPage.jsx index 0d540183..7fd2c7fe 100644 --- a/src/browser/components/SettingsPage.jsx +++ b/src/browser/components/SettingsPage.jsx @@ -8,7 +8,6 @@ import ReactDOM from 'react-dom'; import {Button, Checkbox, Col, FormGroup, Grid, HelpBlock, Navbar, Radio, Row} from 'react-bootstrap'; import {ipcRenderer, remote} from 'electron'; -import AutoLaunch from 'auto-launch'; import {debounce} from 'underscore'; import buildConfig from '../../common/config/buildConfig'; @@ -17,11 +16,6 @@ import settings from '../../common/settings'; import TeamList from './TeamList.jsx'; import AutoSaveIndicator from './AutoSaveIndicator.jsx'; -const appLauncher = new AutoLaunch({ - name: remote.app.getName(), - isHidden: true, -}); - function backToIndex(index) { const target = typeof index === 'undefined' ? 0 : index; const indexURL = remote.getGlobal('isDev') ? 'http://localhost:8080/browser/index.html' : `file://${remote.app.getAppPath()}/browser/index.html`; @@ -58,14 +52,6 @@ const SettingsPage = createReactClass({ return initialState; }, componentDidMount() { - if (process.platform === 'win32' || process.platform === 'linux') { - var self = this; - appLauncher.isEnabled().then((enabled) => { - self.setState({ - autostart: enabled, - }); - }); - } ipcRenderer.on('add-server', () => { this.setState({ showAddTeamForm: true, @@ -140,6 +126,7 @@ const SettingsPage = createReactClass({ useSpellChecker: this.state.useSpellChecker, spellCheckerLocale: this.state.spellCheckerLocale, enableHardwareAcceleration: this.state.enableHardwareAcceleration, + autostart: this.state.autostart, }; settings.writeFile(this.props.configFile, config, (err) => { @@ -149,31 +136,10 @@ const SettingsPage = createReactClass({ } ipcRenderer.send('update-menu', config); ipcRenderer.send('update-config'); - if (process.platform === 'win32' || process.platform === 'linux') { - const autostart = this.state.autostart; - this.saveAutoStart(autostart, callback); - } else { - callback(); - } + callback(); }); }, - saveAutoStart(autostart, callback) { - appLauncher.isEnabled().then((enabled) => { - if (enabled && !autostart) { - appLauncher.disable().then(() => { - callback(); - }).catch(callback); - } else if (!enabled && autostart) { - appLauncher.enable().then(() => { - callback(); - }).catch(callback); - } else { - callback(); - } - }).catch(callback); - }, - handleCancel() { backToIndex(); }, diff --git a/src/common/config/defaultPreferences.js b/src/common/config/defaultPreferences.js index 3c0ca30b..6cd9dc40 100644 --- a/src/common/config/defaultPreferences.js +++ b/src/common/config/defaultPreferences.js @@ -20,6 +20,7 @@ const defaultPreferences = { showUnreadBadge: true, useSpellChecker: true, enableHardwareAcceleration: true, + autostart: false, }; export default defaultPreferences; diff --git a/src/main.js b/src/main.js index e6cfd1ea..42466a58 100644 --- a/src/main.js +++ b/src/main.js @@ -24,6 +24,7 @@ import {parse as parseArgv} from 'yargs'; import {protocols} from '../electron-builder.json'; import squirrelStartup from './main/squirrelStartup'; +import AutoLauncher from './main/AutoLauncher'; import CriticalErrorHandler from './main/CriticalErrorHandler'; const criticalErrorHandler = new CriticalErrorHandler(); @@ -102,6 +103,15 @@ if (config.enableHardwareAcceleration === false) { ipcMain.on('update-config', () => { const configFile = app.getPath('userData') + '/config.json'; config = settings.readFileSync(configFile); + if (process.platform === 'win32' || process.platform === 'linux') { + const appLauncher = new AutoLauncher(); + const autoStartTask = config.autostart ? appLauncher.enable() : appLauncher.disable(); + autoStartTask.then(() => { + console.log('config.autostart has been configured:', config.autostart); + }).catch((err) => { + console.log('error:', err); + }); + } const trustedURLs = settings.mergeDefaultTeams(config.teams).map((team) => team.url); permissionManager.setTrustedURLs(trustedURLs); ipcMain.emit('update-dict', true, config.spellCheckerLocale); diff --git a/src/main/AutoLauncher.js b/src/main/AutoLauncher.js new file mode 100644 index 00000000..2251c7c7 --- /dev/null +++ b/src/main/AutoLauncher.js @@ -0,0 +1,48 @@ +// Copyright (c) 2015-2016 Yuya Ochiai +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import AutoLaunch from 'auto-launch'; +import {app} from 'electron'; +import isDev from 'electron-is-dev'; + +export default class AutoLauncher { + constructor() { + this.appLauncher = new AutoLaunch({ + name: app.getName(), + isHidden: true, + }); + } + + isEnabled() { + return this.appLauncher.isEnabled(); + } + + async blankPromise() { + return null; + } + + async enable() { + if (isDev) { + console.log('In development mode, autostart config never effects'); + return this.blankPromise(); + } + const enabled = await this.isEnabled(); + if (!enabled) { + return this.appLauncher.enable(); + } + return this.blankPromise(); + } + + async disable() { + if (isDev) { + console.log('In development mode, autostart config never effects'); + return this.blankPromise(); + } + const enabled = await this.isEnabled(); + if (enabled) { + return this.appLauncher.disable(); + } + return this.blankPromise(); + } +} From ac8c692c45e343eda9a576fb40f29317be92bf52 Mon Sep 17 00:00:00 2001 From: Yuya Ochiai Date: Fri, 10 Aug 2018 21:40:53 +0900 Subject: [PATCH 02/18] Set "app start on login" preference to default on --- src/common/config/defaultPreferences.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/config/defaultPreferences.js b/src/common/config/defaultPreferences.js index 6cd9dc40..8a26665f 100644 --- a/src/common/config/defaultPreferences.js +++ b/src/common/config/defaultPreferences.js @@ -20,7 +20,7 @@ const defaultPreferences = { showUnreadBadge: true, useSpellChecker: true, enableHardwareAcceleration: true, - autostart: false, + autostart: true, }; export default defaultPreferences; From cd06b3b709edac5f641bb7ab21ca65eaf8d0308f Mon Sep 17 00:00:00 2001 From: Yuya Ochiai Date: Thu, 16 Aug 2018 23:46:40 +0900 Subject: [PATCH 03/18] Upgrade Electron to 2.0.8 --- package.json | 2 +- yarn.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index e5fdb3b6..c080d50c 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "cross-env": "^5.1.6", "css-loader": "^0.28.11", "devtron": "^1.4.0", - "electron": "2.0.2", + "electron": "2.0.8", "electron-builder": "20.14.7", "electron-builder-squirrel-windows": "~20.14.0", "electron-connect": "^0.6.3", diff --git a/yarn.lock b/yarn.lock index afbe8747..2938946c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3726,9 +3726,9 @@ electron-to-chromium@^1.3.47: version "1.3.48" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.48.tgz#d3b0d8593814044e092ece2108fc3ac9aea4b900" -electron@2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/electron/-/electron-2.0.2.tgz#b77e05f83419cc5ec921a2d21f35b55e4bfc3d68" +electron@2.0.8: + version "2.0.8" + resolved "https://registry.yarnpkg.com/electron/-/electron-2.0.8.tgz#6ec7113b356e09cc9899797e0d41ebff8163e962" dependencies: "@types/node" "^8.0.24" electron-download "^3.0.1" From 9f9f5de5ddd80aced6f01fbbb5afe2e1c9c563f5 Mon Sep 17 00:00:00 2001 From: Yuya Ochiai Date: Tue, 28 Aug 2018 22:28:47 +0900 Subject: [PATCH 04/18] Hide the main window on Mac auto-start --- src/main.js | 7 ++----- src/main/utils.js | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 5 deletions(-) create mode 100644 src/main/utils.js diff --git a/src/main.js b/src/main.js index e6cfd1ea..da7df68c 100644 --- a/src/main.js +++ b/src/main.js @@ -50,6 +50,7 @@ import PermissionManager from './main/PermissionManager'; import permissionRequestHandler from './main/permissionRequestHandler'; import AppStateManager from './main/AppStateManager'; import initCookieManager from './main/cookieManager'; +import {shouldBeHiddenOnStartup} from './main/utils'; import SpellChecker from './main/SpellChecker'; @@ -65,11 +66,7 @@ let appState = null; let permissionManager = null; const argv = parseArgv(process.argv.slice(1)); - -var hideOnStartup; -if (argv.hidden) { - hideOnStartup = true; -} +const hideOnStartup = shouldBeHiddenOnStartup(argv); if (argv['data-dir']) { app.setPath('userData', path.resolve(argv['data-dir'])); diff --git a/src/main/utils.js b/src/main/utils.js new file mode 100644 index 00000000..5baaa415 --- /dev/null +++ b/src/main/utils.js @@ -0,0 +1,17 @@ +// Copyright (c) 2015-2016 Yuya Ochiai +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {app} from 'electron'; + +export function shouldBeHiddenOnStartup(parsedArgv) { + if (parsedArgv.hidden) { + return true; + } + if (process.platform === 'darwin') { + if (app.getLoginItemSettings().wasOpenedAsHidden) { + return true; + } + } + return false; +} From 1d7b937193284628fe9e4389ee26e95ea9d17f43 Mon Sep 17 00:00:00 2001 From: Jason Blais <13119842+jasonblais@users.noreply.github.com> Date: Fri, 31 Aug 2018 07:56:47 -0400 Subject: [PATCH 05/18] Update contextMenu.js --- src/browser/js/contextMenu.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/js/contextMenu.js b/src/browser/js/contextMenu.js index 804b321d..ad8ee923 100644 --- a/src/browser/js/contextMenu.js +++ b/src/browser/js/contextMenu.js @@ -22,7 +22,7 @@ function getSuggestionsMenus(win, suggestions) { function getSpellCheckerLocaleMenus(onSelectSpellCheckerLocale) { const currentLocale = ipcRenderer.sendSync('get-spellchecker-locale'); const locales = [ - {language: 'English', locale: 'en-US'}, + {language: 'English (US)', locale: 'en-US'}, {language: 'French', locale: 'fr-FR'}, {language: 'German', locale: 'de-DE'}, {language: 'Spanish', locale: 'es-ES'}, From 0160894e562fd91c32688abcba7e0b4cbb4633d4 Mon Sep 17 00:00:00 2001 From: Jason Blais <13119842+jasonblais@users.noreply.github.com> Date: Fri, 31 Aug 2018 07:57:39 -0400 Subject: [PATCH 06/18] Update SettingsPage.jsx --- src/browser/components/SettingsPage.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/components/SettingsPage.jsx b/src/browser/components/SettingsPage.jsx index 0d540183..c4563012 100644 --- a/src/browser/components/SettingsPage.jsx +++ b/src/browser/components/SettingsPage.jsx @@ -417,7 +417,7 @@ const SettingsPage = createReactClass({ {'Check spelling'} {'Highlight misspelled words in your messages.'} - {' Available for English, French, German, Spanish, and Dutch.'} + {' Available for English, French, German, Portuguese, Spanish, and Dutch.'} ); From 1bb669f32f67b51c08dcfdd6c3697e302f44893d Mon Sep 17 00:00:00 2001 From: Yuya Ochiai Date: Fri, 31 Aug 2018 21:37:15 +0900 Subject: [PATCH 07/18] Update CHANGELOG.md --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f080160..4684261a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,8 +30,9 @@ Release date: TBD [#818](https://github.com/mattermost/desktop/pull/818) ### Architectural Changes - - Major version upgrade of Electron to v2.0.2. Electron is the underlying technology used to build the Desktop apps. + - Major version upgrade of Electron to v2.0.8. Electron is the underlying technology used to build the Desktop apps. [#820](https://github.com/mattermost/desktop/pull/820) + [#847](https://github.com/mattermost/desktop/pull/847) - Artifact names are configured via `electron-builder.json`. [#825](https://github.com/mattermost/desktop/pull/825) From d69bf3aa5eda00a4733ba9ebab361d08283a1366 Mon Sep 17 00:00:00 2001 From: Yuya Ochiai Date: Mon, 3 Sep 2018 21:08:51 +0900 Subject: [PATCH 08/18] Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4684261a..74680e0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,8 @@ Release date: TBD #### Mac - Add **.dmg** package to support installation. [#588](https://github.com/mattermost/desktop/pull/588) + - Support "Hide" option of Login Items in Preferences. + [#853](https://github.com/mattermost/desktop/pull/853) #### Linux - [tar.gz] Use SVG icon for Linux application menus in place of PNG icon From d73f98825ead6f77c0aac2c790ebb94e67a75526 Mon Sep 17 00:00:00 2001 From: Yuya Ochiai Date: Mon, 3 Sep 2018 21:41:04 +0900 Subject: [PATCH 09/18] Apply no-var eslint rule --- .eslintrc.json | 1 + src/browser/components/LoginModal.jsx | 2 +- src/browser/components/MainPage.jsx | 46 +++++++++++------------ src/browser/components/MattermostView.jsx | 24 ++++++------ src/browser/components/SettingsPage.jsx | 22 +++++------ src/browser/components/TeamList.jsx | 12 +++--- src/browser/webview/mattermost.js | 32 ++++++++-------- src/common/config/upgradePreferences.js | 2 +- src/common/osVersion.js | 2 +- src/common/settings.js | 4 +- src/main.js | 14 +++---- src/main/allowProtocolDialog.js | 2 +- src/main/certificateStore.js | 2 +- src/main/mainWindow.js | 4 +- src/main/menus/app.js | 10 ++--- src/main/menus/tray.js | 2 +- 16 files changed, 92 insertions(+), 89 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 40b124ca..c929e87b 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -17,6 +17,7 @@ "no-console": 0, "no-process-env": 0, "no-underscore-dangle": 1, + "no-var": 2, "react/jsx-indent": [2, 2], "react/jsx-indent-props": [2, 2], "react/no-set-state": 1, diff --git a/src/browser/components/LoginModal.jsx b/src/browser/components/LoginModal.jsx index feab213b..87db828e 100644 --- a/src/browser/components/LoginModal.jsx +++ b/src/browser/components/LoginModal.jsx @@ -22,7 +22,7 @@ export default class LoginModal extends React.Component { } render() { - var theServer = ''; + let theServer = ''; if (!this.props.show) { theServer = ''; } else if (this.props.authInfo.isProxy) { diff --git a/src/browser/components/MainPage.jsx b/src/browser/components/MainPage.jsx index f80695c0..e4b8bed9 100644 --- a/src/browser/components/MainPage.jsx +++ b/src/browser/components/MainPage.jsx @@ -38,7 +38,7 @@ const MainPage = createReactClass({ getInitialState() { let key = this.props.initialIndex; if (this.props.deeplinkingUrl !== null) { - for (var i = 0; i < this.props.teams.length; i++) { + for (let i = 0; i < this.props.teams.length; i++) { if (this.props.deeplinkingUrl.includes(this.props.teams[i].url)) { key = i; break; @@ -57,7 +57,7 @@ const MainPage = createReactClass({ }; }, componentDidMount() { - var self = this; + const self = this; ipcRenderer.on('login-request', (event, request, authInfo) => { self.setState({ loginRequired: true, @@ -96,7 +96,7 @@ const MainPage = createReactClass({ self.refs[`mattermostView${self.state.key}`].focusOnWebView(); } - var currentWindow = remote.getCurrentWindow(); + const currentWindow = remote.getCurrentWindow(); currentWindow.on('focus', focusListener); window.addEventListener('beforeunload', () => { currentWindow.removeListener('focus', focusListener); @@ -132,7 +132,7 @@ const MainPage = createReactClass({ ipcRenderer.on('protocol-deeplink', (event, deepLinkUrl) => { const lastUrlDomain = Utils.getDomain(deepLinkUrl); - for (var i = 0; i < this.props.teams.length; i++) { + for (let i = 0; i < this.props.teams.length; i++) { if (lastUrlDomain === Utils.getDomain(self.refs[`mattermostView${i}`].getSrc())) { if (this.state.key !== i) { this.handleSelect(i); @@ -158,7 +158,7 @@ const MainPage = createReactClass({ key: newKey, finderVisible: false, }); - var webview = document.getElementById('mattermostView' + newKey); + const webview = document.getElementById('mattermostView' + newKey); ipcRenderer.send('update-title', { title: webview.getTitle(), }); @@ -166,10 +166,10 @@ const MainPage = createReactClass({ }, handleUnreadCountChange(index, unreadCount, mentionCount, isUnread, isMentioned) { - var unreadCounts = this.state.unreadCounts; - var mentionCounts = this.state.mentionCounts; - var unreadAtActive = this.state.unreadAtActive; - var mentionAtActiveCounts = this.state.mentionAtActiveCounts; + const unreadCounts = this.state.unreadCounts; + const mentionCounts = this.state.mentionCounts; + const unreadAtActive = this.state.unreadAtActive; + const mentionAtActiveCounts = this.state.mentionAtActiveCounts; unreadCounts[index] = unreadCount; mentionCounts[index] = mentionCount; @@ -189,8 +189,8 @@ const MainPage = createReactClass({ this.handleUnreadCountTotalChange(); }, markReadAtActive(index) { - var unreadAtActive = this.state.unreadAtActive; - var mentionAtActiveCounts = this.state.mentionAtActiveCounts; + const unreadAtActive = this.state.unreadAtActive; + const mentionAtActiveCounts = this.state.mentionAtActiveCounts; unreadAtActive[index] = false; mentionAtActiveCounts[index] = 0; this.setState({ @@ -201,7 +201,7 @@ const MainPage = createReactClass({ }, handleUnreadCountTotalChange() { if (this.props.onUnreadCountChange) { - var allUnreadCount = this.state.unreadCounts.reduce((prev, curr) => { + let allUnreadCount = this.state.unreadCounts.reduce((prev, curr) => { return prev + curr; }, 0); this.state.unreadAtActive.forEach((state) => { @@ -209,7 +209,7 @@ const MainPage = createReactClass({ allUnreadCount += 1; } }); - var allMentionCount = this.state.mentionCounts.reduce((prev, curr) => { + let allMentionCount = this.state.mentionCounts.reduce((prev, curr) => { return prev + curr; }, 0); this.state.mentionAtActiveCounts.forEach((count) => { @@ -277,8 +277,8 @@ const MainPage = createReactClass({ }, render() { - var self = this; - var tabsRow; + const self = this; + let tabsRow; if (this.props.teams.length > 1) { tabsRow = ( @@ -300,15 +300,15 @@ const MainPage = createReactClass({ ); } - var views = this.props.teams.map((team, index) => { + const views = this.props.teams.map((team, index) => { function handleUnreadCountChange(unreadCount, mentionCount, isUnread, isMentioned) { self.handleUnreadCountChange(index, unreadCount, mentionCount, isUnread, isMentioned); } function handleNotificationClick() { self.handleSelect(index); } - var id = 'mattermostView' + index; - var isActive = self.state.key === index; + const id = 'mattermostView' + index; + const isActive = self.state.key === index; let teamUrl = team.url; const deeplinkingUrl = this.props.deeplinkingUrl; @@ -332,21 +332,21 @@ const MainPage = createReactClass({ active={isActive} />); }); - var viewsRow = ( + const viewsRow = ( {views} ); - var request = null; - var authServerURL = null; - var authInfo = null; + let request = null; + let authServerURL = null; + let authInfo = null; if (this.state.loginQueue.length !== 0) { request = this.state.loginQueue[0].request; const tmpURL = url.parse(this.state.loginQueue[0].request.url); authServerURL = `${tmpURL.protocol}//${tmpURL.host}`; authInfo = this.state.loginQueue[0].authInfo; } - var modal = ( + const modal = ( { diff --git a/src/browser/components/MattermostView.jsx b/src/browser/components/MattermostView.jsx index 133d8ea7..ad4d5b2a 100644 --- a/src/browser/components/MattermostView.jsx +++ b/src/browser/components/MattermostView.jsx @@ -72,8 +72,8 @@ const MattermostView = createReactClass({ }, componentDidMount() { - var self = this; - var webview = findDOMNode(this.refs.webview); + const self = this; + const webview = findDOMNode(this.refs.webview); webview.addEventListener('did-fail-load', (e) => { console.log(self.props.name, 'webview did-fail-load', e); @@ -105,8 +105,8 @@ const MattermostView = createReactClass({ // Open link in browserWindow. for exmaple, attached files. webview.addEventListener('new-window', (e) => { - var currentURL = url.parse(webview.getURL()); - var destURL = url.parse(e.url); + const currentURL = url.parse(webview.getURL()); + const destURL = url.parse(e.url); if (destURL.protocol !== 'http:' && destURL.protocol !== 'https:' && destURL.protocol !== `${scheme}:`) { ipcRenderer.send('confirm-protocol', destURL.protocol, e.url); return; @@ -157,15 +157,17 @@ const MattermostView = createReactClass({ isLoaded: true, }); break; - case 'onUnreadCountChange': - var unreadCount = event.args[0]; - var mentionCount = event.args[1]; + case 'onUnreadCountChange': { + const unreadCount = event.args[0]; + const mentionCount = event.args[1]; // isUnread and isMentioned is pulse flag. - var isUnread = event.args[2]; - var isMentioned = event.args[3]; + const isUnread = event.args[2]; + const isMentioned = event.args[3]; self.handleUnreadCountChange(unreadCount, mentionCount, isUnread, isMentioned); + break; + } case 'onNotificationClick': self.props.onNotificationClick(); break; @@ -216,7 +218,7 @@ const MattermostView = createReactClass({ reloadTimeoutID: null, isLoaded: false, }); - var webview = findDOMNode(this.refs.webview); + const webview = findDOMNode(this.refs.webview); webview.reload(); }, @@ -224,7 +226,7 @@ const MattermostView = createReactClass({ this.setState({ errorInfo: null, }); - var webContents = findDOMNode(this.refs.webview).getWebContents(); + const webContents = findDOMNode(this.refs.webview).getWebContents(); webContents.session.clearCache(() => { webContents.reload(); }); diff --git a/src/browser/components/SettingsPage.jsx b/src/browser/components/SettingsPage.jsx index c4563012..21f3b64c 100644 --- a/src/browser/components/SettingsPage.jsx +++ b/src/browser/components/SettingsPage.jsx @@ -38,7 +38,7 @@ const SettingsPage = createReactClass({ }, getInitialState() { - var initialState; + let initialState; try { initialState = settings.readFileSync(this.props.configFile); } catch (e) { @@ -59,7 +59,7 @@ const SettingsPage = createReactClass({ }, componentDidMount() { if (process.platform === 'win32' || process.platform === 'linux') { - var self = this; + const self = this; appLauncher.isEnabled().then((enabled) => { self.setState({ autostart: enabled, @@ -125,7 +125,7 @@ const SettingsPage = createReactClass({ }, saveConfig(callback) { - var config = { + const config = { teams: this.state.teams, showTrayIcon: this.state.showTrayIcon, trayIconTheme: this.state.trayIconTheme, @@ -178,7 +178,7 @@ const SettingsPage = createReactClass({ backToIndex(); }, handleChangeShowTrayIcon() { - var shouldShowTrayIcon = !this.refs.showTrayIcon.props.checked; + const shouldShowTrayIcon = !this.refs.showTrayIcon.props.checked; this.setState({ showTrayIcon: shouldShowTrayIcon, }); @@ -271,7 +271,7 @@ const SettingsPage = createReactClass({ }, updateTeam(index, newData) { - var teams = this.state.teams; + const teams = this.state.teams; teams[index] = newData; this.setState({ teams, @@ -280,7 +280,7 @@ const SettingsPage = createReactClass({ }, addServer(team) { - var teams = this.state.teams; + const teams = this.state.teams; teams.push(team); this.setState({ teams, @@ -324,7 +324,7 @@ const SettingsPage = createReactClass({ }, }; - var teamsRow = ( + const teamsRow = ( ); - var serversRow = ( + const serversRow = ( ); - var srvMgmt; + let srvMgmt; if (this.props.enableServerManagement === true) { srvMgmt = (
@@ -386,7 +386,7 @@ const SettingsPage = createReactClass({ ); } - var options = []; + const options = []; // MacOS has an option in the Dock, to set the app to autostart, so we choose to not support this option for OSX if (process.platform === 'win32' || process.platform === 'linux') { @@ -576,7 +576,7 @@ const SettingsPage = createReactClass({ ); - var optionsRow = (options.length > 0) ? ( + const optionsRow = (options.length > 0) ? (

{'App Options'}

diff --git a/src/browser/components/TeamList.jsx b/src/browser/components/TeamList.jsx index ff21df7b..cc7222a6 100644 --- a/src/browser/components/TeamList.jsx +++ b/src/browser/components/TeamList.jsx @@ -35,12 +35,12 @@ const TeamList = createReactClass({ }, handleTeamRemove(index) { console.log(index); - var teams = this.props.teams; + const teams = this.props.teams; teams.splice(index, 1); this.props.onTeamsChange(teams); }, handleTeamAdd(team) { - var teams = this.props.teams; + const teams = this.props.teams; // check if team already exists and then change existing team or add new one if ((typeof team.index !== 'undefined') && teams[team.index]) { @@ -81,8 +81,8 @@ const TeamList = createReactClass({ }, render() { - var self = this; - var teamNodes = this.props.teams.map((team, i) => { + const self = this; + const teamNodes = this.props.teams.map((team, i) => { function handleTeamRemove() { document.activeElement.blur(); self.openServerRemoveModal(i); @@ -110,7 +110,7 @@ const TeamList = createReactClass({ ); }); - var addServerForm = ( + const addServerForm = ( { - var teamData = { + const teamData = { name: newTeam.name, url: newTeam.url, }; diff --git a/src/browser/webview/mattermost.js b/src/browser/webview/mattermost.js index 77d1dbb7..a1304850 100644 --- a/src/browser/webview/mattermost.js +++ b/src/browser/webview/mattermost.js @@ -51,7 +51,7 @@ window.addEventListener('load', () => { }); function hasClass(element, className) { - var rclass = /[\t\r\n\f]/g; + const rclass = /[\t\r\n\f]/g; if ((' ' + element.className + ' ').replace(rclass, ' ').indexOf(className) > -1) { return true; } @@ -77,7 +77,7 @@ function getUnreadCount() { // unreadCount in sidebar // Note: the active channel doesn't have '.unread-title'. - var unreadCount = document.getElementsByClassName('unread-title').length; + let unreadCount = document.getElementsByClassName('unread-title').length; // unreadCount in team sidebar const teamSideBar = document.getElementsByClassName('team-sidebar'); // team-sidebar doesn't have id @@ -86,30 +86,30 @@ function getUnreadCount() { } // mentionCount in sidebar - var elem = document.getElementsByClassName('badge'); - var mentionCount = 0; - for (var i = 0; i < elem.length; i++) { + const elem = document.getElementsByClassName('badge'); + let mentionCount = 0; + for (let i = 0; i < elem.length; i++) { if (isElementVisible(elem[i]) && !hasClass(elem[i], 'badge-notify')) { mentionCount += Number(elem[i].innerHTML); } } - var postAttrName = 'data-reactid'; - var lastPostElem = document.querySelector('div[' + postAttrName + '="' + this.lastCheckedPostId + '"]'); - var isUnread = false; - var isMentioned = false; + const postAttrName = 'data-reactid'; + const lastPostElem = document.querySelector('div[' + postAttrName + '="' + this.lastCheckedPostId + '"]'); + let isUnread = false; + let isMentioned = false; if (lastPostElem === null || !isElementVisible(lastPostElem)) { // When load channel or change channel, this.lastCheckedPostId is invalid. // So we get latest post and save lastCheckedPostId. // find active post-list. - var postLists = document.querySelectorAll('div.post-list__content'); + const postLists = document.querySelectorAll('div.post-list__content'); if (postLists.length === 0) { setTimeout(getUnreadCount, UNREAD_COUNT_INTERVAL); return; } - var post = null; - for (var j = 0; j < postLists.length; j++) { + let post = null; + for (let j = 0; j < postLists.length; j++) { if (isElementVisible(postLists[j])) { post = postLists[j].children[0]; } @@ -130,19 +130,19 @@ function getUnreadCount() { post = post.nextSibling; } } else if (lastPostElem !== null) { - var newPostElem = lastPostElem.nextSibling; + let newPostElem = lastPostElem.nextSibling; while (newPostElem) { this.lastCheckedPostId = newPostElem.getAttribute(postAttrName); isUnread = true; - var activeChannel = document.querySelector('.active .sidebar-channel'); - var closeButton = activeChannel.getElementsByClassName('btn-close'); + const activeChannel = document.querySelector('.active .sidebar-channel'); + const closeButton = activeChannel.getElementsByClassName('btn-close'); if (closeButton.length === 1 && closeButton[0].getAttribute('aria-describedby') === 'remove-dm-tooltip') { // If active channel is DM, all posts is treated as menion. isMentioned = true; break; } else { // If active channel is public/private channel, only mentioned post is treated as mention. - var highlight = newPostElem.getElementsByClassName('mention-highlight'); + const highlight = newPostElem.getElementsByClassName('mention-highlight'); if (highlight.length !== 0 && isElementVisible(highlight[0])) { isMentioned = true; break; diff --git a/src/common/config/upgradePreferences.js b/src/common/config/upgradePreferences.js index 3e7b70fe..ca9668aa 100644 --- a/src/common/config/upgradePreferences.js +++ b/src/common/config/upgradePreferences.js @@ -20,7 +20,7 @@ function upgradeV0toV1(configV0) { } export default function upgradeToLatest(config) { - var configVersion = config.version ? config.version : 0; + const configVersion = config.version ? config.version : 0; switch (configVersion) { case 0: return upgradeToLatest(upgradeV0toV1(config)); diff --git a/src/common/osVersion.js b/src/common/osVersion.js index 978cf64e..1347d758 100644 --- a/src/common/osVersion.js +++ b/src/common/osVersion.js @@ -4,7 +4,7 @@ 'use strict'; import os from 'os'; -var releaseSplit = os.release().split('.'); +const releaseSplit = os.release().split('.'); export default { major: parseInt(releaseSplit[0], 10), diff --git a/src/common/settings.js b/src/common/settings.js index e73dae90..0327c61c 100644 --- a/src/common/settings.js +++ b/src/common/settings.js @@ -45,7 +45,7 @@ export default { if (config.version !== defaultPreferences.version) { throw new Error('version ' + config.version + ' is not equal to ' + defaultPreferences.version); } - var data = JSON.stringify(config, null, ' '); + const data = JSON.stringify(config, null, ' '); fs.writeFile(configFile, data, 'utf8', callback); }, @@ -59,7 +59,7 @@ export default { fs.mkdirSync(dir); } - var data = JSON.stringify(config, null, ' '); + const data = JSON.stringify(config, null, ' '); fs.writeFileSync(configFile, data, 'utf8'); }, diff --git a/src/main.js b/src/main.js index da7df68c..a826b3e3 100644 --- a/src/main.js +++ b/src/main.js @@ -58,7 +58,7 @@ const assetsDir = path.resolve(app.getAppPath(), 'assets'); // Keep a global reference of the window object, if you don't, the window will // be closed automatically when the JavaScript object is garbage collected. -var mainWindow = null; +let mainWindow = null; let spellChecker = null; let deeplinkingUrl = null; let scheme = null; @@ -74,7 +74,7 @@ if (argv['data-dir']) { global.isDev = isDev && !argv.disableDevMode; -var config = {}; +let config = {}; try { const configFile = app.getPath('userData') + '/config.json'; config = settings.readFileSync(configFile); @@ -117,7 +117,7 @@ function switchMenuIconImages(icons, isDarkMode) { } } -var trayIcon = null; +let trayIcon = null; const trayImages = (() => { switch (process.platform) { case 'win32': @@ -293,7 +293,7 @@ app.on('certificate-error', (event, webContents, url, error, certificate, callba event.preventDefault(); callback(true); } else { - var detail = `URL: ${url}\nError: ${error}`; + let detail = `URL: ${url}\nError: ${error}`; if (certificateStore.isExisting(url)) { detail = 'Certificate is different from previous one.\n\n' + detail; } @@ -543,8 +543,8 @@ app.on('ready', () => { if (process.platform === 'darwin') { session.defaultSession.on('will-download', (event, item) => { - var filename = item.getFilename(); - var savePath = dialog.showSaveDialog({ + const filename = item.getFilename(); + const savePath = dialog.showSaveDialog({ title: filename, defaultPath: os.homedir() + '/Downloads/' + filename, }); @@ -559,7 +559,7 @@ app.on('ready', () => { // Set application menu ipcMain.on('update-menu', (event, configData) => { - var aMenu = appMenu.createMenu(mainWindow, configData, global.isDev); + const aMenu = appMenu.createMenu(mainWindow, configData, global.isDev); Menu.setApplicationMenu(aMenu); // set up context menu for tray icon diff --git a/src/main/allowProtocolDialog.js b/src/main/allowProtocolDialog.js index 18308d9b..43aea5d6 100644 --- a/src/main/allowProtocolDialog.js +++ b/src/main/allowProtocolDialog.js @@ -9,7 +9,7 @@ import fs from 'fs'; import {app, dialog, ipcMain, shell} from 'electron'; const allowedProtocolFile = path.resolve(app.getPath('userData'), 'allowedProtocols.json'); -var allowedProtocols = []; +let allowedProtocols = []; function init(mainWindow) { fs.readFile(allowedProtocolFile, 'utf-8', (err, data) => { diff --git a/src/main/certificateStore.js b/src/main/certificateStore.js index 39119278..051f35ad 100644 --- a/src/main/certificateStore.js +++ b/src/main/certificateStore.js @@ -56,7 +56,7 @@ CertificateStore.prototype.isExisting = function isExisting(targetURL) { }; CertificateStore.prototype.isTrusted = function isTrusted(targetURL, certificate) { - var host = getHost(targetURL); + const host = getHost(targetURL); if (!this.isExisting(targetURL)) { return false; } diff --git a/src/main/mainWindow.js b/src/main/mainWindow.js index 2b260789..47adb886 100644 --- a/src/main/mainWindow.js +++ b/src/main/mainWindow.js @@ -7,7 +7,7 @@ import path from 'path'; import {app, BrowserWindow} from 'electron'; function saveWindowState(file, window) { - var windowState = window.getBounds(); + const windowState = window.getBounds(); windowState.maximized = window.isMaximized(); windowState.fullscreen = window.isFullScreen(); try { @@ -26,7 +26,7 @@ function createMainWindow(config, options) { // Create the browser window. const boundsInfoPath = path.join(app.getPath('userData'), 'bounds-info.json'); - var windowOptions; + let windowOptions; try { windowOptions = JSON.parse(fs.readFileSync(boundsInfoPath, 'utf-8')); } catch (e) { diff --git a/src/main/menus/app.js b/src/main/menus/app.js index eec6037e..f0c9c9f4 100644 --- a/src/main/menus/app.js +++ b/src/main/menus/app.js @@ -15,11 +15,11 @@ function createTemplate(mainWindow, config, isDev) { type: 'separator', }; - var appName = app.getName(); - var firstMenuName = (process.platform === 'darwin') ? appName : 'File'; - var template = []; + const appName = app.getName(); + const firstMenuName = (process.platform === 'darwin') ? appName : 'File'; + const template = []; - var platformAppMenu = process.platform === 'darwin' ? [{ + let platformAppMenu = process.platform === 'darwin' ? [{ label: 'About ' + appName, role: 'about', click() { @@ -217,7 +217,7 @@ function createTemplate(mainWindow, config, isDev) { }], }; template.push(windowMenu); - var submenu = []; + const submenu = []; if (buildConfig.helpLink) { submenu.push({ label: 'Learn More...', diff --git a/src/main/menus/tray.js b/src/main/menus/tray.js index 55bcb788..17ff126d 100644 --- a/src/main/menus/tray.js +++ b/src/main/menus/tray.js @@ -10,7 +10,7 @@ import settings from '../../common/settings'; function createTemplate(mainWindow, config, isDev) { const settingsURL = isDev ? 'http://localhost:8080/browser/settings.html' : `file://${app.getAppPath()}/browser/settings.html`; const teams = settings.mergeDefaultTeams(config.teams); - var template = [ + const template = [ ...teams.slice(0, 9).map((team, i) => { return { label: team.name, From 23ca8bbe5a678668354e082bb23ae57d2094ad73 Mon Sep 17 00:00:00 2001 From: Yuya Ochiai Date: Mon, 3 Sep 2018 22:33:34 +0900 Subject: [PATCH 10/18] Use ES6 class for React components --- .eslintrc.json | 1 - NOTICE.txt | 2 +- src/browser/components/MainPage.jsx | 95 ++++++++++++-------- src/browser/components/MattermostView.jsx | 70 ++++++++------- src/browser/components/SettingsPage.jsx | 100 ++++++++++++++-------- src/browser/components/TeamList.jsx | 54 +++++++----- src/package.json | 1 - src/yarn.lock | 8 -- 8 files changed, 198 insertions(+), 133 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index c929e87b..a1d707ab 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -21,7 +21,6 @@ "react/jsx-indent": [2, 2], "react/jsx-indent-props": [2, 2], "react/no-set-state": 1, - "react/prefer-es6-class": 1, "react/require-optimization": 0 } } diff --git a/NOTICE.txt b/NOTICE.txt index 98d476f5..edfba9f6 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -428,7 +428,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ## react This product contains a modified portion of 'react', a declarative, efficient, and flexible JavaScript library for building user interfaces, -'create-react-class', a drop-in replacement for React.createClass, and 'react-dom', the entry point of the DOM-related +and 'react-dom', the entry point of the DOM-related rendering paths, by Facebook, Inc. * HOMEPAGE: diff --git a/src/browser/components/MainPage.jsx b/src/browser/components/MainPage.jsx index e4b8bed9..3899dcb2 100644 --- a/src/browser/components/MainPage.jsx +++ b/src/browser/components/MainPage.jsx @@ -1,11 +1,13 @@ // Copyright (c) 2015-2016 Yuya Ochiai // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. + +/* eslint-disable react/no-set-state */ + import url from 'url'; import React from 'react'; import PropTypes from 'prop-types'; -import createReactClass from 'create-react-class'; import {CSSTransition, TransitionGroup} from 'react-transition-group'; import {Grid, Row} from 'react-bootstrap'; @@ -21,21 +23,10 @@ import PermissionRequestDialog from './PermissionRequestDialog.jsx'; import Finder from './Finder.jsx'; import NewTeamModal from './NewTeamModal.jsx'; -const MainPage = createReactClass({ - propTypes: { - onUnreadCountChange: PropTypes.func.isRequired, - teams: PropTypes.array.isRequired, - onTeamConfigChange: PropTypes.func.isRequired, - initialIndex: PropTypes.number.isRequired, - useSpellChecker: PropTypes.bool.isRequired, - onSelectSpellCheckerLocale: PropTypes.func.isRequired, - deeplinkingUrl: PropTypes.string, - showAddServerButton: PropTypes.bool.isRequired, - requestingPermission: TabBar.propTypes.requestingPermission, - onClickPermissionDialog: PropTypes.func, - }, +export default class MainPage extends React.Component { + constructor(props) { + super(props); - getInitialState() { let key = this.props.initialIndex; if (this.props.deeplinkingUrl !== null) { for (let i = 0; i < this.props.teams.length; i++) { @@ -46,7 +37,7 @@ const MainPage = createReactClass({ } } - return { + this.state = { key, unreadCounts: new Array(this.props.teams.length), mentionCounts: new Array(this.props.teams.length), @@ -55,7 +46,22 @@ const MainPage = createReactClass({ loginQueue: [], targetURL: '', }; - }, + + this.activateFinder = this.activateFinder.bind(this); + this.addServer = this.addServer.bind(this); + this.closeFinder = this.closeFinder.bind(this); + this.focusOnWebView = this.focusOnWebView.bind(this); + this.handleLogin = this.handleLogin.bind(this); + this.handleLoginCancel = this.handleLoginCancel.bind(this); + this.handleOnTeamFocused = this.handleOnTeamFocused.bind(this); + this.handleSelect = this.handleSelect.bind(this); + this.handleTargetURLChange = this.handleTargetURLChange.bind(this); + this.handleUnreadCountChange = this.handleUnreadCountChange.bind(this); + this.handleUnreadCountTotalChange = this.handleUnreadCountTotalChange.bind(this); + this.inputBlur = this.inputBlur.bind(this); + this.markReadAtActive = this.markReadAtActive.bind(this); + } + componentDidMount() { const self = this; ipcRenderer.on('login-request', (event, request, authInfo) => { @@ -146,12 +152,14 @@ const MainPage = createReactClass({ ipcRenderer.on('toggle-find', () => { this.activateFinder(true); }); - }, + } + componentDidUpdate(prevProps, prevState) { if (prevState.key !== this.state.key) { // i.e. When tab has been changed this.refs[`mattermostView${this.state.key}`].focusOnWebView(); } - }, + } + handleSelect(key) { const newKey = (this.props.teams.length + key) % this.props.teams.length; this.setState({ @@ -163,7 +171,7 @@ const MainPage = createReactClass({ title: webview.getTitle(), }); this.handleOnTeamFocused(newKey); - }, + } handleUnreadCountChange(index, unreadCount, mentionCount, isUnread, isMentioned) { const unreadCounts = this.state.unreadCounts; @@ -187,7 +195,8 @@ const MainPage = createReactClass({ mentionAtActiveCounts, }); this.handleUnreadCountTotalChange(); - }, + } + markReadAtActive(index) { const unreadAtActive = this.state.unreadAtActive; const mentionAtActiveCounts = this.state.mentionAtActiveCounts; @@ -198,7 +207,8 @@ const MainPage = createReactClass({ mentionAtActiveCounts, }); this.handleUnreadCountTotalChange(); - }, + } + handleUnreadCountTotalChange() { if (this.props.onUnreadCountChange) { let allUnreadCount = this.state.unreadCounts.reduce((prev, curr) => { @@ -217,23 +227,26 @@ const MainPage = createReactClass({ }); this.props.onUnreadCountChange(allUnreadCount, allMentionCount); } - }, + } + handleOnTeamFocused(index) { // Turn off the flag to indicate whether unread message of active channel contains at current tab. this.markReadAtActive(index); - }, + } handleLogin(request, username, password) { ipcRenderer.send('login-credentials', request, username, password); const loginQueue = this.state.loginQueue; loginQueue.shift(); this.setState({loginQueue}); - }, + } + handleLoginCancel() { const loginQueue = this.state.loginQueue; loginQueue.shift(); this.setState({loginQueue}); - }, + } + handleTargetURLChange(targetURL) { clearTimeout(this.targetURLDisappearTimeout); if (targetURL === '') { @@ -244,37 +257,38 @@ const MainPage = createReactClass({ } else { this.setState({targetURL}); } - }, + } + addServer() { this.setState({ showNewTeamModal: true, }); - }, + } focusOnWebView(e) { if (e.target.className !== 'finder-input') { this.refs[`mattermostView${this.state.key}`].focusOnWebView(); } - }, + } activateFinder() { this.setState({ finderVisible: true, focusFinder: true, }); - }, + } closeFinder() { this.setState({ finderVisible: false, }); - }, + } inputBlur() { this.setState({ focusFinder: false, }); - }, + } render() { const self = this; @@ -419,7 +433,18 @@ const MainPage = createReactClass({
); - }, -}); + } +} -export default MainPage; +MainPage.propTypes = { + onUnreadCountChange: PropTypes.func.isRequired, + teams: PropTypes.array.isRequired, + onTeamConfigChange: PropTypes.func.isRequired, + initialIndex: PropTypes.number.isRequired, + useSpellChecker: PropTypes.bool.isRequired, + onSelectSpellCheckerLocale: PropTypes.func.isRequired, + deeplinkingUrl: PropTypes.string, + showAddServerButton: PropTypes.bool.isRequired, + requestingPermission: TabBar.propTypes.requestingPermission, + onClickPermissionDialog: PropTypes.func, +}; diff --git a/src/browser/components/MattermostView.jsx b/src/browser/components/MattermostView.jsx index ad4d5b2a..03c0e27c 100644 --- a/src/browser/components/MattermostView.jsx +++ b/src/browser/components/MattermostView.jsx @@ -9,7 +9,6 @@ import url from 'url'; import React from 'react'; import PropTypes from 'prop-types'; -import createReactClass from 'create-react-class'; import {findDOMNode} from 'react-dom'; import {ipcRenderer, remote, shell} from 'electron'; @@ -43,33 +42,34 @@ function isNetworkDrive(fileURL) { return false; } -const MattermostView = createReactClass({ - propTypes: { - name: PropTypes.string, - id: PropTypes.string, - onTargetURLChange: PropTypes.func, - onUnreadCountChange: PropTypes.func, - src: PropTypes.string, - active: PropTypes.bool, - withTab: PropTypes.bool, - useSpellChecker: PropTypes.bool, - onSelectSpellCheckerLocale: PropTypes.func, - }, +export default class MattermostView extends React.Component { + constructor(props) { + super(props); - getInitialState() { - return { + this.state = { errorInfo: null, isContextMenuAdded: false, reloadTimeoutID: null, isLoaded: false, }; - }, + + this.handleUnreadCountChange = this.handleUnreadCountChange.bind(this); + this.reload = this.reload.bind(this); + this.clearCacheAndReload = this.clearCacheAndReload.bind(this); + this.focusOnWebView = this.focusOnWebView.bind(this); + this.canGoBack = this.canGoBack.bind(this); + this.canGoForward = this.canGoForward.bind(this); + this.goBack = this.goBack.bind(this); + this.goForward = this.goForward.bind(this); + this.getSrc = this.getSrc.bind(this); + this.handleDeepLink = this.handleDeepLink.bind(this); + } handleUnreadCountChange(unreadCount, mentionCount, isUnread, isMentioned) { if (this.props.onUnreadCountChange) { this.props.onUnreadCountChange(unreadCount, mentionCount, isUnread, isMentioned); } - }, + } componentDidMount() { const self = this; @@ -209,7 +209,7 @@ const MattermostView = createReactClass({ break; } }); - }, + } reload() { clearTimeout(this.state.reloadTimeoutID); @@ -220,7 +220,7 @@ const MattermostView = createReactClass({ }); const webview = findDOMNode(this.refs.webview); webview.reload(); - }, + } clearCacheAndReload() { this.setState({ @@ -230,7 +230,7 @@ const MattermostView = createReactClass({ webContents.session.clearCache(() => { webContents.reload(); }); - }, + } focusOnWebView() { const webview = findDOMNode(this.refs.webview); @@ -239,32 +239,32 @@ const MattermostView = createReactClass({ webview.focus(); webContents.focus(); } - }, + } canGoBack() { const webview = findDOMNode(this.refs.webview); return webview.getWebContents().canGoBack(); - }, + } canGoForward() { const webview = findDOMNode(this.refs.webview); return webview.getWebContents().canGoForward(); - }, + } goBack() { const webview = findDOMNode(this.refs.webview); webview.getWebContents().goBack(); - }, + } goForward() { const webview = findDOMNode(this.refs.webview); webview.getWebContents().goForward(); - }, + } getSrc() { const webview = findDOMNode(this.refs.webview); return webview.src; - }, + } handleDeepLink(relativeUrl) { const webview = findDOMNode(this.refs.webview); @@ -274,7 +274,7 @@ const MattermostView = createReactClass({ webview.executeJavaScript( 'dispatchEvent(new PopStateEvent("popstate", null));' ); - }, + } render() { const errorView = this.state.errorInfo ? ( @@ -318,7 +318,17 @@ const MattermostView = createReactClass({ /> { loadingImage } ); - }, -}); + } +} -export default MattermostView; +MattermostView.propTypes = { + name: PropTypes.string, + id: PropTypes.string, + onTargetURLChange: PropTypes.func, + onUnreadCountChange: PropTypes.func, + src: PropTypes.string, + active: PropTypes.bool, + withTab: PropTypes.bool, + useSpellChecker: PropTypes.bool, + onSelectSpellCheckerLocale: PropTypes.func, +}; diff --git a/src/browser/components/SettingsPage.jsx b/src/browser/components/SettingsPage.jsx index 21f3b64c..ba2c75eb 100644 --- a/src/browser/components/SettingsPage.jsx +++ b/src/browser/components/SettingsPage.jsx @@ -1,9 +1,11 @@ // Copyright (c) 2015-2016 Yuya Ochiai // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. + +/* eslint-disable react/no-set-state */ + import React from 'react'; import PropTypes from 'prop-types'; -import createReactClass from 'create-react-class'; import ReactDOM from 'react-dom'; import {Button, Checkbox, Col, FormGroup, Grid, HelpBlock, Navbar, Radio, Row} from 'react-bootstrap'; @@ -31,20 +33,16 @@ function backToIndex(index) { const CONFIG_TYPE_SERVERS = 'servers'; const CONFIG_TYPE_APP_OPTIONS = 'appOptions'; -const SettingsPage = createReactClass({ - propTypes: { - configFile: PropTypes.string, - enableServerManagement: PropTypes.bool, - }, +export default class SettingsPage extends React.Component { + constructor(props) { + super(props); - getInitialState() { let initialState; try { initialState = settings.readFileSync(this.props.configFile); } catch (e) { initialState = settings.loadDefault(); } - initialState.showAddTeamForm = false; initialState.trayWasVisible = remote.getCurrentWindow().trayWasVisible; if (initialState.teams.length === 0) { @@ -54,9 +52,30 @@ const SettingsPage = createReactClass({ appOptions: AutoSaveIndicator.SAVING_STATE_DONE, servers: AutoSaveIndicator.SAVING_STATE_DONE, }; + this.state = initialState; + + this.startSaveConfig = this.startSaveConfig.bind(this); + this.didSaveConfig = this.didSaveConfig.bind(this); + this.handleTeamsChange = this.handleTeamsChange.bind(this); + this.saveConfig = this.saveConfig.bind(this); + this.saveAutoStart = this.saveAutoStart.bind(this); + this.handleCancel = this.handleCancel.bind(this); + this.handleChangeShowTrayIcon = this.handleChangeShowTrayIcon.bind(this); + this.handleChangeTrayIconTheme = this.handleChangeTrayIconTheme.bind(this); + this.handleChangeAutoStart = this.handleChangeAutoStart.bind(this); + this.handleChangeMinimizeToTray = this.handleChangeMinimizeToTray.bind(this); + this.toggleShowTeamForm = this.toggleShowTeamForm.bind(this); + this.setShowTeamFormVisibility = this.setShowTeamFormVisibility.bind(this); + this.handleFlashWindow = this.handleFlashWindow.bind(this); + this.handleBounceIcon = this.handleBounceIcon.bind(this); + this.handleBounceIconType = this.handleBounceIconType.bind(this); + this.handleShowUnreadBadge = this.handleShowUnreadBadge.bind(this); + this.handleChangeUseSpellChecker = this.handleChangeUseSpellChecker.bind(this); + this.handleChangeEnableHardwareAcceleration = this.handleChangeEnableHardwareAcceleration.bind(this); + this.updateTeam = this.updateTeam.bind(this); + this.addServer = this.addServer.bind(this); + } - return initialState; - }, componentDidMount() { if (process.platform === 'win32' || process.platform === 'linux') { const self = this; @@ -74,7 +93,7 @@ const SettingsPage = createReactClass({ ipcRenderer.on('switch-tab', (event, key) => { backToIndex(key); }); - }, + } startSaveConfig(configType) { if (!this.startSaveConfigImpl) { @@ -97,7 +116,7 @@ const SettingsPage = createReactClass({ savingState[configType] = AutoSaveIndicator.SAVING_STATE_SAVING; this.setState({savingState}); this.startSaveConfigImpl[configType](); - }, + } didSaveConfig(configType) { if (!this.didSaveConfigImpl) { @@ -111,7 +130,7 @@ const SettingsPage = createReactClass({ }, 2000); } this.didSaveConfigImpl[configType](); - }, + } handleTeamsChange(teams) { this.setState({ @@ -122,7 +141,7 @@ const SettingsPage = createReactClass({ this.setState({showAddTeamForm: true}); } setImmediate(this.startSaveConfig, CONFIG_TYPE_SERVERS); - }, + } saveConfig(callback) { const config = { @@ -156,7 +175,7 @@ const SettingsPage = createReactClass({ callback(); } }); - }, + } saveAutoStart(autostart, callback) { appLauncher.isEnabled().then((enabled) => { @@ -172,11 +191,12 @@ const SettingsPage = createReactClass({ callback(); } }).catch(callback); - }, + } handleCancel() { backToIndex(); - }, + } + handleChangeShowTrayIcon() { const shouldShowTrayIcon = !this.refs.showTrayIcon.props.checked; this.setState({ @@ -190,19 +210,22 @@ const SettingsPage = createReactClass({ } setImmediate(this.startSaveConfig, CONFIG_TYPE_APP_OPTIONS); - }, + } + handleChangeTrayIconTheme() { this.setState({ trayIconTheme: ReactDOM.findDOMNode(this.refs.trayIconTheme).value, }); setImmediate(this.startSaveConfig, CONFIG_TYPE_APP_OPTIONS); - }, + } + handleChangeAutoStart() { this.setState({ autostart: !this.refs.autostart.props.checked, }); setImmediate(this.startSaveConfig, CONFIG_TYPE_APP_OPTIONS); - }, + } + handleChangeMinimizeToTray() { const shouldMinimizeToTray = this.state.showTrayIcon && !this.refs.minimizeToTray.props.checked; @@ -210,18 +233,21 @@ const SettingsPage = createReactClass({ minimizeToTray: shouldMinimizeToTray, }); setImmediate(this.startSaveConfig, CONFIG_TYPE_APP_OPTIONS); - }, + } + toggleShowTeamForm() { this.setState({ showAddTeamForm: !this.state.showAddTeamForm, }); document.activeElement.blur(); - }, + } + setShowTeamFormVisibility(val) { this.setState({ showAddTeamForm: val, }); - }, + } + handleFlashWindow() { this.setState({ notifications: { @@ -230,7 +256,8 @@ const SettingsPage = createReactClass({ }, }); setImmediate(this.startSaveConfig, CONFIG_TYPE_APP_OPTIONS); - }, + } + handleBounceIcon() { this.setState({ notifications: { @@ -239,7 +266,8 @@ const SettingsPage = createReactClass({ }, }); setImmediate(this.startSaveConfig, CONFIG_TYPE_APP_OPTIONS); - }, + } + handleBounceIconType(event) { this.setState({ notifications: { @@ -248,27 +276,28 @@ const SettingsPage = createReactClass({ }, }); setImmediate(this.startSaveConfig, CONFIG_TYPE_APP_OPTIONS); - }, + } + handleShowUnreadBadge() { this.setState({ showUnreadBadge: !this.refs.showUnreadBadge.props.checked, }); setImmediate(this.startSaveConfig, CONFIG_TYPE_APP_OPTIONS); - }, + } handleChangeUseSpellChecker() { this.setState({ useSpellChecker: !this.refs.useSpellChecker.props.checked, }); setImmediate(this.startSaveConfig, CONFIG_TYPE_APP_OPTIONS); - }, + } handleChangeEnableHardwareAcceleration() { this.setState({ enableHardwareAcceleration: !this.refs.enableHardwareAcceleration.props.checked, }); setImmediate(this.startSaveConfig, CONFIG_TYPE_APP_OPTIONS); - }, + } updateTeam(index, newData) { const teams = this.state.teams; @@ -277,7 +306,7 @@ const SettingsPage = createReactClass({ teams, }); setImmediate(this.startSaveConfig, CONFIG_TYPE_SERVERS); - }, + } addServer(team) { const teams = this.state.teams; @@ -286,7 +315,7 @@ const SettingsPage = createReactClass({ teams, }); setImmediate(this.startSaveConfig, CONFIG_TYPE_SERVERS); - }, + } render() { const settingsPage = { @@ -625,7 +654,10 @@ const SettingsPage = createReactClass({ ); - }, -}); + } +} -export default SettingsPage; +SettingsPage.propTypes = { + configFile: PropTypes.string, + enableServerManagement: PropTypes.bool, +}; diff --git a/src/browser/components/TeamList.jsx b/src/browser/components/TeamList.jsx index cc7222a6..53daa34b 100644 --- a/src/browser/components/TeamList.jsx +++ b/src/browser/components/TeamList.jsx @@ -3,27 +3,17 @@ // See LICENSE.txt for license information. import React from 'react'; import PropTypes from 'prop-types'; -import createReactClass from 'create-react-class'; import {ListGroup} from 'react-bootstrap'; import TeamListItem from './TeamListItem.jsx'; import NewTeamModal from './NewTeamModal.jsx'; import RemoveServerModal from './RemoveServerModal.jsx'; -const TeamList = createReactClass({ - propTypes: { - onTeamsChange: PropTypes.func, - showAddTeamForm: PropTypes.bool, - teams: PropTypes.array, - addServer: PropTypes.func, - updateTeam: PropTypes.func, - toggleAddTeamForm: PropTypes.func, - setAddTeamFormVisibility: PropTypes.func, - onTeamClick: PropTypes.func, - }, +export default class TeamList extends React.Component { + constructor(props) { + super(props); - getInitialState() { - return { + this.state = { showEditTeamForm: false, indexToRemoveServer: -1, team: { @@ -32,13 +22,21 @@ const TeamList = createReactClass({ index: false, }, }; - }, + + this.handleTeamRemove = this.handleTeamRemove.bind(this); + this.handleTeamAdd = this.handleTeamAdd.bind(this); + this.handleTeamEditing = this.handleTeamEditing.bind(this); + this.openServerRemoveModal = this.openServerRemoveModal.bind(this); + this.closeServerRemoveModal = this.closeServerRemoveModal.bind(this); + } + handleTeamRemove(index) { console.log(index); const teams = this.props.teams; teams.splice(index, 1); this.props.onTeamsChange(teams); - }, + } + handleTeamAdd(team) { const teams = this.props.teams; @@ -60,7 +58,8 @@ const TeamList = createReactClass({ }); this.props.onTeamsChange(teams); - }, + } + handleTeamEditing(teamName, teamUrl, teamIndex) { this.setState({ showEditTeamForm: true, @@ -70,15 +69,15 @@ const TeamList = createReactClass({ index: teamIndex, }, }); - }, + } openServerRemoveModal(indexForServer) { this.setState({indexToRemoveServer: indexForServer}); - }, + } closeServerRemoveModal() { this.setState({indexToRemoveServer: -1}); - }, + } render() { const self = this; @@ -171,7 +170,16 @@ const TeamList = createReactClass({ { removeServerModal} ); - }, -}); + } +} -export default TeamList; +TeamList.propTypes = { + onTeamsChange: PropTypes.func, + showAddTeamForm: PropTypes.bool, + teams: PropTypes.array, + addServer: PropTypes.func, + updateTeam: PropTypes.func, + toggleAddTeamForm: PropTypes.func, + setAddTeamFormVisibility: PropTypes.func, + onTeamClick: PropTypes.func, +}; diff --git a/src/package.json b/src/package.json index 351fcf12..be1a7fc4 100644 --- a/src/package.json +++ b/src/package.json @@ -11,7 +11,6 @@ "dependencies": { "auto-launch": "^5.0.5", "bootstrap": "^3.3.7", - "create-react-class": "^15.6.3", "electron-context-menu": "0.9.0", "electron-devtools-installer": "^2.2.4", "electron-is-dev": "^0.3.0", diff --git a/src/yarn.lock b/src/yarn.lock index 9dafddc6..a8d4fb7b 100644 --- a/src/yarn.lock +++ b/src/yarn.lock @@ -107,14 +107,6 @@ core-js@^2.4.0: version "2.5.1" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.1.tgz#ae6874dc66937789b80754ff5428df66819ca50b" -create-react-class@^15.6.3: - version "15.6.3" - resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.6.3.tgz#2d73237fb3f970ae6ebe011a9e66f46dbca80036" - dependencies: - fbjs "^0.8.9" - loose-envify "^1.3.1" - object-assign "^4.1.1" - cross-unzip@0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/cross-unzip/-/cross-unzip-0.0.2.tgz#5183bc47a09559befcf98cc4657964999359372f" From 5091f597865761c2295e258b67cb91c6cb1dcc4f Mon Sep 17 00:00:00 2001 From: Yuya Ochiai Date: Thu, 6 Sep 2018 00:30:17 +0900 Subject: [PATCH 11/18] Remove findDOMNode() from components --- .eslintrc.json | 1 + src/browser/components/LoginModal.jsx | 11 +++++----- src/browser/components/MattermostView.jsx | 25 ++++++++++++----------- src/browser/components/SettingsPage.jsx | 6 ++++-- 4 files changed, 24 insertions(+), 19 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index a1d707ab..3b033c4f 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -20,6 +20,7 @@ "no-var": 2, "react/jsx-indent": [2, 2], "react/jsx-indent-props": [2, 2], + "react/no-find-dom-node": 2, "react/no-set-state": 1, "react/require-optimization": 0 } diff --git a/src/browser/components/LoginModal.jsx b/src/browser/components/LoginModal.jsx index 87db828e..3a251fdb 100644 --- a/src/browser/components/LoginModal.jsx +++ b/src/browser/components/LoginModal.jsx @@ -3,19 +3,20 @@ // See LICENSE.txt for license information. import React from 'react'; import PropTypes from 'prop-types'; -import {findDOMNode} from 'react-dom'; import {Button, Col, ControlLabel, Form, FormGroup, FormControl, Modal} from 'react-bootstrap'; export default class LoginModal extends React.Component { constructor(props) { super(props); this.handleSubmit = this.handleSubmit.bind(this); + this.usernameRef = React.createRef(); + this.passwordRef = React.createRef(); } handleSubmit(event) { event.preventDefault(); - const usernameNode = findDOMNode(this.refs.username); - const passwordNode = findDOMNode(this.refs.password); + const usernameNode = this.usernameRef.current; + const passwordNode = this.passwordRef.current; this.props.onLogin(this.props.request, usernameNode.value, passwordNode.value); usernameNode.value = ''; passwordNode.value = ''; @@ -53,7 +54,7 @@ export default class LoginModal extends React.Component { { e.stopPropagation(); }} @@ -69,7 +70,7 @@ export default class LoginModal extends React.Component { { e.stopPropagation(); }} diff --git a/src/browser/components/MattermostView.jsx b/src/browser/components/MattermostView.jsx index 03c0e27c..424c4507 100644 --- a/src/browser/components/MattermostView.jsx +++ b/src/browser/components/MattermostView.jsx @@ -9,7 +9,6 @@ import url from 'url'; import React from 'react'; import PropTypes from 'prop-types'; -import {findDOMNode} from 'react-dom'; import {ipcRenderer, remote, shell} from 'electron'; import contextMenu from '../js/contextMenu'; @@ -63,6 +62,8 @@ export default class MattermostView extends React.Component { this.goForward = this.goForward.bind(this); this.getSrc = this.getSrc.bind(this); this.handleDeepLink = this.handleDeepLink.bind(this); + + this.webviewRef = React.createRef(); } handleUnreadCountChange(unreadCount, mentionCount, isUnread, isMentioned) { @@ -73,7 +74,7 @@ export default class MattermostView extends React.Component { componentDidMount() { const self = this; - const webview = findDOMNode(this.refs.webview); + const webview = this.webviewRef.current; webview.addEventListener('did-fail-load', (e) => { console.log(self.props.name, 'webview did-fail-load', e); @@ -218,7 +219,7 @@ export default class MattermostView extends React.Component { reloadTimeoutID: null, isLoaded: false, }); - const webview = findDOMNode(this.refs.webview); + const webview = this.webviewRef.current; webview.reload(); } @@ -226,14 +227,14 @@ export default class MattermostView extends React.Component { this.setState({ errorInfo: null, }); - const webContents = findDOMNode(this.refs.webview).getWebContents(); + const webContents = this.webviewRef.current.getWebContents(); webContents.session.clearCache(() => { webContents.reload(); }); } focusOnWebView() { - const webview = findDOMNode(this.refs.webview); + const webview = this.webviewRef.current; const webContents = webview.getWebContents(); // webContents might not be created yet. if (webContents && !webContents.isFocused()) { webview.focus(); @@ -242,32 +243,32 @@ export default class MattermostView extends React.Component { } canGoBack() { - const webview = findDOMNode(this.refs.webview); + const webview = this.webviewRef.current; return webview.getWebContents().canGoBack(); } canGoForward() { - const webview = findDOMNode(this.refs.webview); + const webview = this.webviewRef.current; return webview.getWebContents().canGoForward(); } goBack() { - const webview = findDOMNode(this.refs.webview); + const webview = this.webviewRef.current; webview.getWebContents().goBack(); } goForward() { - const webview = findDOMNode(this.refs.webview); + const webview = this.webviewRef.current; webview.getWebContents().goForward(); } getSrc() { - const webview = findDOMNode(this.refs.webview); + const webview = this.webviewRef.current; return webview.src; } handleDeepLink(relativeUrl) { - const webview = findDOMNode(this.refs.webview); + const webview = this.webviewRef.current; webview.executeJavaScript( 'history.pushState(null, null, "' + relativeUrl + '");' ); @@ -314,7 +315,7 @@ export default class MattermostView extends React.Component { id={this.props.id} preload={preloadJS} src={this.props.src} - ref='webview' + ref={this.webviewRef} /> { loadingImage } ); diff --git a/src/browser/components/SettingsPage.jsx b/src/browser/components/SettingsPage.jsx index ba2c75eb..94036b60 100644 --- a/src/browser/components/SettingsPage.jsx +++ b/src/browser/components/SettingsPage.jsx @@ -6,7 +6,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import ReactDOM from 'react-dom'; import {Button, Checkbox, Col, FormGroup, Grid, HelpBlock, Navbar, Radio, Row} from 'react-bootstrap'; import {ipcRenderer, remote} from 'electron'; @@ -74,6 +73,8 @@ export default class SettingsPage extends React.Component { this.handleChangeEnableHardwareAcceleration = this.handleChangeEnableHardwareAcceleration.bind(this); this.updateTeam = this.updateTeam.bind(this); this.addServer = this.addServer.bind(this); + + this.trayIconThemeRef = React.createRef(); } componentDidMount() { @@ -214,7 +215,7 @@ export default class SettingsPage extends React.Component { handleChangeTrayIconTheme() { this.setState({ - trayIconTheme: ReactDOM.findDOMNode(this.refs.trayIconTheme).value, + trayIconTheme: this.trayIconThemeRef.current.value, }); setImmediate(this.startSaveConfig, CONFIG_TYPE_APP_OPTIONS); } @@ -543,6 +544,7 @@ export default class SettingsPage extends React.Component { options.push( {'Icon theme: '} From 11c777d5e83cf8a38c6377d77b98800ead679187 Mon Sep 17 00:00:00 2001 From: Yuya Ochiai Date: Wed, 12 Sep 2018 23:27:08 +0900 Subject: [PATCH 12/18] Update CHANGELOG.md --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74680e0a..df9456d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,10 @@ Release date: TBD - Added `Ctrl/Cmd+F` shortcut to work as browser-like search. [#399](https://github.com/mattermost/desktop/issues/399) +#### Windows + - Set "app start on login" preference to default on and synchronize its state with config.json. + [#846](https://github.com/mattermost/desktop/pull/846) + #### Mac - Add **.dmg** package to support installation. [#588](https://github.com/mattermost/desktop/pull/588) @@ -30,6 +34,8 @@ Release date: TBD - Updated categories in order to be listed under the appropriate submenu of the application starter. [#816](https://github.com/mattermost/desktop/pull/816) [#818](https://github.com/mattermost/desktop/pull/818) + - Set "app start on login" preference to default on and synchronize its state with config.json. + [#846](https://github.com/mattermost/desktop/pull/846) ### Architectural Changes - Major version upgrade of Electron to v2.0.8. Electron is the underlying technology used to build the Desktop apps. From bef3c96ea0f7a41f77abe35fa298e854ba54e7eb Mon Sep 17 00:00:00 2001 From: Dan Maas Date: Fri, 21 Sep 2018 16:46:01 -0700 Subject: [PATCH 13/18] Update NOTICE.txt - Add new dependencies (react-dom) - Update homepage and project owner info for all dependencies - Add SPDX open-source license IDs --- NOTICE.txt | 279 ++++++++++++++++++++++++++++------------------------- 1 file changed, 145 insertions(+), 134 deletions(-) diff --git a/NOTICE.txt b/NOTICE.txt index edfba9f6..a331ec53 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -10,12 +10,14 @@ This document includes a list of open source components used in Mattermost Deskt ## auto-launch -This product contains a modified portion of 'auto-launch', which allows one to launch any application or executable at startup, by Teamwork.com. +This product contains 'auto-launch' by Donal Linehan. + +Launch node applications or executables at login (Mac, Windows, and Linux) * HOMEPAGE: - * https://github.com/Teamwork/node-auto-launch + * https://github.com/4ver/node-auto-launch -* LICENSE: +* LICENSE: MIT MIT License @@ -43,18 +45,18 @@ SOFTWARE. ## bootstrap -This product contains a modified portion of 'bootstrap', which is an HTML, CSS, and JavaScript framework for developing responsive, mobile first -projects on the web, by Twitter, Inc. and Bootstrap Authors. +This product contains 'bootstrap' by The Bootstrap Authors. + +The most popular front-end framework for developing responsive, mobile first projects on the web. * HOMEPAGE: - * https://github.com/twbs/bootstrap + * https://getbootstrap.com/ -* LICENSE: +* LICENSE: MIT The MIT License (MIT) -Copyright (c) 2011-2017 Twitter, Inc. -Copyright (c) 2011-2017 The Bootstrap Authors +Copyright (c) 2011-2016 Twitter, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -78,105 +80,82 @@ THE SOFTWARE. ## electron-context-menu -This product contains a modified portion of 'electron-context-menu', which provides a context menu for Electron apps, by Sindre Sorhus. +This product contains 'electron-context-menu' by Sindre Sorhus. + +Context menu for your Electron app * HOMEPAGE: - * https://github.com/sindresorhus/electron-context-menu + * https://github.com/sindresorhus/electron-context-menu#readme -* LICENSE: +* LICENSE: MIT -The MIT License (MIT) +MIT License Copyright (c) Sindre Sorhus (sindresorhus.com) -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --- ## electron-devtools-installer -This product contains a modified portion of 'electron-devtools-installer', which allows an easy way to ensure Chrome DevTools extensions into Electron, -by Samuel Attard. +This product contains 'electron-devtools-installer' by Samuel Attard. + +An easy way to install Dev Tools extensions into Electron applications * HOMEPAGE: - * https://github.com/MarshallOfSound/electron-devtools-installer + * https://github.com/GPMDP/electron-devtools-installer#readme -* LICENSE: +* LICENSE: MIT The MIT License (MIT) Copyright (c) 2016 Samuel Attard -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files -(the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE -FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --- ## electron-is-dev -This product contains a modified portion of 'electron-is-dev', which checks if Electron is running in development, by Sindre Sorhus. +This product contains 'electron-is-dev' by Sindre Sorhus. + +Check if Electron is running in development * HOMEPAGE: - * https://github.com/sindresorhus/electron-is-dev + * https://github.com/sindresorhus/electron-is-dev#readme -* LICENSE: +* LICENSE: MIT -The MIT License (MIT) +MIT License Copyright (c) Sindre Sorhus (sindresorhus.com) -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --- ## electron-squirrel-startup -This product contains a modified portion of 'electron-squirrel-startup', a default Squirrel.Windows event handler for Electron apps, by mongodb-js. +This product contains 'electron-squirrel-startup' by Lucas Hrabovsky. + +Default Squirrel.Windows event handler for your Electron apps. * HOMEPAGE: - * https://github.com/mongodb-js/electron-squirrel-startup + * http://github.com/mongodb-js/electron-squirrel-startup -* LICENSE: +* LICENSE: Apache-2.0 Apache License Version 2.0, January 2004 @@ -384,57 +363,14 @@ limitations under the License. ## prop-types -This product contains a modified portion of 'prop-types', runtime type checking for React props and similar objects, by Facebook, Inc. +This product contains 'prop-types' by Facebook. + +Runtime type checking for React props and similar objects. * HOMEPAGE: - * https://github.com/facebook/prop-types + * https://facebook.github.io/react/ -* LICENSE: - -BSD License - -For React software - -Copyright (c) 2013-present, Facebook, Inc. -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - - * Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - - * Neither the name Facebook nor the names of its contributors may be used to - endorse or promote products derived from this software without specific - prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ---- - -## react - -This product contains a modified portion of 'react', a declarative, efficient, and flexible JavaScript library for building user interfaces, -and 'react-dom', the entry point of the DOM-related -rendering paths, by Facebook, Inc. - -* HOMEPAGE: - * https://github.com/facebook/react - -* LICENSE: +* LICENSE: MIT MIT License @@ -460,15 +396,51 @@ SOFTWARE. --- -## react-bootstrap +## react -This product contains a modified portion of 'react-bootstrap', Bootstrap 3 components for front-end framework built with React, by Stephen J. Collings, -Matthew Honnibal, Pieter Vanderwerff and react-bootstrap. +This product contains 'react' by Facebook. + +React is a JavaScript library for building user interfaces. * HOMEPAGE: - * https://github.com/react-bootstrap/react-bootstrap + * https://reactjs.org/ -* LICENSE: +* LICENSE: MIT + +MIT License + +Copyright (c) Facebook, Inc. and its affiliates. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +--- + +## react-bootstrap + +This product contains 'react-bootstrap' by Stephen J. Collings. + +Bootstrap 3 components built with React + +* HOMEPAGE: + * https://react-bootstrap.github.io/ + +* LICENSE: MIT The MIT License (MIT) @@ -494,14 +466,51 @@ THE SOFTWARE. --- -## react-transition-group +## react-dom -This product contains a modified portion of 'react-transition-group', a fork, and "drop in" replacement for the original React TransitionGroup addons, by React Community. +This product contains 'react-dom' by Facebook. + +React package for working with the DOM. * HOMEPAGE: - * https://github.com/reactjs/react-transition-group + * https://reactjs.org/ -* LICENSE: +* LICENSE: MIT + +MIT License + +Copyright (c) Facebook, Inc. and its affiliates. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +--- + +## react-transition-group + +This product contains 'react-transition-group' by React Community. + +A react component toolset for managing animations + +* HOMEPAGE: + * https://github.com/reactjs/react-transition-group#readme + +* LICENSE: BSD-3-Clause BSD 3-Clause License @@ -538,16 +547,16 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ## simple-spellchecker -This product contains a modified portion of 'simple-spellchecker', a simple and fast spellchecker with spelling suggestions and Electron's integration, by jfmdev. +This product contains 'simple-spellchecker' by JFMDev. -Access to the source code of Simple Spellchecker ©jfmdev can be found at https://github.com/jfmdev/simple-spellchecker. +A simple spellchecker compatible with Electron * HOMEPAGE: - * https://github.com/jfmdev/simple-spellchecker + * https://github.com/jfmdev/simple-spellchecker#readme -* LICENSE: +* LICENSE: MPL-2.0 -Mozilla Public License Version 2.0 +Mozilla Public License Version 2.0 ================================== 1. Definitions @@ -584,7 +593,7 @@ Mozilla Public License Version 2.0 means any form of the work other than Source Code Form. 1.7. "Larger Work" - means a work that combines Covered Software with other material, in + means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. "License" @@ -925,16 +934,16 @@ Exhibit B - "Incompatible With Secondary Licenses" Notice ## underscore -This product contains a modified portion of 'underscore', a utility-belt library for JavaScript that provides support for usual functional suspects -without extending any core JavaScript objects, by Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors. +This product contains 'underscore' by Jeremy Ashkenas. + +JavaScript's functional programming helper library. * HOMEPAGE: - * https://github.com/jashkenas/underscore + * http://underscorejs.org -* LICENSE: +* LICENSE: MIT -The MIT License (MIT) -Copyright (c) 2009-2017 Jeremy Ashkenas, DocumentCloud and Investigative +Copyright (c) 2009-2018 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors Permission is hereby granted, free of charge, to any person @@ -962,12 +971,14 @@ OTHER DEALINGS IN THE SOFTWARE. ## yargs -This product contains a modified portion of 'yargs', a modern successor to optimist, by James Halliday and Yargs. +This product contains 'yargs' by GitHub user "yargs". + +yargs the modern, pirate-themed, successor to optimist. * HOMEPAGE: - * https://github.com/yargs/yargs + * http://yargs.js.org/ -* LICENSE: +* LICENSE: MIT Copyright 2010 James Halliday (mail@substack.net) Modified work Copyright 2014 Contributors (ben@npmjs.com) From cbe7889f20ead98a3f4e4a58e7c1648d1cfa8913 Mon Sep 17 00:00:00 2001 From: Jesse Hallam Date: Mon, 24 Sep 2018 18:26:18 -0400 Subject: [PATCH 14/18] MM-12275: detect an expired session and show badging --- src/browser/components/MainPage.jsx | 31 ++++++++++++-------- src/browser/components/MattermostView.jsx | 23 +++++++-------- src/browser/components/TabBar.jsx | 12 ++++++-- src/browser/css/components/TabBar.css | 5 ++++ src/browser/index.jsx | 35 +++++++++++++++-------- src/browser/webview/mattermost.js | 13 ++++++--- src/main.js | 9 +++++- 7 files changed, 85 insertions(+), 43 deletions(-) diff --git a/src/browser/components/MainPage.jsx b/src/browser/components/MainPage.jsx index 3899dcb2..0d59490d 100644 --- a/src/browser/components/MainPage.jsx +++ b/src/browser/components/MainPage.jsx @@ -39,6 +39,7 @@ export default class MainPage extends React.Component { this.state = { key, + sessionsExpired: new Array(this.props.teams.length), unreadCounts: new Array(this.props.teams.length), mentionCounts: new Array(this.props.teams.length), unreadAtActive: new Array(this.props.teams.length), @@ -56,8 +57,6 @@ export default class MainPage extends React.Component { this.handleOnTeamFocused = this.handleOnTeamFocused.bind(this); this.handleSelect = this.handleSelect.bind(this); this.handleTargetURLChange = this.handleTargetURLChange.bind(this); - this.handleUnreadCountChange = this.handleUnreadCountChange.bind(this); - this.handleUnreadCountTotalChange = this.handleUnreadCountTotalChange.bind(this); this.inputBlur = this.inputBlur.bind(this); this.markReadAtActive = this.markReadAtActive.bind(this); } @@ -173,11 +172,13 @@ export default class MainPage extends React.Component { this.handleOnTeamFocused(newKey); } - handleUnreadCountChange(index, unreadCount, mentionCount, isUnread, isMentioned) { + handleBadgeChange = (index, sessionExpired, unreadCount, mentionCount, isUnread, isMentioned) => { + const sessionsExpired = this.state.sessionsExpired; const unreadCounts = this.state.unreadCounts; const mentionCounts = this.state.mentionCounts; const unreadAtActive = this.state.unreadAtActive; const mentionAtActiveCounts = this.state.mentionAtActiveCounts; + sessionsExpired[index] = sessionExpired; unreadCounts[index] = unreadCount; mentionCounts[index] = mentionCount; @@ -189,12 +190,13 @@ export default class MainPage extends React.Component { } } this.setState({ + sessionsExpired, unreadCounts, mentionCounts, unreadAtActive, mentionAtActiveCounts, }); - this.handleUnreadCountTotalChange(); + this.handleBadgesChange(); } markReadAtActive(index) { @@ -206,11 +208,13 @@ export default class MainPage extends React.Component { unreadAtActive, mentionAtActiveCounts, }); - this.handleUnreadCountTotalChange(); + this.handleBadgesChange(); } - handleUnreadCountTotalChange() { - if (this.props.onUnreadCountChange) { + handleBadgesChange = () => { + if (this.props.onBadgeChange) { + const someSessionsExpired = this.state.sessionsExpired.some((sessionExpired) => sessionExpired); + let allUnreadCount = this.state.unreadCounts.reduce((prev, curr) => { return prev + curr; }, 0); @@ -219,13 +223,15 @@ export default class MainPage extends React.Component { allUnreadCount += 1; } }); + let allMentionCount = this.state.mentionCounts.reduce((prev, curr) => { return prev + curr; }, 0); this.state.mentionAtActiveCounts.forEach((count) => { allMentionCount += count; }); - this.props.onUnreadCountChange(allUnreadCount, allMentionCount); + + this.props.onBadgeChange(someSessionsExpired, allUnreadCount, allMentionCount); } } @@ -299,6 +305,7 @@ export default class MainPage extends React.Component { { - function handleUnreadCountChange(unreadCount, mentionCount, isUnread, isMentioned) { - self.handleUnreadCountChange(index, unreadCount, mentionCount, isUnread, isMentioned); + function handleBadgeChange(sessionExpired, unreadCount, mentionCount, isUnread, isMentioned) { + self.handleBadgeChange(index, sessionExpired, unreadCount, mentionCount, isUnread, isMentioned); } function handleNotificationClick() { self.handleSelect(index); @@ -340,7 +347,7 @@ export default class MainPage extends React.Component { src={teamUrl} name={team.name} onTargetURLChange={self.handleTargetURLChange} - onUnreadCountChange={handleUnreadCountChange} + onBadgeChange={handleBadgeChange} onNotificationClick={handleNotificationClick} ref={id} active={isActive} @@ -437,7 +444,7 @@ export default class MainPage extends React.Component { } MainPage.propTypes = { - onUnreadCountChange: PropTypes.func.isRequired, + onBadgeChange: PropTypes.func.isRequired, teams: PropTypes.array.isRequired, onTeamConfigChange: PropTypes.func.isRequired, initialIndex: PropTypes.number.isRequired, diff --git a/src/browser/components/MattermostView.jsx b/src/browser/components/MattermostView.jsx index 424c4507..74b987cb 100644 --- a/src/browser/components/MattermostView.jsx +++ b/src/browser/components/MattermostView.jsx @@ -66,9 +66,9 @@ export default class MattermostView extends React.Component { this.webviewRef = React.createRef(); } - handleUnreadCountChange(unreadCount, mentionCount, isUnread, isMentioned) { - if (this.props.onUnreadCountChange) { - this.props.onUnreadCountChange(unreadCount, mentionCount, isUnread, isMentioned); + handleUnreadCountChange(sessionExpired, unreadCount, mentionCount, isUnread, isMentioned) { + if (this.props.onBadgeChange) { + this.props.onBadgeChange(sessionExpired, unreadCount, mentionCount, isUnread, isMentioned); } } @@ -158,14 +158,13 @@ export default class MattermostView extends React.Component { isLoaded: true, }); break; - case 'onUnreadCountChange': { - const unreadCount = event.args[0]; - const mentionCount = event.args[1]; - - // isUnread and isMentioned is pulse flag. - const isUnread = event.args[2]; - const isMentioned = event.args[3]; - self.handleUnreadCountChange(unreadCount, mentionCount, isUnread, isMentioned); + case 'onBadgeChange': { + const sessionExpired = event.args[0]; + const unreadCount = event.args[1]; + const mentionCount = event.args[2]; + const isUnread = event.args[3]; + const isMentioned = event.args[4]; + self.handleUnreadCountChange(sessionExpired, unreadCount, mentionCount, isUnread, isMentioned); break; } @@ -326,7 +325,7 @@ MattermostView.propTypes = { name: PropTypes.string, id: PropTypes.string, onTargetURLChange: PropTypes.func, - onUnreadCountChange: PropTypes.func, + onBadgeChange: PropTypes.func, src: PropTypes.string, active: PropTypes.bool, withTab: PropTypes.bool, diff --git a/src/browser/components/TabBar.jsx b/src/browser/components/TabBar.jsx index 1df39088..06bcd50d 100644 --- a/src/browser/components/TabBar.jsx +++ b/src/browser/components/TabBar.jsx @@ -10,6 +10,8 @@ import PermissionRequestDialog from './PermissionRequestDialog.jsx'; export default class TabBar extends React.Component { // need "this" render() { const tabs = this.props.teams.map((team, index) => { + const sessionExpired = this.props.sessionsExpired[index]; + let unreadCount = 0; if (this.props.unreadCounts[index] > 0) { unreadCount = this.props.unreadCounts[index]; @@ -27,11 +29,16 @@ export default class TabBar extends React.Component { // need "this" } let badgeDiv; - if (mentionCount !== 0) { + if (sessionExpired) { + badgeDiv = ( +
{'•'}
+ ); + } else if (mentionCount !== 0) { badgeDiv = (
{mentionCount} -
); + + ); } const id = 'teamTabItem' + index; const requestingPermission = this.props.requestingPermission[index]; @@ -114,6 +121,7 @@ TabBar.propTypes = { id: PropTypes.string, onSelect: PropTypes.func, teams: PropTypes.array, + sessionsExpired: PropTypes.array, unreadCounts: PropTypes.array, unreadAtActive: PropTypes.array, mentionCounts: PropTypes.array, diff --git a/src/browser/css/components/TabBar.css b/src/browser/css/components/TabBar.css index 327a6a6d..d2905892 100644 --- a/src/browser/css/components/TabBar.css +++ b/src/browser/css/components/TabBar.css @@ -53,6 +53,11 @@ border-radius: 50%; } +.TabBar .TabBar-badge.TabBar-badge-nomention { + font-size: 22pt; + line-height: 18px; +} + .TabBar .teamTabItem-unread { font-weight: bold; } diff --git a/src/browser/index.jsx b/src/browser/index.jsx index 784c3ece..47c905ce 100644 --- a/src/browser/index.jsx +++ b/src/browser/index.jsx @@ -31,19 +31,23 @@ if (teams.length === 0) { window.location = 'settings.html'; } -function showUnreadBadgeWindows(unreadCount, mentionCount) { +function showBadgeWindows(sessionExpired, unreadCount, mentionCount) { function sendBadge(dataURL, description) { // window.setOverlayIcon() does't work with NativeImage across remote boundaries. // https://github.com/atom/electron/issues/4011 ipcRenderer.send('update-unread', { overlayDataURL: dataURL, description, + sessionExpired, unreadCount, mentionCount, }); } - if (mentionCount > 0) { + if (sessionExpired) { + const dataURL = createBadgeDataURL('•'); + sendBadge(dataURL, 'Session Expired: Please sign in to continue receiving notifications.'); + } else if (mentionCount > 0) { const dataURL = createBadgeDataURL(mentionCount.toString()); sendBadge(dataURL, 'You have unread mentions (' + mentionCount + ')'); } else if (unreadCount > 0 && AppConfig.data.showUnreadBadge) { @@ -54,8 +58,10 @@ function showUnreadBadgeWindows(unreadCount, mentionCount) { } } -function showUnreadBadgeOSX(unreadCount, mentionCount) { - if (mentionCount > 0) { +function showBadgeOSX(sessionExpired, unreadCount, mentionCount) { + if (sessionExpired) { + remote.app.dock.setBadge('•'); + } else if (mentionCount > 0) { remote.app.dock.setBadge(mentionCount.toString()); } else if (unreadCount > 0 && AppConfig.data.showUnreadBadge) { remote.app.dock.setBadge('•'); @@ -64,34 +70,39 @@ function showUnreadBadgeOSX(unreadCount, mentionCount) { } ipcRenderer.send('update-unread', { + sessionExpired, unreadCount, mentionCount, }); } -function showUnreadBadgeLinux(unreadCount, mentionCount) { +function showBadgeLinux(sessionExpired, unreadCount, mentionCount) { if (remote.app.isUnityRunning()) { - remote.app.setBadgeCount(mentionCount); + if (sessionExpired) { + remote.app.setBadgeCount(1); + } else { + remote.app.setBadgeCount(mentionCount); + } } ipcRenderer.send('update-unread', { + sessionExpired, unreadCount, mentionCount, }); } -function showUnreadBadge(unreadCount, mentionCount) { +function showBadge(sessionExpired, unreadCount, mentionCount) { switch (process.platform) { case 'win32': - showUnreadBadgeWindows(unreadCount, mentionCount); + showBadgeWindows(sessionExpired, unreadCount, mentionCount); break; case 'darwin': - showUnreadBadgeOSX(unreadCount, mentionCount); + showBadgeOSX(sessionExpired, unreadCount, mentionCount); break; case 'linux': - showUnreadBadgeLinux(unreadCount, mentionCount); + showBadgeLinux(sessionExpired, unreadCount, mentionCount); break; - default: } } @@ -168,7 +179,7 @@ ReactDOM.render( Log out => Count should be 0. + // LHS not found => Log out => Count should be 0, but session may be expired. if (document.getElementById('sidebar-left') === null) { - ipcRenderer.sendToHost('onUnreadCountChange', 0, 0, false, false); + const extraParam = (new URLSearchParams(window.location.search)).get('extra'); + const sessionExpired = extraParam === 'expired'; + + ipcRenderer.sendToHost('onBadgeChange', sessionExpired, 0, 0, false, false); + this.sessionExpired = sessionExpired; this.unreadCount = 0; this.mentionCount = 0; setTimeout(getUnreadCount, UNREAD_COUNT_INTERVAL); @@ -152,11 +156,12 @@ function getUnreadCount() { } } - if (this.unreadCount !== unreadCount || this.mentionCount !== mentionCount || isUnread || isMentioned) { - ipcRenderer.sendToHost('onUnreadCountChange', unreadCount, mentionCount, isUnread, isMentioned); + if (this.sessionExpired || this.unreadCount !== unreadCount || this.mentionCount !== mentionCount || isUnread || isMentioned) { + ipcRenderer.sendToHost('onBadgeChange', false, unreadCount, mentionCount, isUnread, isMentioned); } this.unreadCount = unreadCount; this.mentionCount = mentionCount; + this.sessionExpired = false; setTimeout(getUnreadCount, UNREAD_COUNT_INTERVAL); } setTimeout(getUnreadCount, UNREAD_COUNT_INTERVAL); diff --git a/src/main.js b/src/main.js index eca0ec98..93345a3d 100644 --- a/src/main.js +++ b/src/main.js @@ -528,7 +528,14 @@ app.on('ready', () => { } if (trayIcon && !trayIcon.isDestroyed()) { - if (arg.mentionCount > 0) { + if (arg.sessionExpired) { + // reuse the mention icon when the session is expired + trayIcon.setImage(trayImages.mention); + if (process.platform === 'darwin') { + trayIcon.setPressedImage(trayImages.clicked.mention); + } + trayIcon.setToolTip('Session Expired: Please sign in to continue receiving notifications.'); + } else if (arg.mentionCount > 0) { trayIcon.setImage(trayImages.mention); if (process.platform === 'darwin') { trayIcon.setPressedImage(trayImages.clicked.mention); From 6f6e80bdacde10c19051a7336a6be18ed5f9f991 Mon Sep 17 00:00:00 2001 From: Hans-Peter Schadler Date: Sat, 6 Oct 2018 09:42:15 +0200 Subject: [PATCH 15/18] Preserve case of first letter in spell check, fixes #562 --- src/main/SpellChecker.js | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/main/SpellChecker.js b/src/main/SpellChecker.js index 018c0c59..da7f0291 100644 --- a/src/main/SpellChecker.js +++ b/src/main/SpellChecker.js @@ -70,7 +70,24 @@ export default class SpellChecker extends EventEmitter { } getSuggestions(word, maxSuggestions) { - return this.dict.getSuggestions(word, maxSuggestions); + const suggestions = this.dict.getSuggestions(word, maxSuggestions); + + const firstCharWord = word.charAt(0); + let i; + for (i = 0; i < suggestions.length; i++) { + if (suggestions[i].charAt(0).toUpperCase() === firstCharWord.toUpperCase()) { + suggestions[i] = firstCharWord + suggestions[i].slice(1); + } + } + + const uniqueSuggestions = suggestions.reduce((a, b) => { + if (a.indexOf(b) < 0) { + a.push(b); + } + return a; + }, []); + + return uniqueSuggestions; } } From 8bad9e002e8750ece3c39cd614ffe2abe9148221 Mon Sep 17 00:00:00 2001 From: Hans-Peter Schadler Date: Tue, 9 Oct 2018 18:58:32 +0200 Subject: [PATCH 16/18] Add tests for spellcheck suggestions --- test/specs/spellchecker_test.js | 36 +++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/test/specs/spellchecker_test.js b/test/specs/spellchecker_test.js index fd3853ba..6b85eb0e 100644 --- a/test/specs/spellchecker_test.js +++ b/test/specs/spellchecker_test.js @@ -67,6 +67,27 @@ describe('main/Spellchecker.js', function() { spellchecker.spellCheck('Mattermost').should.equal(true); spellchecker.spellCheck('mattermost').should.equal(true); }); + + it('should give at most the requested number of suggestions', function() { + // helllo known to give at least 4 suggestions + spellchecker.getSuggestions('helllo', 4).length.should.be.equal(4); + spellchecker.getSuggestions('helllo', 1).length.should.be.equal(1); + }); + + it('should give suggestions which preserve case of first letter', function() { + let suggestions = spellchecker.getSuggestions('carr', 4); + suggestions.length.should.not.be.equal(0); + let i; + for (i = 0; i < suggestions.length; i++) { + suggestions[i].charAt(0).should.be.equal('c'); + } + + suggestions = spellchecker.getSuggestions('Carr', 4); + suggestions.length.should.not.be.equal(0); + for (i = 0; i < suggestions.length; i++) { + suggestions[i].charAt(0).should.be.equal('C'); + } + }); }); describe('en-GB', function() { @@ -107,5 +128,20 @@ describe('main/Spellchecker.js', function() { spellchecker.spellCheck('-100').should.equal(true); spellchecker.spellCheck('3.14').should.equal(true); }); + + it('should give suggestions which preserve case of first letter', function() { + let suggestions = spellchecker.getSuggestions('gutenn', 4); + suggestions.length.should.not.be.equal(0); + let i; + for (i = 0; i < suggestions.length; i++) { + suggestions[i].charAt(0).should.be.equal('g'); + } + + suggestions = spellchecker.getSuggestions('Gutenn', 4); + suggestions.length.should.not.be.equal(0); + for (i = 0; i < suggestions.length; i++) { + suggestions[i].charAt(0).should.be.equal('G'); + } + }); }); }); From 037c5c433366326a40f1879b3de18962b61afc04 Mon Sep 17 00:00:00 2001 From: Jesse Hallam Date: Fri, 12 Oct 2018 08:45:12 -0400 Subject: [PATCH 17/18] increment mentionCount for linux instead --- src/browser/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/index.jsx b/src/browser/index.jsx index 47c905ce..aa3919b3 100644 --- a/src/browser/index.jsx +++ b/src/browser/index.jsx @@ -79,7 +79,7 @@ function showBadgeOSX(sessionExpired, unreadCount, mentionCount) { function showBadgeLinux(sessionExpired, unreadCount, mentionCount) { if (remote.app.isUnityRunning()) { if (sessionExpired) { - remote.app.setBadgeCount(1); + remote.app.setBadgeCount(mentionCount + 1); } else { remote.app.setBadgeCount(mentionCount); } From 2c9a816942d590179f5942bb761a85013cf231e4 Mon Sep 17 00:00:00 2001 From: Yuya Ochiai Date: Mon, 15 Oct 2018 23:54:30 +0900 Subject: [PATCH 18/18] Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index df9456d8..b8612ce1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ Release date: TBD [#843](https://github.com/mattermost/desktop/pull/843) - Added `Ctrl/Cmd+F` shortcut to work as browser-like search. [#399](https://github.com/mattermost/desktop/issues/399) + - Preserved case of first letter in spellcheck. + [#869](https://github.com/mattermost/desktop/pull/869) #### Windows - Set "app start on login" preference to default on and synchronize its state with config.json.