diff --git a/.eslintrc.json b/.eslintrc.json index 283fa26b..cc864d9b 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -7,11 +7,8 @@ "no-eval": 1, "no-process-env": 0, "no-underscore-dangle": 1, - "react/jsx-boolean-value": [1, "always"], "react/jsx-indent": [2, 2], "react/jsx-indent-props": [2, 2], - "react/no-multi-comp": 1, - "react/prefer-es6-class": 1, - "react/prop-types": 1 + "react/prefer-es6-class": 1 } } diff --git a/src/browser/components/ErrorView.jsx b/src/browser/components/ErrorView.jsx new file mode 100644 index 00000000..b1061e3a --- /dev/null +++ b/src/browser/components/ErrorView.jsx @@ -0,0 +1,95 @@ +// ErrorCode: https://code.google.com/p/chromium/codesearch#chromium/src/net/base/net_error_list.h + +const React = require('react'); +const {Grid, Row, Col} = require('react-bootstrap'); + +const errorPage = { + tableStyle: { + display: 'table', + width: '100%', + height: '100%', + position: 'absolute', + top: '0', + left: '0' + }, + + cellStyle: { + display: 'table-cell', + verticalAlign: 'top', + paddingTop: '2em' + }, + + bullets: { + paddingLeft: '15px', + lineHeight: '1.7' + }, + + techInfo: { + fontSize: '12px', + color: '#aaa' + } +}; + +class ErrorView extends React.Component { + render() { + return ( + +
+
+ + + +

{'Cannot connect to Mattermost'}

+
+

{'We\'re having trouble connecting to Mattermost. If refreshing this page (Ctrl+R or Command+R) does not work please verify that:'}

