From 5e8f6f271d8d3f2a02412c37822cf09a14671e3f Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Tue, 29 Nov 2016 13:50:09 +0100 Subject: [PATCH] Display local/completed transactions (#3630) * Initial fetch of local transactions * Container allows for title specification * Introduce TxList component * Display local transactions in signer list * Simplify * Pass only hashes from calling components * Simplify no pending display * Render pending blocks at the top * Get rid of time for 0 blocks * Indeed sort Pending to the top * Allow retrieval of pending transactions * setTimeout with clearTimeout --- js/src/ui/Container/container.js | 20 +- .../Transaction => ui/TxList}/index.js | 2 +- js/src/ui/TxList/store.js | 131 +++++++++++++ js/src/ui/TxList/txList.css | 81 ++++++++ js/src/ui/TxList/txList.js | 178 +++++++++++++++++ js/src/ui/index.js | 4 +- .../Transactions/Transaction/transaction.js | 184 ------------------ .../Account/Transactions/transactions.css | 71 ------- .../Account/Transactions/transactions.js | 83 ++------ .../containers/RequestsPage/RequestsPage.js | 65 ++++--- js/src/views/Signer/signer.js | 6 +- js/src/views/Signer/store.js | 35 +++- 12 files changed, 499 insertions(+), 361 deletions(-) rename js/src/{views/Account/Transactions/Transaction => ui/TxList}/index.js (94%) create mode 100644 js/src/ui/TxList/store.js create mode 100644 js/src/ui/TxList/txList.css create mode 100644 js/src/ui/TxList/txList.js delete mode 100644 js/src/views/Account/Transactions/Transaction/transaction.js diff --git a/js/src/ui/Container/container.js b/js/src/ui/Container/container.js index 143115f45..55b0b838e 100644 --- a/js/src/ui/Container/container.js +++ b/js/src/ui/Container/container.js @@ -17,6 +17,8 @@ import React, { Component, PropTypes } from 'react'; import { Card } from 'material-ui/Card'; +import Title from './Title'; + import styles from './container.css'; export default class Container extends Component { @@ -25,7 +27,10 @@ export default class Container extends Component { className: PropTypes.string, compact: PropTypes.bool, light: PropTypes.bool, - style: PropTypes.object + style: PropTypes.object, + title: PropTypes.oneOfType([ + PropTypes.string, PropTypes.node + ]) } render () { @@ -35,9 +40,22 @@ export default class Container extends Component { return (
+ { this.renderTitle() } { children }
); } + + renderTitle () { + const { title } = this.props; + + if (!title) { + return null; + } + + return ( + + ); + } } diff --git a/js/src/views/Account/Transactions/Transaction/index.js b/js/src/ui/TxList/index.js similarity index 94% rename from js/src/views/Account/Transactions/Transaction/index.js rename to js/src/ui/TxList/index.js index 28a39ad46..820d4b3aa 100644 --- a/js/src/views/Account/Transactions/Transaction/index.js +++ b/js/src/ui/TxList/index.js @@ -14,4 +14,4 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see <http://www.gnu.org/licenses/>. -export default from './transaction'; +export default from './txList'; diff --git a/js/src/ui/TxList/store.js b/js/src/ui/TxList/store.js new file mode 100644 index 000000000..ab35d5468 --- /dev/null +++ b/js/src/ui/TxList/store.js @@ -0,0 +1,131 @@ +// Copyright 2015, 2016 Ethcore (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 <http://www.gnu.org/licenses/>. + +import { action, observable, transaction } from 'mobx'; +import { uniq } from 'lodash'; + +export default class Store { + @observable blocks = {}; + @observable sortedHashes = []; + @observable transactions = {}; + + constructor (api) { + this._api = api; + this._subscriptionId = 0; + this._pendingHashes = []; + + this.subscribe(); + } + + @action addBlocks = (blocks) => { + this.blocks = Object.assign({}, this.blocks, blocks); + } + + @action addTransactions = (transactions) => { + transaction(() => { + this.transactions = Object.assign({}, this.transactions, transactions); + this.sortedHashes = Object + .keys(this.transactions) + .sort((ahash, bhash) => { + const bnA = this.transactions[ahash].blockNumber; + const bnB = this.transactions[bhash].blockNumber; + + if (bnB.eq(0)) { + return bnB.eq(bnA) ? 0 : 1; + } + + return bnB.comparedTo(bnA); + }); + this._pendingHashes = this.sortedHashes.filter((hash) => this.transactions[hash].blockNumber.eq(0)); + }); + } + + @action clearPending () { + this._pendingHashes = []; + } + + subscribe () { + this._api + .subscribe('eth_blockNumber', (error, blockNumber) => { + if (error) { + return; + } + + if (this._pendingHashes.length) { + this.loadTransactions(this._pendingHashes); + this.clearPending(); + } + }) + .then((subscriptionId) => { + this._subscriptionId = subscriptionId; + }); + } + + unsubscribe () { + if (!this._subscriptionId) { + return; + } + + this._api.unsubscribe(this._subscriptionId); + this._subscriptionId = 0; + } + + loadTransactions (_txhashes) { + const txhashes = _txhashes.filter((hash) => !this.transactions[hash] || this._pendingHashes.includes(hash)); + + if (!txhashes || !txhashes.length) { + return; + } + + Promise + .all(txhashes.map((txhash) => this._api.eth.getTransactionByHash(txhash))) + .then((transactions) => { + this.addTransactions( + transactions.reduce((transactions, tx, index) => { + transactions[txhashes[index]] = tx; + return transactions; + }, {}) + ); + + this.loadBlocks(transactions.map((tx) => tx.blockNumber ? tx.blockNumber.toNumber() : 0)); + }) + .catch((error) => { + console.warn('loadTransactions', error); + }); + } + + loadBlocks (_blockNumbers) { + const blockNumbers = uniq(_blockNumbers).filter((bn) => !this.blocks[bn]); + + if (!blockNumbers || !blockNumbers.length) { + return; + } + + Promise + .all(blockNumbers.map((blockNumber) => this._api.eth.getBlockByNumber(blockNumber))) + .then((blocks) => { + this.addBlocks( + blocks.reduce((blocks, block, index) => { + blocks[blockNumbers[index]] = block; + return blocks; + }, {}) + ); + }) + .catch((error) => { + console.warn('loadBlocks', error); + }); + } +} diff --git a/js/src/ui/TxList/txList.css b/js/src/ui/TxList/txList.css new file mode 100644 index 000000000..ebe672bf8 --- /dev/null +++ b/js/src/ui/TxList/txList.css @@ -0,0 +1,81 @@ +/* Copyright 2015, 2016 Ethcore (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 <http://www.gnu.org/licenses/>. +*/ + +.transactions { + width: 100%; + border-collapse: collapse; + + tr { + line-height: 32px; + vertical-align: top; + + &:nth-child(even) { + background: rgba(255, 255, 255, 0.04); + } + } + + th { + color: #aaa; + text-align: center; + } + + td { + vertical-align: top; + padding: 0.75em 0.75em; + + &.method { + width: 40%; + } + + &.timestamp { + padding-top: 1.5em; + text-align: right; + line-height: 1.5em; + opacity: 0.5; + } + + &.transaction { + padding-top: 1.5em; + text-align: center; + + & div { + line-height: 1.25em; + min-height: 1.25em; + } + } + } + + .icon { + margin: 0; + } + + .link { + vertical-align: top; + } + + .right { + text-align: right; + } + + .center { + text-align: center; + } + + .left { + text-align: left; + } +} diff --git a/js/src/ui/TxList/txList.js b/js/src/ui/TxList/txList.js new file mode 100644 index 000000000..5b47c2ca8 --- /dev/null +++ b/js/src/ui/TxList/txList.js @@ -0,0 +1,178 @@ +// Copyright 2015, 2016 Ethcore (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 <http://www.gnu.org/licenses/>. + +import moment from 'moment'; +import React, { Component, PropTypes } from 'react'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import { observer } from 'mobx-react'; + +import { txLink, addressLink } from '../../3rdparty/etherscan/links'; + +import IdentityIcon from '../IdentityIcon'; +import IdentityName from '../IdentityName'; +import MethodDecoding from '../MethodDecoding'; +import Store from './store'; + +import styles from './txList.css'; + +@observer +class TxList extends Component { + static contextTypes = { + api: PropTypes.object.isRequired + } + + static propTypes = { + address: PropTypes.string.isRequired, + hashes: PropTypes.oneOfType([ + PropTypes.array, + PropTypes.object + ]).isRequired, + isTest: PropTypes.bool.isRequired + } + + store = new Store(this.context.api); + + componentWillMount () { + this.store.loadTransactions(this.props.hashes); + } + + componentWillUnmount () { + this.store.unsubscribe(); + } + + componentWillReceiveProps (newProps) { + this.store.loadTransactions(newProps.hashes); + } + + render () { + return ( + <table className={ styles.transactions }> + <tbody> + { this.renderRows() } + </tbody> + </table> + ); + } + + renderRows () { + const { address, isTest } = this.props; + + return this.store.sortedHashes.map((txhash) => { + const tx = this.store.transactions[txhash]; + + return ( + <tr key={ tx.hash }> + { this.renderBlockNumber(tx.blockNumber) } + { this.renderAddress(tx.from) } + <td className={ styles.transaction }> + { this.renderEtherValue(tx.value) } + <div>⇒</div> + <div> + <a + className={ styles.link } + href={ txLink(tx.hash, isTest) } + target='_blank'> + { `${tx.hash.substr(2, 6)}...${tx.hash.slice(-6)}` } + </a> + </div> + </td> + { this.renderAddress(tx.to) } + <td className={ styles.method }> + <MethodDecoding + historic + address={ address } + transaction={ tx } /> + </td> + </tr> + ); + }); + } + + renderAddress (address) { + const { isTest } = this.props; + + let esLink = null; + if (address) { + esLink = ( + <a + href={ addressLink(address, isTest) } + target='_blank' + className={ styles.link }> + <IdentityName address={ address } shorten /> + </a> + ); + } + + return ( + <td className={ styles.address }> + <div className={ styles.center }> + <IdentityIcon + center + className={ styles.icon } + address={ address } /> + </div> + <div className={ styles.center }> + { esLink || 'DEPLOY' } + </div> + </td> + ); + } + + renderEtherValue (_value) { + const { api } = this.context; + const value = api.util.fromWei(_value); + + if (value.eq(0)) { + return <div className={ styles.value }>{ ' ' }</div>; + } + + return ( + <div className={ styles.value }> + { value.toFormat(5) }<small>ETH</small> + </div> + ); + } + + renderBlockNumber (_blockNumber) { + const blockNumber = _blockNumber.toNumber(); + const block = this.store.blocks[blockNumber]; + + return ( + <td className={ styles.timestamp }> + <div>{ blockNumber && block ? moment(block.timestamp).fromNow() : null }</div> + <div>{ blockNumber ? _blockNumber.toFormat() : 'Pending' }</div> + </td> + ); + } +} + +function mapStateToProps (state) { + const { isTest } = state.nodeStatus; + + return { + isTest + }; +} + +function mapDispatchToProps (dispatch) { + return bindActionCreators({}, dispatch); +} + +export default connect( + mapStateToProps, + mapDispatchToProps +)(TxList); diff --git a/js/src/ui/index.js b/js/src/ui/index.js index 6824d9887..4c16bc4a2 100644 --- a/js/src/ui/index.js +++ b/js/src/ui/index.js @@ -42,6 +42,7 @@ import SignerIcon from './SignerIcon'; import Tags from './Tags'; import Tooltips, { Tooltip } from './Tooltips'; import TxHash from './TxHash'; +import TxList from './TxList'; export { Actionbar, @@ -85,5 +86,6 @@ export { Tags, Tooltip, Tooltips, - TxHash + TxHash, + TxList }; diff --git a/js/src/views/Account/Transactions/Transaction/transaction.js b/js/src/views/Account/Transactions/Transaction/transaction.js deleted file mode 100644 index 32aa406a8..000000000 --- a/js/src/views/Account/Transactions/Transaction/transaction.js +++ /dev/null @@ -1,184 +0,0 @@ -// Copyright 2015, 2016 Ethcore (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 <http://www.gnu.org/licenses/>. - -import BigNumber from 'bignumber.js'; -import React, { Component, PropTypes } from 'react'; -import moment from 'moment'; - -import { IdentityIcon, IdentityName, MethodDecoding } from '../../../../ui'; -import ShortenedHash from '../../../../ui/ShortenedHash'; -import { txLink, addressLink } from '../../../../3rdparty/etherscan/links'; - -import styles from '../transactions.css'; - -export default class Transaction extends Component { - static contextTypes = { - api: PropTypes.object.isRequired - } - - static propTypes = { - address: PropTypes.string.isRequired, - isTest: PropTypes.bool.isRequired, - transaction: PropTypes.object.isRequired - } - - state = { - isContract: false, - isReceived: false, - transaction: null, - block: null - } - - componentDidMount () { - this.lookup(); - } - - render () { - const { block } = this.state; - const { transaction } = this.props; - - return ( - <tr> - <td className={ styles.timestamp }> - <div>{ this.formatBlockTimestamp(block) }</div> - <div>{ this.formatNumber(transaction.blockNumber) }</div> - </td> - { this.renderAddress(transaction.from) } - { this.renderTransaction() } - { this.renderAddress(transaction.to) } - <td className={ styles.method }> - { this.renderMethod() } - </td> - </tr> - ); - } - - renderMethod () { - const { address } = this.props; - const { transaction } = this.state; - - if (!transaction) { - return null; - } - - return ( - <MethodDecoding - historic - address={ address } - transaction={ transaction } /> - ); - } - - renderTransaction () { - const { isTest } = this.props; - const { transaction } = this.props; - - return ( - <td className={ styles.transaction }> - { this.renderEtherValue() } - <div>⇒</div> - <div> - <a - className={ styles.link } - href={ txLink(transaction.hash, isTest) } - target='_blank' - > - <ShortenedHash data={ transaction.hash } /> - </a> - </div> - </td> - ); - } - - renderAddress (address) { - const { isTest } = this.props; - - const eslink = address ? ( - <a - href={ addressLink(address, isTest) } - target='_blank' - className={ styles.link }> - <IdentityName address={ address } shorten /> - </a> - ) : 'DEPLOY'; - - return ( - <td className={ styles.address }> - <div className={ styles.center }> - <IdentityIcon - center - className={ styles.icon } - address={ address } /> - </div> - <div className={ styles.center }> - { eslink } - </div> - </td> - ); - } - - renderEtherValue () { - const { api } = this.context; - const { transaction } = this.state; - - if (!transaction) { - return null; - } - - const value = api.util.fromWei(transaction.value); - - if (value.eq(0)) { - return <div className={ styles.value }>{ ' ' }</div>; - } - - return ( - <div className={ styles.value }> - { value.toFormat(5) }<small>ETH</small> - </div> - ); - } - - formatNumber (number) { - return new BigNumber(number).toFormat(); - } - - formatBlockTimestamp (block) { - if (!block) { - return null; - } - - return moment(block.timestamp).fromNow(); - } - - lookup () { - const { api } = this.context; - const { transaction, address } = this.props; - - this.setState({ isReceived: address === transaction.to }); - - Promise - .all([ - api.eth.getBlockByNumber(transaction.blockNumber), - api.eth.getTransactionByHash(transaction.hash) - ]) - .then(([block, transaction]) => { - this.setState({ block, transaction }); - }) - .catch((error) => { - console.warn('lookup', error); - }); - } -} diff --git a/js/src/views/Account/Transactions/transactions.css b/js/src/views/Account/Transactions/transactions.css index 62411d8f2..13d727deb 100644 --- a/js/src/views/Account/Transactions/transactions.css +++ b/js/src/views/Account/Transactions/transactions.css @@ -14,49 +14,10 @@ /* You should have received a copy of the GNU General Public License /* along with Parity. If not, see <http://www.gnu.org/licenses/>. */ -.right { - text-align: right; -} - -.center { - text-align: center; -} - -.left { - text-align: left; -} .transactions { } -.transactions table { - width: 100%; - border-collapse: collapse; -} - -.transactions tr { - line-height: 32px; - vertical-align: top; -} - -.transactions tr:nth-child(even) { - background: rgba(255, 255, 255, 0.04); -} - -.transactions th { - color: #aaa; - text-align: center; -} - -.transactions td { - vertical-align: top; - padding: 0.75em 0.75em; -} - -.transactions .link { - vertical-align: top; -} - .infonone { opacity: 0.25; } @@ -67,35 +28,3 @@ font-size: 0.75em; color: #aaa; } - -.address { - text-align: center; -} - -.transaction { - text-align: center; -} - -.transactions td.transaction { - padding-top: 1.5em; -} - -.transaction div { - line-height: 1.25em; - min-height: 1.25em; -} - -.icon { - margin: 0; -} - -.method { - width: 40%; -} - -.transactions td.timestamp { - padding-top: 1.5em; - text-align: right; - line-height: 1.5em; - opacity: 0.5; -} diff --git a/js/src/views/Account/Transactions/transactions.js b/js/src/views/Account/Transactions/transactions.js index 2261284a1..8a205b343 100644 --- a/js/src/views/Account/Transactions/transactions.js +++ b/js/src/views/Account/Transactions/transactions.js @@ -17,12 +17,9 @@ import React, { Component, PropTypes } from 'react'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; -import LinearProgress from 'material-ui/LinearProgress'; import etherscan from '../../../3rdparty/etherscan'; -import { Container, ContainerTitle } from '../../../ui'; - -import Transaction from './Transaction'; +import { Container, TxList } from '../../../ui'; import styles from './transactions.css'; @@ -33,16 +30,12 @@ class Transactions extends Component { static propTypes = { address: PropTypes.string.isRequired, - accounts: PropTypes.object, - contacts: PropTypes.object, - contracts: PropTypes.object, - tokens: PropTypes.object, isTest: PropTypes.bool, traceMode: PropTypes.bool } state = { - transactions: [], + hashes: [], loading: true, callInfo: {} } @@ -67,38 +60,16 @@ class Transactions extends Component { } render () { - return ( - <Container> - <ContainerTitle title='transactions' /> - { this.renderTransactions() } - </Container> - ); - } - - renderTransactions () { - const { loading, transactions } = this.state; - - if (loading) { - return ( - <LinearProgress mode='indeterminate' /> - ); - } else if (!transactions.length) { - return ( - <div className={ styles.infonone }> - No transactions were found for this account - </div> - ); - } + const { address } = this.props; + const { hashes } = this.state; return ( - <div className={ styles.transactions }> - <table> - <tbody> - { this.renderRows() } - </tbody> - </table> + <Container title='transactions'> + <TxList + address={ address } + hashes={ hashes } /> { this.renderEtherscanFooter() } - </div> + </Container> ); } @@ -116,30 +87,6 @@ class Transactions extends Component { ); } - renderRows () { - const { address, accounts, contacts, contracts, tokens, isTest } = this.props; - const { transactions } = this.state; - - return (transactions || []) - .sort((tA, tB) => { - return tB.blockNumber.comparedTo(tA.blockNumber); - }) - .slice(0, 25) - .map((transaction, index) => { - return ( - <Transaction - key={ index } - transaction={ transaction } - address={ address } - accounts={ accounts } - contacts={ contacts } - contracts={ contracts } - tokens={ tokens } - isTest={ isTest } /> - ); - }); - } - getTransactions = (props) => { const { isTest, address, traceMode } = props; @@ -151,9 +98,9 @@ class Transactions extends Component { return this .fetchTransactions(isTest, address, traceMode) - .then(transactions => { + .then((transactions) => { this.setState({ - transactions, + hashes: transactions.map((transaction) => transaction.hash), loading: false }); }); @@ -204,16 +151,10 @@ class Transactions extends Component { function mapStateToProps (state) { const { isTest, traceMode } = state.nodeStatus; - const { accounts, contacts, contracts } = state.personal; - const { tokens } = state.balances; return { isTest, - traceMode, - accounts, - contacts, - contracts, - tokens + traceMode }; } diff --git a/js/src/views/Signer/containers/RequestsPage/RequestsPage.js b/js/src/views/Signer/containers/RequestsPage/RequestsPage.js index c53d39264..ae2ba05fb 100644 --- a/js/src/views/Signer/containers/RequestsPage/RequestsPage.js +++ b/js/src/views/Signer/containers/RequestsPage/RequestsPage.js @@ -18,15 +18,17 @@ import BigNumber from 'bignumber.js'; import React, { Component, PropTypes } from 'react'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; +import { observer } from 'mobx-react'; import Store from '../../store'; import * as RequestsActions from '../../../../redux/providers/signerActions'; -import { Container, ContainerTitle } from '../../../../ui'; +import { Container, Page, TxList } from '../../../../ui'; import { RequestPending, RequestFinished } from '../../components'; import styles from './RequestsPage.css'; +@observer class RequestsPage extends Component { static contextTypes = { api: PropTypes.object.isRequired @@ -44,20 +46,19 @@ class RequestsPage extends Component { isTest: PropTypes.bool.isRequired }; - store = new Store(this.context.api); + store = new Store(this.context.api, true); + + componentWillUnmount () { + this.store.unsubscribe(); + } render () { - const { pending, finished } = this.props.signer; - - if (!pending.length && !finished.length) { - return this.renderNoRequestsMsg(); - } - return ( - <div> - { this.renderPendingRequests() } - { this.renderFinishedRequests() } - </div> + <Page> + <div>{ this.renderPendingRequests() }</div> + <div>{ this.renderLocalQueue() }</div> + <div>{ this.renderFinishedRequests() }</div> + </Page> ); } @@ -65,18 +66,39 @@ class RequestsPage extends Component { return new BigNumber(b.id).cmp(a.id); } + renderLocalQueue () { + const { localHashes } = this.store; + + if (!localHashes.length) { + return null; + } + + return ( + <Container title='Local Transactions'> + <TxList + address='' + hashes={ localHashes } /> + </Container> + ); + } + renderPendingRequests () { const { pending } = this.props.signer; if (!pending.length) { - return; + return ( + <Container> + <div className={ styles.noRequestsMsg }> + There are no requests requiring your confirmation. + </div> + </Container> + ); } const items = pending.sort(this._sortRequests).map(this.renderPending); return ( - <Container> - <ContainerTitle title='Pending Requests' /> + <Container title='Pending Requests'> <div className={ styles.items }> { items } </div> @@ -94,8 +116,7 @@ class RequestsPage extends Component { const items = finished.sort(this._sortRequests).map(this.renderFinished); return ( - <Container> - <ContainerTitle title='Finished Requests' /> + <Container title='Finished Requests'> <div className={ styles.items }> { items } </div> @@ -143,16 +164,6 @@ class RequestsPage extends Component { /> ); } - - renderNoRequestsMsg () { - return ( - <Container> - <div className={ styles.noRequestsMsg }> - There are no requests requiring your confirmation. - </div> - </Container> - ); - } } function mapStateToProps (state) { diff --git a/js/src/views/Signer/signer.js b/js/src/views/Signer/signer.js index b9aace595..32aca9c82 100644 --- a/js/src/views/Signer/signer.js +++ b/js/src/views/Signer/signer.js @@ -16,7 +16,7 @@ import React, { Component } from 'react'; -import { Actionbar, Page } from '../../ui'; +import { Actionbar } from '../../ui'; import RequestsPage from './containers/RequestsPage'; import styles from './signer.css'; @@ -27,9 +27,7 @@ export default class Signer extends Component { <div className={ styles.signer }> <Actionbar title='Trusted Signer' /> - <Page> - <RequestsPage /> - </Page> + <RequestsPage /> </div> ); } diff --git a/js/src/views/Signer/store.js b/js/src/views/Signer/store.js index 1bb63bbe2..0eeb99861 100644 --- a/js/src/views/Signer/store.js +++ b/js/src/views/Signer/store.js @@ -14,13 +14,26 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see <http://www.gnu.org/licenses/>. +import { isEqual } from 'lodash'; import { action, observable } from 'mobx'; export default class Store { @observable balances = {}; + @observable localHashes = []; - constructor (api) { + constructor (api, withLocalTransactions = false) { this._api = api; + this._timeoutId = 0; + + if (withLocalTransactions) { + this.fetchLocalTransactions(); + } + } + + @action unsubscribe () { + if (this._timeoutId) { + clearTimeout(this._timeoutId); + } } @action setBalance = (address, balance) => { @@ -31,6 +44,12 @@ export default class Store { this.balances = Object.assign({}, this.balances, balances); } + @action setLocalHashes = (localHashes) => { + if (!isEqual(localHashes, this.localHashes)) { + this.localHashes = localHashes; + } + } + fetchBalance (address) { this._api.eth .getBalance(address) @@ -63,4 +82,18 @@ export default class Store { console.warn('Store:fetchBalances', error); }); } + + fetchLocalTransactions = () => { + const nextTimeout = () => { + this._timeoutId = setTimeout(this.fetchLocalTransactions, 1500); + }; + + this._api.parity + .localTransactions() + .then((localTransactions) => { + this.setLocalHashes(Object.keys(localTransactions)); + }) + .then(nextTimeout) + .catch(nextTimeout); + } }