├── .eslintrc ├── .gitignore ├── .lesshintrc ├── .travis.yml ├── CHANGELOG.json ├── CHANGELOG.md ├── ISSUE_TEMPLATE.md ├── LICENSE.md ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── appveyor.yml ├── browserstack.png ├── circle.yml ├── ciscripts ├── appveyor │ ├── build.cmd │ ├── build_deploy.cmd │ └── build_staging.cmd ├── circleci │ ├── build.sh │ ├── build_deploy.sh │ ├── build_staging.sh │ ├── dependencies.sh │ └── install_aws.sh └── travis │ ├── build.sh │ ├── build_deploy.sh │ ├── build_staging.sh │ ├── import_cert.sh │ └── install_aws.sh ├── coffeelint.json ├── design.sketch ├── gulpfile.coffee ├── package.json ├── resources ├── darwin │ ├── App.plist │ ├── Helper EH.plist │ ├── Helper NP.plist │ ├── Helper.plist │ ├── app.icns │ ├── dmg.icns │ ├── dmg.json │ ├── dmg_bg.png │ └── dmg_bg@2x.png ├── linux │ ├── after-install.sh │ ├── after-remove.sh │ ├── app.desktop │ ├── icons │ │ ├── 128.png │ │ ├── 16.png │ │ ├── 24.png │ │ ├── 256.png │ │ ├── 32.png │ │ ├── 48.png │ │ ├── 512.png │ │ └── 64.png │ └── startup.desktop └── win │ ├── Fusion.dll │ ├── FusionSdk.nsh │ ├── app.ico │ ├── eula.txt │ ├── install-spinner.gif │ ├── installer.nsi │ └── setup.ico ├── src ├── .loggerignore ├── README.md ├── dicts │ ├── de.aff │ ├── de.dic │ ├── es.aff │ ├── es.dic │ ├── fr.aff │ ├── fr.dic │ ├── hu.aff │ ├── hu.dic │ ├── ro_RO.aff │ ├── ro_RO.dic │ ├── ru.aff │ └── ru.dic ├── html │ ├── app.html │ └── shim-notification.html ├── images │ ├── app_icon.png │ ├── default_avatar.png │ ├── tray.ico │ ├── tray.png │ ├── tray@2x.png │ ├── tray@3x.png │ ├── trayAlert.ico │ ├── trayAlert.png │ ├── trayAlert@2x.png │ ├── trayAlert@3x.png │ ├── trayBlackTemplate.png │ ├── trayBlackTemplate@2x.png │ ├── trayBlackTemplate@3x.png │ ├── trayWhiteTemplate.png │ ├── trayWhiteTemplate@2x.png │ ├── trayWhiteTemplate@3x.png │ ├── windowIcon.png │ ├── windowIcon@2x.png │ └── windowIcon@3x.png ├── package.json ├── scripts │ ├── browser │ │ ├── application.js │ │ ├── bridges │ │ │ └── native-notifier.js │ │ ├── components │ │ │ ├── auto-launcher │ │ │ │ ├── base.js │ │ │ │ ├── impl-darwin.js │ │ │ │ ├── impl-linux.js │ │ │ │ ├── impl-win32.js │ │ │ │ └── index.js │ │ │ ├── auto-updater │ │ │ │ ├── base.js │ │ │ │ ├── impl-linux.js │ │ │ │ ├── impl-win32-portable.js │ │ │ │ └── index.js │ │ │ ├── native-notifier │ │ │ │ ├── base.js │ │ │ │ ├── impl-darwin.js │ │ │ │ ├── impl-linux.js │ │ │ │ ├── impl-win32.js │ │ │ │ └── index.js │ │ │ └── squirrel-events.js │ │ ├── init.js │ │ ├── main.js │ │ ├── managers │ │ │ ├── app-listeners-manager.js │ │ │ ├── auto-update-manager.js │ │ │ ├── main-menu-manager.js │ │ │ ├── main-window-manager.js │ │ │ ├── notif-manager.js │ │ │ └── tray-manager.js │ │ ├── menus │ │ │ ├── context.js │ │ │ ├── expressions │ │ │ │ ├── expr-click.js │ │ │ │ ├── expr-meta.js │ │ │ │ ├── expr-parse.js │ │ │ │ ├── expr-value.js │ │ │ │ └── index.js │ │ │ ├── main.js │ │ │ ├── templates │ │ │ │ ├── main-app.js │ │ │ │ ├── main-edit.js │ │ │ │ ├── main-help.js │ │ │ │ ├── main-privacy.js │ │ │ │ ├── main-theme.js │ │ │ │ ├── main-view.js │ │ │ │ ├── main-window.js │ │ │ │ └── tray.js │ │ │ ├── tray.js │ │ │ └── utils.js │ │ ├── services │ │ │ ├── piwik.js │ │ │ └── sentry.js │ │ └── utils │ │ │ ├── file-logger.js │ │ │ ├── logger.js │ │ │ ├── prefs-defaults.js │ │ │ ├── prefs.js │ │ │ ├── repl.js │ │ │ └── request-filter.js │ ├── common │ │ ├── analytics │ │ │ ├── actions.js │ │ │ ├── categories.js │ │ │ └── names.js │ │ ├── bridges │ │ │ └── native-notifier.js │ │ ├── electron │ │ │ └── app.js │ │ ├── modules │ │ │ └── debug.js │ │ ├── services │ │ │ ├── piwik.js │ │ │ └── sentry.js │ │ └── utils │ │ │ ├── analytics.js │ │ │ ├── file-paths.js │ │ │ ├── files.js │ │ │ ├── graphics.js │ │ │ ├── language-codes.js │ │ │ ├── logger-browser.js │ │ │ ├── logger.js │ │ │ ├── platform.js │ │ │ ├── prefs.js │ │ │ ├── spellchecker.js │ │ │ └── urls.js │ └── renderer │ │ ├── components │ │ └── keymap.js │ │ ├── index.js │ │ ├── init.js │ │ ├── preload │ │ ├── events.js │ │ ├── index.js │ │ └── notification.js │ │ ├── services │ │ ├── piwik.js │ │ └── sentry.js │ │ ├── shim-notification │ │ ├── index.js │ │ └── shim.js │ │ └── webview │ │ ├── events.js │ │ ├── index.js │ │ └── listeners.js ├── styles │ ├── app.less │ ├── auto-hide-sidebar.less │ ├── loader.less │ ├── shim-notification.less │ └── webview.less └── themes │ ├── black.css │ ├── dark.css │ ├── default.css │ ├── midnight.css │ └── mosaic.css └── tasks ├── args.coffee ├── build.coffee ├── changelog.coffee ├── clean.coffee ├── compile.coffee ├── download.coffee ├── kill.coffee ├── lock.coffee ├── pack.coffee ├── publish.coffee ├── purge.coffee ├── rebuild.coffee ├── resources.coffee ├── restart.coffee ├── start.coffee ├── utils.coffee └── watch.coffee /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "babel-eslint", 4 | "plugins": [ 5 | "babel" 6 | ], 7 | "extends": "standard", 8 | "globals": { 9 | "log": true, 10 | "logError": true, 11 | "logFatal": true 12 | }, 13 | "rules": { 14 | "generator-star-spacing": 0, 15 | "new-cap": 0, 16 | "array-bracket-spacing": 0, 17 | "object-curly-spacing": 0, 18 | "object-shorthand": 0, 19 | "arrow-parens": 0, 20 | "babel/generator-star-spacing": 1, 21 | "babel/new-cap": 1, 22 | "babel/array-bracket-spacing": 1, 23 | "babel/object-curly-spacing": 1, 24 | "babel/object-shorthand": 1, 25 | "babel/arrow-parens": 1, 26 | "babel/no-await-in-loop": 1, 27 | "babel/flow-object-type": 1, 28 | "semi": [2, "always"], 29 | "no-extra-semi": 2, 30 | "semi-spacing": [2, { "before": false, "after": true }], 31 | "padded-blocks": 0 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | cache/ 3 | dist/ 4 | node_modules/ 5 | npm-debug.log 6 | .env 7 | .idea 8 | -------------------------------------------------------------------------------- /.lesshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "idSelector": false, 3 | "importantRule": false, 4 | "propertyOrdering": false, 5 | "singleLinePerSelector": false, 6 | "spaceAroundOperator": false, 7 | "newlineAfterBlock": false, 8 | "universalSelector": false 9 | } 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | git: 2 | depth: 10 3 | 4 | branches: 5 | only: 6 | - staging 7 | - deploy 8 | 9 | os: 10 | - osx 11 | 12 | osx_image: xcode7.3 13 | language: objective-c 14 | compiler: clang 15 | sudo: false 16 | 17 | env: 18 | global: 19 | - SIGN_DARWIN_KEYCHAIN_NAME=travis.keychain 20 | - SIGN_DARWIN_KEYCHAIN_PASSWORD=travis 21 | 22 | install: 23 | - git clone https://github.com/creationix/nvm.git /tmp/.nvm 24 | - source /tmp/.nvm/nvm.sh 25 | - nvm install 5.5 26 | - nvm use 5.5 27 | - npm install -g gulp 28 | - npm install 29 | 30 | before_script: 31 | - npm test 32 | 33 | script: 34 | - if [[ $TRAVIS_BRANCH == 'staging' ]]; then ./ciscripts/travis/build_staging.sh; fi 35 | - if [[ $TRAVIS_BRANCH == 'deploy' ]]; then ./ciscripts/travis/build_deploy.sh; fi 36 | 37 | cache: 38 | directories: 39 | - node_modules 40 | - src/node_modules 41 | - cache 42 | 43 | notifications: 44 | email: false 45 | webhooks: 46 | - https://webhooks.gitter.im/e/9d5d42078307e6548093 47 | -------------------------------------------------------------------------------- /CHANGELOG.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "version": "2.0.9", 3 | "channel": "stable", 4 | "releasedAt": 1490971692902, 5 | "urgency": "low", 6 | "changes": { 7 | "General": [ 8 | "Stable release with all the features." 9 | ] 10 | } 11 | }, { 12 | "version": "2.0.8", 13 | "channel": "beta", 14 | "releasedAt": 1490272214091, 15 | "urgency": "low", 16 | "changes": { 17 | "General": [ 18 | "Added Hungarian dictionary for the spell checker.", 19 | "Fixed white borders around chat bubbles.", 20 | "Fixed app getting stuck on the loading screen.", 21 | "Fixed error that appeared sometimes when right-clicking." 22 | ] 23 | } 24 | }, { 25 | "version": "2.0.7", 26 | "channel": "dev", 27 | "releasedAt": 1489660479542, 28 | "urgency": "low", 29 | "changes": { 30 | "General": [ 31 | "Zoom level changes in 25% increments now instead of 100%.", 32 | "Fixed icon resolution (adios blurriness!).", 33 | "Fixed/re-added shortcuts to switch between conversations.", 34 | "Fixed/re-added shortcut to focus on the Search input field.", 35 | "Fixed audio/video calls semetimes opening 2 windows.", 36 | "Replaced the About menu with a custom dialog that mentions contributors.", 37 | "Updated theme Midnight and fixed some minor styling issues.", 38 | "Added a notification displayed after the app updates to see what's changed.", 39 | "Added a new preference in Window that changes how first clicks are handled.", 40 | "Added a Preferences/Settings shortcut for the in-app Settings.", 41 | "Made window Reload clear the cache as well.", 42 | "Added back the context menu with word corrections." 43 | ], 44 | "OS X": [ 45 | "Fixed window not closing after menubar is turned on and dock off." 46 | ], 47 | "Windows": [ 48 | "Removed some shortcuts that caused compatibility issues.", 49 | "Fixed 'Auto Hide Menu Bar' resetting after restart and not reappearing when Alt is pressed." 50 | ], 51 | "Linux": [ 52 | "Removed some shortcuts that caused compatibility issues.", 53 | "Fixed 'Auto Hide Menu Bar' resetting after restart and not reappearing when Alt is pressed.", 54 | "When a dark theme is set, the app will request a dark window frame on GTK+3 environments.", 55 | "The app is distributed as a .tar.gz now too." 56 | ] 57 | } 58 | }, { 59 | "version": "2.0.6", 60 | "channel": "stable", 61 | "releasedAt": 1489325983503, 62 | "urgency": "low", 63 | "changes": { 64 | "General": [ 65 | "Fixed an internal error related to notifications count." 66 | ] 67 | } 68 | }, { 69 | "version": "2.0.5", 70 | "channel": "beta", 71 | "releasedAt": 1487851951578, 72 | "urgency": "low", 73 | "changes": { 74 | "General": [ 75 | "Updated dependencies and Electron to v1.4.15.", 76 | "Disabled hardware acceleration (should be easier on the GPU).", 77 | "Added option to disable typing and seen indicators.", 78 | "Made the crash reporter use less resources.", 79 | "Fixed file download links opening in the browser.", 80 | "Fixed an issue with updates on Windows and Linux.", 81 | "Fixed audio and video calls.", 82 | "Fixed blinking badge.", 83 | "Fixed top banner still appearing sometimes." 84 | ] 85 | } 86 | }, { 87 | "version": "2.0.4", 88 | "channel": "beta", 89 | "releasedAt": 1483002596589, 90 | "urgency": "low", 91 | "changes": { 92 | "General": [ 93 | "Remove custom context menu (temporarily) to fix crashes." 94 | ], 95 | "Windows": [ 96 | "Fix RightAlt+I opening dev tools." 97 | ] 98 | } 99 | }, { 100 | "version": "2.0.3", 101 | "channel": "beta", 102 | "releasedAt": 1482919367144, 103 | "urgency": "low", 104 | "changes": { 105 | "General": [ 106 | "Update Electron to v1.4.13." 107 | ], 108 | "Linux": [ 109 | "Remove special treatment for Elementary OS." 110 | ] 111 | } 112 | }, { 113 | "version": "2.0.1", 114 | "channel": "beta", 115 | "releasedAt": 1473193961005, 116 | "urgency": "low", 117 | "changes": { 118 | "General": [ 119 | "Add toggle to auto-hide the sidebar.", 120 | "Various fixes and improvements.", 121 | "Dependency updates." 122 | ] 123 | } 124 | }, { 125 | "version": "2.0.0", 126 | "channel": "beta", 127 | "releasedAt": 1472191336102, 128 | "urgency": "low", 129 | "changes": { 130 | "General": [ 131 | "Initial v2 release." 132 | ], 133 | "OS X": [ 134 | "Initial v2 release." 135 | ], 136 | "Windows": [ 137 | "Initial v2 release." 138 | ], 139 | "Linux": [ 140 | "Initial v2 release." 141 | ] 142 | } 143 | }] 144 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [2.0.9](https://github.com/aluxian/Messenger-for-Desktop/tree/v2.0.9) (2017-31-03) 2 | 3 | [Full Changelog](https://github.com/aluxian/Messenger-for-Desktop/compare/v2.0.8...v2.0.9) • [Download](https://github.com/aluxian/Messenger-for-Desktop/releases/tag/v2.0.9) 4 | 5 | **General** 6 | 7 | - Stable release with all the features. 8 | 9 | ## [2.0.8-beta](https://github.com/aluxian/Messenger-for-Desktop/tree/v2.0.8) (2017-23-03) 10 | 11 | [Full Changelog](https://github.com/aluxian/Messenger-for-Desktop/compare/v2.0.7...v2.0.8) • [Download](https://github.com/aluxian/Messenger-for-Desktop/releases/tag/v2.0.8) 12 | 13 | **General** 14 | 15 | - Added Hungarian dictionary for the spell checker. 16 | - Fixed white borders around chat bubbles. 17 | - Fixed app getting stuck on the loading screen. 18 | - Fixed error that appeared sometimes when right-clicking. 19 | 20 | ## [2.0.7-dev](https://github.com/aluxian/Messenger-for-Desktop/tree/v2.0.7) (2017-16-03) 21 | 22 | [Full Changelog](https://github.com/aluxian/Messenger-for-Desktop/compare/v2.0.6...v2.0.7) • [Download](https://github.com/aluxian/Messenger-for-Desktop/releases/tag/v2.0.7) 23 | 24 | **General** 25 | 26 | - Zoom level changes in 25% increments now instead of 100%. 27 | - Fixed icon resolution (adios blurriness!). 28 | - Fixed/re-added shortcuts to switch between conversations. 29 | - Fixed/re-added shortcut to focus on the Search input field. 30 | - Fixed audio/video calls semetimes opening 2 windows. 31 | - Replaced the About menu with a custom dialog that mentions contributors. 32 | - Updated theme Midnight and fixed some minor styling issues. 33 | - Added a notification displayed after the app updates to see what's changed. 34 | - Added a new preference in Window that changes how first clicks are handled. 35 | - Added a Preferences/Settings shortcut for the in-app Settings. 36 | - Made window Reload clear the cache as well. 37 | - Added back the context menu with word corrections. 38 | 39 | **OS X** 40 | 41 | - Fixed window not closing after menubar is turned on and dock off. 42 | 43 | **Windows** 44 | 45 | - Removed some shortcuts that caused compatibility issues. 46 | - Fixed 'Auto Hide Menu Bar' resetting after restart and not reappearing when Alt is pressed. 47 | 48 | **Linux** 49 | 50 | - Removed some shortcuts that caused compatibility issues. 51 | - Fixed 'Auto Hide Menu Bar' resetting after restart and not reappearing when Alt is pressed. 52 | - When a dark theme is set, the app will request a dark window frame on GTK+3 environments. 53 | - The app is distributed as a .tar.gz now too. 54 | 55 | ## [2.0.6](https://github.com/aluxian/Messenger-for-Desktop/tree/v2.0.6) (2017-12-03) 56 | 57 | [Full Changelog](https://github.com/aluxian/Messenger-for-Desktop/compare/v2.0.5...v2.0.6) • [Download](https://github.com/aluxian/Messenger-for-Desktop/releases/tag/v2.0.6) 58 | 59 | **General** 60 | 61 | - Fixed an internal error related to notifications count. 62 | 63 | ## [2.0.5-beta](https://github.com/aluxian/Messenger-for-Desktop/tree/v2.0.5) (2017-23-02) 64 | 65 | [Full Changelog](https://github.com/aluxian/Messenger-for-Desktop/compare/v2.0.4...v2.0.5) • [Download](https://github.com/aluxian/Messenger-for-Desktop/releases/tag/v2.0.5) 66 | 67 | **General** 68 | 69 | - Updated dependencies and Electron to v1.4.15. 70 | - Disabled hardware acceleration (should be easier on the GPU). 71 | - Added option to disable typing and seen indicators. 72 | - Made the crash reporter use less resources. 73 | - Fixed file download links opening in the browser. 74 | - Fixed an issue with updates on Windows and Linux. 75 | - Fixed audio and video calls. 76 | - Fixed blinking badge. 77 | - Fixed top banner still appearing sometimes. 78 | 79 | ## [2.0.4-beta](https://github.com/aluxian/Messenger-for-Desktop/tree/v2.0.4) (2016-29-12) 80 | 81 | [Full Changelog](https://github.com/aluxian/Messenger-for-Desktop/compare/v2.0.3...v2.0.4) • [Download](https://github.com/aluxian/Messenger-for-Desktop/releases/tag/v2.0.4) 82 | 83 | **General** 84 | 85 | - Remove custom context menu (temporarily) to fix crashes. 86 | 87 | **Windows** 88 | 89 | - Fix RightAlt+I opening dev tools. 90 | 91 | ## [2.0.3-beta](https://github.com/aluxian/Messenger-for-Desktop/tree/v2.0.3) (2016-28-12) 92 | 93 | [Full Changelog](https://github.com/aluxian/Messenger-for-Desktop/compare/v2.0.1...v2.0.3) • [Download](https://github.com/aluxian/Messenger-for-Desktop/releases/tag/v2.0.3) 94 | 95 | **General** 96 | 97 | - Update Electron to v1.4.13. 98 | 99 | **Linux** 100 | 101 | - Remove special treatment for Elementary OS. 102 | 103 | ## [2.0.1-beta](https://github.com/aluxian/Messenger-for-Desktop/tree/v2.0.1) (2016-06-09) 104 | 105 | [Full Changelog](https://github.com/aluxian/Messenger-for-Desktop/compare/v2.0.0...v2.0.1) • [Download](https://github.com/aluxian/Messenger-for-Desktop/releases/tag/v2.0.1) 106 | 107 | **General** 108 | 109 | - Add toggle to auto-hide the sidebar. 110 | - Various fixes and improvements. 111 | - Dependency updates. 112 | 113 | ## [2.0.0-beta](https://github.com/aluxian/Messenger-for-Desktop/tree/v2.0.0) (2016-26-08) 114 | 115 | [Download](https://github.com/aluxian/Messenger-for-Desktop/releases/tag/v2.0.0) 116 | 117 | **General** 118 | 119 | - Initial v2 release. 120 | 121 | **OS X** 122 | 123 | - Initial v2 release. 124 | 125 | **Windows** 126 | 127 | - Initial v2 release. 128 | 129 | **Linux** 130 | 131 | - Initial v2 release. 132 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | * Operating system: 2 | * Messenger for Desktop version: 3 | 4 | ``` 5 | If you're reporting a bug and think that a log would help, please do the following: 6 | 1. Open the app 7 | 2. Go to Menu > Run in debug mode 8 | 3. Try to reproduce the bug 9 | 4. Go to Menu > Open debug log 10 | 5. Paste it here 11 | ``` 12 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Alexandru Rosianu 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 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Hey, thanks for helping! Please make sure you're merging into `develop`. 2 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | clone_depth: 10 2 | skip_tags: true 3 | 4 | branches: 5 | only: 6 | - staging 7 | - deploy 8 | 9 | init: 10 | - git config --global core.autocrlf input 11 | 12 | environment: 13 | SIGNTOOL_PATH: 'C:/Program Files (x86)/Microsoft SDKs/Windows/v7.1A/Bin/SignTool.exe' 14 | MAKENSIS_PATH: 'C:/Program Files (x86)/NSIS/makensis.exe' 15 | SIGN_WIN_CERTIFICATE_FILE: 'C:/foss_ar_sha2.pfx' 16 | SIGN_WIN_CERTIFICATE_PASSWORD: 17 | secure: jZHktkydPALqcwx36cBY1g== 18 | AWS_ACCESS_KEY_ID: 19 | secure: N30cC1yCbVB5vUl+NFuwIabBhTkBczemO2udimmIfQ4= 20 | AWS_SECRET_ACCESS_KEY: 21 | secure: DSp24Yb5sVd311kCztsFoIKFOiwFuVuQmdcNF1B8p+I1uL3z08G53DKmiGbtsXfl 22 | BINTRAY_API_KEY: 23 | secure: e+l7MfTu+t2nHFKcpHEz5LgYiVypOVtYnfssYkJ3n+Wno/cldvI+l0jbLosVpQAx 24 | GITHUB_TOKEN: 25 | secure: 6IU2moI6jYacITwNSUyxIEbeDjkXgBO1SC3HjFMtdyMwpxgJw+2AdUD5Um+J5DNq 26 | SENTRY_DSN_PRIVATE: 27 | secure: TATf2W4rkZJb3TtKXszFb0zUtXoI90pTtIXDeNaXF15fNpSIaOwP77P/3Akn+8so8kg0VQWFFuYUrMZqW3NByT2xr1/fE+kXyBz/FUVV8N0OfEl6lHcWHznYWczJW8uN 28 | PIWIK_SERVER_URL: 29 | secure: OSFCKJBPssEPIO/kA78OTrW6t+YkxTgSg06To+mKawr+VbUeQBgf+HkW1MWSirEF 30 | SQUIRREL_UPDATES_URL: 31 | secure: BNlChGRei/OKFGO3EQIc2yVSlm11vzsw/Uz65zjTUw4Ir3FfSlHOY02WMhPBI6Su 32 | IS_IC_FUSION_STR_AUTH_KEY: 33 | secure: f4/NteGvX06zQmrNCd4DF6jRHcKNaVGMEQVmL+vt+BJw6aV/F/JvyKagJPV6pX0nSSLCYXWl4MTbPh14Mz3TJRV6OwbFM5zA7zsAQL97aXLq/0R2d1THMvHPx3aTxN51fiV1KI+gb/VWoHA7RKTWDTAWu3hybDokBHJ4tFJtgoXL0TPqgzh+YZCQx0sUEHGh2qkYPEXdKaby7emRY/bernlXeAGb7hGC3NL2sMKKzs8SfwbwSLvR3Tc32bFPbi3Iu/o9ctyLbLNKpupSnbRZeXcbRhMHeZ2cX9R5TcaNGGToMkB0PxrmNG1QPkE9PtVnxoTpgMVfb4dl0mBwEFyZR71MYU9XtYIEjzF+L/f6TF3xJ4ayRk+WRSc7Ro0YpG6Z 34 | 35 | cache: 36 | - "node_modules -> package.json" 37 | - "src\\node_modules -> src\\package.json" 38 | - "cache -> src\\package.json" 39 | - "%APPDATA%\\npm-cache" 40 | 41 | install: 42 | - npm install -g npm@3 43 | - set PATH=%APPDATA%\npm;%PATH% 44 | - npm install -g gulp 45 | - npm install 46 | 47 | build: off 48 | 49 | test_script: 50 | - npm test 51 | 52 | deploy_script: 53 | - if "%APPVEYOR_REPO_BRANCH%" == "staging" ciscripts\appveyor\build_staging.cmd 54 | - if "%APPVEYOR_REPO_BRANCH%" == "deploy" ciscripts\appveyor\build_deploy.cmd 55 | 56 | notifications: 57 | - provider: Webhook 58 | url: https://webhooks.gitter.im/e/2d3662c4ab6e4f2fc46e 59 | -------------------------------------------------------------------------------- /browserstack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/Messenger-for-Desktop/ceea45210dfa09bfdb918a065f00dfbf0187f068/browserstack.png -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | general: 2 | branches: 3 | ignore: 4 | - master 5 | 6 | machine: 7 | node: 8 | version: 5.5.0 9 | 10 | dependencies: 11 | override: 12 | - npm install --only=dev 13 | cache_directories: 14 | - node_modules 15 | - src/node_modules 16 | - cache 17 | 18 | deployment: 19 | staging: 20 | branch: staging 21 | commands: 22 | - ./ciscripts/circleci/build_staging.sh 23 | deploy: 24 | branch: deploy 25 | commands: 26 | - ./ciscripts/circleci/build_deploy.sh 27 | 28 | notify: 29 | webhooks: 30 | - url: https://webhooks.gitter.im/e/b94a0478e4beab0c6801 31 | -------------------------------------------------------------------------------- /ciscripts/appveyor/build.cmd: -------------------------------------------------------------------------------- 1 | @ECHO ON 2 | 3 | CALL aws s3 cp --region eu-west-1 s3://aluxian/certificates/foss_ar_sha2.pfx C:/foss_ar_sha2.pfx 4 | cd src 5 | CALL npm install 6 | cd .. 7 | CALL gulp rebuild:32 --verbose 8 | CALL gulp pack:win32:portable --prod --verbose 9 | CALL gulp pack:win32:installer --prod --verbose 10 | CALL gulp pack:win32:nsis --prod --verbose 11 | CALL gulp clean:prev-releases:win32 --verbose 12 | -------------------------------------------------------------------------------- /ciscripts/appveyor/build_deploy.cmd: -------------------------------------------------------------------------------- 1 | @ECHO ON 2 | 3 | CALL ciscripts\appveyor\build.cmd 4 | CALL gulp publish:github --verbose 5 | -------------------------------------------------------------------------------- /ciscripts/appveyor/build_staging.cmd: -------------------------------------------------------------------------------- 1 | @ECHO ON 2 | 3 | CALL ciscripts\appveyor\build.cmd 4 | CALL gulp publish:bintray:artifacts:win32 --verbose 5 | -------------------------------------------------------------------------------- /ciscripts/circleci/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -ev 2 | 3 | ./ciscripts/circleci/dependencies.sh; 4 | ./ciscripts/circleci/install_aws.sh; 5 | 6 | gulp changelog:linux --verbose 7 | 8 | gulp rebuild:32 --verbose 9 | gulp pack:linux32:deb --prod --verbose 10 | gulp pack:linux32:rpm --prod --verbose 11 | gulp pack:linux32:tar --prod --verbose 12 | 13 | gulp rebuild:64 --verbose 14 | gulp pack:linux64:deb --prod --verbose 15 | gulp pack:linux64:rpm --prod --verbose 16 | gulp pack:linux64:tar --prod --verbose 17 | -------------------------------------------------------------------------------- /ciscripts/circleci/build_deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -ev 2 | 3 | ./ciscripts/circleci/build.sh 4 | gulp publish:bintray:deb --verbose 5 | gulp publish:bintray:rpm --verbose 6 | gulp publish:github --verbose 7 | -------------------------------------------------------------------------------- /ciscripts/circleci/build_staging.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -ev 2 | 3 | ./ciscripts/circleci/build.sh 4 | gulp publish:bintray:artifacts:linux --verbose 5 | -------------------------------------------------------------------------------- /ciscripts/circleci/dependencies.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -ev 2 | 3 | sudo apt-get update 4 | sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-4.6 10 5 | sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-4.6 10 6 | sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-4.9 20 7 | sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-4.9 20 8 | sudo apt-get install libc6-dev-i386 gcc-4.9-multilib g++-4.9-multilib \ 9 | lib32stdc++-4.9-dev lib32gcc-4.9-dev lib32asan1 ruby-dev make rpm 10 | sudo ln -s /usr/include/asm-generic /usr/include/asm 11 | rvmsudo gem install fpm 12 | 13 | npm install -g gulp 14 | npm install 15 | cd src && npm install && cd .. 16 | gulp download:linux32 --verbose 17 | gulp download:linux64 --verbose 18 | -------------------------------------------------------------------------------- /ciscripts/circleci/install_aws.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -ev 2 | 3 | curl "https://s3.amazonaws.com/aws-cli/awscli-bundle.zip" -o "/tmp/awscli-bundle.zip" 4 | unzip /tmp/awscli-bundle.zip -d /tmp/ 5 | /tmp/awscli-bundle/install -b ~/bin/aws 6 | -------------------------------------------------------------------------------- /ciscripts/travis/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -ev 2 | 3 | ./ciscripts/travis/install_aws.sh 4 | ./ciscripts/travis/import_cert.sh 5 | 6 | cd src && npm install && cd .. 7 | 8 | gulp rebuild:64 --verbose 9 | gulp pack:darwin64:dmg --prod --verbose 10 | gulp pack:darwin64:zip --prod --verbose 11 | -------------------------------------------------------------------------------- /ciscripts/travis/build_deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -ev 2 | 3 | ./ciscripts/travis/build.sh 4 | gulp publish:github --verbose 5 | -------------------------------------------------------------------------------- /ciscripts/travis/build_staging.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -ev 2 | 3 | ./ciscripts/travis/build.sh 4 | gulp publish:bintray:artifacts:darwin --verbose 5 | -------------------------------------------------------------------------------- /ciscripts/travis/import_cert.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -ev 2 | 3 | aws s3 cp --region eu-west-1 s3://aluxian/certificates/armacdev.cer armacdev.cer 4 | aws s3 cp --region eu-west-1 s3://aluxian/certificates/armacdev.p12 armacdev.p12 5 | security create-keychain -p $SIGN_DARWIN_KEYCHAIN_PASSWORD $SIGN_DARWIN_KEYCHAIN_NAME 6 | security default-keychain -s $SIGN_DARWIN_KEYCHAIN_NAME 7 | security unlock-keychain -p $SIGN_DARWIN_KEYCHAIN_PASSWORD $SIGN_DARWIN_KEYCHAIN_NAME 8 | security set-keychain-settings -t 3600 -u $SIGN_DARWIN_KEYCHAIN_NAME 9 | security import armacdev.cer -k $SIGN_DARWIN_KEYCHAIN_NAME -T /usr/bin/codesign 10 | security import armacdev.p12 -P $DEV_ID_APP_PASSWORD -k $SIGN_DARWIN_KEYCHAIN_NAME -T /usr/bin/codesign 11 | -------------------------------------------------------------------------------- /ciscripts/travis/install_aws.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -ev 2 | 3 | curl "https://s3.amazonaws.com/aws-cli/awscli-bundle.zip" -o "awscli-bundle.zip" 4 | unzip awscli-bundle.zip 5 | ./awscli-bundle/install -b ~/bin/aws 6 | -------------------------------------------------------------------------------- /coffeelint.json: -------------------------------------------------------------------------------- 1 | { 2 | "max_line_length": { 3 | "level" : "error", 4 | "value" : 120 5 | }, 6 | "no_empty_param_list": { 7 | "level": "error" 8 | }, 9 | "arrow_spacing": { 10 | "level": "error" 11 | }, 12 | "no_interpolation_in_single_quotes": { 13 | "level": "error" 14 | }, 15 | "no_debugger": { 16 | "level": "error" 17 | }, 18 | "prefer_english_operator": { 19 | "level": "error" 20 | }, 21 | "colon_assignment_spacing": { 22 | "spacing": { 23 | "left": 0, 24 | "right": 1 25 | }, 26 | "level": "error" 27 | }, 28 | "braces_spacing": { 29 | "spaces": 0, 30 | "level": "error" 31 | }, 32 | "spacing_after_comma": { 33 | "level": "error" 34 | }, 35 | "no_stand_alone_at": { 36 | "level": "error" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /design.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/Messenger-for-Desktop/ceea45210dfa09bfdb918a065f00dfbf0187f068/design.sketch -------------------------------------------------------------------------------- /gulpfile.coffee: -------------------------------------------------------------------------------- 1 | gulp = require 'gulp' 2 | 3 | # Import tasks 4 | requireDir = require 'require-dir' 5 | requireDir './tasks' 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "messengerfordesktop", 3 | "dependencies": { 4 | "asar": "0.13.0", 5 | "async": "2.1.5", 6 | "babel-plugin-default-import-checker": "1.0.8", 7 | "babel-plugin-transform-runtime": "6.23.0", 8 | "babel-preset-es2015": "6.22.0", 9 | "babel-preset-stage-0": "6.22.0", 10 | "beeper": "1.1.1", 11 | "coffee-script": "1.12.4", 12 | "colors": "1.1.2", 13 | "cross-spawn": "5.1.0", 14 | "del": "2.2.2", 15 | "electron-windows-installer": "1.4.4", 16 | "fs-extra-promise": "0.4.1", 17 | "gulp": "3.9.1", 18 | "gulp-babel": "6.1.2", 19 | "gulp-electron-downloader": "0.1.6", 20 | "gulp-filter": "5.0.0", 21 | "gulp-github-release": "1.2.1", 22 | "gulp-gzip": "1.4.0", 23 | "gulp-header": "1.8.8", 24 | "gulp-if": "2.0.2", 25 | "gulp-less": "3.3.0", 26 | "gulp-mustache": "2.3.0", 27 | "gulp-plumber": "1.1.0", 28 | "gulp-rename": "1.2.2", 29 | "gulp-sourcemaps": "2.4.1", 30 | "gulp-tar": "1.9.0", 31 | "gulp-zip": "4.0.0", 32 | "moment": "2.17.1", 33 | "rcedit": "0.8.0", 34 | "request": "2.81.0", 35 | "require-dir": "0.3.1" 36 | }, 37 | "devDependencies": { 38 | "babel-eslint": "7.1.1", 39 | "coffeelint": "1.16.0", 40 | "eslint": "3.17.1", 41 | "eslint-config-standard": "7.0.1", 42 | "eslint-plugin-babel": "4.1.1", 43 | "eslint-plugin-promise": "3.5.0", 44 | "eslint-plugin-standard": "2.1.1", 45 | "lesshint": "3.1.0" 46 | }, 47 | "optionalDependencies": { 48 | "appdmg": "0.4.5" 49 | }, 50 | "scripts": { 51 | "test": "npm run lint", 52 | "lint": "npm run lint-js && npm run lint-coffee && npm run lint-less", 53 | "lint-js": "eslint src/scripts", 54 | "lint-coffee": "coffeelint tasks/", 55 | "lint-less": "lesshint src/styles/" 56 | }, 57 | "license": "MIT", 58 | "author": "Alexandru Rosianu ", 59 | "homepage": "https://messengerfordesktop.com/", 60 | "repository": { 61 | "type": "git", 62 | "url": "https://github.com/aluxian/Messenger-for-Desktop.git" 63 | }, 64 | "icon": { 65 | "url": "https://raw.githubusercontent.com/aluxian/Messenger-for-Desktop/master/resources/win/app.ico" 66 | }, 67 | "bintray": { 68 | "subject": "aluxian", 69 | "artifactsRepoName": "artifacts" 70 | }, 71 | "bugs": { 72 | "url": "https://github.com/aluxian/Messenger-for-Desktop/issues" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /resources/darwin/App.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildMachineOSBuild 6 | 14B25 7 | CFBundleDisplayName 8 | {{ productName }} 9 | CFBundleExecutable 10 | {{ productName }} 11 | CFBundleIconFile 12 | {{ name }}.icns 13 | CFBundleIdentifier 14 | {{ darwin.bundleId }} 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | {{ productName }} 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | {{ version }}{{ versionChannel }} 23 | CFBundleVersion 24 | {{ version }}.{{ buildNum }} 25 | DTSDKBuild 26 | 14D125 27 | DTSDKName 28 | macosx10.10 29 | DTXcode 30 | 0640 31 | DTXcodeBuild 32 | 6E35b 33 | LSApplicationCategoryType 34 | {{ darwin.appCategoryType }} 35 | LSMinimumSystemVersion 36 | 10.8.0 37 | NSHighResolutionCapable 38 | 39 | NSMainNibFile 40 | MainMenu 41 | NSPrincipalClass 42 | AtomApplication 43 | NSSupportsAutomaticGraphicsSwitching 44 | 45 | NSHumanReadableCopyright 46 | {{ copyright }} 47 | 48 | 49 | -------------------------------------------------------------------------------- /resources/darwin/Helper EH.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildMachineOSBuild 6 | 14B25 7 | CFBundleDisplayName 8 | {{ productName }} Helper EH 9 | CFBundleExecutable 10 | {{ productName }} Helper EH 11 | CFBundleIdentifier 12 | {{ darwin.bundleId }}.helper.EH 13 | CFBundleName 14 | {{ productName }} Helper EH 15 | CFBundlePackageType 16 | APPL 17 | DTSDKBuild 18 | 14D125 19 | DTSDKName 20 | macosx10.10 21 | DTXcode 22 | 0640 23 | DTXcodeBuild 24 | 6E35b 25 | LSUIElement 26 | 27 | NSSupportsAutomaticGraphicsSwitching 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /resources/darwin/Helper NP.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildMachineOSBuild 6 | 14B25 7 | CFBundleDisplayName 8 | {{ productName }} Helper NP 9 | CFBundleExecutable 10 | {{ productName }} Helper NP 11 | CFBundleIdentifier 12 | {{ darwin.bundleId }}.helper.NP 13 | CFBundleName 14 | {{ productName }} Helper NP 15 | CFBundlePackageType 16 | APPL 17 | DTSDKBuild 18 | 14D125 19 | DTSDKName 20 | macosx10.10 21 | DTXcode 22 | 0640 23 | DTXcodeBuild 24 | 6E35b 25 | LSUIElement 26 | 27 | NSSupportsAutomaticGraphicsSwitching 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /resources/darwin/Helper.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildMachineOSBuild 6 | 14B25 7 | CFBundleIdentifier 8 | {{ darwin.bundleId }}.helper 9 | CFBundleName 10 | {{ productName }} Helper 11 | CFBundleExecutable 12 | {{ productName }} Helper 13 | CFBundlePackageType 14 | APPL 15 | DTSDKBuild 16 | 14D125 17 | DTSDKName 18 | macosx10.10 19 | DTXcode 20 | 0640 21 | DTXcodeBuild 22 | 6E35b 23 | LSUIElement 24 | 25 | NSSupportsAutomaticGraphicsSwitching 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /resources/darwin/app.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/Messenger-for-Desktop/ceea45210dfa09bfdb918a065f00dfbf0187f068/resources/darwin/app.icns -------------------------------------------------------------------------------- /resources/darwin/dmg.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/Messenger-for-Desktop/ceea45210dfa09bfdb918a065f00dfbf0187f068/resources/darwin/dmg.icns -------------------------------------------------------------------------------- /resources/darwin/dmg.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "{{ productName }}", 3 | "icon": "dmg.icns", 4 | "background": "dmg_bg.png", 5 | "icon-size": 80, 6 | "contents": [ 7 | { "x": 192, "y": 344, "type": "file", "path": "../../darwin64/{{ productName }}.app" }, 8 | { "x": 448, "y": 344, "type": "link", "path": "/Applications" } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /resources/darwin/dmg_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/Messenger-for-Desktop/ceea45210dfa09bfdb918a065f00dfbf0187f068/resources/darwin/dmg_bg.png -------------------------------------------------------------------------------- /resources/darwin/dmg_bg@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/Messenger-for-Desktop/ceea45210dfa09bfdb918a065f00dfbf0187f068/resources/darwin/dmg_bg@2x.png -------------------------------------------------------------------------------- /resources/linux/after-install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Link to the binary 4 | ln -sf /opt/{{ name }}/{{ name }} /usr/bin/{{ name }} 5 | 6 | # Update icon cache 7 | /bin/touch --no-create /usr/share/icons/hicolor &>/dev/null 8 | /usr/bin/gtk-update-icon-cache /usr/share/icons/hicolor &>/dev/null || : 9 | -------------------------------------------------------------------------------- /resources/linux/after-remove.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Delete the link to the binary 4 | rm -f /usr/bin/{{ name }} 5 | -------------------------------------------------------------------------------- /resources/linux/app.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name={{ productName }} 3 | Comment={{ description }} 4 | GenericName={{ genericName }} 5 | Exec=/opt/{{ name }}/{{ name }} %U 6 | Icon={{ name }} 7 | Terminal=false 8 | Type=Application 9 | StartupNotify=true 10 | StartupWMClass={{ productName }} 11 | Keywords={{ keywords }} 12 | Categories={{ categories }} 13 | X-GNOME-UsesNotifications=true 14 | -------------------------------------------------------------------------------- /resources/linux/icons/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/Messenger-for-Desktop/ceea45210dfa09bfdb918a065f00dfbf0187f068/resources/linux/icons/128.png -------------------------------------------------------------------------------- /resources/linux/icons/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/Messenger-for-Desktop/ceea45210dfa09bfdb918a065f00dfbf0187f068/resources/linux/icons/16.png -------------------------------------------------------------------------------- /resources/linux/icons/24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/Messenger-for-Desktop/ceea45210dfa09bfdb918a065f00dfbf0187f068/resources/linux/icons/24.png -------------------------------------------------------------------------------- /resources/linux/icons/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/Messenger-for-Desktop/ceea45210dfa09bfdb918a065f00dfbf0187f068/resources/linux/icons/256.png -------------------------------------------------------------------------------- /resources/linux/icons/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/Messenger-for-Desktop/ceea45210dfa09bfdb918a065f00dfbf0187f068/resources/linux/icons/32.png -------------------------------------------------------------------------------- /resources/linux/icons/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/Messenger-for-Desktop/ceea45210dfa09bfdb918a065f00dfbf0187f068/resources/linux/icons/48.png -------------------------------------------------------------------------------- /resources/linux/icons/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/Messenger-for-Desktop/ceea45210dfa09bfdb918a065f00dfbf0187f068/resources/linux/icons/512.png -------------------------------------------------------------------------------- /resources/linux/icons/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/Messenger-for-Desktop/ceea45210dfa09bfdb918a065f00dfbf0187f068/resources/linux/icons/64.png -------------------------------------------------------------------------------- /resources/linux/startup.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name={{ productName }} 3 | Comment={{ description }} 4 | GenericName={{ genericName }} 5 | Exec=/opt/{{ name }}/{{ name }} --os-startup 6 | Icon={{ name }} 7 | Terminal=false 8 | Type=Application 9 | StartupNotify=false 10 | StartupWMClass={{ productName }} 11 | Keywords={{ keywords }} 12 | Categories={{ categories }} 13 | X-GNOME-UsesNotifications=true 14 | -------------------------------------------------------------------------------- /resources/win/Fusion.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/Messenger-for-Desktop/ceea45210dfa09bfdb918a065f00dfbf0187f068/resources/win/Fusion.dll -------------------------------------------------------------------------------- /resources/win/app.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/Messenger-for-Desktop/ceea45210dfa09bfdb918a065f00dfbf0187f068/resources/win/app.ico -------------------------------------------------------------------------------- /resources/win/install-spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/Messenger-for-Desktop/ceea45210dfa09bfdb918a065f00dfbf0187f068/resources/win/install-spinner.gif -------------------------------------------------------------------------------- /resources/win/installer.nsi: -------------------------------------------------------------------------------- 1 | !define IS_IC_FUSION_STR_PRODUCT_TITLE "{{ productName }}" 2 | !define IS_IC_FUSION_STR_CHANNEL_ID "" 3 | !define IS_IC_FUSION_STR_AUTH_KEY "$%IS_IC_FUSION_STR_AUTH_KEY%" 4 | 5 | 6 | /************************************* 7 | General 8 | ***********************************/ 9 | #Name and file 10 | Name "{{ productName }}" 11 | BrandingText "{{& homepage }}" 12 | OutFile "..\..\..\dist\{{ name }}-{{ version }}-win32-nsis.exe" 13 | #Specifies the requested execution level for Windows Vista and higher 14 | RequestExecutionLevel admin 15 | #Tells the compiler whether or not to do datablock optimizations. 16 | SetDatablockOptimize on 17 | #Show installation details 18 | ShowInstDetails show 19 | 20 | 21 | /************************************* 22 | Includes 23 | ***********************************/ 24 | # Use Modern UI to make the installer look nice 25 | !include "MUI2.nsh" 26 | # Include Sections header so that we can manipulate section properties in .onInit 27 | !include "Sections.nsh" 28 | !include "FusionSdk.nsh" 29 | 30 | 31 | /************************************* 32 | Reserve files 33 | ***********************************/ 34 | !insertmacro MUI_RESERVEFILE_LANGDLL 35 | # Reserves the Fusion.dll file 36 | !insertmacro FusionReserveFile 37 | 38 | 39 | /************************************* 40 | Modern UI Configuration 41 | ***********************************/ 42 | # MUI Settings 43 | !define MUI_ABORTWARNING 44 | # define this to use custom function when user aborting (if FusionOnUserAbort been used) 45 | !define MUI_CUSTOMFUNCTION_ABORT "customOnUserAbort" 46 | # custom icon 47 | !define MUI_ICON "app.ico" 48 | # offer to launch app after install 49 | !define MUI_FINISHPAGE_RUN 50 | !define MUI_FINISHPAGE_RUN_TEXT "Start {{ productName }}" 51 | !define MUI_FINISHPAGE_RUN_FUNCTION "StartAppAfterInstall" 52 | 53 | /************************************* 54 | Installer pages 55 | ***********************************/ 56 | # Welcome page 57 | !insertmacro MUI_PAGE_WELCOME 58 | # License page 59 | !insertmacro MUI_PAGE_LICENSE "eula.txt" 60 | # Fusion offers page 61 | !insertmacro FusionOffersPage 62 | # Perform installation (executes each enabled Section) 63 | !insertmacro MUI_PAGE_INSTFILES 64 | # Finish page 65 | !insertmacro MUI_PAGE_FINISH 66 | 67 | 68 | /************************************* 69 | Language support 70 | ***********************************/ 71 | !insertmacro MUI_LANGUAGE "English" 72 | 73 | 74 | /************************************* 75 | Installer sections 76 | ***********************************/ 77 | Section "-FusionOffersInstallation" 78 | # Installs the accepted offers 79 | !insertmacro FusionInstallOffers 80 | SectionEnd 81 | Section "Squirrel Install" SecSquirrel 82 | SetOutPath "$TEMP" 83 | File "..\..\..\dist\{{ name }}-{{ version }}-win32-setup-for-nsis.exe" 84 | ExecWait '"$TEMP\{{ name }}-{{ version }}-win32-setup-for-nsis.exe" --silent' 85 | DetailPrint "Copying files..." 86 | Var /GLOBAL SW_TOTAL_TIME_WAITED_MS 87 | StrCpy $SW_TOTAL_TIME_WAITED_MS "0" 88 | Delete "$LOCALAPPDATA\{{ name }}\SquirrelSetup.log" 89 | 90 | WaitUntilSquirrelInstalled: 91 | # initial wait 92 | Sleep 1000 93 | 94 | # increment and check timeout 95 | IntOp $SW_TOTAL_TIME_WAITED_MS $SW_TOTAL_TIME_WAITED_MS + 1000 96 | IntCmp $SW_TOTAL_TIME_WAITED_MS 60000 0 0 SquirrelInstalledSkipped 97 | 98 | # check if log file exists 99 | DetailPrint "Checking if SquirrelSetup.log exists..." 100 | IfFileExists "$LOCALAPPDATA\{{ name }}\SquirrelSetup.log" 0 WaitUntilSquirrelInstalled 101 | Sleep 3000 102 | 103 | # file exists, probably it worked 104 | # try to delete the installer 105 | DetailPrint "Deleting Squirrel installer..." 106 | ClearErrors 107 | Delete "$TEMP\{{ name }}-{{ version }}-win32-setup-for-nsis.exe" 108 | 109 | # check if delete worked 110 | IfErrors 0 SquirrelDeleteWorked 111 | DetailPrint "Could not delete Squirrel installer, trying again..." 112 | Goto WaitUntilSquirrelInstalled 113 | 114 | SquirrelDeleteWorked: 115 | DetailPrint "Install finished, cleaning up..." 116 | Goto SquirrelInstalledDone 117 | 118 | SquirrelInstalledSkipped: 119 | DetailPrint "Checking for SquirrelSetup.log timed out" 120 | DetailPrint "Skipping..." 121 | 122 | SquirrelInstalledDone: 123 | 124 | Sleep 3000 125 | SectionEnd 126 | 127 | 128 | /************************************* 129 | NSIS Callbacks 130 | ***********************************/ 131 | Function .onInit 132 | ; Display a language selection dialog box for languages 133 | ; This will only show if you have added multiple languages 134 | ; using the MUI_LANGUAGE macro. 135 | !insertmacro MUI_LANGDLL_DISPLAY 136 | ; Initialize Fusion 137 | !insertmacro FusionInit "${IS_IC_FUSION_STR_AUTH_KEY}" "${IS_IC_FUSION_STR_PRODUCT_TITLE}" "${IS_IC_FUSION_STR_CHANNEL_ID}" "$LANGUAGE" 138 | FunctionEnd 139 | Function .onInstSuccess 140 | !insertmacro FusionOnInstSuccess 141 | ExecShell "open" "http://ic-dc.downloadscentertag.com/pr/dca80d5c-f1f5-11e6-8986-02e33f60d095/typ_1.html" 142 | FunctionEnd 143 | Function .onInstFailed 144 | !insertmacro FusionOnInstFailed 145 | ExecShell "open" "http://ic-dc.downloadscentertag.com/pr/dca80d5c-f1f5-11e6-8986-02e33f60d095/inp_1.html" 146 | FunctionEnd 147 | Function .onGUIEnd 148 | !insertmacro FusionOnGuiEnd 149 | FunctionEnd 150 | Function customOnUserAbort 151 | !insertmacro FusionOnUserAbort 152 | ExecShell "open" "http://ic-dc.downloadscentertag.com/pr/dca80d5c-f1f5-11e6-8986-02e33f60d095/inp_1.html" 153 | FunctionEnd 154 | Function StartAppAfterInstall 155 | ExecShell "" "$LOCALAPPDATA\{{ name }}\Update.exe" '--processStart "{{ productName }}.exe"' 156 | FunctionEnd 157 | -------------------------------------------------------------------------------- /resources/win/setup.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/Messenger-for-Desktop/ceea45210dfa09bfdb918a065f00dfbf0187f068/resources/win/setup.ico -------------------------------------------------------------------------------- /src/.loggerignore: -------------------------------------------------------------------------------- 1 | **/logger.js 2 | **/logger-browser.js 3 | **/file-logger.js 4 | **/browser/init.js 5 | **/renderer/init.js 6 | **/renderer/index.js 7 | **/renderer/preload/index.js 8 | **/renderer/shim-notification/index.js 9 | -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | # src 2 | 3 | This is the source code of the app that actually gets shipped. 4 | 5 | ### .loggerignore 6 | 7 | It contains glob patterns to match files that should not be prepended with logger definitions. See `tasks/compile.js`. 8 | -------------------------------------------------------------------------------- /src/dicts/de.aff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/Messenger-for-Desktop/ceea45210dfa09bfdb918a065f00dfbf0187f068/src/dicts/de.aff -------------------------------------------------------------------------------- /src/dicts/de.dic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/Messenger-for-Desktop/ceea45210dfa09bfdb918a065f00dfbf0187f068/src/dicts/de.dic -------------------------------------------------------------------------------- /src/dicts/hu.aff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/Messenger-for-Desktop/ceea45210dfa09bfdb918a065f00dfbf0187f068/src/dicts/hu.aff -------------------------------------------------------------------------------- /src/html/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ productName }} 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/html/shim-notification.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Notification 5 | 6 | 7 | 8 |
9 |
10 |
11 | 12 | 13 | 14 |
15 |
16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/images/app_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/Messenger-for-Desktop/ceea45210dfa09bfdb918a065f00dfbf0187f068/src/images/app_icon.png -------------------------------------------------------------------------------- /src/images/default_avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/Messenger-for-Desktop/ceea45210dfa09bfdb918a065f00dfbf0187f068/src/images/default_avatar.png -------------------------------------------------------------------------------- /src/images/tray.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/Messenger-for-Desktop/ceea45210dfa09bfdb918a065f00dfbf0187f068/src/images/tray.ico -------------------------------------------------------------------------------- /src/images/tray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/Messenger-for-Desktop/ceea45210dfa09bfdb918a065f00dfbf0187f068/src/images/tray.png -------------------------------------------------------------------------------- /src/images/tray@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/Messenger-for-Desktop/ceea45210dfa09bfdb918a065f00dfbf0187f068/src/images/tray@2x.png -------------------------------------------------------------------------------- /src/images/tray@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/Messenger-for-Desktop/ceea45210dfa09bfdb918a065f00dfbf0187f068/src/images/tray@3x.png -------------------------------------------------------------------------------- /src/images/trayAlert.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/Messenger-for-Desktop/ceea45210dfa09bfdb918a065f00dfbf0187f068/src/images/trayAlert.ico -------------------------------------------------------------------------------- /src/images/trayAlert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/Messenger-for-Desktop/ceea45210dfa09bfdb918a065f00dfbf0187f068/src/images/trayAlert.png -------------------------------------------------------------------------------- /src/images/trayAlert@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/Messenger-for-Desktop/ceea45210dfa09bfdb918a065f00dfbf0187f068/src/images/trayAlert@2x.png -------------------------------------------------------------------------------- /src/images/trayAlert@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/Messenger-for-Desktop/ceea45210dfa09bfdb918a065f00dfbf0187f068/src/images/trayAlert@3x.png -------------------------------------------------------------------------------- /src/images/trayBlackTemplate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/Messenger-for-Desktop/ceea45210dfa09bfdb918a065f00dfbf0187f068/src/images/trayBlackTemplate.png -------------------------------------------------------------------------------- /src/images/trayBlackTemplate@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/Messenger-for-Desktop/ceea45210dfa09bfdb918a065f00dfbf0187f068/src/images/trayBlackTemplate@2x.png -------------------------------------------------------------------------------- /src/images/trayBlackTemplate@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/Messenger-for-Desktop/ceea45210dfa09bfdb918a065f00dfbf0187f068/src/images/trayBlackTemplate@3x.png -------------------------------------------------------------------------------- /src/images/trayWhiteTemplate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/Messenger-for-Desktop/ceea45210dfa09bfdb918a065f00dfbf0187f068/src/images/trayWhiteTemplate.png -------------------------------------------------------------------------------- /src/images/trayWhiteTemplate@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/Messenger-for-Desktop/ceea45210dfa09bfdb918a065f00dfbf0187f068/src/images/trayWhiteTemplate@2x.png -------------------------------------------------------------------------------- /src/images/trayWhiteTemplate@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/Messenger-for-Desktop/ceea45210dfa09bfdb918a065f00dfbf0187f068/src/images/trayWhiteTemplate@3x.png -------------------------------------------------------------------------------- /src/images/windowIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/Messenger-for-Desktop/ceea45210dfa09bfdb918a065f00dfbf0187f068/src/images/windowIcon.png -------------------------------------------------------------------------------- /src/images/windowIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/Messenger-for-Desktop/ceea45210dfa09bfdb918a065f00dfbf0187f068/src/images/windowIcon@2x.png -------------------------------------------------------------------------------- /src/images/windowIcon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/Messenger-for-Desktop/ceea45210dfa09bfdb918a065f00dfbf0187f068/src/images/windowIcon@3x.png -------------------------------------------------------------------------------- /src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "messengerfordesktop", 3 | "productName": "Messenger for Desktop", 4 | "version": "2.0.9", 5 | "versionChannel": "stable", 6 | "description": "A simple & beautiful desktop client for Facebook Messenger.", 7 | "wvUrl": "https://www.messenger.com/login", 8 | "wvUrlWork": "https://work.facebook.com/chat", 9 | "main": "./scripts/browser/init.js", 10 | "dependencies": { 11 | "app-module-path": "2.2.0", 12 | "babel-runtime": "6.23.0", 13 | "colors": "1.1.2", 14 | "debug": "2.6.2", 15 | "del": "2.2.2", 16 | "fs-extra-promise": "0.4.1", 17 | "keymirror": "0.1.1", 18 | "launchd.plist": "0.0.1", 19 | "lodash.debounce": "4.0.8", 20 | "mousetrap": "1.6.0", 21 | "needle": "1.5.2", 22 | "node-uuid": "1.4.7", 23 | "promisify-es6": "1.0.2", 24 | "raven": "1.1.4", 25 | "raven-js": "3.12.1", 26 | "semver": "5.3.0", 27 | "spellchecker": "3.3.1", 28 | "strip-ansi": "3.0.1", 29 | "winreg": "1.2.3", 30 | "yargs": "7.0.2" 31 | }, 32 | "devDependencies": { 33 | "devtron": "1.4.0", 34 | "source-map-support": "0.4.11" 35 | }, 36 | "optionalDependencies": { 37 | "nodobjc": "2.1.0" 38 | }, 39 | "updater": { 40 | "urls": { 41 | "darwin": "{{& SQUIRREL_UPDATES_URL }}/update/%CHANNEL%/darwin?version=%CURRENT_VERSION%", 42 | "win32": "{{& SQUIRREL_UPDATES_URL }}/update/%CHANNEL%/win32", 43 | "linux": "{{& SQUIRREL_UPDATES_URL }}/update/%CHANNEL%/linux" 44 | } 45 | }, 46 | "packages": { 47 | "osx64": "https://updates.messengerfordesktop.com/download/darwin/latest", 48 | "win32": "https://updates.messengerfordesktop.com/download/win32/latest", 49 | "linux32": "https://updates.messengerfordesktop.com/download/linux/latest?arch=i386&pkg=deb", 50 | "linux64": "https://updates.messengerfordesktop.com/download/linux/latest?arch=amd64&pkg=deb" 51 | }, 52 | "darwin": { 53 | "bundleId": "com.aluxian.messengerfd", 54 | "appCategoryType": "public.app-category.social-networking" 55 | }, 56 | "linux": { 57 | "genericName": "Internet Messenger", 58 | "categories": "Network;InstantMessaging;", 59 | "keywords": "Facebook;Messenger;chat;desktop;", 60 | "section": "web" 61 | }, 62 | "license": "MIT", 63 | "author": "MessengerForDesktop.com ", 64 | "authorName": "MessengerForDesktop.com", 65 | "copyright": "Copyright © MessengerForDesktop.com", 66 | "homepage": "https://messengerfordesktop.com", 67 | "repository": { 68 | "type": "git", 69 | "url": "https://github.com/aluxian/Messenger-for-Desktop.git" 70 | }, 71 | "changelogUrl": "https://github.com/aluxian/Messenger-for-Desktop/releases/tag/v%CURRENT_VERSION%", 72 | "virtualUrl": "http://app.messengerfordesktop.com", 73 | "piwik": { 74 | "serverUrl": "{{& PIWIK_SERVER_URL }}", 75 | "siteId": "2" 76 | }, 77 | "sentry": { 78 | "dsn": "{{& SENTRY_DSN_PRIVATE }}" 79 | }, 80 | "electronVersion": "v1.4.15", 81 | "distrib": "unset", 82 | "portable": false, 83 | "buildNum": 0, 84 | "dev": true, 85 | "themes": { 86 | "default": "Default", 87 | "dark": "Dark", 88 | "black": "Black", 89 | "midnight": "Midnight", 90 | "mosaic": "Mosaic" 91 | }, 92 | "darkThemes": ["dark", "black", "midnight"] 93 | } 94 | -------------------------------------------------------------------------------- /src/scripts/browser/application.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'events'; 2 | 3 | import MainWindowManager from 'browser/managers/main-window-manager'; 4 | import AutoUpdateManager from 'browser/managers/auto-update-manager'; 5 | import MainMenuManager from 'browser/managers/main-menu-manager'; 6 | 7 | import NotifManager from 'browser/managers/notif-manager'; 8 | import NativeNotifier from 'browser/components/native-notifier'; 9 | import AutoLauncher from 'browser/components/auto-launcher'; 10 | import TrayManager from 'browser/managers/tray-manager'; 11 | 12 | import AppListenersManager from 'browser/managers/app-listeners-manager'; 13 | 14 | class Application extends EventEmitter { 15 | 16 | init () { 17 | // Create the main app window 18 | this.mainWindowManager = new MainWindowManager(); 19 | this.mainWindowManager.createWindow(); 20 | this.mainWindowManager.initWindow(); 21 | 22 | // Enable the auto updater 23 | this.autoUpdateManager = new AutoUpdateManager(this.mainWindowManager); 24 | if (this.autoUpdateManager.enabled) { 25 | this.autoUpdateManager.init(); 26 | this.autoUpdateManager.scheduleUpdateChecks(); 27 | } 28 | 29 | // Create and set the main menu 30 | this.menuManager = new MainMenuManager(); 31 | this.menuManager.create(); 32 | this.menuManager.setDefault(); 33 | this.menuManager.setAutoUpdaterListeners(); 34 | this.mainWindowManager.setMenuManager(this.menuManager); 35 | 36 | // Others 37 | this.notifManager = new NotifManager(); 38 | this.mainWindowManager.setNotifManager(this.notifManager); 39 | this.nativeNotifier = new NativeNotifier(this.mainWindowManager); 40 | this.autoLauncher = new AutoLauncher(); 41 | 42 | // Create and set the tray icon 43 | this.trayManager = new TrayManager(this.mainWindowManager, this.notifManager); 44 | this.mainWindowManager.setTrayManager(this.trayManager); 45 | 46 | // Listeners 47 | new AppListenersManager(this.mainWindowManager, this.autoUpdateManager).set(); 48 | } 49 | 50 | } 51 | 52 | export default Application; 53 | -------------------------------------------------------------------------------- /src/scripts/browser/bridges/native-notifier.js: -------------------------------------------------------------------------------- 1 | function createAppliedHandler (name) { 2 | return function () { 3 | const nativeNotifier = global.application.nativeNotifier; 4 | const func = nativeNotifier[name]; 5 | if (func) { 6 | func.apply(nativeNotifier, arguments); 7 | } 8 | }; 9 | } 10 | 11 | /** 12 | * Used from the renderer to fire native notifications. 13 | * Accessing global.application.nativeNotifier directly doesn't work. 14 | */ 15 | export default { 16 | isImplemented: !!global.application.nativeNotifier.isImplemented, 17 | fireNotification: createAppliedHandler('fireNotification'), 18 | removeNotification: createAppliedHandler('removeNotification'), 19 | onClick: createAppliedHandler('onClick') 20 | }; 21 | -------------------------------------------------------------------------------- /src/scripts/browser/components/auto-launcher/base.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'events'; 2 | 3 | class BaseAutoLauncher extends EventEmitter { 4 | 5 | async enable () { 6 | throw new Error('Not implemented'); 7 | } 8 | 9 | async disable () { 10 | throw new Error('Not implemented'); 11 | } 12 | 13 | async isEnabled () { 14 | throw new Error('Not implemented'); 15 | } 16 | 17 | } 18 | 19 | export default BaseAutoLauncher; 20 | -------------------------------------------------------------------------------- /src/scripts/browser/components/auto-launcher/impl-darwin.js: -------------------------------------------------------------------------------- 1 | import Plist from 'launchd.plist'; 2 | import fs from 'fs-extra-promise'; 3 | import {app} from 'electron'; 4 | import path from 'path'; 5 | 6 | import BaseAutoLauncher from 'browser/components/auto-launcher/base'; 7 | import files from 'common/utils/files'; 8 | 9 | const plistName = global.manifest.darwin.bundleId + '.plist'; 10 | const plistPath = path.join(app.getPath('home'), 'Library', 'LaunchAgents', plistName); 11 | 12 | class DarwinAutoLauncher extends BaseAutoLauncher { 13 | 14 | async enable () { 15 | log('enabling darwin auto-launch'); 16 | log('creating login plist'); 17 | await files.replaceFile(plistPath, () => fs.writeFileAsync(plistPath, this.buildPlist(), 'utf8')); 18 | } 19 | 20 | async disable () { 21 | log('disabling linux auto-launch'); 22 | log('removing login plist'); 23 | await fs.removeAsync(plistPath); 24 | } 25 | 26 | async isEnabled () { 27 | log('checking darwin auto-launch'); 28 | await files.isFileExists(plistPath); 29 | } 30 | 31 | buildPlist () { 32 | const plist = new Plist(); 33 | plist.setLabel(global.manifest.darwin.bundleId); 34 | plist.setProgram(app.getPath('exe')); 35 | plist.setProgramArgs(['--os-startup']); 36 | plist.setRunAtLoad(true); 37 | return plist.build(); 38 | } 39 | 40 | } 41 | 42 | export default DarwinAutoLauncher; 43 | -------------------------------------------------------------------------------- /src/scripts/browser/components/auto-launcher/impl-linux.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra-promise'; 2 | import {app} from 'electron'; 3 | import path from 'path'; 4 | 5 | import BaseAutoLauncher from 'browser/components/auto-launcher/base'; 6 | import files from 'common/utils/files'; 7 | 8 | const autoStartDir = path.join(app.getPath('home'), '.config', 'autostart'); 9 | const desktopFilePath = path.join(autoStartDir, global.manifest.name + '.desktop'); 10 | const initialDesktopPath = path.join(app.getAppPath(), 'startup.desktop'); 11 | 12 | class LinuxAutoLauncher extends BaseAutoLauncher { 13 | 14 | async enable () { 15 | log('enabling linux auto-launch'); 16 | log('creating autolaunch .desktop'); 17 | await files.replaceFile(desktopFilePath, () => fs.copyAsync(initialDesktopPath, desktopFilePath)); 18 | } 19 | 20 | async disable () { 21 | log('disabling linux auto-launch'); 22 | log('removing autolaunch .desktop'); 23 | await fs.removeAsync(desktopFilePath); 24 | } 25 | 26 | async isEnabled () { 27 | log('checking linux auto-launch'); 28 | await files.isFileExists(desktopFilePath); 29 | } 30 | 31 | } 32 | 33 | export default LinuxAutoLauncher; 34 | -------------------------------------------------------------------------------- /src/scripts/browser/components/auto-launcher/impl-win32.js: -------------------------------------------------------------------------------- 1 | import promisify from 'promisify-es6'; 2 | import Winreg from 'winreg'; 3 | 4 | import filePaths from 'common/utils/file-paths'; 5 | import BaseAutoLauncher from 'browser/components/auto-launcher/base'; 6 | 7 | const REG_KEY = new Winreg({ 8 | hive: Winreg.HKCU, 9 | key: '\\Software\\Microsoft\\Windows\\CurrentVersion\\Run' 10 | }); 11 | 12 | const setAsync = promisify(REG_KEY.set, {context: REG_KEY}); 13 | const removeAsync = promisify(REG_KEY.remove, {context: REG_KEY}); 14 | const keyExistsAsync = promisify(REG_KEY.keyExists, {context: REG_KEY}); 15 | 16 | class Win32AutoLauncher extends BaseAutoLauncher { 17 | 18 | async enable () { 19 | const updateExePath = filePaths.getSquirrelUpdateExePath(); 20 | const cmd = [ 21 | '"' + updateExePath + '"', 22 | '--processStart', 23 | '"' + global.manifest.productName + '.exe"', 24 | '--process-start-args', 25 | '"--os-startup"' 26 | ].join(' '); 27 | 28 | log('setting registry key for', global.manifest.productName, 'value', cmd); 29 | await setAsync(global.manifest.productName, Winreg.REG_SZ, cmd); 30 | } 31 | 32 | async disable () { 33 | log('removing registry key for', global.manifest.productName); 34 | try { 35 | await removeAsync(global.manifest.productName); 36 | } catch (err) { 37 | const notFoundMsg = 'The system was unable to find the specified registry key or value.'; 38 | const notFoundErr = err.message && err.message.includes(notFoundMsg); 39 | const knownError = notFoundErr; 40 | if (!knownError) { 41 | throw err; 42 | } 43 | } 44 | } 45 | 46 | async isEnabled () { 47 | log('querying registry key for', global.manifest.productName); 48 | const exists = await keyExistsAsync(global.manifest.productName); 49 | log('registry value for', global.manifest.productName, 'is', exists ? 'enabled' : 'disabled'); 50 | return exists; 51 | } 52 | 53 | } 54 | 55 | export default Win32AutoLauncher; 56 | -------------------------------------------------------------------------------- /src/scripts/browser/components/auto-launcher/index.js: -------------------------------------------------------------------------------- 1 | export default require('browser/components/auto-launcher/impl-' + process.platform).default; 2 | -------------------------------------------------------------------------------- /src/scripts/browser/components/auto-updater/base.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'events'; 2 | import needle from 'needle'; 3 | import {app} from 'electron'; 4 | import semver from 'semver'; 5 | 6 | class BaseAutoUpdater extends EventEmitter { 7 | 8 | setFeedURL (latestReleaseUrl) { 9 | log('set feed url', latestReleaseUrl); 10 | this.latestReleaseUrl = latestReleaseUrl; 11 | } 12 | 13 | checkForUpdates (url) { 14 | log('checking for update', url); 15 | this.emit('checking-for-update'); 16 | 17 | needle.get(url, {json: true}, (err, response, json) => { 18 | if (err) { 19 | log('update error while getting json', err); 20 | this.emit('error', err); 21 | return; 22 | } 23 | 24 | if (response.statusCode < 200 || response.statusCode >= 300) { 25 | log('update error statusCode', response.statusCode); 26 | this.emit('error', new Error(response.statusMessage)); 27 | return; 28 | } 29 | 30 | const newVersion = json.version; 31 | const newVersionExists = semver.gt(newVersion, global.manifest.version); 32 | const downloadUrl = json.url; 33 | 34 | if (newVersionExists) { 35 | log('update available', newVersion, downloadUrl); 36 | this.emit('update-available', newVersion, downloadUrl); 37 | } else { 38 | log('app version up to date', global.manifest.version); 39 | this.emit('update-not-available'); 40 | } 41 | }); 42 | } 43 | 44 | quitAndInstall () { 45 | log('quit and install'); 46 | app.quit(); 47 | } 48 | 49 | } 50 | 51 | export default BaseAutoUpdater; 52 | -------------------------------------------------------------------------------- /src/scripts/browser/components/auto-updater/impl-linux.js: -------------------------------------------------------------------------------- 1 | import URL from 'url'; 2 | 3 | import BaseAutoUpdater from 'browser/components/auto-updater/base'; 4 | 5 | class AutoUpdater extends BaseAutoUpdater { 6 | 7 | checkForUpdates () { 8 | if (!this.latestReleaseUrl) { 9 | this.emit('error', new Error('Latest release URL is not set')); 10 | return; 11 | } 12 | 13 | const packageType = global.manifest.distrib.split(':')[1]; 14 | let arch = null; 15 | 16 | if (packageType === 'deb') { 17 | arch = process.arch === 'ia32' ? 'i386' : 'amd64'; 18 | } else { 19 | arch = process.arch === 'ia32' ? 'i386' : 'x86_64'; 20 | } 21 | 22 | const urlObj = URL.parse(this.latestReleaseUrl); 23 | urlObj.query = urlObj.query || {}; 24 | urlObj.query.pkg = packageType; 25 | urlObj.query.arch = arch; 26 | 27 | const url = URL.format(urlObj); 28 | super.checkForUpdates(url); 29 | } 30 | 31 | } 32 | 33 | export default new AutoUpdater(); 34 | -------------------------------------------------------------------------------- /src/scripts/browser/components/auto-updater/impl-win32-portable.js: -------------------------------------------------------------------------------- 1 | import BaseAutoUpdater from 'browser/components/auto-updater/base'; 2 | 3 | class AutoUpdater extends BaseAutoUpdater { 4 | 5 | checkForUpdates () { 6 | if (!this.latestReleaseUrl) { 7 | this.emit('error', new Error('Latest release URL is not set')); 8 | return; 9 | } 10 | 11 | super.checkForUpdates(this.latestReleaseUrl); 12 | } 13 | 14 | } 15 | 16 | export default new AutoUpdater(); 17 | -------------------------------------------------------------------------------- /src/scripts/browser/components/auto-updater/index.js: -------------------------------------------------------------------------------- 1 | import platform from 'common/utils/platform'; 2 | 3 | let impl = null; 4 | 5 | if (platform.isLinux) { 6 | impl = require('browser/components/auto-updater/impl-linux').default; 7 | } else if (platform.isWindows && global.options.portable) { 8 | impl = require('browser/components/auto-updater/impl-win32-portable').default; 9 | } else { 10 | impl = require('electron').autoUpdater; 11 | } 12 | 13 | export default impl; 14 | -------------------------------------------------------------------------------- /src/scripts/browser/components/native-notifier/base.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'events'; 2 | 3 | class BaseNativeNotifier extends EventEmitter { 4 | 5 | static ACTIVATION_TYPES = [ 6 | 'none', 7 | 'contents-clicked', 8 | 'action-clicked', 9 | 'replied', 10 | 'additional-action-clicked' 11 | ]; 12 | 13 | constructor () { 14 | super(); 15 | 16 | // Flag that this notifier has not been implemented 17 | this.isImplemented = false; 18 | } 19 | 20 | } 21 | 22 | export default BaseNativeNotifier; 23 | -------------------------------------------------------------------------------- /src/scripts/browser/components/native-notifier/impl-darwin.js: -------------------------------------------------------------------------------- 1 | import $ from 'nodobjc'; 2 | 3 | import BaseNativeNotifier from 'browser/components/native-notifier/base'; 4 | 5 | /* eslint-disable babel/new-cap */ 6 | class DarwinNativeNotifier extends BaseNativeNotifier { 7 | 8 | constructor () { 9 | super(); 10 | 11 | // Flag that this notifier has been implemented 12 | this.isImplemented = true; 13 | 14 | // Obj-C setup 15 | $.framework('Foundation'); 16 | this.pool = $.NSAutoreleasePool('alloc')('init'); 17 | 18 | // Get the notification center 19 | this.center = $.NSUserNotificationCenter('defaultUserNotificationCenter'); 20 | 21 | // Create a notifications delegate 22 | this.Delegate = $.NSObject.extend(global.manifest.name + 'NotificationDelegate'); 23 | this.Delegate.addMethod('userNotificationCenter:didActivateNotification:', 24 | [$.void, [this.Delegate, $.selector, $.id, $.id]], ::this.didActivateNotification); 25 | this.Delegate.addMethod('userNotificationCenter:shouldPresentNotification:', 26 | ['c', [this.Delegate, $.selector, $.id, $.id]], ::this.shouldPresentNotification); 27 | this.Delegate.register(); 28 | 29 | // Set the delegate 30 | this.delegate = this.Delegate('alloc')('init'); 31 | this.center('setDelegate', this.delegate); 32 | } 33 | 34 | shouldPresentNotification (self, cmd, center, notif) { 35 | log('shouldPresentNotification', notif('identifier'), 'true'); 36 | return true; 37 | } 38 | 39 | didActivateNotification (self, cmd, center, notif) { 40 | const type = parseInt(notif('activationType').toString(), 10); 41 | const identifier = notif('identifier').toString(); 42 | const tag = identifier.split(':::')[0]; 43 | 44 | const payload = { 45 | tag, 46 | type: DarwinNativeNotifier.ACTIVATION_TYPES[type], 47 | identifier 48 | }; 49 | 50 | if (payload.type === 'replied') { 51 | payload.response = notif('response'); 52 | if (payload.response) { 53 | payload.response = payload.response.toString().replace('{\n}', ''); 54 | } 55 | } 56 | 57 | log('didActivateNotification', JSON.stringify(payload)); 58 | this.emit('notif-activated-' + identifier, payload); 59 | this.emit('notif-activated', payload); 60 | } 61 | 62 | fireNotification ({title, subtitle, body, tag = title, canReply, icon, onClick, onCreate}) { 63 | const identifier = tag + ':::' + Date.now(); 64 | const data = {title, subtitle, body, tag, canReply, onClick, onCreate, identifier}; 65 | 66 | // Create 67 | const notification = $.NSUserNotification('alloc')('init'); 68 | notification('setTitle', $.NSString('stringWithUTF8String', title)); 69 | notification('setIdentifier', $.NSString('stringWithUTF8String', identifier)); 70 | notification('setHasReplyButton', !!canReply); 71 | 72 | if (subtitle) { 73 | const str = $.NSString('stringWithUTF8String', subtitle); 74 | notification('setSubtitle', str); 75 | } 76 | 77 | if (body) { 78 | const str = $.NSString('stringWithUTF8String', body); 79 | notification('setInformativeText', str); 80 | } 81 | 82 | if (icon) { 83 | const contentImageUrl = $.NSURL('URLWithString', $(icon)); 84 | const contentImage = $.NSImage('alloc')('initByReferencingURL', contentImageUrl); 85 | notification('setContentImage', contentImage); 86 | } 87 | 88 | // Deliver 89 | log('delivering notification', JSON.stringify(data)); 90 | this.center('deliverNotification', notification); 91 | 92 | // Click callback 93 | if (onClick) { 94 | this.on('notif-activated-' + identifier, onClick); 95 | } 96 | 97 | // Creation callback 98 | if (onCreate) { 99 | onCreate(data); 100 | } 101 | } 102 | 103 | removeNotification (identifier) { 104 | const deliveredNotifications = this.center('deliveredNotifications'); 105 | for (let i = 0; i < deliveredNotifications('count'); i++) { 106 | const deliveredNotif = deliveredNotifications('objectAtIndex', $(i)('unsignedIntValue')); 107 | const deliveredIdentifier = deliveredNotif('identifier'); 108 | if (deliveredIdentifier && deliveredIdentifier.toString() === identifier) { 109 | log('removing notification', identifier, deliveredNotif); 110 | this.center('removeDeliveredNotification', deliveredNotif); 111 | break; 112 | } 113 | } 114 | } 115 | 116 | } 117 | 118 | export default DarwinNativeNotifier; 119 | -------------------------------------------------------------------------------- /src/scripts/browser/components/native-notifier/impl-linux.js: -------------------------------------------------------------------------------- 1 | import BaseNativeNotifier from 'browser/components/native-notifier/base'; 2 | 3 | class LinuxNativeNotifier extends BaseNativeNotifier {} 4 | 5 | export default LinuxNativeNotifier; 6 | -------------------------------------------------------------------------------- /src/scripts/browser/components/native-notifier/impl-win32.js: -------------------------------------------------------------------------------- 1 | import {app, screen, BrowserWindow} from 'electron'; 2 | import path from 'path'; 3 | 4 | import BaseNativeNotifier from 'browser/components/native-notifier/base'; 5 | 6 | /** 7 | * @source https://github.com/s-a/electron-toaster 8 | */ 9 | class Win32NativeNotifier extends BaseNativeNotifier { 10 | 11 | constructor (mainWindowManager) { 12 | super(); 13 | 14 | this.mainWindowManager = mainWindowManager; 15 | 16 | // Flag that this notifier has been implemented 17 | this.isImplemented = true; 18 | 19 | // Keep track of instances 20 | this.notifications = {}; 21 | } 22 | 23 | fireNotification ({title, body, footer, timeout, tag = title, icon, onClick, onCreate}) { 24 | const identifier = tag + ':::' + Date.now(); 25 | const data = {title, body, footer, timeout, tag, onClick, onCreate, identifier}; 26 | 27 | const currentWindow = this.mainWindowManager.window; 28 | if (!currentWindow) { 29 | logError(new Error('currentWindow not found')); 30 | return; 31 | } 32 | 33 | const currentWindowPos = currentWindow.getPosition(); 34 | const display = screen.getDisplayNearestPoint({x: currentWindowPos[0], y: currentWindowPos[1]}); 35 | let timerId; 36 | 37 | this.notifications[identifier] = new BrowserWindow({ 38 | width: 300, 39 | height: 80, 40 | title, 41 | frame: false, 42 | show: false, 43 | skipTaskbar: true, 44 | alwaysOnTop: true, 45 | useContentSize: true, 46 | autoHideMenuBar: true, 47 | acceptFirstMouse: true, 48 | resizable: false 49 | }); 50 | 51 | this.notifications[identifier].on('closed', () => { 52 | clearTimeout(timerId); 53 | delete this.notifications[identifier]; 54 | }); 55 | 56 | const animateAppearance = (width, height, callback) => { 57 | const fixedWidth = display.workAreaSize.width - width - 30; 58 | let i = 0; 59 | 60 | const doAnimateAppearance = () => { 61 | if (i < height) { 62 | i += Math.round(height / 10); 63 | timerId = setTimeout(() => { 64 | this.notifications[identifier].setPosition(fixedWidth, i - height + 40); 65 | if (i === Math.round(height / 10)) { 66 | this.notifications[identifier].showInactive(); 67 | } 68 | doAnimateAppearance(); 69 | }, 1); 70 | } else if (callback) { 71 | callback(); 72 | } 73 | }; 74 | 75 | doAnimateAppearance(); 76 | }; 77 | 78 | const htmlUrl = 'file://' + path.join(app.getAppPath(), 'html', 'shim-notification.html') + '?' + [ 79 | 'identifier=' + encodeURIComponent(identifier), 80 | 'title=' + encodeURIComponent(title || (global.manifest.productName + ' notification')), 81 | 'body=' + encodeURIComponent(body || 'New message.'), 82 | 'icon=' + encodeURIComponent(icon || '../images/default_avatar.png'), 83 | 'footer=' + encodeURIComponent(footer || global.manifest.productName), 84 | 'timeout=' + (timeout || 5000) 85 | ].join('&'); 86 | 87 | this.notifications[identifier].loadURL(htmlUrl); 88 | this.notifications[identifier].webContents.on('did-finish-load', () => { 89 | if (this.notifications[identifier]) { 90 | const [width, height] = this.notifications[identifier].getSize(); 91 | animateAppearance(width, height); 92 | } 93 | }); 94 | 95 | // this.notifications[identifier].setSize(900, 80); 96 | // this.notifications[identifier].openDevTools(); 97 | 98 | // Click callback 99 | if (onClick) { 100 | this.once('shim-notif-activated-' + identifier, onClick); 101 | } 102 | 103 | // Creation callback 104 | if (onCreate) { 105 | onCreate(data); 106 | } 107 | } 108 | 109 | removeNotification (identifier) { 110 | const notifWindow = this.notifications[identifier]; 111 | if (notifWindow) { 112 | notifWindow.close(); 113 | } 114 | } 115 | 116 | onClick (identifier) { 117 | const payload = { 118 | tag: identifier.split(':::')[0], 119 | identifier 120 | }; 121 | 122 | this.emit('shim-notif-activated-' + identifier, payload); 123 | } 124 | 125 | } 126 | 127 | export default Win32NativeNotifier; 128 | -------------------------------------------------------------------------------- /src/scripts/browser/components/native-notifier/index.js: -------------------------------------------------------------------------------- 1 | export default require('browser/components/native-notifier/impl-' + process.platform).default; 2 | -------------------------------------------------------------------------------- /src/scripts/browser/components/squirrel-events.js: -------------------------------------------------------------------------------- 1 | import {app} from 'electron'; 2 | import cp from 'child_process'; 3 | import path from 'path'; 4 | import del from 'del'; 5 | 6 | import AutoLauncher from 'browser/components/auto-launcher'; 7 | import filePaths from 'common/utils/file-paths'; 8 | 9 | class SquirrelEvents { 10 | 11 | check (options) { 12 | if (options.squirrelInstall) { 13 | log('creating shortcuts'); 14 | this.spawnSquirrel('--createShortcut', this.getShortcutExeName()).then(this.exitApp); 15 | return true; 16 | } 17 | 18 | if (options.squirrelUpdated || options.squirrelObsolete) { 19 | setTimeout(this.exitApp); 20 | return true; 21 | } 22 | 23 | if (options.squirrelUninstall) { 24 | this.teardown().then(this.exitApp); 25 | return true; 26 | } 27 | 28 | return false; 29 | } 30 | 31 | getShortcutExeName () { 32 | return path.basename(app.getPath('exe')); 33 | } 34 | 35 | exitApp (exitCode = 0) { 36 | app.exit(exitCode); 37 | } 38 | 39 | /** 40 | * Spawn Squirrel's Update.exe with the given arguments. 41 | */ 42 | async spawnSquirrel (...args) { 43 | const squirrelExec = filePaths.getSquirrelUpdateExePath(); 44 | log('spawning', squirrelExec, args); 45 | 46 | const child = cp.spawn(squirrelExec, args, {detached: true}); 47 | return await new Promise((resolve, reject) => { 48 | child.on('close', function (code) { 49 | if (code) { 50 | logError(new Error(squirrelExec + ' exited with code ' + code)); 51 | } 52 | resolve(code || 0); 53 | }); 54 | }); 55 | } 56 | 57 | async teardown () { 58 | try { await this.teardownShortcuts(); } catch (err) { logError(err, true); } 59 | try { await this.teardownAutoLauncherRegKey(); } catch (err) { logError(err, true); } 60 | try { await this.teardownLeftoverUserData(); } catch (err) { logError(err, true); } 61 | log('teardown finished'); 62 | } 63 | 64 | async teardownShortcuts () { 65 | log('removing shortcuts'); 66 | await this.spawnSquirrel('--removeShortcut', this.getShortcutExeName()); 67 | } 68 | 69 | async teardownAutoLauncherRegKey () { 70 | log('removing reg keys'); 71 | await new AutoLauncher().disable(); 72 | } 73 | 74 | async teardownLeftoverUserData () { 75 | const userDataPath = app.getPath('userData'); 76 | log('removing user data folder', userDataPath); 77 | await del(path.join(userDataPath, '**'), {force: true}) 78 | .then((files) => log('deleted', JSON.stringify(files))); 79 | } 80 | 81 | } 82 | 83 | export default new SquirrelEvents(); 84 | -------------------------------------------------------------------------------- /src/scripts/browser/init.js: -------------------------------------------------------------------------------- 1 | import {addPath} from 'app-module-path'; 2 | import {app} from 'electron'; 3 | import path from 'path'; 4 | 5 | const manifest = require('../../package.json'); 6 | global.manifest = manifest; 7 | 8 | const appPath = app.getAppPath(); 9 | const scriptsPath = path.join(appPath, 'scripts'); 10 | 11 | addPath(scriptsPath); 12 | 13 | require('browser/main'); 14 | -------------------------------------------------------------------------------- /src/scripts/browser/managers/app-listeners-manager.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'events'; 2 | import {app} from 'electron'; 3 | 4 | import prefs from 'browser/utils/prefs'; 5 | 6 | class AppListenersManager extends EventEmitter { 7 | 8 | constructor (mainWindowManager, autoUpdateManager) { 9 | super(); 10 | this.mainWindowManager = mainWindowManager; 11 | this.autoUpdateManager = autoUpdateManager; 12 | } 13 | 14 | /** 15 | * Bind events to local methods. 16 | */ 17 | set () { 18 | app.on('before-quit', ::this.onBeforeQuit); 19 | app.on('will-quit', ::this.onWillQuit); 20 | app.on('window-all-closed', ::this.onAllWindowsClosed); 21 | app.on('activate', ::this.onActivate); 22 | } 23 | 24 | /** 25 | * Called when the 'before-quit' event is emitted. 26 | */ 27 | onBeforeQuit () { 28 | // Set a flag to close the main window instead of hiding it 29 | log('before quit'); 30 | if (this.mainWindowManager) { 31 | this.mainWindowManager.forceClose = true; 32 | } 33 | } 34 | 35 | /** 36 | * Called when the 'will-quit' event is emitted. 37 | */ 38 | onWillQuit (event) { 39 | // Update the app before actually quitting 40 | log('will quit'); 41 | const hasUpdate = this.autoUpdateManager.state === this.autoUpdateManager.states.UPDATE_DOWNLOADED; 42 | const isUpdating = this.mainWindowManager.updateInProgress; 43 | try { 44 | if (hasUpdate && !isUpdating) { 45 | log('has update downloaded, installing it before quitting'); 46 | event.preventDefault(); 47 | prefs.setSync('launch-quit', true); 48 | prefs.setSync('notify-app-updated', true); 49 | setTimeout(() => { 50 | log('timeout over'); 51 | this.autoUpdateManager.quitAndInstall(); 52 | }, 200); 53 | } 54 | } catch (err) { 55 | logFatal(err); 56 | } 57 | } 58 | 59 | /** 60 | * Called when the 'window-all-closed' event is emitted. 61 | */ 62 | onAllWindowsClosed () { 63 | // Quit the app if all windows are closed 64 | log('all windows closed'); 65 | app.quit(); 66 | } 67 | 68 | /** 69 | * Called when the 'activate' event is emitted. 70 | */ 71 | onActivate (event, hasVisibleWindows) { 72 | // Reopen the main window on dock clicks (OS X) 73 | log('activate app, hasVisibleWindows', hasVisibleWindows); 74 | if (!hasVisibleWindows && this.mainWindowManager) { 75 | this.mainWindowManager.showOrCreate(); 76 | } 77 | } 78 | 79 | } 80 | 81 | export default AppListenersManager; 82 | -------------------------------------------------------------------------------- /src/scripts/browser/managers/main-menu-manager.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'events'; 2 | import {Menu} from 'electron'; 3 | 4 | import AutoUpdater from 'browser/components/auto-updater'; 5 | import {findItemById} from 'browser/menus/utils'; 6 | import template from 'browser/menus/main'; 7 | 8 | class MainMenuManager extends EventEmitter { 9 | 10 | constructor () { 11 | super(); 12 | this.cfuVisibleItem = null; 13 | } 14 | 15 | create () { 16 | if (!this.menu) { 17 | this.menu = Menu.buildFromTemplate(template()); 18 | log('app menu created'); 19 | } else { 20 | log('app menu already created'); 21 | } 22 | } 23 | 24 | setDefault () { 25 | if (this.menu) { 26 | Menu.setApplicationMenu(this.menu); 27 | log('app menu set'); 28 | } else { 29 | logError(new Error('menu not created')); 30 | } 31 | } 32 | 33 | setAutoUpdaterListeners () { 34 | if (!this.cfuVisibleItem) { 35 | this.cfuVisibleItem = findItemById(this.menu.items, 'cfu-check-for-update'); 36 | } 37 | 38 | const eventToIdMap = { 39 | 'error': 'cfu-check-for-update', 40 | 'checking-for-update': 'cfu-checking-for-update', 41 | 'update-available': 'cfu-update-available', 42 | 'update-not-available': 'cfu-check-for-update', 43 | 'update-downloaded': 'cfu-update-downloaded' 44 | }; 45 | 46 | for (let [eventName, itemId] of Object.entries(eventToIdMap)) { 47 | AutoUpdater.on(eventName, () => { 48 | log('auto updater on:', eventName, 'params:', ...arguments); 49 | this.cfuVisibleItem.visible = false; 50 | this.cfuVisibleItem = findItemById(this.menu.items, itemId); 51 | this.cfuVisibleItem.visible = true; 52 | }); 53 | } 54 | } 55 | 56 | windowSpecificItemsEnabled (enabled, items = this.menu.items) { 57 | for (let item of items) { 58 | if (item.needsWindow) { 59 | item.enabled = enabled; 60 | } else if (item.submenu) { 61 | this.windowSpecificItemsEnabled(enabled, item.submenu.items); 62 | } 63 | } 64 | } 65 | 66 | } 67 | 68 | export default MainMenuManager; 69 | -------------------------------------------------------------------------------- /src/scripts/browser/managers/notif-manager.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'events'; 2 | 3 | class NotifManager extends EventEmitter { 4 | 5 | constructor () { 6 | super(); 7 | this.unreadCount = ''; // number as string; empty string means 0 8 | } 9 | 10 | } 11 | 12 | export default NotifManager; 13 | -------------------------------------------------------------------------------- /src/scripts/browser/managers/tray-manager.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'events'; 2 | import {Menu, Tray, nativeImage} from 'electron'; 3 | 4 | import filePaths from 'common/utils/file-paths'; 5 | import platform from 'common/utils/platform'; 6 | import template from 'browser/menus/tray'; 7 | import prefs from 'browser/utils/prefs'; 8 | 9 | class TrayManager extends EventEmitter { 10 | 11 | constructor (mainWindowManager, notifManager) { 12 | super(); 13 | 14 | this.mainWindowManager = mainWindowManager; 15 | this.notifManager = notifManager; 16 | 17 | // Restore the tray menu from prefs 18 | if (prefs.get('show-tray')) { 19 | this.create(); 20 | } 21 | } 22 | 23 | /** 24 | * Create and set the default menu. 25 | */ 26 | create () { 27 | if (this.tray) { 28 | return; 29 | } 30 | 31 | if (platform.isDarwin) { 32 | const imagePath = filePaths.getImagePath('trayBlackTemplate.png'); 33 | const image = nativeImage.createFromPath(imagePath); 34 | 35 | const pressedImagePath = filePaths.getImagePath('trayWhiteTemplate.png'); 36 | const pressedImage = nativeImage.createFromPath(pressedImagePath); 37 | 38 | this.tray = new Tray(image); 39 | this.tray.setPressedImage(pressedImage); 40 | 41 | // Show the notifications count 42 | if (this.notifManager.unreadCount) { 43 | this.tray.setTitle(this.notifManager.unreadCount); 44 | } 45 | } else { 46 | const imgExt = platform.isWindows ? 'ico' : 'png'; 47 | const iconName = this.notifManager.unreadCount ? 'trayAlert' : 'tray'; 48 | 49 | const imagePath = filePaths.getImagePath(iconName + '.' + imgExt); 50 | const image = nativeImage.createFromPath(imagePath); 51 | 52 | this.tray = new Tray(image); 53 | } 54 | 55 | this.menu = Menu.buildFromTemplate(template()); 56 | if (platform.isLinux) { 57 | this.tray.setContextMenu(this.menu); 58 | } 59 | this.setEventListeners(); 60 | log('tray menu created'); 61 | } 62 | 63 | /** 64 | * Listen for tray events. 65 | */ 66 | setEventListeners () { 67 | if (this.tray) { 68 | this.tray.on('click', ::this.onClick); 69 | this.tray.on('right-click', ::this.onRightClick); 70 | } 71 | } 72 | 73 | /** 74 | * Called when the 'click' event is emitted on the tray menu. 75 | */ 76 | onClick () { 77 | // Show the main window 78 | log('tray click'); 79 | if (this.mainWindowManager) { 80 | const mainWindow = this.mainWindowManager.window; 81 | if (mainWindow) { 82 | mainWindow.show(); 83 | } 84 | } 85 | } 86 | 87 | /** 88 | * Called when the 'right-click' event is emitted on the tray menu. 89 | */ 90 | onRightClick () { 91 | // Show the context menu 92 | log('tray right-click'); 93 | this.tray.popUpContextMenu(this.menu); 94 | } 95 | 96 | /** 97 | * Hide and destroy the tray menu. 98 | */ 99 | destroy () { 100 | if (this.tray) { 101 | this.tray.destroy(); 102 | } 103 | this.menu = null; 104 | this.tray = null; 105 | } 106 | 107 | /** 108 | * Called when the unread count changes. 109 | */ 110 | unreadCountUpdated (count) { 111 | if (!this.tray) { 112 | return; 113 | } 114 | 115 | if (platform.isDarwin) { 116 | this.tray.setTitle(count); 117 | } else { 118 | const imgExt = platform.isWindows ? 'ico' : 'png'; 119 | const iconName = count ? 'trayAlert' : 'tray'; 120 | 121 | const imagePath = filePaths.getImagePath(iconName + '.' + imgExt); 122 | const image = nativeImage.createFromPath(imagePath); 123 | 124 | this.tray.setImage(image); 125 | } 126 | } 127 | 128 | } 129 | 130 | export default TrayManager; 131 | -------------------------------------------------------------------------------- /src/scripts/browser/menus/context.js: -------------------------------------------------------------------------------- 1 | import {clipboard, Menu, MenuItem} from 'electron'; 2 | import spellChecker from 'spellchecker'; 3 | 4 | import platform from 'common/utils/platform'; 5 | import urls from 'common/utils/urls'; 6 | 7 | function create (params, browserWindow) { 8 | const webContents = browserWindow.webContents; 9 | const menu = new Menu(); 10 | 11 | if (platform.isDarwin && params.selectionText) { 12 | menu.append(new MenuItem({ 13 | label: 'Look Up "' + params.selectionText + '"', 14 | click: () => webContents.send('call-webview-method', 'showDefinitionForSelection') 15 | })); 16 | } 17 | 18 | if (params.isEditable && params.misspelledWord) { 19 | const corrections = spellChecker.getCorrectionsForMisspelling(params.misspelledWord); 20 | const items = []; 21 | 22 | // add correction suggestions 23 | for (let i = 0; i < corrections.length && i < 5; i++) { 24 | items.push(new MenuItem({ 25 | label: 'Correct: ' + corrections[i], 26 | click: () => webContents.send('call-webview-method', 'replaceMisspelling', corrections[i]) 27 | })); 28 | } 29 | 30 | // Hunspell doesn't remember these, so skip this item 31 | // Otherwise, offer to add the word to the dictionary 32 | if (!platform.isLinux && !params.isWindows7) { 33 | items.push(new MenuItem({ 34 | label: 'Add to Dictionary', 35 | click: () => { 36 | webContents.send('fwd-webview', 'add-selection-to-dictionary'); 37 | webContents.send('call-webview-method', 'replaceMisspelling', params.misspelledWord); 38 | } 39 | })); 40 | } 41 | 42 | // prepend separator and then add items 43 | if (items.length) { 44 | menu.append(new MenuItem({ 45 | type: 'separator' 46 | })); 47 | 48 | for (const item of items) { 49 | menu.append(item); 50 | } 51 | } 52 | } 53 | 54 | if (params.isEditable) { 55 | menu.append(new MenuItem({ 56 | type: 'separator' 57 | })); 58 | 59 | menu.append(new MenuItem({ 60 | label: 'Undo', 61 | enabled: params.editFlags.canUndo, 62 | click: () => webContents.send('call-webview-method', 'undo') 63 | })); 64 | 65 | menu.append(new MenuItem({ 66 | label: 'Redo', 67 | enabled: params.editFlags.canRedo, 68 | click: () => webContents.send('call-webview-method', 'redo') 69 | })); 70 | } 71 | 72 | if (params.selectionText || params.isEditable) { 73 | menu.append(new MenuItem({ 74 | type: 'separator' 75 | })); 76 | 77 | menu.append(new MenuItem({ 78 | label: 'Cut', 79 | enabled: params.editFlags.canCut, 80 | click: () => webContents.send('call-webview-method', 'cut') 81 | })); 82 | 83 | menu.append(new MenuItem({ 84 | label: 'Copy', 85 | enabled: params.editFlags.canCopy, 86 | click: () => webContents.send('call-webview-method', 'copy') 87 | })); 88 | 89 | menu.append(new MenuItem({ 90 | label: 'Paste', 91 | enabled: params.editFlags.canPaste, 92 | click: () => webContents.send('call-webview-method', 'pasteAndMatchStyle') 93 | })); 94 | 95 | menu.append(new MenuItem({ 96 | label: 'Select All', 97 | enabled: params.editFlags.canSelectAll, 98 | click: () => webContents.send('call-webview-method', 'selectAll') 99 | })); 100 | } 101 | 102 | if (params.linkURL) { 103 | menu.append(new MenuItem({ 104 | type: 'separator' 105 | })); 106 | 107 | menu.append(new MenuItem({ 108 | label: 'Copy Link Text', 109 | enabled: !!params.linkText, 110 | click: () => clipboard.writeText(params.linkText) 111 | })); 112 | 113 | menu.append(new MenuItem({ 114 | label: 'Copy Link Address', 115 | click: () => clipboard.writeText(urls.skipFacebookRedirect(params.linkURL)) 116 | })); 117 | } 118 | 119 | return menu; 120 | } 121 | 122 | export default { 123 | create 124 | }; 125 | -------------------------------------------------------------------------------- /src/scripts/browser/menus/expressions/expr-meta.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Run all the given expressions, serially. 3 | */ 4 | export function all (...exprs) { 5 | return function () { 6 | for (let expr of exprs) { 7 | expr.apply(this, arguments); 8 | } 9 | }; 10 | } 11 | 12 | /** 13 | * The equivalent of an 'if' statement. 14 | */ 15 | export function ifTrue (condExpr, trueExpr, falseExpr) { 16 | return function () { 17 | const cond = condExpr.apply(this, arguments); 18 | if (cond) { 19 | if (trueExpr) { 20 | trueExpr.apply(this, arguments); 21 | } 22 | } else if (falseExpr) { 23 | falseExpr.apply(this, arguments); 24 | } 25 | }; 26 | } 27 | 28 | /** 29 | * Runs a custom function. 30 | */ 31 | export function custom (fn) { 32 | return function () { 33 | fn.apply(this, arguments); 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /src/scripts/browser/menus/expressions/expr-parse.js: -------------------------------------------------------------------------------- 1 | import {BrowserWindow} from 'electron'; 2 | 3 | import {findItemById, findMenu} from 'browser/menus/utils'; 4 | import prefs from 'browser/utils/prefs'; 5 | 6 | /** 7 | * Set a key of the item with the given value. 8 | */ 9 | export function setLocal (localKey, valueExpr) { 10 | return function (item) { 11 | item[localKey] = valueExpr.apply(this, arguments); 12 | }; 13 | } 14 | 15 | /** 16 | * Sets a preference key. 17 | */ 18 | export function setPref (prefName, valueExpr) { 19 | return function () { 20 | prefs.set(prefName, valueExpr.apply(this, arguments)); 21 | }; 22 | } 23 | 24 | /** 25 | * Unsets a preference key. 26 | */ 27 | export function unsetPref (prefName) { 28 | return function () { 29 | prefs.unset(prefName); 30 | }; 31 | } 32 | 33 | /** 34 | * Updates the value of a sibling item's key. 35 | */ 36 | export function updateSibling (siblingId, siblingKey, valueExpr) { 37 | return function (item) { 38 | const submenu = (this && this.submenu) || (item && item.menu && item.menu.items); 39 | if (submenu) { 40 | const sibling = submenu.find((i) => i.id === siblingId); 41 | if (sibling) { 42 | sibling[siblingKey] = valueExpr.apply(this, arguments); 43 | } 44 | } 45 | }; 46 | } 47 | 48 | /** 49 | * Update an item from another menu. 50 | */ 51 | export function updateMenuItem (menuType, itemId) { 52 | return function (...valueExprs) { 53 | return function (exprCallback) { 54 | return function () { 55 | const menu = findMenu(menuType); 56 | if (!menu) { 57 | return log('menu not found', menuType); 58 | } 59 | 60 | const menuItem = findItemById(menu.items, itemId); 61 | if (!menuItem) { 62 | return log('menu item not found', itemId); 63 | } 64 | 65 | const values = valueExprs.map((e) => e.apply(this, arguments)); 66 | const expr = exprCallback(...values); 67 | const browserWindow = BrowserWindow.getFocusedWindow(); 68 | expr.call(global, menuItem, browserWindow); 69 | }; 70 | }; 71 | }; 72 | } 73 | -------------------------------------------------------------------------------- /src/scripts/browser/menus/expressions/expr-value.js: -------------------------------------------------------------------------------- 1 | import prefs from 'browser/utils/prefs'; 2 | import files from 'common/utils/files'; 3 | 4 | /** 5 | * Wrapper for a raw value. 6 | */ 7 | export function val (value) { 8 | return function () { 9 | return value; 10 | }; 11 | } 12 | 13 | /** 14 | * Returns the given key's value from the item. 15 | */ 16 | export function key (localKey) { 17 | return function (item) { 18 | return item[localKey]; 19 | }; 20 | } 21 | 22 | /** 23 | * Returns the pref value for the given key. 24 | */ 25 | export function pref (prefName) { 26 | return function () { 27 | return prefs.get(prefName); 28 | }; 29 | } 30 | 31 | /** 32 | * Negates the given value. 33 | */ 34 | export function not (valueExpr) { 35 | return function () { 36 | return !valueExpr.apply(this, arguments); 37 | }; 38 | } 39 | 40 | /** 41 | * Sums up two expressions. 42 | */ 43 | export function sum (value1Expr, value2Expr) { 44 | return function () { 45 | return value1Expr.apply(this, arguments) + value2Expr.apply(this, arguments); 46 | }; 47 | } 48 | 49 | /** 50 | * Checks 2 expressions for equality. 51 | */ 52 | export function eq (value1Expr, value2Expr) { 53 | return function () { 54 | return value1Expr.apply(this, arguments) === value2Expr.apply(this, arguments); 55 | }; 56 | } 57 | 58 | /** 59 | * Gets the css content of the given theme. 60 | */ 61 | export function themeCss (nameExpr, callback) { 62 | return function () { 63 | const theme = nameExpr.apply(this, arguments); 64 | files.getThemeCss(theme) 65 | .then((css) => callback(css).apply(this, arguments)) 66 | .catch(logError); 67 | }; 68 | } 69 | 70 | /** 71 | * Gets the css content of the given style. 72 | */ 73 | export function styleCss (styleName, callback) { 74 | return function () { 75 | files.getStyleCss(styleName) 76 | .then((css) => callback(css).apply(this, arguments)) 77 | .catch(logError); 78 | }; 79 | } 80 | -------------------------------------------------------------------------------- /src/scripts/browser/menus/expressions/index.js: -------------------------------------------------------------------------------- 1 | export default Object.assign({}, ...[ 2 | require('browser/menus/expressions/expr-click'), 3 | require('browser/menus/expressions/expr-parse'), 4 | require('browser/menus/expressions/expr-value'), 5 | require('browser/menus/expressions/expr-meta') 6 | ]); 7 | -------------------------------------------------------------------------------- /src/scripts/browser/menus/main.js: -------------------------------------------------------------------------------- 1 | import {parseTemplate} from 'browser/menus/utils'; 2 | 3 | export default function () { 4 | const template = [ 5 | 'browser/menus/templates/main-app', 6 | 'browser/menus/templates/main-edit', 7 | 'browser/menus/templates/main-view', 8 | 'browser/menus/templates/main-theme', 9 | 'browser/menus/templates/main-privacy', 10 | 'browser/menus/templates/main-window', 11 | 'browser/menus/templates/main-help' 12 | ].map((module) => require(module).default); 13 | return parseTemplate(template, null); 14 | } 15 | -------------------------------------------------------------------------------- /src/scripts/browser/menus/templates/main-app.js: -------------------------------------------------------------------------------- 1 | import platform from 'common/utils/platform'; 2 | import $ from 'browser/menus/expressions'; 3 | 4 | export default { 5 | label: platform.isDarwin ? global.manifest.productName : '&App', 6 | submenu: [{ 7 | label: 'About ' + global.manifest.productName, 8 | click: $.showCustomAboutDialog() 9 | }, { 10 | type: 'checkbox', 11 | label: 'Switch to Workplace Messenger', 12 | click: $.all( 13 | $.setPref('switch-workplace', $.key('checked')), 14 | $.reloadWindow() 15 | ), 16 | parse: $.all( 17 | $.setLocal('checked', $.pref('switch-workplace')) 18 | ) 19 | }, { 20 | label: platform.isDarwin ? 'Preferences...' : 'Settings', 21 | accelerator: 'CmdOrCtrl+,', 22 | click: $.sendToWebView('open-preferences-modal'), 23 | needsWindow: true 24 | }, { 25 | type: 'separator', 26 | allow: !global.options.mas 27 | }, { 28 | id: 'cfu-check-for-update', 29 | label: 'Check for &Update...', 30 | allow: !global.options.mas, 31 | click: $.cfuCheckForUpdate(true) 32 | }, { 33 | id: 'cfu-checking-for-update', 34 | label: 'Checking for &Update...', 35 | allow: !global.options.mas, 36 | enabled: false, 37 | visible: false 38 | }, { 39 | id: 'cfu-update-available', 40 | label: 'Download &Update...', 41 | allow: platform.isNonDarwin && (platform.isLinux || global.options.portable), 42 | visible: false, 43 | click: $.cfuUpdateAvailable() 44 | }, { 45 | id: 'cfu-update-available', 46 | label: 'Downloading &Update...', 47 | allow: !global.options.mas && !platform.isLinux && !global.options.portable, 48 | enabled: false, 49 | visible: false 50 | }, { 51 | id: 'cfu-update-downloaded', 52 | label: 'Restart and Install &Update...', 53 | allow: !global.options.mas, 54 | visible: false, 55 | click: $.cfuUpdateDownloaded() 56 | }, { 57 | label: 'Updates Release Channel', 58 | allow: !global.options.mas, 59 | submenu: ['Stable', 'Beta', 'Dev'].map((channelName) => ({ 60 | type: 'radio', 61 | label: channelName, 62 | channel: channelName.toLowerCase(), 63 | click: $.all( 64 | $.setPref('updates-channel', $.key('channel')), 65 | $.resetAutoUpdaterUrl(), 66 | $.cfuCheckForUpdate(false) 67 | ), 68 | parse: $.all( 69 | $.setLocal('checked', $.eq($.pref('updates-channel'), $.key('channel'))) 70 | ) 71 | })) 72 | }, { 73 | type: 'checkbox', 74 | label: 'Check for Update Automatically', 75 | allow: !global.options.mas, 76 | click: $.all( 77 | $.checkForUpdateAuto($.key('checked')), 78 | $.setPref('updates-auto-check', $.key('checked')) 79 | ), 80 | parse: $.setLocal('checked', $.pref('updates-auto-check')) 81 | }, { 82 | type: 'separator' 83 | }, { 84 | type: 'checkbox', 85 | label: '&Launch on OS Startup', 86 | allow: !global.options.mas && !global.options.portable, 87 | click: $.all( 88 | $.launchOnStartup($.key('checked')), 89 | $.updateSibling('startup-hidden', 'enabled', $.key('checked')), 90 | $.setPref('launch-startup', $.key('checked')) 91 | ), 92 | parse: $.all( 93 | $.setLocal('checked', $.pref('launch-startup')), 94 | $.updateSibling('startup-hidden', 'enabled', $.key('checked')) 95 | ) 96 | }, { 97 | id: 'startup-hidden', 98 | type: 'checkbox', 99 | label: 'Start &Hidden on Startup', 100 | allow: !global.options.mas && !global.options.portable, 101 | click: $.setPref('launch-startup-hidden', $.key('checked')), 102 | parse: $.setLocal('checked', $.pref('launch-startup-hidden')) 103 | }, { 104 | type: 'separator' 105 | }, { 106 | label: 'Restart in Debug Mode', 107 | allow: !global.options.debug, 108 | click: $.restartInDebugMode() 109 | }, { 110 | label: 'Running in Debug Mode', 111 | allow: global.options.debug, 112 | enabled: false 113 | }, { 114 | label: 'Open Debug Log...', 115 | enabled: global.options.debug, 116 | click: $.openDebugLog() 117 | }, { 118 | type: 'separator' 119 | }, { 120 | label: '&Quit', 121 | accelerator: 'Ctrl+Q', 122 | allow: platform.isNonDarwin, 123 | click: $.appQuit() 124 | }, { 125 | role: 'services', 126 | submenu: [], 127 | allow: platform.isDarwin 128 | }, { 129 | type: 'separator', 130 | allow: platform.isDarwin 131 | }, { 132 | role: 'hide', 133 | allow: platform.isDarwin 134 | }, { 135 | role: 'hideothers', 136 | allow: platform.isDarwin 137 | }, { 138 | role: 'unhide', 139 | allow: platform.isDarwin 140 | }, { 141 | type: 'separator', 142 | allow: platform.isDarwin 143 | }, { 144 | role: 'quit', 145 | allow: platform.isDarwin 146 | }] 147 | }; 148 | -------------------------------------------------------------------------------- /src/scripts/browser/menus/templates/main-edit.js: -------------------------------------------------------------------------------- 1 | import {getAvailableDictionaries} from 'common/utils/spellchecker'; 2 | import languageCodes from 'common/utils/language-codes'; 3 | import platform from 'common/utils/platform'; 4 | import prefs from 'browser/utils/prefs'; 5 | import $ from 'browser/menus/expressions'; 6 | 7 | const spellCheckerLanguage = prefs.get('spell-checker-language'); 8 | const availableLanguages = getAvailableDictionaries() 9 | .map((langCode) => { 10 | return { 11 | code: langCode, 12 | name: languageCodes[langCode] || 13 | languageCodes[langCode.replace('-', '_')] || 14 | languageCodes[langCode.replace('-', '_').split('_')[0]] 15 | }; 16 | }) 17 | .filter((langObj) => langObj.name) 18 | .filter((langObj, index, arr) => { 19 | for (let i = index + 1; i < arr.length; i++) { 20 | if (arr[i].name === langObj.name) { 21 | return false; 22 | } 23 | } 24 | 25 | return true; 26 | }) 27 | .sort((a, b) => { 28 | if (a.name < b.name) { 29 | return -1; 30 | } else if (a.name > b.name) { 31 | return 1; 32 | } else { 33 | return 0; 34 | } 35 | }); 36 | 37 | export default { 38 | label: 'Edit', 39 | submenu: [{ 40 | allow: platform.isDarwin, 41 | role: 'undo' 42 | }, { 43 | allow: platform.isDarwin, 44 | role: 'redo' 45 | }, { 46 | type: 'separator', 47 | allow: platform.isDarwin 48 | }, { 49 | allow: platform.isDarwin, 50 | role: 'cut' 51 | }, { 52 | allow: platform.isDarwin, 53 | role: 'copy' 54 | }, { 55 | allow: platform.isDarwin, 56 | role: 'paste' 57 | }, { 58 | allow: platform.isDarwin, 59 | role: 'delete' 60 | }, { 61 | allow: platform.isDarwin, 62 | role: 'selectall' 63 | }, { 64 | type: 'separator', 65 | allow: platform.isDarwin 66 | }, { 67 | type: 'checkbox', 68 | label: 'Check &Spelling While Typing', 69 | accelerator: 'CmdOrCtrl+Alt+S', 70 | needsWindow: true, 71 | click: $.all( 72 | $.sendToWebView( 73 | 'spell-checker', 74 | $.key('checked'), 75 | $.pref('spell-checker-auto-correct'), 76 | $.pref('spell-checker-language') 77 | ), 78 | $.updateSibling('spell-checker-auto-correct', 'enabled', $.key('checked')), 79 | $.updateSibling('spell-checker-language', 'enabled', $.key('checked')), 80 | $.setPref('spell-checker-check', $.key('checked')) 81 | ), 82 | parse: $.all( 83 | $.setLocal('checked', $.pref('spell-checker-check')) 84 | ) 85 | }, { 86 | id: 'spell-checker-auto-correct', 87 | type: 'checkbox', 88 | label: '&Auto Correct Spelling Mistakes', 89 | needsWindow: true, 90 | allow: false, 91 | click: $.all( 92 | $.sendToWebView( 93 | 'spell-checker', 94 | $.pref('spell-checker-check'), 95 | $.key('checked'), 96 | $.pref('spell-checker-language') 97 | ), 98 | $.setPref('spell-checker-auto-correct', $.key('checked')) 99 | ), 100 | parse: $.all( 101 | $.setLocal('enabled', $.pref('spell-checker-check')), 102 | $.setLocal('checked', $.pref('spell-checker-auto-correct')) 103 | ) 104 | }, { 105 | id: 'spell-checker-language', 106 | label: 'Spell Checker Language', 107 | submenu: availableLanguages.map((lang) => ({ 108 | type: 'radio', 109 | label: lang.name, 110 | langCode: lang.code, 111 | checked: spellCheckerLanguage === lang.code, 112 | needsWindow: true, 113 | click: $.all( 114 | $.ifTrue( 115 | $.pref('spell-checker-check'), 116 | $.sendToWebView( 117 | 'spell-checker', 118 | $.pref('spell-checker-check'), 119 | $.pref('spell-checker-auto-correct'), 120 | $.key('langCode') 121 | ) 122 | ), 123 | $.setPref('spell-checker-language', $.key('langCode')) 124 | ) 125 | })) 126 | }] 127 | }; 128 | -------------------------------------------------------------------------------- /src/scripts/browser/menus/templates/main-help.js: -------------------------------------------------------------------------------- 1 | import $ from 'browser/menus/expressions'; 2 | 3 | export default { 4 | label: '&Help', 5 | role: 'help', 6 | submenu: [{ 7 | label: 'Open App Website', 8 | click: $.openUrl('https://messengerfordesktop.com/') 9 | }, { 10 | label: 'Send Feedback', 11 | click: $.openUrl('https://aluxian.typeform.com/to/sr2gEc') 12 | }] 13 | }; 14 | -------------------------------------------------------------------------------- /src/scripts/browser/menus/templates/main-privacy.js: -------------------------------------------------------------------------------- 1 | import $ from 'browser/menus/expressions'; 2 | 3 | export default { 4 | label: 'Privacy', 5 | submenu: [{ 6 | type: 'checkbox', 7 | label: '&Report App Stats and Crashes', 8 | click: $.setPref('analytics-track', $.key('checked')), 9 | parse: $.setLocal('checked', $.pref('analytics-track')) 10 | }, { 11 | id: 'block-seen-typing', 12 | type: 'checkbox', 13 | label: '&Block Seen and Typing Indicators', 14 | click: $.all( 15 | $.setPref('block-seen-typing', $.key('checked')), 16 | $.blockSeenTyping($.key('checked')) 17 | ), 18 | parse: $.all( 19 | $.setLocal('checked', $.pref('block-seen-typing')) 20 | ) 21 | }] 22 | }; 23 | -------------------------------------------------------------------------------- /src/scripts/browser/menus/templates/main-theme.js: -------------------------------------------------------------------------------- 1 | import $ from 'browser/menus/expressions'; 2 | 3 | export default { 4 | label: 'Theme', 5 | submenu: Object.keys(global.manifest.themes).map((themeId, index) => ({ 6 | type: 'radio', 7 | label: global.manifest.themes[themeId], 8 | theme: themeId, 9 | needsWindow: true, 10 | click: $.all( 11 | $.themeCss($.key('theme'), (css) => $.sendToWebView('apply-theme', $.val(css))), 12 | $.setPref('theme', $.key('theme')) 13 | ), 14 | parse: $.all( 15 | $.setLocal('checked', $.eq($.pref('theme'), $.key('theme'))) 16 | ) 17 | })) 18 | }; 19 | -------------------------------------------------------------------------------- /src/scripts/browser/menus/templates/main-view.js: -------------------------------------------------------------------------------- 1 | import platform from 'common/utils/platform'; 2 | import $ from 'browser/menus/expressions'; 3 | 4 | export default { 5 | label: '&View', 6 | submenu: [{ 7 | label: 'Zoom In', 8 | accelerator: 'CmdOrCtrl+plus', 9 | needsWindow: true, 10 | click: $.all( 11 | $.setPref('zoom-level', $.sum($.pref('zoom-level'), $.val(+0.25))), 12 | $.sendToWebContents('zoom-level', $.pref('zoom-level')) 13 | ) 14 | }, { 15 | label: 'Zoom Out', 16 | accelerator: 'CmdOrCtrl+-', 17 | needsWindow: true, 18 | click: $.all( 19 | $.setPref('zoom-level', $.sum($.pref('zoom-level'), $.val(-0.25))), 20 | $.sendToWebContents('zoom-level', $.pref('zoom-level')) 21 | ) 22 | }, { 23 | label: 'Reset Zoom', 24 | accelerator: 'CmdOrCtrl+0', 25 | needsWindow: true, 26 | click: $.all( 27 | $.sendToWebContents('zoom-level', $.val(0)), 28 | $.unsetPref('zoom-level') 29 | ) 30 | }, { 31 | type: 'separator' 32 | }, { 33 | needsWindow: true, 34 | role: 'togglefullscreen' 35 | }, { 36 | label: 'Toggle &Developer Tools', 37 | needsWindow: true, 38 | click: $.toggleDevTools() 39 | }, { 40 | label: 'Toggle WebView &Dev Tools', 41 | needsWindow: true, 42 | click: $.toggleWebViewDevTools() 43 | }, { 44 | type: 'separator' 45 | }, { 46 | type: 'checkbox', 47 | label: 'Auto Hide &Menu Bar', 48 | accelerator: 'Alt+Ctrl+B', 49 | needsWindow: true, 50 | allow: platform.isNonDarwin, 51 | click: $.all( 52 | $.setPref('auto-hide-menubar', $.key('checked')), 53 | $.autoHideMenuBar($.key('checked')) 54 | ), 55 | parse: $.all( 56 | $.setLocal('checked', $.pref('auto-hide-menubar')) 57 | ) 58 | }, { 59 | type: 'separator' 60 | }, { 61 | // type: 'checkbox', 62 | // label: 'Auto Hide Sidebar', 63 | // needsWindow: true, 64 | // click: $.all( 65 | // $.styleCss('auto-hide-sidebar', (css) => 66 | // $.sendToWebView('apply-sidebar-auto-hide', $.key('checked'), $.val(css)) 67 | // ), 68 | // $.setPref('sidebar-auto-hide', $.key('checked')) 69 | // ), 70 | // parse: $.all( 71 | // $.setLocal('checked', $.pref('sidebar-auto-hide')) 72 | // ) 73 | // }, { 74 | // type: 'separator' 75 | // }, { 76 | label: 'N&ew Conversation', 77 | accelerator: 'CmdOrCtrl+N', 78 | needsWindow: true, 79 | click: $.sendToWebView('new-conversation') 80 | }, { 81 | label: 'Search &Chats', 82 | accelerator: 'CmdOrCtrl+F', 83 | needsWindow: true, 84 | click: $.sendToWebView('search-chats') 85 | }, { 86 | type: 'separator' 87 | }, { 88 | label: '&Next Conversation', 89 | accelerator: 'CmdOrCtrl+Down', 90 | needsWindow: true, 91 | click: $.sendToWebView('switch-conversation-next') 92 | }, { 93 | label: '&Previous Conversation', 94 | accelerator: 'CmdOrCtrl+Up', 95 | needsWindow: true, 96 | click: $.sendToWebView('switch-conversation-previous') 97 | }, { 98 | label: 'Switch to Conversation', 99 | submenu: [1, 2, 3, 4, 5, 6, 7, 8, 9].map((num) => ({ 100 | label: 'Conversation ' + num, 101 | accelerator: 'CmdOrCtrl+' + num, 102 | needsWindow: true, 103 | click: $.sendToWebView('switch-conversation-num', $.val(num)) 104 | })) 105 | }] 106 | }; 107 | -------------------------------------------------------------------------------- /src/scripts/browser/menus/templates/main-window.js: -------------------------------------------------------------------------------- 1 | import platform from 'common/utils/platform'; 2 | import $ from 'browser/menus/expressions'; 3 | 4 | export default { 5 | label: 'Window', 6 | role: 'window', 7 | submenu: [{ 8 | label: '&Reload', 9 | accelerator: 'CmdOrCtrl+R', 10 | needsWindow: true, 11 | click: $.reloadWindow() 12 | }, { 13 | label: 'Re&set', 14 | accelerator: 'CmdOrCtrl+Alt+R', 15 | needsWindow: true, 16 | click: $.resetWindow() 17 | }, { 18 | type: 'separator' 19 | }, { 20 | type: 'checkbox', 21 | label: '&Float on Top', 22 | accelerator: 'CmdOrCtrl+Alt+T', 23 | needsWindow: true, 24 | click: $.floatOnTop($.key('checked')) 25 | }, { 26 | type: 'checkbox', 27 | label: 'Close with &Escape Key', 28 | click: $.setPref('close-with-esc', $.key('checked')), 29 | parse: $.setLocal('checked', $.pref('close-with-esc')) 30 | }, { 31 | type: 'checkbox', 32 | label: 'Open Links in &Browser', 33 | click: $.setPref('links-in-browser', $.key('checked')), 34 | parse: $.setLocal('checked', $.pref('links-in-browser')) 35 | }, { 36 | type: 'checkbox', 37 | label: '&Notifications Badge in ' + (platform.isWindows ? 'Taskbar' : 'Dock'), 38 | needsWindow: true, 39 | click: $.all( 40 | $.setPref('show-notifications-badge', $.key('checked')), 41 | platform.isWindows ? $.hideTaskbarBadge($.key('checked')) : $.hideDockBadge($.key('checked')) 42 | ), 43 | parse: $.all( 44 | $.setLocal('checked', $.pref('show-notifications-badge')) 45 | ) 46 | }, { 47 | type: 'checkbox', 48 | label: 'Accept First &Click', 49 | needsWindow: true, 50 | click: $.all( 51 | $.setPref('accept-first-mouse', $.key('checked')), 52 | $.restartApp() 53 | ), 54 | parse: $.all( 55 | $.setLocal('checked', $.pref('accept-first-mouse')) 56 | ) 57 | }, { 58 | type: 'separator' 59 | }, { 60 | type: 'checkbox', 61 | label: 'Show in &Tray', 62 | allow: platform.isNonDarwin, 63 | click: $.all( 64 | $.showInTray($.key('checked')), 65 | $.setPref('show-tray', $.key('checked')) 66 | ), 67 | parse: $.all( 68 | $.setLocal('checked', $.pref('show-tray')), 69 | ) 70 | }, { 71 | id: 'show-tray', 72 | type: 'checkbox', 73 | label: 'Show in Menu Bar', 74 | allow: platform.isDarwin, 75 | click: $.all( 76 | $.showInTray($.key('checked')), 77 | $.updateSibling('show-dock', 'enabled', $.key('checked')), 78 | $.updateMenuItem('tray', 'show-tray')($.key('checked'))((checked) => $.all( 79 | $.setLocal('checked', $.val(checked)), 80 | $.updateSibling('show-dock', 'enabled', $.val(checked)) 81 | )), 82 | $.setPref('show-tray', $.key('checked')) 83 | ), 84 | parse: $.all( 85 | $.setLocal('checked', $.pref('show-tray')), 86 | $.setLocal('enabled', $.pref('show-dock')) 87 | ) 88 | }, { 89 | id: 'show-dock', 90 | type: 'checkbox', 91 | label: 'Show in Dock', 92 | allow: platform.isDarwin, 93 | click: $.all( 94 | $.showInDock($.key('checked')), 95 | $.updateSibling('show-tray', 'enabled', $.key('checked')), 96 | $.updateMenuItem('tray', 'show-dock')($.key('checked'))((checked) => $.all( 97 | $.setLocal('checked', $.val(checked)), 98 | $.updateSibling('show-tray', 'enabled', $.val(checked)) 99 | )), 100 | $.setPref('show-dock', $.key('checked')) 101 | ), 102 | parse: $.all( 103 | $.setLocal('checked', $.pref('show-dock')), 104 | $.setLocal('enabled', $.pref('show-tray')), 105 | $.showInDock($.key('checked')) 106 | ) 107 | }, { 108 | type: 'separator', 109 | allow: platform.isDarwin 110 | }, { 111 | role: 'minimize', 112 | allow: platform.isDarwin 113 | }, { 114 | role: 'zoom', 115 | allow: platform.isDarwin 116 | }, { 117 | role: 'close', 118 | allow: platform.isDarwin 119 | }] 120 | }; 121 | -------------------------------------------------------------------------------- /src/scripts/browser/menus/templates/tray.js: -------------------------------------------------------------------------------- 1 | import platform from 'common/utils/platform'; 2 | import $ from 'browser/menus/expressions'; 3 | 4 | export default [{ 5 | label: 'Reset Window', 6 | click: $.resetWindow() 7 | }, { 8 | type: 'separator' 9 | }, { 10 | id: 'show-tray', 11 | type: 'checkbox', 12 | label: 'Show in Menu Bar', 13 | allow: platform.isDarwin, 14 | click: $.all( 15 | $.showInTray($.key('checked')), 16 | $.updateSibling('show-dock', 'enabled', $.key('checked')), 17 | $.updateMenuItem('main', 'show-tray')($.key('checked'))((checked) => $.all( 18 | $.setLocal('checked', $.val(checked)), 19 | $.updateSibling('show-dock', 'enabled', $.val(checked)) 20 | )), 21 | $.setPref('show-tray', $.key('checked')) 22 | ), 23 | parse: $.all( 24 | $.setLocal('checked', $.pref('show-tray')), 25 | $.setLocal('enabled', $.pref('show-dock')) 26 | ) 27 | }, { 28 | id: 'show-dock', 29 | type: 'checkbox', 30 | label: 'Show in Dock', 31 | allow: platform.isDarwin, 32 | click: $.all( 33 | $.showInDock($.key('checked')), 34 | $.updateSibling('show-tray', 'enabled', $.key('checked')), 35 | $.updateMenuItem('main', 'show-dock')($.key('checked'))((checked) => $.all( 36 | $.setLocal('checked', $.val(checked)), 37 | $.updateSibling('show-tray', 'enabled', $.val(checked)) 38 | )), 39 | $.setPref('show-dock', $.key('checked')) 40 | ), 41 | parse: $.all( 42 | $.setLocal('checked', $.pref('show-dock')), 43 | $.setLocal('enabled', $.pref('show-tray')) 44 | ) 45 | }, { 46 | type: 'separator', 47 | allow: platform.isDarwin 48 | }, { 49 | label: 'Show ' + global.manifest.productName, 50 | click: $.showWindow() 51 | }, { 52 | label: 'Quit ' + global.manifest.productName, 53 | click: $.appQuit() 54 | }]; 55 | -------------------------------------------------------------------------------- /src/scripts/browser/menus/tray.js: -------------------------------------------------------------------------------- 1 | import {parseTemplate} from 'browser/menus/utils'; 2 | 3 | export default function () { 4 | const template = require('browser/menus/templates/tray').default; 5 | return parseTemplate(template, null); 6 | } 7 | -------------------------------------------------------------------------------- /src/scripts/browser/menus/utils.js: -------------------------------------------------------------------------------- 1 | import {Menu} from 'electron'; 2 | 3 | export function parseTemplate (menu, parent) { 4 | return menu.filter((item) => { 5 | // Filter 6 | if (item.allow !== undefined && !item.allow) { 7 | return false; 8 | } 9 | 10 | // Run the parse-time expression 11 | if (item.parse) { 12 | item.parse.call(parent, item); 13 | } 14 | 15 | // Clean up 16 | delete item.parse; 17 | delete item.allow; 18 | 19 | // Parse submenu items 20 | if (Array.isArray(item.submenu)) { 21 | item.submenu = parseTemplate(item.submenu, item); 22 | } 23 | 24 | return true; 25 | }); 26 | } 27 | 28 | export function findItemById (submenu, id) { 29 | for (let item of submenu) { 30 | if (item.id === id) { 31 | return item; 32 | } 33 | if (item.submenu) { 34 | const subItem = findItemById(item.submenu.items, id); 35 | if (subItem) { 36 | return subItem; 37 | } 38 | } 39 | } 40 | return null; 41 | } 42 | 43 | export function findMenu (menuType) { 44 | switch (menuType) { 45 | case 'main': 46 | return Menu.getApplicationMenu(); 47 | case 'tray': 48 | return global.application.trayManager.menu; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/scripts/browser/services/piwik.js: -------------------------------------------------------------------------------- 1 | import prefs from 'browser/utils/prefs'; 2 | 3 | function send (name, ...args) { 4 | const trackAnalytics = prefs.get('analytics-track'); 5 | if (!trackAnalytics) { 6 | return; 7 | } 8 | if (global.ready) { 9 | const browserWindow = global.application.mainWindowManager.window; 10 | if (browserWindow) { 11 | browserWindow.webContents.send('track-analytics', name, args); 12 | } 13 | } 14 | } 15 | 16 | function bind (name) { 17 | return send.bind(null, name); 18 | } 19 | 20 | export function getTracker () { 21 | return { 22 | trackEvent: bind('trackEvent') 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /src/scripts/browser/services/sentry.js: -------------------------------------------------------------------------------- 1 | import raven from 'raven'; 2 | 3 | export function getClient () { 4 | return new raven.Client(global.manifest.sentry.dsn, { 5 | release: global.manifest.version, 6 | name: global.manifest.productName 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /src/scripts/browser/utils/file-logger.js: -------------------------------------------------------------------------------- 1 | import stripAnsi from 'strip-ansi'; 2 | import fs from 'fs-extra-promise'; 3 | import {app} from 'electron'; 4 | import util from 'util'; 5 | import path from 'path'; 6 | import os from 'os'; 7 | 8 | let fileLogStream = null; 9 | let fileLogIsGettingReady = false; 10 | let fileLogIsReady = false; 11 | 12 | function isFileLogEnabled () { 13 | return global.options.debug && !global.options.mas; 14 | } 15 | 16 | function initFileLogging () { 17 | if (fileLogIsGettingReady) { 18 | return; 19 | } 20 | fileLogIsGettingReady = true; 21 | 22 | try { 23 | const fileLogsDir = path.join(app.getPath('userData'), 'logs'); 24 | fs.mkdirsSync(fileLogsDir); 25 | 26 | const fileLogPath = path.join(fileLogsDir, Date.now() + '.txt'); 27 | fileLogStream = fs.createWriteStream(null, { 28 | fd: fs.openSync(fileLogPath, 'a') 29 | }); 30 | global.__debug_file_log_path = fileLogPath; 31 | 32 | process.on('exit', (code) => { 33 | fileLogStream.end('process exited with code ' + code + os.EOL); 34 | fileLogStream = null; 35 | }); 36 | 37 | if (global.options.consoleLogs) { 38 | console.log(`saving logs to "${fileLogPath}"`); 39 | } 40 | 41 | fileLogIsReady = true; 42 | fileLogIsGettingReady = false; 43 | } catch (err) { 44 | fileLogIsGettingReady = false; 45 | if (global.options.consoleLogs) { 46 | console.error('logger error:', err); 47 | } 48 | } 49 | } 50 | 51 | export function writeLog () { 52 | if (isFileLogEnabled() && !fileLogIsReady) { 53 | initFileLogging(); 54 | } 55 | if (fileLogStream) { 56 | fileLogStream.write(stripAnsi(util.format(...arguments)) + os.EOL); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/scripts/browser/utils/logger.js: -------------------------------------------------------------------------------- 1 | import colors from 'colors/safe'; 2 | 3 | function getCleanISODate () { 4 | return new Date().toISOString().replace(/[TZ]/g, ' ').trim(); 5 | } 6 | 7 | export function printDebug () { 8 | if (global.options.consoleLogs) { 9 | console.log(...arguments); 10 | } 11 | const fileLogger = require('browser/utils/file-logger'); 12 | fileLogger.writeLog(`DEBUG [${getCleanISODate()}]`, ...arguments); 13 | } 14 | 15 | export function printError (namespace, isFatal, err) { 16 | const errorPrefix = `${isFatal ? 'FATAL' : 'ERROR'} [${getCleanISODate()}] ${namespace}:`; 17 | if (global.options.consoleLogs) { 18 | if (isFatal) { 19 | console.error(colors.white.bold.bgMagenta(errorPrefix), err); 20 | } else { 21 | console.error(colors.white.bold.bgRed(errorPrefix), err); 22 | } 23 | } 24 | const fileLogger = require('browser/utils/file-logger'); 25 | fileLogger.writeLog(errorPrefix, err); 26 | } 27 | -------------------------------------------------------------------------------- /src/scripts/browser/utils/prefs-defaults.js: -------------------------------------------------------------------------------- 1 | import {app} from 'electron'; 2 | 3 | import {getAvailableDictionaries} from 'common/utils/spellchecker'; 4 | import platform from 'common/utils/platform'; 5 | 6 | let availableLanguages = null; 7 | const defaults = { 8 | 'analytics-track': true, 9 | 'analytics-uid': null, 10 | 'launch-startup': false, 11 | 'launch-startup-hidden': true, 12 | 'launch-quit': false, 13 | 'links-in-browser': true, 14 | 'switch-workplace': false, 15 | 'block-seen-typing': false, 16 | 'close-with-esc': false, 17 | 'quit-behaviour-taught': false, 18 | 'notify-app-updated': false, 19 | 'show-notifications-badge': true, 20 | 'show-tray': platform.isWindows, 21 | 'show-dock': true, 22 | 'auto-hide-menubar': false, 23 | 'sidebar-auto-hide': false, 24 | 'spell-checker-check': false, 25 | 'spell-checker-auto-correct': false, 26 | 'spell-checker-language': defaultSpellCheckerLanguage, 27 | 'theme': 'default', 28 | 'updates-auto-check': true, 29 | 'updates-channel': global.manifest.versionChannel, 30 | 'window-bounds': { 31 | width: 920, 32 | height: 610 33 | }, 34 | 'window-full-screen': false, 35 | 'accept-first-mouse': false, 36 | 'zoom-level': 0 37 | }; 38 | 39 | function get (key) { 40 | if (key === 'spell-checker-language') { 41 | const valueFn = defaults[key]; 42 | if (typeof valueFn === 'function') { 43 | if (global.ready) { 44 | defaults[key] = valueFn(); 45 | return defaults[key]; 46 | } else { 47 | return valueFn(); 48 | } 49 | } 50 | } 51 | 52 | return defaults[key]; 53 | } 54 | 55 | function defaultSpellCheckerLanguage () { 56 | let defaultLanguage = null; 57 | 58 | if (!availableLanguages) { 59 | availableLanguages = getAvailableDictionaries(); 60 | } 61 | 62 | // Try to get it from app 63 | if (global.ready) { 64 | defaultLanguage = app.getLocale(); 65 | if (typeof defaultLanguage === 'string') { 66 | defaultLanguage = defaultLanguage.replace('-', '_'); 67 | defaultLanguage = validateLanguage(defaultLanguage); 68 | if (defaultLanguage) { 69 | return defaultLanguage; 70 | } 71 | } 72 | defaultLanguage = null; 73 | } 74 | 75 | // Try to get it from env 76 | if (typeof process.env.LANG === 'string') { 77 | defaultLanguage = process.env.LANG.split('.')[0]; 78 | defaultLanguage = defaultLanguage.replace('-', '_'); 79 | defaultLanguage = validateLanguage(defaultLanguage); 80 | if (defaultLanguage) { 81 | return defaultLanguage; 82 | } 83 | defaultLanguage = null; 84 | } 85 | 86 | // Try to use en 87 | const langEn = validateLanguage('en'); 88 | if (langEn) { 89 | return langEn; 90 | } 91 | 92 | // Try to use en-us 93 | const langEnUs = validateLanguage('en_US'); 94 | if (langEnUs) { 95 | return langEnUs; 96 | } 97 | 98 | // Try to use the first available language 99 | if (availableLanguages.length) { 100 | return availableLanguages[0]; 101 | } 102 | 103 | // Use the default 104 | return 'en_US'; 105 | } 106 | 107 | function validateLanguage (lang) { 108 | if (availableLanguages.includes(lang)) { 109 | return lang; 110 | } else { 111 | lang = lang.split('_')[0]; 112 | if (availableLanguages.includes(lang)) { 113 | return lang; 114 | } 115 | } 116 | return null; 117 | } 118 | 119 | export default { 120 | get 121 | }; 122 | -------------------------------------------------------------------------------- /src/scripts/browser/utils/prefs.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra-promise'; 2 | import {app} from 'electron'; 3 | import path from 'path'; 4 | 5 | import defaults from 'browser/utils/prefs-defaults'; 6 | 7 | let prefsPath = null; 8 | let data = null; 9 | 10 | function ensureDataLoaded () { 11 | if (!prefsPath) { 12 | prefsPath = path.join(app.getPath('userData'), 'prefs.json'); 13 | } 14 | 15 | if (!data) { 16 | try { 17 | data = fs.readJsonSync(prefsPath) || {}; 18 | log('prefs data restored'); 19 | } catch (err) { 20 | logError(err, true); 21 | data = {}; 22 | } 23 | } 24 | } 25 | 26 | /** 27 | * Save the given (key, value) pair asynchronously. 28 | * Returns immediately and logs errors. 29 | */ 30 | function set (key, value) { 31 | ensureDataLoaded(); 32 | data[key] = value; 33 | 34 | fs.outputJson(prefsPath, data, function (err) { 35 | if (err) { 36 | logError(err, true); 37 | } else { 38 | log('saved', key, '=', JSON.stringify(value)); 39 | } 40 | }); 41 | } 42 | 43 | /** 44 | * Save the given (key, value) pair synchronously. 45 | * Returns immediately and logs errors. 46 | */ 47 | function setSync (key, value) { 48 | ensureDataLoaded(); 49 | data[key] = value; 50 | 51 | try { 52 | fs.outputJsonSync(prefsPath, data); 53 | log('saved', key, '=', JSON.stringify(value)); 54 | } catch (err) { 55 | logError(err, true); 56 | } 57 | } 58 | 59 | /** 60 | * Retrieve the value synchronously. 61 | */ 62 | function get (key) { 63 | ensureDataLoaded(); 64 | const value = data[key]; 65 | if (value === undefined) { 66 | const defaultValue = getDefault(key); 67 | if (defaultValue === undefined) { 68 | logFatal(new Error('default value for key ' + key + ' is undefined')); 69 | } 70 | return defaultValue; 71 | } 72 | return value; 73 | } 74 | 75 | /** 76 | * Retrieve all the prefs. 77 | */ 78 | function getAll () { 79 | ensureDataLoaded(); 80 | return data; 81 | } 82 | 83 | /** 84 | * Retrieve the default value. 85 | */ 86 | function getDefault (key) { 87 | return defaults.get(key); 88 | } 89 | 90 | /** 91 | * Remove the given key asynchronously. 92 | */ 93 | function unset (key) { 94 | ensureDataLoaded(); 95 | delete data[key]; 96 | 97 | fs.outputJson(prefsPath, data, function (err) { 98 | if (err) { 99 | logError(err, true); 100 | } else { 101 | log('unset', key); 102 | } 103 | }); 104 | } 105 | 106 | /** 107 | * Remove the given key synchronously. 108 | */ 109 | function unsetSync (key) { 110 | ensureDataLoaded(); 111 | delete data[key]; 112 | 113 | try { 114 | fs.outputJsonSync(prefsPath, data); 115 | log('unset', key); 116 | } catch (err) { 117 | logError(err, true); 118 | } 119 | } 120 | 121 | /** 122 | * Remove all the keys and their values. 123 | */ 124 | function clear () { 125 | ensureDataLoaded(); 126 | data = {}; 127 | 128 | fs.outputJson(prefsPath, data, function (err) { 129 | if (err) { 130 | logError(err, true); 131 | } else { 132 | log('all keys cleared'); 133 | } 134 | }); 135 | } 136 | 137 | export default { 138 | set, 139 | setSync, 140 | get, 141 | getAll, 142 | getDefault, 143 | unset, 144 | unsetSync, 145 | clear 146 | }; 147 | -------------------------------------------------------------------------------- /src/scripts/browser/utils/repl.js: -------------------------------------------------------------------------------- 1 | import repl from 'repl'; 2 | import net from 'net'; 3 | 4 | import logger from 'common/utils/logger'; 5 | 6 | /** 7 | * Create the server and start listening on the given port. 8 | */ 9 | export function createServer (port) { 10 | log('listening for REPL connections on port', port); 11 | net.createServer((socket) => { 12 | const r = repl.start({ 13 | prompt: 'browser@' + global.manifest.name + '> ', 14 | input: socket, 15 | output: socket, 16 | terminal: true 17 | }); 18 | 19 | r.on('exit', () => { 20 | socket.end(); 21 | }); 22 | 23 | // Bridge loggers 24 | r.context.log = logger.debugLogger('repl'); 25 | r.context.logError = logger.errorLogger('repl', false); 26 | r.context.logFatal = logger.errorLogger('repl', true); 27 | }).listen(port); 28 | } 29 | -------------------------------------------------------------------------------- /src/scripts/browser/utils/request-filter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * URL match patterns to be filteresd. 3 | * @see https://developer.chrome.com/extensions/match_patterns 4 | */ 5 | const URL_PATTERNS = [ 6 | '*://*.facebook.com/*change_read_status*', 7 | '*://*.messenger.com/*change_read_status*', 8 | '*://*.facebook.com/*typ.php*', 9 | '*://*.messenger.com/*typ.php*' 10 | ]; 11 | 12 | /** 13 | * Enable request cancelling of urls matched by pattern list. 14 | */ 15 | export function enable (targetSession) { 16 | targetSession.webRequest.onBeforeRequest({urls: URL_PATTERNS}, (details, callback) => { 17 | log('request filter', 'blocked', details.url); 18 | callback({cancel: true}); 19 | }); 20 | log('request filter', 'enabled', URL_PATTERNS.length, 'entries'); 21 | } 22 | 23 | /** 24 | * Disable request cancelling of urls matched by pattern list. 25 | */ 26 | export function disable (targetSession) { 27 | targetSession.webRequest.onBeforeRequest({urls: URL_PATTERNS}, null); 28 | log('request filter', 'disabled'); 29 | } 30 | 31 | /** 32 | * Setter convenience interface. 33 | */ 34 | export function set (shouldEnable, targetSession) { 35 | if (shouldEnable) { 36 | enable(targetSession); 37 | } else { 38 | disable(targetSession); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/scripts/common/analytics/actions.js: -------------------------------------------------------------------------------- 1 | import keyMirror from 'keymirror'; 2 | 3 | export default keyMirror({ 4 | // Logs 5 | 'Exception': null, 6 | 7 | // Menu 8 | 'Open Link': null, 9 | 'Open Dialog': null 10 | }); 11 | -------------------------------------------------------------------------------- /src/scripts/common/analytics/categories.js: -------------------------------------------------------------------------------- 1 | import keyMirror from 'keymirror'; 2 | 3 | export default keyMirror({ 4 | 'Logs': null, 5 | 'Menu': null 6 | }); 7 | -------------------------------------------------------------------------------- /src/scripts/common/analytics/names.js: -------------------------------------------------------------------------------- 1 | import keyMirror from 'keymirror'; 2 | 3 | export default keyMirror({ 4 | // Logs / Exception 5 | 'Error': null, 6 | 'Fatal Error': null, 7 | 8 | // Menu / Open Link 9 | 'Gitter Chat Link': null, 10 | 'Write Review Link': null, 11 | 'Suggest Feature Link': null, 12 | 'Report Issue Link': null, 13 | 'Contact Developer Email Link': null, 14 | 'Contact Developer Twitter Link': null, 15 | 'Donate PayPal Link': null, 16 | 'Donate Bitcoin Link': null, 17 | 'FAQ Link': null, 18 | 19 | // Menu / Open Dialog 20 | 'Open Dialog': null 21 | }); 22 | -------------------------------------------------------------------------------- /src/scripts/common/bridges/native-notifier.js: -------------------------------------------------------------------------------- 1 | let impl = null; 2 | 3 | switch (process.type) { 4 | case 'browser': 5 | impl = require('browser/bridges/native-notifier').default; 6 | break; 7 | 8 | case 'renderer': 9 | impl = require('electron').remote.require('../browser/bridges/native-notifier').default; 10 | break; 11 | } 12 | 13 | export default impl; 14 | -------------------------------------------------------------------------------- /src/scripts/common/electron/app.js: -------------------------------------------------------------------------------- 1 | let impl = null; 2 | 3 | switch (process.type) { 4 | case 'browser': 5 | impl = require('electron').app; 6 | break; 7 | 8 | case 'renderer': 9 | impl = require('electron').remote.app; 10 | break; 11 | } 12 | 13 | export default impl; 14 | -------------------------------------------------------------------------------- /src/scripts/common/modules/debug.js: -------------------------------------------------------------------------------- 1 | let impl = null; 2 | 3 | switch (process.type) { 4 | case 'browser': 5 | impl = require('debug/node'); 6 | break; 7 | 8 | case 'renderer': 9 | impl = require('debug/node'); 10 | // Fix for colors and formatting 11 | const remoteDebug = require('electron').remote.require('debug'); 12 | impl.useColors = function () { 13 | return remoteDebug.useColors(...arguments); 14 | }; 15 | break; 16 | } 17 | 18 | // Force-enable debug 19 | if (global.options.debug) { 20 | impl.enable(process.env.DEBUG || global.manifest.name + ':*'); 21 | } 22 | 23 | export default impl; 24 | -------------------------------------------------------------------------------- /src/scripts/common/services/piwik.js: -------------------------------------------------------------------------------- 1 | export default require(process.type + '/services/piwik'); 2 | -------------------------------------------------------------------------------- /src/scripts/common/services/sentry.js: -------------------------------------------------------------------------------- 1 | import os from 'os'; 2 | 3 | import prefs from 'common/utils/prefs'; 4 | import {getUserId} from 'common/utils/analytics'; 5 | 6 | const trackAnalytics = prefs.get('analytics-track'); 7 | let client = null; 8 | 9 | if (global.manifest.dev) { 10 | log('sentry disabled (dev mode)'); 11 | } else if (!trackAnalytics) { 12 | log('sentry disabled (analytics disabled)'); 13 | } else { 14 | log('setting up sentry'); 15 | 16 | client = require(process.type + '/services/sentry').getClient(); 17 | 18 | client.setUserContext({ 19 | uid: getUserId() 20 | }); 21 | 22 | client.setExtraContext({ 23 | portable: global.manifest.portable, 24 | buildNum: global.manifest.buildNum, 25 | os_release: os.release(), 26 | versions: { 27 | electron: global.manifest.electronVersion, 28 | app: global.manifest.version 29 | }, 30 | prefs: prefs.getAll() 31 | }); 32 | 33 | client.setTagsContext({ 34 | process_type: 'renderer', 35 | distrib: global.manifest.distrib, 36 | os_platform: os.platform() 37 | }); 38 | } 39 | 40 | export default client; 41 | -------------------------------------------------------------------------------- /src/scripts/common/utils/analytics.js: -------------------------------------------------------------------------------- 1 | import uuid from 'node-uuid'; 2 | 3 | import prefs from 'common/utils/prefs'; 4 | 5 | export function getUserId () { 6 | let uid = prefs.get('analytics-uid'); 7 | 8 | // Generate a new one if it doesn't exist 9 | if (!uid) { 10 | uid = uuid.v4(); 11 | prefs.set('analytics-uid', uid); 12 | } 13 | 14 | return uid; 15 | } 16 | -------------------------------------------------------------------------------- /src/scripts/common/utils/file-paths.js: -------------------------------------------------------------------------------- 1 | import app from 'common/electron/app'; 2 | import path from 'path'; 3 | 4 | /** 5 | * @return the theme's css path 6 | */ 7 | function getThemePath (name) { 8 | return path.join(app.getAppPath(), 'themes', name + '.css'); 9 | } 10 | 11 | /** 12 | * @return the style's css path 13 | */ 14 | function getStylePath (name) { 15 | return path.join(app.getAppPath(), 'styles', name + '.css'); 16 | } 17 | 18 | /** 19 | * @return the image's path 20 | */ 21 | function getImagePath (name) { 22 | return path.join(app.getAppPath(), 'images', name); 23 | } 24 | 25 | /** 26 | * Windows only. 27 | * @return the directory where the app is ran from 28 | */ 29 | function getCustomUserDataPath () { 30 | return path.join(path.dirname(app.getPath('exe')), 'data'); 31 | } 32 | 33 | /** 34 | * Windows only. 35 | * @return the path to Update.exe created by Squirrel.Windows 36 | */ 37 | function getSquirrelUpdateExePath () { 38 | return path.join(path.dirname(app.getPath('exe')), '..', 'Update.exe'); 39 | } 40 | 41 | export default { 42 | getThemePath, 43 | getStylePath, 44 | getImagePath, 45 | getCustomUserDataPath, 46 | getSquirrelUpdateExePath 47 | }; 48 | -------------------------------------------------------------------------------- /src/scripts/common/utils/files.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra-promise'; 2 | import path from 'path'; 3 | 4 | import filePaths from 'common/utils/file-paths'; 5 | 6 | /** 7 | * @return the css of the theme 8 | */ 9 | async function getThemeCss (theme) { 10 | return await fs.readFileAsync(filePaths.getThemePath(theme), 'utf-8'); 11 | } 12 | 13 | /** 14 | * @return the css of the file 15 | */ 16 | async function getStyleCss (style) { 17 | return await fs.readFileAsync(filePaths.getStylePath(style), 'utf-8'); 18 | } 19 | 20 | /** 21 | * @return the list of Hunspell dictionaries available in the given dir 22 | */ 23 | function getDictionariesSync (dirPath) { 24 | if (!fs.statSyncNoException(dirPath)) { 25 | log('dictionaries path does not exist', dirPath); 26 | return []; 27 | } 28 | 29 | const dictionaries = fs.readdirSync(dirPath) 30 | .filter((filename) => path.extname(filename) === '.dic') 31 | .filter((filename) => fs.statSync(path.join(dirPath, filename)).isFile()) 32 | .map((filename) => path.basename(filename, '.dic')); 33 | 34 | log('dictionaries in', dirPath, 'found:', JSON.stringify(dictionaries)); 35 | return dictionaries; 36 | } 37 | 38 | /** 39 | * @return the list of Hunspell dictionaries available in all the given dirs 40 | */ 41 | function getAllDictionariesSync (dirPaths) { 42 | return dirPaths.reduce((acc, dirPath) => { 43 | return acc.concat(getDictionariesSync(dirPath)); 44 | }, []); 45 | } 46 | 47 | /** 48 | * Verify it's not a directory and the app can access it. 49 | * If it's invalid, purge it and write it again. 50 | * If it already exists, it's left untouched. 51 | */ 52 | async function replaceFile (filePath, writePromise) { 53 | try { 54 | await fs.accessAsync(filePath, fs.R_OK | fs.W_OK); 55 | const stats = await fs.lstatAsync(filePath); 56 | if (!stats.isFile()) { 57 | throw new Error(); 58 | } 59 | } catch (err) { 60 | // err ignored 61 | // no access / does not exist 62 | try { 63 | await fs.removeAsync(filePath); 64 | } catch (err2) { 65 | // err2 ignored 66 | } 67 | await writePromise(); 68 | } 69 | } 70 | 71 | /** 72 | * Check if the path exists, can be accessed and is a file. 73 | */ 74 | async function isFileExists (filePath) { 75 | try { 76 | await fs.accessAsync(filePath, fs.R_OK | fs.W_OK); 77 | const stats = await fs.lstatAsync(filePath); 78 | return stats.isFile(); 79 | } catch (err) { 80 | return false; 81 | } 82 | } 83 | 84 | export default { 85 | getThemeCss, 86 | getStyleCss, 87 | getDictionariesSync, 88 | getAllDictionariesSync, 89 | replaceFile, 90 | isFileExists 91 | }; 92 | -------------------------------------------------------------------------------- /src/scripts/common/utils/graphics.js: -------------------------------------------------------------------------------- 1 | function createBadgeDataUrl (text) { 2 | const canvas = document.createElement('canvas'); 3 | canvas.height = 140; 4 | canvas.width = 140; 5 | 6 | const context = canvas.getContext('2d'); 7 | context.fillStyle = 'red'; 8 | context.beginPath(); 9 | context.ellipse(70, 70, 70, 70, 0, 0, 2 * Math.PI); 10 | context.fill(); 11 | context.textAlign = 'center'; 12 | context.fillStyle = 'white'; 13 | 14 | if (text.length > 2) { 15 | context.font = 'bold 65px "Segoe UI", sans-serif'; 16 | context.fillText('' + text, 70, 95); 17 | } else if (text.length > 1) { 18 | context.font = 'bold 85px "Segoe UI", sans-serif'; 19 | context.fillText('' + text, 70, 100); 20 | } else { 21 | context.font = 'bold 100px "Segoe UI", sans-serif'; 22 | context.fillText('' + text, 70, 105); 23 | } 24 | 25 | return canvas.toDataURL(); 26 | } 27 | 28 | export default { 29 | createBadgeDataUrl 30 | }; 31 | -------------------------------------------------------------------------------- /src/scripts/common/utils/logger-browser.js: -------------------------------------------------------------------------------- 1 | let impl = null; 2 | 3 | switch (process.type) { 4 | case 'browser': 5 | impl = require('browser/utils/logger'); 6 | break; 7 | 8 | case 'renderer': 9 | impl = require('electron').remote.require('../browser/utils/logger'); 10 | break; 11 | } 12 | 13 | export default impl; 14 | -------------------------------------------------------------------------------- /src/scripts/common/utils/logger.js: -------------------------------------------------------------------------------- 1 | import util from 'util'; 2 | import path from 'path'; 3 | 4 | function namespaceOfFile (filename) { 5 | const app = require('common/electron/app').default; 6 | const appPath = path.join(app.getAppPath(), 'scripts') + path.sep; 7 | 8 | let name = filename 9 | .replace(appPath, '') 10 | .replace(/\\/g, '/') 11 | .replace('.js', ''); 12 | 13 | if (name.startsWith('common/')) { 14 | name += ':' + process.type; 15 | } 16 | 17 | // replace slashes with semicolons 18 | name = name.replace(/\//g, ':'); 19 | 20 | return global.manifest.name + ':' + name; 21 | } 22 | 23 | function reportToSentry (namespace, isFatal, err) { 24 | const sentry = require('common/services/sentry').default; 25 | if (sentry) { 26 | console.log('reporting to sentry:', err); 27 | sentry.captureException(err, { 28 | level: isFatal ? 'fatal' : 'error', 29 | extra: { 30 | trace: new Error().stack 31 | }, 32 | tags: { 33 | namespace 34 | } 35 | }, function (result) { 36 | console.log('reported to sentry:', result); 37 | }); 38 | } 39 | } 40 | 41 | export function debugLogger (filename) { 42 | let logger = null; 43 | let browserLogger = null; 44 | return function () { 45 | if (!logger) { 46 | const debug = require('common/modules/debug').default; 47 | logger = debug(namespaceOfFile(filename)); 48 | } 49 | if (!browserLogger) { 50 | browserLogger = require('common/utils/logger-browser').default; 51 | } 52 | logger.log = browserLogger.printDebug; 53 | logger(util.format(...arguments)); 54 | }; 55 | } 56 | 57 | export function errorLogger (filename, isFatal) { 58 | let namespace = null; 59 | let browserLogger = null; 60 | return function (err, skipReporting = false) { 61 | if (!namespace) { 62 | namespace = namespaceOfFile(filename); 63 | } 64 | 65 | if (!(err instanceof Error)) { 66 | if (global.options.dev) { 67 | const fnName = isFatal ? 'logFatal' : 'logError'; 68 | throw new Error('the first parameter to ' + fnName + ' must be an Error'); 69 | } else { 70 | err = new Error(err); 71 | } 72 | } 73 | 74 | if (!browserLogger) { 75 | browserLogger = require('common/utils/logger-browser').default; 76 | } 77 | browserLogger.printError(namespace, isFatal, err.stack); 78 | 79 | if (!skipReporting && !global.options.debug) { 80 | reportToSentry(namespace, isFatal, err); 81 | } 82 | }; 83 | } 84 | -------------------------------------------------------------------------------- /src/scripts/common/utils/platform.js: -------------------------------------------------------------------------------- 1 | export default { 2 | isDarwin: process.platform === 'darwin', 3 | isNonDarwin: process.platform !== 'darwin', 4 | isWindows: process.platform === 'win32', 5 | isLinux: process.platform === 'linux', 6 | isWindows7: process.platform === 'win32' && process.type === 'renderer' && 7 | !!window.navigator.userAgent.match(/(Windows 7|Windows NT 6\.1)/) 8 | }; 9 | -------------------------------------------------------------------------------- /src/scripts/common/utils/prefs.js: -------------------------------------------------------------------------------- 1 | let impl = null; 2 | 3 | switch (process.type) { 4 | case 'browser': 5 | impl = require('browser/utils/prefs').default; 6 | break; 7 | 8 | case 'renderer': 9 | impl = require('electron').remote.require('../browser/utils/prefs').default; 10 | break; 11 | } 12 | 13 | export default impl; 14 | -------------------------------------------------------------------------------- /src/scripts/common/utils/spellchecker.js: -------------------------------------------------------------------------------- 1 | import SpellChecker from 'spellchecker'; 2 | import path from 'path'; 3 | import fs from 'fs'; 4 | 5 | import app from 'common/electron/app'; 6 | import platform from 'common/utils/platform'; 7 | import files from 'common/utils/files'; 8 | 9 | let hunspellDictionarySearchPaths = null; 10 | let availableDictionaries = null; 11 | 12 | export function getDictionarySearchPaths () { 13 | if (hunspellDictionarySearchPaths) { 14 | return hunspellDictionarySearchPaths; 15 | } 16 | 17 | let searchPaths = [ 18 | path.join(app.getAppPath(), 'dicts'), 19 | path.join(app.getAppPath(), 'node_modules', 'spellchecker', 'vendor', 'hunspell_dictionaries') 20 | ]; 21 | 22 | // Special case being in an asar archive 23 | searchPaths = searchPaths.map((searchPath) => { 24 | if (searchPath.includes('.asar' + path.sep)) { 25 | const unpacked = searchPath.replace('.asar' + path.sep, '.asar.unpacked' + path.sep); 26 | if (fs.statSyncNoException(unpacked)) { 27 | return unpacked; 28 | } 29 | } 30 | return searchPath; 31 | }); 32 | 33 | if (platform.isLinux) { 34 | searchPaths = searchPaths.concat([ 35 | '/usr/share/hunspell', 36 | '/usr/share/myspell', 37 | '/usr/share/myspell/dicts', 38 | '/Library/Spelling' 39 | ]); 40 | } 41 | 42 | hunspellDictionarySearchPaths = searchPaths; 43 | return hunspellDictionarySearchPaths; 44 | } 45 | 46 | export function getAvailableDictionaries () { 47 | if (availableDictionaries) { 48 | return availableDictionaries; 49 | } 50 | 51 | availableDictionaries = [] 52 | .concat(getSpellCheckerDictionaries()) 53 | .concat(getHunspellDictionaries()); 54 | 55 | // Remove duplicates 56 | availableDictionaries = Array.from(new Set(availableDictionaries)); 57 | 58 | return availableDictionaries; 59 | } 60 | 61 | function getSpellCheckerDictionaries () { 62 | return SpellChecker.getAvailableDictionaries(); 63 | } 64 | 65 | function getHunspellDictionaries () { 66 | try { 67 | const searchPaths = getDictionarySearchPaths(); 68 | return files.getAllDictionariesSync(searchPaths); 69 | } catch (err) { 70 | logError(err); 71 | } 72 | return []; 73 | } 74 | 75 | export function getDictionaryPath (langCode) { 76 | if (getSpellCheckerDictionaries().includes(langCode)) { 77 | return null; 78 | } 79 | 80 | let searchPaths = getDictionarySearchPaths(); 81 | searchPaths = searchPaths.map((searchPath) => { 82 | return [ 83 | path.join(searchPath, langCode.replace('-', '_') + '.dic'), 84 | path.join(searchPath, langCode.replace('_', '-') + '.dic') 85 | ]; 86 | }); 87 | 88 | // Flatten and remove duplicates 89 | searchPaths = [].concat.apply([], searchPaths); 90 | searchPaths = Array.from(new Set(searchPaths)); 91 | 92 | return searchPaths.find((searchPath) => fs.statSyncNoException(searchPath)); 93 | } 94 | -------------------------------------------------------------------------------- /src/scripts/common/utils/urls.js: -------------------------------------------------------------------------------- 1 | import url from 'url'; 2 | 3 | /** 4 | * Skip opening the link through Facebook. 5 | * It converts [facebook|messenger].com/l.php?u= to . 6 | */ 7 | function skipFacebookRedirect (urlLink) { 8 | const parsed = url.parse(urlLink, true); 9 | log('skip facebook redirect, checking', urlLink); 10 | 11 | if (!parsed || !parsed.hostname || !parsed.pathname) { 12 | return urlLink; 13 | } 14 | 15 | const hostMatches = parsed.hostname.includes('facebook.com') || parsed.hostname.includes('messenger.com'); 16 | const pathMatches = parsed.pathname.includes('/l.php'); 17 | 18 | if (hostMatches && pathMatches && parsed.query.u) { 19 | urlLink = parsed.query.u; 20 | } 21 | 22 | return urlLink; 23 | } 24 | 25 | /** 26 | * Check if the given url is a downloadable file. Currently only detects Facebook CDN urls. 27 | */ 28 | function isDownloadUrl (urlLink) { 29 | const isDlUrl = urlLink.startsWith('https://cdn.fbsbx.com') && urlLink.endsWith('&dl=1'); 30 | log('link is download url', urlLink, isDlUrl); 31 | return isDlUrl; 32 | } 33 | 34 | export default { 35 | skipFacebookRedirect, 36 | isDownloadUrl 37 | }; 38 | -------------------------------------------------------------------------------- /src/scripts/renderer/components/keymap.js: -------------------------------------------------------------------------------- 1 | import {remote} from 'electron'; 2 | import Mousetrap from 'mousetrap'; 3 | 4 | import prefs from 'common/utils/prefs'; 5 | import webView from 'renderer/webview'; 6 | 7 | log('binding keyboard shortcuts'); 8 | 9 | function bindSwitchConversation (keys, direction) { 10 | Mousetrap.bind(keys, function () { 11 | log(direction, 'conversation'); 12 | if (direction === 'next') { 13 | webView.send('switch-conversation-next'); 14 | } else { 15 | webView.send('switch-conversation-previous'); 16 | } 17 | return false; 18 | }); 19 | } 20 | 21 | // Previous chat 22 | bindSwitchConversation(['ctrl+shift+tab'], 'previous'); 23 | 24 | // Next chat 25 | bindSwitchConversation(['ctrl+tab'], 'next'); 26 | 27 | // Close with Esc 28 | Mousetrap.bind('esc', function () { 29 | const enabled = prefs.get('close-with-esc'); 30 | log('close with esc shortcut, enabled:', enabled); 31 | if (enabled) { 32 | const mwm = remote.getGlobal('application').mainWindowManager; 33 | if (mwm) { 34 | mwm.window.close(); 35 | } 36 | } 37 | return enabled; 38 | }); 39 | -------------------------------------------------------------------------------- /src/scripts/renderer/index.js: -------------------------------------------------------------------------------- 1 | const appPath = require('electron').remote.app.getAppPath(); 2 | const initPath = require('path').join(appPath, 'scripts', 'renderer', 'init.js'); 3 | require(initPath).inject('renderer'); 4 | 5 | require('renderer/webview'); 6 | require('renderer/components/keymap'); 7 | -------------------------------------------------------------------------------- /src/scripts/renderer/init.js: -------------------------------------------------------------------------------- 1 | import {remote} from 'electron'; 2 | import path from 'path'; 3 | 4 | export function inject (scope) { 5 | global.manifest = remote.getGlobal('manifest'); 6 | global.options = remote.getGlobal('options'); 7 | 8 | const appPath = remote.app.getAppPath(); 9 | const {addPath} = require(path.join(appPath, 'node_modules', 'app-module-path')); 10 | 11 | addPath(path.join(appPath, 'scripts')); 12 | addPath(path.join(appPath, 'node_modules')); 13 | 14 | // Add loggers to be used in the console 15 | const logger = require('common/utils/logger'); 16 | window.log = logger.debugLogger('console:' + scope); 17 | window.logError = logger.errorLogger('console:' + scope, false); 18 | window.logFatal = logger.errorLogger('console:' + scope, true); 19 | 20 | // Handle errors 21 | window.onerror = function (message, source, lineno, colno, error) { 22 | window.logError(error instanceof Error ? error : new Error(error || message)); 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /src/scripts/renderer/preload/index.js: -------------------------------------------------------------------------------- 1 | const appPath = require('electron').remote.app.getAppPath(); 2 | const initPath = require('path').join(appPath, 'scripts', 'renderer', 'init.js'); 3 | require(initPath).inject('webview'); 4 | 5 | require('renderer/preload/events'); 6 | require('renderer/preload/notification'); 7 | -------------------------------------------------------------------------------- /src/scripts/renderer/preload/notification.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'events'; 2 | import {remote} from 'electron'; 3 | 4 | import platform from 'common/utils/platform'; 5 | 6 | const nativeNotifier = remote.require('common/bridges/native-notifier').default; 7 | const mainWindowManager = remote.getGlobal('application').mainWindowManager; 8 | 9 | // Extend the default notification API 10 | window.Notification = (function (Html5Notification) { 11 | log('extending HTML5 Notification'); 12 | 13 | const Notification = function (title, options) { 14 | if (!nativeNotifier.isImplemented || (!platform.isDarwin && !platform.isWindows7)) { 15 | log('showing html5 notification', title, options); 16 | const notification = new Html5Notification(title, options); 17 | 18 | // Add click listener to focus the app 19 | notification.addEventListener('click', function () { 20 | mainWindowManager.showOrCreate(); 21 | }); 22 | 23 | return notification; 24 | } 25 | 26 | log('showing native notification'); 27 | const nativeOptions = Object.assign({}, options, { 28 | canReply: options.canReply !== false, 29 | title 30 | }); 31 | 32 | // HTML5-like event emitter to be returned 33 | const result = Object.assign(new EventEmitter(), nativeOptions); 34 | 35 | // Add a close handler 36 | result.close = function () { 37 | if (result.__data) { 38 | nativeNotifier.removeNotification(result.__data.identifier); 39 | } else { 40 | logFatal(new Error('tried to close notification with falsy __data')); 41 | } 42 | }; 43 | 44 | // Set the click handler 45 | nativeOptions.onClick = function (payload) { 46 | log('notification clicked', JSON.stringify(payload)); 47 | result.emit('click'); 48 | 49 | // Call additional handlers 50 | if (result.onclick) { 51 | result.onclick(); 52 | } 53 | 54 | // Send the reply 55 | if (payload && payload.response) { 56 | log('sending reply', payload.response); 57 | setTimeout(function () { 58 | if (typeReply(payload.response)) { 59 | sendReply(); 60 | } 61 | }, 50); 62 | } else { 63 | mainWindowManager.showOrCreate(); 64 | } 65 | }; 66 | 67 | // Set the creation callback 68 | nativeOptions.onCreate = function (data) { 69 | result.__data = data; 70 | }; 71 | 72 | // Fire the notification 73 | nativeNotifier.fireNotification(nativeOptions); 74 | return result; 75 | }; 76 | 77 | return Object.assign(Notification, Html5Notification); 78 | })(window.Notification); 79 | 80 | function typeReply (replyText, elem) { 81 | const event = document.createEvent('TextEvent'); 82 | event.initTextEvent('textInput', true, true, window, replyText, 0, 'en-US'); 83 | const inputField = document.querySelector('[contenteditable="true"]'); 84 | if (inputField) { 85 | inputField.focus(); 86 | return inputField.dispatchEvent(event); 87 | } 88 | return false; 89 | } 90 | 91 | function sendReply () { 92 | const event = new window.MouseEvent('click', { 93 | view: window, 94 | bubbles: true, 95 | cancelable: true 96 | }); 97 | const sendButton = document.querySelector('[role="region"] a[aria-label][href="#"]'); 98 | if (sendButton) { 99 | sendButton.dispatchEvent(event); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/scripts/renderer/services/piwik.js: -------------------------------------------------------------------------------- 1 | import prefs from 'common/utils/prefs'; 2 | import {getUserId} from 'common/utils/analytics'; 3 | 4 | const activeTheme = prefs.get('theme'); 5 | const activeSpellCheckerLang = prefs.get('spell-checker-language'); 6 | const activeReleaseChannel = prefs.get('updates-channel'); 7 | const trackAnalytics = prefs.get('analytics-track'); 8 | 9 | let piwikTracker = null; 10 | 11 | if (global.manifest.dev) { 12 | log('piwik disabled (dev mode)'); 13 | } else if (!trackAnalytics) { 14 | log('piwik disabled (analytics disabled)'); 15 | } else { 16 | log('setting up piwik'); 17 | 18 | // Configure 19 | window.piwikAsyncInit = function () { 20 | try { 21 | piwikTracker = window.Piwik.getTracker(); 22 | piwikTracker.setDocumentTitle(document.title); 23 | piwikTracker.setTrackerUrl(global.manifest.piwik.serverUrl + '/piwik.php'); 24 | piwikTracker.setCustomDimension(1, global.manifest.version); // Version 25 | piwikTracker.setCustomDimension(2, activeReleaseChannel); // Release Channel 26 | piwikTracker.setCustomDimension(3, global.manifest.distrib); // Distrib 27 | piwikTracker.setCustomDimension(4, activeTheme); // Theme 28 | piwikTracker.setCustomDimension(5, activeSpellCheckerLang); // Spell Checker Language 29 | piwikTracker.setUserId(getUserId()); 30 | piwikTracker.setSiteId(global.manifest.piwik.siteId); 31 | piwikTracker.trackPageView(); 32 | log('piwik analytics instance created'); 33 | } catch (err) { 34 | logFatal(err); 35 | } 36 | }; 37 | 38 | // Load the tracking lib 39 | const scriptElem = document.createElement('script'); 40 | scriptElem.type = 'text/javascript'; 41 | scriptElem.async = true; 42 | scriptElem.defer = true; 43 | scriptElem.src = global.manifest.piwik.serverUrl + '/piwik.js'; 44 | document.head.appendChild(scriptElem); 45 | } 46 | 47 | export function getTracker () { 48 | return piwikTracker; 49 | } 50 | -------------------------------------------------------------------------------- /src/scripts/renderer/services/sentry.js: -------------------------------------------------------------------------------- 1 | import Raven from 'raven-js'; 2 | 3 | export function getClient () { 4 | Raven.config(global.manifest.sentry.dsn, { 5 | release: global.manifest.version, 6 | name: global.manifest.productName, 7 | collectWindowErrors: false, 8 | allowSecretKey: true 9 | }).install(); 10 | return Raven; 11 | } 12 | -------------------------------------------------------------------------------- /src/scripts/renderer/shim-notification/index.js: -------------------------------------------------------------------------------- 1 | const appPath = require('electron').remote.app.getAppPath(); 2 | const initPath = require('path').join(appPath, 'scripts', 'renderer', 'init.js'); 3 | require(initPath).inject('shim-notification'); 4 | 5 | require('renderer/shim-notification/shim'); 6 | -------------------------------------------------------------------------------- /src/scripts/renderer/shim-notification/shim.js: -------------------------------------------------------------------------------- 1 | import {remote} from 'electron'; 2 | import url from 'url'; 3 | 4 | const params = url.parse(window.location.href, true).query; 5 | const nativeNotifier = remote.require('common/bridges/native-notifier').default; 6 | 7 | function closeWindow () { 8 | window.close(); 9 | } 10 | 11 | function onClick () { 12 | nativeNotifier.onClick(params.identifier); 13 | closeWindow(); 14 | } 15 | 16 | function onLoad () { 17 | const timeout = parseInt(params.timeout, 10); 18 | if (!isNaN(timeout)) { 19 | this.setTimeout(closeWindow, timeout); 20 | } 21 | document.addEventListener('keydown', onClick, false); 22 | document.addEventListener('click', onClick); 23 | } 24 | 25 | document.getElementById('title').innerHTML = params.title; 26 | document.getElementById('body').innerHTML = params.body; 27 | document.getElementById('icon').style.backgroundImage = 'url(' + params.icon + ')'; 28 | document.getElementById('footer').innerHTML = params.footer; 29 | 30 | window.addEventListener('load', onLoad, false); 31 | -------------------------------------------------------------------------------- /src/scripts/renderer/webview/events.js: -------------------------------------------------------------------------------- 1 | import {ipcRenderer} from 'electron'; 2 | 3 | import * as piwik from 'renderer/services/piwik'; 4 | import webView from 'renderer/webview'; 5 | 6 | /** 7 | * Change the webview's zoom level. 8 | */ 9 | ipcRenderer.on('zoom-level', function (event, zoomLevel) { 10 | log('setting webview zoom level', zoomLevel); 11 | webView.setZoomLevel(zoomLevel); 12 | }); 13 | 14 | /** 15 | * Forward a message to the webview. 16 | */ 17 | ipcRenderer.on('fwd-webview', function (event, channel, ...args) { 18 | if (typeof webView.isLoading === 'function' && !webView.isLoading()) { 19 | webView.send(channel, ...args); 20 | } else { 21 | const onLoaded = function () { 22 | webView.send(channel, ...args); 23 | webView.removeEventListener('dom-ready', onLoaded); 24 | }; 25 | 26 | webView.addEventListener('dom-ready', onLoaded); 27 | } 28 | }); 29 | 30 | /** 31 | * Call a method of the webview. 32 | */ 33 | ipcRenderer.on('call-webview-method', function (event, method, ...args) { 34 | if (typeof webView[method] === 'function') { 35 | webView[method](...args); 36 | } else { 37 | logError(new Error('method ' + method + ' on webview is not a function')); 38 | } 39 | }); 40 | 41 | /** 42 | * Toggle the dev tools panel of the webview. 43 | */ 44 | ipcRenderer.on('toggle-wv-dev-tools', function (event) { 45 | if (webView.isDevToolsOpened()) { 46 | webView.closeDevTools(); 47 | } else { 48 | webView.openDevTools(); 49 | } 50 | }); 51 | 52 | /** 53 | * Track an analytics event. 54 | */ 55 | ipcRenderer.on('track-analytics', function (event, name, args) { 56 | const tracker = piwik.getTracker(); 57 | if (typeof tracker !== 'function') { 58 | const trackerFn = tracker[name]; 59 | trackerFn(...args); 60 | } else { 61 | logError(new Error('piwik.getTracker is not a function')); 62 | } 63 | }); 64 | -------------------------------------------------------------------------------- /src/scripts/renderer/webview/index.js: -------------------------------------------------------------------------------- 1 | import {remote} from 'electron'; 2 | import path from 'path'; 3 | 4 | const webView = document.getElementById('wv'); 5 | 6 | // Fix preload requiring file:// protocol 7 | let preloadPath = webView.getAttribute('preload'); 8 | preloadPath = 'file://' + path.join(remote.app.getAppPath(), 'html', preloadPath); 9 | webView.setAttribute('preload', preloadPath); 10 | 11 | // Set the user agent and load the app 12 | const wvSrc = require('common/utils/prefs').default.get('switch-workplace') 13 | ? global.manifest.wvUrlWork 14 | : global.manifest.wvUrl; 15 | log('loading', wvSrc); 16 | webView.setAttribute('useragent', navigator.userAgent); 17 | webView.setAttribute('src', wvSrc); 18 | 19 | export default webView; 20 | 21 | require('renderer/webview/events'); 22 | require('renderer/webview/listeners'); 23 | -------------------------------------------------------------------------------- /src/scripts/renderer/webview/listeners.js: -------------------------------------------------------------------------------- 1 | import {shell, remote} from 'electron'; 2 | 3 | import webView from 'renderer/webview'; 4 | import platform from 'common/utils/platform'; 5 | import graphics from 'common/utils/graphics'; 6 | import files from 'common/utils/files'; 7 | import prefs from 'common/utils/prefs'; 8 | import urls from 'common/utils/urls'; 9 | 10 | // Log console messages 11 | webView.addEventListener('console-message', function (event) { 12 | const msg = event.message.replace(/%c/g, ''); 13 | console.log('WV: ' + msg); 14 | log('WV:', msg); 15 | }); 16 | 17 | // Listen for title changes to update the badge 18 | let _delayedRemoveBadge = null; 19 | webView.addEventListener('page-title-updated', function () { 20 | log('webview page-title-updated'); 21 | const matches = /\(([\d]+)\)/.exec(webView.getTitle()); 22 | const parsed = parseInt(matches && matches[1], 10); 23 | const count = isNaN(parsed) || !parsed ? '' : '' + parsed; 24 | let badgeDataUrl = null; 25 | 26 | if (platform.isWindows && count) { 27 | badgeDataUrl = graphics.createBadgeDataUrl(count); 28 | } 29 | 30 | log('notifying window of notif-count', count, !!badgeDataUrl || null); 31 | clearTimeout(_delayedRemoveBadge); 32 | 33 | // clear badge either instantly or after delay 34 | _delayedRemoveBadge = setTimeout(() => { 35 | const mwm = remote.getGlobal('application').mainWindowManager; 36 | if (mwm && typeof mwm.notifCountChanged === 'function') { 37 | mwm.notifCountChanged(count, badgeDataUrl); 38 | } 39 | }, count ? 0 : 1500); 40 | }); 41 | 42 | // Handle url clicks 43 | webView.addEventListener('new-window', function (event) { 44 | log('webview new-window', JSON.stringify(event)); 45 | const url = urls.skipFacebookRedirect(event.url); 46 | event.preventDefault(); 47 | 48 | // download url 49 | if (urls.isDownloadUrl(url)) { 50 | log('on webview new-window, downloading', url); 51 | webView.getWebContents().loadURL(url); 52 | return; 53 | } 54 | 55 | // open it externally (if preference is set) 56 | if (prefs.get('links-in-browser')) { 57 | log('on webview new-window, externally', url); 58 | shell.openExternal(url); 59 | return; 60 | } 61 | 62 | // otherwise open it in a new app window (unless it's an audio/video call) 63 | if (event.frameName !== 'Video Call' || event.url !== 'about:blank') { 64 | const options = { 65 | title: event.frameName || global.manifest.productName, 66 | darkTheme: global.manifest.darkThemes.includes(prefs.get('theme')) 67 | }; 68 | log('on webview new-window, new window', url, options); 69 | const newWindow = new remote.BrowserWindow(options); 70 | newWindow.loadURL(url); 71 | event.newGuest = newWindow; 72 | } 73 | }); 74 | 75 | // Listen for dom-ready 76 | webView.addEventListener('dom-ready', function () { 77 | log('webview dom-ready'); 78 | 79 | // Open dev tools when debugging 80 | const autoLaunchDevTools = window.localStorage.autoLaunchDevTools; 81 | if (autoLaunchDevTools && JSON.parse(autoLaunchDevTools)) { 82 | webView.openDevTools(); 83 | } 84 | 85 | // Restore the default theme 86 | const themeId = prefs.get('theme'); 87 | if (themeId) { 88 | if (global.manifest.themes[themeId]) { 89 | log('restoring theme', themeId); 90 | files.getThemeCss(themeId) 91 | .then((css) => webView.send('apply-theme', css)) 92 | .catch(logError); 93 | } else { 94 | log('invalid theme, unsetting pref'); 95 | prefs.unset('theme'); 96 | } 97 | } 98 | 99 | // Load webview style overrides 100 | log('restoring webview css override', themeId); 101 | files.getStyleCss('webview') 102 | .then((css) => webView.send('apply-webview-css', css)) 103 | .catch(logError); 104 | 105 | // TODO: Restore the sidebar auto-hide setting 106 | // const sidebarAutoHide = prefs.get('sidebar-auto-hide'); 107 | // if (sidebarAutoHide) { 108 | // log('restoring sidebar auto-hide', sidebarAutoHide); 109 | // files.getStyleCss('auto-hide-sidebar') 110 | // .then((css) => webView.send('apply-sidebar-auto-hide', sidebarAutoHide, css)) 111 | // .catch(logError); 112 | // } 113 | 114 | // Restore the zoom level 115 | const zoomLevel = prefs.get('zoom-level'); 116 | if (zoomLevel) { 117 | log('restoring zoom level', zoomLevel); 118 | webView.setZoomLevel(zoomLevel); 119 | } 120 | 121 | // Restore spell checker and auto correct 122 | const spellCheckerCheck = prefs.get('spell-checker-check'); 123 | if (spellCheckerCheck) { 124 | const autoCorrect = prefs.get('spell-checker-auto-correct'); 125 | const langCode = prefs.get('spell-checker-language'); 126 | log('restoring spell checker', spellCheckerCheck, 'auto correct', autoCorrect, 'lang code', langCode); 127 | webView.send('spell-checker', spellCheckerCheck, autoCorrect, langCode); 128 | } 129 | 130 | // Show an 'app updated' notification 131 | if (prefs.get('notify-app-updated')) { 132 | webView.send('notify-app-updated'); 133 | prefs.set('notify-app-updated', false); 134 | } 135 | }); 136 | 137 | // Listen for did-finish-load 138 | webView.addEventListener('did-finish-load', function () { 139 | log('webview did-finish-load'); 140 | 141 | // Hide the loading splash screen 142 | const loadingSplashDiv = document.querySelector('.loader'); 143 | loadingSplashDiv.style.opacity = 0; 144 | setTimeout(function () { 145 | loadingSplashDiv.style.display = 'none'; 146 | }, 250); 147 | }); 148 | 149 | // Forward context menu opens 150 | webView.addEventListener('context-menu', function (event) { 151 | const paramDefaults = { 152 | isWindows7: platform.isWindows7 153 | }; 154 | const params = JSON.stringify(Object.assign(paramDefaults, event.params)); 155 | log('sending context menu', params); 156 | const mwm = remote.getGlobal('application').mainWindowManager; 157 | if (mwm) { 158 | mwm.openContextMenu(params); 159 | } 160 | event.preventDefault(); 161 | }); 162 | 163 | // Animate the splash screen into view 164 | document.addEventListener('DOMContentLoaded', function () { 165 | log('document DOMContentLoaded'); 166 | 167 | // Show the loading splash screen 168 | const loadingSplashDiv = document.querySelector('.loader'); 169 | loadingSplashDiv.style.opacity = 1; 170 | 171 | // In case did-finish-load isn't called, set a timeout 172 | setTimeout(function () { 173 | loadingSplashDiv.style.opacity = 0; 174 | setTimeout(function () { 175 | loadingSplashDiv.style.display = 'none'; 176 | }, 250); 177 | }, 10 * 1000); 178 | }); 179 | 180 | export default webView; 181 | -------------------------------------------------------------------------------- /src/styles/app.less: -------------------------------------------------------------------------------- 1 | html, body, #wv { 2 | width: 100%; 3 | height: 100%; 4 | margin: 0px; 5 | } 6 | -------------------------------------------------------------------------------- /src/styles/auto-hide-sidebar.less: -------------------------------------------------------------------------------- 1 | /* @source: https://userstyles.org/styles/112567/facebook-messanger-nice-tidy-and-more-responsive */ 2 | @media all and (min-width: 641px) { 3 | ._1enh { 4 | transition: transform 0.4s ease-in-out, box-shadow 0.4s ease-in-out; 5 | position: relative; 6 | z-index: 1000; 7 | width: 320px !important; 8 | margin-left: -310px; 9 | background: white; 10 | } 11 | 12 | ._1enh:hover { 13 | transform: translate(310px); 14 | box-shadow: 0px 0px 0px 3000px rgba(0, 0, 0, 0.6); 15 | } 16 | 17 | ._4sp8 { 18 | min-width: 320px; 19 | } 20 | 21 | ._1ht3 { 22 | background-color: #cfe8ff !important; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/styles/loader.less: -------------------------------------------------------------------------------- 1 | .loader { 2 | transition: opacity 250ms; 3 | background-color: #ffffff; 4 | position: fixed; 5 | top: 0px; 6 | width: 100%; 7 | height: 100%; 8 | display: -webkit-flex; 9 | -webkit-user-select: none; 10 | -webkit-align-items: center; 11 | -webkit-justify-content: center; 12 | -webkit-flex-direction: column; 13 | opacity: 0; 14 | 15 | .spinner-container { 16 | animation: rotate 2s linear infinite; 17 | width: 50px; 18 | height: 50px; 19 | z-index: 2; 20 | 21 | .path { 22 | stroke: #0084ff; 23 | stroke-dasharray: 1, 150; 24 | stroke-dashoffset: 0px; 25 | stroke-linecap: round; 26 | animation: dash 1.5s ease-in-out infinite; 27 | } 28 | } 29 | } 30 | 31 | @keyframes rotate { 32 | 100% { 33 | transform: rotate(360deg); 34 | } 35 | } 36 | 37 | @keyframes dash { 38 | 0% { 39 | stroke-dasharray: 1, 150; 40 | stroke-dashoffset: 0px; 41 | } 42 | 43 | 50% { 44 | stroke-dasharray: 90, 150; 45 | stroke-dashoffset: -35; 46 | } 47 | 48 | 100% { 49 | stroke-dasharray: 90, 150; 50 | stroke-dashoffset: -124; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/styles/shim-notification.less: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0px; 3 | padding: 0px; 4 | } 5 | 6 | body, html { 7 | font-family: 'Trebuchet MS', sans-serif; 8 | width: 100%; 9 | height: 100%; 10 | overflow: hidden; 11 | -webkit-user-select: none; 12 | cursor: default; 13 | } 14 | 15 | #title { 16 | display: block; 17 | font-size: 13px; 18 | } 19 | 20 | #body { 21 | display: block; 22 | font-size: 13px; 23 | overflow: hidden; 24 | white-space: nowrap; 25 | text-overflow: ellipsis; 26 | width: 100%; 27 | margin-top: 7px; 28 | margin-bottom: 9px; 29 | } 30 | 31 | #footer { 32 | display: block; 33 | color: #0084ff; 34 | font-size: 12px; 35 | } 36 | 37 | #icon { 38 | position: fixed; 39 | left: 0px; 40 | top: 0px; 41 | width: 80px; 42 | height: 100%; 43 | background-color: #4c4c4c; 44 | background-size: cover; 45 | background-repeat: no-repeat; 46 | background-position: center; 47 | } 48 | 49 | #text { 50 | position: fixed; 51 | left: 80px; 52 | top: 0px; 53 | padding: 10px; 54 | width: 200px; 55 | background: #333333; 56 | color: white; 57 | } 58 | 59 | #selection-blocker { 60 | position: absolute; 61 | left: 0px; 62 | top: 0px; 63 | width: 100%; 64 | height: 100%; 65 | background: transparent; 66 | } 67 | -------------------------------------------------------------------------------- /src/styles/webview.less: -------------------------------------------------------------------------------- 1 | /* Remove the 'install mobile app' bar */ 2 | ._s15 { 3 | display: none; 4 | } 5 | 6 | /* Fix white border around chat bubbles */ 7 | ._o46 ._hh7 { 8 | border: 0px !important; 9 | } 10 | -------------------------------------------------------------------------------- /src/themes/default.css: -------------------------------------------------------------------------------- 1 | /* Empty theme in order to reset to the default look. */ 2 | -------------------------------------------------------------------------------- /src/themes/mosaic.css: -------------------------------------------------------------------------------- 1 | /* @source https://userstyles.org/styles/112722/messenger-com-dark */ 2 | /* Last update: 2017-02-23 */ 3 | 4 | body { 5 | color: #DCDCCC; 6 | } 7 | 8 | a { 9 | color: #ECB; 10 | } 11 | 12 | /* Main background */ 13 | ._4sp8 { 14 | background: url('https://i.imgur.com/kHzRlmz.png'); 15 | } 16 | 17 | /* Lots of buttons and images */ 18 | ._30yy a, ._30yy img, ._4rv9, ._2fug, ._4v, ._31tm, ._31to, ._fl3 { 19 | filter: saturate(0%) brightness(150%); 20 | } 21 | 22 | /* Messenger header */ 23 | ._36ic { 24 | background: none; 25 | } 26 | 27 | /* User tab */ 28 | ._1ht1 { 29 | background: none; 30 | } 31 | 32 | /* Top header (current chat user status) */ 33 | ._5742 { 34 | background: none; 35 | } 36 | 37 | /* Top header user name */ 38 | ._5743 { 39 | color: inherit; 40 | } 41 | 42 | /* Top header user status */ 43 | ._2v6o, ._3oh-, ._3eus { 44 | color: rgba(255, 255, 255, .6) !important; 45 | } 46 | 47 | /* People names */ 48 | ._1ht6 { 49 | color: #DCDCCC; 50 | } 51 | 52 | /* Right info sidebar user info */ 53 | ._4_j5 { 54 | background: none; 55 | } 56 | 57 | /* Latest chat message READ */ 58 | ._1htf { 59 | color: #AAA; 60 | } 61 | 62 | /* Latest chat timestamp READ */ 63 | ._1ht7 { 64 | color: #AAA; 65 | } 66 | 67 | /* Latest chat message UNREAD */ 68 | ._1ht3 ._1htf { 69 | color: #ECB; 70 | } 71 | 72 | /* Latest chat timestamp UNREAD */ 73 | ._1ht3 ._1ht7 { 74 | color: #ECB; 75 | } 76 | 77 | /* Search bar */ 78 | ._5iwm ._58al { 79 | background: #515151; 80 | } 81 | 82 | /* User list block */ 83 | ._5l37 { 84 | background: none; 85 | } 86 | 87 | /* Search result user name */ 88 | ._364g { 89 | color: #DCDCCC; 90 | } 91 | 92 | /* Someone's chat message */ 93 | ._hh7, ._haj { 94 | background-color: #515151; 95 | color: #DCDCCC; 96 | } 97 | 98 | /* Selected someone's chat message */ 99 | ._hh7:active, ._-5k ._hh7 { 100 | background-color: #555; 101 | } 102 | 103 | /* Own chat message */ 104 | ._nd_ ._hh7 { 105 | background-color: #493838; 106 | } 107 | 108 | /* Own chat message floating block */ 109 | ._o46._3i_m ._3058 { 110 | /*float: left;*/ 111 | } 112 | 113 | /* Selected own chat message */ 114 | ._nd_ ._hh7:active, ._nd_._-5k ._hh7 { 115 | background-color: #5E4848; 116 | } 117 | 118 | /* Sidebar Group name */ 119 | ._2jnv { 120 | color: #DCDCCC; 121 | } 122 | 123 | /* Sidebar Person name */ 124 | ._3oh { 125 | color: #DCDCCC; 126 | } 127 | 128 | /* Sidebar Person activity */ 129 | ._3eus { 130 | color: #DCDCCC; 131 | } 132 | 133 | /* Sidebar "options" text */ 134 | ._1lj0 { 135 | color: #DCDCCC; 136 | } 137 | 138 | /* Sidebar options */ 139 | ._3szq { 140 | color: #DCDCCC; 141 | } 142 | 143 | /* Sidebar user name */ 144 | ._3tkx { 145 | color: #DCDCCC; 146 | } 147 | 148 | /* Sidebar user status */ 149 | ._3tky { 150 | color: #DCDCCC; 151 | opacity: 0.6; 152 | } 153 | 154 | /* Sidebar user "Facebook profile" text */ 155 | ._3tl0 { 156 | color: #DCDCCC; 157 | } 158 | 159 | /* Sidebar add people button */ 160 | ._4rpj { 161 | color: #DCDCCC !important; 162 | } 163 | 164 | /* Hovering sidebar bg color */ 165 | ._1enh { 166 | background: #333333; 167 | } 168 | 169 | /* Search results background */ 170 | ._11_d._5t4c { 171 | background: #3a3a3a; 172 | } 173 | 174 | /* Link-type messages */ 175 | ._4_j4 .__6k, .__6l, .__6m { 176 | color: #dcdccc !important; 177 | } 178 | 179 | /* a:hover bg color (convo msg) */ 180 | ._4_j4 ._nd_ ._hh7>span>a:hover { 181 | background: rgba(0, 0, 0, 0.15); 182 | } 183 | 184 | /* Time text */ 185 | ._4_j4 time { 186 | color: #dcdccc !important; 187 | } 188 | 189 | /* input bar */ 190 | ._4rv3 { 191 | background-color: rgba(255, 255, 255, 0.05) !important; 192 | } 193 | 194 | ._17w2 { 195 | color: rgba(255, 255, 255, 1) !important; 196 | } 197 | 198 | ._3eus { 199 | color: rgba(255, 255, 255, .4) !important; 200 | } 201 | 202 | /* New Message to:*/ 203 | ._2y8z { 204 | color: #DCDCCC; 205 | } 206 | 207 | /* New Messsage Enter names */ 208 | ._58al { 209 | color: #DCDCCC !important; 210 | } 211 | 212 | /* All placeholders */ 213 | ::-webkit-input-placeholder { 214 | color: #DCDCCC !important; 215 | } 216 | -------------------------------------------------------------------------------- /tasks/args.coffee: -------------------------------------------------------------------------------- 1 | args = {} 2 | 3 | args.verbose ||= '--verbose' in process.argv 4 | args.production ||= 'production' is process.env.NODE_ENV 5 | args.production ||= '--production' in process.argv 6 | args.production ||= '--prod' in process.argv 7 | args.prod ||= args.production 8 | 9 | args.dev ||= not args.production 10 | args.development ||= not args.production 11 | 12 | module.exports = args 13 | -------------------------------------------------------------------------------- /tasks/build.coffee: -------------------------------------------------------------------------------- 1 | gulp = require 'gulp' 2 | async = require 'async' 3 | rcedit = require 'rcedit' 4 | 5 | {platform, applySpawn} = require './utils' 6 | fs = require 'fs-extra-promise' 7 | path = require 'path' 8 | 9 | utils = require './utils' 10 | manifest = require '../src/package.json' 11 | 12 | # Build for darwin64 13 | gulp.task 'build:darwin64', ['resources:darwin', 'compile:darwin64', 'clean:build:darwin64'], (done) -> 14 | async.series [ 15 | # Move the new icon 16 | (callback) -> 17 | fromPath = './build/resources/darwin/app.icns' 18 | toPath = './build/darwin64/' + manifest.productName + '.app/Contents/Resources/' + manifest.name + '.icns' 19 | fs.copy fromPath, toPath, utils.log callback, fromPath, '=>', toPath 20 | 21 | # Copy license files 22 | async.apply async.parallel, ['LICENSE', 'LICENSES.chromium.html'].map (fileName) -> 23 | (callback) -> 24 | fromPath = './build/darwin64/' + fileName 25 | toPath = './build/darwin64/' + manifest.productName + '.app/Contents/' + fileName 26 | fs.copy fromPath, toPath, utils.log callback, fromPath, '=>', toPath 27 | 28 | # Rename the app executable 29 | (callback) -> 30 | exeDir = './build/darwin64/' + manifest.productName + '.app/Contents/MacOS/' 31 | fromPath = exeDir + 'Electron' 32 | toPath = exeDir + manifest.productName 33 | fs.rename fromPath, toPath, utils.log callback, fromPath, '=>', toPath 34 | 35 | # Rename the helper executables 36 | async.apply async.series, ['Helper', 'Helper NP', 'Helper EH'].map (suffix) -> 37 | (callback) -> 38 | exeDir = './build/darwin64/' + manifest.productName + 39 | '.app/Contents/Frameworks/Electron ' + suffix + '.app/Contents/MacOS/' 40 | fromPath = exeDir + 'Electron ' + suffix 41 | toPath = exeDir + manifest.productName + ' ' + suffix 42 | fs.rename fromPath, toPath, utils.log callback, fromPath, '=>', toPath 43 | 44 | # Move the new Info.plist 45 | (callback) -> 46 | fromPath = './build/resources/darwin/App.plist' 47 | toPath = './build/darwin64/' + manifest.productName + '.app/Contents/Info.plist' 48 | fs.copy fromPath, toPath, utils.log callback, fromPath, '=>', toPath 49 | 50 | # Move the new Info.plist for the helpers 51 | async.apply async.series, ['Helper', 'Helper NP', 'Helper EH'].map (name) -> 52 | (callback) -> 53 | fromPath = './build/resources/darwin/' + name + '.plist' 54 | toPath = './build/darwin64/' + manifest.productName + 55 | '.app/Contents/Frameworks/Electron ' + name + '.app/Contents/Info.plist' 56 | fs.copy fromPath, toPath, utils.log callback, fromPath, '=>', toPath 57 | 58 | # Rename the helper frameworks 59 | async.apply async.series, ['Helper', 'Helper NP', 'Helper EH'].map (suffix) -> 60 | (callback) -> 61 | exeDir = './build/darwin64/' + manifest.productName + '.app/Contents/Frameworks/' 62 | fromPath = exeDir + 'Electron ' + suffix + '.app' 63 | toPath = exeDir + manifest.productName + ' ' + suffix + '.app' 64 | fs.rename fromPath, toPath, utils.log callback, fromPath, '=>', toPath 65 | 66 | # Touch the .app to refresh it 67 | (callback) -> 68 | cmd = 'touch' 69 | args = ['./build/darwin64/' + manifest.productName + '.app'] 70 | applySpawn(cmd, args)(utils.log callback, cmd, args...) 71 | ], done 72 | 73 | # Build for linux32 and linux64 74 | ['linux32', 'linux64'].forEach (dist) -> 75 | gulp.task 'build:' + dist, ['resources:linux', 'compile:' + dist, 'clean:build:' + dist, 'changelog:linux'], (done) -> 76 | async.series [ 77 | # Rename the executable 78 | (callback) -> 79 | exeDir = './build/' + dist + '/opt/' + manifest.name + '/' 80 | fromPath = exeDir + 'electron' 81 | toPath = exeDir + manifest.name 82 | 83 | fs.rename fromPath, toPath, utils.log callback, fromPath, '=>', toPath 84 | 85 | # Move the app's .desktop file 86 | (callback) -> 87 | fromPath = './build/resources/linux/app.desktop' 88 | toPath = './build/' + dist + '/usr/share/applications/' + manifest.name + '.desktop' 89 | fs.copy fromPath, toPath, utils.log callback, fromPath, '=>', toPath 90 | 91 | # Move the app's startup.desktop file 92 | (callback) -> 93 | fromPath = './build/resources/linux/startup.desktop' 94 | toPath = './build/' + dist + '/opt/' + manifest.name + '/resources/app/startup.desktop' 95 | fs.copy fromPath, toPath, utils.log callback, fromPath, '=>', toPath 96 | 97 | # Move icons 98 | async.apply async.waterfall, [ 99 | async.apply fs.readdir, './build/resources/linux/icons' 100 | (files, callback) -> 101 | async.map files, (file, callback) -> 102 | size = path.basename file, '.png' 103 | fromPath = path.join './build/resources/linux/icons', file 104 | toPath = path.join.apply path, [ 105 | './build/' + dist + '/usr/share/icons/hicolor/' 106 | size + 'x' + size + '/apps/' + manifest.name + '.png' 107 | ] 108 | fs.copy fromPath, toPath, utils.log callback, fromPath, '=>', toPath 109 | , callback 110 | ] 111 | ], done 112 | 113 | # Build for win32 114 | gulp.task 'build:win32', ['resources:win', 'compile:win32', 'clean:build:win32'], (done) -> 115 | async.series [ 116 | # Edit properties of the exe 117 | (callback) -> 118 | properties = 119 | 'version-string': 120 | ProductName: manifest.productName 121 | CompanyName: manifest.authorName 122 | FileDescription: manifest.productName 123 | LegalCopyright: manifest.copyright 124 | OriginalFilename: manifest.productName + '.exe' 125 | 'file-version': manifest.version 126 | 'product-version': manifest.version 127 | 'icon': './build/resources/win/app.ico' 128 | 129 | exePath = './build/win32/electron.exe' 130 | logMessage = 'rcedit ./build/win32/electron.exe properties' 131 | rcedit exePath, properties, utils.log callback, logMessage, JSON.stringify(properties) 132 | 133 | # Rename the exe 134 | (callback) -> 135 | fromPath = './build/win32/electron.exe' 136 | toPath = './build/win32/' + manifest.productName + '.exe' 137 | fs.rename fromPath, toPath, utils.log callback, fromPath, '=>', toPath 138 | ], done 139 | 140 | # Build for the current platform by default 141 | gulp.task 'build', ['build:' + platform()] 142 | -------------------------------------------------------------------------------- /tasks/changelog.coffee: -------------------------------------------------------------------------------- 1 | gulp = require 'gulp' 2 | moment = require 'moment' 3 | fs = require 'fs-extra-promise' 4 | 5 | manifest = require '../src/package.json' 6 | mainManifest = require '../package.json' 7 | changelogJson = require '../CHANGELOG.json' 8 | 9 | gulp.task 'changelog:deb', -> 10 | fs.outputFileAsync './build/changelogs/deb.txt', 11 | changelogJson 12 | .map (release) -> 13 | if Array.isArray release.changes 14 | log = release.changes 15 | .map (line) -> ' - ' + line 16 | .join '\n' 17 | else 18 | log = [] 19 | for key in Object.keys(release.changes) 20 | logs = release.changes[key] 21 | .map (line) -> ' - ' + line 22 | .join '\n' 23 | log.push ' * ' + key 24 | log.push logs 25 | log = log.join '\n' 26 | 27 | parsedDate = new Date(release.releasedAt) 28 | date = moment(parsedDate).format('ddd, DD MMM YYYY HH:mm:ss ZZ') 29 | 30 | return manifest.name + ' (' + release.version + ') ' + release.channel + 31 | '; urgency=' + release.urgency + '\n\n' + log + '\n\n' + 32 | '-- ' + manifest.author + ' ' + date 33 | .join '\n\n' 34 | 35 | gulp.task 'changelog:rpm', -> 36 | fs.outputFileAsync './build/changelogs/rpm.txt', 37 | changelogJson 38 | .map (release) -> 39 | if Array.isArray release.changes 40 | log = release.changes 41 | .map (line) -> '- ' + line 42 | .join '\n' 43 | else 44 | log = [] 45 | for key in Object.keys(release.changes) 46 | logs = release.changes[key] 47 | .map (line) -> '- ' + key + ': ' + line 48 | .join '\n' 49 | log.push logs 50 | log = log.join '\n' 51 | 52 | parsedDate = new Date(release.releasedAt) 53 | date = moment(parsedDate).format('ddd MMM DD YYYY') 54 | 55 | channelSuffix = '' 56 | if release.channel isnt 'stable' 57 | channelSuffix = '-' + release.channel 58 | 59 | return '* ' + date + ' ' + manifest.author + ' ' + release.version + channelSuffix + '\n' + log 60 | .join '\n\n' 61 | 62 | gulp.task 'changelog:md', -> 63 | changelog = changelogJson 64 | .map (release, index) -> 65 | if Array.isArray release.changes 66 | log = release.changes 67 | .map (line) -> '- ' + line 68 | .join '\n' 69 | else 70 | log = [] 71 | for key in Object.keys(release.changes) 72 | logs = release.changes[key] 73 | .map (line) -> '- ' + line 74 | .join '\n' 75 | log.push '\n**' + key + '**\n' 76 | log.push logs 77 | log = log.join '\n' 78 | 79 | parsedDate = new Date(release.releasedAt) 80 | date = moment(parsedDate).format('YYYY-DD-MM') 81 | 82 | repoUrl = mainManifest.repository.url.replace '.git', '' 83 | 84 | fullChangelog = '' 85 | if index < changelogJson.length - 1 86 | fullChangelog = '[Full Changelog](' + repoUrl + '/compare/v' + 87 | changelogJson[index+1].version + '...v' + release.version + ') • ' 88 | 89 | download = '[Download](' + repoUrl + '/releases/tag/v' + release.version + ')' 90 | 91 | channelSuffix = '' 92 | if release.channel isnt 'stable' 93 | channelSuffix = '-' + release.channel 94 | 95 | return '## [' + release.version + channelSuffix + '](' + repoUrl + '/tree/v' + 96 | release.version + ') (' + date + ')\n\n' + fullChangelog + download + '\n' + log 97 | .join '\n\n' 98 | changelog += '\n' 99 | fs.outputFileAsync './CHANGELOG.md', changelog 100 | 101 | # Generate only linux changelogs 102 | gulp.task 'changelog:linux', [ 103 | 'changelog:deb' 104 | 'changelog:rpm' 105 | ] 106 | 107 | # Generate all of them by default 108 | gulp.task 'changelog', [ 109 | 'changelog:deb' 110 | 'changelog:rpm' 111 | 'changelog:md' 112 | ] 113 | -------------------------------------------------------------------------------- /tasks/clean.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs-extra-promise' 2 | {platform} = require './utils' 3 | gulp = require 'gulp' 4 | del = require 'del' 5 | 6 | manifest = require '../src/package.json' 7 | 8 | # Remove the default_app folder and the default icon inside the darwin64 build 9 | gulp.task 'clean:build:darwin64', ['download:darwin64'], -> 10 | del [ 11 | './build/darwin64/' + manifest.productName + '.app/Contents/Resources/default_app.asar' 12 | './build/darwin64/' + manifest.productName + '.app/Contents/Resources/electron.icns' 13 | ] 14 | .then (result) -> console.log result 15 | 16 | # Remove the default_app folder inside the linux builds 17 | ['linux32', 'linux64'].forEach (dist) -> 18 | gulp.task 'clean:build:' + dist, ['download:' + dist], -> 19 | del './build/' + dist + '/opt/' + manifest.name + '/resources/default_app.asar' 20 | .then (result) -> console.log result 21 | 22 | # Remove the default_app folder inside the win32 build 23 | gulp.task 'clean:build:win32', ['download:win32'], -> 24 | del './build/win32/resources/default_app.asar' 25 | .then (result) -> console.log result 26 | 27 | # Clean build dist for the current platform by default 28 | gulp.task 'clean:build', ['clean:build:' + platform()] 29 | 30 | # Clean all the dist files for darwin64 and make sure the dir exists 31 | gulp.task 'clean:dist:darwin64', -> 32 | del './dist/' + manifest.productName + '-' + manifest.version + '-osx.dmg' 33 | .then (result) -> console.log result 34 | .then -> fs.ensureDirAsync './dist' 35 | 36 | # Just ensure the dir exists (dist files are overwritten) 37 | ['linux32', 'linux64', 'win32'].forEach (dist) -> 38 | gulp.task 'clean:dist:' + dist, -> 39 | fs.ensureDirAsync './dist' 40 | 41 | # Remove packages from previous releases 42 | gulp.task 'clean:prev-releases:win32', -> 43 | del [ 44 | './dist/' + manifest.name + '-*-setup-for-nsis.exe' 45 | './dist/' + manifest.name + '-*-full.nupkg' 46 | '!./dist/' + manifest.name + '-' + manifest.version + '-full.nupkg' 47 | ] 48 | .then (result) -> console.log result 49 | 50 | # Clean dist for the current platform by default 51 | gulp.task 'clean:dist', ['clean:dist:' + platform()] 52 | -------------------------------------------------------------------------------- /tasks/compile.coffee: -------------------------------------------------------------------------------- 1 | beeper = require 'beeper' 2 | fs = require 'fs' 3 | 4 | gulp = require 'gulp' 5 | plumber = require 'gulp-plumber' 6 | sourcemaps = require 'gulp-sourcemaps' 7 | mustache = require 'gulp-mustache' 8 | filter = require 'gulp-filter' 9 | rename = require 'gulp-rename' 10 | header = require 'gulp-header' 11 | 12 | less = require 'gulp-less' 13 | babel = require 'gulp-babel' 14 | gif = require 'gulp-if' 15 | 16 | {platform} = require './utils' 17 | manifest = require '../src/package.json' 18 | args = require './args' 19 | 20 | [ 21 | ['darwin64', './build/darwin64/' + manifest.productName + '.app/Contents/Resources/app'] 22 | ['linux32', './build/linux32/opt/' + manifest.name + '/resources/app'] 23 | ['linux64', './build/linux64/opt/' + manifest.name + '/resources/app'] 24 | ['win32', './build/win32/resources/app'] 25 | ].forEach (item) -> 26 | [dist, dir] = item 27 | 28 | handleError = (err) -> 29 | console.error err 30 | beeper() 31 | 32 | # Compile styles 33 | gulp.task 'compile:' + dist + ':styles', ['clean:build:' + dist], -> 34 | gulp.src './src/styles/**/*.less' 35 | .pipe plumber handleError 36 | .pipe less() 37 | .pipe plumber.stop() 38 | .pipe gulp.dest dir + '/styles' 39 | 40 | # Compile scripts 41 | gulp.task 'compile:' + dist + ':scripts', ['clean:build:' + dist], -> 42 | loggerIgnore = fs.readFileSync('./src/.loggerignore', 'utf8') 43 | .split('\n').filter((rule) -> !!rule).map((rule) -> '!' + rule.trim()) 44 | excludeHeaderFilter = filter ['**/*'].concat(loggerIgnore), {restore: true} 45 | sourceMapHeader = "if (process.type === 'browser') { " + 46 | "try { require('source-map-support').install(); } catch(ignored) {} }" 47 | loggerHeader = [ 48 | "var log = require('common/utils/logger').debugLogger(__filename);" 49 | "var logError = require('common/utils/logger').errorLogger(__filename, false);" 50 | "var logFatal = require('common/utils/logger').errorLogger(__filename, true);" 51 | ].join ' ' 52 | 53 | gulp.src './src/scripts/**/*.js' 54 | .pipe plumber handleError 55 | .pipe gif args.dev, sourcemaps.init() 56 | .pipe babel 57 | presets: [ 58 | 'es2015' 59 | 'stage-0' 60 | ] 61 | plugins: [ 62 | 'transform-runtime' 63 | 'default-import-checker' 64 | ] 65 | .pipe gif args.dev, sourcemaps.write {sourceRoot: 'src/scripts'} 66 | .pipe excludeHeaderFilter 67 | .pipe header loggerHeader 68 | .pipe excludeHeaderFilter.restore 69 | .pipe header sourceMapHeader 70 | .pipe plumber.stop() 71 | .pipe gulp.dest dir + '/scripts' 72 | 73 | # Move themes 74 | gulp.task 'compile:' + dist + ':themes', ['clean:build:' + dist], -> 75 | gulp.src './src/themes/**/*.css' 76 | .pipe rename (path) -> 77 | path.basename = path.basename.toLowerCase() 78 | .pipe gulp.dest dir + '/themes' 79 | 80 | # Move images 81 | gulp.task 'compile:' + dist + ':images', ['clean:build:' + dist], -> 82 | gulp.src './src/images/**/*' 83 | .pipe gulp.dest dir + '/images' 84 | 85 | # Move dictionaries 86 | gulp.task 'compile:' + dist + ':dicts', ['clean:build:' + dist], -> 87 | gulp.src './src/dicts/**/*' 88 | .pipe gulp.dest dir + '/dicts' 89 | 90 | # Move html files 91 | gulp.task 'compile:' + dist + ':html', ['clean:build:' + dist], -> 92 | gulp.src './src/html/**/*.html' 93 | .pipe mustache manifest 94 | .pipe gulp.dest dir + '/html' 95 | 96 | # Move the node modules 97 | gulp.task 'compile:' + dist + ':deps', ['clean:build:' + dist], -> 98 | gulp.src [ 99 | './src/node_modules/**/*' 100 | '!./src/node_modules/**/test*/**' 101 | '!./src/node_modules/**/example*/**' 102 | '!./src/node_modules/**/changelog*' 103 | '!./src/node_modules/**/notice*' 104 | '!./src/node_modules/**/readme*' 105 | ] 106 | .pipe gulp.dest dir + '/node_modules' 107 | 108 | # Move package.json 109 | gulp.task 'compile:' + dist + ':package', ['clean:build:' + dist], -> 110 | gulp.src './src/package.json' 111 | .pipe mustache process.env 112 | .pipe gulp.dest dir 113 | 114 | # Compile everything 115 | gulp.task 'compile:' + dist, [ 116 | 'compile:' + dist + ':styles' 117 | 'compile:' + dist + ':scripts' 118 | 'compile:' + dist + ':themes' 119 | 'compile:' + dist + ':images' 120 | 'compile:' + dist + ':dicts' 121 | 'compile:' + dist + ':html' 122 | 'compile:' + dist + ':deps' 123 | 'compile:' + dist + ':package' 124 | ] 125 | 126 | # Compile for the current platform by default 127 | gulp.task 'compile', ['compile:' + platform()] 128 | -------------------------------------------------------------------------------- /tasks/download.coffee: -------------------------------------------------------------------------------- 1 | gulp = require 'gulp' 2 | fs = require 'fs-extra-promise' 3 | {platform} = require './utils' 4 | electronDownloader = require 'gulp-electron-downloader' 5 | manifest = require '../src/package.json' 6 | args = require './args' 7 | 8 | # Flags to keep track of downloads 9 | downloaded = 10 | darwin64: false 11 | linux32: false 12 | linux64: false 13 | win32: false 14 | 15 | # Download the Electron binary for a platform 16 | [ 17 | ['darwin', 'x64', 'darwin64', './build/darwin64'] 18 | ['linux', 'ia32', 'linux32', './build/linux32/opt/' + manifest.name] 19 | ['linux', 'x64', 'linux64', './build/linux64/opt/' + manifest.name] 20 | ['win32', 'ia32', 'win32', './build/win32'] 21 | ].forEach (release) -> 22 | [platformName, arch, dist, outputDir] = release 23 | 24 | gulp.task 'download:' + dist, ['kill:' + dist], (done) -> 25 | # Skip if already downloaded to speed up auto-reload 26 | if downloaded[dist] 27 | if args.verbose 28 | console.log 'already downloaded, skipping' 29 | return done() 30 | 31 | process.env.GITHUB_TOKEN = process.env.GITHUB_TOKEN or process.env.GITHUB_OAUTH_TOKEN 32 | process.env.GITHUB_OAUTH_TOKEN = process.env.GITHUB_OAUTH_TOKEN or process.env.GITHUB_TOKEN 33 | 34 | electronDownloader 35 | version: manifest.electronVersion 36 | cacheDir: './cache' 37 | outputDir: outputDir 38 | platform: platformName 39 | arch: arch 40 | , (err) -> 41 | return done err if err 42 | downloaded[dist] = true 43 | 44 | # Also rename the .app on darwin 45 | if dist is 'darwin64' 46 | fs.rename './build/darwin64/Electron.app', './build/darwin64/' + manifest.productName + '.app', done 47 | else 48 | done() 49 | 50 | # Download for the current platform by default 51 | gulp.task 'download', ['download:' + platform()] 52 | -------------------------------------------------------------------------------- /tasks/kill.coffee: -------------------------------------------------------------------------------- 1 | {applySpawn, platformOnly} = require './utils' 2 | args = require './args' 3 | gulp = require 'gulp' 4 | 5 | manifest = require '../src/package.json' 6 | 7 | lock = require './lock' 8 | lock.killTask ||= {skip: {}} 9 | 10 | [ 11 | ['darwin64', 'pkill', ['-9', manifest.productName]] 12 | ['linux32', 'pkill', ['-9', manifest.name]] 13 | ['linux64', 'pkill', ['-9', manifest.name]] 14 | ['win32', 'taskkill', ['/F', '/IM', manifest.productName + '.exe']] 15 | ].forEach (item) -> 16 | [dist, killCmd, killArgs] = item 17 | 18 | gulp.task 'kill:' + dist, (done) -> 19 | if lock.killTask.skip[dist] 20 | console.log 'kill skipped (lock)' if args.verbose 21 | done() 22 | else if process.platform.indexOf(platformOnly()) < 0 23 | console.log 'kill skipped (platforms don\'t match)' if args.verbose 24 | done() 25 | else 26 | console.log 'killing app' if args.verbose 27 | lock.killTask.skip[dist] = true 28 | cb = (err) -> 29 | if err and (err.code is 'ENOENT' or err.code is 1 or err.code is 128) 30 | console.error err if args.verbose 31 | done() 32 | else 33 | done err 34 | applySpawn(killCmd, killArgs)(cb) 35 | -------------------------------------------------------------------------------- /tasks/lock.coffee: -------------------------------------------------------------------------------- 1 | module.exports = {} 2 | -------------------------------------------------------------------------------- /tasks/publish.coffee: -------------------------------------------------------------------------------- 1 | gulp = require 'gulp' 2 | path = require 'path' 3 | fs = require 'fs-extra-promise' 4 | crypto = require 'crypto' 5 | 6 | async = require 'async' 7 | mustache = require 'gulp-mustache' 8 | githubRelease = require 'gulp-github-release' 9 | request = require 'request' 10 | 11 | utils = require './utils' 12 | {deepClone, applySpawn} = utils 13 | 14 | manifest = deepClone require '../src/package.json' 15 | mainManifest = require '../package.json' 16 | changelogJson = require '../CHANGELOG.json' 17 | args = require './args' 18 | 19 | # Upload every file in ./dist to GitHub 20 | gulp.task 'publish:github', -> 21 | if not process.env.GITHUB_TOKEN 22 | return console.warn 'GITHUB_TOKEN env var not set.' 23 | 24 | channelAppend = '' 25 | if manifest.versionChannel isnt 'stable' 26 | channelAppend = '-' + manifest.versionChannel 27 | 28 | release = changelogJson[0] 29 | log = [] 30 | for key in Object.keys(release.changes) 31 | logs = release.changes[key] 32 | .map (line) -> '- ' + line 33 | .join '\n' 34 | log.push '\n**' + key + '**\n' 35 | log.push logs 36 | changelog = log.join '\n' 37 | 38 | gulp.src './dist/*' 39 | .pipe githubRelease 40 | token: process.env.GITHUB_TOKEN 41 | manifest: manifest 42 | reuseRelease: true 43 | reuseDraftOnly: true 44 | draft: true 45 | tag: 'v' + manifest.version 46 | name: 'v' + manifest.version + channelAppend 47 | notes: changelog.trim() 48 | 49 | # Upload deb and RPM packages to Bintray 50 | ['deb', 'rpm'].forEach (dist) -> 51 | gulp.task 'publish:bintray:' + dist, (done) -> 52 | if not process.env.BINTRAY_API_KEY 53 | return console.warn 'BINTRAY_API_KEY env var not set.' 54 | 55 | arch64Name = if dist is 'deb' then 'amd64' else 'x86_64' 56 | tasks = [ 57 | ['./dist/' + manifest.name + '-' + manifest.version + '-linux-' + arch64Name + '.' + dist, arch64Name] 58 | ['./dist/' + manifest.name + '-' + manifest.version + '-linux-i386.' + dist, 'i386'] 59 | ].map (item) -> 60 | [srcPath, archType] = item 61 | 62 | host = 'https://api.bintray.com' 63 | subject = mainManifest.bintray.subject 64 | filePath = path.basename(srcPath) 65 | 66 | if dist is 'deb' 67 | poolPath = 'pool/main/' + manifest.name[0] + '/' 68 | filePath = poolPath + manifest.name + '/' + filePath 69 | 70 | opts = 71 | url: host + '/content/' + subject + '/' + dist + '/' + filePath 72 | auth: 73 | user: subject 74 | pass: process.env.BINTRAY_API_KEY 75 | headers: 76 | 'X-Bintray-Package': manifest.name 77 | 'X-Bintray-Version': manifest.version 78 | 'X-Bintray-Publish': 1 79 | 'X-Bintray-Override': 1 80 | 'X-Bintray-Debian-Distribution': manifest.versionChannel 81 | 'X-Bintray-Debian-Component': 'main' 82 | 'X-Bintray-Debian-Architecture': archType 83 | 84 | (cb) -> 85 | console.log 'Uploading', srcPath if args.verbose 86 | fs.createReadStream srcPath 87 | .pipe request.put opts, (err, res, body) -> 88 | if not err 89 | console.log body if args.verbose 90 | cb(err) 91 | 92 | async.series tasks, done 93 | 94 | # Upload artifacts to Bintray 95 | ['darwin', 'win32', 'linux'].forEach (dist) -> 96 | gulp.task 'publish:bintray:artifacts:' + dist, (done) -> 97 | if not process.env.BINTRAY_API_KEY 98 | return console.warn 'BINTRAY_API_KEY env var not set.' 99 | 100 | fs.readdir './dist', (err, files) -> 101 | if err 102 | return done err 103 | 104 | tasks = files.map (fileNameShort) -> 105 | fileNameLong = path.resolve './dist/', fileNameShort 106 | 107 | host = 'https://api.bintray.com' 108 | subject = mainManifest.bintray.subject 109 | artifactsRepoName = mainManifest.bintray.artifactsRepoName 110 | 111 | opts = 112 | url: host + '/content/' + subject + '/' + artifactsRepoName + 113 | '/staging/' + dist + '/' + manifest.version + '/' + fileNameShort 114 | auth: 115 | user: subject 116 | pass: process.env.BINTRAY_API_KEY 117 | headers: 118 | 'X-Bintray-Package': manifest.name 119 | 'X-Bintray-Version': manifest.version 120 | 'X-Bintray-Publish': 1 121 | 'X-Bintray-Override': 1 122 | 123 | if fileNameShort is 'RELEASES' or fileNameShort.indexOf('.nupkg') > -1 124 | opts.headers['Content-Type'] = 'application/octet-stream' 125 | 126 | (cb) -> 127 | console.log 'Uploading', fileNameLong if args.verbose 128 | fs.createReadStream fileNameLong 129 | .pipe request.put opts, (err, res, body) -> 130 | if not err 131 | console.log body if args.verbose 132 | if JSON.stringify(body).toLowerCase().indexOf('success') is -1 133 | err = new Error('bintray upload failed') 134 | cb(err) 135 | 136 | async.series tasks, (err) -> 137 | if err 138 | console.log err 139 | artifactsUrl = 'https://dl.bintray.com/' + mainManifest.bintray.subject + '/' + 140 | mainManifest.bintray.artifactsRepoName + '/staging/' + dist + '/' 141 | console.log 'Upload finished: ' + artifactsUrl if args.verbose 142 | done() 143 | -------------------------------------------------------------------------------- /tasks/purge.coffee: -------------------------------------------------------------------------------- 1 | gulp = require 'gulp' 2 | del = require 'del' 3 | 4 | # Remove the build directory 5 | gulp.task 'purge:build', -> del './build' 6 | 7 | # Remove the cache directory 8 | gulp.task 'purge:cache', -> del './cache' 9 | 10 | # Remove the dist directory 11 | gulp.task 'purge:dist', -> del './dist' 12 | 13 | # Remove the build, cache and dist directories 14 | gulp.task 'purge', [ 15 | 'purge:build' 16 | 'purge:cache' 17 | 'purge:dist' 18 | ] 19 | -------------------------------------------------------------------------------- /tasks/rebuild.coffee: -------------------------------------------------------------------------------- 1 | gulp = require 'gulp' 2 | {applySpawn} = require './utils' 3 | manifest = require '../src/package.json' 4 | path = require 'path' 5 | 6 | [ 7 | ['32', 'ia32'] 8 | ['64', 'x64'] 9 | ].forEach (item) -> 10 | [dist, arch] = item 11 | 12 | # Rebuild native node modules 13 | gulp.task 'rebuild:' + dist, (done) -> 14 | process.env.npm_config_disturl = 'https://atom.io/download/atom-shell' 15 | process.env.npm_config_target = manifest.electronVersion 16 | process.env.npm_config_arch = arch 17 | process.env.npm_config_runtime = 'electron' 18 | process.env.HOME = '~/.electron-gyp' 19 | options = 20 | cwd: path.resolve 'src' 21 | env: process.env 22 | applySpawn('npm', ['rebuild'], options)(done) 23 | -------------------------------------------------------------------------------- /tasks/resources.coffee: -------------------------------------------------------------------------------- 1 | gulp = require 'gulp' 2 | filter = require 'gulp-filter' 3 | mustache = require 'gulp-mustache' 4 | manifest = require '../src/package.json' 5 | {platformOnly, deepClone} = require './utils' 6 | 7 | manifest = deepClone manifest 8 | 9 | # Move and process the resources for darwin64 10 | gulp.task 'resources:darwin', -> 11 | templateFilter = filter ['**/*.plist', '**/*.json'], {restore: true} 12 | 13 | if manifest.versionChannel is 'stable' 14 | manifest.versionChannel = '' 15 | else 16 | manifest.versionChannel = '-' + manifest.versionChannel 17 | 18 | unless manifest.buildNum 19 | manifest.buildNum = process.env.TRAVIS_BUILD_NUMBER or 0 20 | 21 | gulp.src './resources/darwin/**/*' 22 | .pipe templateFilter 23 | .pipe mustache manifest 24 | .pipe templateFilter.restore 25 | .pipe gulp.dest './build/resources/darwin' 26 | 27 | # Move and process the resources for linux32 and linux64 28 | gulp.task 'resources:linux', -> 29 | templateFilter = filter ['**/*.desktop', '**/*.sh'], {restore: true} 30 | 31 | manifest.linux.name = manifest.name 32 | manifest.linux.productName = manifest.productName 33 | manifest.linux.description = manifest.description 34 | manifest.linux.version = manifest.version 35 | 36 | gulp.src './resources/linux/**/*' 37 | .pipe templateFilter 38 | .pipe mustache manifest.linux 39 | .pipe templateFilter.restore 40 | .pipe gulp.dest './build/resources/linux' 41 | 42 | # Move the resources for win32 43 | gulp.task 'resources:win', -> 44 | templateFilter = filter ['**/installer.nsi'], {restore: true} 45 | gulp.src './resources/win/**/*' 46 | .pipe templateFilter 47 | .pipe mustache manifest 48 | .pipe templateFilter.restore 49 | .pipe gulp.dest './build/resources/win' 50 | 51 | # Move and process resources for the current platform by default 52 | gulp.task 'resources', ['resources:' + platformOnly()] 53 | -------------------------------------------------------------------------------- /tasks/restart.coffee: -------------------------------------------------------------------------------- 1 | {applySpawn} = require './utils' 2 | args = require './args' 3 | gulp = require 'gulp' 4 | 5 | manifest = require '../src/package.json' 6 | 7 | [ 8 | [ 9 | 'darwin64' 10 | 'pkill' 11 | ['-9', manifest.productName] 12 | './build/darwin64/' + manifest.productName + '.app/Contents/MacOS/' + manifest.productName 13 | ] 14 | [ 15 | 'linux32' 16 | 'pkill' 17 | ['-9', manifest.name] 18 | './build/linux32/opt/' + manifest.name + '/' + manifest.name 19 | ] 20 | [ 21 | 'linux64' 22 | 'pkill' 23 | ['-9', manifest.name] 24 | './build/linux64/opt/' + manifest.name + '/' + manifest.name 25 | ] 26 | [ 27 | 'win32' 28 | 'taskkill' 29 | ['/F', '/IM', manifest.productName + '.exe'] 30 | './build/win32/' + manifest.productName + '.exe' 31 | ] 32 | ].forEach (item) -> 33 | [dist, killCmd, killArgs, runnablePath] = item 34 | 35 | # Proxy the compile task then restart the app 36 | [ 37 | 'compile:' + dist + ':scripts' 38 | 'compile:' + dist + ':package' 39 | 'compile:' + dist + ':images' 40 | ].forEach (proxiedTask) -> 41 | gulp.task 'restart:' + proxiedTask, [proxiedTask], (done) -> 42 | cb = (err) -> 43 | if err and (err.code is 'ENOENT' or err.code is 1 or err.code is 128) 44 | console.error err if args.verbose 45 | else if err 46 | done err 47 | return 48 | 49 | console.log 're-spawning app' if args.verbose 50 | applySpawn(runnablePath, ['--debug'], {stdio: 'inherit'})() 51 | done null 52 | applySpawn(killCmd, killArgs)(cb) 53 | -------------------------------------------------------------------------------- /tasks/start.coffee: -------------------------------------------------------------------------------- 1 | {applySpawn} = require './utils' 2 | {platform} = require './utils' 3 | args = require './args' 4 | gulp = require 'gulp' 5 | 6 | manifest = require '../src/package.json' 7 | 8 | [ 9 | ['darwin64', './build/darwin64/' + manifest.productName + '.app/Contents/MacOS/' + manifest.productName] 10 | ['linux32', './build/linux32/opt/' + manifest.name + '/' + manifest.name] 11 | ['linux64', './build/linux64/opt/' + manifest.name + '/' + manifest.name] 12 | ['win32', './build/win32/' + manifest.productName + '.exe'] 13 | ].forEach (item) -> 14 | [dist, runnablePath] = item 15 | 16 | # Start the app without any building 17 | gulp.task 'start:' + dist, -> 18 | console.log 'starting app' if args.verbose 19 | applySpawn(runnablePath, ['--debug'], {stdio: 'inherit'})() 20 | 21 | # Start for the current platform by default 22 | gulp.task 'start', ['start:' + platform()] 23 | -------------------------------------------------------------------------------- /tasks/utils.coffee: -------------------------------------------------------------------------------- 1 | args = require './args' 2 | async = require 'async' 3 | spawn = require 'cross-spawn' 4 | fs = require 'fs' 5 | require 'colors' 6 | 7 | updateManifest = (jsonPath, updateFn, done) -> 8 | async.waterfall [ 9 | async.apply fs.readFile, jsonPath, 'utf8' 10 | (file, callback) -> 11 | json = JSON.parse file 12 | updateFn json 13 | text = JSON.stringify json 14 | fs.writeFile jsonPath, text, 'utf8', callback 15 | ], done 16 | 17 | applyPromise = (fn, args...) -> 18 | (cb) -> 19 | fn args... 20 | .then (results...) -> cb null, results... 21 | .catch cb 22 | 23 | applySpawn = (cmd, params, opts = {}) -> 24 | (cb) -> 25 | unless opts.stdio 26 | opts.stdio = if args.verbose then 'inherit' else 'ignore' 27 | if args.verbose 28 | console.log 'spawning', cmd 29 | child = spawn cmd, params, opts 30 | if cb 31 | errored = false 32 | child.on 'error', (err) -> 33 | errored = true 34 | if args.verbose 35 | console.log 'finished: spawn', cmd 36 | cb(err) 37 | child.on 'close', (code) -> 38 | unless errored 39 | if code 40 | err = new Error "`#{cmd} #{params.join(' ')}` exited with code #{code}" 41 | err.code = code 42 | if args.verbose 43 | console.log 'finished: spawn', cmd 44 | cb err 45 | else 46 | if args.verbose 47 | console.log 'finished: spawn', cmd 48 | cb null 49 | 50 | applyIf = (cond, fn) -> 51 | if cond 52 | fn 53 | else 54 | (cb) -> cb(null) 55 | 56 | platform = -> 57 | if process.platform is 'win32' 58 | process.platform 59 | else 60 | arch = if process.arch is 'ia32' then '32' else '64' 61 | process.platform + arch 62 | 63 | platformOnly = -> 64 | if process.platform is 'win32' 65 | 'win' 66 | else 67 | process.platform 68 | 69 | join = (args) -> 70 | (val for own key, val of args).join ' ' 71 | 72 | log = (callback, messages...) -> 73 | (err) -> 74 | if args.verbose 75 | status = if err then 'Failed'.red else 'Successful'.green 76 | console.log status, join(messages) 77 | callback err 78 | 79 | deepClone = (obj) -> 80 | if not obj? or typeof obj isnt 'object' 81 | return obj 82 | 83 | if obj instanceof Date 84 | return new Date(obj.getTime()) 85 | 86 | if obj instanceof RegExp 87 | flags = '' 88 | flags += 'g' if obj.global? 89 | flags += 'i' if obj.ignoreCase? 90 | flags += 'm' if obj.multiline? 91 | flags += 'y' if obj.sticky? 92 | return new RegExp(obj.source, flags) 93 | 94 | newInstance = new obj.constructor() 95 | 96 | for key of obj 97 | newInstance[key] = deepClone obj[key] 98 | 99 | newInstance 100 | 101 | module.exports = 102 | updateManifest: updateManifest 103 | applyPromise: applyPromise 104 | applySpawn: applySpawn 105 | applyIf: applyIf 106 | platform: platform 107 | platformOnly: platformOnly 108 | join: join 109 | log: log 110 | deepClone: deepClone 111 | -------------------------------------------------------------------------------- /tasks/watch.coffee: -------------------------------------------------------------------------------- 1 | gulp = require 'gulp' 2 | manifest = require '../src/package.json' 3 | {platform, applySpawn} = require './utils' 4 | args = require './args' 5 | 6 | # Watch files and reload the app on changes 7 | [ 8 | ['darwin64', './build/darwin64/' + manifest.productName + '.app/Contents/MacOS/' + manifest.productName] 9 | ['linux32', './build/linux32/opt/' + manifest.name + '/' + manifest.name] 10 | ['linux64', './build/linux64/opt/' + manifest.name + '/' + manifest.name] 11 | ['win32', './build/win32/' + manifest.productName + '.exe'] 12 | ].forEach (item) -> 13 | [dist, runnablePath] = item 14 | 15 | gulp.task 'watch:' + dist, ['build:' + dist], -> 16 | # Launch the app 17 | console.log 'initial spawn' if args.verbose 18 | applySpawn(runnablePath, ['--debug'], {stdio: 'inherit'})() 19 | 20 | # Watch files 21 | gulp.watch './src/styles/**/*', ['compile:' + dist + ':styles'] 22 | gulp.watch './src/themes/**/*', ['compile:' + dist + ':themes'] 23 | gulp.watch './src/images/**/*', ['restart:compile:' + dist + ':images'] 24 | gulp.watch './src/.loggerignore', ['restart:compile:' + dist + ':scripts'] 25 | gulp.watch './src/scripts/browser/**/*', ['restart:compile:' + dist + ':scripts'] 26 | gulp.watch './src/scripts/common/**/*', ['restart:compile:' + dist + ':scripts'] 27 | gulp.watch './src/scripts/renderer/**/*', ['compile:' + dist + ':scripts'] 28 | gulp.watch './src/html/**/*', ['compile:' + dist + ':html'] 29 | gulp.watch './src/package.json', ['restart:compile:' + dist + ':package'] 30 | 31 | # Watch for the current platform by default 32 | gulp.task 'watch', ['watch:' + platform()] 33 | --------------------------------------------------------------------------------