├── .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 | //
12 | // google
13 | // bing
14 | //
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 |
--------------------------------------------------------------------------------