Merge pull request #92 from mattermost/release/v1.1.0

Release/v1.1.0
This commit is contained in:
Yuya Ochiai 2016-03-30 21:07:56 +09:00
commit 108cf91bf6
18 changed files with 446 additions and 113 deletions

8
.editorconfig Normal file
View file

@ -0,0 +1,8 @@
root = true
[*]
end_of_line = lf
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true

View file

@ -1,17 +1,54 @@
# 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.
### 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
- 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
- Windows and Linux: **File** > **About** does not bring up version number dialog
- Windows: Application does not appear in Windows volume mixer
- All platforms: Embedded markdown images with `http://` do not render
#### All platforms
- 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.

View file

@ -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.
### Does it happen on web browsers? (especially Chrome)
electron-mattermost is based on Electron. It integrates Chrome as a browser window.
If the problem appears on web browsers, it may be the issue for Mattermost (or Chrome).
Mattermost Desktop is based on Electron, which integrates the Chrome engine within a standalone application.
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"
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.
### 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
* Expected behavior (or what is wrong)
* Screenshots (for GUI issues)
* Application version
* Operating system
* Mattermost version
* Mattermost server version
## Feature idea
Please see http://www.mattermost.org/feature-requests/ .
@ -29,10 +30,9 @@ Please see http://www.mattermost.org/feature-requests/ .
## Pull request
Pull requests are welcome. Thank you for your great work!
1. When you edit the code, please run `npm run prettify` to format your code before `git commit`.
2. In the description of your pull request, please include:
* Operating System version on which you tested
* Mattermost server version on which you tested
* New or updated unit tests for your changes
1. When you edit the code, please run `npm run prettify` to format your code before `git commit`.
2. In the description of your pull request, please include:
* Operating System version on which you tested
* Mattermost server version on which you tested
* 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.

View file

