Merge pull request #371 from yuya-oc/latest-react-bootstrap

Latest react bootstrap
This commit is contained in:
Jason Blais
2016-11-30 12:30:34 -05:00
committed by GitHub
14 changed files with 1424 additions and 1325 deletions

View File

@@ -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
}
}

View File

@@ -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 (
<Grid
id={this.props.id}
style={this.props.style}
>
<div style={errorPage.tableStyle}>
<div style={errorPage.cellStyle}>
<Row>
<Col
xs={0}
sm={1}
md={1}
lg={2}
/>
<Col
xs={12}
sm={10}
md={10}
lg={8}
>
<h2>{'Cannot connect to Mattermost'}</h2>
<hr/>
<p>{'We\'re having trouble connecting to Mattermost. If refreshing this page (Ctrl+R or Command+R) does not work please verify that:'}</p>
<br/>
<ul style={errorPage.bullets}>
<li>{'Your computer is connected to the internet.'}</li>
<li>{'The Mattermost URL '}
<a href={this.props.errorInfo.validatedURL}>
{this.props.errorInfo.validatedURL}
</a>{' is correct.'}</li>
<li>{'You can reach '}
<a href={this.props.errorInfo.validatedURL}>
{this.props.errorInfo.validatedURL}
</a>{' from a browser window.'}</li>
</ul>
<br/>
<div style={errorPage.techInfo}>
{this.props.errorInfo.errorDescription}{' ('}
{this.props.errorInfo.errorCode }{')'}</div>
</Col>
<Col
xs={0}
sm={1}
md={1}
lg={2}
/>
</Row>
</div>
</div>
</Grid>
);
}
}
ErrorView.propTypes = {
errorInfo: React.PropTypes.object,
id: React.PropTypes.number,
style: React.PropTypes.object
};
module.exports = ErrorView;

View File

@@ -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 }
</p>
<Form
horizontal
horizontal={true}
onSubmit={this.handleSubmit}
>
<FormGroup>
@@ -85,6 +83,15 @@ const LoginModal = React.createClass({
</Modal>
);
}
});
}
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;

View File

@@ -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 = (
<Row>
<TabBar
id='tabBar'
teams={this.props.teams}
unreadCounts={this.state.unreadCounts}
mentionCounts={this.state.mentionCounts}
unreadAtActive={this.state.unreadAtActive}
mentionAtActiveCounts={this.state.mentionAtActiveCounts}
activeKey={this.state.key}
onSelect={this.handleSelect}
/>
</Row>
);
}
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 (
<MattermostView
key={id}
id={id}
style={self.visibleStyle(isActive)}
src={team.url}
name={team.name}
disablewebsecurity={this.props.disablewebsecurity}
onUnreadCountChange={handleUnreadCountChange}
onNotificationClick={handleNotificationClick}
ref={id}
active={isActive}
/>);
});
var viewsRow = (
<Row>
{views}
</Row>);
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 (
<div>
<LoginModal
show={this.state.loginQueue.length !== 0}
request={request}
authInfo={authInfo}
authServerURL={authServerURL}
onLogin={this.handleLogin}
onCancel={this.handleLoginCancel}
/>
<Grid fluid={true}>
{ tabsRow }
{ viewsRow }
</Grid>
</div>
);
}
});
module.exports = MainPage;

View File

@@ -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 ? (
<ErrorView
id={this.props.id + '-fail'}
style={this.props.style}
className='errorView'
errorInfo={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 (
<div>
{ errorView }
<webview
id={this.props.id}
className='mattermostView'
style={this.props.style}
preload='webview/mattermost.js'
src={this.props.src}
ref='webview'
nodeintegration='false'
/>
</div>);
}
});
module.exports = MattermostView;

View File

