// 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 { 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 store from 'store'; 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 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, externalLink: PropTypes.string, pending: PropTypes.array }; state = { 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) { const count = this.props.pending.length; const newCount = nextProps.pending.length; if (count === newCount) { return; } if (count < newCount) { this.setOpened(true, DISPLAY_SIGNER); } else if (newCount === 0 && count === 1) { this.setOpened(false); } } setOpened (opened, displayType = DISPLAY_SIGNER) { this.setState({ displayType, opened }); if (!this.bar) { return; } // Fire up custom even to support having parity bar iframed. const event = new CustomEvent('parity.bar.visibility', { bubbles: true, detail: { opened } }); this.bar.dispatchEvent(event); } onRef = (element) => { this.bar = element; } render () { 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); } 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 () { const { dapp } = this.props; if (!dapp) { return null; } return (
); } renderDrag () { if (this.props.externalLink) { return; } const dragButtonClasses = [ styles.dragButton ]; if (this.state.moving) { dragButtonClasses.push(styles.moving); } return (
); } renderLink (button) { const { externalLink } = this.props; if (!externalLink) { return ( { button } ); } return ( { button } ); } renderExpanded () { const { displayType } = this.state; return (
) : ( ) } />
{ displayType === DISPLAY_ACCOUNTS ? ( ) : ( ) }
); } renderAccount = (account) => { const onMakeDefault = () => { this.toggleAccountsDisplay(); this.accountStore.makeDefaultAccount(account.address); }; return (
); } renderLabel (name, bubble) { return (
{ name }
{ bubble }
); } renderSignerLabel () { const { pending } = this.props; let bubble = null; if (pending && pending.length) { bubble = ( ); } return this.renderLabel( , bubble ); } getHorizontal (x) { const { page, button, container } = this.measures; const left = x - button.offset.left; const centerX = left + container.width / 2; // left part of the screen if (centerX < page.width / 2) { return { left: Math.max(0, left) }; } const right = page.width - x - button.offset.right; return { right: Math.max(0, right) }; } getVertical (y) { const STICKY_SIZE = 75; const { page, button, container } = this.measures; const top = y - button.offset.top; const centerY = top + container.height / 2; // top part of the screen if (centerY < page.height / 2) { // Add Sticky edges const stickyTop = top < STICKY_SIZE ? 0 : top; return { top: Math.max(0, stickyTop) }; } const bottom = page.height - y - button.offset.bottom; // Add Sticky edges const stickyBottom = bottom < STICKY_SIZE ? 0 : bottom; return { bottom: Math.max(0, stickyBottom) }; } getPosition (x, y) { if (!this.moving || !this.measures) { return {}; } const horizontal = this.getHorizontal(x); const vertical = this.getVertical(y); const position = { ...horizontal, ...vertical }; return position; } onMouseDown = (event) => { const containerElt = ReactDOM.findDOMNode(this.refs.container); const dragButtonElt = ReactDOM.findDOMNode(this.refs.dragButton); if (!containerElt || !dragButtonElt) { console.warn(containerElt ? 'drag button' : 'container', 'not found...'); return; } const bodyRect = document.body.getBoundingClientRect(); const containerRect = containerElt.getBoundingClientRect(); const buttonRect = dragButtonElt.getBoundingClientRect(); const buttonOffset = { top: (buttonRect.top + buttonRect.height / 2) - containerRect.top, left: (buttonRect.left + buttonRect.width / 2) - containerRect.left }; buttonOffset.bottom = containerRect.height - buttonOffset.top; buttonOffset.right = containerRect.width - buttonOffset.left; const button = { offset: buttonOffset, height: buttonRect.height, width: buttonRect.width }; const container = { height: containerRect.height, width: containerRect.width }; const page = { height: bodyRect.height, width: bodyRect.width }; this.moving = true; this.measures = { button, container, page }; this.setState({ moving: true }); } onMouseEnter = (event) => { if (!this.moving) { return; } const { buttons } = event; // If no left-click, stop move if (buttons !== 1) { this.onMouseUp(event); } } onMouseLeave = (event) => { if (!this.moving) { return; } event.stopPropagation(); event.preventDefault(); } onMouseMove = (event) => { const { pageX, pageY } = event; // this._onMouseMove({ pageX, pageY }); this.debouncedMouseMove({ pageX, pageY }); event.stopPropagation(); event.preventDefault(); } _onMouseMove = (event) => { if (!this.moving) { return; } const { pageX, pageY } = event; const position = this.getPosition(pageX, pageY); this.setState({ position }); } onMouseUp = (event) => { if (!this.moving) { return; } const { pageX, pageY } = event; const position = this.getPosition(pageX, pageY); // Stick to bottom or top if (position.top !== undefined) { position.top = 0; } else { position.bottom = 0; } // Stick to bottom or top if (position.left !== undefined) { position.left = '1em'; } else { position.right = '1em'; } this.moving = false; this.setState({ moving: false, position }); this.savePosition(position); } toggleAccountsDisplay = () => { const { opened } = this.state; this.setOpened(!opened, DISPLAY_ACCOUNTS); if (!opened) { this.accountStore.loadAccounts(); } } toggleSignerDisplay = () => { const { opened } = this.state; this.setOpened(!opened, DISPLAY_SIGNER); } get config () { let config; try { config = JSON.parse(store.get(LS_STORE_KEY)); } catch (error) { config = {}; } return config; } loadPosition (props = this.props) { const { app, config } = this; if (!app) { return this.setState({ position: DEFAULT_POSITION }); } if (config[app.id]) { return this.setState({ position: config[app.id] }); } const position = this.stringToPosition(app.position); this.setState({ position }); } savePosition (position) { const { app, config } = this; config[app.id] = position; store.set(LS_STORE_KEY, JSON.stringify(config)); } stringToPosition (value) { switch (value) { case 'top-left': return { left: '1em', top: 0 }; case 'top-right': return { right: '1em', top: 0 }; case 'bottom-left': return { bottom: 0, left: '1em' }; case 'bottom-right': default: return DEFAULT_POSITION; } } } function mapStateToProps (state) { const { pending } = state.signer; return { pending }; } export default connect( mapStateToProps, null )(ParityBar);