+
+ +
+
+ {this.props.errorInfo.errorDescription}{' ('} + {this.props.errorInfo.errorCode }{')'}
+ + +
+
+
+
+ ); + } +} + +ErrorView.propTypes = { + errorInfo: React.PropTypes.object, + id: React.PropTypes.number, + style: React.PropTypes.object +}; + +module.exports = ErrorView; diff --git a/src/browser/components/loginModal.jsx b/src/browser/components/LoginModal.jsx similarity index 81% rename from src/browser/components/loginModal.jsx rename to src/browser/components/LoginModal.jsx index 50c48e04..0e04156e 100644 --- a/src/browser/components/loginModal.jsx +++ b/src/browser/components/LoginModal.jsx @@ -1,16 +1,13 @@ const React = require('react'); const ReactDOM = require('react-dom'); -const ReactBootstrap = require('react-bootstrap'); -const Modal = ReactBootstrap.Modal; -const Form = ReactBootstrap.Form; -const FormGroup = ReactBootstrap.FormGroup; -const FormControl = ReactBootstrap.FormControl; -const ControlLabel = ReactBootstrap.ControlLabel; -const Col = ReactBootstrap.Col; +const {Button, Col, ControlLabel, Form, FormGroup, FormControl, Modal} = require('react-bootstrap'); -const Button = ReactBootstrap.Button; +class LoginModal extends React.Component { + constructor(props) { + super(props); + this.handleSubmit = this.handleSubmit.bind(this); + } -const LoginModal = React.createClass({ handleSubmit(event) { event.preventDefault(); const usernameNode = ReactDOM.findDOMNode(this.refs.username); @@ -18,7 +15,8 @@ const LoginModal = React.createClass({ this.props.onLogin(this.props.request, usernameNode.value, passwordNode.value); usernameNode.value = ''; passwordNode.value = ''; - }, + } + render() { var theServer = ''; if (!this.props.show) { @@ -39,7 +37,7 @@ const LoginModal = React.createClass({ { message }

@@ -85,6 +83,15 @@ const LoginModal = React.createClass({ ); } -}); +} + +LoginModal.propTypes = { + authInfo: React.PropTypes.object, + authServerURL: React.PropTypes.string, + onCancel: React.PropTypes.func, + onLogin: React.PropTypes.func, + request: React.PropTypes.object, + show: React.PropTypes.bool +}; module.exports = LoginModal; diff --git a/src/browser/components/MainPage.jsx b/src/browser/components/MainPage.jsx new file mode 100644 index 00000000..059a69ad --- /dev/null +++ b/src/browser/components/MainPage.jsx @@ -0,0 +1,281 @@ +const React = require('react'); +const {Grid, Row} = require('react-bootstrap'); + +const {ipcRenderer, remote} = require('electron'); +const url = require('url'); + +const LoginModal = require('./LoginModal.jsx'); +const MattermostView = require('./MattermostView.jsx'); +const TabBar = require('./TabBar.jsx'); + +const MainPage = React.createClass({ + propTypes: { + disablewebsecurity: React.PropTypes.bool.isRequired, + onUnreadCountChange: React.PropTypes.func.isRequired, + teams: React.PropTypes.array.isRequired + }, + + getInitialState() { + return { + key: 0, + unreadCounts: new Array(this.props.teams.length), + mentionCounts: new Array(this.props.teams.length), + unreadAtActive: new Array(this.props.teams.length), + mentionAtActiveCounts: new Array(this.props.teams.length), + loginQueue: [] + }; + }, + componentDidMount() { + var self = this; + ipcRenderer.on('login-request', (event, request, authInfo) => { + self.setState({ + loginRequired: true + }); + const loginQueue = self.state.loginQueue; + loginQueue.push({ + request, + authInfo + }); + self.setState({ + loginQueue + }); + }); + + // can't switch tabs sequencially for some reason... + ipcRenderer.on('switch-tab', (event, key) => { + this.handleSelect(key); + }); + ipcRenderer.on('select-next-tab', () => { + this.handleSelect(this.state.key + 1); + }); + ipcRenderer.on('select-previous-tab', () => { + this.handleSelect(this.state.key - 1); + }); + + // reload the activated tab + ipcRenderer.on('reload-tab', () => { + this.refs[`mattermostView${this.state.key}`].reload(); + }); + ipcRenderer.on('clear-cache-and-reload-tab', () => { + this.refs[`mattermostView${this.state.key}`].clearCacheAndReload(); + }); + + // activate search box in current tab + ipcRenderer.on('activate-search-box', () => { + const webview = document.getElementById('mattermostView' + self.state.key); + webview.send('activate-search-box'); + }); + + // activate search box in current chunnel + ipcRenderer.on('activate-search-box-in-channel', () => { + const webview = document.getElementById('mattermostView' + self.state.key); + webview.send('activate-search-box-in-channel'); + }); + + function focusListener() { + self.handleOnTeamFocused(self.state.key); + self.refs[`mattermostView${self.state.key}`].focusOnWebView(); + } + + var currentWindow = remote.getCurrentWindow(); + currentWindow.on('focus', focusListener); + window.addEventListener('beforeunload', () => { + currentWindow.removeListener('focus', focusListener); + }); + + // https://github.com/mattermost/desktop/pull/371#issuecomment-263072803 + currentWindow.webContents.on('devtools-closed', () => { + focusListener(); + }); + + //goBack and goForward + ipcRenderer.on('go-back', () => { + const mattermost = self.refs[`mattermostView${self.state.key}`]; + if (mattermost.canGoBack()) { + mattermost.goBack(); + } + }); + + ipcRenderer.on('go-forward', () => { + const mattermost = self.refs[`mattermostView${self.state.key}`]; + if (mattermost.canGoForward()) { + mattermost.goForward(); + } + }); + }, + 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({ + key: newKey + }); + this.handleOnTeamFocused(newKey); + + var webview = document.getElementById('mattermostView' + newKey); + ipcRenderer.send('update-title', { + title: webview.getTitle() + }); + }, + 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; + unreadCounts[index] = unreadCount; + mentionCounts[index] = mentionCount; + + // Never turn on the unreadAtActive flag at current focused tab. + if (this.state.key !== index || !remote.getCurrentWindow().isFocused()) { + unreadAtActive[index] = unreadAtActive[index] || isUnread; + if (isMentioned) { + mentionAtActiveCounts[index]++; + } + } + this.setState({ + unreadCounts, + mentionCounts, + unreadAtActive, + mentionAtActiveCounts + }); + this.handleUnreadCountTotalChange(); + }, + markReadAtActive(index) { + var unreadAtActive = this.state.unreadAtActive; + var mentionAtActiveCounts = this.state.mentionAtActiveCounts; + unreadAtActive[index] = false; + mentionAtActiveCounts[index] = 0; + this.setState({ + unreadAtActive, + mentionAtActiveCounts + }); + this.handleUnreadCountTotalChange(); + }, + handleUnreadCountTotalChange() { + if (this.props.onUnreadCountChange) { + var allUnreadCount = this.state.unreadCounts.reduce((prev, curr) => { + return prev + curr; + }, 0); + this.state.unreadAtActive.forEach((state) => { + if (state) { + allUnreadCount += 1; + } + }); + var allMentionCount = this.state.mentionCounts.reduce((prev, curr) => { + return prev + curr; + }, 0); + this.state.mentionAtActiveCounts.forEach((count) => { + allMentionCount += count; + }); + 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); + }, + + visibleStyle(visible) { + var visibility = visible ? 'visible' : 'hidden'; + return { + position: 'absolute', + top: (this.props.teams.length > 1) ? 42 : 0, + right: 0, + bottom: 0, + left: 0, + visibility + }; + }, + + 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}); + }, + render() { + var self = this; + + var tabsRow; + if (this.props.teams.length > 1) { + tabsRow = ( + + + + ); + } + + var 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; + return ( + ); + }); + var viewsRow = ( + + {views} + ); + + var request = null; + var authServerURL = null; + var 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; + } + return ( +
+ + + { tabsRow } + { viewsRow } + +
+ ); + } +}); + +module.exports = MainPage; diff --git a/src/browser/components/MattermostView.jsx b/src/browser/components/MattermostView.jsx new file mode 100644 index 00000000..7a7aec86 --- /dev/null +++ b/src/browser/components/MattermostView.jsx @@ -0,0 +1,231 @@ +const React = require('react'); +const {findDOMNode} = require('react-dom'); +const {ipcRenderer, shell} = require('electron'); +const fs = require('fs'); +const url = require('url'); +const osLocale = require('os-locale'); +const electronContextMenu = require('electron-context-menu'); + +const ErrorView = require('./ErrorView.jsx'); + +const MattermostView = React.createClass({ + propTypes: { + disablewebsecurity: React.PropTypes.bool, + name: React.PropTypes.string, + id: React.PropTypes.string, + onUnreadCountChange: React.PropTypes.func, + src: React.PropTypes.string, + style: React.PropTypes.object + }, + + getInitialState() { + return { + errorInfo: null + }; + }, + + handleUnreadCountChange(unreadCount, mentionCount, isUnread, isMentioned) { + if (this.props.onUnreadCountChange) { + this.props.onUnreadCountChange(unreadCount, mentionCount, isUnread, isMentioned); + } + }, + + componentDidMount() { + var self = this; + var webview = findDOMNode(this.refs.webview); + + // This option allows insecure content, when set to true it is possible to + // load content via HTTP while the mattermost server serves HTTPS + if (this.props.disablewebsecurity === true) { + webview.setAttribute('webpreferences', 'allowDisplayingInsecureContent'); + } + + webview.addEventListener('did-fail-load', (e) => { + console.log(self.props.name, 'webview did-fail-load', e); + if (e.errorCode === -3) { // An operation was aborted (due to user action). + return; + } + + self.setState({ + errorInfo: e + }); + function reload() { + window.removeEventListener('online', reload); + self.reload(); + } + if (navigator.onLine) { + setTimeout(reload, 30000); + } else { + window.addEventListener('online', reload); + } + }); + + // 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); + if (destURL.protocol !== 'http:' && destURL.protocol !== 'https:') { + ipcRenderer.send('confirm-protocol', destURL.protocol, e.url); + return; + } + if (currentURL.host === destURL.host) { + // New window should disable nodeIntergration. + window.open(e.url, 'Mattermost', 'nodeIntegration=no'); + } else { + // if the link is external, use default browser. + shell.openExternal(e.url); + } + }); + + webview.addEventListener('dom-ready', () => { + // webview.openDevTools(); + + // Use 'Meiryo UI' and 'MS Gothic' to prevent CJK fonts on Windows(JP). + if (process.platform === 'win32') { + function applyCssFile(cssFile) { + fs.readFile(cssFile, 'utf8', (err, data) => { + if (err) { + console.log(err); + return; + } + webview.insertCSS(data); + }); + } + + osLocale((err, locale) => { + if (err) { + console.log(err); + return; + } + if (locale === 'ja_JP') { + applyCssFile(__dirname + '/css/jp_fonts.css'); + } + }); + } + + electronContextMenu({ + window: webview + }); + }); + + webview.addEventListener('ipc-message', (event) => { + switch (event.channel) { + case 'onUnreadCountChange': + var unreadCount = event.args[0]; + var mentionCount = event.args[1]; + + // isUnread and isMentioned is pulse flag. + var isUnread = event.args[2]; + var isMentioned = event.args[3]; + self.handleUnreadCountChange(unreadCount, mentionCount, isUnread, isMentioned); + break; + case 'onNotificationClick': + self.props.onNotificationClick(); + break; + } + }); + + webview.addEventListener('page-title-updated', (event) => { + if (self.props.active) { + ipcRenderer.send('update-title', { + title: event.title + }); + } + }); + + webview.addEventListener('console-message', (e) => { + const message = `[${this.props.name}] ${e.message}`; + switch (e.level) { + case 0: + console.log(message); + break; + case 1: + console.warn(message); + break; + case 2: + console.error(message); + break; + default: + console.log(message); + break; + } + }); + }, + + reload() { + this.setState({ + errorInfo: null + }); + var webview = findDOMNode(this.refs.webview); + webview.reload(); + }, + + clearCacheAndReload() { + this.setState({ + errorInfo: null + }); + var webContents = findDOMNode(this.refs.webview).getWebContents(); + webContents.session.clearCache(() => { + webContents.reload(); + }); + }, + + focusOnWebView() { + const webview = findDOMNode(this.refs.webview); + if (!webview.getWebContents().isFocused()) { + webview.focus(); + webview.getWebContents().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(); + }, + + render() { + const errorView = this.state.errorInfo ? ( + ) : null; + + // 'disablewebsecurity' is necessary to display external images. + // However, it allows also CSS/JavaScript. + // So webview should use 'allowDisplayingInsecureContent' as same as BrowserWindow. + + // Need to keep webview mounted when failed to load. + return ( +
+ { errorView } + +
); + } +}); + +module.exports = MattermostView; diff --git a/src/browser/components/SettingsPage.jsx b/src/browser/components/SettingsPage.jsx new file mode 100644 index 00000000..0eaeeb2d --- /dev/null +++ b/src/browser/components/SettingsPage.jsx @@ -0,0 +1,387 @@ +const React = require('react'); +const {Button, Checkbox, Col, FormGroup, Grid, Navbar, Row} = require('react-bootstrap'); + +const {ipcRenderer, remote} = require('electron'); +const AutoLaunch = require('auto-launch'); + +const settings = require('../../common/settings'); + +const TeamList = require('./TeamList.jsx'); + +const appLauncher = new AutoLaunch({ + name: 'Mattermost', + isHidden: true +}); + +function backToIndex() { + remote.getCurrentWindow().loadURL('file://' + __dirname + '/index.html'); +} + +const SettingsPage = React.createClass({ + propTypes: { + configFile: React.PropTypes.string + }, + + getInitialState() { + var initialState; + try { + initialState = settings.readFileSync(this.props.configFile); + } catch (e) { + initialState = settings.loadDefault(); + } + + initialState.showAddTeamForm = false; + initialState.trayWasVisible = remote.getCurrentWindow().trayWasVisible; + + return initialState; + }, + componentDidMount() { + if (process.platform === 'win32' || process.platform === 'linux') { + var self = this; + appLauncher.isEnabled().then((enabled) => { + self.setState({ + autostart: enabled + }); + }); + } + }, + handleTeamsChange(teams) { + this.setState({ + showAddTeamForm: false, + teams + }); + }, + handleSave() { + var config = { + teams: this.state.teams, + hideMenuBar: this.state.hideMenuBar, + showTrayIcon: this.state.showTrayIcon, + trayIconTheme: this.state.trayIconTheme, + disablewebsecurity: this.state.disablewebsecurity, + version: settings.version, + minimizeToTray: this.state.minimizeToTray, + toggleWindowOnTrayIconClick: this.state.toggleWindowOnTrayIconClick, + notifications: { + flashWindow: this.state.notifications.flashWindow + }, + showUnreadBadge: this.state.showUnreadBadge + }; + settings.writeFileSync(this.props.configFile, config); + if (process.platform === 'win32' || process.platform === 'linux') { + var currentWindow = remote.getCurrentWindow(); + currentWindow.setAutoHideMenuBar(config.hideMenuBar); + currentWindow.setMenuBarVisibility(!config.hideMenuBar); + + var autostart = this.state.autostart; + appLauncher.isEnabled().then((enabled) => { + if (enabled && !autostart) { + appLauncher.disable(); + } else if (!enabled && autostart) { + appLauncher.enable(); + } + }); + } + + ipcRenderer.send('update-menu', config); + ipcRenderer.send('update-config'); + + backToIndex(); + }, + handleCancel() { + backToIndex(); + }, + handleChangeDisableWebSecurity() { + this.setState({ + disablewebsecurity: !this.refs.disablewebsecurity.props.checked + }); + }, + handleChangeHideMenuBar() { + this.setState({ + hideMenuBar: !this.refs.hideMenuBar.props.checked + }); + }, + handleChangeShowTrayIcon() { + var shouldShowTrayIcon = !this.refs.showTrayIcon.props.checked; + this.setState({ + showTrayIcon: shouldShowTrayIcon + }); + + if (process.platform === 'darwin' && !shouldShowTrayIcon) { + this.setState({ + minimizeToTray: false + }); + } + }, + handleChangeTrayIconTheme() { + this.setState({ + trayIconTheme: !this.refs.trayIconTheme.props.checked + }); + }, + handleChangeAutoStart() { + this.setState({ + autostart: !this.refs.autostart.props.checked + }); + }, + handleChangeMinimizeToTray() { + var shouldMinimizeToTray = + (process.platform !== 'darwin' || !this.refs.showTrayIcon.props.checked) && + !this.refs.minimizeToTray.props.checked; + + this.setState({ + minimizeToTray: shouldMinimizeToTray + }); + }, + handleChangeToggleWindowOnTrayIconClick() { + this.setState({ + toggleWindowOnTrayIconClick: !this.refs.toggleWindowOnTrayIconClick.props.checked + }); + }, + toggleShowTeamForm() { + this.setState({ + showAddTeamForm: !this.state.showAddTeamForm + }); + }, + handleFlashWindow() { + this.setState({ + notifications: { + flashWindow: this.refs.flashWindow.props.checked ? 0 : 2 + } + }); + }, + handleShowUnreadBadge() { + this.setState({ + showUnreadBadge: !this.refs.showUnreadBadge.props.checked + }); + }, + render() { + var teamsRow = ( + + + + + + ); + + var options = []; + if (process.platform === 'win32' || process.platform === 'linux') { + options.push( + {'Hide menu bar (Press Alt to show menu bar)'}); + } + if (process.platform === 'darwin' || process.platform === 'linux') { + options.push( + {process.platform === 'darwin' ? + 'Show icon on menu bar (need to restart the application)' : + 'Show icon in notification area (need to restart the application)'}); + } + if (process.platform === 'linux') { + options.push( + {'Icon theme (Need to restart the application)'} + + + ); + } + options.push( + {'Allow mixed content (Enabling allows both secure and insecure content, images and scripts to render and execute. Disabling allows only secure content.)'}); + + //OSX 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') { + options.push( + {'Start app on login.'}); + } + + if (process.platform === 'darwin' || process.platform === 'linux') { + options.push( + {this.state.trayWasVisible || !this.state.showTrayIcon ? 'Leave app running in notification area when the window is closed' : 'Leave app running in notification area when the window is closed (available on next restart)'}); + } + + if (process.platform === 'win32') { + options.push( + {'Toggle window visibility when clicking on the tray icon.'}); + } + + if (process.platform === 'darwin' || process.platform === 'win32') { + options.push( + {'Show red badge on taskbar icon to indicate unread messages. Regardless of this setting, mentions are always indicated with a red badge and item count on the taskbar icon.'}); + } + + if (process.platform === 'win32' || process.platform === 'linux') { + options.push( + {'Flash the taskbar icon when a new message is received.'}); + } + + const settingsPage = { + navbar: { + backgroundColor: '#fff' + }, + close: { + position: 'absolute', + right: '0', + top: '10px', + fontSize: '35px', + fontWeight: 'normal', + color: '#bbb', + cursor: 'pointer' + }, + heading: { + textAlign: 'center', + fontSize: '24px', + margin: '0', + padding: '1em 0' + }, + sectionHeading: { + fontSize: '20px', + margin: '0', + padding: '1em 0' + }, + sectionHeadingLink: { + marginTop: '24px', + display: 'inline-block', + fontSize: '15px' + }, + footer: { + padding: '0.4em 0' + } + }; + + var optionsRow = (options.length > 0) ? ( + + +

{'App options'}

+ { options.map((opt, i) => ( + + {opt} + + )) } + +
+ ) : null; + + return ( +
+ +
+

{'Settings'}

+
+ {'×'} +
+
+
+ + + +

{'Team Management'}

+ + +

+ {'⊞ Add new team'} +

+ +
+ { teamsRow } +
+ { optionsRow } +
+ +
+ + { ' ' } + +
+
+
+ ); + } +}); + +module.exports = SettingsPage; diff --git a/src/browser/components/TabBar.jsx b/src/browser/components/TabBar.jsx new file mode 100644 index 00000000..4282bd2e --- /dev/null +++ b/src/browser/components/TabBar.jsx @@ -0,0 +1,90 @@ +const React = require('react'); +const {Nav, NavItem} = require('react-bootstrap'); + +class TabBar extends React.Component { + render() { + var self = this; + var tabs = this.props.teams.map((team, index) => { + var unreadCount = 0; + var badgeStyle = { + background: '#FF1744', + float: 'right', + color: 'white', + minWidth: '19px', + fontSize: '12px', + textAlign: 'center', + lineHeight: '20px', + height: '19px', + marginLeft: '5px', + borderRadius: '50%' + }; + + if (self.props.unreadCounts[index] > 0) { + unreadCount = self.props.unreadCounts[index]; + } + if (self.props.unreadAtActive[index]) { + unreadCount += 1; + } + + var mentionCount = 0; + if (self.props.mentionCounts[index] > 0) { + mentionCount = self.props.mentionCounts[index]; + } + if (self.props.mentionAtActiveCounts[index] > 0) { + mentionCount += self.props.mentionAtActiveCounts[index]; + } + + var badgeDiv; + if (mentionCount !== 0) { + badgeDiv = ( +
+ {mentionCount} +
); + } + var id = 'teamTabItem' + index; + if (unreadCount === 0) { + return ( + + { team.name } + { ' ' } + { badgeDiv } + ); + } + return ( + + { team.name } + { ' ' } + { badgeDiv } + ); + }); + return ( + + ); + } +} + +TabBar.propTypes = { + activeKey: React.PropTypes.number, + id: React.PropTypes.string, + onSelect: React.PropTypes.func, + teams: React.PropTypes.array +}; + +module.exports = TabBar; diff --git a/src/browser/components/TeamList.jsx b/src/browser/components/TeamList.jsx new file mode 100644 index 00000000..fa64a014 --- /dev/null +++ b/src/browser/components/TeamList.jsx @@ -0,0 +1,107 @@ +const React = require('react'); +const {ListGroup} = require('react-bootstrap'); +const TeamListItem = require('./TeamListItem.jsx'); +const TeamListItemNew = require('./TeamListItemNew.jsx'); + +const TeamList = React.createClass({ + propTypes: { + onTeamsChange: React.PropTypes.func, + showAddTeamForm: React.PropTypes.bool, + teams: React.PropTypes.array + }, + + getInitialState() { + return { + showTeamListItemNew: false, + team: { + url: '', + name: '', + index: false + } + }; + }, + handleTeamRemove(index) { + console.log(index); + var teams = this.props.teams; + teams.splice(index, 1); + this.props.onTeamsChange(teams); + }, + handleTeamAdd(team) { + var 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]) { + teams[team.index].name = team.name; + teams[team.index].url = team.url; + } else { + teams.push(team); + } + + this.setState({ + showTeamListItemNew: false, + team: { + url: '', + name: '', + index: false + } + }); + + this.props.onTeamsChange(teams); + }, + handleTeamEditing(teamName, teamUrl, teamIndex) { + this.setState({ + showTeamListItemNew: true, + team: { + url: teamUrl, + name: teamName, + index: teamIndex + } + }); + }, + render() { + var self = this; + var teamNodes = this.props.teams.map((team, i) => { + function handleTeamRemove() { + self.handleTeamRemove(i); + } + + function handleTeamEditing() { + self.handleTeamEditing(team.name, team.url, i); + } + + return ( + + ); + }); + + var addTeamForm; + if (this.props.showAddTeamForm || this.state.showTeamListItemNew) { + addTeamForm = ( + ); + } else { + addTeamForm = ''; + } + + return ( + + { teamNodes } + { addTeamForm } + + ); + } +}); + +module.exports = TeamList; diff --git a/src/browser/components/TeamListItem.jsx b/src/browser/components/TeamListItem.jsx new file mode 100644 index 00000000..7a5223ae --- /dev/null +++ b/src/browser/components/TeamListItem.jsx @@ -0,0 +1,53 @@ +const React = require('react'); + +class TeamListItem extends React.Component { + constructor(props) { + super(props); + this.handleTeamRemove = this.handleTeamRemove.bind(this); + this.handleTeamEditing = this.handleTeamEditing.bind(this); + } + + handleTeamRemove() { + this.props.onTeamRemove(); + } + handleTeamEditing() { + this.props.onTeamEditing(); + } + render() { + var style = { + left: { + display: 'inline-block' + } + }; + return ( +
+
+

{ this.props.name }

+

+ { this.props.url } +

+
+
+ {'Edit'} + {' - '} + {'Remove'} +
+
+ ); + } +} + +TeamListItem.propTypes = { + name: React.PropTypes.string, + onTeamEditing: React.PropTypes.func, + onTeamRemove: React.PropTypes.func, + url: React.PropTypes.string +}; + +module.exports = TeamListItem; diff --git a/src/browser/components/TeamListItemNew.jsx b/src/browser/components/TeamListItemNew.jsx new file mode 100644 index 00000000..07ed8b23 --- /dev/null +++ b/src/browser/components/TeamListItemNew.jsx @@ -0,0 +1,148 @@ +const React = require('react'); +const {findDOMNode} = require('react-dom'); +const {Button, HelpBlock, ListGroupItem} = require('react-bootstrap'); + +const TeamListItemNew = React.createClass({ + propTypes: { + onTeamAdd: React.PropTypes.func, + teamIndex: React.PropTypes.number, + teamName: React.PropTypes.string, + teamUrl: React.PropTypes.string + }, + + getInitialState() { + return { + name: this.props.teamName, + url: this.props.teamUrl, + index: this.props.teamIndex, + errorMessage: null + }; + }, + handleSubmit(e) { + console.log('submit'); + e.preventDefault(); + const errorMessage = this.getValidationErrorMessage(); + if (errorMessage) { + this.setState({ + errorMessage + }); + return; + } + + this.props.onTeamAdd({ + name: this.state.name.trim(), + url: this.state.url.trim(), + index: this.state.index + }); + + this.setState({ + name: '', + url: '', + index: '', + errorMessage: null + }); + }, + handleNameChange(e) { + console.log('name'); + this.setState({ + name: e.target.value + }); + }, + handleURLChange(e) { + console.log('url'); + this.setState({ + url: e.target.value + }); + }, + + getValidationErrorMessage() { + if (this.state.name.trim() === '') { + return 'Name is required.'; + } else if (this.state.url.trim() === '') { + return 'URL is required.'; + } else if (!(/^https?:\/\/.*/).test(this.state.url.trim())) { + return 'URL should start with http:// or https://.'; + } + return null; + }, + + componentDidMount() { + const inputTeamName = findDOMNode(this.refs.inputTeamName); + const setErrorMessage = () => { + this.setState({ + errorMessage: this.getValidationErrorMessage() + }); + }; + inputTeamName.addEventListener('invalid', setErrorMessage); + const inputTeamURL = findDOMNode(this.refs.inputTeamURL); + inputTeamURL.addEventListener('invalid', setErrorMessage); + }, + + render() { + var existingTeam = false; + if (this.state.name !== '' && this.state.url !== '') { + existingTeam = true; + } + + var btnAddText; + if (existingTeam) { + btnAddText = 'Save'; + } else { + btnAddText = 'Add'; + } + + return ( + + +
+ + { ' ' } + +
+ { ' ' } +
+ + { ' ' } + +
+ { ' ' } + + + { (() => { + if (this.state.errorMessage !== null) { + return ( + + { this.state.errorMessage } + ); + } + return null; + })() } +
+ ); + } +}); + +module.exports = TeamListItemNew; diff --git a/src/browser/index.jsx b/src/browser/index.jsx index bb0dd8a7..2dcc56c7 100644 --- a/src/browser/index.jsx +++ b/src/browser/index.jsx @@ -6,22 +6,8 @@ window.eval = global.eval = () => { const React = require('react'); const ReactDOM = require('react-dom'); -const ReactBootstrap = require('react-bootstrap'); - -const Grid = ReactBootstrap.Grid; -const Row = ReactBootstrap.Row; -const Col = ReactBootstrap.Col; -const Nav = ReactBootstrap.Nav; -const NavItem = ReactBootstrap.NavItem; - -const LoginModal = require('./components/loginModal.jsx'); - -const {remote, ipcRenderer, shell} = require('electron'); -const electronContextMenu = require('electron-context-menu'); - -const osLocale = require('os-locale'); -const fs = require('fs'); -const url = require('url'); +const {remote, ipcRenderer} = require('electron'); +const MainPage = require('./components/MainPage.jsx'); const settings = require('../common/settings'); const badge = require('./js/badge'); @@ -39,635 +25,6 @@ if (config.teams.length === 0) { window.location = 'settings.html'; } -var MainPage = React.createClass({ - getInitialState() { - return { - key: 0, - unreadCounts: new Array(this.props.teams.length), - mentionCounts: new Array(this.props.teams.length), - unreadAtActive: new Array(this.props.teams.length), - mentionAtActiveCounts: new Array(this.props.teams.length), - loginQueue: [] - }; - }, - componentDidMount() { - var self = this; - ipcRenderer.on('login-request', (event, request, authInfo) => { - self.setState({ - loginRequired: true - }); - const loginQueue = self.state.loginQueue; - loginQueue.push({ - request, - authInfo - }); - self.setState({ - loginQueue - }); - }); - - // can't switch tabs sequencially for some reason... - ipcRenderer.on('switch-tab', (event, key) => { - this.handleSelect(key); - }); - ipcRenderer.on('select-next-tab', () => { - this.handleSelect(this.state.key + 1); - }); - ipcRenderer.on('select-previous-tab', () => { - this.handleSelect(this.state.key - 1); - }); - - // reload the activated tab - ipcRenderer.on('reload-tab', () => { - this.refs[`mattermostView${this.state.key}`].reload(); - }); - ipcRenderer.on('clear-cache-and-reload-tab', () => { - this.refs[`mattermostView${this.state.key}`].clearCacheAndReload(); - }); - - // activate search box in current tab - ipcRenderer.on('activate-search-box', () => { - const webview = document.getElementById('mattermostView' + self.state.key); - webview.send('activate-search-box'); - }); - - // activate search box in current chunnel - ipcRenderer.on('activate-search-box-in-channel', () => { - const webview = document.getElementById('mattermostView' + self.state.key); - webview.send('activate-search-box-in-channel'); - }); - - function focusListener() { - self.handleOnTeamFocused(self.state.key); - self.refs[`mattermostView${self.state.key}`].focusOnWebView(); - } - - var currentWindow = remote.getCurrentWindow(); - currentWindow.on('focus', focusListener); - window.addEventListener('beforeunload', () => { - currentWindow.removeListener('focus', focusListener); - }); - - //goBack and goForward - ipcRenderer.on('go-back', () => { - const mattermost = self.refs[`mattermostView${self.state.key}`]; - if (mattermost.canGoBack()) { - mattermost.goBack(); - } - }); - - ipcRenderer.on('go-forward', () => { - const mattermost = self.refs[`mattermostView${self.state.key}`]; - if (mattermost.canGoForward()) { - mattermost.goForward(); - } - }); - }, - 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({ - key: newKey - }); - this.handleOnTeamFocused(newKey); - - var webview = document.getElementById('mattermostView' + newKey); - ipcRenderer.send('update-title', { - title: webview.getTitle() - }); - }, - 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; - unreadCounts[index] = unreadCount; - mentionCounts[index] = mentionCount; - - // Never turn on the unreadAtActive flag at current focused tab. - if (this.state.key !== index || !remote.getCurrentWindow().isFocused()) { - unreadAtActive[index] = unreadAtActive[index] || isUnread; - if (isMentioned) { - mentionAtActiveCounts[index]++; - } - } - this.setState({ - unreadCounts, - mentionCounts, - unreadAtActive, - mentionAtActiveCounts - }); - this.handleUnreadCountTotalChange(); - }, - markReadAtActive(index) { - var unreadAtActive = this.state.unreadAtActive; - var mentionAtActiveCounts = this.state.mentionAtActiveCounts; - unreadAtActive[index] = false; - mentionAtActiveCounts[index] = 0; - this.setState({ - unreadAtActive, - mentionAtActiveCounts - }); - this.handleUnreadCountTotalChange(); - }, - handleUnreadCountTotalChange() { - if (this.props.onUnreadCountChange) { - var allUnreadCount = this.state.unreadCounts.reduce((prev, curr) => { - return prev + curr; - }, 0); - this.state.unreadAtActive.forEach((state) => { - if (state) { - allUnreadCount += 1; - } - }); - var allMentionCount = this.state.mentionCounts.reduce((prev, curr) => { - return prev + curr; - }, 0); - this.state.mentionAtActiveCounts.forEach((count) => { - allMentionCount += count; - }); - 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); - }, - - visibleStyle(visible) { - var visibility = visible ? 'visible' : 'hidden'; - return { - position: 'absolute', - top: (this.props.teams.length > 1) ? 42 : 0, - right: 0, - bottom: 0, - left: 0, - visibility - }; - }, - - 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); - }, - render() { - var self = this; - - var tabsRow; - if (this.props.teams.length > 1) { - tabsRow = ( - - - - ); - } - - var 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; - return ( - ); - }); - var viewsRow = ( - - {views} - ); - - var request = null; - var authServerURL = null; - var 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; - } - return ( -
- - - { tabsRow } - { viewsRow } - -
- ); - } -}); - -var TabBar = React.createClass({ - render() { - var self = this; - var tabs = this.props.teams.map((team, index) => { - var unreadCount = 0; - var badgeStyle = { - background: '#FF1744', - float: 'right', - color: 'white', - minWidth: '19px', - fontSize: '12px', - textAlign: 'center', - lineHeight: '20px', - height: '19px', - marginLeft: '5px', - borderRadius: '50%' - }; - - if (self.props.unreadCounts[index] > 0) { - unreadCount = self.props.unreadCounts[index]; - } - if (self.props.unreadAtActive[index]) { - unreadCount += 1; - } - - var mentionCount = 0; - if (self.props.mentionCounts[index] > 0) { - mentionCount = self.props.mentionCounts[index]; - } - if (self.props.mentionAtActiveCounts[index] > 0) { - mentionCount += self.props.mentionAtActiveCounts[index]; - } - - var badgeDiv; - if (mentionCount !== 0) { - badgeDiv = ( -
- {mentionCount} -
); - } - var id = 'teamTabItem' + index; - if (unreadCount === 0) { - return ( - - { team.name } - { ' ' } - { badgeDiv } - ); - } - return ( - - { team.name } - { ' ' } - { badgeDiv } - ); - }); - return ( - - ); - } -}); - -var MattermostView = React.createClass({ - getInitialState() { - return { - errorInfo: null - }; - }, - handleUnreadCountChange(unreadCount, mentionCount, isUnread, isMentioned) { - if (this.props.onUnreadCountChange) { - this.props.onUnreadCountChange(unreadCount, mentionCount, isUnread, isMentioned); - } - }, - - componentDidMount() { - var self = this; - var webview = ReactDOM.findDOMNode(this.refs.webview); - - // This option allows insecure content, when set to true it is possible to - // load content via HTTP while the mattermost server serves HTTPS - if (config.disablewebsecurity === true) { - webview.setAttribute('webpreferences', 'allowDisplayingInsecureContent'); - } - - webview.addEventListener('did-fail-load', (e) => { - console.log(self.props.name, 'webview did-fail-load', e); - if (e.errorCode === -3) { // An operation was aborted (due to user action). - return; - } - - self.setState({ - errorInfo: e - }); - function reload() { - window.removeEventListener('online', reload); - self.reload(); - } - if (navigator.onLine) { - setTimeout(reload, 30000); - } else { - window.addEventListener('online', reload); - } - }); - - // 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); - if (destURL.protocol !== 'http:' && destURL.protocol !== 'https:') { - ipcRenderer.send('confirm-protocol', destURL.protocol, e.url); - return; - } - if (currentURL.host === destURL.host) { - // New window should disable nodeIntergration. - window.open(e.url, 'Mattermost', 'nodeIntegration=no'); - } else { - // if the link is external, use default browser. - shell.openExternal(e.url); - } - }); - - webview.addEventListener('dom-ready', () => { - // webview.openDevTools(); - - // Use 'Meiryo UI' and 'MS Gothic' to prevent CJK fonts on Windows(JP). - if (process.platform === 'win32') { - function applyCssFile(cssFile) { - fs.readFile(cssFile, 'utf8', (err, data) => { - if (err) { - console.log(err); - return; - } - webview.insertCSS(data); - }); - } - - osLocale((err, locale) => { - if (err) { - console.log(err); - return; - } - if (locale === 'ja_JP') { - applyCssFile(__dirname + '/css/jp_fonts.css'); - } - }); - } - - electronContextMenu({ - window: webview - }); - }); - - webview.addEventListener('ipc-message', (event) => { - switch (event.channel) { - case 'onUnreadCountChange': - var unreadCount = event.args[0]; - var mentionCount = event.args[1]; - - // isUnread and isMentioned is pulse flag. - var isUnread = event.args[2]; - var isMentioned = event.args[3]; - self.handleUnreadCountChange(unreadCount, mentionCount, isUnread, isMentioned); - break; - case 'onNotificationClick': - self.props.onNotificationClick(); - break; - } - }); - - webview.addEventListener('page-title-updated', (event) => { - if (self.props.active) { - ipcRenderer.send('update-title', { - title: event.title - }); - } - }); - - webview.addEventListener('console-message', (e) => { - const message = `[${this.props.name}] ${e.message}`; - switch (e.level) { - case 0: - console.log(message); - break; - case 1: - console.warn(message); - break; - case 2: - console.error(message); - break; - default: - console.log(message); - break; - } - }); - }, - reload() { - this.setState({ - errorInfo: null - }); - var webview = ReactDOM.findDOMNode(this.refs.webview); - webview.reload(); - }, - clearCacheAndReload() { - this.setState({ - errorInfo: null - }); - var webContents = ReactDOM.findDOMNode(this.refs.webview).getWebContents(); - webContents.session.clearCache(() => { - webContents.reload(); - }); - }, - - focusOnWebView() { - const webview = ReactDOM.findDOMNode(this.refs.webview); - if (!webview.getWebContents().isFocused()) { - webview.focus(); - webview.getWebContents().focus(); - } - }, - - canGoBack() { - const webview = ReactDOM.findDOMNode(this.refs.webview); - return webview.getWebContents().canGoBack(); - }, - - canGoForward() { - const webview = ReactDOM.findDOMNode(this.refs.webview); - return webview.getWebContents().canGoForward(); - }, - - goBack() { - const webview = ReactDOM.findDOMNode(this.refs.webview); - webview.getWebContents().goBack(); - }, - - goForward() { - const webview = ReactDOM.findDOMNode(this.refs.webview); - webview.getWebContents().goForward(); - }, - - render() { - const errorView = this.state.errorInfo ? ( - ) : null; - - // 'disablewebsecurity' is necessary to display external images. - // However, it allows also CSS/JavaScript. - // So webview should use 'allowDisplayingInsecureContent' as same as BrowserWindow. - - // Need to keep webview mounted when failed to load. - return ( -
- { errorView } - -
); - } -}); - -// ErrorCode: https://code.google.com/p/chromium/codesearch#chromium/src/net/base/net_error_list.h -const errorPage = { - tableStyle: { - display: 'table', - width: '100%', - height: '100%', - position: 'absolute', - top: '0', - left: '0' - }, - - cellStyle: { - display: 'table-cell', - verticalAlign: 'top', - paddingTop: '2em' - }, - - bullets: { - paddingLeft: '15px', - lineHeight: '1.7' - }, - - techInfo: { - fontSize: '12px', - color: '#aaa' - } -}; - -var ErrorView = React.createClass({ - render() { - return ( - -
-
- - - -