@@ -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 = (
<Row>
<Col md={12}>
<TeamList
teams={this.state.teams}
showAddTeamForm={this.state.showAddTeamForm}
onTeamsChange={this.handleTeamsChange}
/>
</Col>
</Row>
);
var options = [];
if (process.platform === 'win32' || process.platform === 'linux') {
options.push(
<Checkbox
key='inputHideMenuBar'
id='inputHideMenuBar'
ref='hideMenuBar'
checked={this.state.hideMenuBar}
onChange={this.handleChangeHideMenuBar}
>{'Hide menu bar (Press Alt to show menu bar)'}</Checkbox>);
}
if (process.platform === 'darwin' || process.platform === 'linux') {
options.push(
<Checkbox
key='inputShowTrayIcon'
id='inputShowTrayIcon'
ref='showTrayIcon'
checked={this.state.showTrayIcon}
onChange={this.handleChangeShowTrayIcon}
>{process.platform === 'darwin' ?
'Show icon on menu bar (need to restart the application)' :
'Show icon in notification area (need to restart the application)'}</Checkbox>);
}
if (process.platform === 'linux') {
options.push(
<Checkbox
key='inputTrayIconTheme'
ref='trayIconTheme'
type='select'
value={this.state.trayIconTheme}
onChange={this.handleChangeTrayIconTheme}
>{'Icon theme (Need to restart the application)'}
<option value='light'>{'Light'}</option>
<option value='dark'>{'Dark'}</option>
</Checkbox>);
}
options.push(
<Checkbox
key='inputDisableWebSecurity'
id='inputDisableWebSecurity'
ref='disablewebsecurity'
checked={this.state.disablewebsecurity}
onChange={this.handleChangeDisableWebSecurity}
>{'Allow mixed content (Enabling allows both secure and insecure content, images and scripts to render and execute. Disabling allows only secure content.)'}</Checkbox>);
//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(
<Checkbox
key='inputAutoStart'
id='inputAutoStart'
ref='autostart'
checked={this.state.autostart}
onChange={this.handleChangeAutoStart}
>{'Start app on login.'}</Checkbox>);
}
if (process.platform === 'darwin' || process.platform === 'linux') {
options.push(
<Checkbox
key='inputMinimizeToTray'
id='inputMinimizeToTray'
ref='minimizeToTray'
disabled={!this.state.showTrayIcon || !this.state.trayWasVisible}
checked={this.state.minimizeToTray}
onChange={this.handleChangeMinimizeToTray}
>{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)'}</Checkbox>);
}
if (process.platform === 'win32') {
options.push(
<Checkbox
key='inputToggleWindowOnTrayIconClick'
id='inputToggleWindowOnTrayIconClick'
ref='toggleWindowOnTrayIconClick'
checked={this.state.toggleWindowOnTrayIconClick}
onChange={this.handleChangeToggleWindowOnTrayIconClick}
>{'Toggle window visibility when clicking on the tray icon.'}</Checkbox>);
}
if (process.platform === 'darwin' || process.platform === 'win32') {
options.push(
<Checkbox
key='inputShowUnreadBadge'
id='inputShowUnreadBadge'
ref='showUnreadBadge'
checked={this.state.showUnreadBadge}
onChange={this.handleShowUnreadBadge}
>{'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.'}</Checkbox>);
}
if (process.platform === 'win32' || process.platform === 'linux') {
options.push(
<Checkbox
key='flashWindow'
id='inputflashWindow'
ref='flashWindow'
checked={this.state.notifications.flashWindow === 2}
onChange={this.handleFlashWindow}
>{'Flash the taskbar icon when a new message is received.'}</Checkbox>);
}
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) ? (
<Row>
<Col md={12}>
<h2 style={settingsPage.sectionHeading}>{'App options'}</h2>
{ options.map((opt, i) => (
<FormGroup key={`fromGroup${i}`}>
{opt}
</FormGroup>
)) }
</Col>
</Row>
) : null;
return (
<div>
<Navbar
className='navbar-fixed-top'
style={settingsPage.navbar}
>
<div style={{position: 'relative'}}>
<h1 style={settingsPage.heading}>{'Settings'}</h1>
<div
style={settingsPage.close}
onClick={this.handleCancel}
>
<span>{'×'}</span>
</div>
</div>
</Navbar>
<Grid
className='settingsPage'
style={{padding: '100px 15px'}}
>
<Row>
<Col
md={10}
xs={8}
>
<h2 style={settingsPage.sectionHeading}>{'Team Management'}</h2>
</Col>
<Col
md={2}
xs={4}
>
<p className='text-right'>
<a
style={settingsPage.sectionHeadingLink}
href='#'
onClick={this.toggleShowTeamForm}
>{'⊞ Add new team'}</a>
</p>
</Col>
</Row>
{ teamsRow }
<hr/>
{ optionsRow }
</Grid>
<Navbar className='navbar-fixed-bottom'>
<div
className='text-right'
style={settingsPage.footer}
>
<Button
id='btnCancel'
className='btn-link'
onClick={this.handleCancel}
>{'Cancel'}</Button>
{ ' ' }
<Button
id='btnSave'
className='navbar-btn'
bsStyle='primary'
onClick={this.handleSave}
disabled={this.state.teams.length === 0}
>{'Save'}</Button>
</div>
</Navbar>
</div>
);
}
});
module.exports = SettingsPage;

