diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..a86e0128 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*] +end_of_line = lf +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true diff --git a/CHANGELOG.md b/CHANGELOG.md index 450ebb44..3b8621cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. Ctrl+W, Command+,) +- Basic authentication requires a command line. + +#### Windows +- Application does not appear properly in Windows volume mixer. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index be0aab8e..709c4665 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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. - diff --git a/README.md b/README.md index ffd53707..ca0dae7d 100644 --- a/README.md +++ b/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`. 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=:` +* `--proxy-pac-url=` + +*Note: Authorization is not supported yet.* + + ## Testing and Development Node.js is required to test this app. diff --git a/circle.yml b/circle.yml index 54408052..1ac6199e 100644 --- a/circle.yml +++ b/circle.yml @@ -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/ diff --git a/gulpfile.js b/gulpfile.js index 8a3fd391..a8a50002 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -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')); diff --git a/package.json b/package.json index 77a5abd9..3492d439 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/browser/index.jsx b/src/browser/index.jsx index a35f6175..5f8d7bb4 100644 --- a/src/browser/index.jsx +++ b/src/browser/index.jsx @@ -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 (); + if (this.state.did_fail_load === null) { + return (); + } else { + return () + } + } +}); + +// 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 ( + +

Failed to load the URL

+

+ { 'URL: ' } + { this.props.errorInfo.validatedURL } +

+

+ { 'Error code: ' } + { this.props.errorInfo.errorCode } +

+

+ { this.props.errorInfo.errorDescription } +

+

Please check below. Then, reload this window. (Ctrl+R or Command+R)

+ + Is your computer online? + Is the server alive? + Is the URL correct? + +
+ ); } }); @@ -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'); } } diff --git a/src/browser/js/badge.js b/src/browser/js/badge.js index b77e1f22..0b0b168e 100644 --- a/src/browser/js/badge.js +++ b/src/browser/js/badge.js @@ -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 }; diff --git a/src/browser/js/notification.js b/src/browser/js/notification.js new file mode 100644 index 00000000..f893d879 --- /dev/null +++ b/src/browser/js/notification.js @@ -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 +}; diff --git a/src/browser/settings.jsx b/src/browser/settings.jsx index 850cccee..59a08500 100644 --- a/src/browser/settings.jsx +++ b/src/browser/settings.jsx @@ -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 ( @@ -202,7 +205,7 @@ var TeamListItemNew = React.createClass({ { ' ' } - + ); diff --git a/src/browser/webview/mattermost.js b/src/browser/webview/mattermost.js index 859472da..ba9500c4 100644 --- a/src/browser/webview/mattermost.js +++ b/src/browser/webview/mattermost.js @@ -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(); -} diff --git a/src/main.js b/src/main.js index a6a382d8..9e6c2884 100644 --- a/src/main.js +++ b/src/main.js @@ -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; diff --git a/src/main/certificateStore.js b/src/main/certificateStore.js new file mode 100644 index 00000000..da091ac6 --- /dev/null +++ b/src/main/certificateStore.js @@ -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); + } +}; diff --git a/src/package.json b/src/package.json index a194b086..01b7e492 100644 --- a/src/package.json +++ b/src/package.json @@ -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", diff --git a/src/resources/tray_mention.png b/src/resources/tray_mention.png new file mode 100644 index 00000000..3a3ee1a9 Binary files /dev/null and b/src/resources/tray_mention.png differ diff --git a/src/resources/tray_unread.png b/src/resources/tray_unread.png new file mode 100644 index 00000000..4b2a7ea5 Binary files /dev/null and b/src/resources/tray_unread.png differ diff --git a/test/browser_test.js b/test/browser_test.js index d05c1364..65334a4a 100644 --- a/test/browser_test.js +++ b/test/browser_test.js @@ -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() {