@ -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`.
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
Node.js is required to test this app.

View file

@ -13,6 +13,8 @@ dependencies:
cache_directories:
- "~/.electron"
- "src/node_modules"
pre:
- npm install -g npm@3.3.12
post:
- 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
@ -30,6 +32,7 @@ dependencies:
test:
override:
- node_modules/.bin/mocha --reporter mocha-circleci-reporter
- node_modules/.bin/gulp prettify:verify
post:
- mv test-results.xml $CIRCLE_TEST_REPORTS/

View file

@ -7,6 +7,8 @@ var webpack = require('webpack-stream');
var named = require('vinyl-named');
var changed = require('gulp-changed');
var esformatter = require('gulp-esformatter');
var esformatter_origin = require('esformatter');
var through = require('through2');
var del = require('del');
var electron = require('electron-connect').server.create({
path: './dist'
@ -16,35 +18,65 @@ var packager = require('electron-packager');
var sources = ['**/*.js', '**/*.css', '**/*.html', '!**/node_modules/**', '!dist/**', '!release/**'];
gulp.task('prettify', ['prettify:sources', 'prettify:jsx']);
gulp.task('prettify:verify', ['prettify:sources:verify', 'prettify:jsx:verify'])
var prettify_options = {
html: {
eol: '\n',
indentSize: 2
},
css: {
eol: '\n',
indentSize: 2
},
js: {
eol: '\n',
indentSize: 2,
braceStyle: "end-expand"
}
};
gulp.task('prettify:sources', ['sync-meta'], function() {
prettify_options.mode = "VERIFY_AND_WRITE";
return gulp.src(sources)
.pipe(prettify({
html: {
indentSize: 2
},
css: {
indentSize: 2
},
js: {
indentSize: 2,
braceStyle: "end-expand"
}
}))
.pipe(prettify(prettify_options))
.pipe(gulp.dest('.'));
});
gulp.task('prettify:sources:verify', function() {
prettify_options.mode = "VERIFY_ONLY";
prettify_options.showDiff = false;
return gulp.src(sources)
.pipe(prettify(prettify_options));
});
var esformatter_jsx_options = {
indent: {
value: ' '
},
plugins: ['esformatter-jsx']
};
gulp.task('prettify:jsx', function() {
return gulp.src('src/browser/**/*.jsx')
.pipe(esformatter({
indent: {
value: ' '
},
plugins: ['esformatter-jsx']
}))
.pipe(esformatter(esformatter_jsx_options))
.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() {
return gulp.src('src/package.json')
.pipe(gulp.dest('dist'));

View file

@ -1,11 +1,14 @@
{
"name": "mattermost-desktop",
"productName": "Mattermost",
"version": "1.0.7",
"version": "1.1.0",
"description": "Mattermost Desktop application for Windows, Mac and Linux",
"main": "main.js",
"author": "Yuya Ochiai",
"license": "MIT",
"engines": {
"node": ">=4.2.0"
},
"scripts": {
"install": "cd src && npm install",
"postinstall": "npm run build",
@ -13,7 +16,7 @@
"start": "electron dist",
"watch": "gulp watch",
"serve": "gulp watch",
"test": "gulp build && mocha",
"test": "gulp build && mocha && gulp prettify:verify",
"package": "gulp package",
"package:windows": "gulp package:windows",
"package:osx": "gulp package:osx",
@ -29,7 +32,7 @@
"del": "^2.2.0",
"electron-connect": "^0.3.3",
"electron-packager": "^5.1.0",
"electron-prebuilt": "0.36.7",
"electron-prebuilt": "0.36.11",
"esformatter": "^0.8.1",
"esformatter-jsx": "^4.0.6",
"gulp": "^3.9.0",
@ -42,6 +45,7 @@
"mocha-circleci-reporter": "0.0.1",
"should": "^8.0.1",
"style-loader": "^0.13.0",
"through2": "^2.0.1",
"vinyl-named": "^1.1.0",
"webdriverio": "^3.3.0",
"webpack-stream": "^3.1.0"

View file

@ -10,6 +10,8 @@ const Col = ReactBootstrap.Col;
const Nav = ReactBootstrap.Nav;
const NavItem = ReactBootstrap.NavItem;
const Badge = ReactBootstrap.Badge;
const ListGroup = ReactBootstrap.ListGroup;
const ListGroupItem = ReactBootstrap.ListGroupItem;
const electron = require('electron');
const remote = electron.remote;
@ -204,6 +206,7 @@ var TabBar = React.createClass({
var MattermostView = React.createClass({
getInitialState: function() {
return {
did_fail_load: null
};
},
handleUnreadCountChange: function(unreadCount, mentionCount, isUnread, isMentioned) {
@ -216,6 +219,22 @@ var MattermostView = React.createClass({
var thisObj = this;
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.
webview.addEventListener('new-window', function(e) {
var currentURL = url.parse(webview.getURL());
@ -293,7 +312,40 @@ var MattermostView = React.createClass({
// 'disablewebsecurity' is necessary to display external images.
// However, it allows also CSS/JavaScript.
// So webview should use 'allowDisplayingInsecureContent' as same as BrowserWindow.
return (<webview id={ this.props.id } className="mattermostView" style={ this.props.style } preload="webview/mattermost.js" src={ this.props.src } ref="webview"></webview>);
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 {
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
electron.ipcRenderer.send('win32-overlay', {
overlayDataURL: dataURL,
description: description
description: description,
unreadCount: unreadCount,
mentionCount: mentionCount
});
};
@ -332,7 +386,7 @@ var showUnreadBadgeWindows = function(unreadCount, mentionCount) {
const dataURL = badge.createDataURL('•');
sendBadge(dataURL, 'You have unread channels');
} else {
remote.getCurrentWindow().setOverlayIcon(null, '');
sendBadge(null, 'You have no unread messages');
}
}

View file

@ -1,29 +1,29 @@
'use strict';
var createDataURL = function(text) {
const scale = 2; // should rely display dpi
const size = 16 * scale;
const canvas = document.createElement('canvas');
canvas.setAttribute('width', size);
canvas.setAttribute('height', size);
const ctx = canvas.getContext('2d');
// circle
ctx.fillStyle = "#FF1744"; // Material Red A400
ctx.beginPath();
ctx.arc(size / 2, size / 2, size / 2, 0, Math.PI * 2);
ctx.fill();
// text
ctx.fillStyle = "#ffffff"
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.font = (11 * scale) + "px sans-serif";
ctx.fillText(text, size / 2, size / 2, size);
return canvas.toDataURL();
};
module.exports = {
createDataURL: createDataURL
'use strict';
var createDataURL = function(text) {
const scale = 2; // should rely display dpi
const size = 16 * scale;
const canvas = document.createElement('canvas');
canvas.setAttribute('width', size);
canvas.setAttribute('height', size);
const ctx = canvas.getContext('2d');
// circle
ctx.fillStyle = "#FF1744"; // Material Red A400
ctx.beginPath();
ctx.arc(size / 2, size / 2, size / 2, 0, Math.PI * 2);
ctx.fill();
// text
ctx.fillStyle = "#ffffff"
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.font = (11 * scale) + "px sans-serif";
ctx.fillText(text, size / 2, size / 2, size);
return canvas.toDataURL();
};
module.exports = {
createDataURL: createDataURL
};

View 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
};

View file

@ -169,8 +169,8 @@ var TeamListItemNew = React.createClass({
console.log('submit');
e.preventDefault();
this.props.onTeamAdd({
name: this.state.name,
url: this.state.url
name: this.state.name.trim(),
url: this.state.url.trim()
});
this.setState(this.getInitialState());
},
@ -186,6 +186,9 @@ var TeamListItemNew = React.createClass({
url: e.target.value
});
},
shouldEnableAddButton: function() {
return (this.state.name.trim() !== '') && (this.state.url.trim() !== '');
},
render: function() {
return (
<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 } />
</div>
{ ' ' }
<Button type="submit">Add</Button>
<Button type="submit" disabled={ !this.shouldEnableAddButton() }>Add</Button>
</form>
</ListGroupItem>
);

View file

@ -2,7 +2,7 @@
const electron = require('electron');
const ipc = electron.ipcRenderer;
const NativeNotification = Notification;
const notification = require('../js/notification');
var hasClass = function(element, className) {
var rclass = /[\t\r\n\f]/g;
@ -107,35 +107,21 @@ function isLowerThanOrEqualWindows8_1() {
return (osVersion.major <= 6 && osVersion.minor <= 3);
};
// Show balloon when notified.
function overrideNotificationWithBalloon() {
Notification = function(title, options) {
ipc.send('notified', {
title: title,
options: options
});
};
Notification.permission = NativeNotification.permission;
Notification.requestPermission = function(callback) {
callback('granted');
};
Notification.prototype.close = function() {};
};
// Show window even if it is hidden/minimized when notification is clicked.
function overrideNotification() {
Notification = function(title, options) {
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' && isLowerThanOrEqualWindows8_1()) {
// Show balloon when notified.
notification.override({
notification: function(title, options) {
ipc.send('notified', {
title: title,
options: options
});
}
});
}
else {
// Show window even if it is hidden/minimized when notification is clicked.
notification.override({
onclick: function() {
if (process.platform === 'win32') {
// show() breaks Aero Snap state.
electron.remote.getCurrentWindow().focus();
@ -144,14 +130,6 @@ function overrideNotification() {
electron.remote.getCurrentWindow().show();
}
ipc.sendToHost('onNotificationClick');
callback();
};
}
});
}
if (process.platform === 'win32' && isLowerThanOrEqualWindows8_1()) {
overrideNotificationWithBalloon();
}
else {
overrideNotification();
}

View file

@ -10,6 +10,7 @@ const fs = require('fs');
const path = require('path');
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 argv = require('yargs').argv;
@ -89,6 +90,38 @@ app.on('before-quit', function() {
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
// initialization and is ready to create browser windows.
app.on('ready', function() {
@ -113,9 +146,22 @@ app.on('ready', function() {
});
// Set overlay icon from dataURL
// Set trayicon to show "dot"
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);
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
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.fullScreenable = true;

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

View file

@ -1,7 +1,7 @@
{
"name": "mattermost-desktop",
"productName": "Mattermost",
"version": "1.0.7",
"version": "1.1.0",
"description": "Mattermost Desktop application for Windows, Mac and Linux",
"main": "main.js",
"author": "Yuya Ochiai",

Binary file not shown.

After

Width:  |  Height:  |  Size: 735 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 715 B

View file

@ -171,6 +171,21 @@ describe('mattermost-desktop', function() {
})
.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() {