View File

@@ -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 = (
<div style={badgeStyle}>
{mentionCount}
</div>);
}
var id = 'teamTabItem' + index;
if (unreadCount === 0) {
return (
<NavItem
className='teamTabItem'
key={id}
id={id}
eventKey={index}
>
{ team.name }
{ ' ' }
{ badgeDiv }
</NavItem>);
}
return (
<NavItem
className='teamTabItem'
key={id}
id={id}
eventKey={index}
>
<b>{ team.name }</b>
{ ' ' }
{ badgeDiv }
</NavItem>);
});
return (
<Nav
id={this.props.id}
bsStyle='tabs'
activeKey={this.props.activeKey}
onSelect={this.props.onSelect}
>
{ tabs }
</Nav>
);
}
}
TabBar.propTypes = {
activeKey: React.PropTypes.number,
id: React.PropTypes.string,
onSelect: React.PropTypes.func,
teams: React.PropTypes.array
};
module.exports = TabBar;

View File

@@ -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 (
<TeamListItem
index={i}
key={'teamListItem' + i}
name={team.name}
url={team.url}
onTeamRemove={handleTeamRemove}
onTeamEditing={handleTeamEditing}
/>
);
});
var addTeamForm;
if (this.props.showAddTeamForm || this.state.showTeamListItemNew) {
addTeamForm = (
<TeamListItemNew
key={this.state.team.index}
onTeamAdd={this.handleTeamAdd}
teamIndex={this.state.team.index}
teamName={this.state.team.name}
teamUrl={this.state.team.url}
/>);
} else {
addTeamForm = '';
}
return (
<ListGroup className='teamList'>
{ teamNodes }
{ addTeamForm }
</ListGroup>
);
}
});
module.exports = TeamList;

View File

@@ -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 (
<div className='teamListItem list-group-item'>
<div style={style.left}>
<h4 className='list-group-item-heading'>{ this.props.name }</h4>
<p className='list-group-item-text'>
{ this.props.url }
</p>
</div>
<div className='pull-right'>
<a
href='#'
onClick={this.handleTeamEditing}
>{'Edit'}</a>
{' - '}
<a
href='#'
onClick={this.handleTeamRemove}
>{'Remove'}</a>
</div>
</div>
);
}
}
TeamListItem.propTypes = {
name: React.PropTypes.string,
onTeamEditing: React.PropTypes.func,
onTeamRemove: React.PropTypes.func,
url: React.PropTypes.string
};
module.exports = TeamListItem;

View File

@@ -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 (
<ListGroupItem>
<form
className='form-inline'
onSubmit={this.handleSubmit}
>
<div className='form-group'>
<label htmlFor='inputTeamName'>{'Name'}</label>
{ ' ' }
<input
type='text'
required={true}
className='form-control'
id='inputTeamName'
ref='inputTeamName'
placeholder='Example team'
value={this.state.name}
onChange={this.handleNameChange}
/>
</div>
{ ' ' }
<div className='form-group'>
<label htmlFor='inputTeamURL'>{'URL'}</label>
{ ' ' }
<input
type='url'
required={true}
className='form-control'
id='inputTeamURL'
ref='inputTeamURL'
placeholder='https://example.com/team'
value={this.state.url}
onChange={this.handleURLChange}
/>
</div>
{ ' ' }
<Button type='submit'>
{ btnAddText }
</Button>
</form>
{ (() => {
if (this.state.errorMessage !== null) {
return (
<HelpBlock style={{color: '#777777'}}>
{ this.state.errorMessage }
</HelpBlock>);
}
return null;
})() }
</ListGroupItem>
);
}
});
module.exports = TeamListItemNew;

