diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fc67168c..7e2a608c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ ## Current Master - Added copy address button to account list. +- Fixed back button on confirm transaction screen. +- Add indication of pending transactions to account list screen. - Fixed bug where error warning was sometimes not cleared on view transition. ## 2.0.0 2016-05-23 diff --git a/ui/app/account-detail.js b/ui/app/account-detail.js index bae44ec85..1dcce1d08 100644 --- a/ui/app/account-detail.js +++ b/ui/app/account-detail.js @@ -7,10 +7,11 @@ const copyToClipboard = require('copy-to-clipboard') const actions = require('./actions') const addressSummary = require('./util').addressSummary const ReactCSSTransitionGroup = require('react-addons-css-transition-group') +const valuesFor = require('./util').valuesFor const Identicon = require('./components/identicon') const EtherBalance = require('./components/eth-balance') -const transactionList = require('./components/transaction-list') +const TransactionList = require('./components/transaction-list') const ExportAccountView = require('./components/account-export') const ethUtil = require('ethereumjs-util') const EditableLabel = require('./components/editable-label') @@ -24,7 +25,9 @@ function mapStateToProps(state) { address: state.metamask.selectedAccount, accountDetail: state.appState.accountDetail, transactions: state.metamask.transactions, - networkVersion: state.metamask.network, + network: state.metamask.network, + unconfTxs: valuesFor(state.metamask.unconfTxs), + unconfMsgs: valuesFor(state.metamask.unconfMsgs), } } @@ -139,7 +142,7 @@ AccountDetailScreen.prototype.render = function() { }), h('button', { - onClick: () => this.props.dispatch(actions.showSendPage()), + onClick: () => props.dispatch(actions.showSendPage()), style: { margin: 10, }, @@ -183,18 +186,22 @@ AccountDetailScreen.prototype.subview = function() { } AccountDetailScreen.prototype.transactionList = function() { - var state = this.props - var transactions = state.transactions + const { transactions, unconfTxs, unconfMsgs, address, network } = this.props var txsToRender = transactions - // only transactions that are from the current address - .filter(tx => tx.txParams.from === state.address) - // only transactions that are on the current network - .filter(tx => tx.txParams.metamaskNetworkId === state.networkVersion) - // sort by recency - .sort((a, b) => b.time - a.time) - - return transactionList(txsToRender, state.networkVersion) + // only transactions that are from the current address + .filter(tx => tx.txParams.from === address) + // only transactions that are on the current network + .filter(tx => tx.txParams.metamaskNetworkId === network) + // sort by recency + .sort((a, b) => b.time - a.time) + + return h(TransactionList, { + txsToRender, + network, + unconfTxs, + unconfMsgs, + }) } AccountDetailScreen.prototype.navigateToAccounts = function(event){ diff --git a/ui/app/accounts/account-panel.js b/ui/app/accounts/account-panel.js index 5ade7fe0e..8afb7308d 100644 --- a/ui/app/accounts/account-panel.js +++ b/ui/app/accounts/account-panel.js @@ -34,6 +34,7 @@ NewComponent.prototype.render = function() { }, [ h('.identicon-wrapper.flex-column.flex-center.select-none', [ + this.pendingOrNot(), h(Identicon, { address: identity.address }), @@ -61,3 +62,9 @@ NewComponent.prototype.render = function() { ]) ) } + +NewComponent.prototype.pendingOrNot = function() { + const pending = this.props.pending + if (pending.length === 0) return null + return h('.pending-dot', pending.length) +} diff --git a/ui/app/accounts/index.js b/ui/app/accounts/index.js index 1a42f7470..9cfab54e5 100644 --- a/ui/app/accounts/index.js +++ b/ui/app/accounts/index.js @@ -12,6 +12,10 @@ module.exports = connect(mapStateToProps)(AccountsScreen) function mapStateToProps(state) { + const pendingTxs = valuesFor(state.metamask.unconfTxs) + const pendingMsgs = valuesFor(state.metamask.unconfMsgs) + const pending = pendingTxs.concat(pendingMsgs) + return { accounts: state.metamask.accounts, identities: state.metamask.identities, @@ -19,6 +23,7 @@ function mapStateToProps(state) { selectedAddress: state.metamask.selectedAddress, currentDomain: state.appState.currentDomain, scrollToBottom: state.appState.scrollToBottom, + pending, } } @@ -62,12 +67,23 @@ AccountsScreen.prototype.render = function() { }, [ identityList.map((identity) => { + const pending = this.props.pending.filter((txOrMsg) => { + if ('txParams' in txOrMsg) { + return txOrMsg.txParams.from === identity.address + } else if ('msgParams' in txOrMsg) { + return txOrMsg.msgParams.from === identity.address + } else { + return false + } + }) + return h(AccountPanel, { key: `acct-panel-${identity.address}`, identity, selectedAddress: this.props.selectedAddress, accounts: this.props.accounts, onShowDetail: this.onShowDetail.bind(this), + pending, }) }), diff --git a/ui/app/app.js b/ui/app/app.js index 511012fab..7e7ca24ad 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -86,43 +86,6 @@ App.prototype.render = function() { this.renderPrimary(), ]), ]), - - // footer - // h('.app-footer.flex-row.flex-space-around', { - // style: { - // display: shouldHaveFooter ? 'flex' : 'none', - // alignItems: 'center', - // height: '56px', - // } - // }, [ - - // // settings icon - // h('i.fa.fa-cog.fa-lg' + (view === 'config' ? '.active' : '.cursor-pointer'), { - // style: { - // opacity: state.isUnlocked ? '1.0' : '0.0', - // transition: 'opacity 200ms ease-in', - // //transform: `translateX(${state.isUnlocked ? '0px' : '-100px'})`, - // }, - // onClick: function(ev) { - // state.dispatch(actions.showConfigPage()) - // }, - // }), - - // // toggle - // onOffToggle({ - // toggleMetamaskActive: this.toggleMetamaskActive.bind(this), - // isUnlocked: state.isUnlocked, - // }), - - // // help - // h('i.fa.fa-question.fa-lg.cursor-pointer', { - // style: { - // opacity: state.isUnlocked ? '1.0' : '0.0', - // }, - // onClick() { state.dispatch(actions.showInfoPage()) } - // }), - // ]), - ]) ) } @@ -276,11 +239,7 @@ App.prototype.renderPrimary = function(){ return h(CreateVaultScreen, {key: 'createVault'}) default: - if (this.hasPendingTxs()) { - return h(ConfirmTxScreen, {key: 'confirm-tx'}) - } else { - return h(AccountDetailScreen, {key: 'account-detail'}) - } + return h(AccountDetailScreen, {key: 'account-detail'}) } } @@ -296,14 +255,6 @@ App.prototype.toggleMetamaskActive = function(){ } } -App.prototype.hasPendingTxs = function() { - var state = this.props - var unconfTxs = state.unconfTxs - var unconfMsgs = state.unconfMsgs - var unconfTxList = txHelper(unconfTxs, unconfMsgs) - return unconfTxList.length > 0 -} - function onOffToggle(state){ var buttonSize = '50px'; var lockWidth = '20px'; diff --git a/ui/app/components/transaction-list-item.js b/ui/app/components/transaction-list-item.js new file mode 100644 index 000000000..a0715db0b --- /dev/null +++ b/ui/app/components/transaction-list-item.js @@ -0,0 +1,126 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +const Identicon = require('./identicon') +const EtherBalance = require('./eth-balance') +const addressSummary = require('../util').addressSummary +const explorerLink = require('../../lib/explorer-link') +const formatBalance = require('../util').formatBalance +const vreme = new (require('vreme')) + +module.exports = TransactionListItem + + +inherits(TransactionListItem, Component) +function TransactionListItem() { + Component.call(this) +} + +TransactionListItem.prototype.render = function() { + const { transaction, i } = this.props + + var date = formatDate(transaction.time) + + var isMsg = ('msgParams' in transaction) + var isTx = ('txParams' in transaction) + + var txParams = transaction.txParams + + return ( + h(`.transaction-list-item.flex-row.flex-space-between${transaction.hash ? '.pointer' : ''}`, { + key: `tx-${transaction.id + i}`, + onClick: (event) => { + if (!transaction.hash) return + var url = explorerLink(transaction.hash, parseInt(network)) + chrome.tabs.create({ url }) + }, + style: { + padding: '20px 0', + }, + }, [ + + // large identicon + h('.identicon-wrapper.flex-column.flex-center.select-none', [ + identicon(txParams, transaction), + ]), + + h('.flex-column', [ + + h('div', date), + + recipientField(txParams, transaction), + + ]), + + h(EtherBalance, { + value: txParams.value, + }), + ]) + ) +} + + +function recipientField(txParams, transaction) { + if (txParams.to) { + return h('div', { + style: { + fontSize: 'small', + color: '#ABA9AA', + }, + }, [ + addressSummary(txParams.to), + failIfFailed(transaction), + ]) + + } else { + + return h('div', { + style: { + fontSize: 'small', + color: '#ABA9AA', + }, + },[ + 'Contract Published', + failIfFailed(transaction), + ]) + } +} + +TransactionListItem.prototype.renderMessage = function() { + const { transaction, i } = this.props + return h('div', 'wowie, thats a message') +} + +function formatDate(date){ + return vreme.format(new Date(date), 'March 16 2014 14:30') +} + +function identicon(txParams, transaction) { + if (transaction.status === 'rejected') { + return h('i.fa.fa-exclamation-triangle.fa-lg.error', { + style: { + width: '24px', + } + }) + } + + if (txParams.to) { + return h(Identicon, { + diameter: 24, + address: txParams.to || transaction.hash, + }) + } else { + return h('i.fa.fa-file-text-o.fa-lg', { + style: { + width: '24px', + } + }) + } +} + +function failIfFailed(transaction) { + if (transaction.status === 'rejected') { + return h('span.error', ' (Failed)') + } +} diff --git a/ui/app/components/transaction-list.js b/ui/app/components/transaction-list.js index f85aab70f..86abd9709 100644 --- a/ui/app/components/transaction-list.js +++ b/ui/app/components/transaction-list.js @@ -1,14 +1,21 @@ +const Component = require('react').Component const h = require('react-hyperscript') -const vreme = new (require('vreme')) -const formatBalance = require('../util').formatBalance -const addressSummary = require('../util').addressSummary -const explorerLink = require('../../lib/explorer-link') -const Panel = require('./panel') -const Identicon = require('./identicon') -const EtherBalance = require('./eth-balance') +const inherits = require('util').inherits +const TransactionListItem = require('./transaction-list-item') + +module.exports = TransactionList + + +inherits(TransactionList, Component) +function TransactionList() { + Component.call(this) +} + +TransactionList.prototype.render = function() { + const { txsToRender, network, unconfTxs, unconfMsgs } = this.props + const transactions = txsToRender -module.exports = function(transactions, network) { return ( h('section.transaction-list', [ @@ -42,118 +49,19 @@ module.exports = function(transactions, network) { }, ( transactions.length ? - transactions.map(renderTransaction) + transactions.map((transaction, i) => { + return h(TransactionListItem, { + transaction, i + }) + }) : [h('.flex-center', { style: { height: '100%', }, }, 'No transaction history...')] - )) - ]) - ) - - - function renderTransaction(transaction, i){ - - var txParams = transaction.txParams - var date = formatDate(transaction.time) - - return ( - - h(`.transaction-list-item.flex-row.flex-space-between${transaction.hash ? '.pointer' : ''}`, { - key: `tx-${transaction.id + i}`, - onClick: (event) => { - if (!transaction.hash) return - var url = explorerLink(transaction.hash, parseInt(network)) - chrome.tabs.create({ url }) - }, - style: { - padding: '20px 0', - }, - }, [ - - // large identicon - h('.identicon-wrapper.flex-column.flex-center.select-none', [ - identicon(txParams, transaction), - ]), - - h('.flex-column', [ - - h('div', date), - - recipientField(txParams, transaction), - - ]), - - h(EtherBalance, { - value: txParams.value, - }), - ]) - - ) - } -} - -function recipientField(txParams, transaction) { - if (txParams.to) { - return h('div', { - style: { - fontSize: 'small', - color: '#ABA9AA', - }, - }, [ - addressSummary(txParams.to), - failIfFailed(transaction), - ]) - - } else { - - return h('div', { - style: { - fontSize: 'small', - color: '#ABA9AA', - }, - },[ - 'Contract Published', - failIfFailed(transaction), - ]) - - } -} - -function formatDate(date){ - return vreme.format(new Date(date), 'March 16 2014 14:30') } -function identicon(txParams, transaction) { - if (transaction.status === 'rejected') { - return h('i.fa.fa-exclamation-triangle.fa-lg.error', { - style: { - width: '24px', - } - }) - } - - if (txParams.to) { - return h(Identicon, { - diameter: 24, - address: txParams.to || transaction.hash, - }) - } else { - return h('i.fa.fa-file-text-o.fa-lg', { - style: { - width: '24px', - } - }) - } -} - -function failIfFailed(transaction) { - if (transaction.status === 'rejected') { - return h('span.error', ' (Failed)') - } -} diff --git a/ui/app/css/lib.css b/ui/app/css/lib.css index d9719b1e3..73be4023e 100644 --- a/ui/app/css/lib.css +++ b/ui/app/css/lib.css @@ -166,3 +166,17 @@ hr.horizontal-line { .hover-white:hover { background: white; } + +.pending-dot { + background: red; + left: 57px; + color: white; + border-radius: 10px; + height: 20px; + min-width: 20px; + position: absolute; + display: flex; + align-items: center; + justify-content: center; + padding: 4px; +} diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js index 493246a35..a98b809d6 100644 --- a/ui/app/reducers/app.js +++ b/ui/app/reducers/app.js @@ -9,8 +9,17 @@ function reduceApp(state, action) { // clone and defaults const selectedAccount = state.metamask.selectedAccount + const pendingTxs = hasPendingTxs(state) + let name = 'accounts' + if (selectedAccount) { + defaultView = 'accountDetail' + } + if (pendingTxs) { + defaultView = 'confTx' + } + var defaultView = { - name: selectedAccount ? 'accountDetail' : 'accounts', + name, detailView: null, context: selectedAccount, } @@ -122,7 +131,6 @@ function reduceApp(state, action) { case actions.UNLOCK_METAMASK: return extend(appState, { - currentView: {}, detailView: {}, transForward: true, isLoading: false, @@ -145,7 +153,9 @@ function reduceApp(state, action) { case actions.GO_HOME: return extend(appState, { - currentView: {}, + currentView: extend(appState.currentView, { + name: 'accountDetail', + }), accountDetail: { subview: 'transactions', accountExport: 'none', @@ -347,6 +357,12 @@ function reduceApp(state, action) { default: return appState - } } + +function hasPendingTxs (state) { + var unconfTxs = state.metamask.unconfTxs + var unconfMsgs = state.metamask.unconfMsgs + var unconfTxList = txHelper(unconfTxs, unconfMsgs) + return unconfTxList.length > 0 +}