├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── app ├── app.html ├── helpers │ ├── context_menu.js │ └── external_links.js ├── icon-256x256-unread.png ├── icon-256x256.png ├── icon-32x32-unread.png ├── icon-32x32.png └── icon-template.psd ├── appveyor.yml ├── build ├── icon.icns ├── icon.ico └── icons │ └── 512x512.png ├── config ├── env_development.json ├── env_production.json └── env_test.json ├── e2e ├── hello_world.e2e.js └── utils.js ├── gulpfile.js ├── package.json ├── scripts ├── istanbul-reporter.js └── travis-build.sh ├── src ├── app.js ├── background.js ├── env.js ├── helpers │ ├── updater.js │ └── window.js └── menu │ ├── DevelopmentMenuTemplateMenu.js │ ├── File.js │ ├── FileMenu.js │ ├── Help.js │ ├── HelpMenu.js │ ├── RightClick.js │ ├── Tray.js │ ├── TrayMenu.js │ └── dev_menu_template.js └── tasks ├── build_app.js ├── build_tests.js ├── bundle.js ├── start.js └── utils.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | charset = utf-8 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | indent_style = space 14 | indent_size = 2 15 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'airbnb-base/rules/imports', 4 | 'airbnb-base/rules/node.js', 5 | 'airbnb-base/rules/variables', 6 | 'prettier', 7 | ], 8 | env: { 9 | browser: true, 10 | node: true, 11 | jest: true, 12 | }, 13 | plugins: ['import'], 14 | rules: { 15 | 'import/no-extraneous-dependencies': 'off', 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | Thumbs.db 4 | *.log 5 | *.autogenerated 6 | 7 | # ignore everything in 'app' folder what had been generated from 'src' folder 8 | /app/stylesheets 9 | /app/app.js 10 | /app/background.js 11 | /app/env.json 12 | /app/**/*.map 13 | 14 | /dist 15 | 16 | /coverage 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 6 4 | cache: 5 | directories: 6 | - node_modules 7 | script: 8 | - npm run eslint 9 | - npm test 10 | - npm run release 11 | deploy: 12 | provider: releases 13 | api_key: 14 | secure: rRowrv41Kg1vCK3cy/2A4RiBOv9P43zKZ3KBkcHxgXnfh9ZDyIcJmx7eg8QxM5WkHHQHnwPqo8UuQLjin/BBbK7M9VshXE1pVc1Cn93+YqhW+4oiOwOK9LSg5yktMoSZTb4SRraoVEZ9MP+l0mBUxYwl5Ue7cTRAGqD2kWfFMGiSu6tFjlPWNTMlcfMiDlR/zKe5RoKdst0576lhVa7uGUyD6Z6zchHvuUC+56bAyF+4+ZvTcnLkxCQhfrwgu08NkU2S9ObqFSNWmPG97i2DBqhNX5C6LFDgTFKsIl5zen1QzcpYGtaK5IhOEqhlNYLsAxKo0bfEcd1sJN8ET7KJ/yK+jRWrEoW5+7jU7bkY8K5Ey0dlf4Z/nI2wsN0R67ASPUFGkDziKmXWqqZnibJqOHcbFJgGB5+DZKpbbDgWtT0HkSXv1TMf1ft1XalQ34WaP/z5em8Ix7f1dxNk9esOAUFa5aUkL3zLEzNQ7NQFboIJ/ffxYwEBpJvyl3KG3fucTe/tH3/ejHk4ipT59bE7G6aA2Edwde2E0ZHCXknKtAYpbhTMk2hzG+kB3MVrEIiZf+OR9jRrQ/A4Wouwc7HqhxzbB6q0IAtoCRYMGtrG0i2t3bP/gz96CQqzmuXyn4ZBOLOmnF5p67rZrXhxBqas7geh4B30RRHIbKQm+xNKEf8= 15 | file_glob: true 16 | file: "dist/*.AppImage" 17 | on: 18 | tags: true 19 | repo: sportradar/ms-teams-linux 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Vadzim Miashaikin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Microsoft Teams Capsule - unofficial Microsoft Teams app for Linux 2 | 3 | 💀 Project is no longer maintained. 4 | 5 | ## Latest release 6 | **0.5.1** - 12 Dec 2017 7 | 8 | ## Description 9 | Microsoft Teams Capsule is unofficial Microsoft Teams application for Linux, which uses [electron framework](http://electron.atom.io/) to wrap web version of [Microsoft Teams](https://teams.microsoft.com/). 10 | 11 | With this app you will be able to launch web version as an application with no need to keep the browser tap always open. 12 | 13 | Application also offers notifications for unread messages, but it's very limited. 14 | 15 | ## Installation 16 | Applications is distributed in the AppImage format and should run on all common Linux distributions. 17 | 18 | 1. Download installation file 19 | * using direct link: [AppImage](https://github.com/karmainside/ms-teams-linux/releases/download/0.5.1/ms-teams-capsule-0.5.1-x86_64.AppImage) 20 | * using wget: `$ wget https://github.com/karmainside/ms-teams-linux/releases/download/0.5.1/ms-teams-capsule-0.5.1-x86_64.AppImage` 21 | 22 | 2. Make it executable 23 | * `$ chmod a+x ms-teams-capsule*.AppImage` 24 | 25 | 3. Run the app! 26 | * `$ ./ms-teams-capsule*.AppImage` 27 | 28 | You can also get more information about AppImage on the [official website](http://appimage.org/). 29 | 30 | ## Usage 31 | Install the application and launch it. After logging in you will be able to use Microsoft Teams. 32 | Application supports very limited version of notifications. 33 | If you have something unread in chats, you will receive push notification and the icon will change with one with big red dot. 34 | 35 | ### Known issues 36 | If you are stucked with blank white, blue or purple screen, just click File -> Reload, there is no need to relaunch the application. 37 | 38 | ### If you don't see tray icon 39 | On Linux distributions that only have app indicator support, you have to install libappindicator1 to make the tray icon work. 40 | 41 | `$ apt-get install libappindicator1` 42 | 43 | ## Development 44 | This project is based on awesome [electron-boilerplate](https://github.com/szwacz/electron-boilerplate). Please, follow the link to learn more about using the development environment. 45 | 46 | Since I'm quite new to Electron, PR and advices are more than welcome! 47 | 48 | ## License 49 | MIT 50 | -------------------------------------------------------------------------------- /app/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Electron Boilerplate 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |

18 |

19 | Welcome to Electron app running on this magnificent machine. 20 |

21 |

22 | You are in environment. 23 |

24 |
25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/helpers/context_menu.js: -------------------------------------------------------------------------------- 1 | // This gives you default context menu (cut, copy, paste) 2 | // in all input fields and textareas across your app. 3 | 4 | (function() { 5 | 'use strict'; 6 | var remote = require('electron').remote; 7 | var Menu = remote.Menu; 8 | var MenuItem = remote.MenuItem; 9 | 10 | var isAnyTextSelected = function() { 11 | return window.getSelection().toString() !== ''; 12 | }; 13 | 14 | var cut = new MenuItem({ 15 | label: 'Cut', 16 | click: function() { 17 | document.execCommand('cut'); 18 | }, 19 | }); 20 | 21 | var copy = new MenuItem({ 22 | label: 'Copy', 23 | click: function() { 24 | document.execCommand('copy'); 25 | }, 26 | }); 27 | 28 | var paste = new MenuItem({ 29 | label: 'Paste', 30 | click: function() { 31 | document.execCommand('paste'); 32 | }, 33 | }); 34 | 35 | var normalMenu = new Menu(); 36 | normalMenu.append(copy); 37 | 38 | var textEditingMenu = new Menu(); 39 | textEditingMenu.append(cut); 40 | textEditingMenu.append(copy); 41 | textEditingMenu.append(paste); 42 | 43 | document.addEventListener( 44 | 'contextmenu', 45 | function(e) { 46 | switch (e.target.nodeName) { 47 | case 'TEXTAREA': 48 | case 'INPUT': 49 | e.preventDefault(); 50 | textEditingMenu.popup(remote.getCurrentWindow()); 51 | break; 52 | default: 53 | if (isAnyTextSelected()) { 54 | e.preventDefault(); 55 | normalMenu.popup(remote.getCurrentWindow()); 56 | } 57 | } 58 | }, 59 | false 60 | ); 61 | })(); 62 | -------------------------------------------------------------------------------- /app/helpers/external_links.js: -------------------------------------------------------------------------------- 1 | // Convenient way for opening links in external browser, not in the app. 2 | // Useful especially if you have a lot of links to deal with. 3 | // 4 | // Usage: 5 | // 6 | // Every link with class ".js-external-link" will be opened in external browser. 7 | // google 8 | // 9 | // The same behaviour for many links can be achieved by adding 10 | // this class to any parent tag of an anchor tag. 11 | // 15 | 16 | (function() { 17 | 'use strict'; 18 | console.log('asd'); 19 | 20 | var shell = require('electron').shell; 21 | 22 | var supportExternalLinks = function(e) { 23 | var href; 24 | var isExternal = false; 25 | 26 | var checkDomElement = function(element) { 27 | if (element.nodeName === 'A') { 28 | href = element.getAttribute('href'); 29 | } 30 | if (element.getAttribute('target') === '_blank') { 31 | isExternal = true; 32 | } 33 | if (href && isExternal) { 34 | shell.openExternal(href); 35 | e.preventDefault(); 36 | } else if (element.parentElement) { 37 | checkDomElement(element.parentElement); 38 | } 39 | }; 40 | 41 | checkDomElement(e.target); 42 | }; 43 | 44 | document.addEventListener('click', supportExternalLinks, false); 45 | })(); 46 | -------------------------------------------------------------------------------- /app/icon-256x256-unread.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karmainside/ms-teams-linux/1c20adff7d0d75471588176f7eb0489aa4efc308/app/icon-256x256-unread.png -------------------------------------------------------------------------------- /app/icon-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karmainside/ms-teams-linux/1c20adff7d0d75471588176f7eb0489aa4efc308/app/icon-256x256.png -------------------------------------------------------------------------------- /app/icon-32x32-unread.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karmainside/ms-teams-linux/1c20adff7d0d75471588176f7eb0489aa4efc308/app/icon-32x32-unread.png -------------------------------------------------------------------------------- /app/icon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karmainside/ms-teams-linux/1c20adff7d0d75471588176f7eb0489aa4efc308/app/icon-32x32.png -------------------------------------------------------------------------------- /app/icon-template.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karmainside/ms-teams-linux/1c20adff7d0d75471588176f7eb0489aa4efc308/app/icon-template.psd -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | build: off 2 | 3 | os: unstable 4 | 5 | branches: 6 | only: 7 | - master 8 | 9 | skip_tags: true 10 | 11 | environment: 12 | nodejs_version: "6.3.0" 13 | 14 | cache: 15 | - node_modules -> package.json 16 | 17 | install: 18 | - ps: Install-Product node $env:nodejs_version 19 | - npm install npm 20 | - .\node_modules\.bin\npm install 21 | 22 | test_script: 23 | - node --version 24 | - .\node_modules\.bin\npm --version 25 | - .\node_modules\.bin\npm test 26 | - .\node_modules\.bin\npm run e2e 27 | -------------------------------------------------------------------------------- /build/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karmainside/ms-teams-linux/1c20adff7d0d75471588176f7eb0489aa4efc308/build/icon.icns -------------------------------------------------------------------------------- /build/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karmainside/ms-teams-linux/1c20adff7d0d75471588176f7eb0489aa4efc308/build/icon.ico -------------------------------------------------------------------------------- /build/icons/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karmainside/ms-teams-linux/1c20adff7d0d75471588176f7eb0489aa4efc308/build/icons/512x512.png -------------------------------------------------------------------------------- /config/env_development.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "development", 3 | "description": "Add here any environment specific stuff you like." 4 | } 5 | -------------------------------------------------------------------------------- /config/env_production.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "production", 3 | "description": "Add here any environment specific stuff you like." 4 | } 5 | -------------------------------------------------------------------------------- /config/env_test.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test", 3 | "description": "Add here any environment specific stuff you like." 4 | } 5 | -------------------------------------------------------------------------------- /e2e/hello_world.e2e.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import testUtils from './utils'; 3 | 4 | describe('application launch', function () { 5 | 6 | beforeEach(testUtils.beforeEach); 7 | afterEach(testUtils.afterEach); 8 | 9 | it('shows hello world text on screen after launch', function () { 10 | return this.app.client.getText('#greet').then(function (text) { 11 | expect(text).to.equal('Hello World!'); 12 | }); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /e2e/utils.js: -------------------------------------------------------------------------------- 1 | import electron from 'electron'; 2 | import { Application } from 'spectron'; 3 | 4 | var beforeEach = function () { 5 | this.timeout(10000); 6 | this.app = new Application({ 7 | path: electron, 8 | args: ['.'], 9 | startTimeout: 10000, 10 | waitTimeout: 10000, 11 | }); 12 | return this.app.start(); 13 | }; 14 | 15 | var afterEach = function () { 16 | this.timeout(10000); 17 | if (this.app && this.app.isRunning()) { 18 | return this.app.stop(); 19 | } 20 | }; 21 | 22 | export default { 23 | beforeEach: beforeEach, 24 | afterEach: afterEach, 25 | }; 26 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | require('./tasks/build_app'); 2 | require('./tasks/start'); 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ms-teams-capsule", 3 | "productName": "Microsoft Teams Capsule", 4 | "description": "Microsoft Teams Capsule allows to run MS Teams web version as a standalone app", 5 | "version": "0.5.2", 6 | "author": "Vadzim Miashaikin ", 7 | "copyright": "© 2017, Vadzim Miashaikin", 8 | "license": "MIT", 9 | "main": "app/background.js", 10 | "build": { 11 | "appId": "com.karmainsie.ms-teams-capsule", 12 | "files": [ 13 | "app/**/*", 14 | "node_modules/**/*", 15 | "package.json" 16 | ], 17 | "appImage": { 18 | "category": "Network" 19 | } 20 | }, 21 | "scripts": { 22 | "postinstall": "install-app-deps electron-builder install-app-deps", 23 | "build": "gulp build", 24 | "prerelease": "gulp build --env=production", 25 | "release": "build", 26 | "start": "gulp start", 27 | "test": "jest", 28 | "prettier": "prettier --write --print-width 80 --single-quote --trailing-comma es5 \"{config,src,tasks}/**/*.js\"", 29 | "eslint": "eslint \"{config,src,tasks}/**/*.js\"", 30 | "precommit": "lint-staged" 31 | }, 32 | "lint-staged": { 33 | "*.js": [ 34 | "npm run eslint", 35 | "npm run prettier", 36 | "git add" 37 | ] 38 | }, 39 | "dependencies": { 40 | "about-window": "^1.6.1", 41 | "compare-versions": "^3.1.0", 42 | "fs-jetpack": "^1.2.0", 43 | "node-fetch": "^1.7.3" 44 | }, 45 | "devDependencies": { 46 | "electron": "^1.8.2-beta.3", 47 | "electron-builder": "^19.48.3", 48 | "eslint": "^4.13.1", 49 | "eslint-config-airbnb-base": "^12.1.0", 50 | "eslint-config-prettier": "^2.9.0", 51 | "eslint-plugin-import": "^2.2.0", 52 | "gulp": "^3.9.0", 53 | "gulp-batch": "^1.0.5", 54 | "gulp-watch": "^4.3.5", 55 | "husky": "^0.14.3", 56 | "jest": "^21.2.1", 57 | "lint-staged": "^6.0.0", 58 | "minimist": "^1.2.0", 59 | "prettier": "^1.9.2", 60 | "rollup": "^0.44.0" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /scripts/istanbul-reporter.js: -------------------------------------------------------------------------------- 1 | var istanbul = require('istanbul'); 2 | 3 | module.exports = function (runner, options) { 4 | mocha.reporters.Base.call(this, runner); 5 | 6 | var reporterOpts = { dir: 'coverage' }, 7 | reporters = ['text-summary', 'html']; 8 | 9 | options = options || {}; 10 | if (options.reporters) reporters = options.reporters.split(','); 11 | if (process.env.ISTANBUL_REPORTERS) reporters = process.env.ISTANBUL_REPORTERS.split(','); 12 | if (options.reportDir) reporterOpts.dir = options.reportDir; 13 | if (process.env.ISTANBUL_REPORT_DIR) reporterOpts.dir = process.env.ISTANBUL_REPORT_DIR; 14 | 15 | runner.on('end', function(){ 16 | var cov = global.__coverage__ || {}, 17 | collector = new istanbul.Collector(); 18 | 19 | collector.add(cov); 20 | 21 | reporters.forEach(function(reporter) { 22 | istanbul.Report.create(reporter, reporterOpts).writeReport(collector, true); 23 | }); 24 | 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /scripts/travis-build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | git clone https://github.com/creationix/nvm.git /tmp/.nvm 4 | source /tmp/.nvm/nvm.sh 5 | nvm install "$NODE_VERSION" 6 | nvm use --delete-prefix "$NODE_VERSION" 7 | 8 | if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then 9 | export DISPLAY=:99.0 10 | sh -e /etc/init.d/xvfb start 11 | sleep 3 12 | fi 13 | 14 | node --version 15 | npm --version 16 | 17 | npm install 18 | npm test & npm run e2e 19 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | // Here is the starting point for your application code. 2 | // All stuff below is just to show you how it works. You can delete all of it. 3 | 4 | // Use new ES6 modules syntax for everything. 5 | import { remote } from 'electron'; // native electron module 6 | import jetpack from 'fs-jetpack'; // module loaded from npm 7 | import env from './env'; 8 | 9 | console.log('Loaded environment variables:', env); 10 | 11 | const app = remote.app; 12 | const appDir = jetpack.cwd(app.getAppPath()); 13 | 14 | // Holy crap! This is browser window with HTML and stuff, but I can read 15 | // here files like it is node.js! Welcome to Electron world :) 16 | console.log( 17 | 'The author of this app is:', 18 | appDir.read('package.json', 'json').author 19 | ); 20 | 21 | document.addEventListener('DOMContentLoaded', () => { 22 | // DO something 23 | }); 24 | -------------------------------------------------------------------------------- /src/background.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { app, Menu, Tray, shell } from 'electron'; 3 | import DevelopmentMenuTemplateMenu from './menu/DevelopmentMenuTemplateMenu'; 4 | import FileMenu from './menu/FileMenu'; 5 | import HelpMenu from './menu/HelpMenu'; 6 | import TrayMenu from './menu/TrayMenu'; 7 | import HandleRightClick from './menu/RightClick'; 8 | import createWindow from './helpers/window'; 9 | import { checkUpdate } from './helpers/updater'; 10 | 11 | // Special module holding environment variables which you declared 12 | // in config/env_xxx.json file. 13 | import env from './env'; 14 | 15 | const notifRegex = '\([0-9]+\).*?'; 16 | 17 | let appIcon = null; 18 | const iconPath = { 19 | default: path.join(__dirname, 'icon-32x32.png'), 20 | unread: path.join(__dirname, 'icon-32x32-unread.png'), 21 | appDefault: path.join(__dirname, 'icon-256x256.png'), 22 | appUnread: path.join(__dirname, 'icon-256x256-unread.png'), 23 | }; 24 | 25 | const setApplicationMenu = function() { 26 | const menus = [FileMenu, HelpMenu]; 27 | if (env.name !== 'production') { 28 | menus.push(DevelopmentMenuTemplateMenu); 29 | } 30 | Menu.setApplicationMenu(Menu.buildFromTemplate(menus)); 31 | }; 32 | 33 | // Save userData in separate folders for each environment. 34 | // Thanks to this you can use production and development versions of the app 35 | // on same machine like those are two separate apps. 36 | if (env.name !== 'production') { 37 | const userDataPath = app.getPath('userData'); 38 | app.setPath('userData', `${userDataPath} (${env.name})`); 39 | } 40 | 41 | app.on('ready', () => { 42 | setApplicationMenu(); 43 | appIcon = new Tray(iconPath.default); 44 | appIcon.setContextMenu(TrayMenu); 45 | 46 | const mainWindow = createWindow('main', { 47 | width: 1000, 48 | height: 600, 49 | webPreferences: { 50 | partition: 'persist:teams', 51 | nodeIntegration: false, 52 | }, 53 | icon: iconPath.appDefault, 54 | }); 55 | 56 | checkUpdate(); 57 | 58 | mainWindow.webContents.setUserAgent( 59 | 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.2883.87 Safari/537.36' 60 | ); 61 | 62 | mainWindow.loadURL('https://teams.microsoft.com/'); 63 | console.log(mainWindow.id); 64 | 65 | mainWindow.on('page-title-updated', (event, title) => { 66 | if (title.match(notifRegex)) { 67 | // notifier.notify({ 68 | // title: 'Microsoft Teams for Linux', 69 | // message: 'You have new chat message!', 70 | // icon: iconPath.appDefault, 71 | // wait: true 72 | // 73 | // }); 74 | appIcon.setImage(iconPath.unread); 75 | mainWindow.setIcon(iconPath.appUnread); 76 | mainWindow.flashFrame(true); 77 | } else { 78 | appIcon.setImage(iconPath.default); 79 | mainWindow.setIcon(iconPath.appDefault); 80 | mainWindow.flashFrame(false); 81 | } 82 | }); 83 | 84 | // notifier.on('click', function (notifierObject, options) { 85 | // mainWindow.restore(); 86 | // }); 87 | 88 | if (env.name === 'development') { 89 | mainWindow.openDevTools(); 90 | } 91 | 92 | const ignoreOpenInNewWindow = ['teams.microsoft', 'microsoftonline']; 93 | 94 | const handleRedirect = (e, url) => { 95 | let ignoreOpen = false; 96 | 97 | ignoreOpenInNewWindow.forEach(ignoreUrl => { 98 | if (url.toLowerCase().indexOf(ignoreUrl) > -1) { 99 | ignoreOpen = true; 100 | } 101 | }); 102 | 103 | if (!ignoreOpen) { 104 | e.preventDefault(); 105 | shell.openExternal(url); 106 | } 107 | }; 108 | 109 | mainWindow.webContents.on('will-navigate', handleRedirect); 110 | mainWindow.webContents.on('new-window', handleRedirect); 111 | mainWindow.webContents.on('context-menu', (event, props) => 112 | HandleRightClick(event, props, mainWindow)); 113 | }); 114 | 115 | app.on('window-all-closed', () => { 116 | app.quit(); 117 | }); 118 | -------------------------------------------------------------------------------- /src/env.js: -------------------------------------------------------------------------------- 1 | // Simple wrapper exposing environment variables to rest of the code. 2 | 3 | import jetpack from 'fs-jetpack'; 4 | 5 | // The variables have been written to `env.json` by the build process. 6 | const env = jetpack.cwd(__dirname).read('env.json', 'json'); 7 | 8 | export default env; 9 | -------------------------------------------------------------------------------- /src/helpers/updater.js: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | import { app, dialog, shell } from 'electron'; 3 | import compareVersions from 'compare-versions'; 4 | 5 | export function checkUpdate(showModal = false) { 6 | fetch('https://api.github.com/repos/karmainside/ms-teams-linux/releases/latest').then(function(response) { 7 | return response.json(); 8 | }).then(function(j) { 9 | const modal = { 10 | buttons: ['Ok'], 11 | message: 'You are using the latest version (' + app.getVersion() + ')', 12 | url: '', 13 | new: false 14 | } 15 | if (compareVersions(j.tag_name, app.getVersion()) === 1) { 16 | modal.buttons = ['Open download page', 'Not now']; 17 | modal.message = 'New version is available: ' + j.tag_name; 18 | modal.url = j.html_url; 19 | modal.new = true; 20 | } 21 | 22 | if (modal.new || showModal) { 23 | dialog.showMessageBox({ 24 | type: 'info', 25 | buttons: modal.buttons, 26 | message: modal.message 27 | }, function (buttonIndex) { 28 | if (modal.new && buttonIndex === 0) { 29 | shell.openExternal(modal.url); 30 | } 31 | }); 32 | } 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /src/helpers/window.js: -------------------------------------------------------------------------------- 1 | // This helper remembers the size and position of your windows (and restores 2 | // them in that place after app relaunch). 3 | // Can be used for more than one window, just construct many 4 | // instances of it and give each different name. 5 | 6 | import { app, BrowserWindow, screen } from 'electron'; 7 | import jetpack from 'fs-jetpack'; 8 | 9 | export default function(name, options) { 10 | const userDataDir = jetpack.cwd(app.getPath('userData')); 11 | const stateStoreFile = `window-state-${name}.json`; 12 | const defaultSize = { 13 | width: options.width, 14 | height: options.height, 15 | }; 16 | let state = {}; 17 | let win; 18 | 19 | const restore = function() { 20 | let restoredState = {}; 21 | try { 22 | restoredState = userDataDir.read(stateStoreFile, 'json'); 23 | } catch (err) { 24 | // For some reason json can't be read (might be corrupted). 25 | // No worries, we have defaults. 26 | } 27 | return Object.assign({}, defaultSize, restoredState); 28 | }; 29 | 30 | const getCurrentPosition = function() { 31 | const position = win.getPosition(); 32 | const size = win.getSize(); 33 | return { 34 | x: position[0], 35 | y: position[1], 36 | width: size[0], 37 | height: size[1], 38 | }; 39 | }; 40 | 41 | const windowWithinBounds = function(windowState, bounds) { 42 | return windowState.x >= bounds.x && 43 | windowState.y >= bounds.y && 44 | windowState.x + windowState.width <= bounds.x + bounds.width && 45 | windowState.y + windowState.height <= bounds.y + bounds.height; 46 | }; 47 | 48 | const resetToDefaults = function() { 49 | const bounds = screen.getPrimaryDisplay().bounds; 50 | return Object.assign({}, defaultSize, { 51 | x: (bounds.width - defaultSize.width) / 2, 52 | y: (bounds.height - defaultSize.height) / 2, 53 | }); 54 | }; 55 | 56 | const ensureVisibleOnSomeDisplay = function(windowState) { 57 | const visible = screen 58 | .getAllDisplays() 59 | .some(display => windowWithinBounds(windowState, display.bounds)); 60 | if (!visible) { 61 | // Window is partially or fully not visible now. 62 | // Reset it to safe defaults. 63 | return resetToDefaults(windowState); 64 | } 65 | return windowState; 66 | }; 67 | 68 | const saveState = function() { 69 | if (!win.isMinimized() && !win.isMaximized()) { 70 | Object.assign(state, getCurrentPosition()); 71 | } 72 | userDataDir.write(stateStoreFile, state, { atomic: true }); 73 | }; 74 | 75 | state = ensureVisibleOnSomeDisplay(restore()); 76 | 77 | win = new BrowserWindow(Object.assign({}, options, state)); 78 | 79 | win.on('close', saveState); 80 | 81 | return win; 82 | } 83 | -------------------------------------------------------------------------------- /src/menu/DevelopmentMenuTemplateMenu.js: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow } from 'electron'; 2 | 3 | const DevelopmentMenuTemplateMenu = { 4 | label: 'Development', 5 | submenu: [ 6 | { 7 | label: 'Reload', 8 | accelerator: 'CmdOrCtrl+R', 9 | click() { 10 | BrowserWindow.getFocusedWindow().webContents.reloadIgnoringCache(); 11 | }, 12 | }, 13 | { 14 | label: 'Toggle DevTools', 15 | accelerator: 'Alt+CmdOrCtrl+I', 16 | click() { 17 | BrowserWindow.getFocusedWindow().toggleDevTools(); 18 | }, 19 | }, 20 | { 21 | label: 'Quit', 22 | accelerator: 'CmdOrCtrl+Q', 23 | click() { 24 | app.quit(); 25 | }, 26 | }, 27 | ], 28 | }; 29 | 30 | export default DevelopmentMenuTemplateMenu; 31 | -------------------------------------------------------------------------------- /src/menu/File.js: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow, session } from 'electron'; 2 | 3 | export const fileMenu = { 4 | label: 'File', 5 | submenu: [ 6 | { 7 | label: 'Reload', 8 | accelerator: 'CmdOrCtrl+R', 9 | click: () => { 10 | BrowserWindow.getFocusedWindow().webContents.reloadIgnoringCache(); 11 | } 12 | }, 13 | { 14 | label: 'Quit', 15 | accelerator: 'CmdOrCtrl+Q', 16 | click: () => { 17 | app.quit(); 18 | } 19 | } 20 | ] 21 | }; 22 | -------------------------------------------------------------------------------- /src/menu/FileMenu.js: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow } from 'electron'; 2 | 3 | const FileMenu = { 4 | label: 'Menu', 5 | submenu: [ 6 | { 7 | label: 'Reload', 8 | accelerator: 'CmdOrCtrl+R', 9 | click: () => { 10 | BrowserWindow.getFocusedWindow().webContents.reloadIgnoringCache(); 11 | }, 12 | }, 13 | { 14 | label: 'Quit', 15 | accelerator: 'CmdOrCtrl+Q', 16 | click: () => { 17 | app.quit(); 18 | }, 19 | }, 20 | ], 21 | }; 22 | 23 | export default FileMenu; 24 | -------------------------------------------------------------------------------- /src/menu/Help.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { app, BrowserWindow, session } from 'electron'; 3 | import openAboutWindow from 'about-window'; 4 | 5 | const copyrightText = ` 6 |
7 | Copyright (c) 2017 karmainside
8 | Microsoft, Microsoft Teams, Microsoft Teams logo are either registered trademarks or trademarks of Microsoft Corporation in the United States and/or other countries.
9 |
10 | `; 11 | 12 | export const helpMenu = { 13 | label: 'Help', 14 | submenu: [ 15 | { 16 | label: 'About', 17 | accelerator: 'F1', 18 | click: () => openAboutWindow({ 19 | icon_path: path.join(__dirname, 'icon-256x256.png'), 20 | description: 'Whilst waiting for the official version of MS Teams for Linux, you are very free to use this app.', 21 | license: 'MIT', 22 | copyright: copyrightText, 23 | use_inner_html: true, 24 | adjust_window_size: true, 25 | win_options: false 26 | }) 27 | } 28 | ] 29 | }; 30 | -------------------------------------------------------------------------------- /src/menu/HelpMenu.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import openAboutWindow from 'about-window'; 3 | import { checkUpdate } from '../helpers/updater'; 4 | 5 | const copyrightText = ` 6 |
7 | Copyright (c) 2017 karmainside
8 | Microsoft, Microsoft Teams, Microsoft Teams logo are either registered trademarks or trademarks of Microsoft Corporation in the United States and/or other countries.
9 |
10 | `; 11 | 12 | const HelpMenu = { 13 | label: 'Help', 14 | submenu: [ 15 | { 16 | label: 'About', 17 | accelerator: 'F1', 18 | click: () => openAboutWindow({ 19 | icon_path: path.join(__dirname, 'icon-256x256.png'), 20 | description: 'Whilst waiting for the official version of MS Teams for Linux, you are very free to use this app.', 21 | license: 'MIT', 22 | copyright: copyrightText, 23 | use_inner_html: true, 24 | adjust_window_size: true, 25 | win_options: false, 26 | }), 27 | }, 28 | { 29 | label: 'Check for updates...', 30 | click: () => checkUpdate(true), 31 | } 32 | ], 33 | }; 34 | 35 | export default HelpMenu; 36 | -------------------------------------------------------------------------------- /src/menu/RightClick.js: -------------------------------------------------------------------------------- 1 | import { clipboard, Menu } from 'electron'; 2 | 3 | const HandleRightClick = (event, props, windowContext) => { 4 | const { selectionText, linkURL, linkText } = props; 5 | const menuItems = []; 6 | 7 | if (selectionText && selectionText.length > 0) { 8 | menuItems.push({ 9 | role: 'copy', 10 | label: 'Copy', 11 | }); 12 | } 13 | 14 | if (linkURL && linkURL.length > 0) { 15 | menuItems.push({ 16 | label: 'Copy link address', 17 | click: () => { 18 | clipboard.writeText(linkURL); 19 | }, 20 | }); 21 | menuItems.push({ 22 | label: 'Copy link text', 23 | click: () => { 24 | clipboard.writeText(linkText); 25 | }, 26 | }); 27 | } 28 | 29 | if (menuItems.length > 0) { 30 | const rightClickMenu = Menu.buildFromTemplate(menuItems); 31 | rightClickMenu.popup(windowContext); 32 | } 33 | }; 34 | 35 | export default HandleRightClick; 36 | -------------------------------------------------------------------------------- /src/menu/Tray.js: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow, session, Menu } from 'electron'; 2 | 3 | export const trayMenu = Menu.buildFromTemplate([ 4 | { 5 | label: 'Open', 6 | click: () => { 7 | BrowserWindow.fromId(1).show(); 8 | } 9 | }, 10 | { 11 | label: 'Reload', 12 | click: () => { 13 | BrowserWindow.fromId(1).show(); 14 | BrowserWindow.getFocusedWindow().webContents.reloadIgnoringCache(); 15 | } 16 | }, 17 | { 18 | label: 'Quit', 19 | click: () => { 20 | app.quit(); 21 | } 22 | } 23 | ]); 24 | -------------------------------------------------------------------------------- /src/menu/TrayMenu.js: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow, Menu } from 'electron'; 2 | 3 | const TrayMenu = Menu.buildFromTemplate([ 4 | { 5 | label: 'Open', 6 | click: () => { 7 | BrowserWindow.fromId(1).show(); 8 | }, 9 | }, 10 | { 11 | label: 'Reload', 12 | click: () => { 13 | BrowserWindow.fromId(1).show(); 14 | BrowserWindow.getFocusedWindow().webContents.reloadIgnoringCache(); 15 | }, 16 | }, 17 | { 18 | label: 'Quit', 19 | click: () => { 20 | app.quit(); 21 | }, 22 | }, 23 | ]); 24 | 25 | export default TrayMenu; 26 | -------------------------------------------------------------------------------- /src/menu/dev_menu_template.js: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow } from 'electron'; 2 | 3 | export var devMenuTemplate = { 4 | label: 'Development', 5 | submenu: [{ 6 | label: 'Reload', 7 | accelerator: 'CmdOrCtrl+R', 8 | click: function () { 9 | BrowserWindow.getFocusedWindow().webContents.reloadIgnoringCache(); 10 | } 11 | },{ 12 | label: 'Toggle DevTools', 13 | accelerator: 'Alt+CmdOrCtrl+I', 14 | click: function () { 15 | BrowserWindow.getFocusedWindow().toggleDevTools(); 16 | } 17 | },{ 18 | label: 'Quit', 19 | accelerator: 'CmdOrCtrl+Q', 20 | click: function () { 21 | app.quit(); 22 | } 23 | }] 24 | }; 25 | -------------------------------------------------------------------------------- /tasks/build_app.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const watch = require('gulp-watch'); 3 | const batch = require('gulp-batch'); 4 | const jetpack = require('fs-jetpack'); 5 | const bundle = require('./bundle'); 6 | const utils = require('./utils'); 7 | 8 | const projectDir = jetpack; 9 | const srcDir = jetpack.cwd('./src'); 10 | const destDir = jetpack.cwd('./app'); 11 | 12 | gulp.task('bundle', () => 13 | Promise.all([ 14 | bundle(srcDir.path('background.js'), destDir.path('background.js')), 15 | bundle(srcDir.path('app.js'), destDir.path('app.js')), 16 | ])); 17 | 18 | gulp.task('environment', () => { 19 | const configFile = `config/env_${utils.getEnvName()}.json`; 20 | projectDir.copy(configFile, destDir.path('env.json'), { overwrite: true }); 21 | }); 22 | 23 | gulp.task('watch', () => { 24 | const beepOnError = function(done) { 25 | return function(err) { 26 | if (err) { 27 | utils.beepSound(); 28 | } 29 | done(err); 30 | }; 31 | }; 32 | 33 | watch( 34 | 'src/**/*.js', 35 | batch((events, done) => { 36 | gulp.start('bundle', beepOnError(done)); 37 | }) 38 | ); 39 | }); 40 | 41 | gulp.task('build', ['bundle', 'environment']); 42 | -------------------------------------------------------------------------------- /tasks/build_tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | var jetpack = require('fs-jetpack'); 5 | var bundle = require('./bundle'); 6 | var istanbul = require('rollup-plugin-istanbul'); 7 | 8 | // Spec files are scattered through the whole project. Here we're searching 9 | // for them and generate one entry file which will run all the tests. 10 | var generateEntryFile = function (dir, destFileName, filePattern) { 11 | var fileBanner = "// This file is generated automatically.\n" 12 | + "// All modifications will be lost.\n"; 13 | 14 | return dir.findAsync('.', { matching: filePattern }) 15 | .then(function (specPaths) { 16 | var fileContent = specPaths.map(function (path) { 17 | return 'import "./' + path.replace(/\\/g, '/') + '";'; 18 | }).join('\n'); 19 | return dir.writeAsync(destFileName, fileBanner + fileContent); 20 | }) 21 | .then(function () { 22 | return dir.path(destFileName); 23 | }); 24 | }; 25 | 26 | gulp.task('build-unit', ['environment'], function () { 27 | var srcDir = jetpack.cwd('src'); 28 | var destDir = jetpack.cwd('app'); 29 | 30 | return generateEntryFile(srcDir, 'specs.js.autogenerated', '*.spec.js') 31 | .then(function (entryFilePath) { 32 | return bundle(entryFilePath, destDir.path('specs.js.autogenerated'), { 33 | rollupPlugins: [ 34 | istanbul({ 35 | exclude: ['**/*.spec.js', '**/specs.js.autogenerated'], 36 | sourceMap: true 37 | }) 38 | ] 39 | }); 40 | }); 41 | }); 42 | 43 | gulp.task('build-e2e', ['build'], function () { 44 | var srcDir = jetpack.cwd('e2e'); 45 | var destDir = jetpack.cwd('app'); 46 | 47 | return generateEntryFile(srcDir, 'e2e.js.autogenerated', '*.e2e.js') 48 | .then(function (entryFilePath) { 49 | return bundle(entryFilePath, destDir.path('e2e.js.autogenerated')); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /tasks/bundle.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const jetpack = require('fs-jetpack'); 3 | const rollup = require('rollup').rollup; 4 | 5 | const nodeBuiltInModules = [ 6 | 'assert', 7 | 'buffer', 8 | 'child_process', 9 | 'cluster', 10 | 'console', 11 | 'constants', 12 | 'crypto', 13 | 'dgram', 14 | 'dns', 15 | 'domain', 16 | 'events', 17 | 'fs', 18 | 'http', 19 | 'https', 20 | 'module', 21 | 'net', 22 | 'os', 23 | 'path', 24 | 'process', 25 | 'punycode', 26 | 'querystring', 27 | 'readline', 28 | 'repl', 29 | 'stream', 30 | 'string_decoder', 31 | 'timers', 32 | 'tls', 33 | 'tty', 34 | 'url', 35 | 'util', 36 | 'v8', 37 | 'vm', 38 | 'zlib', 39 | ]; 40 | 41 | const electronBuiltInModules = ['electron']; 42 | 43 | const generateExternalModulesList = function() { 44 | const appManifest = jetpack.read('./package.json', 'json'); 45 | return [].concat( 46 | nodeBuiltInModules, 47 | electronBuiltInModules, 48 | Object.keys(appManifest.dependencies), 49 | Object.keys(appManifest.devDependencies) 50 | ); 51 | }; 52 | 53 | const cached = {}; 54 | 55 | module.exports = function(src, dest, opts) { 56 | opts = opts || {}; 57 | opts.rollupPlugins = opts.rollupPlugins || []; 58 | return rollup({ 59 | entry: src, 60 | external: generateExternalModulesList(), 61 | cache: cached[src], 62 | plugins: opts.rollupPlugins, 63 | }).then(bundle => { 64 | cached[src] = bundle; 65 | 66 | const jsFile = path.basename(dest); 67 | const result = bundle.generate({ 68 | format: 'cjs', 69 | sourceMap: true, 70 | sourceMapFile: jsFile, 71 | }); 72 | // Wrap code in self invoking function so the variables don't 73 | // pollute the global namespace. 74 | const isolatedCode = `(function () {${result.code}\n}());`; 75 | return Promise.all([ 76 | jetpack.writeAsync( 77 | dest, 78 | `${isolatedCode}\n//# sourceMappingURL=${jsFile}.map` 79 | ), 80 | jetpack.writeAsync(`${dest}.map`, result.map.toString()), 81 | ]); 82 | }); 83 | }; 84 | -------------------------------------------------------------------------------- /tasks/start.js: -------------------------------------------------------------------------------- 1 | const childProcess = require('child_process'); 2 | const electron = require('electron'); 3 | const gulp = require('gulp'); 4 | 5 | gulp.task('start', ['build', 'watch'], () => { 6 | childProcess 7 | .spawn(electron, ['.'], { 8 | stdio: 'inherit', 9 | }) 10 | .on('close', () => { 11 | // User closed the app. Kill the host process. 12 | process.exit(); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /tasks/utils.js: -------------------------------------------------------------------------------- 1 | const argv = require('minimist')(process.argv); 2 | 3 | exports.getEnvName = function() { 4 | return argv.env || 'development'; 5 | }; 6 | 7 | exports.beepSound = function() { 8 | process.stdout.write('\u0007'); 9 | }; 10 | --------------------------------------------------------------------------------