diff --git a/README.md b/README.md index baee5ff915d..fd9b8bcc1ec 100644 --- a/README.md +++ b/README.md @@ -86,5 +86,10 @@ $ npm run package:linux $ npm run package:all (Packages for all platform) ``` +Create a windows installer with the following command. It will appear in the `release\windows-installer` directory. +``` +$ npm run installer +``` + ## Contributing Please see [CONTRIBUTING.md](./CONTRIBUTING.md). diff --git a/package.json b/package.json index e453f34061b..d90114fc4a9 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "package:osx": "gulp package:osx", "package:linux": "gulp build && build --platform linux --arch all -d deb", "package:all": "gulp package:all", - "prettify": "gulp prettify" + "prettify": "gulp prettify", + "installer": "node ./script/installer.js" }, "devDependencies": { "babel-core": "^6.7.5", @@ -33,10 +34,11 @@ "babel-preset-react": "^6.5.0", "chromedriver": "^2.20.0", "del": "^2.2.0", - "electron-builder": "^3.11.0", + "electron-builder": "3.20.0", "electron-connect": "^0.3.7", "electron-packager": "^7.0.1", "electron-prebuilt": "0.37.8", + "electron-winstaller": "^2.2.0", "esformatter": "^0.9.3", "esformatter-jsx": "^5.0.0", "gulp": "^3.9.0", @@ -47,6 +49,7 @@ "json-loader": "^0.5.4", "mocha": "^2.3.4", "mocha-circleci-reporter": "0.0.1", + "rimraf": "^2.5.2", "should": "^8.0.1", "style-loader": "^0.13.0", "through2": "^2.0.1", @@ -56,9 +59,11 @@ "webpack-stream": "^3.1.0" }, "build": { - "linux": { - "synopsis": "Mattermost Desktop" - } + "app-bundle-id": "org.mattermost.desktop", + "app-category-type": "public.app-category.productivity", + "linux": { + "synopsis": "Mattermost Desktop" + } }, "directories":{ "buildResources": "resources", diff --git a/script/installer.js b/script/installer.js new file mode 100644 index 00000000000..98551214aa5 --- /dev/null +++ b/script/installer.js @@ -0,0 +1,38 @@ +#!/usr/bin/env node + +const createWindowsInstaller = require('electron-winstaller').createWindowsInstaller +const path = require('path') +const rimraf = require('rimraf') + +deleteOutputFolder() + .then(getInstallerConfig) + .then(createWindowsInstaller) + .catch((error) => { + console.error(error.message || error) + process.exit(1) + }) + +function getInstallerConfig () { + const rootPath = path.join(__dirname, '..') + const outPath = path.join(rootPath, 'release') + + return Promise.resolve({ + appDirectory: path.join(outPath, 'Mattermost-win32-ia32'), + iconUrl: 'https://raw.githubusercontent.com/mattermost/desktop/master/resources/icon.ico', + //loadingGif: path.join(rootPath, 'assets', 'img', 'loading.gif'), + noMsi: true, + outputDirectory: path.join(outPath, 'windows-installer'), + setupExe: 'Mattermost.exe', + setupIcon: path.join(rootPath, 'resources', 'icon.ico'), + skipUpdateIcon: true, + exe: 'Mattermost.exe' + }) +} + +function deleteOutputFolder () { + return new Promise((resolve, reject) => { + rimraf(path.join(__dirname, '..', 'out', 'windows-installer'), (error) => { + error ? reject(error) : resolve() + }) + }) +} diff --git a/src/auto-updater.js b/src/auto-updater.js new file mode 100644 index 00000000000..196575a2add --- /dev/null +++ b/src/auto-updater.js @@ -0,0 +1,49 @@ +const ChildProcess = require('child_process') +const path = require('path') + +exports.createShortcut = function (callback) { + spawnUpdate([ + '--createShortcut', + path.basename(process.execPath), + '--shortcut-locations', + 'StartMenu' + ], callback) +} + +exports.removeShortcut = function (callback) { + spawnUpdate([ + '--removeShortcut', + path.basename(process.execPath) + ], callback) +} + +function spawnUpdate (args, callback) { + var updateExe = path.resolve(process.execPath, '..', '..', 'Update.exe') + var stdout = '' + var spawned = null + + try { + spawned = ChildProcess.spawn(updateExe, args) + } catch (error) { + if (error && error.stdout == null) error.stdout = stdout + process.nextTick(function () { callback(error) }) + return + } + + var error = null + + spawned.stdout.on('data', function (data) { stdout += data }) + + spawned.on('error', function (processError) { + if (!error) error = processError + }) + + spawned.on('close', function (code, signal) { + if (!error && code !== 0) { + error = new Error('Command failed: ' + code + ' ' + signal) + } + if (error && error.code == null) error.code = code + if (error && error.stdout == null) error.stdout = stdout + callback(error) + }) +} diff --git a/src/main.js b/src/main.js index c0b33f193a5..37587001ea0 100644 --- a/src/main.js +++ b/src/main.js @@ -9,6 +9,7 @@ const ipc = electron.ipcMain; const nativeImage = electron.nativeImage; const fs = require('fs'); const path = require('path'); +const autoUpdater = require('./auto-updater'); var settings = require('./common/settings'); var certificateStore = require('./main/certificateStore').load(path.resolve(app.getPath('userData'), 'certificate.json')); @@ -16,279 +17,297 @@ var appMenu = require('./main/menus/app'); var argv = require('yargs').argv; -var client = null; -if (argv.livereload) { - client = require('electron-connect').client.create(); - client.on('reload', function() { - mainWindow.reload(); - }); -} - -if (argv['config-file']) { - global['config-file'] = argv['config-file']; -} -else { - global['config-file'] = app.getPath('userData') + '/config.json' -} - -var config = {}; -try { - var configFile = global['config-file']; - config = settings.readFileSync(configFile); - if (config.version != settings.version) { - config = settings.upgrade(config); - settings.writeFileSync(configFile, config); +function initialize () { + var client = null; + if (argv.livereload) { + client = require('electron-connect').client.create(); + client.on('reload', function() { + mainWindow.reload(); + }); } -} -catch (e) { - config = settings.loadDefault(); - console.log('Failed to read or upgrade config.json'); -} -// Keep a global reference of the window object, if you don't, the window will -// be closed automatically when the JavaScript object is garbage collected. -var mainWindow = null; -var trayIcon = null; -const trayImages = function() { - switch (process.platform) { - case 'win32': - return { - normal: nativeImage.createFromPath(path.resolve(__dirname, 'resources/tray.png')), - unread: nativeImage.createFromPath(path.resolve(__dirname, 'resources/tray_unread.png')), - mention: nativeImage.createFromPath(path.resolve(__dirname, 'resources/tray_mention.png')) - }; - case 'darwin': - return { - normal: nativeImage.createFromPath(path.resolve(__dirname, 'resources/osx/MenuIconTemplate.png')), - unread: nativeImage.createFromPath(path.resolve(__dirname, 'resources/osx/MenuIconUnreadTemplate.png')), - mention: nativeImage.createFromPath(path.resolve(__dirname, 'resources/osx/MenuIconMentionTemplate.png')) - }; - default: - return {}; + if (argv['config-file']) { + global['config-file'] = argv['config-file']; + } + else { + global['config-file'] = app.getPath('userData') + '/config.json' } -}(); -var willAppQuit = false; -function shouldShowTrayIcon() { - if (process.platform === 'win32') { - return true; + var config = {}; + try { + var configFile = global['config-file']; + config = settings.readFileSync(configFile); + if (config.version != settings.version) { + config = settings.upgrade(config); + settings.writeFileSync(configFile, config); + } } - if (process.platform === 'darwin' && config.showTrayIcon === true) { - return true; + catch (e) { + config = settings.loadDefault(); + console.log('Failed to read or upgrade config.json'); } - return false; -} -// Quit when all windows are closed. -app.on('window-all-closed', function() { - // On OS X it is common for applications and their menu bar - // to stay active until the user quits explicitly with Cmd + Q - if (process.platform !== 'darwin') { - app.quit(); - } -}); + // Keep a global reference of the window object, if you don't, the window will + // be closed automatically when the JavaScript object is garbage collected. + var mainWindow = null; + var trayIcon = null; + const trayImages = function() { + switch (process.platform) { + case 'win32': + return { + normal: nativeImage.createFromPath(path.resolve(__dirname, 'resources/tray.png')), + unread: nativeImage.createFromPath(path.resolve(__dirname, 'resources/tray_unread.png')), + mention: nativeImage.createFromPath(path.resolve(__dirname, 'resources/tray_mention.png')) + }; + case 'darwin': + return { + normal: nativeImage.createFromPath(path.resolve(__dirname, 'resources/osx/MenuIconTemplate.png')), + unread: nativeImage.createFromPath(path.resolve(__dirname, 'resources/osx/MenuIconUnreadTemplate.png')), + mention: nativeImage.createFromPath(path.resolve(__dirname, 'resources/osx/MenuIconMentionTemplate.png')) + }; + default: + return {}; + } + }(); + var willAppQuit = false; -// For win32, auto-hide menu bar. -app.on('browser-window-created', function(event, window) { - if (process.platform === 'win32' || process.platform === 'linux') { - if (config.hideMenuBar) { - window.setAutoHideMenuBar(true); - window.setMenuBarVisibility(false); + function shouldShowTrayIcon() { + if (process.platform === 'win32') { + return true; } + if (process.platform === 'darwin' && config.showTrayIcon === true) { + return true; + } + return false; } -}); - -// For OSX, show hidden mainWindow when clicking dock icon. -app.on('activate', function(event) { - mainWindow.show(); -}); - -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; + // Quit when all windows are closed. + app.on('window-all-closed', function() { + // On OS X it is common for applications and their menu bar + // to stay active until the user quits explicitly with Cmd + Q + if (process.platform !== 'darwin') { + app.quit(); } + }); - 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); + // For win32, auto-hide menu bar. + app.on('browser-window-created', function(event, window) { + if (process.platform === 'win32' || process.platform === 'linux') { + if (config.hideMenuBar) { + window.setAutoHideMenuBar(true); + window.setMenuBarVisibility(false); } - }); - callback(false); - } -}); + } + }); -const loginCallbackMap = new Map(); + // For OSX, show hidden mainWindow when clicking dock icon. + app.on('activate', function(event) { + mainWindow.show(); + }); -ipc.on('login-credentials', function(event, request, user, password) { - const callback = loginCallbackMap.get(JSON.stringify(request)); - if (callback != null) { - callback(user, password); - } -}) + app.on('before-quit', function() { + willAppQuit = true; + }); -app.on('login', function(event, webContents, request, authInfo, callback) { - event.preventDefault(); - loginCallbackMap.set(JSON.stringify(request), callback); - mainWindow.webContents.send('login-request', request, authInfo); -}); + 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; + } -// This method will be called when Electron has finished -// initialization and is ready to create browser windows. -app.on('ready', function() { - if (shouldShowTrayIcon()) { - // set up tray icon - trayIcon = new Tray(trayImages.normal); - trayIcon.setToolTip(app.getName()); - var tray_menu = require('./main/menus/tray').createDefault(); - trayIcon.setContextMenu(tray_menu); - trayIcon.on('click', function() { - mainWindow.focus(); - }); - trayIcon.on('balloon-click', function() { - mainWindow.focus(); - }); - ipc.on('notified', function(event, arg) { - trayIcon.displayBalloon({ - icon: path.resolve(__dirname, 'resources/appicon.png'), - title: arg.title, - content: arg.options.body + 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); + } + }); - // Set overlay icon from dataURL - // Set trayicon to show "dot" - ipc.on('update-unread', function(event, arg) { - if (process.platform === 'win32') { - const overlay = arg.overlayDataURL ? electron.nativeImage.createFromDataURL(arg.overlayDataURL) : null; - mainWindow.setOverlayIcon(overlay, arg.description); - } + const loginCallbackMap = new Map(); - if (arg.mentionCount > 0) { - trayIcon.setImage(trayImages.mention); - } - else if (arg.unreadCount > 0) { - trayIcon.setImage(trayImages.unread); - } - else { - trayIcon.setImage(trayImages.normal); - } - }); - } + ipc.on('login-credentials', function(event, request, user, password) { + const callback = loginCallbackMap.get(JSON.stringify(request)); + if (callback != null) { + callback(user, password); + } + }) - // Create the browser window. - var bounds_info_path = path.resolve(app.getPath("userData"), "bounds-info.json"); - var window_options; - try { - window_options = JSON.parse(fs.readFileSync(bounds_info_path, 'utf-8')); - } - catch (e) { - // follow Electron's defaults - window_options = {}; - } - 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; - mainWindow = new BrowserWindow(window_options); - mainWindow.setFullScreenable(true); // fullscreenable option has no effect. - if (window_options.maximized) { - mainWindow.maximize(); - } - if (window_options.fullscreen) { - mainWindow.setFullScreen(true); - } + app.on('login', function(event, webContents, request, authInfo, callback) { + event.preventDefault(); + loginCallbackMap.set(JSON.stringify(request), callback); + mainWindow.webContents.send('login-request', request, authInfo); + }); - // and load the index.html of the app. - mainWindow.loadURL('file://' + __dirname + '/browser/index.html'); + // This method will be called when Electron has finished + // initialization and is ready to create browser windows. + app.on('ready', function() { + if (shouldShowTrayIcon()) { + // set up tray icon + trayIcon = new Tray(trayImages.normal); + trayIcon.setToolTip(app.getName()); + var tray_menu = require('./main/menus/tray').createDefault(); + trayIcon.setContextMenu(tray_menu); + trayIcon.on('click', function() { + mainWindow.focus(); + }); + trayIcon.on('balloon-click', function() { + mainWindow.focus(); + }); + ipc.on('notified', function(event, arg) { + trayIcon.displayBalloon({ + icon: path.resolve(__dirname, 'resources/appicon.png'), + title: arg.title, + content: arg.options.body + }); + }); + + // Set overlay icon from dataURL + // Set trayicon to show "dot" + ipc.on('update-unread', function(event, arg) { + if (process.platform === 'win32') { + const overlay = arg.overlayDataURL ? electron.nativeImage.createFromDataURL(arg.overlayDataURL) : null; + mainWindow.setOverlayIcon(overlay, arg.description); + } - // Open the DevTools. - // mainWindow.openDevTools(); + if (arg.mentionCount > 0) { + trayIcon.setImage(trayImages.mention); + } + else if (arg.unreadCount > 0) { + trayIcon.setImage(trayImages.unread); + } + else { + trayIcon.setImage(trayImages.normal); + } + }); + } - var saveWindowState = function(file, window) { - var window_state = window.getBounds(); - window_state.maximized = window.isMaximized(); - window_state.fullscreen = window.isFullScreen(); + // Create the browser window. + var bounds_info_path = path.resolve(app.getPath("userData"), "bounds-info.json"); + var window_options; try { - fs.writeFileSync(bounds_info_path, JSON.stringify(window_state)); + window_options = JSON.parse(fs.readFileSync(bounds_info_path, 'utf-8')); } catch (e) { - // [Linux] error happens only when the window state is changed before the config dir is creatied. + // follow Electron's defaults + window_options = {}; } - }; - - mainWindow.on('close', function(event) { - if (willAppQuit) { // when [Ctrl|Cmd]+Q - saveWindowState(bounds_info_path, mainWindow); + 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'); } - else { // Minimize or hide the window for close button. - event.preventDefault(); - switch (process.platform) { - case 'win32': - case 'linux': - mainWindow.minimize(); - break; - case 'darwin': - mainWindow.hide(); - break; - default: - } + window_options.fullScreenable = true; + mainWindow = new BrowserWindow(window_options); + mainWindow.setFullScreenable(true); // fullscreenable option has no effect. + if (window_options.maximized) { + mainWindow.maximize(); + } + if (window_options.fullscreen) { + mainWindow.setFullScreen(true); } - }); - // App should save bounds when a window is closed. - // However, 'close' is not fired in some situations(shutdown, ctrl+c) - // because main process is killed in such situations. - // 'blur' event was effective in order to avoid this. - // Ideally, app should detect that OS is shutting down. - mainWindow.on('blur', function() { - saveWindowState(bounds_info_path, mainWindow); - }); + // and load the index.html of the app. + mainWindow.loadURL('file://' + __dirname + '/browser/index.html'); - var app_menu = appMenu.createMenu(mainWindow); - Menu.setApplicationMenu(app_menu); + // Open the DevTools. + // mainWindow.openDevTools(); - // Emitted when the window is closed. - mainWindow.on('closed', function() { - // Dereference the window object, usually you would store windows - // in an array if your app supports multi windows, this is the time - // when you should delete the corresponding element. - mainWindow = null; - }); + var saveWindowState = function(file, window) { + var window_state = window.getBounds(); + window_state.maximized = window.isMaximized(); + window_state.fullscreen = window.isFullScreen(); + try { + fs.writeFileSync(bounds_info_path, JSON.stringify(window_state)); + } + catch (e) { + // [Linux] error happens only when the window state is changed before the config dir is creatied. + } + }; - // Deny drag&drop navigation in mainWindow. - // Drag&drop is allowed in webview of index.html. - mainWindow.webContents.on('will-navigate', function(event, url) { - var dirname = __dirname; - if (process.platform === 'win32') { - dirname = '/' + dirname.replace(/\\/g, '/'); - } + mainWindow.on('close', function(event) { + if (willAppQuit) { // when [Ctrl|Cmd]+Q + saveWindowState(bounds_info_path, mainWindow); + } + else { // Minimize or hide the window for close button. + event.preventDefault(); + switch (process.platform) { + case 'win32': + case 'linux': + mainWindow.minimize(); + break; + case 'darwin': + mainWindow.hide(); + break; + default: + } + } + }); - var index = url.indexOf('file://' + dirname); - if (index !== 0) { - event.preventDefault(); - } + // App should save bounds when a window is closed. + // However, 'close' is not fired in some situations(shutdown, ctrl+c) + // because main process is killed in such situations. + // 'blur' event was effective in order to avoid this. + // Ideally, app should detect that OS is shutting down. + mainWindow.on('blur', function() { + saveWindowState(bounds_info_path, mainWindow); + }); + + var app_menu = appMenu.createMenu(mainWindow); + Menu.setApplicationMenu(app_menu); + + // Emitted when the window is closed. + mainWindow.on('closed', function() { + // Dereference the window object, usually you would store windows + // in an array if your app supports multi windows, this is the time + // when you should delete the corresponding element. + mainWindow = null; + }); + + // Deny drag&drop navigation in mainWindow. + // Drag&drop is allowed in webview of index.html. + mainWindow.webContents.on('will-navigate', function(event, url) { + var dirname = __dirname; + if (process.platform === 'win32') { + dirname = '/' + dirname.replace(/\\/g, '/'); + } + + var index = url.indexOf('file://' + dirname); + if (index !== 0) { + event.preventDefault(); + } + }); }); -}); +} + +// Handle Squirrel on Windows startup events +switch (process.argv[1]) { + case '--squirrel-install': + autoUpdater.createShortcut(function () { app.quit() }) + break + case '--squirrel-uninstall': + autoUpdater.removeShortcut(function () { app.quit() }) + break + case '--squirrel-obsolete': + case '--squirrel-updated': + app.quit() + break + default: + initialize() +} \ No newline at end of file