summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorelijah <elijah@riseup.net>2017-04-24 00:53:41 -0700
committerKali Kaneko (leap communications) <kali@leap.se>2017-04-24 11:12:26 +0200
commit9d301349ab434dc744546918fe026d50485a0797 (patch)
tree802bec24bb446c9ed3af11f7ccfcd9793250b070
parent2976cf11e451f1086d98eae20bdfb0fffa87abb0 (diff)
[feat] usable vpn ui
-rw-r--r--ui/app/app.js18
-rw-r--r--ui/app/components/main_panel/email_section.js6
-rw-r--r--ui/app/components/main_panel/index.js3
-rw-r--r--ui/app/components/main_panel/main_panel.less34
-rw-r--r--ui/app/components/main_panel/section_layout.js10
-rw-r--r--ui/app/components/main_panel/user_section.js4
-rw-r--r--ui/app/components/main_panel/vpn_section.js312
-rw-r--r--ui/app/components/wizard/wizard.less1
-rw-r--r--ui/app/css/bootstrap.less69
-rw-r--r--ui/app/css/colors.less329
-rw-r--r--ui/app/css/common.less (renamed from ui/app/css/common.css)22
-rw-r--r--ui/app/index.html5
-rw-r--r--ui/app/lib/bitmask.js36
-rw-r--r--ui/app/lib/event_logger.js2
-rw-r--r--ui/app/models/account.js33
-rw-r--r--ui/package.json4
-rw-r--r--ui/webpack.config.js28
17 files changed, 793 insertions, 123 deletions
diff --git a/ui/app/app.js b/ui/app/app.js
index ea7f0f52..fe1cc47d 100644
--- a/ui/app/app.js
+++ b/ui/app/app.js
@@ -2,6 +2,9 @@ import bitmask from 'lib/bitmask'
import Account from 'models/account'
import Provider from 'models/provider'
+require('css/bootstrap.less')
+require('css/common.less')
+
class Application {
constructor() {
}
@@ -15,6 +18,13 @@ class Application {
this.start()
}
+ //
+ // (1) check to see if any accounts are authenticated.
+ // if any are, show main panel
+ // (2) check to see if any accounts are 'vpn ready'.
+ // if any are, show main panel.
+ // (3) otherwise, show login greeter
+ //
start() {
Provider.list(false).then(
domains => {
@@ -22,7 +32,13 @@ class Application {
Account.active().then(
accounts => {
if (0 == accounts.length) {
- this.show('greeter')
+ Account.vpnReady().then(accounts => {
+ if (0 == accounts.length) {
+ this.show('greeter', {showLogin: true})
+ } else {
+ this.show('main', {initialAccount: accounts[0]})
+ }
+ })
} else {
accounts.forEach(account => {
Account.addActive(account)
diff --git a/ui/app/components/main_panel/email_section.js b/ui/app/components/main_panel/email_section.js
index cab637be..fcfd36cf 100644
--- a/ui/app/components/main_panel/email_section.js
+++ b/ui/app/components/main_panel/email_section.js
@@ -30,11 +30,11 @@ export default class EmailSection extends React.Component {
componentWillMount() {
this.updateStatus(this.props.account.address)
- bitmask.events.register("MAIL_STATUS_CHANGED", this.updateStatus)
+ bitmask.events.register("MAIL_STATUS_CHANGED", 'email section update', this.updateStatus)
}
componentWillUnmount() {
- bitmask.events.unregister("MAIL_STATUS_CHANGED")
+ bitmask.events.unregister("MAIL_STATUS_CHANGED", 'email section update', this.updateStatus)
}
updateStatus(address) {
@@ -46,7 +46,7 @@ export default class EmailSection extends React.Component {
status: status.status,
error: status.error
})
- },
+ },
error => {
this.setState({
error: error,
diff --git a/ui/app/components/main_panel/index.js b/ui/app/components/main_panel/index.js
index 83c14cfa..b70af7c9 100644
--- a/ui/app/components/main_panel/index.js
+++ b/ui/app/components/main_panel/index.js
@@ -68,7 +68,8 @@ export default class MainPanel extends React.Component {
if (this.state.account && this.state.provider) {
return this.renderPanel()
} else {
- return <Spinner />
+ return <div className="main-panel">
+ </div>
}
}
diff --git a/ui/app/components/main_panel/main_panel.less b/ui/app/components/main_panel/main_panel.less
index f15ecedc..2ec9d059 100644
--- a/ui/app/components/main_panel/main_panel.less
+++ b/ui/app/components/main_panel/main_panel.less
@@ -44,6 +44,7 @@
.main-panel > .body {
padding: 20px;
+ background-color: white;
}
.main-panel .accounts {
@@ -196,7 +197,6 @@
}
}
.body-row {
-
}
}
@@ -244,8 +244,13 @@
padding: 0;
font-size: @icon-size - 10;
line-height: @icon-size;
+ display: inline;
word-break: break-all;
}
+ span.info {
+ word-break: break-all;
+ margin-left: 10px;
+ }
}
.buttons {
padding-left: 10px;
@@ -255,12 +260,33 @@
padding-left: @section-padding;
width: @section-padding + @status-size;
img {
- width: @status-size;
- height: @status-size;
+ // this fixes a bug where sometimes the status icons are not
+ // correctly rendered in the web widget
+ width: 100%;
+ height: auto;
+ }
+ &.spin img {
+ animation: icon-spin 10s linear infinite;
+ -webkit-animation: icon-spin 10s linear infinite;
}
}
.body-row {
- padding-top: @section-padding;
+ .alert {
+ margin-bottom: 0px;
+ }
+ // all immediate child divs are considered to be separate sections in
+ // in the body row of the section.
+ & > div {
+ margin-top: @section-padding;
+ }
}
}
+@-webkit-keyframes icon-spin {
+ from { -webkit-transform: rotate(0deg); }
+ to { -webkit-transform: rotate(360deg); }
+}
+@keyframes icon-spin {
+ from {transform:rotate(0deg);}
+ to {transform:rotate(360deg);}
+}
diff --git a/ui/app/components/main_panel/section_layout.js b/ui/app/components/main_panel/section_layout.js
index 3b06d681..e2f1ae17 100644
--- a/ui/app/components/main_panel/section_layout.js
+++ b/ui/app/components/main_panel/section_layout.js
@@ -16,6 +16,7 @@ export default class SectionLayout extends React.Component {
header: null, // the first line content
body: null, // expanded content
message: null, // alert content
+ error: null, // error content
onExpand: null, // callback
className: "",
style: {}
@@ -55,9 +56,13 @@ export default class SectionLayout extends React.Component {
)
}
if (status) {
+ let className = 'status'
+ if (status == 'wait') {
+ className = 'status spin'
+ }
statusIcon = (
- <div className="status">
- <img src={'img/' + status + '.svg' } />
+ <div className={className}>
+ <img width="24px" height="24px" src={'img/' + status + '.svg' } />
</div>
)
}
@@ -79,6 +84,7 @@ export default class SectionLayout extends React.Component {
body = (
<div className="body-row">
{this.props.message}
+ {this.props.error}
{this.props.body}
</div>
)
diff --git a/ui/app/components/main_panel/user_section.js b/ui/app/components/main_panel/user_section.js
index 17e492d7..539d03df 100644
--- a/ui/app/components/main_panel/user_section.js
+++ b/ui/app/components/main_panel/user_section.js
@@ -69,7 +69,9 @@ export default class UserSection extends React.Component {
)
}
if (this.state.expanded) {
- body = <UserPasswordForm account={this.props.account} />
+ body = <div>
+ <UserPasswordForm account={this.props.account} />
+ </div>
}
if (this.state.loading) {
button = <Button disabled={true}><Spinner /></Button>
diff --git a/ui/app/components/main_panel/vpn_section.js b/ui/app/components/main_panel/vpn_section.js
index 400d2133..4430ad96 100644
--- a/ui/app/components/main_panel/vpn_section.js
+++ b/ui/app/components/main_panel/vpn_section.js
@@ -1,11 +1,11 @@
import React from 'react'
-import { Button, Glyphicon, Alert } from 'react-bootstrap'
+import { Button, ButtonToolbar, Glyphicon, Alert } from 'react-bootstrap'
import SectionLayout from './section_layout'
import Spinner from 'components/spinner'
import bitmask from 'lib/bitmask'
-export default class VPNSection extends React.Component {
+export default class vpnSection extends React.Component {
static get defaultProps() {return{
account: null
@@ -14,32 +14,74 @@ export default class VPNSection extends React.Component {
constructor(props) {
super(props)
this.state = {
- error: null,
- expanded: false,
- vpn: "waiting",
+ interval: null, // timer callback
+ error: null, // error message
+ message: null, // info message
+ expanded: false, // show vpn section expanded or compact
+ ready: false, // true if vpn can start
+ vpn: "unknown", // current state of vpn
+ up: null, // \ throughput
+ down: null // / labels
}
this.expand = this.expand.bind(this)
this.connect = this.connect.bind(this)
this.disconnect = this.disconnect.bind(this)
- this.cancel = this.cancel.bind(this)
- this.unblock = this.unblock.bind(this)
- this.location = this.location.bind(this)
+ this.retry = this.retry.bind(this)
+ this.enable = this.enable.bind(this)
+ this.installHelper = this.installHelper.bind(this)
+
+ this.statusEvent = this.statusEvent.bind(this)
+ this.loginEvent = this.loginEvent.bind(this)
}
+ // called whenever a new account is selected
+ componentWillReceiveProps(nextProps) {
+ this.stopWatchingStatus()
+ if (this.props.account.domain != nextProps.account.domain) {
+ this.checkReadiness(nextProps.account.domain)
+ }
+ }
+
+ // called only once
componentWillMount() {
+ this.checkReadiness()
+ bitmask.events.register("VPN_STATUS_CHANGED", 'vpn section update', this.statusEvent)
+ bitmask.events.register("BONAFIDE_AUTH_DONE", 'vpn section auth', this.loginEvent)
+ }
+
+ // not sure if this is ever called
+ componentWillUnmount() {
+ bitmask.events.unregister("VPN_STATUS_CHANGED", 'vpn section update')
+ bitmask.events.unregister("BONAFIDE_AUTH_DONE", 'vpn section auth')
+ this.stopWatchingStatus()
+ }
+
+ updateStatus(domain = null) {
+ domain = domain || this.props.account.domain
bitmask.vpn.status().then(
- status => {
- console.log("status: ", status)
- if (status.VPN == "OFF") {
+ vpn => {
+ this.stopWatchingStatus()
+ if (vpn.status == "off") {
this.setState({vpn: "down"})
- } else if (status.VPN == "ON") {
- if (status.domain == this.props.account.domain) {
- this.setState({vpn: "up"})
+ } else if (vpn.status == "on") {
+ if (vpn.domain == domain) {
+ this.setState({
+ vpn: "up",
+ up: vpn.up,
+ down: vpn.down
+ })
+ this.startWatchingStatus()
} else {
this.setState({vpn: "down"})
}
+ } else if (vpn.status == "disabled") {
+ this.setState({vpn: "disabled"})
+ } else if (vpn.status == "starting") {
+ this.setState({vpn: "connecting"})
} else {
- // this.setState({vpn: "????"})
+ console.log("UNKNOWN STATUS", vpn.status)
+ // it should not get here...
+ this.setState({vpn: "waiting"})
}
},
error => {
@@ -48,44 +90,119 @@ export default class VPNSection extends React.Component {
)
}
- expand() {
- this.setState({expanded: !this.state.expanded})
- }
-
- // turn on button pressed
- connect() {
- this.setState({vpn: "connecting", error: null})
- bitmask.vpn.check(this.props.account.domain).then(
+ //
+ // Check if all the prerequisites have been met.
+ // This is called whenever the widget is shown, and also whenever a user
+ // authenticates
+ //
+ checkReadiness(domain = null) {
+ domain = domain || this.props.account.domain
+ bitmask.vpn.check(domain).then(
status => {
- console.log('check: ', status)
- if (status.vpn_ready == true) {
- this.startVPN()
- } else if (status.vpn == "disabled") {
- this.setState({vpn: "failed", error: "VPN support disabled"})
+ console.log('check()', status)
+ if (status.vpn == 'disabled') {
+ this.setState({vpn: "disabled"})
+ } else if (!status.installed) {
+ this.setState({vpn: "nohelpers"})
+ } else if (!status.vpn_ready) {
+ this.renewCert()
+ } else {
+ this.setState({
+ message: null,
+ error: null,
+ ready: true
+ })
+ this.updateStatus(domain)
+ }
+ },
+ error => {
+ console.log('check()', error)
+ if (error == "Missing VPN certificate") {
+ this.renewCert()
} else {
- bitmask.vpn.get_cert(this.props.account.id).then(
- uid => {
- this.startVPN()
- },
- error => {
- this.setState({vpn: "failed", error: error})
- }
- )
+ this.setState({vpn: "failed", error: error})
}
+ }
+ )
+ }
+
+ //
+ // install the necessary helper files
+ //
+ installHelper() {
+ bitmask.vpn.install().then(
+ ok => {
+ this.checkReadiness()
},
error => {
+ console.log('install()', error)
this.setState({vpn: "failed", error: error})
}
)
}
+
+ //
+ // event callback: something new has happened, time to re-poll
+ //
+ statusEvent() {
+ console.log('statusEvent')
+ this.updateStatus()
+ }
+
+ //
+ // event callback: the user successfully logged in
+ //
+ loginEvent(event, user) {
+ let address = user[1]
+ this.checkReadiness(address.split('@')[1])
+ }
+
+ //
+ // get new vpn cert from provider
+ //
+ renewCert() {
+ if (!this.props.account.authenticated) {
+ this.setState({
+ message: 'Please log in to renew VPN credentials.',
+ vpn: null
+ })
+ } else {
+ let message = (<div>
+ <Spinner/>&nbsp;<span>Renewing VPN credentials...</span>
+ </div>)
+ this.setState({message: message})
+ bitmask.vpn.get_cert(this.props.account.id).then(
+ ok => {
+ console.log('get_cert()', ok)
+ this.checkReadiness()
+ }, error => {
+ console.log('get_cert()', error)
+ this.setState({vpn: "failed", error: error})
+ }
+ )
+ }
+ }
+
+ // section expand/collapse button pressed
+ expand() {
+ this.setState({expanded: !this.state.expanded})
+ }
+
+ // turn on button pressed
+ connect() {
+ this.setState({vpn: "connecting", error: null})
+ bitmask.vpn.stop().then(
+ wasRunning => {this.startvpn()},
+ wasntRunning => {this.startvpn()}
+ )
+ }
+
// turn off button pressed
disconnect() {
this.setState({vpn: "disconnecting", error: null})
bitmask.vpn.stop().then(
success => {
- console.log('stop:')
- console.log(success)
this.setState({vpn: "down"})
},
error => {
@@ -94,49 +211,72 @@ export default class VPNSection extends React.Component {
)
}
- cancel() {
-
+ // retry button pressed
+ retry() {
+ this.setState({error: null, message: null, vpn: 'waiting'})
+ this.updateStatus()
+ this.checkReadiness()
}
- unblock() {
-
- }
-
- location() {
-
+ // enable button pressed
+ enable() {
+ this.setState({error: null, message: null, vpn: 'waiting'})
+ bitmask.vpn.enable().then(
+ ok => {
+ console.log('enable()', ok)
+ this.retry()
+ },
+ error => {
+ this.setState({error: error, message: null, vpn: 'failed'})
+ console.log('enable(error)', error)
+ }
+ )
}
- // call startVPN() only when everything is ready
- startVPN() {
+ //
+ // call startvpn() only when everything is ready, and no vpn is currently
+ // running.
+ //
+ startvpn() {
bitmask.vpn.start(this.props.account.domain).then(
status => {
- console.log('start: ', status)
- if (status.result == "started") {
- this.setState({vpn: "up", error: null})
- } else {
- this.setState({vpn: "failed"})
- }
- },
- error => {
+ console.log('start success', status)
+ }, error => {
+ console.log('start error', error)
this.setState({vpn: "failed", error: error})
}
)
}
+ startWatchingStatus() {
+ this.interval = setInterval(this.statusEvent, 1000)
+ }
+
+ stopWatchingStatus() {
+ clearInterval(this.interval)
+ }
+
render () {
- console.log(this.state)
let message = null
+ let error = null
let body = null
let button = null
let icon = null
+ let info = null
+ let expand = null // this.expand
- let header = <h1>VPN</h1>
+ // style may be: success, warning, danger, info
if (this.state.error) {
- // style may be: success, warning, danger, info
- message = (
+ error = (
<Alert bsStyle="danger">{this.state.error}</Alert>
)
}
+ if (this.state.message) {
+ message = (
+ <Alert bsStyle="info">{this.state.message}</Alert>
+ )
+ }
+
if (this.state.expanded) {
body = <div>traffic details go here</div>
}
@@ -149,35 +289,71 @@ export default class VPNSection extends React.Component {
case "up":
button = <Button onClick={this.disconnect}>Turn OFF</Button>
icon = "on"
+ info = "Connected"
+ if (this.state.up) {
+ info = <span>
+ <Glyphicon glyph="chevron-down" />
+ {this.state.down}
+ &nbsp;&nbsp;
+ <Glyphicon glyph="chevron-up" />
+ {this.state.up}
+ </span>
+ }
break
case "connecting":
- button = <Button onClick={this.cancel}>Cancel</Button>
+ button = <Button onClick={this.disconnect}>Cancel</Button>
icon = "wait"
+ info = "Connecting..."
break
case "disconnecting":
- button = <Button onClick={this.cancel}>Cancel</Button>
+ button = <Button onClick={this.disconnect}>Cancel</Button>
icon = "wait"
break
case "failed":
- button = <div>
- <Button onClick={this.connect}>Turn ON</Button>
- </div>
- // <Button onClick={this.unblock}>Unblock</Button>
+ info = "Failed"
+ if (this.state.ready) {
+ button = <ButtonToolbar>
+ <Button onClick={this.connect}>Turn ON</Button>
+ <Button onClick={this.disconnect}>Unblock</Button>
+ </ButtonToolbar>
+ } else {
+ button = <Button onClick={this.retry}>Retry</Button>
+ }
icon = "off"
break
case "disabled":
- button = <div>Disabled</div>
+ button = <Button onClick={this.enable}>Enable</Button>
icon = "disabled"
break
case "waiting":
button = <Spinner />
icon = "wait"
break
+ case "nohelpers":
+ body = (
+ <div>
+ <p>The VPN requires that certain helpers are installed on your system.</p>
+ <Button onClick={this.installHelper}>Install Helper Files</Button>
+ </div>
+ )
+ break
+ }
+
+ let header = (
+ <div>
+ <h1>VPN</h1>
+ <span className="info">{info}</span>
+ </div>
+ )
+
+ if (button == null) {
+ expand = null
}
return (
<SectionLayout icon="planet" buttons={button} status={icon}
- onExpand={this.expand} header={header} body={body} message={message} />
+ onExpand={expand} header={header} body={body}
+ message={message} error={error} />
)
}
diff --git a/ui/app/components/wizard/wizard.less b/ui/app/components/wizard/wizard.less
index 29efc20e..1606e892 100644
--- a/ui/app/components/wizard/wizard.less
+++ b/ui/app/components/wizard/wizard.less
@@ -33,6 +33,7 @@
}
.wizard .stage .body {
+ background-color: white;
-webkit-flex: 1 1 auto;
flex: 1 1 auto;
padding: 20px;
diff --git a/ui/app/css/bootstrap.less b/ui/app/css/bootstrap.less
index 3b772284..6ed519db 100644
--- a/ui/app/css/bootstrap.less
+++ b/ui/app/css/bootstrap.less
@@ -1,5 +1,72 @@
//
-// require npm modules 'bootstrap'
+// requires npm modules 'bootstrap'
//
+
@import "~bootstrap/less/bootstrap";
+//
+// overrides the bootstrap defaults
+// note: in less, you put overrides after the definition.
+// this works because of lazy loading.
+//
+
+@import "colors";
+
+@brand-primary: @teal;
+@brand-success: @green;
+@brand-danger: @red;
+@brand-warning: @deep-orange;
+@brand-info: @light-blue;
+
+// // Define colors for form feedback states and, by default, alerts.
+
+@state-success-text: @brand-success;
+@state-success-bg: @brand-success;
+@state-success-border: @brand-success;
+
+@state-info-text: @brand-info;
+@state-info-bg: @brand-info;
+@state-info-border: @brand-info;
+
+@state-warning-text: @brand-warning;
+@state-warning-bg: @brand-warning;
+@state-warning-border: @brand-warning;
+
+@state-danger-text: @brand-danger;
+@state-danger-bg: @brand-danger;
+@state-danger-border: @brand-danger;
+
+@alert-succcess-text: @white;
+@alert-info-text: @white;
+@alert-warning-text: @white;
+@alert-danger-text: @white;
+
+.alert {
+ font-weight: bold;
+}
+
+.help-block {
+ margin: 2px 0 0 0;
+ font-size: small;
+ opacity: 0.7;
+}
+.btn.btn-inverse {
+ color: white;
+ background-color: #333;
+}
+.btn.btn-flat {
+ border-color: transparent;
+ background-color: transparent;
+}
+
+//
+// the web widget has a weird default focus. override it here
+// to match the input decoration when focused.
+//
+.btn:focus, .btn:active:focus, .btn.active:focus,
+.btn.focus, .btn:active.focus, .btn.active.focus {
+ border-color: #66afe9;
+ outline: 0;
+ -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);
+ box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);
+}
diff --git a/ui/app/css/colors.less b/ui/app/css/colors.less
new file mode 100644
index 00000000..a563f803
--- /dev/null
+++ b/ui/app/css/colors.less
@@ -0,0 +1,329 @@
+//
+// this is the material design palette
+//
+
+@red-50: #ffebee;
+@red-100: #ffcdd2;
+@red-200: #ef9a9a;
+@red-300: #e57373;
+@red-400: #ef5350;
+@red-500: #f44336;
+@red-600: #e53935;
+@red-700: #d32f2f;
+@red-800: #c62828;
+@red-900: #b71c1c;
+@red-A100: #ff8a80;
+@red-A200: #ff5252;
+@red-A400: #ff1744;
+@red-A700: #d50000;
+@red: @red-500;
+
+
+@pink-50: #fce4ec;
+@pink-100: #f8bbd0;
+@pink-200: #f48fb1;
+@pink-300: #f06292;
+@pink-400: #ec407a;
+@pink-500: #e91e63;
+@pink-600: #d81b60;
+@pink-700: #c2185b;
+@pink-800: #ad1457;
+@pink-900: #880e4f;
+@pink-A100: #ff80ab;
+@pink-A200: #ff4081;
+@pink-A400: #f50057;
+@pink-A700: #c51162;
+@pink: @pink-500;
+
+
+@purple-50: #f3e5f5;
+@purple-100: #e1bee7;
+@purple-200: #ce93d8;
+@purple-300: #ba68c8;
+@purple-400: #ab47bc;
+@purple-500: #9c27b0;
+@purple-600: #8e24aa;
+@purple-700: #7b1fa2;
+@purple-800: #6a1b9a;
+@purple-900: #4a148c;
+@purple-A100: #ea80fc;
+@purple-A200: #e040fb;
+@purple-A400: #d500f9;
+@purple-A700: #aa00ff;
+@purple: @purple-500;
+
+
+@deep-purple-50: #ede7f6;
+@deep-purple-100: #d1c4e9;
+@deep-purple-200: #b39ddb;
+@deep-purple-300: #9575cd;
+@deep-purple-400: #7e57c2;
+@deep-purple-500: #673ab7;
+@deep-purple-600: #5e35b1;
+@deep-purple-700: #512da8;
+@deep-purple-800: #4527a0;
+@deep-purple-900: #311b92;
+@deep-purple-A100: #b388ff;
+@deep-purple-A200: #7c4dff;
+@deep-purple-A400: #651fff;
+@deep-purple-A700: #6200ea;
+@deep-purple: @deep-purple-500;
+
+
+@indigo-50: #e8eaf6;
+@indigo-100: #c5cae9;
+@indigo-200: #9fa8da;
+@indigo-300: #7986cb;
+@indigo-400: #5c6bc0;
+@indigo-500: #3f51b5;
+@indigo-600: #3949ab;
+@indigo-700: #303f9f;
+@indigo-800: #283593;
+@indigo-900: #1a237e;
+@indigo-A100: #8c9eff;
+@indigo-A200: #536dfe;
+@indigo-A400: #3d5afe;
+@indigo-A700: #304ffe;
+@indigo: @indigo-500;
+
+
+@blue-50: #e3f2fd;
+@blue-100: #bbdefb;
+@blue-200: #90caf9;
+@blue-300: #64b5f6;
+@blue-400: #42a5f5;
+@blue-500: #2196f3;
+@blue-600: #1e88e5;
+@blue-700: #1976d2;
+@blue-800: #1565c0;
+@blue-900: #0d47a1;
+@blue-A100: #82b1ff;
+@blue-A200: #448aff;
+@blue-A400: #2979ff;
+@blue-A700: #2962ff;
+@blue: @blue-500;
+
+
+@light-blue-50: #e1f5fe;
+@light-blue-100: #b3e5fc;
+@light-blue-200: #81d4fa;
+@light-blue-300: #4fc3f7;
+@light-blue-400: #29b6f6;
+@light-blue-500: #03a9f4;
+@light-blue-600: #039be5;
+@light-blue-700: #0288d1;
+@light-blue-800: #0277bd;
+@light-blue-900: #01579b;
+@light-blue-A100: #80d8ff;
+@light-blue-A200: #40c4ff;
+@light-blue-A400: #00b0ff;
+@light-blue-A700: #0091ea;
+@light-blue: @light-blue-500;
+
+
+@cyan-50: #e0f7fa;
+@cyan-100: #b2ebf2;
+@cyan-200: #80deea;
+@cyan-300: #4dd0e1;
+@cyan-400: #26c6da;
+@cyan-500: #00bcd4;
+@cyan-600: #00acc1;
+@cyan-700: #0097a7;
+@cyan-800: #00838f;
+@cyan-900: #006064;
+@cyan-A100: #84ffff;
+@cyan-A200: #18ffff;
+@cyan-A400: #00e5ff;
+@cyan-A700: #00b8d4;
+@cyan: @cyan-500;
+
+
+@teal-50: #e0f2f1;
+@teal-100: #b2dfdb;
+@teal-200: #80cbc4;
+@teal-300: #4db6ac;
+@teal-400: #26a69a;
+@teal-500: #009688;
+@teal-600: #00897b;
+@teal-700: #00796b;
+@teal-800: #00695c;
+@teal-900: #004d40;
+@teal-A100: #a7ffeb;
+@teal-A200: #64ffda;
+@teal-A400: #1de9b6;
+@teal-A700: #00bfa5;
+@teal: @teal-500;
+
+
+@green-50: #e8f5e9;
+@green-100: #c8e6c9;
+@green-200: #a5d6a7;
+@green-300: #81c784;
+@green-400: #66bb6a;
+@green-500: #4caf50;
+@green-600: #43a047;
+@green-700: #388e3c;
+@green-800: #2e7d32;
+@green-900: #1b5e20;
+@green-A100: #b9f6ca;
+@green-A200: #69f0ae;
+@green-A400: #00e676;
+@green-A700: #00c853;
+@green: @green-500;
+
+
+@light-green-50: #f1f8e9;
+@light-green-100: #dcedc8;
+@light-green-200: #c5e1a5;
+@light-green-300: #aed581;
+@light-green-400: #9ccc65;
+@light-green-500: #8bc34a;
+@light-green-600: #7cb342;
+@light-green-700: #689f38;
+@light-green-800: #558b2f;
+@light-green-900: #33691e;
+@light-green-A100: #ccff90;
+@light-green-A200: #b2ff59;
+@light-green-A400: #76ff03;
+@light-green-A700: #64dd17;
+@light-green: @light-green-500;
+
+
+@lime-50: #f9fbe7;
+@lime-100: #f0f4c3;
+@lime-200: #e6ee9c;
+@lime-300: #dce775;
+@lime-400: #d4e157;
+@lime-500: #cddc39;
+@lime-600: #c0ca33;
+@lime-700: #afb42b;
+@lime-800: #9e9d24;
+@lime-900: #827717;
+@lime-A100: #f4ff81;
+@lime-A200: #eeff41;
+@lime-A400: #c6ff00;
+@lime-A700: #aeea00;
+@lime: @lime-500;
+
+
+@yellow-50: #fffde7;
+@yellow-100: #fff9c4;
+@yellow-200: #fff59d;
+@yellow-300: #fff176;
+@yellow-400: #ffee58;
+@yellow-500: #ffeb3b;
+@yellow-600: #fdd835;
+@yellow-700: #fbc02d;
+@yellow-800: #f9a825;
+@yellow-900: #f57f17;
+@yellow-A100: #ffff8d;
+@yellow-A200: #ffff00;
+@yellow-A400: #ffea00;
+@yellow-A700: #ffd600;
+@yellow: @yellow-500;
+
+
+@amber-50: #fff8e1;
+@amber-100: #ffecb3;
+@amber-200: #ffe082;
+@amber-300: #ffd54f;
+@amber-400: #ffca28;
+@amber-500: #ffc107;
+@amber-600: #ffb300;
+@amber-700: #ffa000;
+@amber-800: #ff8f00;
+@amber-900: #ff6f00;
+@amber-A100: #ffe57f;
+@amber-A200: #ffd740;
+@amber-A400: #ffc400;
+@amber-A700: #ffab00;
+@amber: @amber-500;
+
+
+@orange-50: #fff3e0;
+@orange-100: #ffe0b2;
+@orange-200: #ffcc80;
+@orange-300: #ffb74d;
+@orange-400: #ffa726;
+@orange-500: #ff9800;
+@orange-600: #fb8c00;
+@orange-700: #f57c00;
+@orange-800: #ef6c00;
+@orange-900: #e65100;
+@orange-A100: #ffd180;
+@orange-A200: #ffab40;
+@orange-A400: #ff9100;
+@orange-A700: #ff6d00;
+@orange: @orange-500;
+
+
+@deep-orange-50: #fbe9e7;
+@deep-orange-100: #ffccbc;
+@deep-orange-200: #ffab91;
+@deep-orange-300: #ff8a65;
+@deep-orange-400: #ff7043;
+@deep-orange-500: #ff5722;
+@deep-orange-600: #f4511e;
+@deep-orange-700: #e64a19;
+@deep-orange-800: #d84315;
+@deep-orange-900: #bf360c;
+@deep-orange-A100: #ff9e80;
+@deep-orange-A200: #ff6e40;
+@deep-orange-A400: #ff3d00;
+@deep-orange-A700: #dd2c00;
+@deep-orange: @deep-orange-500;
+
+
+@brown-50: #efebe9;
+@brown-100: #d7ccc8;
+@brown-200: #bcaaa4;
+@brown-300: #a1887f;
+@brown-400: #8d6e63;
+@brown-500: #795548;
+@brown-600: #6d4c41;
+@brown-700: #5d4037;
+@brown-800: #4e342e;
+@brown-900: #3e2723;
+@brown-A100: #d7ccc8;
+@brown-A200: #bcaaa4;
+@brown-A400: #8d6e63;
+@brown-A700: #5d4037;
+@brown: @brown-500;
+
+
+@grey-50: #fafafa;
+@grey-100: #f5f5f5;
+@grey-200: #eeeeee;
+@grey-300: #e0e0e0;
+@grey-400: #bdbdbd;
+@grey-500: #9e9e9e; @rgb-grey-500: "158, 158, 158";
+@grey-600: #757575;
+@grey-700: #616161;
+@grey-800: #424242;
+@grey-900: #212121;
+@grey-A100: #f5f5f5;
+@grey-A200: #eeeeee;
+@grey-A400: #bdbdbd;
+@grey-A700: #616161;
+@grey: @grey-500;
+
+
+@blue-grey-50: #eceff1;
+@blue-grey-100: #cfd8dc;
+@blue-grey-200: #b0bec5;
+@blue-grey-300: #90a4ae;
+@blue-grey-400: #78909c;
+@blue-grey-500: #607d8b;
+@blue-grey-600: #546e7a;
+@blue-grey-700: #455a64;
+@blue-grey-800: #37474f;
+@blue-grey-900: #263238;
+@blue-grey-A100: #cfd8dc;
+@blue-grey-A200: #b0bec5;
+@blue-grey-A400: #78909c;
+@blue-grey-A700: #455a64;
+@blue-grey: @blue-grey-500;
+
+
+@black: #000000; @rgb-black: "0,0,0";
+@white: #ffffff; @rgb-white: "255,255,255";
diff --git a/ui/app/css/common.css b/ui/app/css/common.less
index 7d8680f4..9743c955 100644
--- a/ui/app/css/common.css
+++ b/ui/app/css/common.less
@@ -35,28 +35,6 @@ body {
border-width: 8px;
}
-/*
- * bootstrap
- */
-
-.help-block {
- margin: 2px 0 0 0;
- font-size: small;
- opacity: 0.7;
-}
-.btn.btn-inverse {
- color: white;
- background-color: #333;
-}
-.btn.btn-flat {
- border-color: transparent;
- background-color: transparent;
-}
-
-/*.btn.btn-default {
- background-color: #eee !important;
-}
-*/
/*
* center component
diff --git a/ui/app/index.html b/ui/app/index.html
index 440e0b6f..d500e369 100644
--- a/ui/app/index.html
+++ b/ui/app/index.html
@@ -3,8 +3,9 @@
<head>
<meta charset="UTF-8">
<title>Bitmask</title>
- <link rel="stylesheet" href="css/bootstrap.min.css">
- <link rel="stylesheet" href="css/common.css">
+ <style>
+ body {background: #333 !important}
+ </style>
</head>
<body>
<div id="app"></div>
diff --git a/ui/app/lib/bitmask.js b/ui/app/lib/bitmask.js
index 1fdce433..9d772242 100644
--- a/ui/app/lib/bitmask.js
+++ b/ui/app/lib/bitmask.js
@@ -90,10 +90,12 @@ var bitmask = function(){
if (api_token) {
call(['events', 'poll']).then(function(response) {
if (response !== null) {
- var evnt = response[0];
+ var event = response[0];
var content = response[1];
- if (evnt in event_handlers) {
- event_handlers[evnt](evnt, content);
+ if (event in event_handlers) {
+ Object.values(event_handlers[event]).forEach(function(handler) {
+ handler(event, content);
+ })
}
}
event_polling();
@@ -401,24 +403,36 @@ var bitmask = function(){
/**
* Register func for an event
*
- * @param {string} evnt The event to register
+ * @param {string} event The event to register
+ * @param {string} name The unique name for the callback
* @param {function} func The function that will be called on each event.
* It has to be like: function(event, content) {}
* Where content will be a list of strings.
*/
- register: function(evnt, func) {
- event_handlers[evnt] = func;
- return call(['events', 'register', evnt])
+ register: function(event, name, func) {
+ event_handlers[event] = event_handlers[event] || {}
+ if (event_handlers[event][name]) {
+ return null;
+ } else {
+ event_handlers[event][name] = func;
+ return call(['events', 'register', event])
+ }
},
/**
* Unregister from an event
*
- * @param {string} evnt The event to unregister
+ * @param {string} event The event to unregister
+ * @param {string} name The unique name of the callback to remove
*/
- unregister: function(evnt) {
- delete event_handlers[evnt];
- return call(['events', 'unregister', evnt])
+ unregister: function(event, name) {
+ event_handlers[event] = event_handlers[event] || {}
+ delete event_handlers[event][name]
+ if (Object.keys(event_handlers[event]).length == 0) {
+ return call(['events', 'unregister', event]);
+ } else {
+ return null;
+ }
}
}
};
diff --git a/ui/app/lib/event_logger.js b/ui/app/lib/event_logger.js
index fac1a510..283d2434 100644
--- a/ui/app/lib/event_logger.js
+++ b/ui/app/lib/event_logger.js
@@ -59,7 +59,7 @@ export default class EventLogger {
this.logEvent = this.logEvent.bind(this)
for (let event of EVENTS) {
console.log('register event ' + event)
- bitmask.events.register(event, this.logEvent)
+ bitmask.events.register(event, 'logger ' + event, this.logEvent)
}
}
logEvent(event, msg) {
diff --git a/ui/app/models/account.js b/ui/app/models/account.js
index 14773602..34104f62 100644
--- a/ui/app/models/account.js
+++ b/ui/app/models/account.js
@@ -98,7 +98,12 @@ export default class Account {
})
// failing that, search by domain
if (!account) {
- let domain = '@' + address.split('@')[1]
+ let domain = null
+ if (address.indexOf('@') == -1) {
+ domain = '@' + address
+ } else {
+ domain = '@' + address.split('@')[1]
+ }
account = Account.list.find(i => {
return i.address == domain
})
@@ -133,6 +138,32 @@ export default class Account {
)
}
+ static vpnReady() {
+ return Provider.list(false).then(domains => {
+ let promises = domains.map(domain => {
+ return new Promise((resolve, reject) => {
+ bitmask.vpn.check(domain).then(status => {
+ if (status.vpn != 'disabled' && status.installed && status.vpn_ready) {
+ resolve(domain)
+ } else {
+ resolve("")
+ }
+ }, error => {
+ resolve("")
+ })
+ })
+ })
+ return Promise.all(promises).then(domains => {
+ domains = domains.filter(i => {
+ return i != ""
+ })
+ return domains.map(domain => {
+ return Account.find(domain)
+ })
+ })
+ })
+ }
+
static add(account) {
if (!Account.list.find(i => {return i.id == account.id})) {
Account.list.push(account)
diff --git a/ui/package.json b/ui/package.json
index 39d7f175..ed99e955 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -5,7 +5,9 @@
"license": "GPL-3.0",
"homepage": "https://bitmask.net",
"repository": "https://leap.se/git/bitmask_client.git",
- "dependencies": {},
+ "dependencies": {
+ "file-loader": "^0.11.1"
+ },
"devDependencies": {
"babel": "^6.5.2",
"babel-core": "^6.17.0",
diff --git a/ui/webpack.config.js b/ui/webpack.config.js
index 786addd0..1f969026 100644
--- a/ui/webpack.config.js
+++ b/ui/webpack.config.js
@@ -31,6 +31,30 @@ var config = {
{
test: /\.less$/,
loader: "style!css!less?noIeCompat"
+ },
+ {
+ test: /\.png$/,
+ loader: "file-loader"
+ },
+ {
+ test: /\.jpg$/,
+ loader: "file-loader"
+ },
+ {
+ test: /\.(woff|woff2)(\?v=\d+\.\d+\.\d+)?$/,
+ loader: 'file-loader'
+ },
+ {
+ test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
+ loader: 'file-loader'
+ },
+ {
+ test: /\.eot(\?v=\d+\.\d+\.\d+)?$/,
+ loader: 'file-loader'
+ },
+ {
+ test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
+ loader: 'file-loader'
}
]
},
@@ -50,12 +74,8 @@ var config = {
// For more information: https://github.com/kevlened/copy-webpack-plugin
//
new CopyWebpackPlugin([
- { from: 'css/*.css' },
{ from: 'img/*'},
{ from: 'index.html' },
- { from: '../node_modules/bootstrap/dist/css/bootstrap.min.css', to: 'css' },
- { from: '../node_modules/bootstrap/dist/fonts/glyphicons-halflings-regular.woff2', to: 'fonts' },
- { from: '../node_modules/bootstrap/dist/fonts/glyphicons-halflings-regular.woff', to: 'fonts' },
{ from: '../node_modules/zxcvbn/dist/zxcvbn.js', to: 'js' }
])
],