{'Cannot connect to Mattermost'}

-
-

{'We\'re having trouble connecting to Mattermost. If refreshing this page (Ctrl+R or Command+R) does not work please verify that:'}

-
- -
-
- {this.props.errorInfo.errorDescription}{' ('} - {this.props.errorInfo.errorCode }{')'}
- - -
-
-
-
- ); - } -}); - function showUnreadBadgeWindows(unreadCount, mentionCount) { function sendBadge(dataURL, description) { // window.setOverlayIcon() does't work with NativeImage across remote boundaries. @@ -734,6 +91,7 @@ function showUnreadBadge(unreadCount, mentionCount) { ReactDOM.render( , diff --git a/src/browser/settings.jsx b/src/browser/settings.jsx index 8e11e663..8fa6f24d 100644 --- a/src/browser/settings.jsx +++ b/src/browser/settings.jsx @@ -4,666 +4,11 @@ window.eval = global.eval = () => { throw new Error('Sorry, Mattermost does not support window.eval() for security reasons.'); }; -const {remote, ipcRenderer} = require('electron'); -const settings = require('../common/settings'); +const {remote} = require('electron'); const React = require('react'); const ReactDOM = require('react-dom'); -const {Grid, Row, Col, Input, Button, ListGroup, ListGroupItem, HelpBlock, Navbar} = require('react-bootstrap'); -var AutoLaunch = require('auto-launch'); - -var appLauncher = new AutoLaunch({ - name: 'Mattermost', - isHidden: true -}); - -function backToIndex() { - remote.getCurrentWindow().loadURL('file://' + __dirname + '/index.html'); -} - -var SettingsPage = React.createClass({ - getInitialState() { - var initialState; - try { - initialState = settings.readFileSync(this.props.configFile); - } catch (e) { - initialState = settings.loadDefault(); - } - - initialState.showAddTeamForm = false; - initialState.trayWasVisible = remote.getCurrentWindow().trayWasVisible; - - return initialState; - }, - componentDidMount() { - if (process.platform === 'win32' || process.platform === 'linux') { - var self = this; - appLauncher.isEnabled().then((enabled) => { - self.setState({ - autostart: enabled - }); - }); - } - }, - handleTeamsChange(teams) { - this.setState({ - showAddTeamForm: false, - teams - }); - }, - handleSave() { - var config = { - teams: this.state.teams, - hideMenuBar: this.state.hideMenuBar, - showTrayIcon: this.state.showTrayIcon, - trayIconTheme: this.state.trayIconTheme, - disablewebsecurity: this.state.disablewebsecurity, - version: settings.version, - minimizeToTray: this.state.minimizeToTray, - toggleWindowOnTrayIconClick: this.state.toggleWindowOnTrayIconClick, - notifications: { - flashWindow: this.state.notifications.flashWindow - }, - showUnreadBadge: this.state.showUnreadBadge - }; - settings.writeFileSync(this.props.configFile, config); - if (process.platform === 'win32' || process.platform === 'linux') { - var currentWindow = remote.getCurrentWindow(); - currentWindow.setAutoHideMenuBar(config.hideMenuBar); - currentWindow.setMenuBarVisibility(!config.hideMenuBar); - - var autostart = this.state.autostart; - appLauncher.isEnabled().then((enabled) => { - if (enabled && !autostart) { - appLauncher.disable(); - } else if (!enabled && autostart) { - appLauncher.enable(); - } - }); - } - - ipcRenderer.send('update-menu', config); - ipcRenderer.send('update-config'); - - backToIndex(); - }, - handleCancel() { - backToIndex(); - }, - handleChangeDisableWebSecurity() { - this.setState({ - disablewebsecurity: this.refs.disablewebsecurity.getChecked() - }); - }, - handleChangeHideMenuBar() { - this.setState({ - hideMenuBar: this.refs.hideMenuBar.getChecked() - }); - }, - handleChangeShowTrayIcon() { - var shouldShowTrayIcon = this.refs.showTrayIcon.getChecked(); - this.setState({ - showTrayIcon: shouldShowTrayIcon - }); - - if (process.platform === 'darwin' && !shouldShowTrayIcon) { - this.setState({ - minimizeToTray: false - }); - } - }, - handleChangeTrayIconTheme() { - this.setState({ - trayIconTheme: this.refs.trayIconTheme.getValue() - }); - }, - handleChangeAutoStart() { - this.setState({ - autostart: this.refs.autostart.getChecked() - }); - }, - handleChangeMinimizeToTray() { - var shouldMinimizeToTray = - (process.platform !== 'darwin' || this.refs.showTrayIcon.getChecked()) && - this.refs.minimizeToTray.getChecked(); - - this.setState({ - minimizeToTray: shouldMinimizeToTray - }); - }, - handleChangeToggleWindowOnTrayIconClick() { - this.setState({ - toggleWindowOnTrayIconClick: this.refs.toggleWindowOnTrayIconClick.getChecked() - }); - }, - toggleShowTeamForm() { - this.setState({ - showAddTeamForm: !this.state.showAddTeamForm - }); - }, - handleFlashWindow() { - this.setState({ - notifications: { - flashWindow: this.refs.flashWindow.getChecked() ? 2 : 0 - } - }); - }, - handleShowUnreadBadge() { - this.setState({ - showUnreadBadge: this.refs.showUnreadBadge.getChecked() - }); - }, - render() { - var teamsRow = ( - - - - - - ); - - var options = []; - if (process.platform === 'win32' || process.platform === 'linux') { - options.push( - ); - } - if (process.platform === 'darwin' || process.platform === 'linux') { - options.push( - ); - } - if (process.platform === 'linux') { - options.push( - - - - ); - } - options.push( - ); - - //OSX 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') { - options.push( - ); - } - - if (process.platform === 'darwin' || process.platform === 'linux') { - options.push( - ); - } - - if (process.platform === 'win32') { - options.push( - ); - } - - if (process.platform === 'darwin' || process.platform === 'win32') { - options.push( - ); - } - - if (process.platform === 'win32' || process.platform === 'linux') { - options.push( - ); - } - - const settingsPage = { - navbar: { - backgroundColor: '#fff' - }, - close: { - position: 'absolute', - right: '0', - top: '10px', - fontSize: '35px', - fontWeight: 'normal', - color: '#bbb', - cursor: 'pointer' - }, - heading: { - textAlign: 'center', - fontSize: '24px', - margin: '0', - padding: '1em 0' - }, - sectionHeading: { - fontSize: '20px', - margin: '0', - padding: '1em 0' - }, - sectionHeadingLink: { - marginTop: '24px', - display: 'inline-block', - fontSize: '15px' - }, - footer: { - padding: '0.4em 0' - } - }; - - var optionsRow = (options.length > 0) ? ( - - -

{'App options'}

- { options } - -
- ) : null; - - return ( -
- -
-

{'Settings'}

-
- {'×'} -
-
-
- - - -

{'Team Management'}

- - -

- {'⊞ Add new team'} -

- -
- { teamsRow } -
- { optionsRow } -
- -
- - { ' ' } - -
-
-
- ); - } -}); - -var TeamList = React.createClass({ - getInitialState() { - return { - showTeamListItemNew: false, - team: { - url: '', - name: '', - index: false - } - }; - }, - handleTeamRemove(index) { - console.log(index); - var teams = this.props.teams; - teams.splice(index, 1); - this.props.onTeamsChange(teams); - }, - handleTeamAdd(team) { - var 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]) { - teams[team.index].name = team.name; - teams[team.index].url = team.url; - } else { - teams.push(team); - } - - this.setState({ - showTeamListItemNew: false, - team: { - url: '', - name: '', - index: false - } - }); - - this.props.onTeamsChange(teams); - }, - handleTeamEditing(teamName, teamUrl, teamIndex) { - this.setState({ - showTeamListItemNew: true, - team: { - url: teamUrl, - name: teamName, - index: teamIndex - } - }); - }, - render() { - var self = this; - var teamNodes = this.props.teams.map((team, i) => { - function handleTeamRemove() { - self.handleTeamRemove(i); - } - - function handleTeamEditing() { - self.handleTeamEditing(team.name, team.url, i); - } - - return ( - - ); - }); - - var addTeamForm; - if (this.props.showAddTeamForm || this.state.showTeamListItemNew) { - addTeamForm = ( - ); - } else { - addTeamForm = ''; - } - - return ( - - { teamNodes } - { addTeamForm } - - ); - } -}); - -var TeamListItem = React.createClass({ - handleTeamRemove() { - this.props.onTeamRemove(); - }, - handleTeamEditing() { - this.props.onTeamEditing(); - }, - render() { - var style = { - left: { - display: 'inline-block' - } - }; - return ( -
-
-

{ this.props.name }

-

- { this.props.url } -

-
-
- {'Edit'} - {' - '} - {'Remove'} -
-
- ); - } -}); - -var TeamListItemNew = React.createClass({ - getInitialState() { - return { - name: this.props.teamName, - url: this.props.teamUrl, - index: this.props.teamIndex, - errorMessage: null - }; - }, - handleSubmit(e) { - console.log('submit'); - e.preventDefault(); - const errorMessage = this.getValidationErrorMessage(); - if (errorMessage) { - this.setState({ - errorMessage - }); - return; - } - - this.props.onTeamAdd({ - name: this.state.name.trim(), - url: this.state.url.trim(), - index: this.state.index - }); - - this.setState({ - name: '', - url: '', - index: '', - errorMessage: null - }); - }, - handleNameChange(e) { - console.log('name'); - this.setState({ - name: e.target.value - }); - }, - handleURLChange(e) { - console.log('url'); - this.setState({ - url: e.target.value - }); - }, - - getValidationErrorMessage() { - if (this.state.name.trim() === '') { - return 'Name is required.'; - } else if (this.state.url.trim() === '') { - return 'URL is required.'; - } else if (!(/^https?:\/\/.*/).test(this.state.url.trim())) { - return 'URL should start with http:// or https://.'; - } - return null; - }, - - componentDidMount() { - const inputTeamName = ReactDOM.findDOMNode(this.refs.inputTeamName); - const setErrorMessage = () => { - this.setState({ - errorMessage: this.getValidationErrorMessage() - }); - }; - inputTeamName.addEventListener('invalid', setErrorMessage); - const inputTeamURL = ReactDOM.findDOMNode(this.refs.inputTeamURL); - inputTeamURL.addEventListener('invalid', setErrorMessage); - }, - - render() { - var existingTeam = false; - if (this.state.name !== '' && this.state.url !== '') { - existingTeam = true; - } - - var btnAddText; - if (existingTeam) { - btnAddText = 'Save'; - } else { - btnAddText = 'Add'; - } - - return ( - -
-
- - { ' ' } - -
- { ' ' } -
- - { ' ' } - -
- { ' ' } - -
- { (() => { - if (this.state.errorMessage !== null) { - return ( - - { this.state.errorMessage } - ); - } - return null; - })() } -
- ); - } -}); +const SettingsPage = require('./components/SettingsPage.jsx'); var configFile = remote.getGlobal('config-file'); diff --git a/src/package.json b/src/package.json index 4af7c912..33521b60 100644 --- a/src/package.json +++ b/src/package.json @@ -21,7 +21,7 @@ "electron-squirrel-startup": "^1.0.0", "os-locale": "^1.4.0", "react": "^15.3.0", - "react-bootstrap": "~0.29.0", + "react-bootstrap": "~0.30.6", "react-dom": "^15.3.0", "yargs": "^3.32.0" } diff --git a/test/specs/browser/settings_test.js b/test/specs/browser/settings_test.js index ff1e07a5..0859abcc 100644 --- a/test/specs/browser/settings_test.js +++ b/test/specs/browser/settings_test.js @@ -65,9 +65,9 @@ describe('browser/settings.html', function desc() { return this.app.client. loadSettingsPage(). scroll('#inputHideMenuBar'). - isSelected('#inputHideMenuBar input').then((isSelected) => { + isSelected('#inputHideMenuBar').then((isSelected) => { if (isSelected !== v) { - return this.app.client.click('#inputHideMenuBar input'); + return this.app.client.click('#inputHideMenuBar'); } return true; }). @@ -83,7 +83,7 @@ describe('browser/settings.html', function desc() { return this.app.client. // confirm actual behavior browserWindow.isMenuBarAutoHide().should.eventually.equal(v). loadSettingsPage(). - isSelected('#inputHideMenuBar input').should.eventually.equal(v); + isSelected('#inputHideMenuBar').should.eventually.equal(v); }); }); }); @@ -98,9 +98,9 @@ describe('browser/settings.html', function desc() { return this.app.client. loadSettingsPage(). scroll('#inputDisableWebSecurity'). - isSelected('#inputDisableWebSecurity input').then((isSelected) => { + isSelected('#inputDisableWebSecurity').then((isSelected) => { if (isSelected !== v) { - return this.app.client.click('#inputDisableWebSecurity input'); + return this.app.client.click('#inputDisableWebSecurity'); } return true; }). @@ -125,7 +125,7 @@ describe('browser/settings.html', function desc() { }); }). loadSettingsPage(). - isSelected('#inputDisableWebSecurity input').should.eventually.equal(v); + isSelected('#inputDisableWebSecurity').should.eventually.equal(v); }); }); });