summaryrefslogtreecommitdiffhomepage
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/actions/connect.js2
-rw-r--r--app/app.js31
-rw-r--r--app/assets/css/style.css2
-rw-r--r--app/assets/images/app-triangle-error.svg5
-rw-r--r--app/assets/images/app-triangle-success.svg5
-rw-r--r--app/assets/images/app-triangle.svg5
-rw-r--r--app/assets/images/trayIconTemplate.png (renamed from app/assets/images/trayicon.png)bin950 -> 950 bytes
-rw-r--r--app/assets/images/trayIconTemplate@2x.png (renamed from app/assets/images/trayicon@2x.png)bin1316 -> 1316 bytes
-rw-r--r--app/components/Connect.js9
-rw-r--r--app/components/HeaderBar.css29
-rw-r--r--app/components/HeaderBar.js2
-rw-r--r--app/components/Layout.css2
-rw-r--r--app/components/Layout.js4
-rw-r--r--app/components/Tray.js39
-rw-r--r--app/containers/ConnectPage.js11
-rw-r--r--app/containers/Tray.js14
-rw-r--r--app/lib/components/TrayMenu.js120
-rw-r--r--app/main.js180
-rw-r--r--app/store.js5
19 files changed, 192 insertions, 273 deletions
diff --git a/app/actions/connect.js b/app/actions/connect.js
index b1c6ea436a..ff8b4c5632 100644
--- a/app/actions/connect.js
+++ b/app/actions/connect.js
@@ -1 +1 @@
-export default {}
+export default {};
diff --git a/app/app.js b/app/app.js
index 6ac3ce492f..8789608887 100644
--- a/app/app.js
+++ b/app/app.js
@@ -3,21 +3,27 @@ import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { Router, hashHistory } from 'react-router';
import { syncHistoryWithStore } from 'react-router-redux';
-import { remote, webFrame } from 'electron';
-import path from 'path';
+import { webFrame } from 'electron';
import routes from './routes';
import configureStore from './store';
-import Tray from './containers/Tray';
import Backend from './lib/backend';
const backend = new Backend();
-const iconPath = path.join(__dirname, 'assets/images/trayicon.png');
-const tray = new remote.Tray(iconPath);
-
const initialState = {};
const store = configureStore(initialState);
-const routerHistory = syncHistoryWithStore(hashHistory, store);
+
+// desperately trying to fix https://github.com/reactjs/react-router-redux/issues/534
+hashHistory.replace('/');
+
+// see https://github.com/reactjs/react-router-redux/issues/534
+const recentLocation = (store.getState().routing || {}).locationBeforeTransitions;
+const routerHistory = syncHistoryWithStore(hashHistory, store, { adjustUrlOnReplay: true });
+
+if(recentLocation && recentLocation.pathname) {
+ routerHistory.replace(recentLocation.pathname);
+}
+
const rootElement = document.querySelector(document.currentScript.getAttribute('data-container'));
// disable smart pinch.
@@ -32,13 +38,8 @@ const createElement = (Component, props) => {
};
ReactDOM.render(
- <div>
- <Provider store={ store }>
- <Router history={ routerHistory } routes={ routes } createElement={ createElement } />
- </Provider>
- <Provider store={ store }>
- <Tray handle={ tray } backend={ backend } />
- </Provider>
- </div>,
+ <Provider store={ store }>
+ <Router history={ routerHistory } routes={ routes } createElement={ createElement } />
+ </Provider>,
rootElement
);
diff --git a/app/assets/css/style.css b/app/assets/css/style.css
index fa883fee98..3b8706f63e 100644
--- a/app/assets/css/style.css
+++ b/app/assets/css/style.css
@@ -2,6 +2,6 @@
@import 'fonts.css';
@import 'global.css';
@import '../../components/Login.css';
-@import '../../components/LoggedIn.css';
+@import '../../components/Connect.css';
@import '../../components/HeaderBar.css';
@import '../../components/Layout.css';
diff --git a/app/assets/images/app-triangle-error.svg b/app/assets/images/app-triangle-error.svg
new file mode 100644
index 0000000000..2914d7e389
--- /dev/null
+++ b/app/assets/images/app-triangle-error.svg
@@ -0,0 +1,5 @@
+<svg width="30px" height="13px" viewBox="0 0 30 13" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <title>app-triangle-extended</title>
+ <desc>Mullvad VPN app</desc>
+ <path fill="#D0021B" d="M0,12 L30,12 L30,13 L0,13 L0,12 Z M0,12 C7.24137931,12 12.9310345,1.0135008e-16 15,0 C17.0689655,0 23.7931034,12 30,12 L0,12 Z" id="app-triangle-extended"></path>
+</svg> \ No newline at end of file
diff --git a/app/assets/images/app-triangle-success.svg b/app/assets/images/app-triangle-success.svg
new file mode 100644
index 0000000000..711e99afd7
--- /dev/null
+++ b/app/assets/images/app-triangle-success.svg
@@ -0,0 +1,5 @@
+<svg width="30px" height="13px" viewBox="0 0 30 13" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <title>app-triangle-extended</title>
+ <desc>Mullvad VPN app</desc>
+ <path fill="#44AD4D" d="M0,12 L30,12 L30,13 L0,13 L0,12 Z M0,12 C7.24137931,12 12.9310345,1.0135008e-16 15,0 C17.0689655,0 23.7931034,12 30,12 L0,12 Z" id="app-triangle-extended"></path>
+</svg> \ No newline at end of file
diff --git a/app/assets/images/app-triangle.svg b/app/assets/images/app-triangle.svg
new file mode 100644
index 0000000000..e2f7ca044b
--- /dev/null
+++ b/app/assets/images/app-triangle.svg
@@ -0,0 +1,5 @@
+<svg width="30px" height="13px" viewBox="0 0 30 13" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <title>app-triangle-extended</title>
+ <desc>Mullvad VPN app</desc>
+ <path fill="#294D73" d="M0,12 L30,12 L30,13 L0,13 L0,12 Z M0,12 C7.24137931,12 12.9310345,1.0135008e-16 15,0 C17.0689655,0 23.7931034,12 30,12 L0,12 Z" id="app-triangle-extended"></path>
+</svg> \ No newline at end of file
diff --git a/app/assets/images/trayicon.png b/app/assets/images/trayIconTemplate.png
index 3888745a8f..3888745a8f 100644
--- a/app/assets/images/trayicon.png
+++ b/app/assets/images/trayIconTemplate.png
Binary files differ
diff --git a/app/assets/images/trayicon@2x.png b/app/assets/images/trayIconTemplate@2x.png
index 3b5f364903..3b5f364903 100644
--- a/app/assets/images/trayicon@2x.png
+++ b/app/assets/images/trayIconTemplate@2x.png
Binary files differ
diff --git a/app/components/Connect.js b/app/components/Connect.js
index dc32594067..27aaedd4ee 100644
--- a/app/components/Connect.js
+++ b/app/components/Connect.js
@@ -1,14 +1,19 @@
-import React, { Component } from 'react';
+import React, { Component, PropTypes } from 'react';
import { Layout, Container, Header } from './Layout';
export default class Connect extends Component {
+
+ static propTypes = {
+ logout: PropTypes.func.isRequired
+ }
+
render() {
return (
<Layout>
<Header />
<Container>
<div className="connect">
-
+ <button style={{ width: '100px', display: 'block', margin: '10px auto' }} onClick={ this.props.logout }>Log out</button>
</div>
</Container>
</Layout>
diff --git a/app/components/HeaderBar.css b/app/components/HeaderBar.css
index 4d9d326eff..140e64df55 100644
--- a/app/components/HeaderBar.css
+++ b/app/components/HeaderBar.css
@@ -1,15 +1,40 @@
.headerbar {
+ margin-top: 12px;
padding: 12px;
+ background-color: #294D73;
+ border-radius: 8px 8px 0 0;
+ position: relative;
}
-.header--style-error {
+.headerbar:before {
+ display: block;
+ content: '';
+ width: 30px;
+ height: 13px;
+ margin: -12px 0 0 -15px;
+ position: absolute;
+ left: 50%;
+ top: 0%;
+ background-image: url(../assets/images/app-triangle.svg);
+ background-repeat: no-repeat;
+}
+
+.headerbar--style-error {
background-color: #D0021B;
}
-.header--style-success {
+.headerbar--style-error:before {
+ background-image: url(../assets/images/app-triangle-error.svg);
+}
+
+.headerbar--style-success {
background-color: #44AD4D;
}
+.headerbar--style-success:before {
+ background-image: url(../assets/images/app-triangle-success.svg);
+}
+
.headerbar__title {
font-family: DINPro;
font-size: 24px;
diff --git a/app/components/HeaderBar.js b/app/components/HeaderBar.js
index d13c5c3462..ce5d039f1f 100644
--- a/app/components/HeaderBar.js
+++ b/app/components/HeaderBar.js
@@ -15,7 +15,7 @@ export default class HeaderBar extends Component {
let containerClass = ['headerbar'];
if(HeaderBar.Style.isValid(style)) {
- containerClass.push(`header--style-${style}`);
+ containerClass.push(`headerbar--style-${style}`);
}
return (
diff --git a/app/components/Layout.css b/app/components/Layout.css
index 8ef242d7f5..5d1aa92445 100644
--- a/app/components/Layout.css
+++ b/app/components/Layout.css
@@ -2,7 +2,6 @@
display: flex;
flex-direction: column;
height: 100vh;
- background: #294D73;
}
.layout__header {
@@ -11,4 +10,5 @@
.layout__container {
flex: 1 1 100%;
+ background: #294D73;
} \ No newline at end of file
diff --git a/app/components/Layout.js b/app/components/Layout.js
index b0cc0a8420..50f52e7dca 100644
--- a/app/components/Layout.js
+++ b/app/components/Layout.js
@@ -10,7 +10,7 @@ export class Header extends Component {
<div className="layout__header">
<HeaderBar {...this.props} />
</div>
- )
+ );
}
}
@@ -24,7 +24,7 @@ export class Container extends Component {
<div className="layout__container">
{this.props.children}
</div>
- )
+ );
}
}
diff --git a/app/components/Tray.js b/app/components/Tray.js
deleted file mode 100644
index 00dc679981..0000000000
--- a/app/components/Tray.js
+++ /dev/null
@@ -1,39 +0,0 @@
-import React, { Component, PropTypes } from 'react';
-import { TrayMenu, TrayItem } from '../lib/components/TrayMenu';
-import { shell } from 'electron';
-
-import { LoginState } from '../constants';
-
-export default class Tray extends Component {
-
- static propTypes = {
- handle: PropTypes.object.isRequired,
- backend: PropTypes.object.isRequired
- }
-
- logout() {
- this.props.logout(this.props.backend);
- }
-
- openPrivacyPolicy() {
- shell.openExternal('https://mullvad.net/#privacy');
- }
-
- openHomepage() {
- shell.openExternal('https://mullvad.net');
- }
-
- render() {
- const loggedIn = this.props.user.status === LoginState.ok;
-
- return (
- <TrayMenu tray={ this.props.handle }>
- <TrayItem label="Log out" click={ ::this.logout } visible={ loggedIn } />
- <TrayItem type="separator" visible={ loggedIn } />
- <TrayItem label="Privacy Policy" click={ ::this.openPrivacyPolicy } />
- <TrayItem label="Visit homepage" click={ ::this.openHomepage } />
- </TrayMenu>
- );
- }
-
-} \ No newline at end of file
diff --git a/app/containers/ConnectPage.js b/app/containers/ConnectPage.js
index 9aad1f81f5..9802e8b2d8 100644
--- a/app/containers/ConnectPage.js
+++ b/app/containers/ConnectPage.js
@@ -1,12 +1,19 @@
import { connect } from 'react-redux';
+import { bindActionCreators } from 'redux';
import Connect from '../components/Connect';
+import userActions from '../actions/user';
const mapStateToProps = (state) => {
return state;
};
-const mapDispatchToProps = (dispatch) => { // eslint-disable-line no-unused-vars
- return {};
+const mapDispatchToProps = (dispatch, props) => { // eslint-disable-line no-unused-vars
+ const user = bindActionCreators(userActions, dispatch);
+ return {
+ logout: () => {
+ return user.logout(props.backend);
+ }
+ };
};
export default connect(mapStateToProps, mapDispatchToProps)(Connect);
diff --git a/app/containers/Tray.js b/app/containers/Tray.js
deleted file mode 100644
index 3916263d0d..0000000000
--- a/app/containers/Tray.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import { connect } from 'react-redux';
-import { bindActionCreators } from 'redux';
-import Tray from '../components/Tray';
-import userActions from '../actions/user';
-
-const mapStateToProps = (state) => {
- return state;
-};
-
-const mapDispatchToProps = (dispatch) => {
- return bindActionCreators(userActions, dispatch);
-};
-
-export default connect(mapStateToProps, mapDispatchToProps)(Tray);
diff --git a/app/lib/components/TrayMenu.js b/app/lib/components/TrayMenu.js
deleted file mode 100644
index 5f14ca71f9..0000000000
--- a/app/lib/components/TrayMenu.js
+++ /dev/null
@@ -1,120 +0,0 @@
-/**
- * Declarative Tray implementation for React + Electron
- */
-
-import React, { Component, PropTypes } from 'react';
-import { remote } from 'electron';
-
-const { Menu, MenuItem } = remote;
-
-/**
- * Tray menu component
- *
- * Example:
- *
- * const tray = new remote.Tray('/path/to/icon');
- *
- * return (
- * <TrayMenu tray={tray}>
- * <TrayItem label="Visit homepage" />
- * </TrayMenu>
- * )
- */
-export class TrayMenu extends Component {
-
- static childContextTypes = {
- menu: PropTypes.object.isRequired
- };
-
- static propTypes = {
- tray: PropTypes.object.isRequired,
- children: PropTypes.arrayOf(PropTypes.node).isRequired
- };
-
- _contextMenu = null;
-
- getChildContext() {
- return { menu: this._contextMenu };
- }
-
- componentDidMount() {
- this.props.tray.setContextMenu(this._contextMenu);
- }
-
- componentDidUpdate() {
- this.props.tray.setContextMenu(this._contextMenu);
- }
-
- render() {
- // create new menu during each rendering
- // see: https://github.com/electron/electron/issues/8598
- this._contextMenu = new Menu();
-
- return (
- <div>{this.props.children}</div>
- );
- }
-
-}
-
-/**
- * Submenu component
- *
- * Example:
- *
- * <TrayMenu tray={this.props.handle}>
- * <TraySubmenu label="Resources">
- * <TrayItem label="Homepage" />
- * </TraySubmenu>
- * </TrayMenu>
- *
- */
-export class TraySubmenu extends Component {
-
- static contextTypes = {
- menu: PropTypes.object.isRequired
- };
-
- static childContextTypes = {
- menu: PropTypes.object.isRequired
- };
-
- static propTypes = {
- children: PropTypes.arrayOf(PropTypes.node).isRequired
- };
-
- _contextMenu = null;
-
- getChildContext() {
- return { menu: this._contextMenu };
- }
-
- render() {
- // create new menu during each rendering
- // see: https://github.com/electron/electron/issues/8598
- this._contextMenu = new Menu();
-
- this.context.menu.append(new MenuItem({ ...this.props, submenu: this._contextMenu }));
-
- return (
- <div>{this.props.children}</div>
- );
- }
-
-}
-
-/**
- * Item component
- */
-export class TrayItem extends Component {
-
- static contextTypes = {
- menu: PropTypes.object.isRequired
- };
-
- render() {
- this.context.menu.append(new MenuItem(this.props));
- return null;
- }
-
-}
diff --git a/app/main.js b/app/main.js
index caea31ab57..81e5d39e4c 100644
--- a/app/main.js
+++ b/app/main.js
@@ -1,11 +1,13 @@
import path from 'path';
-import url from 'url';
-import { app, crashReporter, BrowserWindow, Menu } from 'electron';
+import { app, crashReporter, BrowserWindow, ipcMain, Tray, Menu } from 'electron';
const isDevelopment = (process.env.NODE_ENV === 'development');
-let mainWindow = null;
-let forceQuit = false;
+let window = null;
+let tray = null;
+
+// hide dock icon
+app.dock.hide();
const installExtensions = async () => {
const installer = require('electron-devtools-installer');
@@ -14,7 +16,7 @@ const installExtensions = async () => {
'REDUX_DEVTOOLS'
];
const forceDownload = !!process.env.UPGRADE_EXTENSIONS;
- for (const name of extensions) {
+ for(const name of extensions) {
try {
await installer.default(installer[name], forceDownload);
} catch (e) {
@@ -23,86 +25,122 @@ const installExtensions = async () => {
}
};
-crashReporter.start({
- productName: 'YourName',
- companyName: 'YourCompany',
- submitURL: 'https://your-domain.com/url-to-submit',
- uploadToServer: false
-});
+const installDevTools = async () => {
+ await installExtensions();
-app.on('window-all-closed', () => {
- // On OS X it is common for applications and their menu bar
- // to stay active until the user quits explicitly with Cmd + Q
- if (process.platform !== 'darwin') {
- app.quit();
- }
-});
+ // show devtools when ctrl clicked
+ tray.on('click', function () {
+ if(!window) { return; }
-app.on('ready', async () => {
- if (isDevelopment) {
- await installExtensions();
- }
+ if(window.isDevToolsOpened()) {
+ window.devToolsWebContents.focus();
+ } else {
+ window.openDevTools({ mode: 'detach' });
+ }
+ });
+
+ // add inspect element on right click menu
+ window.webContents.on('context-menu', (e, props) => {
+ Menu.buildFromTemplate([{
+ label: 'Inspect element',
+ click() {
+ window.openDevTools({ mode: 'detach' });
+ window.inspectElement(props.x, props.y);
+ }
+ }]).popup(window);
+ });
+};
+
+const getWindowPosition = () => {
+ const windowBounds = window.getBounds();
+ const trayBounds = tray.getBounds();
+
+ // center window horizontally below the tray icon
+ const x = Math.round(trayBounds.x + (trayBounds.width / 2) - (windowBounds.width / 2));
- mainWindow = new BrowserWindow({
+ // position window vertically below the tray icon
+ const y = Math.round(trayBounds.y + trayBounds.height);
+
+ return { x, y };
+};
+
+const createWindow = () => {
+ window = new BrowserWindow({
width: 320,
- height: 568 + 20, // 20pt window chrome
- show: false,
- backgroundColor: '#000000',
+ height: 568,
+ frame: false,
resizable: false,
maximizable: false,
- fullscreenable: false
+ fullscreenable: false,
+ transparent: true,
+ show: false,
+ webPreferences: {
+ // prevents renderer process code from not running when window is hidden
+ backgroundThrottling: false
+ }
});
- mainWindow.loadURL(url.format({
- pathname: path.join(__dirname, 'index.html'),
- protocol: 'file:',
- slashes: true
- }));
+ window.loadURL('file://' + path.join(__dirname, 'index.html'));
- // show window once on first load
- mainWindow.webContents.once('did-finish-load', () => {
- mainWindow.show();
+ // hide the window when it loses focus
+ window.on('blur', () => {
+ if(!window.webContents.isDevToolsOpened()) {
+ window.hide();
+ }
});
- mainWindow.webContents.on('did-finish-load', () => {
- // Handle window logic properly on macOS:
- // 1. App should not terminate if window has been closed
- // 2. Click on icon in dock should re-open the window
- // 3. ⌘+Q should close the window and quit the app
- if (process.platform === 'darwin') {
- mainWindow.on('close', function (e) {
- if (!forceQuit) {
- e.preventDefault();
- mainWindow.hide();
- }
- });
+ window.on('show', () => {
+ tray.setHighlightMode('always');
+ });
- app.on('activate', () => {
- mainWindow.show();
- });
-
- app.on('before-quit', () => {
- forceQuit = true;
- });
- } else {
- mainWindow.on('closed', () => {
- mainWindow = null;
- });
- }
+ window.on('hide', () => {
+ tray.setHighlightMode('never');
});
- if (isDevelopment) {
- // auto-open dev tools
- mainWindow.webContents.openDevTools();
+};
+
+const toggleWindow = () => {
+ if (window.isVisible()) {
+ window.hide();
+ } else {
+ showWindow();
+ }
+};
+
+const showWindow = () => {
+ const position = getWindowPosition();
+ window.setPosition(position.x, position.y, false);
+ window.show();
+ window.focus();
+};
+
+ipcMain.on('show-window', () => {
+ showWindow();
+});
+
+const createTray = () => {
+ tray = new Tray(path.join(__dirname, 'assets/images/trayIconTemplate.png'));
+ tray.on('right-click', toggleWindow);
+ tray.on('double-click', toggleWindow);
+ tray.on('click', toggleWindow);
+};
+
+crashReporter.start({
+ productName: 'YourName',
+ companyName: 'YourCompany',
+ submitURL: 'https://your-domain.com/url-to-submit',
+ uploadToServer: false
+});
+
+app.on('window-all-closed', () => {
+ app.quit();
+});
+
+app.on('ready', () => {
+ createTray();
+ createWindow();
- // add inspect element on right click menu
- mainWindow.webContents.on('context-menu', (e, props) => {
- Menu.buildFromTemplate([{
- label: 'Inspect element',
- click() {
- mainWindow.inspectElement(props.x, props.y);
- }
- }]).popup(mainWindow);
- });
+ if(isDevelopment) {
+ installDevTools();
}
});
diff --git a/app/store.js b/app/store.js
index 350650631b..55a3df85f8 100644
--- a/app/store.js
+++ b/app/store.js
@@ -1,6 +1,6 @@
import { createStore, applyMiddleware, combineReducers, compose } from 'redux';
import { hashHistory } from 'react-router';
-import { routerMiddleware, routerReducer as routing, push } from 'react-router-redux';
+import { routerMiddleware, routerReducer as routing, push, replace } from 'react-router-redux';
import persistState from 'redux-localstorage';
import thunk from 'redux-thunk';
@@ -11,7 +11,8 @@ const router = routerMiddleware(hashHistory);
const actionCreators = {
...userActions,
- push
+ pushRoute: (route) => push(route),
+ replaceRoute: (route) => replace(route),
};
const reducers = {