diff --git a/js/src/views/ParityBar/accountStore.js b/js/src/views/ParityBar/accountStore.js new file mode 100644 index 00000000000..b53f40dd284 --- /dev/null +++ b/js/src/views/ParityBar/accountStore.js @@ -0,0 +1,101 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import { action, observable, transaction } from 'mobx'; + +export default class AccountStore { + @observable accounts = []; + @observable defaultAccount = null; + @observable isLoading = false; + + constructor (api) { + this._api = api; + + this.loadAccounts(); + this.subscribeDefaultAccount(); + } + + @action setAccounts = (accounts) => { + this.accounts = accounts; + } + + @action setDefaultAccount = (defaultAccount) => { + this.defaultAccount = defaultAccount; + } + + @action setLoading = (isLoading) => { + this.isLoading = isLoading; + } + + makeDefaultAccount = (address) => { + const accounts = [address].concat( + this.accounts + .filter((account) => account.address !== address) + .map((account) => account.address) + ); + + return this._api.parity + .setNewDappsWhitelist(accounts) + .catch((error) => { + console.warn('makeDefaultAccount', error); + }); + } + + loadAccounts () { + this.setLoading(true); + + return Promise + .all([ + this._api.parity.getNewDappsWhitelist(), + this._api.parity.allAccountsInfo() + ]) + .then(([whitelist, accounts]) => { + transaction(() => { + this.setLoading(false); + this.setAccounts( + Object + .keys(accounts) + .filter((address) => { + const isAccount = accounts[address].uuid; + const isWhitelisted = !whitelist || whitelist.includes(address); + + return isAccount && isWhitelisted; + }) + .map((address) => { + const account = accounts[address]; + + account.address = address; + account.default = address === this.defaultAccount; + + return account; + }) + ); + }); + }) + .catch((error) => { + this.setLoading(false); + console.warn('loadAccounts', error); + }); + } + + subscribeDefaultAccount () { + return this._api.subscribe('parity_defaultAccount', (error, defaultAccount) => { + if (!error) { + this.setDefaultAccount(defaultAccount); + } + }); + } +} diff --git a/js/src/views/ParityBar/accountStore.spec.js b/js/src/views/ParityBar/accountStore.spec.js new file mode 100644 index 00000000000..6dd21980630 --- /dev/null +++ b/js/src/views/ParityBar/accountStore.spec.js @@ -0,0 +1,104 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import sinon from 'sinon'; + +import AccountStore from './accountStore'; + +import { ACCOUNT_DEFAULT, ACCOUNT_FIRST, ACCOUNT_NEW, createApi } from './parityBar.test.js'; + +let api; +let store; + +function create () { + api = createApi(); + store = new AccountStore(api); + + return store; +} + +describe('views/ParityBar/AccountStore', () => { + beforeEach(() => { + create(); + }); + + describe('constructor', () => { + it('subscribes to defaultAccount', () => { + expect(api.subscribe).to.have.been.calledWith('parity_defaultAccount'); + }); + }); + + describe('@action', () => { + describe('setAccounts', () => { + it('sets the accounts', () => { + store.setAccounts('testing'); + expect(store.accounts).to.equal('testing'); + }); + }); + + describe('setDefaultAccount', () => { + it('sets the default account', () => { + store.setDefaultAccount('testing'); + expect(store.defaultAccount).to.equal('testing'); + }); + }); + + describe('setLoading', () => { + it('sets the loading status', () => { + store.setLoading('testing'); + expect(store.isLoading).to.equal('testing'); + }); + }); + }); + + describe('operations', () => { + describe('loadAccounts', () => { + beforeEach(() => { + sinon.spy(store, 'setAccounts'); + + return store.loadAccounts(); + }); + + afterEach(() => { + store.setAccounts.restore(); + }); + + it('calls into parity_getNewDappsWhitelist', () => { + expect(api.parity.getNewDappsWhitelist).to.have.been.called; + }); + + it('calls into parity_allAccountsInfo', () => { + expect(api.parity.allAccountsInfo).to.have.been.called; + }); + + it('sets the accounts', () => { + expect(store.setAccounts).to.have.been.called; + }); + }); + + describe('makeDefaultAccount', () => { + beforeEach(() => { + return store.makeDefaultAccount(ACCOUNT_NEW); + }); + + it('calls into parity_setNewDappsWhitelist (with ordering)', () => { + expect(api.parity.setNewDappsWhitelist).to.have.been.calledWith([ + ACCOUNT_NEW, ACCOUNT_FIRST, ACCOUNT_DEFAULT + ]); + }); + }); + }); +}); diff --git a/js/src/views/ParityBar/parityBar.css b/js/src/views/ParityBar/parityBar.css index a955bd6870f..1d37cb17709 100644 --- a/js/src/views/ParityBar/parityBar.css +++ b/js/src/views/ParityBar/parityBar.css @@ -1,4 +1,4 @@ -/* Copyright 2015, 2016 Parity Technologies (UK) Ltd. +/* Copyright 2015-2017 Parity Technologies (UK) Ltd. /* This file is part of Parity. /* /* Parity is free software: you can redistribute it and/or modify @@ -15,13 +15,42 @@ /* along with Parity. If not, see . */ -.bar, .expanded { - position: fixed; - bottom: 0; - right: 0; - font-size: 16px; - font-family: 'Roboto', sans-serif; - z-index: 10001; +.account { + display: flex; + flex: 1; + position: relative; + + .accountOverlay { + position: absolute; + right: 0.5em; + top: 0.5em; + } + + .iconDisabled { + opacity: 0.15; + } + + .selected, + .unselected { + margin: 0.125em 0; + + &:focus { + outline: none; + } + } + + .unselected { + background: rgba(0, 0, 0, 0.4) !important; + } + + .selected { + background: rgba(255, 255, 255, 0.35) !important; + } +} + +.container { + display: flex; + flex-direction: column; } .overlay { @@ -32,6 +61,16 @@ left: 0; background: rgba(255, 255, 255, 0.5); z-index: 10000; + user-select: none; +} + +.bar, +.expanded { + position: fixed; + font-size: 16px; + font-family: 'Roboto', sans-serif; + z-index: 10001; + user-select: none; } .bar { @@ -39,35 +78,57 @@ display: flex; flex-wrap: wrap; width: 100%; + top: 0; + left: 0; + + &.moving { + bottom: 0; + right: 0; + + &:hover { + cursor: move; + } + } +} + +.parityBg { + position: fixed; + + transition-property: left, top, right, bottom; + transition-duration: 0.25s; + transition-timing-function: ease; + + &.moving { + transition-duration: 0.05s; + transition-timing-function: ease-in-out; + } } .expanded { - right: 1em; border-radius: 4px 4px 0 0; display: flex; flex-direction: column; max-height: 50vh; -} -.expanded .content { - flex: 1; - overflow-y: auto; - overflow-x: hidden; - display: flex; - background: rgba(0, 0, 0, 0.8); - min-height: 16em; + .content { + flex: 1; + overflow-y: auto; + overflow-x: hidden; + display: flex; + background: rgba(0, 0, 0, 0.8); + min-height: 16em; + } } .corner { - position: absolute; - bottom: 0; - right: 1em; border-radius: 4px 4px 0 0; } .cornercolor { background: rgba(0, 0, 0, 0.5); padding: 0.5em 1em; + display: flex; + align-items: center; } .link { @@ -76,19 +137,21 @@ outline: none !important; color: white !important; display: inline-block; -} -.link img, .link svg { - height: 24px !important; - width: 24px !important; - margin: 2px 0.5em 0 0; + img, svg { + height: 24px !important; + width: 24px !important; + margin: 2px 0.5em 0 0; + } } .link+.link { margin-left: 1em; } -.button, .parityButton { +.button, +.iconButton, +.parityButton { overflow: visible !important; } @@ -101,6 +164,14 @@ fill: white !important; } +.iconButton { + min-width: 2em !important; + + img { + margin: 6px 0.5em 0 0.5em; + } +} + .label { position: relative; display: inline-block; @@ -123,20 +194,21 @@ padding: 0.5em 1em; background: rgba(0, 0, 0, 0.25); margin-bottom: 0; -} -.header:after { - clear: both; + &:after { + clear: both; + } } -.header button, -.corner button { - color: white !important; -} +.header, +.corner { + button { + color: white !important; + } -.header svg, -.coner svg { - fill: white !important; + svg { + fill: white !important; + } } .body { @@ -150,17 +222,49 @@ .actions { float: right; margin-top: -2px; -} -.actions div { - margin-left: 1em; - display: inline-block; - cursor: pointer; + div { + margin-left: 1em; + display: inline-block; + cursor: pointer; + } } -.parityIcon, .signerIcon { +.parityIcon, +.signerIcon { width: 24px; height: 24px; vertical-align: middle; margin-left: 12px; } + +.moveIcon { + display: flex; + align-items: center; + + &:hover { + cursor: move; + } +} + +.dragButton { + width: 1em; + height: 1em; + margin-left: 0.5em; + + background-color: white; + opacity: 0.25; + border-radius: 50%; + + transition-property: opacity; + transition-duration: 0.1s; + transition-timing-function: ease-in-out; + + &:hover { + opacity: 0.5; + } + + &.moving { + opacity: 0.75; + } +} diff --git a/js/src/views/ParityBar/parityBar.js b/js/src/views/ParityBar/parityBar.js index 0ecb57ae791..63036b9e44a 100644 --- a/js/src/views/ParityBar/parityBar.js +++ b/js/src/views/ParityBar/parityBar.js @@ -1,4 +1,4 @@ -// Copyright 2015, 2016 Parity Technologies (UK) Ltd. +// Copyright 2015-2017 Parity Technologies (UK) Ltd. // This file is part of Parity. // Parity is free software: you can redistribute it and/or modify @@ -14,19 +14,38 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . +import { throttle } from 'lodash'; +import { observer } from 'mobx-react'; import React, { Component, PropTypes } from 'react'; +import ReactDOM from 'react-dom'; +import { FormattedMessage } from 'react-intl'; import { Link } from 'react-router'; import { connect } from 'react-redux'; -import ActionFingerprint from 'material-ui/svg-icons/action/fingerprint'; -import ContentClear from 'material-ui/svg-icons/content/clear'; +import store from 'store'; -import { Badge, Button, ContainerTitle, ParityBackground } from '~/ui'; -import { Embedded as Signer } from '../Signer'; +import imagesEthcoreBlock from '~/../assets/images/parity-logo-white-no-text.svg'; +import { AccountCard, Badge, Button, ContainerTitle, IdentityIcon, ParityBackground, SectionList } from '~/ui'; +import { CancelIcon, FingerprintIcon } from '~/ui/Icons'; +import DappsStore from '~/views/Dapps/dappsStore'; +import { Embedded as Signer } from '~/views/Signer'; -import imagesEthcoreBlock from '!url-loader!../../../assets/images/parity-logo-white-no-text.svg'; +import AccountStore from './accountStore'; import styles from './parityBar.css'; +const LS_STORE_KEY = '_parity::parityBar'; +const DEFAULT_POSITION = { right: '1em', bottom: 0 }; +const DISPLAY_ACCOUNTS = 'accounts'; +const DISPLAY_SIGNER = 'signer'; + +@observer class ParityBar extends Component { + app = null; + measures = null; + moving = false; + + static contextTypes = { + api: PropTypes.object.isRequired + }; static propTypes = { dapp: PropTypes.bool, @@ -35,7 +54,33 @@ class ParityBar extends Component { }; state = { - opened: false + displayType: DISPLAY_SIGNER, + moving: false, + opened: false, + position: DEFAULT_POSITION + }; + + constructor (props) { + super(props); + + this.debouncedMouseMove = throttle( + this._onMouseMove, + 40, + { leading: true, trailing: true } + ); + } + + componentWillMount () { + const { api } = this.context; + + this.accountStore = new AccountStore(api); + + // Hook to the dapp loaded event to position the + // Parity Bar accordingly + DappsStore.get(api).on('loaded', (app) => { + this.app = app; + this.loadPosition(); + }); } componentWillReceiveProps (nextProps) { @@ -47,14 +92,14 @@ class ParityBar extends Component { } if (count < newCount) { - this.setOpened(true); + this.setOpened(true, DISPLAY_SIGNER); } else if (newCount === 0 && count === 1) { this.setOpened(false); } } - setOpened (opened) { - this.setState({ opened }); + setOpened (opened, displayType = DISPLAY_SIGNER) { + this.setState({ displayType, opened }); if (!this.bar) { return; @@ -74,11 +119,71 @@ class ParityBar extends Component { } render () { - const { opened } = this.state; + const { moving, opened, position } = this.state; + + const containerClassNames = opened + ? [ styles.overlay ] + : [ styles.bar ]; + + if (!opened && moving) { + containerClassNames.push(styles.moving); + } + + const parityBgClassNames = [ + opened + ? styles.expanded + : styles.corner, + styles.parityBg + ]; + + if (moving) { + parityBgClassNames.push(styles.moving); + } - return opened - ? this.renderExpanded() - : this.renderBar(); + const parityBgStyle = { + ...position + }; + + // Open the Signer at one of the four corners + // of the screen + if (opened) { + // Set at top or bottom of the screen + if (position.top !== undefined) { + parityBgStyle.top = 0; + } else { + parityBgStyle.bottom = 0; + } + + // Set at left or right of the screen + if (position.left !== undefined) { + parityBgStyle.left = '1em'; + } else { + parityBgStyle.right = '1em'; + } + } + + return ( +
+ + { + opened + ? this.renderExpanded() + : this.renderBar() + } + +
+ ); } renderBar () { @@ -88,36 +193,72 @@ class ParityBar extends Component { return null; } - const parityIcon = ( - + return ( +
+
); + } - const parityButton = ( -