View File

@@ -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 = (
<Row>
<TabBar
id='tabBar'
teams={this.props.teams}
unreadCounts={this.state.unreadCounts}
mentionCounts={this.state.mentionCounts}
unreadAtActive={this.state.unreadAtActive}
mentionAtActiveCounts={this.state.mentionAtActiveCounts}
activeKey={this.state.key}
onSelect={this.handleSelect}
/>
</Row>
);
}
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 (
<MattermostView
key={id}
id={id}
style={self.visibleStyle(isActive)}
src={team.url}
name={team.name}
onUnreadCountChange={handleUnreadCountChange}
onNotificationClick={handleNotificationClick}
ref={id}
active={isActive}
/>);
});
var viewsRow = (
<Row>
{views}
</Row>);
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 (
<div>
<LoginModal
show={this.state.loginQueue.length !== 0}
request={request}
authInfo={authInfo}
authServerURL={authServerURL}
onLogin={this.handleLogin}
onCancel={this.handleLoginCancel}
/>
<Grid fluid>
{ tabsRow }
{ viewsRow }
</Grid>
</div>
);
}
});
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 = (
<div style={badgeStyle}>
{mentionCount}
</div>);
}
var id = 'teamTabItem' + index;
if (unreadCount === 0) {
return (
<NavItem
className='teamTabItem'
key={id}
id={id}
eventKey={index}
>
{ team.name }
{ ' ' }
{ badgeDiv }
</NavItem>);
}
return (
<NavItem
className='teamTabItem'
key={id}
id={id}
eventKey={index}
>
<b>{ team.name }</b>
{ ' ' }
{ badgeDiv }
</NavItem>);
});
return (
<Nav
id={this.props.id}
bsStyle='tabs'
activeKey={this.props.activeKey}
onSelect={this.props.onSelect}
>
{ tabs }
</Nav>
);
}
});
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 ? (
<ErrorView
id={this.props.id + '-fail'}
style={this.props.style}
className='errorView'
errorInfo={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 (
<div>
{ errorView }
<webview
id={this.props.id}
className='mattermostView'
style={this.props.style}
preload='webview/mattermost.js'
src={this.props.src}
ref='webview'
nodeintegration='false'
/>
</div>);
}
});
// 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 (
<Grid
id={this.props.id}
style={this.props.style}
>
<div style={errorPage.tableStyle}>
<div style={errorPage.cellStyle}>
<Row>
<Col
xs={0}
sm={1}
md={1}
lg={2}
/>
<Col
xs={12}
sm={10}
md={10}
lg={8}
>
<h2>{'Cannot connect to Mattermost'}</h2>
<hr/>
<p>{'We\'re having trouble connecting to Mattermost. If refreshing this page (Ctrl+R or Command+R) does not work please verify that:'}</p>
<br/>
<ul style={errorPage.bullets}>
<li>{'Your computer is connected to the internet.'}</li>
<li>{'The Mattermost URL '}
<a href={this.props.errorInfo.validatedURL}>
{this.props.errorInfo.validatedURL}
</a>{' is correct.'}</li>
<li>{'You can reach '}
<a href={this.props.errorInfo.validatedURL}>
{this.props.errorInfo.validatedURL}
</a>{' from a browser window.'}</li>
</ul>
<br/>
<div style={errorPage.techInfo}>
{this.props.errorInfo.errorDescription}{' ('}
{this.props.errorInfo.errorCode }{')'}</div>
</Col>
<Col
xs={0}
sm={1}
md={1}
lg={2}
/>
</Row>
</div>
</div>
</Grid>
);
}
});
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(
<MainPage
disablewebsecurity={config.disablewebsecurity}
teams={config.teams}
onUnreadCountChange={showUnreadBadge}
/>,

View File

@@ -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 = (
<Row>
<Col md={12}>
<TeamList
teams={this.state.teams}
showAddTeamForm={this.state.showAddTeamForm}
onTeamsChange={this.handleTeamsChange}
/>
</Col>
</Row>
);
var options = [];
if (process.platform === 'win32' || process.platform === 'linux') {
options.push(
<Input
key='inputHideMenuBar'
id='inputHideMenuBar'
ref='hideMenuBar'
type='checkbox'
label='Hide menu bar (Press Alt to show menu bar)'
checked={this.state.hideMenuBar}
onChange={this.handleChangeHideMenuBar}
/>);
}
if (process.platform === 'darwin' || process.platform === 'linux') {
options.push(
<Input
key='inputShowTrayIcon'
id='inputShowTrayIcon'
ref='showTrayIcon'
type='checkbox'
label={process.platform === 'darwin' ?
'Show icon on menu bar (need to restart the application)' :
'Show icon in notification area (need to restart the application)'}
checked={this.state.showTrayIcon}
onChange={this.handleChangeShowTrayIcon}
/>);
}
if (process.platform === 'linux') {
options.push(
<Input
key='inputTrayIconTheme'
ref='trayIconTheme'
type='select'
label='Icon theme (Need to restart the application)'
value={this.state.trayIconTheme}
onChange={this.handleChangeTrayIconTheme}
>
<option value='light'>{'Light'}</option>
<option value='dark'>{'Dark'}</option>
</Input>);
}
options.push(
<Input
key='inputDisableWebSecurity'
id='inputDisableWebSecurity'
ref='disablewebsecurity'
type='checkbox'
label='Allow mixed content (Enabling allows both secure and insecure content, images and scripts to render and execute. Disabling allows only secure content.)'
checked={this.state.disablewebsecurity}
onChange={this.handleChangeDisableWebSecurity}
/>);
//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(
<Input
key='inputAutoStart'
id='inputAutoStart'
ref='autostart'
type='checkbox'
label='Start app on login.'
checked={this.state.autostart}
onChange={this.handleChangeAutoStart}
/>);
}
if (process.platform === 'darwin' || process.platform === 'linux') {
options.push(
<Input
key='inputMinimizeToTray'
id='inputMinimizeToTray'
ref='minimizeToTray'
type='checkbox'
label={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)'}
disabled={!this.state.showTrayIcon || !this.state.trayWasVisible}
checked={this.state.minimizeToTray}
onChange={this.handleChangeMinimizeToTray}
/>);
}
if (process.platform === 'win32') {
options.push(
<Input
key='inputToggleWindowOnTrayIconClick'
id='inputToggleWindowOnTrayIconClick'
ref='toggleWindowOnTrayIconClick'
type='checkbox'
label='Toggle window visibility when clicking on the tray icon.'
checked={this.state.toggleWindowOnTrayIconClick}
onChange={this.handleChangeToggleWindowOnTrayIconClick}
/>);
}
if (process.platform === 'darwin' || process.platform === 'win32') {
options.push(
<Input
key='inputShowUnreadBadge'
id='inputShowUnreadBadge'
ref='showUnreadBadge'
type='checkbox'
label='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.'
checked={this.state.showUnreadBadge}
onChange={this.handleShowUnreadBadge}
/>);
}
if (process.platform === 'win32' || process.platform === 'linux') {
options.push(
<Input
key='flashWindow'
id='inputflashWindow'
ref='flashWindow'
type='checkbox'
label='Flash the taskbar icon when a new message is received.'
checked={this.state.notifications.flashWindow === 2}
onChange={this.handleFlashWindow}
/>);
}
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) ? (
<Row>
<Col md={12}>
<h2 style={settingsPage.sectionHeading}>{'App options'}</h2>
{ options }
</Col>
</Row>
) : null;
return (
<div>
<Navbar
className='navbar-fixed-top'
style={settingsPage.navbar}
>
<div style={{position: 'relative'}}>
<h1 style={settingsPage.heading}>{'Settings'}</h1>
<div
style={settingsPage.close}
onClick={this.handleCancel}
>
<span>{'×'}</span>
</div>
</div>
</Navbar>
<Grid
className='settingsPage'
style={{padding: '100px 15px'}}
>
<Row>
<Col
md={10}
xs={8}
>
<h2 style={settingsPage.sectionHeading}>{'Team Management'}</h2>
</Col>
<Col
md={2}
xs={4}
>
<p className='text-right'>
<a
style={settingsPage.sectionHeadingLink}
href='#'
onClick={this.toggleShowTeamForm}
>{'⊞ Add new team'}</a>
</p>
</Col>
</Row>
{ teamsRow }
<hr/>
{ optionsRow }
</Grid>
<Navbar className='navbar-fixed-bottom'>
<div
className='text-right'
style={settingsPage.footer}
>
<button
id='btnCancel'
className='btn btn-link'
onClick={this.handleCancel}
>{'Cancel'}</button>
{ ' ' }
<button
id='btnSave'
className='btn btn-primary navbar-btn'
bsStyle='primary'
onClick={this.handleSave}
disabled={this.state.teams.length === 0}
>{'Save'}</button>
</div>
</Navbar>
</div>
);
}
});
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 (
<TeamListItem
index={i}
key={'teamListItem' + i}
name={team.name}
url={team.url}
onTeamRemove={handleTeamRemove}
onTeamEditing={handleTeamEditing}
/>
);
});
var addTeamForm;
if (this.props.showAddTeamForm || this.state.showTeamListItemNew) {
addTeamForm = (
<TeamListItemNew
key={this.state.team.index}
onTeamAdd={this.handleTeamAdd}
teamIndex={this.state.team.index}
teamName={this.state.team.name}
teamUrl={this.state.team.url}
/>);
} else {
addTeamForm = '';
}
return (
<ListGroup class='teamList'>
{ teamNodes }
{ addTeamForm }
</ListGroup>
);
}
});
var TeamListItem = React.createClass({
handleTeamRemove() {
this.props.onTeamRemove();
},
handleTeamEditing() {
this.props.onTeamEditing();
},
render() {
var style = {
left: {
display: 'inline-block'
}
};
return (
<div className='teamListItem list-group-item'>
<div style={style.left}>
<h4 className='list-group-item-heading'>{ this.props.name }</h4>
<p className='list-group-item-text'>
{ this.props.url }
</p>
</div>
<div className='pull-right'>
<a
href='#'
onClick={this.handleTeamEditing}
>{'Edit'}</a>
{' - '}
<a
href='#'
onClick={this.handleTeamRemove}
>{'Remove'}</a>
</div>
</div>
);
}
});
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 (
<ListGroupItem>
<form
className='form-inline'
onSubmit={this.handleSubmit}
>
<div className='form-group'>
<label htmlFor='inputTeamName'>{'Name'}</label>
{ ' ' }
<input
type='text'
required
className='form-control'
id='inputTeamName'
ref='inputTeamName'
placeholder='Example team'
value={this.state.name}
onChange={this.handleNameChange}
/>
</div>
{ ' ' }
<div className='form-group'>
<label htmlFor='inputTeamURL'>{'URL'}</label>
{ ' ' }
<input
type='url'
required
className='form-control'
id='inputTeamURL'
ref='inputTeamURL'
placeholder='https://example.com/team'
value={this.state.url}
onChange={this.handleURLChange}
/>
</div>
{ ' ' }
<Button type='submit'>
{ btnAddText }
</Button>
</form>
{ (() => {
if (this.state.errorMessage !== null) {
return (
<HelpBlock style={{color: '#777777'}}>
{ this.state.errorMessage }
</HelpBlock>);
}
return null;
})() }
</ListGroupItem>
);
}
});
const SettingsPage = require('./components/SettingsPage.jsx');
var configFile = remote.getGlobal('config-file');

View File

@@ -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"
}

View File

@@ -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);
});
});
});