├── osx └── create-dmg │ ├── .gitignore │ ├── sample │ ├── builder │ └── create-dmg.builder │ ├── LICENSE │ ├── support │ ├── template.applescript │ └── dmg-license.py │ ├── README.md │ └── create-dmg ├── icons ├── logo.png ├── AppIcon.icns ├── dmg-bg.png └── gitter.ico ├── .jshintrc ├── nwapp ├── img │ ├── logo.png │ ├── logo16.png │ ├── favicon.ico │ ├── icon-logo-connected.png │ ├── icon-logo-connected@2x.png │ ├── icon-logo-disconnected.png │ ├── osx-icon-logo-connected.tiff │ ├── win-icon-logo-connected.ico │ ├── icon-logo-connected-unread.png │ ├── icon-logo-disconnected@2x.png │ ├── linux-icon-logo-connected.png │ ├── win-icon-logo-disconnected.ico │ ├── icon-logo-connected-selected.png │ ├── icon-logo-connected-unread@2x.png │ ├── linux-icon-logo-disconnected.png │ ├── osx-icon-logo-disconnected.tiff │ ├── icon-logo-connected-selected@2x.png │ ├── osx-icon-logo-connected-unread.tiff │ ├── win-icon-logo-connected-unread.ico │ ├── linux-icon-logo-connected-unread.png │ ├── osx-icon-logo-connected-selected.tiff │ └── win-icon-logo-connected-selected.ico ├── audio │ ├── gitter-long.ogg │ └── gitter-ping.ogg ├── utils │ ├── custom-events.js │ ├── sounds.js │ ├── client-type.js │ ├── quit-app.js │ ├── trace.js │ ├── settings.js │ ├── notifier.js │ └── auto-update.js ├── main.css ├── index.html ├── loggedin.html ├── loggedin.js ├── oauth.html ├── icons.json ├── components │ ├── sound-items.js │ ├── context-menu.js │ ├── tray.js │ ├── menu-items.js │ ├── menu.js │ └── tray-menu.js ├── package.json ├── lib │ └── login-view.js └── index.js ├── .travis.yml ├── README.md ├── linux ├── README.md ├── gitter.desktop ├── after-remove.sh └── after-install.sh ├── .gitignore ├── .jsbeautifyrc ├── snap └── snapcraft.yaml ├── latest-template.html ├── windows ├── build.bat ├── README.md ├── gitter.iss └── build.js ├── bootstrap.sh ├── package.json ├── CHANGELOG.md └── gulpfile.js /osx/create-dmg/.gitignore: -------------------------------------------------------------------------------- 1 | .svn 2 | -------------------------------------------------------------------------------- /icons/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitterHQ/desktop/HEAD/icons/logo.png -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "unused": true 5 | } 6 | -------------------------------------------------------------------------------- /icons/AppIcon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitterHQ/desktop/HEAD/icons/AppIcon.icns -------------------------------------------------------------------------------- /icons/dmg-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitterHQ/desktop/HEAD/icons/dmg-bg.png -------------------------------------------------------------------------------- /icons/gitter.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitterHQ/desktop/HEAD/icons/gitter.ico -------------------------------------------------------------------------------- /nwapp/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitterHQ/desktop/HEAD/nwapp/img/logo.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | git: 5 | depth: 10 6 | 7 | -------------------------------------------------------------------------------- /nwapp/img/logo16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitterHQ/desktop/HEAD/nwapp/img/logo16.png -------------------------------------------------------------------------------- /nwapp/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitterHQ/desktop/HEAD/nwapp/img/favicon.ico -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This project has now moved to [GitLab](https://gitlab.com/gitlab-org/gitter/desktop/). 2 | -------------------------------------------------------------------------------- /nwapp/audio/gitter-long.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitterHQ/desktop/HEAD/nwapp/audio/gitter-long.ogg -------------------------------------------------------------------------------- /nwapp/audio/gitter-ping.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitterHQ/desktop/HEAD/nwapp/audio/gitter-ping.ogg -------------------------------------------------------------------------------- /nwapp/img/icon-logo-connected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitterHQ/desktop/HEAD/nwapp/img/icon-logo-connected.png -------------------------------------------------------------------------------- /nwapp/img/icon-logo-connected@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitterHQ/desktop/HEAD/nwapp/img/icon-logo-connected@2x.png -------------------------------------------------------------------------------- /nwapp/img/icon-logo-disconnected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitterHQ/desktop/HEAD/nwapp/img/icon-logo-disconnected.png -------------------------------------------------------------------------------- /nwapp/img/osx-icon-logo-connected.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitterHQ/desktop/HEAD/nwapp/img/osx-icon-logo-connected.tiff -------------------------------------------------------------------------------- /nwapp/img/win-icon-logo-connected.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitterHQ/desktop/HEAD/nwapp/img/win-icon-logo-connected.ico -------------------------------------------------------------------------------- /nwapp/img/icon-logo-connected-unread.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitterHQ/desktop/HEAD/nwapp/img/icon-logo-connected-unread.png -------------------------------------------------------------------------------- /nwapp/img/icon-logo-disconnected@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitterHQ/desktop/HEAD/nwapp/img/icon-logo-disconnected@2x.png -------------------------------------------------------------------------------- /nwapp/img/linux-icon-logo-connected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitterHQ/desktop/HEAD/nwapp/img/linux-icon-logo-connected.png -------------------------------------------------------------------------------- /nwapp/img/win-icon-logo-disconnected.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitterHQ/desktop/HEAD/nwapp/img/win-icon-logo-disconnected.ico -------------------------------------------------------------------------------- /nwapp/img/icon-logo-connected-selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitterHQ/desktop/HEAD/nwapp/img/icon-logo-connected-selected.png -------------------------------------------------------------------------------- /nwapp/img/icon-logo-connected-unread@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitterHQ/desktop/HEAD/nwapp/img/icon-logo-connected-unread@2x.png -------------------------------------------------------------------------------- /nwapp/img/linux-icon-logo-disconnected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitterHQ/desktop/HEAD/nwapp/img/linux-icon-logo-disconnected.png -------------------------------------------------------------------------------- /nwapp/img/osx-icon-logo-disconnected.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitterHQ/desktop/HEAD/nwapp/img/osx-icon-logo-disconnected.tiff -------------------------------------------------------------------------------- /linux/README.md: -------------------------------------------------------------------------------- 1 | # Linux related assets 2 | 3 | - `after-install` and `after-remove` scripts 4 | - `gitter.desktop` for the Unity Launcher 5 | -------------------------------------------------------------------------------- /nwapp/img/icon-logo-connected-selected@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitterHQ/desktop/HEAD/nwapp/img/icon-logo-connected-selected@2x.png -------------------------------------------------------------------------------- /nwapp/img/osx-icon-logo-connected-unread.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitterHQ/desktop/HEAD/nwapp/img/osx-icon-logo-connected-unread.tiff -------------------------------------------------------------------------------- /nwapp/img/win-icon-logo-connected-unread.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitterHQ/desktop/HEAD/nwapp/img/win-icon-logo-connected-unread.ico -------------------------------------------------------------------------------- /nwapp/img/linux-icon-logo-connected-unread.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitterHQ/desktop/HEAD/nwapp/img/linux-icon-logo-connected-unread.png -------------------------------------------------------------------------------- /nwapp/img/osx-icon-logo-connected-selected.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitterHQ/desktop/HEAD/nwapp/img/osx-icon-logo-connected-selected.tiff -------------------------------------------------------------------------------- /nwapp/img/win-icon-logo-connected-selected.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitterHQ/desktop/HEAD/nwapp/img/win-icon-logo-connected-selected.ico -------------------------------------------------------------------------------- /nwapp/utils/custom-events.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var EventEmitter = require('events').EventEmitter; 4 | var events = new EventEmitter(); 5 | 6 | module.exports = events; 7 | -------------------------------------------------------------------------------- /linux/gitter.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Gitter 3 | Comment=Where developers come to talk 4 | Exec=/opt/Gitter/{{arch}}/Gitter 5 | Icon=/opt/Gitter/{{arch}}/logo.png 6 | Terminal=false 7 | Type=Application 8 | Categories=Utility; 9 | -------------------------------------------------------------------------------- /nwapp/main.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0px; 3 | padding: 0px; 4 | width: 100%; 5 | height: 100%; 6 | font-family: helvetica, arial, sans-serif; 7 | } 8 | 9 | iframe { 10 | width: 100%; 11 | height: 100%; 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .ruby-version 3 | *.sw* 4 | *.deb 5 | *.dmg 6 | keys.json 7 | isolate-*-v8.log 8 | node_modules 9 | latest 10 | cache 11 | output 12 | opt 13 | nwapp/oauth.json 14 | certificates/* 15 | artefacts/* 16 | npm-debug.log 17 | env.sh 18 | -------------------------------------------------------------------------------- /nwapp/utils/sounds.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = [ 4 | { 5 | label: 'None', 6 | path: null, 7 | }, 8 | { 9 | label: 'Gitter Default', 10 | path: '../audio/gitter-long.ogg' 11 | }, 12 | { 13 | label: 'Gitter Ping', 14 | path: '../audio/gitter-ping.ogg' 15 | } 16 | ]; 17 | -------------------------------------------------------------------------------- /nwapp/utils/client-type.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var os = require('os'); 4 | 5 | module.exports = (function () { 6 | var platform = os.platform(); 7 | if (platform.match(/darwin/)) return 'osx'; 8 | if (platform.match(/^win/)) return 'win'; 9 | if (platform.match(/linux/)) return 'linux'; 10 | return os; 11 | })(); 12 | -------------------------------------------------------------------------------- /nwapp/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Gitter 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /osx/create-dmg/sample: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | test -f test2.dmg && rm test2.dmg 3 | ./create-dmg --window-size 500 300 --background ~/Projects/eclipse-osx-repackager/build/background.gif --icon-size 96 --volname "Hyper Foo" --app-drop-link 380 205 --icon "Eclipse OS X Repackager" 110 205 test2.dmg /Users/andreyvit/Projects/eclipse-osx-repackager/temp/Eclipse\ OS\ X\ Repackager\ r10/ 4 | -------------------------------------------------------------------------------- /nwapp/utils/quit-app.js: -------------------------------------------------------------------------------- 1 | var log = require('loglevel'); 2 | var gui = window.require('nw.gui'); 3 | var events = require('./custom-events'); 4 | 5 | // We wrap their quit method as it doesn't emit a `quit` or `close` event that 6 | // we can listen to and do any proper cleanup 7 | module.exports = function() { 8 | log.info('Quitting app, app:quit'); 9 | events.emit('app:quit'); 10 | 11 | gui.App.quit(); 12 | }; 13 | -------------------------------------------------------------------------------- /nwapp/loggedin.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Gitter 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.jsbeautifyrc: -------------------------------------------------------------------------------- 1 | { 2 | "indent_with_tabs": false, 3 | "preserve_newlines": true, 4 | "max_preserve_newlines": 10, 5 | "jslint_happy": false, 6 | "space_after_anon_function": false, 7 | "brace_style": "collapse", 8 | "keep_array_indentation": true, 9 | "keep_function_indentation": false, 10 | "space_before_conditional": true, 11 | "break_chained_methods": false, 12 | "eval_code": false, 13 | "unescape_strings": false, 14 | "wrap_line_length": 0 15 | } 16 | -------------------------------------------------------------------------------- /snap/snapcraft.yaml: -------------------------------------------------------------------------------- 1 | name: gitter 2 | version: master 3 | summary: Where developers come to talk 4 | description: gitter desktop client 5 | 6 | grade: devel # must be 'stable' to release into candidate/stable channels 7 | confinement: classic 8 | 9 | apps: 10 | gitter: 11 | command: opt/Gitter/linux64/Gitter 12 | plugs: [browser-support, network, network-bind, x11, opengl] 13 | 14 | parts: 15 | desktop: 16 | source: https://update.gitter.im/linux64/gitter_3.1.0_amd64.deb 17 | source-type: deb 18 | plugin: dump 19 | -------------------------------------------------------------------------------- /latest-template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | Page Redirection 10 | 11 | 12 |

If you are not redirected automatically, follow the link to download Gitter

13 | 14 | 15 | -------------------------------------------------------------------------------- /nwapp/loggedin.js: -------------------------------------------------------------------------------- 1 | var gui = require('nw.gui'); 2 | var win = gui.Window.get(); 3 | var frame = win.window.document.getElementById('mainframe'); 4 | var settings = require('./utils/settings'); 5 | var version = require('./package.json').version; 6 | 7 | // FIXME This is a hack that allows us to send auth token in the user-agent header. 8 | // The nwUserAgent will be used for the iframe and all the requests that originate inside it 9 | frame.nwUserAgent = navigator.userAgent + ' Gitter/' + version + ' Gitter Token/' + settings.token; 10 | 11 | frame.src = 'https://gitter.im/'; 12 | -------------------------------------------------------------------------------- /linux/after-remove.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # removing what we added on after-install 4 | libpaths=( 5 | "/lib/x86_64-linux-gnu" # Ubuntu, Xubuntu, Mint 6 | "/usr/lib64" # SUSE, Fedora 7 | "/usr/lib" # Arch, Fedora 32bit 8 | "/lib/i386-linux-gnu" # Ubuntu 32bit 9 | ) 10 | 11 | for i in "${libpaths[@]}" 12 | do 13 | if [ -f "$i/libudev.so.0" ] 14 | then 15 | rm "$i/libudev.so.0" 16 | break 17 | fi 18 | done 19 | 20 | # Link to the Gitter binary 21 | rm -f /usr/local/bin/gitter 22 | 23 | # Unity Launcher icon 24 | rm -f /usr/share/applications/gitter.desktop 25 | -------------------------------------------------------------------------------- /linux/after-install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | libpaths=( 4 | "/lib/x86_64-linux-gnu" # Ubuntu, Xubuntu, Mint 5 | "/usr/lib64" # SUSE, Fedora 6 | "/usr/lib" # Arch, Fedora 32bit 7 | "/lib/i386-linux-gnu" # Ubuntu 32bit 8 | ) 9 | 10 | for i in "${libpaths[@]}" 11 | do 12 | if [ -f "$i/libudev.so.1" ] 13 | then 14 | ln -sf "$i/libudev.so.1" "$i/libudev.so.0" 15 | break 16 | fi 17 | done 18 | 19 | # Link to the Gitter binary 20 | ln -sf /opt/Gitter/{{arch}}/Gitter /usr/local/bin/gitter 21 | 22 | # Unity Launcher icon 23 | desktop-file-install /opt/Gitter/{{arch}}/gitter.desktop 24 | -------------------------------------------------------------------------------- /nwapp/oauth.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Gitter Login 4 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /windows/build.bat: -------------------------------------------------------------------------------- 1 | echo "Parameter 1: version x.x.x" 2 | echo "Parameter 2: .pfx password" 3 | 4 | if "%1"=="" ( 5 | echo "please provide a version x.x.x" 6 | ) else ( 7 | "C:\Program Files (x86)\Microsoft SDKs\Windows\v7.1A\Bin\signtool.exe" sign /f "%cd%\certificates\troupe-cert.pfx" /p "%2" "%cd%\opt\Gitter\win32\Gitter.exe" 8 | "C:\Program Files (x86)\Inno Setup 5\ISCC.exe" "%cd%\windows\gitter.iss" 9 | "C:\Program Files (x86)\Microsoft SDKs\Windows\v7.1A\Bin\signtool.exe" sign /f "%cd%\certificates\troupe-cert.pfx" /p "%2" "%cd%\artefacts\GitterSetup*" 10 | rename "%cd%\artefacts\GitterSetup.exe" "GitterSetup-%1%.exe" 11 | ) 12 | -------------------------------------------------------------------------------- /osx/create-dmg/builder/create-dmg.builder: -------------------------------------------------------------------------------- 1 | SET app_name create-dmg 2 | 3 | VERSION create-dmg.cur create-dmg heads/master 4 | 5 | NEWDIR build.dir temp %-build - 6 | 7 | NEWFILE create-dmg.zip featured %.zip % 8 | 9 | 10 | COPYTO [build.dir] 11 | INTO create-dmg [create-dmg.cur]/create-dmg 12 | INTO sample [create-dmg.cur]/sample 13 | INTO support [create-dmg.cur]/support 14 | 15 | SUBSTVARS [build.dir]/create-dmg [[]] 16 | 17 | 18 | ZIP [create-dmg.zip] 19 | INTO [build-files-prefix] [build.dir] 20 | 21 | 22 | PUT megabox-builds create-dmg.zip 23 | PUT megabox-builds build.log 24 | 25 | PUT s3-builds create-dmg.zip 26 | PUT s3-builds build.log 27 | -------------------------------------------------------------------------------- /bootstrap.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Installing dependencies... (assuming XCode is installed)" 4 | 5 | echo "Installing awscli via Brew" 6 | brew install awscli 7 | 8 | echo "Installing Wine via Brew" 9 | brew install wine 10 | 11 | echo "Installing Gnu-tar via Brew" 12 | brew install gnu-tar 13 | 14 | echo "Installing npm deps for the app" 15 | (cd nwapp && npm install) 16 | 17 | echo "Installing packaging deps" 18 | npm install 19 | 20 | echo "Installing fpm Ruby gem" 21 | sudo gem install fpm 22 | 23 | echo "Cloning DMG tools" 24 | git remote add create-dmg git@github.com:andreyvit/create-dmg.git 25 | git subtree add --prefix=osx/create-dmg --squash create-dmg master 26 | 27 | echo "Done. If you need to configure AWS CLI tools, run: aws configure" 28 | -------------------------------------------------------------------------------- /nwapp/icons.json: -------------------------------------------------------------------------------- 1 | { 2 | "osx" : { 3 | "disconnected" : "img/osx-icon-logo-disconnected.tiff", 4 | "connected" : "img/osx-icon-logo-connected.tiff", 5 | "unread" : "img/osx-icon-logo-connected-unread.tiff", 6 | "selected" : "img/osx-icon-logo-connected-selected.tiff" 7 | }, 8 | "win" : { 9 | "disconnected" : "img/icon-logo-disconnected.png", 10 | "connected" : "img/icon-logo-connected.png", 11 | "unread" : "img/icon-logo-connected-unread.png", 12 | "selected" : "img/icon-logo-connected-selected.png" 13 | }, 14 | "linux" : { 15 | "disconnected" : "img/linux-icon-logo-disconnected.png", 16 | "connected" : "img/linux-icon-logo-connected.png", 17 | "unread" : "img/linux-icon-logo-connected-unread.png", 18 | "selected" : "img/linux-icon-logo-connected.png" 19 | } 20 | } 21 | 22 | 23 | -------------------------------------------------------------------------------- /nwapp/components/sound-items.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var events = require('../utils/custom-events'); 4 | var settings = require('../utils/settings'); 5 | var SOUNDS = require('../utils/sounds'); 6 | 7 | // this array will be MenuItems thus the mapping is to add defaults values 8 | var SOUND_ITEMS = SOUNDS 9 | .map(function (sound, index) { 10 | sound.type = 'checkbox'; 11 | sound.checked = settings.notificationSound === index; 12 | sound.click = function (e) { 13 | settings.notificationSound = index; 14 | }; 15 | return sound; 16 | }); 17 | 18 | // update the checked sounds on change 19 | events.on('settings:change:notificationSound', function () { 20 | SOUND_ITEMS.forEach(function (sound, index) { 21 | sound.checked = settings.notificationSound === index; 22 | }); 23 | }); 24 | 25 | module.exports = SOUND_ITEMS; 26 | -------------------------------------------------------------------------------- /nwapp/components/context-menu.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gui = window.require('nw.gui'); 4 | var menu = new gui.Menu(); 5 | 6 | function Menu(cutLabel, copyLabel, pasteLabel) { 7 | var cut = new gui.MenuItem({ 8 | label: cutLabel || "Cut", 9 | click: function() { 10 | document.execCommand("cut"); 11 | } 12 | }); 13 | 14 | var copy = new gui.MenuItem({ 15 | label: copyLabel || "Copy", 16 | click: function() { 17 | document.execCommand("copy"); 18 | } 19 | }); 20 | 21 | var paste = new gui.MenuItem({ 22 | label: pasteLabel || "Paste", 23 | click: function() { 24 | document.execCommand("paste"); 25 | } 26 | }) 27 | ; 28 | 29 | menu.append(cut); 30 | menu.append(copy); 31 | menu.append(paste); 32 | 33 | return menu; 34 | } 35 | 36 | 37 | module.exports = new Menu(); 38 | 39 | -------------------------------------------------------------------------------- /osx/create-dmg/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2008-2014 Andrey Tarantsov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /nwapp/components/tray.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gui = window.require('nw.gui'); 4 | var settings = require('../utils/settings'); 5 | var events = require('../utils/custom-events'); 6 | 7 | var CLIENT_TYPE = require('../utils/client-type'); 8 | var icon = require('../icons.json')[CLIENT_TYPE]; 9 | 10 | function CustomTray() { 11 | this.tray = new gui.Tray({ 12 | icon: icon.disconnected, 13 | alticon: icon.selected, 14 | iconsAreTemplates: false 15 | }); 16 | 17 | events.on('user:signedOut', this.disconnect.bind(this)); 18 | events.on('user:signedIn', this.connect.bind(this)); 19 | events.on('traymenu:read', this.connect.bind(this)); 20 | events.on('traymenu:unread', this.unread.bind(this)); 21 | } 22 | 23 | CustomTray.prototype.setIcon = function(icon) { 24 | this.tray.icon = icon; 25 | }; 26 | 27 | CustomTray.prototype.get = function() { 28 | return this.tray; 29 | }; 30 | CustomTray.prototype.disconnect = function() { 31 | this.setIcon(icon.disconnected); 32 | }; 33 | CustomTray.prototype.connect = function() { 34 | if (!settings.token) return; // user is signed out 35 | this.setIcon(icon.connected); 36 | }; 37 | CustomTray.prototype.unread = function() { 38 | this.setIcon(icon.unread); 39 | }; 40 | 41 | 42 | module.exports = CustomTray; 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gitter-desktop", 3 | "version": "0.0.1", 4 | "nwversion": "0.12.3", 5 | "description": "Gitter nw.js builder", 6 | "main": "index.js", 7 | "dependencies": { 8 | "gulp": "^3.8.10", 9 | "gulp-shell": "^0.2.11", 10 | "gulp-util": "^3.0.1", 11 | "lodash.template": "^3.0.1", 12 | "nw-builder": "^2.2.0", 13 | "semver": "^4.2.0" 14 | }, 15 | "devDependencies": { 16 | "bluebird": "^3.2.2", 17 | "npm": "^3.7.3", 18 | "nw": "^0.12.3", 19 | "rimraf": "^2.2.8", 20 | "s3": "^4.3.1", 21 | "stream-exhaust": "^1.0.1", 22 | "through2": "^2.0.1", 23 | "yargs": "^3.32.0" 24 | }, 25 | "scripts": { 26 | "postinstall": "(cd nwapp && npm install)", 27 | "build": "gulp build", 28 | "start": "nw nwapp" 29 | }, 30 | "author": "Mauro Pompilio", 31 | "license": "MIT", 32 | "copyright": "copyright flag required by node-webkit-builder", 33 | "directories": { 34 | "test": "test" 35 | }, 36 | "repository": { 37 | "type": "git", 38 | "url": "https://github.com/gitterHQ/desktop.git" 39 | }, 40 | "keywords": [ 41 | "gitter", 42 | "desktop", 43 | "osx", 44 | "windows", 45 | "linux" 46 | ], 47 | "bugs": { 48 | "url": "https://github.com/gitterHQ/desktop/issues" 49 | }, 50 | "homepage": "https://github.com/gitterHQ/desktop" 51 | } 52 | -------------------------------------------------------------------------------- /nwapp/utils/trace.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* Utility class for tracing calls */ 4 | var log = require('loglevel'); 5 | 6 | function wrapFunction(name, fn) { 7 | if (typeof fn !== 'function') throw new Error('Cannot wrap ' + fn); 8 | return function traceWrapper() { 9 | log.trace('enter::' + name); 10 | try { 11 | var args = Array.prototype.slice.apply(arguments); 12 | return fn.apply(this, args); 13 | } catch(e) { 14 | log.trace('error::' + e); 15 | throw e; 16 | } finally { 17 | log.trace('exit::' + name); 18 | } 19 | }; 20 | } 21 | 22 | function wrapObject(object, name) { 23 | if (object.___traced) return object; 24 | 25 | // Attempt to figure out the name 26 | if (!name) name = object.constructor && object.constructor.name; 27 | 28 | Object.defineProperty(object, '___traced', { configurable: true, enumerable: false, value: true }); 29 | var o = {}; // Base object 30 | 31 | Object.keys(object).forEach(function(k) { 32 | if (o.hasOwnProperty(k)) return; 33 | 34 | if (typeof object[k] === 'function') { 35 | object[k] = wrapFunction(name + '::' + k, object[k]); 36 | } 37 | }); 38 | if (object.prototype) { 39 | wrapObject(object.prototype, name); 40 | } 41 | 42 | return object; 43 | } 44 | 45 | module.exports = { 46 | wrapFunction: wrapFunction, 47 | wrapObject: wrapObject, 48 | }; 49 | -------------------------------------------------------------------------------- /windows/README.md: -------------------------------------------------------------------------------- 1 | # Windows build 2 | 3 | On a Windows machine: 4 | 5 | - Install Inno Setup: http://www.jrsoftware.org/isdl.php 6 | - Install Microsoft SDK: http://www.microsoft.com/en-gb/download/details.aspx?id=8279 7 | - Checkout the project or use networked file sharing, for example from your OS X machine to the Windows machine: https://support.apple.com/kb/PH18707. [Just make sure the path doesn't have any spaces](https://github.com/nodejs/node/issues/5160). 8 | - Also ensure that you have ran `gulp build` and that the `opt/Gitter/win32` directory exists and has `Gitter.exe` inside of it, see https://github.com/gitterHQ/desktop#releasing-the-app-win32-and-linux3264 9 | - Run `node "\\ERIC-MACBOOK\eric\Documents\github\desktop\windows\build.js" -p thepfxcertpasswordhere` 10 | 11 | You should end up with a `GitterSetup-x.x.x.exe` installer inside `./artefacts/` 12 | 13 | ## Code Sign 14 | 15 | You'll need to sign the Gitter.exe binary and the installer separately. 16 | 17 | ### SignTool (method used in build.js) 18 | 19 | Install Microsoft SDK from: http://www.microsoft.com/en-gb/download/details.aspx?id=8279 20 | 21 | Usage: `signtool.exe sign /f troupe-cert.pfx binary.exe` 22 | 23 | ### kSign 24 | 25 | Has a GUI. Very easy to use. Just use the cert provided: `troupe-cert.pfx` 26 | 27 | ## Inno Setup 28 | 29 | `gitter.iss` contains an Inno Setup manifest to generate an installer for Windows. 30 | 31 | If you need to modify the Inno Setup file, you can use the Inno Script Studio (or a text editor if you know the config): https://www.kymoto.org/products/inno-script-studio 32 | 33 | Get Inno Setup from: http://www.jrsoftware.org/isdl.php 34 | 35 | Generate an installer with: `"C:\Program Files\Inno Setup 5\ISCC.exe" "C:\gitter-desktop\gitter.iss` 36 | -------------------------------------------------------------------------------- /windows/gitter.iss: -------------------------------------------------------------------------------- 1 | ; Script generated by the Inno Setup Script Wizard. 2 | ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! 3 | 4 | #define MyAppName "Gitter" 5 | #define MyAppPublisher "Troupe Technology Limited" 6 | #define MyAppURL "https://www.gitter.im" 7 | #define MyAppExeName "Gitter.exe" 8 | 9 | [Setup] 10 | ; NOTE: The value of AppId uniquely identifies this application. 11 | ; Do not use the same AppId value in installers for other applications. 12 | ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) 13 | AppId={{03C07717-35D4-40B2-B4F2-05A0EF1B9F6F} 14 | AppName={#MyAppName} 15 | ;AppVersion={#MyAppVersion} 16 | AppVerName={#MyAppName} 17 | AppPublisher={#MyAppPublisher} 18 | AppPublisherURL={#MyAppURL} 19 | AppSupportURL={#MyAppURL} 20 | AppUpdatesURL={#MyAppURL} 21 | DefaultDirName={userpf}\{#MyAppName} 22 | UninstallFilesDir={userpf}\GitterUninstaller 23 | DefaultGroupName={#MyAppName} 24 | OutputBaseFilename=GitterSetup 25 | Compression=lzma 26 | SolidCompression=yes 27 | ;SignTool=SignTool 28 | OutputDir=..\artefacts 29 | ShowLanguageDialog=no 30 | PrivilegesRequired=lowest 31 | 32 | [Languages] 33 | Name: "english"; MessagesFile: "compiler:Default.isl" 34 | 35 | [Tasks] 36 | Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked 37 | Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked; OnlyBelowVersion: 0,6.1 38 | 39 | [Files] 40 | Source: "..\opt\Gitter\win32\Gitter.exe"; DestDir: "{app}"; Flags: ignoreversion 41 | Source: "..\opt\Gitter\win32\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs 42 | ; NOTE: Don't use "Flags: ignoreversion" on any shared system files 43 | 44 | [Icons] 45 | Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" 46 | Name: "{commondesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon 47 | Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: quicklaunchicon 48 | 49 | [Run] 50 | Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent 51 | -------------------------------------------------------------------------------- /nwapp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Gitter", 3 | "description": "Where developers come to talk.", 4 | "version": "3.1.0", 5 | "main": "app://gitter/index.html", 6 | "engineStrict": true, 7 | "engines": { 8 | "npm": "^3.0.0" 9 | }, 10 | "window": { 11 | "icon": "./img/logo.png", 12 | "show": false, 13 | "toolbar": false, 14 | "frame": true, 15 | "width": 1200, 16 | "height": 800, 17 | "position": "center", 18 | "min_width": 200, 19 | "min_height": 200, 20 | "max_width": 8000, 21 | "max_height": 8000 22 | }, 23 | "webkit": { 24 | "plugin": false, 25 | "page-cache": true 26 | }, 27 | "user-agent": "%name %ver %nwver %webkit_ver %osinfo", 28 | "dependencies": { 29 | "auto-launch": "^1.1.1", 30 | "bluebird": "^3.2.1", 31 | "extract-zip": "^1.0.3", 32 | "fs-extra": "^0.26.5", 33 | "gitter-realtime-client": "^1.0.0", 34 | "jfs": "^0.2.6", 35 | "loglevel": "^1.2.0", 36 | "node-notifier": "^4.5.0", 37 | "node-webkit-updater": "^0.3.2", 38 | "node-webkit-winstate": "^1.1.0", 39 | "oauth": "^0.9.12", 40 | "object-assign": "^4.0.1", 41 | "open": "0.0.5", 42 | "request": "^2.69.0", 43 | "rimraf": "^2.4.3", 44 | "semver": "^5.1.0", 45 | "temp": "^0.8.3", 46 | "url-parse": "^1.0.5", 47 | "yargs": "^4.1.0" 48 | }, 49 | "packages": { 50 | "win": { 51 | "url": "https://update.gitter.im/win/win32.zip" 52 | }, 53 | "osx": { 54 | "url": "https://update.gitter.im/osx/osx.zip" 55 | }, 56 | "mac": { 57 | "url": "https://update.gitter.im/osx/osx.zip" 58 | }, 59 | "linux32": { 60 | "url": "https://update.gitter.im/linux32/linux32.zip" 61 | }, 62 | "linux64": { 63 | "url": "https://update.gitter.im/linux64/linux64.zip" 64 | } 65 | }, 66 | "single-instance": false, 67 | "devDependencies": {}, 68 | "scripts": { 69 | "test": "echo \"Error: no test specified\" && exit 1" 70 | }, 71 | "repository": { 72 | "type": "git", 73 | "url": "git@github.com:gitterHQ/desktop.git" 74 | }, 75 | "keywords": [ 76 | "gitter", 77 | "desktop", 78 | "nwapp" 79 | ], 80 | "license": "MIT", 81 | "bugs": { 82 | "url": "https://github.com/gitterHQ/desktop/issues" 83 | }, 84 | "homepage": "https://github.com/gitterHQ/desktop" 85 | } 86 | -------------------------------------------------------------------------------- /osx/create-dmg/support/template.applescript: -------------------------------------------------------------------------------- 1 | on run (volumeName) 2 | tell application "Finder" 3 | tell disk (volumeName as string) 4 | open 5 | 6 | set theXOrigin to WINX 7 | set theYOrigin to WINY 8 | set theWidth to WINW 9 | set theHeight to WINH 10 | 11 | set theBottomRightX to (theXOrigin + theWidth) 12 | set theBottomRightY to (theYOrigin + theHeight) 13 | set dsStore to "\"" & "/Volumes/" & volumeName & "/" & ".DS_STORE\"" 14 | 15 | tell container window 16 | set current view to icon view 17 | set toolbar visible to false 18 | set statusbar visible to false 19 | set the bounds to {theXOrigin, theYOrigin, theBottomRightX, theBottomRightY} 20 | set statusbar visible to false 21 | REPOSITION_HIDDEN_FILES_CLAUSE 22 | end tell 23 | 24 | set opts to the icon view options of container window 25 | tell opts 26 | set icon size to ICON_SIZE 27 | set text size to TEXT_SIZE 28 | set arrangement to not arranged 29 | end tell 30 | BACKGROUND_CLAUSE 31 | 32 | -- Positioning 33 | POSITION_CLAUSE 34 | 35 | -- Hiding 36 | HIDING_CLAUSE 37 | 38 | -- Application Link Clause 39 | APPLICATION_CLAUSE 40 | close 41 | open 42 | 43 | update without registering applications 44 | -- Force saving of the size 45 | delay 1 46 | 47 | tell container window 48 | set statusbar visible to false 49 | set the bounds to {theXOrigin, theYOrigin, theBottomRightX - 10, theBottomRightY - 10} 50 | end tell 51 | 52 | update without registering applications 53 | end tell 54 | 55 | delay 1 56 | 57 | tell disk (volumeName as string) 58 | tell container window 59 | set statusbar visible to false 60 | set the bounds to {theXOrigin, theYOrigin, theBottomRightX, theBottomRightY} 61 | end tell 62 | 63 | update without registering applications 64 | end tell 65 | 66 | --give the finder some time to write the .DS_Store file 67 | delay 3 68 | 69 | set waitTime to 0 70 | set ejectMe to false 71 | repeat while ejectMe is false 72 | delay 1 73 | set waitTime to waitTime + 1 74 | 75 | if (do shell script "[ -f " & dsStore & " ]; echo $?") = "0" then set ejectMe to true 76 | end repeat 77 | log "waited " & waitTime & " seconds for .DS_STORE to be created." 78 | end tell 79 | end run 80 | -------------------------------------------------------------------------------- /nwapp/components/menu-items.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gui = window.require('nw.gui'); 4 | var pkg = require('../package.json'); 5 | var settings = require('../utils/settings'); 6 | var events = require('../utils/custom-events'); 7 | var quitApp = require('../utils/quit-app'); 8 | 9 | var SOUNDS = require('../utils/sounds'); 10 | 11 | module.exports = [ 12 | { 13 | label: 'Gitter v' + pkg.version, 14 | enabled: false, 15 | support: ['linux', 'win'] 16 | }, 17 | { 18 | label: 'Show in Menu Bar', 19 | type: 'checkbox', 20 | checked: settings.showInMacMenuBar, 21 | support: ['osx'], 22 | click: function () { 23 | settings.showInMacMenuBar = !settings.showInMacMenuBar; 24 | } 25 | }, 26 | { 27 | label: 'Launch on startup', 28 | type: 'checkbox', 29 | checked: settings.launchOnStartup, 30 | click: function () { 31 | settings.launchOnStartup = !settings.launchOnStartup; 32 | }, 33 | support: ['linux', 'win', 'osx'] 34 | }, 35 | { 36 | label: 'Launch hidden', 37 | type: 'checkbox', 38 | checked: settings.launchHidden, 39 | click: function () { 40 | settings.launchHidden = !settings.launchHidden; 41 | }, 42 | support: ['linux', 'win', 'osx'] 43 | }, 44 | { 45 | type: 'separator', 46 | }, 47 | { 48 | label: 'Show Notifications', 49 | type: 'checkbox', 50 | checked: settings.showNotifications, 51 | click: function () { 52 | settings.showNotifications = !settings.showNotifications; 53 | }, 54 | auth: true 55 | }, 56 | { 57 | label: 'Notification Sound', 58 | auth: true, 59 | content: SOUNDS 60 | }, 61 | { 62 | label: 'Gitter Next', 63 | type: 'checkbox', 64 | checked: settings.next, 65 | click: events.emit.bind(events, 'menu:toggle:next') 66 | }, 67 | { 68 | type: 'separator', 69 | }, 70 | { 71 | label: 'Developer Tools', 72 | click: events.emit.bind(events, 'menu:toggle:devtools') 73 | }, 74 | { 75 | type: 'separator', 76 | }, 77 | { 78 | label: 'Sign Out', 79 | click: events.emit.bind(events, 'menu:signout'), 80 | auth: true 81 | }, 82 | { 83 | label: 'Exit', 84 | click: quitApp, 85 | support: ['linux', 'win'] 86 | } 87 | ].map(function (item, index) { 88 | item.index = index; // FIXME: unsure whether this is a good way to add index 89 | return item; 90 | }); 91 | -------------------------------------------------------------------------------- /nwapp/utils/settings.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Store = require('jfs'); 4 | var gui = window.require('nw.gui'); 5 | var events = require('./custom-events'); 6 | var log = require('loglevel'); 7 | var SOUNDS = require('./sounds'); 8 | 9 | function isBool(val) { 10 | return typeof val === "boolean"; 11 | } 12 | 13 | // via http://stackoverflow.com/a/1830844/796832 14 | function isNumeric(n) { 15 | return !isNaN(parseFloat(n)) && isFinite(n); 16 | } 17 | 18 | var DEFAULT_SETTINGS = { 19 | showInMacMenuBar: { 20 | value: true, 21 | validate: isBool 22 | }, 23 | launchOnStartup: { 24 | value: true, 25 | validate: isBool 26 | }, 27 | launchHidden: { 28 | value: false, 29 | validate: isBool 30 | }, 31 | next: { 32 | value: false, 33 | validate: isBool 34 | }, 35 | showNotifications: { 36 | value: true, 37 | validate: isBool 38 | }, 39 | notificationSound: { 40 | value: 1, 41 | validate: function (val) { 42 | return val >= 0 && val <= SOUNDS.length; 43 | } 44 | } 45 | }; 46 | 47 | var db = new Store(gui.App.dataPath + '/gitter_preferences.json', { pretty: true }); // FIXME: pretty Boolean - should be environment dependent 48 | 49 | // initial load is done synchronously 50 | var settings = db.getSync('settings'); 51 | 52 | // setting up observable 53 | Object.observe(settings, function (changes) { 54 | // save settings everytime a change is made 55 | db.save('settings', settings, function (err) { 56 | // emit an event which indicates failure or success 57 | if (err) { 58 | log.error('ERROR: Could not save settings.'); 59 | return events.emit('settings:failed', err); 60 | } 61 | events.emit('settings:saved'); 62 | }); 63 | 64 | // in case any component is interested on a particular setting change 65 | changes.forEach(function (change) { 66 | events.emit('settings:change:' + change.name, change.object[change.name]); 67 | }); 68 | 69 | events.emit('settings:change'); 70 | }); 71 | 72 | // performs a check to guarantee health of settings 73 | Object.keys(DEFAULT_SETTINGS) 74 | .forEach(function (key) { 75 | if (!settings.hasOwnProperty(key)) { 76 | settings[key] = DEFAULT_SETTINGS[key].value; 77 | } 78 | 79 | var isValid = DEFAULT_SETTINGS[key].validate(settings[key]); 80 | 81 | if (!isValid) { 82 | log.warn('ERROR: Invalid setting:' + key + '. restoring to default'); 83 | settings[key] = DEFAULT_SETTINGS[key].value; 84 | } 85 | }); 86 | 87 | module.exports = settings; 88 | -------------------------------------------------------------------------------- /nwapp/components/menu.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var events = require('../utils/custom-events'); 4 | var gui = window.require('nw.gui'); 5 | var CLIENT = require('../utils/client-type'); 6 | 7 | // recursively assembles menus and sub-menus 8 | function assembleMenu(items, parent) { 9 | var fold; 10 | 11 | parent = parent ? parent : new gui.Menu(); 12 | 13 | items.forEach(function (item) { 14 | // means that we have a submenu 15 | if (item.content && item.content.length > 0) { 16 | var submenu = new gui.Menu(); 17 | fold = assembleMenu(item.content, submenu); 18 | } else { 19 | fold = new gui.MenuItem(item); 20 | } 21 | 22 | if (fold.type === 'contextmenu') { 23 | item.submenu = fold; 24 | fold = new gui.MenuItem(item); 25 | } 26 | 27 | if (item.index && CLIENT === 'osx') { 28 | parent.insert(fold, item.index); // inserting at specified index 29 | } else { 30 | parent.append(fold); // just append 31 | } 32 | }); 33 | 34 | return parent; 35 | } 36 | 37 | function CustomMenu(spec) { 38 | 39 | this.menu = new gui.Menu({ type: 'menubar' }); 40 | this.items = spec.items || []; 41 | this.filter = spec.filter; 42 | this.label = spec.label; 43 | 44 | events.on('user:signedIn', this.build.bind(this)); 45 | events.on('user:signedOut', this.build.bind(this)); 46 | events.on('settings:saved', this.build.bind(this)); 47 | 48 | this.build(); // initial render 49 | 50 | return this; 51 | } 52 | 53 | // FIXME: this function will probably not work for all platforms 54 | CustomMenu.prototype.clear = function () { 55 | for (var i = this.menu.items.length - 1; i >= 0; i--) { 56 | this.menu.removeAt(i); 57 | } 58 | }; 59 | 60 | CustomMenu.prototype.build = function () { 61 | this.clear(); // clear the menu on every "render" 62 | var filteredItems = this.items; 63 | 64 | if (this.filter && typeof this.filter === 'function') { 65 | filteredItems = this.items.filter(this.filter); 66 | } 67 | 68 | // note that we have different targets based on OS 69 | switch (CLIENT) { 70 | case 'osx': 71 | this.menu.createMacBuiltin(this.label); 72 | assembleMenu(filteredItems, this.menu.items[0].submenu); 73 | break; 74 | default: 75 | var sub = new gui.Menu(); 76 | assembleMenu(filteredItems, sub); 77 | this.menu.append(new gui.MenuItem({ 78 | label: this.label, 79 | submenu: sub 80 | })); 81 | break; 82 | } 83 | events.emit('menu:updated'); 84 | }; 85 | 86 | CustomMenu.prototype.get = function () { 87 | return this.menu; 88 | }; 89 | 90 | module.exports = CustomMenu; 91 | -------------------------------------------------------------------------------- /nwapp/lib/login-view.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var log = require('loglevel'); 4 | var oauthJson = { osx: {}, win: {}, linux: {} }; 5 | try { 6 | oauthJson = require('../oauth.json'); 7 | } catch (e) { 8 | log.warn('nwapp/oauth.json not found. Hopefully OAUTH_KEY and OAUTH_SECRET are set...'); 9 | } 10 | 11 | // See https://github.com/nwjs/nw.js/wiki/window#synopsis 12 | var gui = global.window.nwDispatcher.requireNwGui(); 13 | var os = require('../utils/client-type'); 14 | var OAuth2 = require('oauth').OAuth2; 15 | var EventEmitter = require('events').EventEmitter; 16 | var util = require('util'); 17 | 18 | var clientId = process.env.OAUTH_KEY || oauthJson[os].key || ''; 19 | var clientSecret = process.env.OAUTH_SECRET || oauthJson[os].secret || ''; 20 | var baseSite = 'https://gitter.im/'; 21 | var authorizePath = 'login/oauth/authorize'; 22 | var accessTokenPath = 'login/oauth/token'; 23 | var redirectUri = 'app://gitter/oauth.html'; 24 | 25 | 26 | var LoginView = function(rootWindow) { 27 | var self = this; 28 | 29 | if (!clientId) throw new Error('You must provide an oauth key. Keys can be obtained from https://developer.gitter.im'); 30 | 31 | var auth = new OAuth2(clientId, clientSecret, baseSite, authorizePath, accessTokenPath); 32 | var authUrl = auth.getAuthorizeUrl({ redirect_uri: redirectUri, response_type: 'code' }); 33 | 34 | // new window for login/oauth 35 | this.oauthWindow = rootWindow.open(authUrl, { 36 | // hide page loading 37 | show: false, 38 | toolbar: false, 39 | icon: 'img/logo.png', 40 | 41 | // just big enough to show github login without scrollbars 42 | width: 1024, 43 | height: 640 44 | }); 45 | var mb = new gui.Menu({ 46 | type: 'menubar' 47 | }); 48 | if(os === 'osx') { 49 | mb.createMacBuiltin('Gitter', { 50 | hideEdit: false 51 | }); 52 | } 53 | gui.Window.get().menu = mb; 54 | 55 | this.oauthWindow.on('document-end', function() { 56 | // gitter login page finished loading visible bits 57 | self.oauthWindow.show(); 58 | self.oauthWindow.focus(); 59 | }); 60 | 61 | this.oauthWindow.on('close', function() { 62 | self.destroy(); 63 | }); 64 | 65 | this.oauthWindow.on('error', function(err) { 66 | log.error('oauth error: ' + JSON.stringify(err)); 67 | self.destroy(); 68 | }); 69 | 70 | 71 | this.oauthWindow.on('codeReceived', function(code) { 72 | // login page no longer needed 73 | self.oauthWindow.hide(); 74 | 75 | auth.getOAuthAccessToken(code, { redirect_uri: redirectUri, grant_type: 'authorization_code' }, function(err, accessToken) { 76 | if (err) { 77 | log.error('oauth error: ' + JSON.stringify(err)); 78 | self.destroy(); 79 | return; 80 | } 81 | 82 | self.emit('accessTokenReceived', accessToken); 83 | }); 84 | }); 85 | }; 86 | 87 | util.inherits(LoginView, EventEmitter); 88 | 89 | LoginView.prototype.destroy = function() { 90 | // skips all on close listeners 91 | this.oauthWindow.close(true); 92 | this.emit('destroy'); 93 | }; 94 | 95 | module.exports = LoginView; 96 | -------------------------------------------------------------------------------- /osx/create-dmg/README.md: -------------------------------------------------------------------------------- 1 | create-dmg 2 | ========== 3 | 4 | A shell script to build fancy DMGs. 5 | 6 | 7 | Status and contribution policy 8 | ------------------------------ 9 | 10 | This project is maintained thanks to the contributors who send pull requests. The original author has no use for the project, so his only role is reviewing and merging pull requests. 11 | 12 | I will merge any pull request that adds something useful and does not break existing things. 13 | 14 | Starting in January 2015, everyone who gets a pull request merged gets commit access to the repository. 15 | 16 | 17 | Installation 18 | ------------ 19 | 20 | By being a shell script, yoursway-create-dmg installation is very simple. Simply download and run. 21 | 22 | > git clone https://github.com/andreyvit/yoursway-create-dmg.git 23 | > cd yoursway-create-dmg 24 | > ./create-dmg [options] 25 | 26 | 27 | Usage 28 | ----- 29 | 30 | > create-dmg [options...] [output\_name.dmg] [source\_folder] 31 | 32 | All contents of source\_folder will be copied into the disk image. 33 | 34 | **Options:** 35 | 36 | * **--volname [name]:** set volume name (displayed in the Finder sidebar and window title) 37 | * **--volicon [icon.icns]:** set volume icon 38 | * **--background [pic.png]:** set folder background image (provide png, gif, jpg) 39 | * **--window-pos [x y]:** set position the folder window 40 | * **--window-size [width height]:** set size of the folder window 41 | * **--text-size [text size]:** set window text size (10-16) 42 | * **--icon-size [icon size]:** set window icons size (up to 128) 43 | * **--icon [file name] [x y]:** set position of the file's icon 44 | * **--hide-extension [file name]:** hide the extension of file 45 | * **--custom-icon [file name]/[custom icon]/[sample file] [x y]:** set position and custom icon 46 | * **--app-drop-link [x y]:** make a drop link to Applications, at location x, y 47 | * **--eula [eula file]:** attach a license file to the dmg 48 | * **--no-internet-enable:** disable automatic mount© 49 | * **--version:** show tool version number 50 | * **-h, --help:** display the help 51 | 52 | 53 | Example 54 | ------- 55 | 56 | > \#!/bin/sh 57 | > test -f Application-Installer.dmg && rm Application-Installer.dmg 58 | > create-dmg \ 59 | > --volname "Application Installer" \ 60 | > --volicon "application\_icon.icns" \ 61 | > --background "installer\_background.png" \ 62 | > --window-pos 200 120 \ 63 | > --window-size 800 400 \ 64 | > --icon-size 100 \ 65 | > --icon Application.app 200 190 \ 66 | > --hide-extension Application.app \ 67 | > --app-drop-link 600 185 \ 68 | > Application-Installer.dmg \ 69 | > source\_folder/ 70 | 71 | 72 | Alternatives 73 | ------------ 74 | 75 | * [node-appdmg](https://github.com/LinusU/node-appdmg) 76 | * [dmgbuild](https://pypi.python.org/pypi/dmgbuild) 77 | * see the [StackOverflow question](http://stackoverflow.com/questions/96882/how-do-i-create-a-nice-looking-dmg-for-mac-os-x-using-command-line-tools) 78 | -------------------------------------------------------------------------------- /nwapp/components/tray-menu.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gui = window.require('nw.gui'); 4 | var log = require('loglevel'); 5 | var Gitter = require('gitter-realtime-client'); 6 | var settings = require('../utils/settings'); 7 | var events = require('../utils/custom-events'); 8 | var quitApp = require('../utils/quit-app'); 9 | 10 | function TrayMenu() { 11 | this.menu = new gui.Menu(); 12 | this.unsetRooms(); 13 | 14 | events.on('user:signedOut', function () { 15 | this.unsetRooms(); 16 | this.build(); 17 | }.bind(this)); 18 | 19 | events.on('realtime:connected', this.setRooms.bind(this)); 20 | this.build(); 21 | 22 | return this; 23 | } 24 | 25 | TrayMenu.prototype.unsetRooms = function () { 26 | if (this.rooms && this.rooms.unlisten) { 27 | this.rooms.unlisten(); // TODO: this may NOT be necessary 28 | } 29 | this.rooms = []; 30 | this.favourites = []; 31 | this.recents = []; 32 | this.unreads = []; 33 | }; 34 | 35 | TrayMenu.prototype.setRooms = function (rooms) { 36 | log.debug('TrayMenu: Rooms collection set'); 37 | 38 | // TODO: the existing listeners need to be cleared properly... 39 | this.rooms = rooms; 40 | this.favourites = Gitter.filteredRooms.favourites(rooms); 41 | this.recents = Gitter.filteredRooms.recents(rooms); 42 | this.unreads = Gitter.filteredRooms.unreads(rooms); 43 | 44 | this.rooms.on('reset', function () { 45 | this.build(); 46 | }.bind(this)); 47 | 48 | [this.favourites, this.recents, this.unreads].forEach(function (collection) { 49 | collection.on('add remove', this.build.bind(this)); // Quite wasteful, trashing and regenerating menu everytime. 50 | }.bind(this)); 51 | }; 52 | 53 | TrayMenu.prototype.clear = function () { 54 | for (var i = this.menu.items.length - 1; i >= 0; i--) { 55 | this.menu.removeAt(i); 56 | } 57 | }; 58 | 59 | TrayMenu.prototype.addDefaults = function () { 60 | if (settings.token) { 61 | this.menu.append(new gui.MenuItem({ label: 'Sign Out', click: events.emit.bind(events, 'traymenu:signout') })); 62 | } 63 | this.menu.append(new gui.MenuItem({ label: 'Exit Gitter', click: quitApp })); 64 | }; 65 | 66 | TrayMenu.prototype.build = function () { 67 | log.debug('Rebuilding tray menu'); 68 | this.clear(); 69 | 70 | if (this.unreads.length > 0) { 71 | events.emit('traymenu:unread', this.unreads.length); 72 | } else { 73 | events.emit('traymenu:read'); 74 | } 75 | 76 | [{ collection: this.unreads, label: 'unread' }, 77 | { collection: this.favourites, label: 'favourites' }, 78 | { collection: this.recents, label: 'recents' } 79 | ].forEach(this.addSection.bind(this)); 80 | 81 | this.addDefaults(); 82 | events.emit('traymenu:updated'); 83 | }; 84 | 85 | TrayMenu.prototype.addSection = function (spec) { 86 | if (spec.collection.length <= 0) return; 87 | 88 | var appendRoom = function (room) { 89 | this.menu.append(this.toMenuItem(room)); 90 | }.bind(this); 91 | 92 | this.menu.append(this.toLabel(spec.label)); 93 | spec.collection.slice(0, 5).forEach(appendRoom); 94 | this.menu.append(new gui.MenuItem({ type: 'separator' })); 95 | }; 96 | 97 | TrayMenu.prototype.toMenuItem = function (room) { 98 | return new gui.MenuItem({ 99 | label: room.get('name'), 100 | click: function () { 101 | events.emit('traymenu:clicked', room.get('url')); 102 | } 103 | }); 104 | }; 105 | 106 | TrayMenu.prototype.toLabel = function (name) { 107 | return new gui.MenuItem({ 108 | label: name.toUpperCase(), 109 | enabled: false 110 | }); 111 | }; 112 | 113 | TrayMenu.prototype.get = function () { 114 | return this.menu; 115 | }; 116 | 117 | module.exports = TrayMenu; 118 | -------------------------------------------------------------------------------- /windows/build.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var exec = require('child_process').exec; 4 | var argv = require('yargs').argv; 5 | var Promise = require('bluebird'); 6 | 7 | var nwappManifest = require('../nwapp/package.json'); 8 | var appVersion = nwappManifest.version; 9 | 10 | var stat = Promise.promisify(fs.stat); 11 | 12 | var possibleProgramFilePaths = [ 13 | 'C:\\Program Files (x86)', 14 | 'C:\\Program Files' 15 | ]; 16 | 17 | var possibleSignToolPaths = [ 18 | 'Windows Kits\\10\\bin\\x64\\signtool.exe', 19 | 'Windows Kits\\10\\bin\\x86\\signtool.exe', 20 | 'Windows Kits\\8.1\\bin\\x64\\signtool.exe', 21 | 'Windows Kits\\8.1\\bin\\x86\\signtool.exe', 22 | 'Windows Kits\\8.0\\bin\\x64\\signtool.exe', 23 | 'Windows Kits\\8.0\\bin\\x86\\signtool.exe', 24 | 'Microsoft SDKs\\Windows\\v7.1\\Bin\\signtool.exe', 25 | 'Microsoft SDKs\\Windows\\v7.1A\\Bin\\signtool.exe' 26 | ]; 27 | 28 | var statResults = possibleProgramFilePaths.reduce(function(statResults, programFilePath) { 29 | return statResults.concat(possibleSignToolPaths.map(function(signToolPath) { 30 | var fullSignToolPath = path.join(programFilePath, signToolPath); 31 | return stat(fullSignToolPath) 32 | .then(function() { 33 | return fullSignToolPath; 34 | }); 35 | })); 36 | }, []); 37 | 38 | 39 | Promise.any(statResults) 40 | .then(function(signToolPath) { 41 | console.log('signToolPath', signToolPath); 42 | var certPassword = argv.password || argv.p || ''; 43 | 44 | var commandList = [ 45 | '"' + signToolPath + '" sign /f "' + path.resolve(__dirname, '..\\certificates\\troupe-cert.pfx') + '" /p "' + certPassword + '" "' + path.resolve(__dirname, '..\\opt\\Gitter\\win32\\Gitter.exe') + '"', 46 | '"C:\\Program Files (x86)\\Inno Setup 5\\ISCC.exe" "' + path.resolve(__dirname, 'gitter.iss') + '"', 47 | '"' + signToolPath + '" sign /f "' + path.resolve(__dirname, '..\\certificates\\troupe-cert.pfx') + '" /p "' + certPassword + '" "' + path.resolve(__dirname, '..\\artefacts\\GitterSetup*') + '"', 48 | 'rename "' + path.resolve(__dirname, '..\\artefacts\\GitterSetup.exe') + '" "GitterSetup-' + appVersion + '.exe"', 49 | ]; 50 | 51 | var commandRunner = function(command) { 52 | return new Promise(function(resolve, reject) { 53 | var child = exec(command, function(err, stdout, stderr) { 54 | resolve({ 55 | command: command, 56 | stdout: stdout, 57 | stderr: stderr, 58 | error: err 59 | }); 60 | }); 61 | 62 | child.stdout.pipe(process.stdout); 63 | child.stderr.pipe(process.stderr); 64 | }); 65 | }; 66 | 67 | // Run the commands in series 68 | var commandChain = commandList.reduce(function(seriesCommandChain, command) { 69 | return seriesCommandChain.then(function(commandResult) { 70 | console.log('commandResult', commandResult); 71 | if(commandResult.command) { 72 | console.log('Done: ' + commandResult.command); 73 | console.log('--------------------------------------------'); 74 | } 75 | if(commandResult.error) { 76 | console.log(commandResult.stderr); 77 | throw commandResult.error; 78 | } 79 | 80 | // Keep the chain going 81 | console.log('> ' + command); 82 | return commandRunner(command); 83 | }); 84 | }, Promise.resolve({})); 85 | 86 | return commandChain 87 | .catch(function(err) { 88 | console.log('err', err, err.stack); 89 | }); 90 | }) 91 | .catch(Promise.AggregateError, function(err) { 92 | // ignore any failed checks 93 | console.log('Probably could not find the `signtool.exe`, try installing the Microsoft SDK and check that we are looking in the right place: open this file and look at `possibleSignToolPaths`'); 94 | }); 95 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # `3.1.0` - 2016-8-16 3 | 4 | - Add launch hidden feature to only appear in task bar/menu bar when started: #107 - Thanks @nicolas33 5 | 6 | 7 | # `3.0.3` - 2016-2-28 8 | 9 | - Fix app launch crash for non-logged in users on non-OSX platforms: #118 10 | 11 | 12 | # `3.0.2` - 2016-2-26 13 | 14 | - Fix copy/paste issue on GitHub login-view: #116 15 | - Remove duplicate Windows startup file (now handled in registry): #114 16 | 17 | 18 | # `3.0.1` - 2016-2-23 19 | 20 | - Update auto-update logic to check on start and every 24-hours after that 21 | - Add `packages -> osx` `package.json` manifest check entry to play more nicely with our platform checks. 22 | - Split out `linux32` `linux64` package manifest checks 23 | 24 | # `3.0.0` - 2016-2-23 25 | 26 | This was released but quickly pulled down due to not being happy with the legacy auto-update 5 minute polling notification when there is an update. 27 | 28 | - Update docs to add more detail on how to build 29 | - Upgrade to nw.js v0.12.3 30 | - Fixes issue where the org overview pages had an infinite loading spinner: https://github.com/gitterHQ/desktop/issues/102 31 | - Fixes `Cmd+M` shortcut to minimize: https://github.com/gitterHQ/desktop/issues/91 32 | - Fixes tray icon missing on Ubuntu: https://github.com/gitterHQ/desktop/issues/101 33 | - Fix focus issue on initial load with mac. Mac apps do not have focus when they start up 34 | - Update `notifier.js` so that it no longer plays the native sound, fixes https://github.com/gitterHQ/desktop/issues/99 35 | - Add avatars to notifications for Windows 36 | - I assume this covers as the official release of the nw.js version of the mac app: https://github.com/gitterHQ/desktop/issues/47 37 | - Generalize windows desktop build, see `./windows/README.md` and `./windows/build.js`. `./windows/build.bat` has also gotten some love but is deprecated and is only there until we are confident and settled with the node script. 38 | - Remove startup option from Windows installer because it did not obey whatever you set it to(always enabled launch at startup): https://github.com/gitterHQ/desktop/issues/106 39 | - Add context menu option to toggle launch on startup (tested on Mac and Windows): https://github.com/gitterHQ/desktop/issues/45 40 | - Add "Show in Menu bar" toggle (this option existed previously but didn't do anything) 41 | - Add `gulp check-path-safety-for-windows` to build flow so you don't get [unexpected blank screen when developing on Windows](https://github.com/gitterHQ/desktop/issues/59) 42 | - Update `auto-update` params/logic so we can actually use cli params outside of the autoupdate process. 43 | - `--update-url`: Specify custom URL to check and download updates from 44 | - `--passthrough-remote-debugging-url`: So you can debug the new instances we pop throughout the update process 45 | 46 | Here is the v3 PR (note that it also has v3.0.1 changes): https://github.com/gitterHQ/desktop/pull/100 47 | 48 | # `2.4.0` - 2015-09-04 49 | fixes for the autoupdate process 50 | 51 | # `2.3.3` - 2015-09-01 52 | fix for windows startup sometimes showing node-webkit ascii art 53 | 54 | # `2.3.2` - 2015-04-08 55 | updated realtime client 56 | 57 | # `2.3.1` - 2015-03-10 58 | fix for hard websocket crash 59 | 60 | # `2.3.0` - 2015-03-06 61 | stabilty fixes and first open source release 62 | 63 | # `2.2.5` - 2015-02-20 64 | realtime connection stabilty fixes 65 | 66 | # `2.2.4` - 2015-02-12 67 | fixes issues with tray and badge disconnecting 68 | 69 | # `2.2.3` - 2015-02-09 70 | fix Ubuntu libudev symlinking 71 | 72 | # `2.2.2` - 2015-02-04 73 | oauth fixes 74 | 75 | # `2.2.1` - 2015-02-04 76 | oauth fixes 77 | 78 | # `2.2.0` - 2015-02-04 79 | fixed autoupdater trashing the uninstaller 80 | 81 | # `2.1.3` - 2015-02-03 82 | autoupdate caching fixes 83 | 84 | # `2.1.2` - 2015-02-03 85 | autoupdate tweaks 86 | 87 | # `2.1.1` - 2015-02-03 88 | file menu tweaks 89 | 90 | # `2.1.0` - 2015-02-02 91 | added auto update 92 | 93 | # `2.0.4` - 2015-01-22 94 | alpha release for windows 95 | 96 | # `2.0.3` - 2015-01-22 97 | internal windows release 98 | 99 | # `2.0.2` - 2015-01-21 100 | internal windows release 101 | 102 | # `2.0.1` - 2015-01-21 103 | internal windows release 104 | 105 | # `2.0.0` - 2015-01-21 106 | internal windows release 107 | -------------------------------------------------------------------------------- /nwapp/utils/notifier.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var log = require('loglevel'); 4 | 5 | var Promise = require('bluebird'); 6 | var os = require('os'); 7 | var fs = require('fs-extra'); 8 | var ensureDir = Promise.promisify(fs.ensureDir); 9 | var path = require('path'); 10 | var temp = require('temp'); 11 | var rimraf = require('rimraf'); 12 | var request = require('request'); 13 | var notifier = require('node-notifier'); 14 | var objectAssign = require('object-assign'); 15 | 16 | var settings = require('./settings'); 17 | var events = require('./custom-events'); 18 | 19 | var gui = window.require('nw.gui'); 20 | var win = gui.Window.get(); 21 | 22 | var CLIENT = require('./client-type'); 23 | var SOUNDS_ITEMS = require('../components/sound-items'); 24 | 25 | 26 | function playNotificationSound() { 27 | var audio = win.window.document.createElement('audio'); 28 | var sound = SOUNDS_ITEMS[settings.notificationSound]; 29 | 30 | if (!sound.path) { 31 | return; 32 | } 33 | 34 | audio.src = sound.path; 35 | audio.play(); 36 | audio = null; 37 | } 38 | 39 | var checkActivationNotifierResponse = function(response) { 40 | // 'activated', `timeout'` 41 | var resp = response.trim().toLowerCase(); 42 | if(resp.match(/^activate/)) { 43 | return true; 44 | } 45 | 46 | return false; 47 | }; 48 | 49 | 50 | // Cache lookup 51 | var urlFilePathCacheMap = new Map(); 52 | var tempAvatarDirectory = path.join(os.tmpDir(), 'gitter-notification-avatars_'); 53 | log.info('Saving notification avatars at: ' + tempAvatarDirectory); 54 | var ensureTempAvatarDirectory = ensureDir(tempAvatarDirectory) 55 | .then(function() { 56 | return tempAvatarDirectory; 57 | }); 58 | 59 | events.on('app:quit', function() { 60 | log.info('App is quiting, cleanup avatars'); 61 | try { 62 | rimraf.sync(tempAvatarDirectory); 63 | } 64 | catch(err) { 65 | log.error('Something went wrong while cleaning up the avatars: ' + err.message + err.stack); 66 | } 67 | }); 68 | 69 | function downloadAndCache(url) { 70 | return new Promise(function(resolve, reject) { 71 | var tempFileStream = temp.createWriteStream({ 72 | dir: tempAvatarDirectory 73 | }); 74 | 75 | tempFileStream 76 | .on('finish', function() { 77 | var tempFilePath = tempFileStream.path; 78 | // Store it on our map as a cache lookup 79 | urlFilePathCacheMap.set(url, tempFilePath); 80 | resolve(tempFilePath); 81 | }) 82 | .on('error', function(err) { 83 | reject(err); 84 | }); 85 | 86 | request({ 87 | url: url, 88 | method: 'GET', 89 | timeout: 750 90 | }) 91 | .on('error', function(err) { 92 | reject(err); 93 | }) 94 | .pipe(tempFileStream); 95 | }); 96 | } 97 | function getFilePathForHttpUrl(url) { 98 | return ensureTempAvatarDirectory.then(function() { 99 | var cachedFilePath = urlFilePathCacheMap.get(url); 100 | return cachedFilePath || downloadAndCache(url); 101 | }) 102 | .then(function(filePath) { 103 | return filePath; 104 | }); 105 | } 106 | 107 | 108 | var notifierDefaults = { 109 | title: '', 110 | message: '', 111 | // gitterHQ logo 112 | icon: 'https://avatars.githubusercontent.com/u/5990364?s=60', 113 | click: undefined 114 | }; 115 | 116 | module.exports = function (options) { 117 | var opts = objectAssign({}, notifierDefaults, options); 118 | 119 | if (!settings.showNotifications) return; 120 | 121 | getFilePathForHttpUrl(opts.icon) 122 | // We are not worried if they couldn't fetch the image 123 | // as we fallback to the URL itself 124 | .catch(function(err) { 125 | log.error('Trouble getting avatar for notification' + err.message + err.stack); 126 | return opts.icon; 127 | }) 128 | .then(function(imagePath) { 129 | playNotificationSound(); 130 | 131 | // We use this instead of `new window.Notification(...)` because we can disable the native sound 132 | notifier.notify({ 133 | title: opts.title, 134 | message: opts.message, 135 | icon: imagePath, 136 | // Only Notification Center or Windows Toasters 137 | // We handle the sound outside of this above 138 | // And we set to false because Windows 10 by default makes a separate sound 139 | sound: false, 140 | // wait with callback until user action is taken on notification 141 | wait: true 142 | }, function(err, response) { 143 | log.info('Notification response', response); 144 | var didActivate = checkActivationNotifierResponse(response); 145 | 146 | if(err) { 147 | // If you are seeing errors on each notification on Windows, 148 | // it isn't really a issue because everything works normally and 149 | // we are blocked until it gets fixed: https://github.com/mikaelbr/node-notifier/issues/97 150 | log.error('Problem with notification', err, err.stack); 151 | } 152 | if(didActivate && opts.click) { 153 | opts.click(); 154 | } 155 | }); 156 | }) 157 | .catch(function(err) { 158 | log.error('Problem initializing notification', err, err.stack); 159 | }); 160 | 161 | }; 162 | -------------------------------------------------------------------------------- /osx/create-dmg/support/dmg-license.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | """ 3 | This script adds a license file to a DMG. Requires Xcode and a plain ascii text 4 | license file. 5 | Obviously only runs on a Mac. 6 | 7 | Copyright (C) 2011-2013 Jared Hobbs 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in 17 | all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | THE SOFTWARE. 26 | """ 27 | import os 28 | import sys 29 | import tempfile 30 | import optparse 31 | 32 | 33 | class Path(str): 34 | def __enter__(self): 35 | return self 36 | 37 | def __exit__(self, type, value, traceback): 38 | os.unlink(self) 39 | 40 | 41 | def mktemp(dir=None, suffix=''): 42 | (fd, filename) = tempfile.mkstemp(dir=dir, suffix=suffix) 43 | os.close(fd) 44 | return Path(filename) 45 | 46 | 47 | def main(options, args): 48 | dmgFile, license = args 49 | with mktemp('.') as tmpFile: 50 | with open(tmpFile, 'w') as f: 51 | f.write("""data 'TMPL' (128, "LPic") { 52 | $"1344 6566 6175 6C74 204C 616E 6775 6167" 53 | $"6520 4944 4457 5244 0543 6F75 6E74 4F43" 54 | $"4E54 042A 2A2A 2A4C 5354 430B 7379 7320" 55 | $"6C61 6E67 2049 4444 5752 441E 6C6F 6361" 56 | $"6C20 7265 7320 4944 2028 6F66 6673 6574" 57 | $"2066 726F 6D20 3530 3030 4457 5244 1032" 58 | $"2D62 7974 6520 6C61 6E67 7561 6765 3F44" 59 | $"5752 4404 2A2A 2A2A 4C53 5445" 60 | }; 61 | 62 | data 'LPic' (5000) { 63 | $"0000 0002 0000 0000 0000 0000 0004 0000" 64 | }; 65 | 66 | data 'STR#' (5000, "English buttons") { 67 | $"0006 0D45 6E67 6C69 7368 2074 6573 7431" 68 | $"0541 6772 6565 0844 6973 6167 7265 6505" 69 | $"5072 696E 7407 5361 7665 2E2E 2E7A 4966" 70 | $"2079 6F75 2061 6772 6565 2077 6974 6820" 71 | $"7468 6520 7465 726D 7320 6F66 2074 6869" 72 | $"7320 6C69 6365 6E73 652C 2063 6C69 636B" 73 | $"2022 4167 7265 6522 2074 6F20 6163 6365" 74 | $"7373 2074 6865 2073 6F66 7477 6172 652E" 75 | $"2020 4966 2079 6F75 2064 6F20 6E6F 7420" 76 | $"6167 7265 652C 2070 7265 7373 2022 4469" 77 | $"7361 6772 6565 2E22" 78 | }; 79 | 80 | data 'STR#' (5002, "English") { 81 | $"0006 0745 6E67 6C69 7368 0541 6772 6565" 82 | $"0844 6973 6167 7265 6505 5072 696E 7407" 83 | $"5361 7665 2E2E 2E7B 4966 2079 6F75 2061" 84 | $"6772 6565 2077 6974 6820 7468 6520 7465" 85 | $"726D 7320 6F66 2074 6869 7320 6C69 6365" 86 | $"6E73 652C 2070 7265 7373 2022 4167 7265" 87 | $"6522 2074 6F20 696E 7374 616C 6C20 7468" 88 | $"6520 736F 6674 7761 7265 2E20 2049 6620" 89 | $"796F 7520 646F 206E 6F74 2061 6772 6565" 90 | $"2C20 7072 6573 7320 2244 6973 6167 7265" 91 | $"6522 2E" 92 | };\n\n""") 93 | with open(license, 'r') as l: 94 | kind = 'RTF ' if license.lower().endswith('.rtf') else 'TEXT' 95 | f.write('data \'%s\' (5000, "English") {\n' % kind) 96 | def escape(s): 97 | return s.strip().replace('\\', '\\\\').replace('"', '\\"') 98 | 99 | for line in l: 100 | if len(line) < 1000: 101 | f.write(' "' + escape(line) + '\\n"\n') 102 | else: 103 | for liner in line.split('.'): 104 | f.write(' "' + escape(liner) + '. \\n"\n') 105 | f.write('};\n\n') 106 | f.write("""data 'styl' (5000, "English") { 107 | $"0003 0000 0000 000C 0009 0014 0000 0000" 108 | $"0000 0000 0000 0000 0027 000C 0009 0014" 109 | $"0100 0000 0000 0000 0000 0000 002A 000C" 110 | $"0009 0014 0000 0000 0000 0000 0000" 111 | };\n""") 112 | os.system('hdiutil unflatten -quiet "%s"' % dmgFile) 113 | ret = os.system('%s -a %s -o "%s"' % 114 | (options.rez, tmpFile, dmgFile)) 115 | os.system('hdiutil flatten -quiet "%s"' % dmgFile) 116 | if options.compression is not None: 117 | os.system('cp %s %s.temp.dmg' % (dmgFile, dmgFile)) 118 | os.remove(dmgFile) 119 | if options.compression == "bz2": 120 | os.system('hdiutil convert %s.temp.dmg -format UDBZ -o %s' % 121 | (dmgFile, dmgFile)) 122 | elif options.compression == "gz": 123 | os.system('hdiutil convert %s.temp.dmg -format ' % dmgFile + 124 | 'UDZO -imagekey zlib-devel=9 -o %s' % dmgFile) 125 | os.remove('%s.temp.dmg' % dmgFile) 126 | if ret == 0: 127 | print "Successfully added license to '%s'" % dmgFile 128 | else: 129 | print "Failed to add license to '%s'" % dmgFile 130 | 131 | if __name__ == '__main__': 132 | parser = optparse.OptionParser() 133 | parser.set_usage("""%prog [OPTIONS] 134 | This program adds a software license agreement to a DMG file. 135 | It requires Xcode and either a plain ascii text 136 | or a with the RTF contents. 137 | 138 | See --help for more details.""") 139 | parser.add_option( 140 | '--rez', 141 | '-r', 142 | action='store', 143 | default='/Applications/Xcode.app/Contents/Developer/Tools/Rez', 144 | help='The path to the Rez tool. Defaults to %default' 145 | ) 146 | parser.add_option( 147 | '--compression', 148 | '-c', 149 | action='store', 150 | choices=['bz2', 'gz'], 151 | default=None, 152 | help='Optionally compress dmg using specified compression type. ' 153 | 'Choices are bz2 and gz.' 154 | ) 155 | options, args = parser.parse_args() 156 | cond = len(args) != 2 157 | if not os.path.exists(options.rez): 158 | print 'Failed to find Rez at "%s"!\n' % options.rez 159 | cond = True 160 | if cond: 161 | parser.print_usage() 162 | sys.exit(1) 163 | main(options, args) 164 | -------------------------------------------------------------------------------- /osx/create-dmg/create-dmg: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | # Create a read-only disk image of the contents of a folder 4 | 5 | set -e; 6 | 7 | function pure_version() { 8 | echo '1.0.0.2' 9 | } 10 | 11 | function version() { 12 | echo "create-dmg $(pure_version)" 13 | } 14 | 15 | function usage() { 16 | version 17 | echo "Creates a fancy DMG file." 18 | echo "Usage: $(basename $0) options... image.dmg source_folder" 19 | echo "All contents of source_folder will be copied into the disk image." 20 | echo "Options:" 21 | echo " --volname name" 22 | echo " set volume name (displayed in the Finder sidebar and window title)" 23 | echo " --volicon icon.icns" 24 | echo " set volume icon" 25 | echo " --background pic.png" 26 | echo " set folder background image (provide png, gif, jpg)" 27 | echo " --window-pos x y" 28 | echo " set position the folder window" 29 | echo " --window-size width height" 30 | echo " set size of the folder window" 31 | echo " --text-size text_size" 32 | echo " set window text size (10-16)" 33 | echo " --icon-size icon_size" 34 | echo " set window icons size (up to 128)" 35 | echo " --icon file_name x y" 36 | echo " set position of the file's icon" 37 | echo " --hide-extension file_name" 38 | echo " hide the extension of file" 39 | echo " --custom-icon file_name custom_icon_or_sample_file x y" 40 | echo " set position and custom icon" 41 | echo " --app-drop-link x y" 42 | echo " make a drop link to Applications, at location x,y" 43 | echo " --eula eula_file" 44 | echo " attach a license file to the dmg" 45 | echo " --no-internet-enable" 46 | echo " disable automatic mount©" 47 | echo " --version show tool version number" 48 | echo " -h, --help display this help" 49 | exit 0 50 | } 51 | 52 | WINX=10 53 | WINY=60 54 | WINW=500 55 | WINH=350 56 | ICON_SIZE=128 57 | TEXT_SIZE=16 58 | 59 | while test "${1:0:1}" = "-"; do 60 | case $1 in 61 | --volname) 62 | VOLUME_NAME="$2" 63 | shift; shift;; 64 | --volicon) 65 | VOLUME_ICON_FILE="$2" 66 | shift; shift;; 67 | --background) 68 | BACKGROUND_FILE="$2" 69 | BACKGROUND_FILE_NAME="$(basename $BACKGROUND_FILE)" 70 | BACKGROUND_CLAUSE="set background picture of opts to file \".background:$BACKGROUND_FILE_NAME\"" 71 | REPOSITION_HIDDEN_FILES_CLAUSE="set position of every item to {theBottomRightX + 100, 100}" 72 | shift; shift;; 73 | --icon-size) 74 | ICON_SIZE="$2" 75 | shift; shift;; 76 | --text-size) 77 | TEXT_SIZE="$2" 78 | shift; shift;; 79 | --window-pos) 80 | WINX=$2; WINY=$3 81 | shift; shift; shift;; 82 | --window-size) 83 | WINW=$2; WINH=$3 84 | shift; shift; shift;; 85 | --icon) 86 | POSITION_CLAUSE="${POSITION_CLAUSE}set position of item \"$2\" to {$3, $4} 87 | " 88 | shift; shift; shift; shift;; 89 | --hide-extension) 90 | HIDING_CLAUSE="${HIDING_CLAUSE}set the extension hidden of item \"$2\" to true" 91 | shift; shift;; 92 | --custom-icon) 93 | shift; shift; shift; shift; shift;; 94 | -h | --help) 95 | usage;; 96 | --version) 97 | version; exit 0;; 98 | --pure-version) 99 | pure_version; exit 0;; 100 | --app-drop-link) 101 | APPLICATION_LINK=$2 102 | APPLICATION_CLAUSE="set position of item \"Applications\" to {$2, $3} 103 | " 104 | shift; shift; shift;; 105 | --eula) 106 | EULA_RSRC=$2 107 | shift; shift;; 108 | --no-internet-enable) 109 | NOINTERNET=1 110 | shift;; 111 | -*) 112 | echo "Unknown option $1. Run with --help for help." 113 | exit 1;; 114 | esac 115 | done 116 | 117 | test -z "$2" && { 118 | echo "Not enough arguments. Invoke with --help for help." 119 | exit 1 120 | } 121 | 122 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 123 | DMG_PATH="$1" 124 | DMG_DIRNAME="$(dirname "$DMG_PATH")" 125 | DMG_DIR="$(cd "$DMG_DIRNAME" > /dev/null; pwd)" 126 | DMG_NAME="$(basename "$DMG_PATH")" 127 | DMG_TEMP_NAME="$DMG_DIR/rw.${DMG_NAME}" 128 | SRC_FOLDER="$(cd "$2" > /dev/null; pwd)" 129 | test -z "$VOLUME_NAME" && VOLUME_NAME="$(basename "$DMG_PATH" .dmg)" 130 | 131 | AUX_PATH="$SCRIPT_DIR/support" 132 | 133 | test -d "$AUX_PATH" || { 134 | echo "Cannot find support directory: $AUX_PATH" 135 | exit 1 136 | } 137 | 138 | if [ -f "$SRC_FOLDER/.DS_Store" ]; then 139 | echo "Deleting any .DS_Store in source folder" 140 | rm "$SRC_FOLDER/.DS_Store" 141 | fi 142 | 143 | # Create the image 144 | echo "Creating disk image..." 145 | test -f "${DMG_TEMP_NAME}" && rm -f "${DMG_TEMP_NAME}" 146 | ACTUAL_SIZE=`du -sm "$SRC_FOLDER" | sed -e 's/ .*//g'` 147 | DISK_IMAGE_SIZE=$(expr $ACTUAL_SIZE + 20) 148 | hdiutil create -srcfolder "$SRC_FOLDER" -volname "${VOLUME_NAME}" -fs HFS+ -fsargs "-c c=64,a=16,e=16" -format UDRW -size ${DISK_IMAGE_SIZE}m "${DMG_TEMP_NAME}" 149 | 150 | # mount it 151 | echo "Mounting disk image..." 152 | MOUNT_DIR="/Volumes/${VOLUME_NAME}" 153 | 154 | # try unmount dmg if it was mounted previously (e.g. developer mounted dmg, installed app and forgot to unmount it) 155 | echo "Unmounting disk image..." 156 | DEV_NAME=$(hdiutil info | egrep '^/dev/' | sed 1q | awk '{print $1}') 157 | test -d "${MOUNT_DIR}" && hdiutil detach "${DEV_NAME}" 158 | 159 | echo "Mount directory: $MOUNT_DIR" 160 | DEV_NAME=$(hdiutil attach -readwrite -noverify -noautoopen "${DMG_TEMP_NAME}" | egrep '^/dev/' | sed 1q | awk '{print $1}') 161 | echo "Device name: $DEV_NAME" 162 | 163 | if ! test -z "$BACKGROUND_FILE"; then 164 | echo "Copying background file..." 165 | test -d "$MOUNT_DIR/.background" || mkdir "$MOUNT_DIR/.background" 166 | cp "$BACKGROUND_FILE" "$MOUNT_DIR/.background/$BACKGROUND_FILE_NAME" 167 | fi 168 | 169 | if ! test -z "$APPLICATION_LINK"; then 170 | echo "making link to Applications dir" 171 | echo $MOUNT_DIR 172 | ln -s /Applications "$MOUNT_DIR/Applications" 173 | fi 174 | 175 | if ! test -z "$VOLUME_ICON_FILE"; then 176 | echo "Copying volume icon file '$VOLUME_ICON_FILE'..." 177 | cp "$VOLUME_ICON_FILE" "$MOUNT_DIR/.VolumeIcon.icns" 178 | SetFile -c icnC "$MOUNT_DIR/.VolumeIcon.icns" 179 | fi 180 | 181 | # run applescript 182 | APPLESCRIPT=$(mktemp -t createdmg) 183 | cat "$AUX_PATH/template.applescript" | sed -e "s/WINX/$WINX/g" -e "s/WINY/$WINY/g" -e "s/WINW/$WINW/g" -e "s/WINH/$WINH/g" -e "s/BACKGROUND_CLAUSE/$BACKGROUND_CLAUSE/g" -e "s/REPOSITION_HIDDEN_FILES_CLAUSE/$REPOSITION_HIDDEN_FILES_CLAUSE/g" -e "s/ICON_SIZE/$ICON_SIZE/g" -e "s/TEXT_SIZE/$TEXT_SIZE/g" | perl -pe "s/POSITION_CLAUSE/$POSITION_CLAUSE/g" | perl -pe "s/APPLICATION_CLAUSE/$APPLICATION_CLAUSE/g" | perl -pe "s/HIDING_CLAUSE/$HIDING_CLAUSE/" >"$APPLESCRIPT" 184 | 185 | echo "Running Applescript: /usr/bin/osascript \"${APPLESCRIPT}\" \"${VOLUME_NAME}\"" 186 | "/usr/bin/osascript" "${APPLESCRIPT}" "${VOLUME_NAME}" || true 187 | echo "Done running the applescript..." 188 | sleep 4 189 | 190 | rm "$APPLESCRIPT" 191 | 192 | # make sure it's not world writeable 193 | echo "Fixing permissions..." 194 | chmod -Rf go-w "${MOUNT_DIR}" &> /dev/null || true 195 | echo "Done fixing permissions." 196 | 197 | # make the top window open itself on mount: 198 | echo "Blessing started" 199 | bless --folder "${MOUNT_DIR}" --openfolder "${MOUNT_DIR}" 200 | echo "Blessing finished" 201 | 202 | if ! test -z "$VOLUME_ICON_FILE"; then 203 | # tell the volume that it has a special file attribute 204 | SetFile -a C "$MOUNT_DIR" 205 | fi 206 | 207 | # unmount 208 | echo "Unmounting disk image..." 209 | hdiutil detach "${DEV_NAME}" 210 | 211 | # compress image 212 | echo "Compressing disk image..." 213 | hdiutil convert "${DMG_TEMP_NAME}" -format UDZO -imagekey zlib-level=9 -o "${DMG_DIR}/${DMG_NAME}" 214 | rm -f "${DMG_TEMP_NAME}" 215 | 216 | # adding EULA resources 217 | if [ ! -z "${EULA_RSRC}" -a "${EULA_RSRC}" != "-null-" ]; then 218 | echo "adding EULA resources" 219 | "${AUX_PATH}/dmg-license.py" "${DMG_DIR}/${DMG_NAME}" "${EULA_RSRC}" 220 | fi 221 | 222 | if [ ! -z "${NOINTERNET}" -a "${NOINTERNET}" == 1 ]; then 223 | echo "not setting 'internet-enable' on the dmg" 224 | else 225 | hdiutil internet-enable -yes "${DMG_DIR}/${DMG_NAME}" 226 | fi 227 | 228 | echo "Disk image done" 229 | exit 0 230 | -------------------------------------------------------------------------------- /nwapp/utils/auto-update.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gui = window.require('nw.gui'); 4 | var manifest = require('../package.json'); 5 | var log = require('loglevel'); 6 | var argv = require('yargs')(gui.App.argv).argv; 7 | var os = require('./client-type'); 8 | 9 | var Promise = require('bluebird'); 10 | var urlParse = require('url-parse'); 11 | var rimraf = require('rimraf'); 12 | var path = require('path'); 13 | var temp = require('temp'); 14 | var request = require('request'); 15 | var extract = require('extract-zip'); 16 | var Updater = require('node-webkit-updater'); 17 | 18 | var notifier = require('./notifier'); 19 | var quitApp = require('./quit-app'); 20 | 21 | 22 | var getPlatform = function() { 23 | var platformString = os; 24 | if(os === 'linux') { 25 | platformString += (process.arch === 'ia32' ? '32' : '64'); 26 | } 27 | 28 | return platformString; 29 | }; 30 | 31 | 32 | // 10 minutes 33 | var notifyUpdatePollingRate = 10 * 60 * 1000; 34 | // 24 hours 35 | var notifyUpdateIntervalTime = 24 * 60 * 60 * 1000; 36 | var lastNotifyUpdateTime = -1; 37 | var scheduleUpdateNotifyCallback = function(callback) { 38 | var cb = function() { 39 | callback(); 40 | lastNotifyUpdateTime = Date.now(); 41 | }; 42 | 43 | cb(); 44 | setInterval(function() { 45 | var timeSinceNotify = Date.now() - lastNotifyUpdateTime; 46 | 47 | if(timeSinceNotify >= notifyUpdateIntervalTime) { 48 | cb(); 49 | } 50 | }, notifyUpdatePollingRate); 51 | }; 52 | 53 | 54 | // You can change the place we use to check and download updates with this CLI parameter `--update-url=192.168.0.58:3010` 55 | // We use this for testing a release 56 | var updateUrlOption = argv['update-url'] || 'https://update.gitter.im'; 57 | var transposeUpdateUrl = function(targetUrl) { 58 | targetUrl = targetUrl || ''; 59 | var parsedTargetUrl = urlParse(targetUrl); 60 | var parsedUpdateUrl = urlParse(updateUrlOption); 61 | parsedTargetUrl.protocol = parsedUpdateUrl.protocol || 'http:'; 62 | parsedTargetUrl.auth = parsedUpdateUrl.auth; 63 | parsedTargetUrl.hostname = parsedUpdateUrl.hostname; 64 | parsedTargetUrl.port = parsedUpdateUrl.port; 65 | 66 | var newUrl = parsedTargetUrl.toString(); 67 | return newUrl; 68 | }; 69 | 70 | 71 | var MANIFEST_URLS = { 72 | win: 'https://update.gitter.im/win/package.json', 73 | osx: 'https://update.gitter.im/osx/package.json', 74 | linux32: 'https://update.gitter.im/linux32/package.json', 75 | linux64: 'https://update.gitter.im/linux64/package.json' 76 | }; 77 | Object.keys(MANIFEST_URLS).forEach(function(key) { 78 | MANIFEST_URLS[key] = transposeUpdateUrl(MANIFEST_URLS[key]); 79 | }); 80 | 81 | 82 | 83 | var currentManifest = { 84 | name: manifest.name, 85 | version: manifest.version, 86 | manifestUrl: MANIFEST_URLS[getPlatform()] 87 | }; 88 | 89 | var updater = new Updater(currentManifest); 90 | 91 | 92 | // Because nw.js doesn't pass along the `--remote-debugging-port` to the child app 93 | // we need to use a separate variable name 94 | var passthroughRemoteDebuggingPort = function() { 95 | // If we were debugging before, be sure to allow them to see the install 96 | var remoteDebuggingPort = argv['passthrough-remote-debugging-port']; 97 | if(remoteDebuggingPort) { 98 | log.info('You will be able to monitor the new app launch with the same remote debugging port (just refresh to see new instances)', remoteDebuggingPort); 99 | return ['--remote-debugging-port=' + remoteDebuggingPort, '--passthrough-remote-debugging-port=' + remoteDebuggingPort]; 100 | } 101 | 102 | return []; 103 | }; 104 | 105 | 106 | 107 | function download(url, cb) { 108 | var tempFileStream = temp.createWriteStream('gitter-update-zip'); 109 | 110 | tempFileStream.on('error', cb) 111 | .on('finish', function() { 112 | cb(null, tempFileStream.path); 113 | }); 114 | log.info('downloading update from', url, 'to', tempFileStream.path); 115 | 116 | request.get(url) 117 | .on('error', cb) 118 | .pipe(tempFileStream); 119 | } 120 | 121 | function unpack(zipPath, cb) { 122 | var dir = temp.path('gitter-update-app'); 123 | 124 | log.info('unpacking update from', zipPath, 'to', dir); 125 | 126 | extract(zipPath, { dir: dir }, function(err) { 127 | if (err) return cb(err); 128 | 129 | cb(null, dir); 130 | }); 131 | } 132 | 133 | function getUpdate(manifest, cb) { 134 | // node-webkit-updater logic from hell 135 | var platform = getPlatform(); 136 | var newPackageMap = manifest.packages[platform]; 137 | if(!newPackageMap) { 138 | // Welp, something is not going right, we couldn't find their target in the new 139 | // manifest, so tell them to go get it manually 140 | notifyUpdateFallback(); 141 | 142 | var availablePlatformsString = Object.keys(manifest.packages) 143 | .map(function(key) { 144 | return '`' + key + '`'; 145 | }, '') 146 | .join(', '); 147 | 148 | log.error('We couldn\'t find a package associated with your platform: `' + platform + '`, in the new manifest. Here what was available: ' + availablePlatformsString); 149 | } 150 | else { 151 | var updateBundleUrl = transposeUpdateUrl(newPackageMap.url); 152 | 153 | log.info('Downloading bundle from:', updateBundleUrl); 154 | download(updateBundleUrl, function(err, zipPath) { 155 | if (err) return cb(err); 156 | 157 | unpack(zipPath, function(err, appDir) { 158 | if (err) return cb(err); 159 | 160 | // clean up the zip, but we have no way to track 161 | // the temp appDir, so that cant be cleaned 162 | rimraf(zipPath, function(err) { 163 | if (err) return cb(err); 164 | 165 | cb(null, appDir); 166 | }); 167 | }); 168 | }); 169 | } 170 | } 171 | 172 | function notifyUpdateFallback(version) { 173 | function notify() { 174 | notifier({ 175 | title: 'Gitter ' + version + ' Available', 176 | message: 'Head over to gitter.im/apps to update.' 177 | }); 178 | } 179 | 180 | scheduleUpdateNotifyCallback(notify); 181 | } 182 | 183 | function notifyWinOsxUser(version, newAppExecutable) { 184 | function notify() { 185 | notifier({ 186 | title: 'Gitter ' + version + ' Available', 187 | message: 'Click to restart and apply update.', 188 | click: function() { 189 | log.info('Update notification clicked'); 190 | 191 | var installerArgs = [ 192 | '--current-install-path=' + updater.getAppPath(), 193 | '--new-executable=' + updater.getAppExec() 194 | ]; 195 | // If we were debugging before, be sure to allow them to see the install 196 | installerArgs = installerArgs.concat(passthroughRemoteDebuggingPort()); 197 | 198 | log.info('Starting new app to install itself', newAppExecutable, installerArgs); 199 | updater.runInstaller(newAppExecutable, installerArgs, {}); 200 | 201 | log.info('Quitting outdated app'); 202 | quitApp(); 203 | } 204 | }); 205 | } 206 | 207 | scheduleUpdateNotifyCallback(notify); 208 | } 209 | 210 | 211 | function poll() { 212 | 213 | function update() { 214 | log.info('checking', currentManifest.manifestUrl, 'for app updates'); 215 | updater.checkNewVersion(function (err, newVersionExists, newManifest) { 216 | if (err) { 217 | log.error('request for app update manifest failed', err); 218 | return tryAgainLater(); 219 | } 220 | 221 | if (!newVersionExists) { 222 | log.info('app currently at latest version'); 223 | return tryAgainLater(); 224 | } 225 | 226 | // Update available! 227 | var version = newManifest.version; 228 | log.info('app update ' + version + ' available'); 229 | 230 | if (os === 'linux') { 231 | // linux cannot autoupdate (yet) 232 | return notifyUpdateFallback(version); 233 | } 234 | 235 | getUpdate(newManifest, function(err, newAppDir) { 236 | if (err) { 237 | log.error('app update ' + version + ' failed to download and unpack', err.message); 238 | return tryAgainLater(); 239 | } 240 | 241 | var executable = newManifest.name + (os === 'win' ? '.exe' : '.app'); 242 | var newAppExecutable = path.join(newAppDir, executable); 243 | 244 | return notifyWinOsxUser(version, newAppExecutable); 245 | }); 246 | }); 247 | } 248 | 249 | // A debug way to trigger another update 250 | window.debugUpdateGitter = update; 251 | 252 | // polling with setInterval can cause multiple downloads 253 | // to occur if left unattended, so its best to wait for the updater 254 | // to finish each poll before triggering a new request. 255 | function tryAgainLater() { 256 | log.info('trying app update again in 30 mins'); 257 | setTimeout(update, 30 * 60 * 1000); 258 | } 259 | 260 | // update() retries on failure, so only call once. 261 | update(); 262 | } 263 | 264 | function overwriteOldApp(oldAppDir, executable) { 265 | updater.install(oldAppDir, function(err) { 266 | if (err) { 267 | log.error('update failed, shutting down installer', err.stack); 268 | return quitApp(); 269 | } 270 | 271 | // The recommended updater.run(execPath, null) [1] doesn't work properly on Windows. 272 | // It spawns a new child process but it doesn't detach so when the installer app quits the new version also quits. :poop: 273 | // [1] https://github.com/edjafarov/node-webkit-updater/blob/master/examples/basic.js#L29 274 | // https://github.com/edjafarov/node-webkit-updater/blob/master/app/updater.js#L404-L416 275 | var newAppArgs = []; 276 | // If we were debugging before, be sure to allow them to see the new app 277 | newAppArgs = newAppArgs.concat(passthroughRemoteDebuggingPort()); 278 | log.info('starting new version', executable, newAppArgs); 279 | updater.run(executable, newAppArgs, {}); 280 | 281 | // wait for new version to get going... 282 | setTimeout(function() { 283 | log.info('shutting down installer'); 284 | quitApp(); 285 | }, 5000); 286 | }); 287 | } 288 | 289 | module.exports = { 290 | poll: poll, 291 | overwriteOldApp: overwriteOldApp 292 | }; 293 | -------------------------------------------------------------------------------- /nwapp/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var log = require('loglevel'); 4 | log.setLevel('debug'); 5 | 6 | // @CONST 7 | var CLIENT = require('./utils/client-type'); 8 | 9 | var manifest = require('./package.json'); 10 | var gui = require('nw.gui'); 11 | var pkg = require('./package.json'); 12 | var Gitter = require('gitter-realtime-client'); 13 | var os = require('os'); 14 | var path = require('path'); 15 | 16 | var argv = require('yargs')(gui.App.argv).argv; 17 | var Promise = require('bluebird'); 18 | var semver = require('semver'); 19 | var autoUpdate = require('./utils/auto-update'); 20 | var AutoLaunch = require('auto-launch'); 21 | var fs = require('fs'); 22 | var deleteFile = Promise.promisify(fs.unlink); 23 | 24 | var settings = require('./utils/settings'); 25 | var notifier = require('./utils/notifier'); 26 | var events = require('./utils/custom-events'); 27 | var quitApp = require('./utils/quit-app'); 28 | 29 | // components 30 | var MENU_ITEMS = require('./components/menu-items'); 31 | var CustomTray = require('./components/tray'); 32 | var CustomMenu = require('./components/menu'); 33 | var TrayMenu = require('./components/tray-menu'); 34 | 35 | var LoginView = require('./lib/login-view'); 36 | 37 | var checkFileExistSync = function(target) { 38 | try { 39 | return !!fs.statSync(target); 40 | } 41 | catch(err) { 42 | // swallow the error 43 | } 44 | 45 | return false; 46 | }; 47 | 48 | process.on('uncaughtException', function (err) { 49 | log.error('Caught exception: ' + err); 50 | log.error(err.stack); 51 | process.exit(1); 52 | }); 53 | 54 | var win; 55 | var mainWindow; // this is the chat window (logged in) 56 | var mainWindowFocused = false; // Focus tracker. NWK doesn't have a way to query if the window is in focus 57 | var loginView; // log in form 58 | 59 | 60 | 61 | // Remove the legacy v2.4.0- startup location 62 | // so that we don't have duplicates 63 | // FIXME: remove after August 2016 64 | if(CLIENT === 'win') { 65 | var windowsStartupPathPiece = 'Microsoft/Windows/Start Menu/Programs/Startup'; 66 | var startupBasePaths = []; 67 | if(process.env.SYSTEMDRIVE) { 68 | startupBasePaths.push(path.join(process.env.SYSTEMDRIVE, './ProgramData', windowsStartupPathPiece)); 69 | } 70 | if(process.env.APPDATA) { 71 | startupBasePaths.push(path.join(process.env.APPDATA, windowsStartupPathPiece)); 72 | } 73 | if(process.env.LOCALAPPDATA) { 74 | startupBasePaths.push(path.join(process.env.LOCALAPPDATA, windowsStartupPathPiece)); 75 | } 76 | 77 | startupBasePaths.forEach(function(startupBasePath) { 78 | var startupPath = path.join(startupBasePath, './Gitter.lnk'); 79 | 80 | deleteFile(startupPath) 81 | .then(function() { 82 | log.info('Deleted legacy startup file:', startupPath); 83 | }) 84 | .catch(function(err) { 85 | // Ignore the file not being found 86 | if(err.code !== 'ENOENT') { 87 | log.error('Failed to cleanup legacy startup file:', startupPath, err, err.stack); 88 | } 89 | }); 90 | }); 91 | } 92 | var autoLauncher = new AutoLaunch({ 93 | name: 'Gitter' 94 | }); 95 | 96 | 97 | (function () { 98 | log.info('execPath:', process.execPath); 99 | log.info('argv:', argv); 100 | log.info('version:', pkg.version); 101 | 102 | var legacyCurrentInstallPath; 103 | var legacyNewUpdaterExecutablePath; 104 | if (argv._.length === 2 && checkFileExistSync(argv._[0]) && checkFileExistSync(argv._[1])) { 105 | // This only happens if we have a pre v3.0.0 desktop app attempting to update to v3+ 106 | // FIXME: remove after August 2016 107 | legacyCurrentInstallPath = argv._[0]; 108 | legacyNewUpdaterExecutablePath = argv._[1]; 109 | } 110 | 111 | var currentInstallPath = argv['current-install-path'] || legacyCurrentInstallPath; 112 | var newUpdaterExecutablePath = argv['new-executable'] || legacyNewUpdaterExecutablePath; 113 | 114 | var hasGoodParamsToUpdate = currentInstallPath && newUpdaterExecutablePath; 115 | log.info('Are we going into update mode? ' + (hasGoodParamsToUpdate ? 'Yes' : 'No') + '.', currentInstallPath, newUpdaterExecutablePath); 116 | if(hasGoodParamsToUpdate) { 117 | log.info('I am a new app in a temp dir'); 118 | log.info('I will overwrite the old app with myself and then restart it'); 119 | 120 | return autoUpdate.overwriteOldApp(currentInstallPath, newUpdaterExecutablePath); 121 | } 122 | 123 | initGUI(); // intialises menu and tray, setting up event listeners for both 124 | initApp(); 125 | autoUpdate.poll(); 126 | 127 | autoLauncher.isEnabled(function(enabled) { 128 | if(enabled !== settings.launchOnStartup) { 129 | autoLauncher[(settings.launchOnStartup ? 'enable' : 'disable')](function(err) { 130 | if(err) { 131 | throw err; 132 | } 133 | }); 134 | } 135 | }); 136 | 137 | })(); 138 | 139 | // reopen window when they are all closed 140 | function reopen() { 141 | if (!mainWindow) { 142 | if (!settings.token) { 143 | return showAuth(); 144 | } 145 | return showLoggedInWindow(); 146 | } 147 | } 148 | 149 | function setupTray(win) { 150 | var customTray = new CustomTray(); 151 | win.tray = customTray.get(); 152 | 153 | var roomMenu = new TrayMenu(); 154 | win.tray.menu = roomMenu.get(); 155 | 156 | // FIXME: temporary fix, needs to be repainted 157 | events.on('traymenu:updated', function() { 158 | win.tray.menu = roomMenu.get(); 159 | }); 160 | 161 | // Set unread badge 162 | events.on('traymenu:unread', function(unreadCount) { 163 | win.setBadgeLabel(unreadCount.toString()); 164 | }); 165 | 166 | // Remove badge 167 | events.on('traymenu:read', function() { 168 | win.setBadgeLabel(''); 169 | }); 170 | 171 | if (CLIENT !== 'osx') { 172 | win.tray.on('click', reopen); 173 | } 174 | } 175 | 176 | // initialises and adds listeners to the top level components of the UI 177 | function initGUI() { 178 | log.trace('initGUI()'); 179 | win = gui.Window.get(); 180 | 181 | // Set up tray(OSX)/menu bar(Windows) 182 | if(settings.showInMacMenuBar) { 183 | setupTray(win); 184 | } 185 | 186 | events.on('settings:change:showInMacMenuBar', function(newValue) { 187 | if(newValue) { 188 | setupTray(win); 189 | } 190 | else if(win.tray) { 191 | win.tray.remove(); 192 | } 193 | }); 194 | 195 | gui.App.on('reopen', reopen); // When someone clicks on the dock icon, show window again. 196 | 197 | win.on('close', function (evt) { 198 | log.trace('win:close'); 199 | if (evt === 'quit') { 200 | quitApp(); 201 | } else { 202 | this.close(true); 203 | } 204 | }); 205 | } 206 | 207 | // establishes faye connection and manages signing in/out flow 208 | function initApp() { 209 | 210 | var token = settings.token; 211 | 212 | // user not logged in 213 | if (!token) { 214 | showAuth(); 215 | return; 216 | } 217 | 218 | events.emit('user:signedIn'); 219 | events.on('traymenu:clicked', navigateWindowTo); 220 | events.on('traymenu:signout', signout); 221 | events.on('menu:signout', signout); 222 | 223 | events.on('menu:toggle:next', function () { 224 | if (!mainWindow) return; 225 | var target = mainWindow.window.document.getElementById('mainframe'); 226 | var nextActive = mainWindow.eval(target, "document.cookie.indexOf('gitter_staging=staged');") >= 0; 227 | if (nextActive) { 228 | settings.next = false; 229 | mainWindow.eval(target, "document.cookie='gitter_staging=none;domain=.gitter.im;path=/;expires=' + new Date(Date.now() + 31536000000).toUTCString(); location.reload();"); 230 | } else { 231 | settings.next = true; 232 | mainWindow.eval(target, "document.cookie='gitter_staging=staged;domain=.gitter.im;path=/;expires=' + new Date(Date.now() + 31536000000).toUTCString(); location.reload();"); 233 | } 234 | }); 235 | 236 | events.on('menu:toggle:devtools', toggleDeveloperTools); 237 | 238 | events.on('user:signedOut', function () { 239 | client.disconnect(); 240 | client = null; 241 | }); 242 | 243 | events.on('settings:change:launchOnStartup', function(newValue) { 244 | autoLauncher[(newValue ? 'enable' : 'disable')](function(err) { 245 | if(err) throw err; 246 | }); 247 | }); 248 | 249 | // Realtime client to keep track of the user rooms. 250 | var client = new Gitter.RealtimeClient({ 251 | authProvider: function(cb) { 252 | return cb({ token: token, client: CLIENT }); 253 | } 254 | }); 255 | 256 | var rooms = new Gitter.RoomCollection([], { client: client, listen: true }); 257 | 258 | client.on('change:userId', function (userId) { 259 | events.emit('realtime:connected', rooms); 260 | log.trace('realtime connected()'); 261 | 262 | if (!client) return; 263 | log.trace('attempting to subscribe()'); 264 | var subscription = client.subscribe('/v1/user/' + userId, function (msg) { 265 | if (mainWindowFocused || !msg.notification) return; 266 | 267 | if (msg.notification === 'user_notification') { 268 | notifier({ 269 | title: msg.title, 270 | message: msg.text, 271 | icon: msg.icon, 272 | click: function() { 273 | log.info('Notification user_notification clicked. Moving to', msg.link); 274 | navigateWindowTo(msg.link); 275 | } 276 | }); 277 | } 278 | }); 279 | }); 280 | 281 | if (settings.launchHidden !== true) { 282 | showLoggedInWindow(); 283 | } 284 | } 285 | 286 | function showAuth() { 287 | if (loginView) return; 288 | 289 | // whitelists app:// protocol used for the oAuth callback 290 | gui.App.addOriginAccessWhitelistEntry('https://gitter.im/', 'app', 'gitter', true); 291 | 292 | loginView = new LoginView(gui.Window); 293 | 294 | loginView.on('accessTokenReceived', function(accessToken) { 295 | log.trace('authWindow:loaded'); 296 | settings.token = accessToken; 297 | initApp(accessToken); 298 | loginView.destroy(); 299 | }); 300 | 301 | loginView.on('destroy', function() { 302 | loginView = null; 303 | }); 304 | } 305 | 306 | function signout() { 307 | log.trace('signout()'); 308 | 309 | flushCookies() 310 | .then(function () { 311 | settings.token = null; 312 | 313 | // only close the window if we can, otherwise app may crash 314 | if (mainWindow) { 315 | mainWindow.close(true); 316 | } 317 | 318 | showAuth(); 319 | 320 | events.emit('user:signedOut'); 321 | }); 322 | } 323 | 324 | /** 325 | * showLoggedInWindow() handles the logic for displaying loggedin.html 326 | * 327 | * exec - String, code to be evaluated once the iFrame has loaded. 328 | * @void 329 | */ 330 | function showLoggedInWindow(exec) { 331 | log.trace('showLoggedInWindow()'); 332 | 333 | mainWindow = gui.Window.get( 334 | window.open('loggedin.html', { 335 | focus: true 336 | }) 337 | ); 338 | 339 | mainWindow.setMinimumSize(400, 200); 340 | 341 | var menu = new CustomMenu({ 342 | items: MENU_ITEMS, 343 | label: 'Gitter', 344 | filter: function (item) { 345 | 346 | if (item.support && item.support.indexOf(CLIENT) < 0 ) { 347 | return false; 348 | } 349 | 350 | if (item.auth) { 351 | return item.auth && !!settings.token; 352 | } 353 | 354 | return true; 355 | } 356 | }); 357 | 358 | mainWindow.menu = menu.get(); 359 | 360 | // FIXME: temporary fix, needs to be repainted 361 | events.on('menu:updated', function () { 362 | if (mainWindow) { 363 | mainWindow.menu = menu.get(); 364 | } 365 | }); 366 | 367 | mainWindow.on('loaded', function () { 368 | // When a mac app starts up, it doesn't have focus 369 | // When a Windows or Linux app starts up, it has focus 370 | if(CLIENT !== 'osx') { 371 | mainWindowFocused = true; 372 | } 373 | 374 | if (exec) { 375 | var iFrame = mainWindow.window.document.getElementById('mainframe'); 376 | iFrame.onload = function () { 377 | mainWindow.eval(iFrame, exec); 378 | }; 379 | } 380 | }); 381 | 382 | mainWindow.on('closed', function () { 383 | log.trace('mainWindow:closed'); 384 | mainWindow = null; 385 | mainWindowFocused = false; 386 | }); 387 | 388 | mainWindow.on('focus', function () { 389 | log.trace('mainWindow:focus'); 390 | mainWindowFocused = true; 391 | // TODO: Remove this hack 392 | var toExec = "var cf = document.getElementById('content-frame'); if (cf) cf.contentWindow.dispatchEvent(new Event('focus'));"; 393 | mainWindow.eval(mainWindow.window.document.getElementById('mainframe'),toExec); 394 | }); 395 | 396 | mainWindow.on('blur', function () { 397 | log.trace('mainWindow:blur'); 398 | mainWindowFocused = false; 399 | // TODO: Remove this hack 400 | var toExec = "var cf = document.getElementById('content-frame'); if (cf) cf.contentWindow.dispatchEvent(new Event('blur'));"; 401 | mainWindow.eval(mainWindow.window.document.getElementById('mainframe'),toExec); 402 | }); 403 | 404 | mainWindow.on('new-win-policy', function (frame, url, policy) { 405 | gui.Shell.openExternal(url); 406 | policy.ignore(); 407 | }); 408 | } 409 | 410 | function toggleDeveloperTools() { 411 | log.trace('toggleDeveloperTools()'); 412 | if (mainWindow.isDevToolsOpen()) { 413 | mainWindow.closeDevTools(); 414 | } else { 415 | mainWindow.showDevTools(); 416 | } 417 | } 418 | 419 | function navigateWindowTo(uri) { 420 | var toExec = "window.gitterLoader('" + uri + "');"; 421 | 422 | if (!mainWindow) { 423 | // load window and then navigate 424 | return showLoggedInWindow(toExec); 425 | } 426 | 427 | // simply navigate as we have window 428 | mainWindow.eval(mainWindow.window.document.getElementById('mainframe'), toExec); // FIXME: this is dupe code 429 | mainWindow.focus(); 430 | mainWindow.show(); 431 | } 432 | 433 | function deleteCookie(cookie) { 434 | return new Promise(function (resolve, reject) { 435 | var cookie_url = "http" + (cookie.secure ? "s" : "") + "://" + cookie.domain + cookie.path; 436 | 437 | win.cookies.remove({ url: cookie_url, name: cookie.name }, function (result) { 438 | if (result) { 439 | result = result[0]; 440 | log.info('cookie removed:' + result.name); 441 | resolve(result); 442 | } else { 443 | log.error('failed to remove cookie'); 444 | reject(new Error('Failed to delete cookie.')); 445 | } 446 | }); 447 | }); 448 | } 449 | 450 | function fetchAllCookies() { 451 | log.trace('fetchAllCookies()'); 452 | return new Promise(function (resolve) { 453 | win.cookies.getAll({}, function (cookies) { 454 | resolve(cookies); 455 | }); 456 | }); 457 | } 458 | 459 | function flushCookies() { 460 | return new Promise(function (resolve) { 461 | fetchAllCookies() 462 | .then(function (cookies) { 463 | log.debug('got ' + cookies.length + ' cookies'); 464 | return Promise.all(cookies.map(deleteCookie)); 465 | }) 466 | .then(function () { 467 | log.info('deleted all cookies'); 468 | resolve(true); 469 | }) 470 | .catch(function (err) { 471 | log.error(err.stack); 472 | }); 473 | }); 474 | } 475 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 'use strict'; 3 | 4 | 5 | var SUPPORTED_PLATFORMS = ['win32', 'linux32', 'linux64', 'osx64']; 6 | 7 | var gulp = require('gulp'); 8 | var through = require('through2'); 9 | var gutil = require('gulp-util'); 10 | var shell = require('gulp-shell'); 11 | var exhaustively = require('stream-exhaust'); 12 | var Promise = require('bluebird'); 13 | var os = require('os'); 14 | var fs = require('fs'); 15 | var s3 = require('s3'); 16 | var path = require('path'); 17 | var rimraf = require('rimraf'); 18 | var manifest = require('./package.json'); 19 | var template = require('lodash.template'); 20 | var Builder = require('nw-builder'); 21 | var appmanifest = require('./nwapp/package.json'); 22 | 23 | var LATEST_HTML_TEMPLATE = template(fs.readFileSync('./latest-template.html').toString()); 24 | 25 | var CURRENT_OS = (os.platform().match(/darwin/) ? 'osx' : os.platform()); 26 | 27 | var YEAR = new Date().getFullYear(); 28 | var CACHE_DIR = './cache'; 29 | var OUTPUT_DIR = './opt'; 30 | var SOURCE_DIR = './nwapp'; 31 | 32 | var SIGN_IDENTITY = 'Developer ID Application: Troupe Technology Limited (A86QBWJ43W)'; // To use this identity you must have the appropriate Developer ID cert in your keychain 33 | 34 | var CERTIFICATES_DIR = './certificates'; 35 | var CERTIFICATES_FORMAT = { 36 | 'win': 'troupe-cert.pfx', 37 | 'osx': 'DeveloperID.p12' 38 | }; 39 | 40 | var ARTEFACTS_DIR = './artefacts'; 41 | var LINUX_ARTEFACT_TEMPLATE = template('<%= name %>_<%= version %>_<%= arch %>.deb'); 42 | 43 | var ARTEFACTS = { 44 | 'win': template('<%= name %>Setup-<%= version %>.exe')({ name: appmanifest.name, version: appmanifest.version }), 45 | 'osx': template('<%= name %>-<%= version %>.dmg')({ name: appmanifest.name, version: appmanifest.version }), 46 | 'linux32': LINUX_ARTEFACT_TEMPLATE({ name: appmanifest.name.toLowerCase(), version: appmanifest.version, arch: 'i386' }), 47 | 'linux64': LINUX_ARTEFACT_TEMPLATE({ name: appmanifest.name.toLowerCase(), version: appmanifest.version, arch: 'amd64' }), 48 | }; 49 | 50 | var ARTEFACTS_URL = Object.keys(ARTEFACTS).reduce(function (fold, item) { 51 | fold[item] = path.join(item, ARTEFACTS[item]); 52 | return fold; 53 | }, {}); 54 | 55 | var S3_CONSTS = { 56 | buckets: { 57 | certificates: 'troupe-certs', 58 | updates: 'update.gitter.im' 59 | }, 60 | handleError: function (err) { 61 | gutil.log(err.stack); 62 | }, 63 | handleProgress: function (msg, file) { 64 | if (!this) { 65 | return gutil.log(gutil.colors.red('no progress report')); 66 | } 67 | var progress = ~~((this.progressAmount / this.progressTotal) * 100); 68 | if (progress % 50 !== 0) return; 69 | gutil.log(msg, gutil.colors.cyan('\'' + file + '\''), progress + '%'); 70 | } 71 | }; 72 | 73 | var s3Client = s3.createClient({ 74 | s3Options: { 75 | accessKeyId: S3_CONSTS.credentials && S3_CONSTS.credentials.key, 76 | secretAccessKey: S3_CONSTS.credentials && S3_CONSTS.credentials.secret 77 | } 78 | }); 79 | 80 | function namespace(/* args */) { 81 | return [].slice.call(arguments).join(':'); 82 | } 83 | 84 | function fetchS3(params, done) { 85 | var downloader = s3Client.downloadFile(params); 86 | downloader.on('error', S3_CONSTS.handleError.bind(downloader)); 87 | downloader.on('progress', S3_CONSTS.handleProgress.bind(downloader, 'Downloading', params.localFile)); 88 | downloader.on('end', done); 89 | } 90 | 91 | function pushS3(params) { 92 | var uploader = s3Client.uploadFile(params); 93 | uploader.on('error', S3_CONSTS.handleError.bind(uploader)); 94 | uploader.on('progress', S3_CONSTS.handleProgress.bind(uploader, 'Uploading', params.localFile)); 95 | return Promise.fromCallback(function(cb) { 96 | uploader.on('end', cb); 97 | }); 98 | } 99 | 100 | 101 | // Look for any troubled path lengths so we don't run into problems on the Windows builds: https://github.com/gitterHQ/desktop/issues/59 102 | // If you are running into issues, you can just install the sub-depedency on the root level 103 | // This shouldn't be a problem if we decide to require npm 3 for the desktop builds 104 | gulp.task('check-path-safety-for-windows', function() { 105 | var stream = gulp.src('./nwapp/**/*', {read: false}) 106 | .pipe(through.obj(function(chunk, enc, cb) { 107 | var baseWindowsCheckPath = 'C:\\Users\\some-longish-username\\AppData\\Local\\Temp\\nw2128_17940'; 108 | var pathToCheck = path.join(baseWindowsCheckPath, path.relative(chunk.base, chunk.path)); 109 | 110 | if(pathToCheck.length > 256) { 111 | var nodeModulesMessage = ''; 112 | if(pathToCheck.match('node_modules')) { 113 | nodeModulesMessage = ' --- You can try installing a sub-depedency as a root-level module to shorten up the paths'; 114 | } 115 | throw new gutil.PluginError('checking-path-lengths', { 116 | message: 'You have a path length that exceeds 256 characters and will cause issues on Windows: ' + chunk.path + '. Note, we checked with a base path: ' + baseWindowsCheckPath + nodeModulesMessage 117 | }); 118 | } 119 | 120 | this.push(chunk); 121 | cb(); 122 | })); 123 | 124 | // Avoid the high-water mark: https://github.com/gulpjs/gulp/issues/1356 125 | return exhaustively(stream); 126 | }); 127 | 128 | 129 | [OUTPUT_DIR, ARTEFACTS_DIR, CACHE_DIR].forEach(function (dir) { 130 | gulp.task('clean:' + path.basename(dir).toLowerCase(), function (done) { 131 | rimraf(dir, done); 132 | }); 133 | }); 134 | 135 | gulp.task('clean', [OUTPUT_DIR, ARTEFACTS_DIR, CACHE_DIR].map(function (dir) { return 'clean:' + path.basename(dir); })); 136 | 137 | /* cert:fetch:{{ OS }} */ 138 | Object.keys(CERTIFICATES_FORMAT).forEach(function(OS) { 139 | var file = CERTIFICATES_FORMAT[OS]; 140 | gulp.task('cert:fetch:' + OS, fetchS3.bind(null, { 141 | localFile: path.join(CERTIFICATES_DIR, file), 142 | s3Params: { 143 | Bucket: S3_CONSTS.buckets.certificates, 144 | Key: file 145 | } 146 | })); 147 | }); 148 | 149 | /* fetches the current os certificate */ 150 | gulp.task('cert:fetch', ['cert:fetch:' + CURRENT_OS]); 151 | 152 | gulp.task('build', ['clean:opt', 'clean:artefacts', 'check-path-safety-for-windows'], function (done) { 153 | fs.mkdirSync(ARTEFACTS_DIR); 154 | var builder = new Builder({ 155 | buildDir: OUTPUT_DIR, 156 | version: manifest.nwversion, 157 | files: [path.join(SOURCE_DIR, '**')], 158 | platforms: SUPPORTED_PLATFORMS, 159 | winIco: './icons/gitter.ico', 160 | macIcns: './icons/AppIcon.icns', 161 | macPlist: { 162 | NSHumanReadableCopyright: 'Copyright © 2013-' + YEAR + ' Troupe Technology Limited. All rights reserved.', 163 | CFBundleIdentifier: 'com.gitter.desktop' 164 | } 165 | }); 166 | 167 | builder.on('log', gutil.log.bind(gutil, 'nw-builder:')); 168 | builder.build().nodeify(done); 169 | }); 170 | 171 | gulp.task('build:linux', ['pack:deb:32', 'pack:deb:64']); 172 | gulp.task('build:osx', ['pack:osx']); 173 | gulp.task('build:all', ['build:linux', 'build:osx']); 174 | 175 | gulp.task('run', function (done){ 176 | var builder = new Builder({ 177 | version: manifest.nwversion, 178 | files: path.join(SOURCE_DIR, '**') 179 | }); 180 | 181 | builder.on('log', gutil.log.bind(gutil, 'nw-builder:')); 182 | builder.run().nodeify(done); 183 | }); 184 | 185 | // generated Debian/Ubuntu packages for 32 and 64 bit archs 186 | [32, 64].forEach(function (arch) { 187 | 188 | var fpm = template('fpm -s dir -t deb -a <%= port %> -n <%= name %> --category Utility --after-install ./opt/Gitter/linux<%= arch %>/after-install.sh --after-remove ./opt/Gitter/linux<%= arch %>/after-remove.sh --url "https://gitter.im" --description "Where developers come to talk" --maintainer "Troupe Technology " -p <%= output %> -v <%= version %> ./opt/Gitter/linux<%= arch %>/'); 189 | var catsed = gutil.template('cat ./linux/<%= file %> | sed "s/{{arch}}/linux<%= arch %>/g" > ./opt/Gitter/linux<%= arch %>/<%= file %>'); 190 | 191 | gulp.task('pack:templates:' + arch, ['build'], shell.task([ // this probably waits for build 192 | catsed({ arch: arch, file: 'gitter.desktop' }) , 193 | catsed({ arch: arch, file: 'after-install.sh' }), 194 | catsed({ arch: arch, file: 'after-remove.sh' }), 195 | 'cp ./icons/logo.png ./opt/Gitter/linux' + arch + '/logo.png' 196 | ])); 197 | 198 | gulp.task('pack:deb:' + arch, ['pack:templates:' + arch], shell.task( 199 | fpm({ 200 | arch: arch, 201 | port: arch === 32 ? 'i386' : 'amd64', 202 | version: appmanifest.version, 203 | name: appmanifest.name.toLowerCase(), 204 | output: path.join(ARTEFACTS_DIR, ARTEFACTS['linux' + arch]) 205 | }) 206 | )); 207 | }); 208 | 209 | var dmg_cmd = template('./osx/create-dmg/create-dmg --icon "<%= name %>" 311 50 --icon-size 32 --app-drop-link 311 364 --window-size 622 683 --volname "<%= name %>" --volicon "./icons/AppIcon.icns" --background "./icons/dmg-bg.png" "<%= output %>" "<%= path %>/Gitter/osx64/Gitter.app"'); 210 | 211 | // Only runs on OSX (requires XCode properly configured) 212 | gulp.task('sign:osx', ['build'], shell.task([ 213 | /* * / 214 | 'codesign -v -f -s "'+ SIGN_IDENTITY +'" '+ OUTPUT_DIR +'/Gitter/osx64/Gitter.app/Contents/Frameworks/*', 215 | 'codesign -v -f -s "'+ SIGN_IDENTITY +'" '+ OUTPUT_DIR +'/Gitter/osx64/Gitter.app', 216 | 'codesign -v --display '+ OUTPUT_DIR +'/Gitter/osx64/Gitter.app', 217 | 'codesign -v --verify '+ OUTPUT_DIR +'/Gitter/osx64/Gitter.app' 218 | /* */ 219 | ])); 220 | 221 | // Only runs on OSX 222 | if (ARTEFACTS.osx) { 223 | gulp.task('pack:osx', ['sign:osx'], shell.task(dmg_cmd({ 224 | name: appmanifest.name, 225 | version: appmanifest.version, 226 | path: OUTPUT_DIR, 227 | output: path.join(ARTEFACTS_DIR, ARTEFACTS.osx) 228 | }))); 229 | } 230 | 231 | // Generate auto-updater packages for Windows 232 | gulp.task('autoupdate:zip:win', shell.task([ 233 | 'cd '+ OUTPUT_DIR + '/Gitter/win32; zip win32.zip ./* > /dev/null 2>&1', 234 | 'mv '+ OUTPUT_DIR + '/Gitter/win32/win32.zip ' + ARTEFACTS_DIR + '/win32.zip', 235 | ])); 236 | 237 | // Generate auto-updater packages for OSX 238 | gulp.task('autoupdate:zip:osx', shell.task([ 239 | 'cd '+ OUTPUT_DIR + '/Gitter/osx64; zip -r osx.zip ./Gitter.app > /dev/null 2>&1', 240 | 'mv '+ OUTPUT_DIR + '/Gitter/osx64/osx.zip '+ ARTEFACTS_DIR + '/osx.zip', 241 | ])); 242 | 243 | // Generate auto-updater packages for linux 244 | gulp.task('autoupdate:zip:linux', shell.task([ 245 | 'cd '+ OUTPUT_DIR + '/Gitter/osx64; zip -r osx.zip ./Gitter.app > /dev/null 2>&1', 246 | 'mv '+ OUTPUT_DIR + '/Gitter/osx64/osx.zip '+ ARTEFACTS_DIR + '/linux32.zip', 247 | 'cd '+ OUTPUT_DIR + '/Gitter/osx64; zip -r osx.zip ./Gitter.app > /dev/null 2>&1', 248 | 'mv '+ OUTPUT_DIR + '/Gitter/osx64/osx.zip '+ ARTEFACTS_DIR + '/linux64.zip', 249 | ])); 250 | 251 | gulp.task('autoupdate:push:win', function() { 252 | return pushS3({ 253 | localFile: ARTEFACTS_DIR + '/win32.zip', 254 | s3Params: { 255 | Bucket: S3_CONSTS.buckets.updates, 256 | Key: 'win/win32.zip', 257 | CacheControl: 'public, max-age=0, no-cache', 258 | ACL: 'public-read' 259 | } 260 | }); 261 | }); 262 | 263 | gulp.task('autoupdate:push:osx', function() { 264 | return pushS3({ 265 | localFile: ARTEFACTS_DIR + '/osx.zip', 266 | s3Params: { 267 | Bucket: S3_CONSTS.buckets.updates, 268 | Key: 'osx/osx.zip', 269 | CacheControl: 'public, max-age=0, no-cache', 270 | ACL: 'public-read' 271 | } 272 | }); 273 | }); 274 | 275 | 276 | gulp.task('autoupdate:push:linux', function() { 277 | return Promise.all([ 278 | pushS3({ 279 | localFile: ARTEFACTS_DIR + '/linux32.zip', 280 | s3Params: { 281 | Bucket: S3_CONSTS.buckets.updates, 282 | Key: 'linux32/linux32.zip', 283 | CacheControl: 'public, max-age=0, no-cache', 284 | ACL: 'public-read' 285 | } 286 | }), 287 | pushS3({ 288 | localFile: ARTEFACTS_DIR + '/linux64.zip', 289 | s3Params: { 290 | Bucket: S3_CONSTS.buckets.updates, 291 | Key: 'linux64/linux64.zip', 292 | CacheControl: 'public, max-age=0, no-cache', 293 | ACL: 'public-read' 294 | } 295 | }) 296 | ]); 297 | }); 298 | 299 | 300 | 301 | 302 | 303 | var pushManifestToDest = function(destinationKey) { 304 | return pushS3({ 305 | localFile: SOURCE_DIR + '/package.json', 306 | s3Params: { 307 | Bucket: S3_CONSTS.buckets.updates, 308 | Key: destinationKey, 309 | CacheControl: 'public, max-age=0, no-cache', 310 | ACL: 'public-read' 311 | } 312 | }); 313 | }; 314 | var platformFolders = [ 315 | 'osx', 316 | 'win', 317 | 'linux' 318 | ]; 319 | gulp.task('manifest:push:osx', function() { 320 | return pushManifestToDest('osx/package.json'); 321 | }); 322 | gulp.task('manifest:push:win', function() { 323 | return pushManifestToDest('win/package.json'); 324 | }); 325 | gulp.task('manifest:push:linux', function() { 326 | return Promise.all([ 327 | pushManifestToDest('linux32/package.json'), 328 | pushManifestToDest('linux64/package.json') 329 | ]); 330 | }); 331 | gulp.task('manifest:push', ['manifest:push:osx', 'manifest:push:win', 'manifest:push:linux'], function() { 332 | return true; 333 | }); 334 | 335 | gulp.task('identity', function (done) { 336 | done(); 337 | }); 338 | 339 | Object.keys(ARTEFACTS).forEach(function (platform) { 340 | console.log(platform); 341 | 342 | var LATEST_TEMPLATE = template('latest_<%= platform %>.html'); 343 | 344 | gulp.task(namespace('artefacts', 'push', platform), function() { 345 | return pushS3({ 346 | localFile: path.join(ARTEFACTS_DIR, ARTEFACTS[platform]), 347 | s3Params: { 348 | Bucket: S3_CONSTS.buckets.updates, 349 | Key: path.join(platform, ARTEFACTS[platform]), 350 | CacheControl: 'public, max-age=0, no-cache', 351 | ACL: 'public-read' 352 | } 353 | }); 354 | }); 355 | 356 | // LATEST_HTML_TEMPLATE 357 | gulp.task(namespace('redirect', 'source', platform), function (done) { 358 | fs.writeFile( 359 | path.join(ARTEFACTS_DIR, LATEST_TEMPLATE({ platform: platform })), 360 | LATEST_HTML_TEMPLATE({ url: ARTEFACTS_URL[platform] }), 361 | done 362 | ); 363 | }); 364 | 365 | gulp.task(namespace('redirect', 'push', platform), function() { 366 | return pushS3({ 367 | localFile: path.join(ARTEFACTS_DIR, LATEST_TEMPLATE({ platform: platform })), 368 | s3Params: { 369 | Bucket: S3_CONSTS.buckets.updates, 370 | Key: path.join(platform, 'latest'), 371 | CacheControl: 'public, max-age=0, no-cache', 372 | ACL: 'public-read' 373 | } 374 | }); 375 | }); 376 | }); 377 | 378 | gulp.task('artefacts:push', Object.keys(ARTEFACTS).map(function (platform) { return namespace('artefacts', 'push', platform); })); 379 | gulp.task('redirect:source', Object.keys(ARTEFACTS).map(function (platform) { return namespace('redirect', 'source', platform); })); 380 | gulp.task('redirect:push', Object.keys(ARTEFACTS).map(function (platform) { return namespace('redirect', 'push', platform); })); 381 | 382 | // gulp.task('build:win', ['build'], function (done) { 383 | 384 | // // shell.task([ 385 | // // s3cmd+'./'+ OUTPUT_DIR +'/Gitter/'+winInstaller+' '+s3bucket+'/win/', 386 | // // s3cmd+'./'+ OUTPUT_DIR +'/Gitter/win32/win32.zip '+s3bucket+'/win/', 387 | // // s3cmd+'--content-type "text/html" ./latest '+s3bucket+'/win/', 388 | // // s3cmd+'./nwapp/package.json '+s3bucket+'/desktop/' 389 | // // ])); 390 | 391 | // // var winInstaller = "GitterSetup-"+appmanifest.version+".exe"; 392 | // // var winDownloadUrl = "https://update.gitter.im/win/" + winInstaller; 393 | // // var uploader = s3Client.uploadFile({ 394 | // // localFile: 'README.md', 395 | // // s3Params: { 396 | // // Bucket: S3_CONSTS.buckets.updates, 397 | // // Key: 'nwdev/README.md', 398 | // // CacheControl: 'public, max-age=0, no-cache', 399 | // // ACL: 'public-read' 400 | // // }, 401 | // // }); 402 | // }); 403 | --------------------------------------------------------------------------------