mattermost-desktop/src/browser/index.jsx

508 lines
16 KiB
React
Raw Normal View History

2015-12-22 07:34:24 -08:00
'use strict';
2016-01-30 07:50:43 -08:00
const React = require('react');
const ReactDOM = require('react-dom');
const ReactBootstrap = require('react-bootstrap');
2015-12-22 07:34:24 -08:00
const Grid = ReactBootstrap.Grid;
const Row = ReactBootstrap.Row;
const Col = ReactBootstrap.Col;
2015-12-22 23:21:33 -08:00
const Nav = ReactBootstrap.Nav;
const NavItem = ReactBootstrap.NavItem;
const Badge = ReactBootstrap.Badge;
const ListGroup = ReactBootstrap.ListGroup;
const ListGroupItem = ReactBootstrap.ListGroupItem;
2015-12-22 07:34:24 -08:00
2016-04-22 08:50:57 -07:00
const LoginModal = require('./components/loginModal.jsx');
2015-12-22 07:34:24 -08:00
const electron = require('electron');
const remote = electron.remote;
2016-04-21 06:43:18 -07:00
const ipcRenderer = electron.ipcRenderer;
2015-12-22 07:34:24 -08:00
const osLocale = require('os-locale');
const fs = require('fs');
const url = require('url');
2015-12-23 00:06:17 -08:00
const path = require('path');
2015-12-22 07:34:24 -08:00
const settings = require('../common/settings');
remote.getCurrentWindow().removeAllListeners('focus');
// New window should disable nodeIntergration.
const originalWindowOpen = window.open;
window.open = function(url, name, features) {
var f = 'nodeIntegration=no';
if (features !== null) {
f += ',' + features;
}
originalWindowOpen(url, name, f);
};
2015-12-22 07:34:24 -08:00
var MainPage = React.createClass({
getInitialState: function() {
return {
2015-12-22 23:21:33 -08:00
key: 0,
unreadCounts: new Array(this.props.teams.length),
mentionCounts: new Array(this.props.teams.length),
unreadAtActive: new Array(this.props.teams.length),
2016-04-22 08:50:57 -07:00
mentionAtActiveCounts: new Array(this.props.teams.length),
loginQueue: []
2015-12-22 07:34:24 -08:00
};
},
componentDidMount: function() {
var thisObj = this;
2016-04-22 08:50:57 -07:00
ipcRenderer.on('login-request', function(event, request, authInfo) {
thisObj.setState({
loginRequired: true
});
const loginQueue = thisObj.state.loginQueue;
loginQueue.push({
request: request,
authInfo: authInfo
});
thisObj.setState({
loginQueue: loginQueue
});
});
var focusListener = function() {
var webview = document.getElementById('mattermostView' + thisObj.state.key);
webview.focus();
2016-01-18 20:15:24 -08:00
thisObj.handleOnTeamFocused(thisObj.state.key);
};
var currentWindow = remote.getCurrentWindow();
currentWindow.on('focus', focusListener);
window.addEventListener('beforeunload', function() {
currentWindow.removeListener('focus', focusListener);
});
},
2015-12-22 07:34:24 -08:00
handleSelect: function(key) {
this.setState({
2015-12-22 23:21:33 -08:00
key: key
});
this.handleOnTeamFocused(key);
2015-12-22 23:21:33 -08:00
},
handleUnreadCountChange: function(index, unreadCount, mentionCount, isUnread, isMentioned) {
var unreadCounts = this.state.unreadCounts;
var mentionCounts = this.state.mentionCounts;
var unreadAtActive = this.state.unreadAtActive;
2016-02-01 06:58:59 -08:00
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;
2016-02-01 06:58:59 -08:00
if (isMentioned) {
mentionAtActiveCounts[index]++;
}
}
2015-12-22 23:21:33 -08:00
this.setState({
unreadCounts: unreadCounts,
mentionCounts: mentionCounts,
unreadAtActive: unreadAtActive,
2016-02-01 06:58:59 -08:00
mentionAtActiveCounts: mentionAtActiveCounts
2015-12-22 07:34:24 -08:00
});
this.handleUnreadCountTotalChange();
},
markReadAtActive: function(index) {
var unreadAtActive = this.state.unreadAtActive;
2016-02-01 06:58:59 -08:00
var mentionAtActiveCounts = this.state.mentionAtActiveCounts;
unreadAtActive[index] = false;
2016-02-01 06:58:59 -08:00
mentionAtActiveCounts[index] = 0;
this.setState({
unreadAtActive: unreadAtActive,
2016-02-01 06:58:59 -08:00
mentionAtActiveCounts: mentionAtActiveCounts
});
this.handleUnreadCountTotalChange();
},
handleUnreadCountTotalChange: function() {
2015-12-23 00:06:17 -08:00
if (this.props.onUnreadCountChange) {
var allUnreadCount = this.state.unreadCounts.reduce(function(prev, curr) {
2015-12-23 00:06:17 -08:00
return prev + curr;
}, 0);
this.state.unreadAtActive.forEach(function(state) {
if (state) {
allUnreadCount += 1;
}
2015-12-23 00:06:17 -08:00
});
var allMentionCount = this.state.mentionCounts.reduce(function(prev, curr) {
return prev + curr;
}, 0);
2016-02-01 06:58:59 -08:00
this.state.mentionAtActiveCounts.forEach(function(count) {
allMentionCount += count;
});
this.props.onUnreadCountChange(allUnreadCount, allMentionCount);
2015-12-23 00:06:17 -08:00
}
2015-12-22 07:34:24 -08:00
},
handleOnTeamFocused: function(index) {
// Turn off the flag to indicate whether unread message of active channel contains at current tab.
this.markReadAtActive(index);
},
2015-12-22 07:34:24 -08:00
visibleStyle: function(visible) {
2015-12-23 03:53:54 -08:00
var visibility = visible ? 'visible' : 'hidden';
2015-12-22 07:34:24 -08:00
return {
position: 'absolute',
top: (this.props.teams.length > 1) ? 42 : 0,
2015-12-22 07:34:24 -08:00
right: 0,
bottom: 0,
left: 0,
visibility: visibility
};
},
2016-04-22 08:50:57 -07:00
handleLogin: function(request, username, password) {
ipcRenderer.send('login-credentials', request, username, password);
const loginQueue = this.state.loginQueue;
loginQueue.shift();
this.setState(loginQueue);
},
handleLoginCancel: function() {
const loginQueue = this.state.loginQueue;
loginQueue.shift();
this.setState(loginQueue);
},
2015-12-22 07:34:24 -08:00
render: function() {
2015-12-22 23:21:33 -08:00
var thisObj = this;
var tabs_row;
if (this.props.teams.length > 1) {
tabs_row = (
<Row>
2016-02-01 06:58:59 -08:00
<TabBar id="tabBar" teams={ this.props.teams } unreadCounts={ this.state.unreadCounts } mentionCounts={ this.state.mentionCounts } unreadAtActive={ this.state.unreadAtActive } mentionAtActiveCounts={ this.state.mentionAtActiveCounts }
2016-02-11 08:12:28 -08:00
activeKey={ this.state.key } onSelect={ this.handleSelect }></TabBar>
</Row>
);
}
2015-12-22 07:34:24 -08:00
var views = this.props.teams.map(function(team, index) {
var handleUnreadCountChange = function(unreadCount, mentionCount, isUnread, isMentioned) {
thisObj.handleUnreadCountChange(index, unreadCount, mentionCount, isUnread, isMentioned);
};
var handleNotificationClick = function() {
thisObj.handleSelect(index);
}
var id = 'mattermostView' + index;
return (<MattermostView key={ id } id={ id } style={ thisObj.visibleStyle(thisObj.state.key === index) } src={ team.url } name={ team.name } onUnreadCountChange={ handleUnreadCountChange }
onNotificationClick={ handleNotificationClick } />)
2015-12-22 07:34:24 -08:00
});
var views_row = (<Row>
{ views }
</Row>);
2016-04-22 08:50:57 -07:00
var request = null;
var authServerURL = null;
if (this.state.loginQueue.length !== 0) {
request = this.state.loginQueue[0].request;
const tmp_url = url.parse(this.state.loginQueue[0].request.url);
authServerURL = `${tmp_url.protocol}//${tmp_url.host}`;
}
2015-12-22 07:34:24 -08:00
return (
2016-04-22 08:50:57 -07:00
<div>
<LoginModal show={ this.state.loginQueue.length !== 0 } request={ request } authServerURL={ authServerURL } onLogin={ this.handleLogin } onCancel={ this.handleLoginCancel }></LoginModal>
<Grid fluid>
{ tabs_row }
{ views_row }
</Grid>
</div>
2015-12-22 07:34:24 -08:00
);
}
});
var TabBar = React.createClass({
render: function() {
var thisObj = this;
var tabs = this.props.teams.map(function(team, index) {
var unreadCount = 0;
var badgeStyle = {
unread: {
background: '#383838',
float: 'right',
color: 'white',
textAlign: 'center',
marginTop: '5px',
width: '10px',
height: '10px',
marginLeft: '5px',
borderRadius: '50%',
},
mention: {
background: '#f1342c',
float: 'right',
color: 'white',
minWidth: '19px',
fontSize: '12px',
textAlign: 'center',
lineHeight: '20px',
height: '19px',
marginLeft: '5px',
borderRadius: '50%',
}
};
if (thisObj.props.unreadCounts[index] > 0) {
unreadCount = thisObj.props.unreadCounts[index];
}
if (thisObj.props.unreadAtActive[index]) {
unreadCount += 1;
}
var mentionCount = 0;
if (thisObj.props.mentionCounts[index] > 0) {
mentionCount = thisObj.props.mentionCounts[index];
}
2016-02-01 06:58:59 -08:00
if (thisObj.props.mentionAtActiveCounts[index] > 0) {
mentionCount += thisObj.props.mentionAtActiveCounts[index];
}
var badge;
if (mentionCount != 0) {
badge = (<div style={ badgeStyle.mention }>
{ mentionCount }
</div>);
} else if (unreadCount > 0) {
badge = (<div style={ badgeStyle.unread }></div>);
}
var id = 'teamTabItem' + index;
return (<NavItem className="teamTabItem" key={ id } id={ id } eventKey={ index }>
{ team.name }
{ ' ' }
{ badge }
</NavItem>);
});
return (
<Nav id={ this.props.id } bsStyle="tabs" activeKey={ this.props.activeKey } onSelect={ this.props.onSelect }>
{ tabs }
</Nav>
);
}
});
2015-12-22 23:21:33 -08:00
2015-12-22 07:34:24 -08:00
var MattermostView = React.createClass({
getInitialState: function() {
return {
did_fail_load: null
};
},
handleUnreadCountChange: function(unreadCount, mentionCount, isUnread, isMentioned) {
if (this.props.onUnreadCountChange) {
this.props.onUnreadCountChange(unreadCount, mentionCount, isUnread, isMentioned);
}
},
componentDidMount: function() {
var thisObj = this;
var webview = ReactDOM.findDOMNode(this.refs.webview);
// This option disables the same-origin policy and allows js/css/plugins not only content like images.
2016-04-17 07:07:17 -07:00
if (config.disablewebsecurity === true) {
// webview.setAttribute('disablewebsecurity', false) disables websecurity. (electron's bug?)
webview.setAttribute('disablewebsecurity', true);
}
webview.addEventListener('did-fail-load', function(e) {
console.log(thisObj.props.name, 'webview did-fail-load', e);
if (e.errorCode === -3) { // An operation was aborted (due to user action).
return;
}
2016-03-05 21:23:31 -08:00
// should use permanent way to indicate
var did_fail_load_notification = new Notification(`Failed to load "${thisObj.props.name}"`, {
body: `ErrorCode: ${e.errorCode}`,
icon: '../resources/appicon.png'
});
thisObj.setState({
did_fail_load: e
});
});
// Open link in browserWindow. for exmaple, attached files.
webview.addEventListener('new-window', function(e) {
var currentURL = url.parse(webview.getURL());
var destURL = url.parse(e.url);
if (currentURL.host === destURL.host) {
window.open(e.url, 'Mattermost');
} else {
// if the link is external, use default browser.
require('shell').openExternal(e.url);
}
});
webview.addEventListener("dom-ready", function() {
// webview.openDevTools();
// Use 'Meiryo UI' and 'MS Gothic' to prevent CJK fonts on Windows(JP).
if (process.platform === 'win32') {
var applyCssFile = function(cssFile) {
fs.readFile(cssFile, 'utf8', function(err, data) {
if (err) {
console.log(err);
return;
}
webview.insertCSS(data);
});
};
osLocale(function(err, locale) {
if (err) {
console.log(err);
return;
}
if (locale === 'ja_JP') {
applyCssFile(__dirname + '/css/jp_fonts.css');
}
});
}
});
webview.addEventListener('ipc-message', function(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];
thisObj.handleUnreadCountChange(unreadCount, mentionCount, isUnread, isMentioned);
break;
case 'onNotificationClick':
thisObj.props.onNotificationClick();
break;
}
});
webview.addEventListener('console-message', (e) => {
2016-02-13 02:30:37 -08:00
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;
}
});
},
2015-12-22 07:34:24 -08:00
render: function() {
// 'disablewebsecurity' is necessary to display external images.
// However, it allows also CSS/JavaScript.
// So webview should use 'allowDisplayingInsecureContent' as same as BrowserWindow.
if (this.state.did_fail_load === null) {
return (<webview id={ this.props.id } className="mattermostView" style={ this.props.style } preload="webview/mattermost.js" src={ this.props.src } ref="webview"></webview>);
} else {
2016-03-05 03:52:27 -08:00
return (<ErrorView id={ this.props.id + '-fail' } className="errorView" errorInfo={ this.state.did_fail_load } style={ this.props.style }></ErrorView>)
}
}
});
// ErrorCode: https://code.google.com/p/chromium/codesearch#chromium/src/net/base/net_error_list.h
2016-03-05 03:26:13 -08:00
// FIXME: need better wording in English
var ErrorView = React.createClass({
render: function() {
return (
2016-03-05 03:52:27 -08:00
<Grid id={ this.props.id } style={ this.props.style }>
<h1>Failed to load the URL</h1>
<p>
{ 'URL: ' }
{ this.props.errorInfo.validatedURL }
</p>
<p>
{ 'Error code: ' }
{ this.props.errorInfo.errorCode }
</p>
<p>
{ this.props.errorInfo.errorDescription }
</p>
2016-03-05 03:26:13 -08:00
<p>Please check below. Then, reload this window. (Ctrl+R or Command+R)</p>
<ListGroup>
<ListGroupItem>Is your computer online?</ListGroupItem>
<ListGroupItem>Is the server alive?</ListGroupItem>
<ListGroupItem>Is the URL correct?</ListGroupItem>
</ListGroup>
</Grid>
);
2015-12-22 07:34:24 -08:00
}
});
2015-12-23 02:25:14 -08:00
var config;
try {
var configFile = remote.getGlobal('config-file');
config = settings.readFileSync(configFile);
} catch (e) {
window.location = 'settings.html';
}
if (config.teams.length === 0) {
window.location = 'settings.html';
}
2015-12-22 07:34:24 -08:00
var contextMenu = require('./menus/context');
var menu = contextMenu.createDefault();
window.addEventListener('contextmenu', function(e) {
menu.popup(remote.getCurrentWindow());
}, false);
var showUnreadBadgeWindows = function(unreadCount, mentionCount) {
const badge = require('./js/badge');
const sendBadge = function(dataURL, description) {
// window.setOverlayIcon() does't work with NativeImage across remote boundaries.
// https://github.com/atom/electron/issues/4011
2016-04-06 08:49:20 -07:00
electron.ipcRenderer.send('update-unread', {
overlayDataURL: dataURL,
description: description,
unreadCount: unreadCount,
mentionCount: mentionCount
});
};
if (mentionCount > 0) {
const dataURL = badge.createDataURL(mentionCount.toString());
sendBadge(dataURL, 'You have unread mention (' + mentionCount + ')');
} else if (unreadCount > 0) {
const dataURL = badge.createDataURL('•');
sendBadge(dataURL, 'You have unread channels');
} else {
sendBadge(null, 'You have no unread messages');
}
}
var showUnreadBadgeOSX = function(unreadCount, mentionCount) {
if (mentionCount > 0) {
remote.app.dock.setBadge(mentionCount.toString());
} else if (unreadCount > 0) {
remote.app.dock.setBadge('•');
} else {
remote.app.dock.setBadge('');
}
2016-04-06 08:49:20 -07:00
electron.ipcRenderer.send('update-unread', {
unreadCount: unreadCount,
mentionCount: mentionCount
});
}
var showUnreadBadge = function(unreadCount, mentionCount) {
2015-12-23 00:06:17 -08:00
switch (process.platform) {
case 'win32':
showUnreadBadgeWindows(unreadCount, mentionCount);
2015-12-23 00:06:17 -08:00
break;
case 'darwin':
showUnreadBadgeOSX(unreadCount, mentionCount);
2015-12-23 00:06:17 -08:00
break;
default:
}
}
2015-12-22 07:34:24 -08:00
ReactDOM.render(
2015-12-23 00:06:17 -08:00
<MainPage teams={ config.teams } onUnreadCountChange={ showUnreadBadge } />,
2015-12-22 07:34:24 -08:00
document.getElementById('content')
);