diff options
author | elijah <elijah@riseup.net> | 2017-04-24 00:53:41 -0700 |
---|---|---|
committer | Kali Kaneko (leap communications) <kali@leap.se> | 2017-04-24 11:12:26 +0200 |
commit | 9d301349ab434dc744546918fe026d50485a0797 (patch) | |
tree | 802bec24bb446c9ed3af11f7ccfcd9793250b070 | |
parent | 2976cf11e451f1086d98eae20bdfb0fffa87abb0 (diff) |
[feat] usable vpn ui
-rw-r--r-- | ui/app/app.js | 18 | ||||
-rw-r--r-- | ui/app/components/main_panel/email_section.js | 6 | ||||
-rw-r--r-- | ui/app/components/main_panel/index.js | 3 | ||||
-rw-r--r-- | ui/app/components/main_panel/main_panel.less | 34 | ||||
-rw-r--r-- | ui/app/components/main_panel/section_layout.js | 10 | ||||
-rw-r--r-- | ui/app/components/main_panel/user_section.js | 4 | ||||
-rw-r--r-- | ui/app/components/main_panel/vpn_section.js | 312 | ||||
-rw-r--r-- | ui/app/components/wizard/wizard.less | 1 | ||||
-rw-r--r-- | ui/app/css/bootstrap.less | 69 | ||||
-rw-r--r-- | ui/app/css/colors.less | 329 | ||||
-rw-r--r-- | ui/app/css/common.less (renamed from ui/app/css/common.css) | 22 | ||||
-rw-r--r-- | ui/app/index.html | 5 | ||||
-rw-r--r-- | ui/app/lib/bitmask.js | 36 | ||||
-rw-r--r-- | ui/app/lib/event_logger.js | 2 | ||||
-rw-r--r-- | ui/app/models/account.js | 33 | ||||
-rw-r--r-- | ui/package.json | 4 | ||||
-rw-r--r-- | ui/webpack.config.js | 28 |
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/> <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} + + <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' } ]) ], |