Merge pull request #5992 from MetaMask/i5140-accounts
Add scrolling button to account listfeature/default_network_editable
commit
9b0b2f32f7
After Width: | Height: | Size: 317 B |
@ -0,0 +1,301 @@ |
||||
import React, { PureComponent } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import debounce from 'lodash.debounce' |
||||
import { Menu, Item, Divider, CloseArea } from '../dropdowns/components/menu' |
||||
import { ENVIRONMENT_TYPE_POPUP } from '../../../../app/scripts/lib/enums' |
||||
import { getEnvironmentType } from '../../../../app/scripts/lib/util' |
||||
import Tooltip from '../tooltip' |
||||
import Identicon from '../identicon' |
||||
import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display' |
||||
import { PRIMARY } from '../../constants/common' |
||||
import { |
||||
SETTINGS_ROUTE, |
||||
INFO_ROUTE, |
||||
NEW_ACCOUNT_ROUTE, |
||||
IMPORT_ACCOUNT_ROUTE, |
||||
CONNECT_HARDWARE_ROUTE, |
||||
DEFAULT_ROUTE, |
||||
} from '../../routes' |
||||
|
||||
export default class AccountMenu extends PureComponent { |
||||
static contextTypes = { |
||||
t: PropTypes.func, |
||||
} |
||||
|
||||
static propTypes = { |
||||
accounts: PropTypes.object, |
||||
history: PropTypes.object, |
||||
identities: PropTypes.object, |
||||
isAccountMenuOpen: PropTypes.bool, |
||||
keyrings: PropTypes.array, |
||||
lockMetamask: PropTypes.func, |
||||
selectedAddress: PropTypes.string, |
||||
showAccountDetail: PropTypes.func, |
||||
showRemoveAccountConfirmationModal: PropTypes.func, |
||||
toggleAccountMenu: PropTypes.func, |
||||
} |
||||
|
||||
state = { |
||||
atAccountListBottom: false, |
||||
} |
||||
|
||||
componentDidUpdate (prevProps) { |
||||
const { prevIsAccountMenuOpen } = prevProps |
||||
const { isAccountMenuOpen } = this.props |
||||
|
||||
if (!prevIsAccountMenuOpen && isAccountMenuOpen) { |
||||
this.setAtAccountListBottom() |
||||
} |
||||
} |
||||
|
||||
renderAccounts () { |
||||
const { |
||||
identities, |
||||
accounts, |
||||
selectedAddress, |
||||
keyrings, |
||||
showAccountDetail, |
||||
} = this.props |
||||
|
||||
const accountOrder = keyrings.reduce((list, keyring) => list.concat(keyring.accounts), []) |
||||
|
||||
return accountOrder.filter(address => !!identities[address]).map(address => { |
||||
const identity = identities[address] |
||||
const isSelected = identity.address === selectedAddress |
||||
|
||||
const balanceValue = accounts[address] ? accounts[address].balance : '' |
||||
const simpleAddress = identity.address.substring(2).toLowerCase() |
||||
|
||||
const keyring = keyrings.find(kr => { |
||||
return kr.accounts.includes(simpleAddress) || kr.accounts.includes(identity.address) |
||||
}) |
||||
|
||||
return ( |
||||
<div |
||||
className="account-menu__account menu__item--clickable" |
||||
onClick={() => showAccountDetail(identity.address)} |
||||
key={identity.address} |
||||
> |
||||
<div className="account-menu__check-mark"> |
||||
{ isSelected && <div className="account-menu__check-mark-icon" /> } |
||||
</div> |
||||
<Identicon |
||||
address={identity.address} |
||||
diameter={24} |
||||
/> |
||||
<div className="account-menu__account-info"> |
||||
<div className="account-menu__name"> |
||||
{ identity.name || '' } |
||||
</div> |
||||
<UserPreferencedCurrencyDisplay |
||||
className="account-menu__balance" |
||||
value={balanceValue} |
||||
type={PRIMARY} |
||||
/> |
||||
</div> |
||||
{ this.renderKeyringType(keyring) } |
||||
{ this.renderRemoveAccount(keyring, identity) } |
||||
</div> |
||||
) |
||||
}) |
||||
} |
||||
|
||||
renderRemoveAccount (keyring, identity) { |
||||
const { t } = this.context |
||||
// Any account that's not from the HD wallet Keyring can be removed
|
||||
const { type } = keyring |
||||
const isRemovable = type !== 'HD Key Tree' |
||||
|
||||
return isRemovable && ( |
||||
<Tooltip |
||||
title={t('removeAccount')} |
||||
position="bottom" |
||||
> |
||||
<a |
||||
className="remove-account-icon" |
||||
onClick={e => this.removeAccount(e, identity)} |
||||
/> |
||||
</Tooltip> |
||||
) |
||||
} |
||||
|
||||
removeAccount (e, identity) { |
||||
e.preventDefault() |
||||
e.stopPropagation() |
||||
const { showRemoveAccountConfirmationModal } = this.props |
||||
showRemoveAccountConfirmationModal(identity) |
||||
} |
||||
|
||||
renderKeyringType (keyring) { |
||||
const { t } = this.context |
||||
|
||||
// Sometimes keyrings aren't loaded yet
|
||||
if (!keyring) { |
||||
return null |
||||
} |
||||
|
||||
const { type } = keyring |
||||
let label |
||||
|
||||
switch (type) { |
||||
case 'Trezor Hardware': |
||||
case 'Ledger Hardware': |
||||
label = t('hardware') |
||||
break |
||||
case 'Simple Key Pair': |
||||
label = t('imported') |
||||
break |
||||
} |
||||
|
||||
return label && ( |
||||
<div className="keyring-label allcaps"> |
||||
{ label } |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
setAtAccountListBottom = () => { |
||||
const target = document.querySelector('.account-menu__accounts') |
||||
const { scrollTop, offsetHeight, scrollHeight } = target |
||||
const atAccountListBottom = scrollTop + offsetHeight >= scrollHeight |
||||
this.setState({ atAccountListBottom }) |
||||
} |
||||
|
||||
onScroll = debounce(this.setAtAccountListBottom, 25) |
||||
|
||||
handleScrollDown = e => { |
||||
e.stopPropagation() |
||||
const target = document.querySelector('.account-menu__accounts') |
||||
const { scrollHeight } = target |
||||
target.scroll({ left: 0, top: scrollHeight, behavior: 'smooth' }) |
||||
this.setAtAccountListBottom() |
||||
} |
||||
|
||||
renderScrollButton () { |
||||
const { accounts } = this.props |
||||
const { atAccountListBottom } = this.state |
||||
|
||||
return !atAccountListBottom && Object.keys(accounts).length > 3 && ( |
||||
<div |
||||
className="account-menu__scroll-button" |
||||
onClick={this.handleScrollDown} |
||||
> |
||||
<img |
||||
src="./images/icons/down-arrow.svg" |
||||
width={28} |
||||
height={28} |
||||
/> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
render () { |
||||
const { t } = this.context |
||||
const { |
||||
isAccountMenuOpen, |
||||
toggleAccountMenu, |
||||
lockMetamask, |
||||
history, |
||||
} = this.props |
||||
|
||||
return ( |
||||
<Menu |
||||
className="account-menu" |
||||
isShowing={isAccountMenuOpen} |
||||
> |
||||
<CloseArea onClick={toggleAccountMenu} /> |
||||
<Item className="account-menu__header"> |
||||
{ t('myAccounts') } |
||||
<button |
||||
className="account-menu__logout-button" |
||||
onClick={() => { |
||||
lockMetamask() |
||||
history.push(DEFAULT_ROUTE) |
||||
}} |
||||
> |
||||
{ t('logout') } |
||||
</button> |
||||
</Item> |
||||
<Divider /> |
||||
<div className="account-menu__accounts-container"> |
||||
<div |
||||
className="account-menu__accounts" |
||||
onScroll={this.onScroll} |
||||
> |
||||
{ this.renderAccounts() } |
||||
</div> |
||||
{ this.renderScrollButton() } |
||||
</div> |
||||
<Divider /> |
||||
<Item |
||||
onClick={() => { |
||||
toggleAccountMenu() |
||||
history.push(NEW_ACCOUNT_ROUTE) |
||||
}} |
||||
icon={ |
||||
<img |
||||
className="account-menu__item-icon" |
||||
src="images/plus-btn-white.svg" |
||||
/> |
||||
} |
||||
text={t('createAccount')} |
||||
/> |
||||
<Item |
||||
onClick={() => { |
||||
toggleAccountMenu() |
||||
history.push(IMPORT_ACCOUNT_ROUTE) |
||||
}} |
||||
icon={ |
||||
<img |
||||
className="account-menu__item-icon" |
||||
src="images/import-account.svg" |
||||
/> |
||||
} |
||||
text={t('importAccount')} |
||||
/> |
||||
<Item |
||||
onClick={() => { |
||||
toggleAccountMenu() |
||||
|
||||
if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) { |
||||
global.platform.openExtensionInBrowser(CONNECT_HARDWARE_ROUTE) |
||||
} else { |
||||
history.push(CONNECT_HARDWARE_ROUTE) |
||||
} |
||||
}} |
||||
icon={ |
||||
<img |
||||
className="account-menu__item-icon" |
||||
src="images/connect-icon.svg" |
||||
/> |
||||
} |
||||
text={t('connectHardwareWallet')} |
||||
/> |
||||
<Divider /> |
||||
<Item |
||||
onClick={() => { |
||||
toggleAccountMenu() |
||||
history.push(INFO_ROUTE) |
||||
}} |
||||
icon={ |
||||
<img src="images/mm-info-icon.svg" /> |
||||
} |
||||
text={t('infoHelp')} |
||||
/> |
||||
<Item |
||||
onClick={() => { |
||||
toggleAccountMenu() |
||||
history.push(SETTINGS_ROUTE) |
||||
}} |
||||
icon={ |
||||
<img |
||||
className="account-menu__item-icon" |
||||
src="images/settings.svg" |
||||
/> |
||||
} |
||||
text={t('settings')} |
||||
/> |
||||
</Menu> |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,62 @@ |
||||
import { connect } from 'react-redux' |
||||
import { compose } from 'recompose' |
||||
import { withRouter } from 'react-router-dom' |
||||
import { |
||||
toggleAccountMenu, |
||||
showAccountDetail, |
||||
hideSidebar, |
||||
lockMetamask, |
||||
hideWarning, |
||||
showConfigPage, |
||||
showInfoPage, |
||||
showModal, |
||||
} from '../../actions' |
||||
import { getMetaMaskAccounts } from '../../selectors' |
||||
import AccountMenu from './account-menu.component' |
||||
|
||||
function mapStateToProps (state) { |
||||
const { metamask: { selectedAddress, isAccountMenuOpen, keyrings, identities } } = state |
||||
|
||||
return { |
||||
selectedAddress, |
||||
isAccountMenuOpen, |
||||
keyrings, |
||||
identities, |
||||
accounts: getMetaMaskAccounts(state), |
||||
} |
||||
} |
||||
|
||||
function mapDispatchToProps (dispatch) { |
||||
return { |
||||
toggleAccountMenu: () => dispatch(toggleAccountMenu()), |
||||
showAccountDetail: address => { |
||||
dispatch(showAccountDetail(address)) |
||||
dispatch(hideSidebar()) |
||||
dispatch(toggleAccountMenu()) |
||||
}, |
||||
lockMetamask: () => { |
||||
dispatch(lockMetamask()) |
||||
dispatch(hideWarning()) |
||||
dispatch(hideSidebar()) |
||||
dispatch(toggleAccountMenu()) |
||||
}, |
||||
showConfigPage: () => { |
||||
dispatch(showConfigPage()) |
||||
dispatch(hideSidebar()) |
||||
dispatch(toggleAccountMenu()) |
||||
}, |
||||
showInfoPage: () => { |
||||
dispatch(showInfoPage()) |
||||
dispatch(hideSidebar()) |
||||
dispatch(toggleAccountMenu()) |
||||
}, |
||||
showRemoveAccountConfirmationModal: identity => { |
||||
return dispatch(showModal({ name: 'CONFIRM_REMOVE_ACCOUNT', identity })) |
||||
}, |
||||
} |
||||
} |
||||
|
||||
export default compose( |
||||
withRouter, |
||||
connect(mapStateToProps, mapDispatchToProps) |
||||
)(AccountMenu) |
@ -1,249 +1 @@ |
||||
const inherits = require('util').inherits |
||||
const Component = require('react').Component |
||||
const connect = require('react-redux').connect |
||||
const { compose } = require('recompose') |
||||
const { withRouter } = require('react-router-dom') |
||||
const PropTypes = require('prop-types') |
||||
const h = require('react-hyperscript') |
||||
const actions = require('../../actions') |
||||
const { Menu, Item, Divider, CloseArea } = require('../dropdowns/components/menu') |
||||
const { ENVIRONMENT_TYPE_POPUP } = require('../../../../app/scripts/lib/enums') |
||||
const { getEnvironmentType } = require('../../../../app/scripts/lib/util') |
||||
const Tooltip = require('../tooltip') |
||||
import Identicon from '../identicon' |
||||
import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display' |
||||
import { PRIMARY } from '../../constants/common' |
||||
import { getMetaMaskAccounts } from '../../selectors' |
||||
|
||||
const { |
||||
SETTINGS_ROUTE, |
||||
INFO_ROUTE, |
||||
NEW_ACCOUNT_ROUTE, |
||||
IMPORT_ACCOUNT_ROUTE, |
||||
CONNECT_HARDWARE_ROUTE, |
||||
DEFAULT_ROUTE, |
||||
} = require('../../routes') |
||||
|
||||
module.exports = compose( |
||||
withRouter, |
||||
connect(mapStateToProps, mapDispatchToProps) |
||||
)(AccountMenu) |
||||
|
||||
AccountMenu.contextTypes = { |
||||
t: PropTypes.func, |
||||
} |
||||
|
||||
inherits(AccountMenu, Component) |
||||
function AccountMenu () { Component.call(this) } |
||||
|
||||
function mapStateToProps (state) { |
||||
return { |
||||
selectedAddress: state.metamask.selectedAddress, |
||||
isAccountMenuOpen: state.metamask.isAccountMenuOpen, |
||||
keyrings: state.metamask.keyrings, |
||||
identities: state.metamask.identities, |
||||
accounts: getMetaMaskAccounts(state), |
||||
} |
||||
} |
||||
|
||||
function mapDispatchToProps (dispatch) { |
||||
return { |
||||
toggleAccountMenu: () => dispatch(actions.toggleAccountMenu()), |
||||
showAccountDetail: address => { |
||||
dispatch(actions.showAccountDetail(address)) |
||||
dispatch(actions.hideSidebar()) |
||||
dispatch(actions.toggleAccountMenu()) |
||||
}, |
||||
lockMetamask: () => { |
||||
dispatch(actions.lockMetamask()) |
||||
dispatch(actions.hideWarning()) |
||||
dispatch(actions.hideSidebar()) |
||||
dispatch(actions.toggleAccountMenu()) |
||||
}, |
||||
showConfigPage: () => { |
||||
dispatch(actions.showConfigPage()) |
||||
dispatch(actions.hideSidebar()) |
||||
dispatch(actions.toggleAccountMenu()) |
||||
}, |
||||
showInfoPage: () => { |
||||
dispatch(actions.showInfoPage()) |
||||
dispatch(actions.hideSidebar()) |
||||
dispatch(actions.toggleAccountMenu()) |
||||
}, |
||||
showRemoveAccountConfirmationModal: (identity) => { |
||||
return dispatch(actions.showModal({ name: 'CONFIRM_REMOVE_ACCOUNT', identity })) |
||||
}, |
||||
} |
||||
} |
||||
|
||||
AccountMenu.prototype.render = function () { |
||||
const { |
||||
isAccountMenuOpen, |
||||
toggleAccountMenu, |
||||
lockMetamask, |
||||
history, |
||||
} = this.props |
||||
|
||||
return h(Menu, { className: 'account-menu', isShowing: isAccountMenuOpen }, [ |
||||
h(CloseArea, { onClick: toggleAccountMenu }), |
||||
h(Item, { |
||||
className: 'account-menu__header', |
||||
}, [ |
||||
this.context.t('myAccounts'), |
||||
h('button.account-menu__logout-button', { |
||||
onClick: () => { |
||||
lockMetamask() |
||||
history.push(DEFAULT_ROUTE) |
||||
}, |
||||
}, this.context.t('logout')), |
||||
]), |
||||
h(Divider), |
||||
h('div.account-menu__accounts', this.renderAccounts()), |
||||
h(Divider), |
||||
h(Item, { |
||||
onClick: () => { |
||||
toggleAccountMenu() |
||||
history.push(NEW_ACCOUNT_ROUTE) |
||||
}, |
||||
icon: h('img.account-menu__item-icon', { src: 'images/plus-btn-white.svg' }), |
||||
text: this.context.t('createAccount'), |
||||
}), |
||||
h(Item, { |
||||
onClick: () => { |
||||
toggleAccountMenu() |
||||
history.push(IMPORT_ACCOUNT_ROUTE) |
||||
}, |
||||
icon: h('img.account-menu__item-icon', { src: 'images/import-account.svg' }), |
||||
text: this.context.t('importAccount'), |
||||
}), |
||||
h(Item, { |
||||
onClick: () => { |
||||
toggleAccountMenu() |
||||
if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) { |
||||
global.platform.openExtensionInBrowser(CONNECT_HARDWARE_ROUTE) |
||||
} else { |
||||
history.push(CONNECT_HARDWARE_ROUTE) |
||||
} |
||||
}, |
||||
icon: h('img.account-menu__item-icon', { src: 'images/connect-icon.svg' }), |
||||
text: this.context.t('connectHardwareWallet'), |
||||
}), |
||||
h(Divider), |
||||
h(Item, { |
||||
onClick: () => { |
||||
toggleAccountMenu() |
||||
history.push(INFO_ROUTE) |
||||
}, |
||||
icon: h('img', { src: 'images/mm-info-icon.svg' }), |
||||
text: this.context.t('infoHelp'), |
||||
}), |
||||
h(Item, { |
||||
onClick: () => { |
||||
toggleAccountMenu() |
||||
history.push(SETTINGS_ROUTE) |
||||
}, |
||||
icon: h('img.account-menu__item-icon', { src: 'images/settings.svg' }), |
||||
text: this.context.t('settings'), |
||||
}), |
||||
]) |
||||
} |
||||
|
||||
AccountMenu.prototype.renderAccounts = function () { |
||||
const { |
||||
identities, |
||||
accounts, |
||||
selectedAddress, |
||||
keyrings, |
||||
showAccountDetail, |
||||
} = this.props |
||||
|
||||
const accountOrder = keyrings.reduce((list, keyring) => list.concat(keyring.accounts), []) |
||||
return accountOrder.filter(address => !!identities[address]).map((address) => { |
||||
|
||||
const identity = identities[address] |
||||
const isSelected = identity.address === selectedAddress |
||||
|
||||
const balanceValue = accounts[address] ? accounts[address].balance : '' |
||||
const simpleAddress = identity.address.substring(2).toLowerCase() |
||||
|
||||
const keyring = keyrings.find((kr) => { |
||||
return kr.accounts.includes(simpleAddress) || |
||||
kr.accounts.includes(identity.address) |
||||
}) |
||||
|
||||
return h( |
||||
'div.account-menu__account.menu__item--clickable', |
||||
{ onClick: () => showAccountDetail(identity.address) }, |
||||
[ |
||||
h('div.account-menu__check-mark', [ |
||||
isSelected ? h('div.account-menu__check-mark-icon') : null, |
||||
]), |
||||
|
||||
h( |
||||
Identicon, |
||||
{ |
||||
address: identity.address, |
||||
diameter: 24, |
||||
}, |
||||
), |
||||
|
||||
h('div.account-menu__account-info', [ |
||||
h('div.account-menu__name', identity.name || ''), |
||||
h(UserPreferencedCurrencyDisplay, { |
||||
className: 'account-menu__balance', |
||||
value: balanceValue, |
||||
type: PRIMARY, |
||||
}), |
||||
]), |
||||
|
||||
this.renderKeyringType(keyring), |
||||
this.renderRemoveAccount(keyring, identity), |
||||
], |
||||
) |
||||
}) |
||||
} |
||||
|
||||
AccountMenu.prototype.renderRemoveAccount = function (keyring, identity) { |
||||
// Any account that's not from the HD wallet Keyring can be removed
|
||||
const type = keyring.type |
||||
const isRemovable = type !== 'HD Key Tree' |
||||
if (isRemovable) { |
||||
return h(Tooltip, { |
||||
title: this.context.t('removeAccount'), |
||||
position: 'bottom', |
||||
}, [ |
||||
h('a.remove-account-icon', { |
||||
onClick: (e) => this.removeAccount(e, identity), |
||||
}, ''), |
||||
]) |
||||
} |
||||
return null |
||||
} |
||||
|
||||
AccountMenu.prototype.removeAccount = function (e, identity) { |
||||
e.preventDefault() |
||||
e.stopPropagation() |
||||
const { showRemoveAccountConfirmationModal } = this.props |
||||
showRemoveAccountConfirmationModal(identity) |
||||
} |
||||
|
||||
AccountMenu.prototype.renderKeyringType = function (keyring) { |
||||
try { // Sometimes keyrings aren't loaded yet:
|
||||
const type = keyring.type |
||||
let label |
||||
switch (type) { |
||||
case 'Trezor Hardware': |
||||
case 'Ledger Hardware': |
||||
label = this.context.t('hardware') |
||||
break |
||||
case 'Simple Key Pair': |
||||
label = this.context.t('imported') |
||||
break |
||||
default: |
||||
label = '' |
||||
} |
||||
|
||||
return label !== '' ? h('.keyring-label.allcaps', label) : null |
||||
|
||||
} catch (e) { return } |
||||
} |
||||
export { default } from './account-menu.container' |
||||
|
Loading…
Reference in new issue