commit
108cf91bf6
8
.editorconfig
Normal file
8
.editorconfig
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
insert_final_newline = true
|
49
CHANGELOG.md
49
CHANGELOG.md
|
@ -1,17 +1,54 @@
|
||||||
# Mattermost Desktop Application Changelog
|
# Mattermost Desktop Application Changelog
|
||||||
|
|
||||||
## IN PROGRESS: Release v1.0.8 (Beta)
|
## Release v1.1.0 (Beta)
|
||||||
|
|
||||||
The `electron-mattermost` project is now the official desktop application for the Mattermost open source project.
|
The `electron-mattermost` project is now the official desktop application for the Mattermost open source project.
|
||||||
|
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
- Renaming project from `electron-mattermost` to `desktop`
|
|
||||||
|
#### All platforms
|
||||||
|
|
||||||
|
- Rename project from `electron-mattermost` to `desktop`
|
||||||
|
- Rename the executable file from `electron-mattermost` to `Mattermost`
|
||||||
|
- The configuration directory is also different from previous versions.
|
||||||
|
- Should execute following command to take over `config.json`.
|
||||||
|
- Windows: `copy %APPDATA%\electron-mattermost\config.json %APPDATA%\Mattermost\config.json`
|
||||||
|
- OS X: `cp ~/Library/Application\ Support/electron-mattermost/config.json ~/Library/Application\ Support/Mattermost/config.json`
|
||||||
|
- Linux: `cp ~/.config/electron-mattermost/config.json ~/.config/Mattermost/config.json`
|
||||||
|
|
||||||
|
|
||||||
|
### Improvements
|
||||||
|
|
||||||
|
#### All platforms
|
||||||
|
- Refine application icon.
|
||||||
|
- Show error messages when the application failed in loading Mattermost server.
|
||||||
|
- Show confirmation dialog to continue connection when there is certificate error.
|
||||||
|
- Add validation to check whether both of **Name** and **URL** fields are not blank.
|
||||||
|
- Add simple basic HTTP authentication (requires a command line).
|
||||||
|
|
||||||
|
#### Windows
|
||||||
|
- Show a small circle on the tray icon when there are new messages.
|
||||||
|
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
- On **Settings Page** added validation so that **Name** field value is required before team site can be added.
|
|
||||||
|
#### Windows
|
||||||
|
- **File** > **About** does not bring up version number dialog.
|
||||||
|
|
||||||
|
#### Linux
|
||||||
|
- **File** > **About** does not bring up version number dialog.
|
||||||
|
- Ubuntu: Notification is not showing up.
|
||||||
|
- The view crashes when freetype 2.6.3 is used in system.
|
||||||
|
|
||||||
|
|
||||||
### Known issues
|
### Known issues
|
||||||
|
|
||||||
- Windows and Linux: **File** > **About** does not bring up version number dialog
|
#### All platforms
|
||||||
- Windows: Application does not appear in Windows volume mixer
|
- Images with `http://` do not render.
|
||||||
- All platforms: Embedded markdown images with `http://` do not render
|
- Basic Authentication is not working.
|
||||||
|
- Some keyboard shortcuts are missing. (e.g. <kbd>Ctrl+W</kbd>, <kbd>Command+,</kbd>)
|
||||||
|
- Basic authentication requires a command line.
|
||||||
|
|
||||||
|
#### Windows
|
||||||
|
- Application does not appear properly in Windows volume mixer.
|
||||||
|
|
|
@ -5,8 +5,8 @@ Thank you for your contributing! My requests are few things. Please read below.
|
||||||
Thank you for feedback. When you report a problem, please pay attention to following points.
|
Thank you for feedback. When you report a problem, please pay attention to following points.
|
||||||
|
|
||||||
### Does it happen on web browsers? (especially Chrome)
|
### Does it happen on web browsers? (especially Chrome)
|
||||||
electron-mattermost is based on Electron. It integrates Chrome as a browser window.
|
Mattermost Desktop is based on Electron, which integrates the Chrome engine within a standalone application.
|
||||||
If the problem appears on web browsers, it may be the issue for Mattermost (or Chrome).
|
If the problem you encounter can be reproduced on web browsers, it may be an issue with Mattermost server (or Chrome).
|
||||||
|
|
||||||
### Try "Clear Cache and Reload"
|
### Try "Clear Cache and Reload"
|
||||||
It's available as `Ctrl(Command) + Shift + R`.
|
It's available as `Ctrl(Command) + Shift + R`.
|
||||||
|
@ -14,14 +14,15 @@ Some layout problems are caused by browser cache.
|
||||||
Especially, this kind of issue might happen when you have updated Mattermost server.
|
Especially, this kind of issue might happen when you have updated Mattermost server.
|
||||||
|
|
||||||
### Write detailed information
|
### Write detailed information
|
||||||
Following points are very helpful to understand the problem.
|
Detailed information is very helpful to understand the problem.
|
||||||
|
|
||||||
|
For example:
|
||||||
* How to reproduce, step-by-step
|
* How to reproduce, step-by-step
|
||||||
* Expected behavior (or what is wrong)
|
* Expected behavior (or what is wrong)
|
||||||
* Screenshots (for GUI issues)
|
* Screenshots (for GUI issues)
|
||||||
* Application version
|
* Application version
|
||||||
* Operating system
|
* Operating system
|
||||||
* Mattermost version
|
* Mattermost server version
|
||||||
|
|
||||||
## Feature idea
|
## Feature idea
|
||||||
Please see http://www.mattermost.org/feature-requests/ .
|
Please see http://www.mattermost.org/feature-requests/ .
|
||||||
|
@ -35,4 +36,3 @@ Pull requests are welcome. Thank you for your great work!
|
||||||
* Mattermost server version on which you tested
|
* Mattermost server version on which you tested
|
||||||
* New or updated unit tests for your changes
|
* New or updated unit tests for your changes
|
||||||
3. Please complete the [Mattermost CLA](http://www.mattermost.org/mattermost-contributor-agreement/) prior to submitting a PR.
|
3. Please complete the [Mattermost CLA](http://www.mattermost.org/mattermost-contributor-agreement/) prior to submitting a PR.
|
||||||
|
|
||||||
|
|
10
README.md
10
README.md
|
@ -50,6 +50,16 @@ Configuration will be saved into Electron's userData directory:
|
||||||
*When you upgrade from electron-mattermost, please copy `config.json` from `electron-mattermost`.
|
*When you upgrade from electron-mattermost, please copy `config.json` from `electron-mattermost`.
|
||||||
Otherwise, you have to configure again.*
|
Otherwise, you have to configure again.*
|
||||||
|
|
||||||
|
### Proxy
|
||||||
|
Normally, the application will follow your system settings to use proxy.
|
||||||
|
Or you can set proxy by following command line options.
|
||||||
|
|
||||||
|
* `--proxy-server=<SERVER>:<PORT>`
|
||||||
|
* `--proxy-pac-url=<URL>`
|
||||||
|
|
||||||
|
*Note: Authorization is not supported yet.*
|
||||||
|
|
||||||
|
|
||||||
## Testing and Development
|
## Testing and Development
|
||||||
Node.js is required to test this app.
|
Node.js is required to test this app.
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,8 @@ dependencies:
|
||||||
cache_directories:
|
cache_directories:
|
||||||
- "~/.electron"
|
- "~/.electron"
|
||||||
- "src/node_modules"
|
- "src/node_modules"
|
||||||
|
pre:
|
||||||
|
- npm install -g npm@3.3.12
|
||||||
post:
|
post:
|
||||||
- mkdir -p ~/.electron
|
- mkdir -p ~/.electron
|
||||||
- docker run --rm -it -v `pwd`:/home/xclient/electron-mattermost -v ~/.electron:/home/xclient/.electron yuyaoc/em-builder:dev ./electron-mattermost/docker/package_in_docker.sh
|
- docker run --rm -it -v `pwd`:/home/xclient/electron-mattermost -v ~/.electron:/home/xclient/.electron yuyaoc/em-builder:dev ./electron-mattermost/docker/package_in_docker.sh
|
||||||
|
@ -30,6 +32,7 @@ dependencies:
|
||||||
test:
|
test:
|
||||||
override:
|
override:
|
||||||
- node_modules/.bin/mocha --reporter mocha-circleci-reporter
|
- node_modules/.bin/mocha --reporter mocha-circleci-reporter
|
||||||
|
- node_modules/.bin/gulp prettify:verify
|
||||||
post:
|
post:
|
||||||
- mv test-results.xml $CIRCLE_TEST_REPORTS/
|
- mv test-results.xml $CIRCLE_TEST_REPORTS/
|
||||||
|
|
||||||
|
|
48
gulpfile.js
48
gulpfile.js
|
@ -7,6 +7,8 @@ var webpack = require('webpack-stream');
|
||||||
var named = require('vinyl-named');
|
var named = require('vinyl-named');
|
||||||
var changed = require('gulp-changed');
|
var changed = require('gulp-changed');
|
||||||
var esformatter = require('gulp-esformatter');
|
var esformatter = require('gulp-esformatter');
|
||||||
|
var esformatter_origin = require('esformatter');
|
||||||
|
var through = require('through2');
|
||||||
var del = require('del');
|
var del = require('del');
|
||||||
var electron = require('electron-connect').server.create({
|
var electron = require('electron-connect').server.create({
|
||||||
path: './dist'
|
path: './dist'
|
||||||
|
@ -16,35 +18,65 @@ var packager = require('electron-packager');
|
||||||
var sources = ['**/*.js', '**/*.css', '**/*.html', '!**/node_modules/**', '!dist/**', '!release/**'];
|
var sources = ['**/*.js', '**/*.css', '**/*.html', '!**/node_modules/**', '!dist/**', '!release/**'];
|
||||||
|
|
||||||
gulp.task('prettify', ['prettify:sources', 'prettify:jsx']);
|
gulp.task('prettify', ['prettify:sources', 'prettify:jsx']);
|
||||||
|
gulp.task('prettify:verify', ['prettify:sources:verify', 'prettify:jsx:verify'])
|
||||||
|
|
||||||
gulp.task('prettify:sources', ['sync-meta'], function() {
|
var prettify_options = {
|
||||||
return gulp.src(sources)
|
|
||||||
.pipe(prettify({
|
|
||||||
html: {
|
html: {
|
||||||
|
eol: '\n',
|
||||||
indentSize: 2
|
indentSize: 2
|
||||||
},
|
},
|
||||||
css: {
|
css: {
|
||||||
|
eol: '\n',
|
||||||
indentSize: 2
|
indentSize: 2
|
||||||
},
|
},
|
||||||
js: {
|
js: {
|
||||||
|
eol: '\n',
|
||||||
indentSize: 2,
|
indentSize: 2,
|
||||||
braceStyle: "end-expand"
|
braceStyle: "end-expand"
|
||||||
}
|
}
|
||||||
}))
|
};
|
||||||
|
|
||||||
|
gulp.task('prettify:sources', ['sync-meta'], function() {
|
||||||
|
prettify_options.mode = "VERIFY_AND_WRITE";
|
||||||
|
return gulp.src(sources)
|
||||||
|
.pipe(prettify(prettify_options))
|
||||||
.pipe(gulp.dest('.'));
|
.pipe(gulp.dest('.'));
|
||||||
});
|
});
|
||||||
|
|
||||||
gulp.task('prettify:jsx', function() {
|
gulp.task('prettify:sources:verify', function() {
|
||||||
return gulp.src('src/browser/**/*.jsx')
|
prettify_options.mode = "VERIFY_ONLY";
|
||||||
.pipe(esformatter({
|
prettify_options.showDiff = false;
|
||||||
|
return gulp.src(sources)
|
||||||
|
.pipe(prettify(prettify_options));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
var esformatter_jsx_options = {
|
||||||
indent: {
|
indent: {
|
||||||
value: ' '
|
value: ' '
|
||||||
},
|
},
|
||||||
plugins: ['esformatter-jsx']
|
plugins: ['esformatter-jsx']
|
||||||
}))
|
};
|
||||||
|
|
||||||
|
gulp.task('prettify:jsx', function() {
|
||||||
|
return gulp.src('src/browser/**/*.jsx')
|
||||||
|
.pipe(esformatter(esformatter_jsx_options))
|
||||||
.pipe(gulp.dest('src/browser'));
|
.pipe(gulp.dest('src/browser'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
gulp.task('prettify:jsx:verify', function() {
|
||||||
|
return gulp.src('src/browser/**/*.jsx')
|
||||||
|
.pipe(through.obj(function(file, enc, cb) {
|
||||||
|
var result = esformatter_origin.diff.unified(file.contents.toString(), esformatter_origin.rc(file.path, esformatter_jsx_options));
|
||||||
|
if (result !== "") {
|
||||||
|
console.log('Error: ' + file.path + ' must be formatted');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
cb();
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
gulp.task('build', ['sync-meta', 'webpack', 'copy'], function() {
|
gulp.task('build', ['sync-meta', 'webpack', 'copy'], function() {
|
||||||
return gulp.src('src/package.json')
|
return gulp.src('src/package.json')
|
||||||
.pipe(gulp.dest('dist'));
|
.pipe(gulp.dest('dist'));
|
||||||
|
|
10
package.json
10
package.json
|
@ -1,11 +1,14 @@
|
||||||
{
|
{
|
||||||
"name": "mattermost-desktop",
|
"name": "mattermost-desktop",
|
||||||
"productName": "Mattermost",
|
"productName": "Mattermost",
|
||||||
"version": "1.0.7",
|
"version": "1.1.0",
|
||||||
"description": "Mattermost Desktop application for Windows, Mac and Linux",
|
"description": "Mattermost Desktop application for Windows, Mac and Linux",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"author": "Yuya Ochiai",
|
"author": "Yuya Ochiai",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.2.0"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"install": "cd src && npm install",
|
"install": "cd src && npm install",
|
||||||
"postinstall": "npm run build",
|
"postinstall": "npm run build",
|
||||||
|
@ -13,7 +16,7 @@
|
||||||
"start": "electron dist",
|
"start": "electron dist",
|
||||||
"watch": "gulp watch",
|
"watch": "gulp watch",
|
||||||
"serve": "gulp watch",
|
"serve": "gulp watch",
|
||||||
"test": "gulp build && mocha",
|
"test": "gulp build && mocha && gulp prettify:verify",
|
||||||
"package": "gulp package",
|
"package": "gulp package",
|
||||||
"package:windows": "gulp package:windows",
|
"package:windows": "gulp package:windows",
|
||||||
"package:osx": "gulp package:osx",
|
"package:osx": "gulp package:osx",
|
||||||
|
@ -29,7 +32,7 @@
|
||||||
"del": "^2.2.0",
|
"del": "^2.2.0",
|
||||||
"electron-connect": "^0.3.3",
|
"electron-connect": "^0.3.3",
|
||||||
"electron-packager": "^5.1.0",
|
"electron-packager": "^5.1.0",
|
||||||
"electron-prebuilt": "0.36.7",
|
"electron-prebuilt": "0.36.11",
|
||||||
"esformatter": "^0.8.1",
|
"esformatter": "^0.8.1",
|
||||||
"esformatter-jsx": "^4.0.6",
|
"esformatter-jsx": "^4.0.6",
|
||||||
"gulp": "^3.9.0",
|
"gulp": "^3.9.0",
|
||||||
|
@ -42,6 +45,7 @@
|
||||||
"mocha-circleci-reporter": "0.0.1",
|
"mocha-circleci-reporter": "0.0.1",
|
||||||
"should": "^8.0.1",
|
"should": "^8.0.1",
|
||||||
"style-loader": "^0.13.0",
|
"style-loader": "^0.13.0",
|
||||||
|
"through2": "^2.0.1",
|
||||||
"vinyl-named": "^1.1.0",
|
"vinyl-named": "^1.1.0",
|
||||||
"webdriverio": "^3.3.0",
|
"webdriverio": "^3.3.0",
|
||||||
"webpack-stream": "^3.1.0"
|
"webpack-stream": "^3.1.0"
|
||||||
|
|
|
@ -10,6 +10,8 @@ const Col = ReactBootstrap.Col;
|
||||||
const Nav = ReactBootstrap.Nav;
|
const Nav = ReactBootstrap.Nav;
|
||||||
const NavItem = ReactBootstrap.NavItem;
|
const NavItem = ReactBootstrap.NavItem;
|
||||||
const Badge = ReactBootstrap.Badge;
|
const Badge = ReactBootstrap.Badge;
|
||||||
|
const ListGroup = ReactBootstrap.ListGroup;
|
||||||
|
const ListGroupItem = ReactBootstrap.ListGroupItem;
|
||||||
|
|
||||||
const electron = require('electron');
|
const electron = require('electron');
|
||||||
const remote = electron.remote;
|
const remote = electron.remote;
|
||||||
|
@ -204,6 +206,7 @@ var TabBar = React.createClass({
|
||||||
var MattermostView = React.createClass({
|
var MattermostView = React.createClass({
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
|
did_fail_load: null
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
handleUnreadCountChange: function(unreadCount, mentionCount, isUnread, isMentioned) {
|
handleUnreadCountChange: function(unreadCount, mentionCount, isUnread, isMentioned) {
|
||||||
|
@ -216,6 +219,22 @@ var MattermostView = React.createClass({
|
||||||
var thisObj = this;
|
var thisObj = this;
|
||||||
var webview = ReactDOM.findDOMNode(this.refs.webview);
|
var webview = ReactDOM.findDOMNode(this.refs.webview);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
// Open link in browserWindow. for exmaple, attached files.
|
||||||
webview.addEventListener('new-window', function(e) {
|
webview.addEventListener('new-window', function(e) {
|
||||||
var currentURL = url.parse(webview.getURL());
|
var currentURL = url.parse(webview.getURL());
|
||||||
|
@ -293,7 +312,40 @@ var MattermostView = React.createClass({
|
||||||
// 'disablewebsecurity' is necessary to display external images.
|
// 'disablewebsecurity' is necessary to display external images.
|
||||||
// However, it allows also CSS/JavaScript.
|
// However, it allows also CSS/JavaScript.
|
||||||
// So webview should use 'allowDisplayingInsecureContent' as same as BrowserWindow.
|
// 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>);
|
return (<webview id={ this.props.id } className="mattermostView" style={ this.props.style } preload="webview/mattermost.js" src={ this.props.src } ref="webview"></webview>);
|
||||||
|
} else {
|
||||||
|
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
|
||||||
|
// FIXME: need better wording in English
|
||||||
|
var ErrorView = React.createClass({
|
||||||
|
render: function() {
|
||||||
|
return (
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -321,7 +373,9 @@ var showUnreadBadgeWindows = function(unreadCount, mentionCount) {
|
||||||
// https://github.com/atom/electron/issues/4011
|
// https://github.com/atom/electron/issues/4011
|
||||||
electron.ipcRenderer.send('win32-overlay', {
|
electron.ipcRenderer.send('win32-overlay', {
|
||||||
overlayDataURL: dataURL,
|
overlayDataURL: dataURL,
|
||||||
description: description
|
description: description,
|
||||||
|
unreadCount: unreadCount,
|
||||||
|
mentionCount: mentionCount
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -332,7 +386,7 @@ var showUnreadBadgeWindows = function(unreadCount, mentionCount) {
|
||||||
const dataURL = badge.createDataURL('•');
|
const dataURL = badge.createDataURL('•');
|
||||||
sendBadge(dataURL, 'You have unread channels');
|
sendBadge(dataURL, 'You have unread channels');
|
||||||
} else {
|
} else {
|
||||||
remote.getCurrentWindow().setOverlayIcon(null, '');
|
sendBadge(null, 'You have no unread messages');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
80
src/browser/js/notification.js
Normal file
80
src/browser/js/notification.js
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
const OriginalNotification = Notification;
|
||||||
|
|
||||||
|
function override(eventHandlers) {
|
||||||
|
Notification = function(title, options) {
|
||||||
|
this.notification = new OriginalNotification(title, options);
|
||||||
|
if (eventHandlers.notification) {
|
||||||
|
eventHandlers.notification(title, options);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// static properties
|
||||||
|
Notification.__defineGetter__('permission', function() {
|
||||||
|
return OriginalNotification.permission;
|
||||||
|
});
|
||||||
|
|
||||||
|
// instance properties
|
||||||
|
var defineReadProperty = function(property) {
|
||||||
|
Notification.prototype.__defineGetter__(property, function() {
|
||||||
|
return this.notification[property];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
defineReadProperty('title');
|
||||||
|
defineReadProperty('dir');
|
||||||
|
defineReadProperty('lang');
|
||||||
|
defineReadProperty('body');
|
||||||
|
defineReadProperty('tag');
|
||||||
|
defineReadProperty('icon');
|
||||||
|
defineReadProperty('data');
|
||||||
|
defineReadProperty('silent');
|
||||||
|
|
||||||
|
// unsupported properties
|
||||||
|
defineReadProperty('noscreen');
|
||||||
|
defineReadProperty('renotify');
|
||||||
|
defineReadProperty('sound');
|
||||||
|
defineReadProperty('sticky');
|
||||||
|
defineReadProperty('vibrate');
|
||||||
|
|
||||||
|
// event handlers
|
||||||
|
var defineEventHandler = function(event, callback) {
|
||||||
|
defineReadProperty(event);
|
||||||
|
Notification.prototype.__defineSetter__(event, function(originalCallback) {
|
||||||
|
this.notification[event] = function() {
|
||||||
|
callbackevent = {
|
||||||
|
preventDefault: function() {
|
||||||
|
this.isPrevented = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (callback) {
|
||||||
|
callback(callbackevent);
|
||||||
|
if (!callbackevent.isPrevented) {
|
||||||
|
originalCallback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
originalCallback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
defineEventHandler('onclick', eventHandlers.onclick);
|
||||||
|
defineEventHandler('onerror', eventHandlers.onerror);
|
||||||
|
|
||||||
|
// obsolete handlers
|
||||||
|
defineEventHandler('onclose', eventHandlers.onclose);
|
||||||
|
defineEventHandler('onshow', eventHandlers.onshow);
|
||||||
|
|
||||||
|
// static methods
|
||||||
|
Notification.requestPermission = function(callback) {
|
||||||
|
OriginalNotification.requestPermission(callback);
|
||||||
|
};
|
||||||
|
|
||||||
|
// instance methods
|
||||||
|
Notification.prototype.close = function() {
|
||||||
|
this.notification.close();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
override: override
|
||||||
|
};
|
|
@ -169,8 +169,8 @@ var TeamListItemNew = React.createClass({
|
||||||
console.log('submit');
|
console.log('submit');
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.props.onTeamAdd({
|
this.props.onTeamAdd({
|
||||||
name: this.state.name,
|
name: this.state.name.trim(),
|
||||||
url: this.state.url
|
url: this.state.url.trim()
|
||||||
});
|
});
|
||||||
this.setState(this.getInitialState());
|
this.setState(this.getInitialState());
|
||||||
},
|
},
|
||||||
|
@ -186,6 +186,9 @@ var TeamListItemNew = React.createClass({
|
||||||
url: e.target.value
|
url: e.target.value
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
shouldEnableAddButton: function() {
|
||||||
|
return (this.state.name.trim() !== '') && (this.state.url.trim() !== '');
|
||||||
|
},
|
||||||
render: function() {
|
render: function() {
|
||||||
return (
|
return (
|
||||||
<ListGroupItem>
|
<ListGroupItem>
|
||||||
|
@ -202,7 +205,7 @@ var TeamListItemNew = React.createClass({
|
||||||
<input type="url" className="form-control" id="inputTeamURL" placeholder="https://example.com/team" value={ this.state.url } onChange={ this.handleURLChange } />
|
<input type="url" className="form-control" id="inputTeamURL" placeholder="https://example.com/team" value={ this.state.url } onChange={ this.handleURLChange } />
|
||||||
</div>
|
</div>
|
||||||
{ ' ' }
|
{ ' ' }
|
||||||
<Button type="submit">Add</Button>
|
<Button type="submit" disabled={ !this.shouldEnableAddButton() }>Add</Button>
|
||||||
</form>
|
</form>
|
||||||
</ListGroupItem>
|
</ListGroupItem>
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
const electron = require('electron');
|
const electron = require('electron');
|
||||||
const ipc = electron.ipcRenderer;
|
const ipc = electron.ipcRenderer;
|
||||||
const NativeNotification = Notification;
|
const notification = require('../js/notification');
|
||||||
|
|
||||||
var hasClass = function(element, className) {
|
var hasClass = function(element, className) {
|
||||||
var rclass = /[\t\r\n\f]/g;
|
var rclass = /[\t\r\n\f]/g;
|
||||||
|
@ -107,35 +107,21 @@ function isLowerThanOrEqualWindows8_1() {
|
||||||
return (osVersion.major <= 6 && osVersion.minor <= 3);
|
return (osVersion.major <= 6 && osVersion.minor <= 3);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (process.platform === 'win32' && isLowerThanOrEqualWindows8_1()) {
|
||||||
// Show balloon when notified.
|
// Show balloon when notified.
|
||||||
function overrideNotificationWithBalloon() {
|
notification.override({
|
||||||
Notification = function(title, options) {
|
notification: function(title, options) {
|
||||||
ipc.send('notified', {
|
ipc.send('notified', {
|
||||||
title: title,
|
title: title,
|
||||||
options: options
|
options: options
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
Notification.permission = NativeNotification.permission;
|
});
|
||||||
Notification.requestPermission = function(callback) {
|
}
|
||||||
callback('granted');
|
else {
|
||||||
};
|
|
||||||
Notification.prototype.close = function() {};
|
|
||||||
};
|
|
||||||
|
|
||||||
// Show window even if it is hidden/minimized when notification is clicked.
|
// Show window even if it is hidden/minimized when notification is clicked.
|
||||||
function overrideNotification() {
|
notification.override({
|
||||||
Notification = function(title, options) {
|
onclick: function() {
|
||||||
this.notification = new NativeNotification(title, options);
|
|
||||||
};
|
|
||||||
Notification.permission = NativeNotification.permission;
|
|
||||||
Notification.requestPermission = function(callback) {
|
|
||||||
callback('granted');
|
|
||||||
};
|
|
||||||
Notification.prototype.close = function() {
|
|
||||||
this.notification.close();
|
|
||||||
};
|
|
||||||
Notification.prototype.__defineSetter__('onclick', function(callback) {
|
|
||||||
this.notification.onclick = function() {
|
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
// show() breaks Aero Snap state.
|
// show() breaks Aero Snap state.
|
||||||
electron.remote.getCurrentWindow().focus();
|
electron.remote.getCurrentWindow().focus();
|
||||||
|
@ -144,14 +130,6 @@ function overrideNotification() {
|
||||||
electron.remote.getCurrentWindow().show();
|
electron.remote.getCurrentWindow().show();
|
||||||
}
|
}
|
||||||
ipc.sendToHost('onNotificationClick');
|
ipc.sendToHost('onNotificationClick');
|
||||||
callback();
|
}
|
||||||
};
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.platform === 'win32' && isLowerThanOrEqualWindows8_1()) {
|
|
||||||
overrideNotificationWithBalloon();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
overrideNotification();
|
|
||||||
}
|
|
||||||
|
|
51
src/main.js
51
src/main.js
|
@ -10,6 +10,7 @@ const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
var settings = require('./common/settings');
|
var settings = require('./common/settings');
|
||||||
|
var certificateStore = require('./main/certificateStore').load(path.resolve(app.getPath('userData'), 'certificate.json'));
|
||||||
var appMenu = require('./main/menus/app');
|
var appMenu = require('./main/menus/app');
|
||||||
|
|
||||||
var argv = require('yargs').argv;
|
var argv = require('yargs').argv;
|
||||||
|
@ -89,6 +90,38 @@ app.on('before-quit', function() {
|
||||||
willAppQuit = true;
|
willAppQuit = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.on('certificate-error', function(event, webContents, url, error, certificate, callback) {
|
||||||
|
if (certificateStore.isTrusted(url, certificate)) {
|
||||||
|
event.preventDefault();
|
||||||
|
callback(true);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var detail = `URL: ${url}\nError: ${error}`;
|
||||||
|
if (certificateStore.isExisting(url)) {
|
||||||
|
detail = `Certificate is different from previous one.\n\n` + detail;
|
||||||
|
}
|
||||||
|
|
||||||
|
electron.dialog.showMessageBox(mainWindow, {
|
||||||
|
title: 'Certificate error',
|
||||||
|
message: `Do you trust certificate from "${certificate.issuerName}"?`,
|
||||||
|
detail: detail,
|
||||||
|
type: 'warning',
|
||||||
|
buttons: [
|
||||||
|
'Yes',
|
||||||
|
'No'
|
||||||
|
],
|
||||||
|
cancelId: 1
|
||||||
|
}, function(response) {
|
||||||
|
if (response === 0) {
|
||||||
|
certificateStore.add(url, certificate);
|
||||||
|
certificateStore.save();
|
||||||
|
webContents.loadURL(url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
callback(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// This method will be called when Electron has finished
|
// This method will be called when Electron has finished
|
||||||
// initialization and is ready to create browser windows.
|
// initialization and is ready to create browser windows.
|
||||||
app.on('ready', function() {
|
app.on('ready', function() {
|
||||||
|
@ -113,9 +146,22 @@ app.on('ready', function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set overlay icon from dataURL
|
// Set overlay icon from dataURL
|
||||||
|
// Set trayicon to show "dot"
|
||||||
ipc.on('win32-overlay', function(event, arg) {
|
ipc.on('win32-overlay', function(event, arg) {
|
||||||
var overlay = electron.nativeImage.createFromDataURL(arg.overlayDataURL);
|
const overlay = arg.overlayDataURL ? electron.nativeImage.createFromDataURL(arg.overlayDataURL) : null;
|
||||||
mainWindow.setOverlayIcon(overlay, arg.description);
|
mainWindow.setOverlayIcon(overlay, arg.description);
|
||||||
|
|
||||||
|
var tray_image = null;
|
||||||
|
if (arg.mentionCount > 0) {
|
||||||
|
tray_image = 'tray_mention.png';
|
||||||
|
}
|
||||||
|
else if (arg.unreadCount > 0) {
|
||||||
|
tray_image = 'tray_unread.png';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
tray_image = 'tray.png';
|
||||||
|
}
|
||||||
|
trayIcon.setImage(path.resolve(__dirname, 'resources', tray_image));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,7 +175,8 @@ app.on('ready', function() {
|
||||||
// follow Electron's defaults
|
// follow Electron's defaults
|
||||||
window_options = {};
|
window_options = {};
|
||||||
}
|
}
|
||||||
if (process.platform === 'linux') {
|
if (process.platform === 'win32' || process.platform === 'linux') {
|
||||||
|
// On HiDPI Windows environment, the taskbar icon is pixelated. So this line is necessary.
|
||||||
window_options.icon = path.resolve(__dirname, 'resources/appicon.png');
|
window_options.icon = path.resolve(__dirname, 'resources/appicon.png');
|
||||||
}
|
}
|
||||||
window_options.fullScreenable = true;
|
window_options.fullScreenable = true;
|
||||||
|
|
62
src/main/certificateStore.js
Normal file
62
src/main/certificateStore.js
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const url = require('url');
|
||||||
|
|
||||||
|
function comparableCertificate(certificate) {
|
||||||
|
return {
|
||||||
|
data: certificate.data.toString(),
|
||||||
|
issuerName: certificate.issuerName
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function areEqual(certificate0, certificate1) {
|
||||||
|
if (certificate0.data !== certificate1.data) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (certificate0.issuerName !== certificate1.issuerName) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHost(targetURL) {
|
||||||
|
return url.parse(targetURL).host;
|
||||||
|
}
|
||||||
|
|
||||||
|
var CertificateStore = function(storeFile) {
|
||||||
|
this.storeFile = storeFile
|
||||||
|
try {
|
||||||
|
this.data = JSON.parse(fs.readFileSync(storeFile, 'utf-8'));
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
this.data = {};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
CertificateStore.prototype.save = function() {
|
||||||
|
fs.writeFileSync(this.storeFile, JSON.stringify(this.data, null, ' '));
|
||||||
|
};
|
||||||
|
|
||||||
|
CertificateStore.prototype.add = function(targetURL, certificate) {
|
||||||
|
this.data[getHost(targetURL)] = comparableCertificate(certificate);
|
||||||
|
};
|
||||||
|
|
||||||
|
CertificateStore.prototype.isExisting = function(targetURL) {
|
||||||
|
return this.data.hasOwnProperty(getHost(targetURL));
|
||||||
|
};
|
||||||
|
|
||||||
|
CertificateStore.prototype.isTrusted = function(targetURL, certificate) {
|
||||||
|
var host = getHost(targetURL);
|
||||||
|
if (!this.isExisting(targetURL)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return areEqual(this.data[host], comparableCertificate(certificate));
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
load: function(storeFile) {
|
||||||
|
return new CertificateStore(storeFile);
|
||||||
|
}
|
||||||
|
};
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "mattermost-desktop",
|
"name": "mattermost-desktop",
|
||||||
"productName": "Mattermost",
|
"productName": "Mattermost",
|
||||||
"version": "1.0.7",
|
"version": "1.1.0",
|
||||||
"description": "Mattermost Desktop application for Windows, Mac and Linux",
|
"description": "Mattermost Desktop application for Windows, Mac and Linux",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"author": "Yuya Ochiai",
|
"author": "Yuya Ochiai",
|
||||||
|
|
BIN
src/resources/tray_mention.png
Normal file
BIN
src/resources/tray_mention.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 735 B |
BIN
src/resources/tray_unread.png
Normal file
BIN
src/resources/tray_unread.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 715 B |
|
@ -171,6 +171,21 @@ describe('mattermost-desktop', function() {
|
||||||
})
|
})
|
||||||
.end();
|
.end();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should show error when using incorrect URL', function() {
|
||||||
|
this.timeout(30000)
|
||||||
|
fs.writeFileSync(config_file_path, JSON.stringify({
|
||||||
|
version: 1,
|
||||||
|
teams: [{
|
||||||
|
name: 'error_1',
|
||||||
|
url: 'http://false'
|
||||||
|
}]
|
||||||
|
}));
|
||||||
|
return client
|
||||||
|
.init()
|
||||||
|
.waitForVisible('#mattermostView0-fail', 20000)
|
||||||
|
.end();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('settings.html', function() {
|
describe('settings.html', function() {
|
||||||
|
|
Loading…
Reference in a new issue