mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
Search accounts by name (#7261)
Co-Authored-By: Paweł Lula <pavloblack@hotmail.com> Co-Authored-By: Mark Stacey <markjstacey@gmail.com>
This commit is contained in:
parent
6e0b8f80ad
commit
638e242ce6
@ -1199,6 +1199,12 @@
|
||||
"searchTokens": {
|
||||
"message": "Search Tokens"
|
||||
},
|
||||
"searchAccounts": {
|
||||
"message": "Search Accounts"
|
||||
},
|
||||
"noAccountsFound": {
|
||||
"message": "No accounts found for the given search query"
|
||||
},
|
||||
"selectAnAccount": {
|
||||
"message": "Select an Account"
|
||||
},
|
||||
|
@ -1,6 +1,9 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import debounce from 'lodash.debounce'
|
||||
import Fuse from 'fuse.js'
|
||||
import InputAdornment from '@material-ui/core/InputAdornment'
|
||||
|
||||
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'
|
||||
@ -17,17 +20,19 @@ import {
|
||||
CONNECT_HARDWARE_ROUTE,
|
||||
DEFAULT_ROUTE,
|
||||
} from '../../../helpers/constants/routes'
|
||||
import TextField from '../../ui/text-field'
|
||||
import SearchIcon from '../../ui/search-icon'
|
||||
|
||||
export default class AccountMenu extends PureComponent {
|
||||
export default class AccountMenu extends Component {
|
||||
static contextTypes = {
|
||||
t: PropTypes.func,
|
||||
metricsEvent: PropTypes.func,
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
accounts: PropTypes.object,
|
||||
shouldShowAccountsSearch: PropTypes.bool,
|
||||
accounts: PropTypes.array,
|
||||
history: PropTypes.object,
|
||||
identities: PropTypes.object,
|
||||
isAccountMenuOpen: PropTypes.bool,
|
||||
keyrings: PropTypes.array,
|
||||
lockMetamask: PropTypes.func,
|
||||
@ -39,22 +44,76 @@ export default class AccountMenu extends PureComponent {
|
||||
originOfCurrentTab: PropTypes.string,
|
||||
}
|
||||
|
||||
accountsRef;
|
||||
|
||||
state = {
|
||||
atAccountListBottom: false,
|
||||
shouldShowScrollButton: false,
|
||||
searchQuery: '',
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
addressFuse = new Fuse([], {
|
||||
shouldSort: false,
|
||||
threshold: 0.45,
|
||||
location: 0,
|
||||
distance: 100,
|
||||
maxPatternLength: 32,
|
||||
minMatchCharLength: 1,
|
||||
keys: [
|
||||
{ name: 'name', weight: 0.5 },
|
||||
{ name: 'address', weight: 0.5 },
|
||||
],
|
||||
})
|
||||
|
||||
componentDidUpdate (prevProps, prevState) {
|
||||
const { isAccountMenuOpen: prevIsAccountMenuOpen } = prevProps
|
||||
const { searchQuery: prevSearchQuery } = prevState
|
||||
const { isAccountMenuOpen } = this.props
|
||||
const { searchQuery } = this.state
|
||||
|
||||
if (!prevIsAccountMenuOpen && isAccountMenuOpen) {
|
||||
this.setAtAccountListBottom()
|
||||
this.setShouldShowScrollButton()
|
||||
this.resetSearchQuery()
|
||||
}
|
||||
|
||||
// recalculate on each search query change
|
||||
// whether we can show scroll down button
|
||||
if (isAccountMenuOpen && prevSearchQuery !== searchQuery) {
|
||||
this.setShouldShowScrollButton()
|
||||
}
|
||||
}
|
||||
|
||||
renderAccountsSearch () {
|
||||
const inputAdornment = (
|
||||
<InputAdornment
|
||||
position="start"
|
||||
style={{
|
||||
maxHeight: 'none',
|
||||
marginRight: 0,
|
||||
marginLeft: '8px',
|
||||
}}
|
||||
>
|
||||
<SearchIcon />
|
||||
</InputAdornment>
|
||||
)
|
||||
|
||||
return [
|
||||
<TextField
|
||||
key="search-text-field"
|
||||
id="search-accounts"
|
||||
placeholder={this.context.t('searchAccounts')}
|
||||
type="text"
|
||||
value={this.state.searchQuery}
|
||||
onChange={e => this.setSearchQuery(e.target.value)}
|
||||
startAdornment={inputAdornment}
|
||||
fullWidth
|
||||
theme="material-white-padded"
|
||||
/>,
|
||||
<Divider key="search-divider" />,
|
||||
]
|
||||
}
|
||||
|
||||
renderAccounts () {
|
||||
const {
|
||||
identities,
|
||||
accounts,
|
||||
selectedAddress,
|
||||
keyrings,
|
||||
@ -62,14 +121,21 @@ export default class AccountMenu extends PureComponent {
|
||||
addressConnectedDomainMap,
|
||||
originOfCurrentTab,
|
||||
} = this.props
|
||||
const { searchQuery } = this.state
|
||||
|
||||
const accountOrder = keyrings.reduce((list, keyring) => list.concat(keyring.accounts), [])
|
||||
let filteredIdentities = accounts
|
||||
if (searchQuery) {
|
||||
this.addressFuse.setCollection(accounts)
|
||||
filteredIdentities = this.addressFuse.search(searchQuery)
|
||||
}
|
||||
|
||||
return accountOrder.filter(address => !!identities[address]).map(address => {
|
||||
const identity = identities[address]
|
||||
if (filteredIdentities.length === 0) {
|
||||
return <p className="account-menu__no-accounts">{this.context.t('noAccountsFound')}</p>
|
||||
}
|
||||
|
||||
return filteredIdentities.map(identity => {
|
||||
const isSelected = identity.address === selectedAddress
|
||||
|
||||
const balanceValue = accounts[address] ? accounts[address].balance : ''
|
||||
const simpleAddress = identity.address.substring(2).toLowerCase()
|
||||
|
||||
const keyring = keyrings.find(kr => {
|
||||
@ -106,7 +172,7 @@ export default class AccountMenu extends PureComponent {
|
||||
</div>
|
||||
<UserPreferencedCurrencyDisplay
|
||||
className="account-menu__balance"
|
||||
value={balanceValue}
|
||||
value={identity.balance}
|
||||
type={PRIMARY}
|
||||
/>
|
||||
</div>
|
||||
@ -181,28 +247,41 @@ export default class AccountMenu extends PureComponent {
|
||||
)
|
||||
}
|
||||
|
||||
setAtAccountListBottom = () => {
|
||||
const target = document.querySelector('.account-menu__accounts')
|
||||
const { scrollTop, offsetHeight, scrollHeight } = target
|
||||
const atAccountListBottom = scrollTop + offsetHeight >= scrollHeight
|
||||
this.setState({ atAccountListBottom })
|
||||
resetSearchQuery () {
|
||||
this.setSearchQuery('')
|
||||
}
|
||||
|
||||
onScroll = debounce(this.setAtAccountListBottom, 25)
|
||||
setSearchQuery (searchQuery) {
|
||||
this.setState({ searchQuery })
|
||||
}
|
||||
|
||||
setShouldShowScrollButton = () => {
|
||||
const { scrollTop, offsetHeight, scrollHeight } = this.accountsRef
|
||||
|
||||
const canScroll = scrollHeight > offsetHeight
|
||||
|
||||
const atAccountListBottom = scrollTop + offsetHeight >= scrollHeight
|
||||
|
||||
const shouldShowScrollButton = canScroll && !atAccountListBottom
|
||||
|
||||
this.setState({ shouldShowScrollButton})
|
||||
}
|
||||
|
||||
onScroll = debounce(this.setShouldShowScrollButton, 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()
|
||||
|
||||
const { scrollHeight } = this.accountsRef
|
||||
this.accountsRef.scroll({ left: 0, top: scrollHeight, behavior: 'smooth' })
|
||||
|
||||
this.setShouldShowScrollButton()
|
||||
}
|
||||
|
||||
renderScrollButton () {
|
||||
const { accounts } = this.props
|
||||
const { atAccountListBottom } = this.state
|
||||
const { shouldShowScrollButton } = this.state
|
||||
|
||||
return !atAccountListBottom && Object.keys(accounts).length > 3 && (
|
||||
return shouldShowScrollButton && (
|
||||
<div
|
||||
className="account-menu__scroll-button"
|
||||
onClick={this.handleScrollDown}
|
||||
@ -211,20 +290,21 @@ export default class AccountMenu extends PureComponent {
|
||||
src="./images/icons/down-arrow.svg"
|
||||
width={28}
|
||||
height={28}
|
||||
alt="scroll down"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { t } = this.context
|
||||
const { t, metricsEvent } = this.context
|
||||
const {
|
||||
shouldShowAccountsSearch,
|
||||
isAccountMenuOpen,
|
||||
toggleAccountMenu,
|
||||
lockMetamask,
|
||||
history,
|
||||
} = this.props
|
||||
const { metricsEvent } = this.context
|
||||
|
||||
return (
|
||||
<Menu
|
||||
@ -246,9 +326,13 @@ export default class AccountMenu extends PureComponent {
|
||||
</Item>
|
||||
<Divider />
|
||||
<div className="account-menu__accounts-container">
|
||||
{shouldShowAccountsSearch ? this.renderAccountsSearch() : null}
|
||||
<div
|
||||
className="account-menu__accounts"
|
||||
onScroll={this.onScroll}
|
||||
ref={ref => {
|
||||
this.accountsRef = ref
|
||||
}}
|
||||
>
|
||||
{ this.renderAccounts() }
|
||||
</div>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { connect } from 'react-redux'
|
||||
import { compose } from 'recompose'
|
||||
import { compose, withProps } from 'recompose'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import {
|
||||
toggleAccountMenu,
|
||||
@ -13,23 +13,28 @@ import {
|
||||
} from '../../../store/actions'
|
||||
import {
|
||||
getAddressConnectedDomainMap,
|
||||
getMetaMaskAccounts,
|
||||
getMetaMaskAccountsOrdered,
|
||||
getMetaMaskKeyrings,
|
||||
getOriginOfCurrentTab,
|
||||
getSelectedAddress,
|
||||
} from '../../../selectors/selectors'
|
||||
|
||||
import AccountMenu from './account-menu.component'
|
||||
|
||||
/**
|
||||
* The min amount of accounts to show search field
|
||||
*/
|
||||
const SHOW_SEARCH_ACCOUNTS_MIN_COUNT = 5
|
||||
|
||||
function mapStateToProps (state) {
|
||||
const { metamask: { selectedAddress, isAccountMenuOpen, keyrings, identities } } = state
|
||||
const { metamask: { isAccountMenuOpen } } = state
|
||||
|
||||
return {
|
||||
selectedAddress,
|
||||
isAccountMenuOpen,
|
||||
keyrings,
|
||||
identities,
|
||||
accounts: getMetaMaskAccounts(state),
|
||||
addressConnectedDomainMap: getAddressConnectedDomainMap(state),
|
||||
originOfCurrentTab: getOriginOfCurrentTab(state),
|
||||
selectedAddress: getSelectedAddress(state),
|
||||
keyrings: getMetaMaskKeyrings(state),
|
||||
accounts: getMetaMaskAccountsOrdered(state),
|
||||
}
|
||||
}
|
||||
|
||||
@ -65,5 +70,6 @@ function mapDispatchToProps (dispatch) {
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
connect(mapStateToProps, mapDispatchToProps)
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
withProps(({ accounts }) => ({ shouldShowAccountsSearch: accounts.length >= SHOW_SEARCH_ACCOUNTS_MIN_COUNT}))
|
||||
)(AccountMenu)
|
||||
|
@ -51,22 +51,26 @@
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
&__accounts {
|
||||
&__accounts-container {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
overflow-y: auto;
|
||||
max-height: 256px;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
z-index: 200;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
max-height: 256px;
|
||||
|
||||
@media screen and (max-width: 575px) {
|
||||
max-height: 228px;
|
||||
}
|
||||
}
|
||||
|
||||
&__accounts {
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.keyring-label {
|
||||
margin-top: 5px;
|
||||
@ -77,6 +81,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__no-accounts {
|
||||
font-size: 0.8em;
|
||||
padding: 16px 14px;
|
||||
}
|
||||
|
||||
&__account {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { connect } from 'react-redux'
|
||||
import TransactionBreakdown from './transaction-breakdown.component'
|
||||
import {getIsMainnet, getNativeCurrency, preferencesSelector} from '../../../selectors/selectors'
|
||||
import { getIsMainnet, getNativeCurrency, preferencesSelector } from '../../../selectors/selectors'
|
||||
import { getHexGasTotal } from '../../../helpers/utils/confirm-tx.util'
|
||||
import { sumHexes } from '../../../helpers/utils/transactions.util'
|
||||
|
||||
|
1
ui/app/components/ui/search-icon/index.js
Normal file
1
ui/app/components/ui/search-icon/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './search-icon.component'
|
12
ui/app/components/ui/search-icon/search-icon.component.js
Normal file
12
ui/app/components/ui/search-icon/search-icon.component.js
Normal file
@ -0,0 +1,12 @@
|
||||
import React from 'react'
|
||||
|
||||
export default function SearchIcon () {
|
||||
return (
|
||||
<svg height="20" width="20" xmlns="http://www.w3.org/2000/svg" fill="currentColor">
|
||||
<g clipRule="evenodd" fillRule="evenodd">
|
||||
<path d="M9.167 3.333a5.833 5.833 0 100 11.667 5.833 5.833 0 000-11.667zm-7.5 5.834a7.5 7.5 0 1115 0 7.5 7.5 0 01-15 0z" />
|
||||
<path d="M13.286 13.286a.833.833 0 011.178 0l3.625 3.625a.833.833 0 11-1.178 1.178l-3.625-3.625a.833.833 0 010-1.178z" />
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
@ -28,6 +28,24 @@ const styles = {
|
||||
},
|
||||
},
|
||||
materialError: {},
|
||||
materialWhitePaddedRoot: {
|
||||
color: '#aeaeae',
|
||||
},
|
||||
materialWhitePaddedInput: {
|
||||
padding: '8px',
|
||||
|
||||
'&::placeholder': {
|
||||
color: '#aeaeae',
|
||||
},
|
||||
},
|
||||
materialWhitePaddedFocused: {
|
||||
color: '#fff',
|
||||
},
|
||||
materialWhitePaddedUnderline: {
|
||||
'&:after': {
|
||||
borderBottom: '2px solid #fff',
|
||||
},
|
||||
},
|
||||
// Non-material styles
|
||||
formLabel: {
|
||||
'&$formLabelFocused': {
|
||||
@ -66,35 +84,99 @@ const styles = {
|
||||
},
|
||||
}
|
||||
|
||||
const TextField = props => {
|
||||
const { error, classes, material, startAdornment, largeLabel, dir, ...textFieldProps } = props
|
||||
const getMaterialThemeInputProps = ({
|
||||
dir,
|
||||
classes: { materialLabel, materialFocused, materialError, materialUnderline },
|
||||
startAdornment,
|
||||
}) => ({
|
||||
InputLabelProps: {
|
||||
FormLabelClasses: {
|
||||
root: materialLabel,
|
||||
focused: materialFocused,
|
||||
error: materialError,
|
||||
},
|
||||
},
|
||||
InputProps: {
|
||||
startAdornment,
|
||||
classes: {
|
||||
underline: materialUnderline,
|
||||
},
|
||||
inputProps: {
|
||||
dir,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const getMaterialWhitePaddedThemeInputProps = ({
|
||||
dir,
|
||||
classes: { materialWhitePaddedRoot, materialWhitePaddedFocused, materialWhitePaddedInput, materialWhitePaddedUnderline },
|
||||
startAdornment,
|
||||
}) => ({
|
||||
InputProps: {
|
||||
startAdornment,
|
||||
classes: {
|
||||
root: materialWhitePaddedRoot,
|
||||
focused: materialWhitePaddedFocused,
|
||||
input: materialWhitePaddedInput,
|
||||
underline: materialWhitePaddedUnderline,
|
||||
},
|
||||
inputProps: {
|
||||
dir,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const getBorderedThemeInputProps = ({
|
||||
dir,
|
||||
classes: { formLabel, formLabelFocused, materialError, largeInputLabel, inputLabel, inputRoot, input, inputFocused },
|
||||
largeLabel,
|
||||
startAdornment,
|
||||
}) => ({
|
||||
InputLabelProps: {
|
||||
shrink: true,
|
||||
className: largeLabel ? largeInputLabel : inputLabel,
|
||||
FormLabelClasses: {
|
||||
root: formLabel,
|
||||
focused: formLabelFocused,
|
||||
error: materialError,
|
||||
},
|
||||
},
|
||||
InputProps: {
|
||||
startAdornment,
|
||||
disableUnderline: true,
|
||||
classes: {
|
||||
root: inputRoot,
|
||||
input: input,
|
||||
focused: inputFocused,
|
||||
},
|
||||
inputProps: {
|
||||
dir,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const themeToInputProps = {
|
||||
'material': getMaterialThemeInputProps,
|
||||
'bordered': getBorderedThemeInputProps,
|
||||
'material-white-padded': getMaterialWhitePaddedThemeInputProps,
|
||||
}
|
||||
|
||||
const TextField = ({
|
||||
error,
|
||||
classes,
|
||||
theme,
|
||||
startAdornment,
|
||||
largeLabel,
|
||||
dir,
|
||||
...textFieldProps
|
||||
}) => {
|
||||
const inputProps = themeToInputProps[theme]({ classes, startAdornment, largeLabel, dir })
|
||||
|
||||
return (
|
||||
<MaterialTextField
|
||||
error={Boolean(error)}
|
||||
helperText={error}
|
||||
InputLabelProps={{
|
||||
shrink: material ? undefined : true,
|
||||
className: material ? '' : (largeLabel ? classes.largeInputLabel : classes.inputLabel),
|
||||
FormLabelClasses: {
|
||||
root: material ? classes.materialLabel : classes.formLabel,
|
||||
focused: material ? classes.materialFocused : classes.formLabelFocused,
|
||||
error: classes.materialError,
|
||||
},
|
||||
}}
|
||||
InputProps={{
|
||||
startAdornment: startAdornment || undefined,
|
||||
disableUnderline: !material,
|
||||
classes: {
|
||||
root: material ? '' : classes.inputRoot,
|
||||
input: material ? '' : classes.input,
|
||||
underline: material ? classes.materialUnderline : '',
|
||||
focused: material ? '' : classes.inputFocused,
|
||||
},
|
||||
inputProps: {
|
||||
dir,
|
||||
},
|
||||
}}
|
||||
{...inputProps}
|
||||
{...textFieldProps}
|
||||
/>
|
||||
)
|
||||
@ -103,13 +185,14 @@ const TextField = props => {
|
||||
TextField.defaultProps = {
|
||||
error: null,
|
||||
dir: 'auto',
|
||||
theme: 'bordered',
|
||||
}
|
||||
|
||||
TextField.propTypes = {
|
||||
error: PropTypes.string,
|
||||
classes: PropTypes.object,
|
||||
dir: PropTypes.string,
|
||||
material: PropTypes.bool,
|
||||
theme: PropTypes.oneOf(['bordered', 'material', 'material-white-padded']),
|
||||
startAdornment: PropTypes.element,
|
||||
largeLabel: PropTypes.bool,
|
||||
}
|
||||
|
@ -33,14 +33,14 @@ storiesOf('TextField', module)
|
||||
<TextField
|
||||
label="Text"
|
||||
type="text"
|
||||
material
|
||||
theme="material"
|
||||
/>
|
||||
))
|
||||
.add('Material password', () => (
|
||||
<TextField
|
||||
label="Password"
|
||||
type="password"
|
||||
material
|
||||
theme="material"
|
||||
/>
|
||||
))
|
||||
.add('Material error', () => (
|
||||
@ -48,6 +48,6 @@ storiesOf('TextField', module)
|
||||
type="text"
|
||||
label="Name"
|
||||
error="Invalid value"
|
||||
material
|
||||
theme="material"
|
||||
/>
|
||||
))
|
||||
|
@ -36,12 +36,8 @@ export default class TokenSearch extends Component {
|
||||
error: PropTypes.string,
|
||||
}
|
||||
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
searchQuery: '',
|
||||
}
|
||||
state = {
|
||||
searchQuery: '',
|
||||
}
|
||||
|
||||
handleSearch (searchQuery) {
|
||||
|
@ -165,7 +165,7 @@ export default class UnlockPage extends Component {
|
||||
error={error}
|
||||
autoFocus
|
||||
autoComplete="current-password"
|
||||
material
|
||||
theme="material"
|
||||
fullWidth
|
||||
/>
|
||||
</form>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { NETWORK_TYPES } from '../helpers/constants/common'
|
||||
import { mapObjectValues } from '../../../app/scripts/lib/util'
|
||||
import { stripHexPrefix, addHexPrefix } from 'ethereumjs-util'
|
||||
import { createSelector } from 'reselect'
|
||||
|
||||
import abi from 'human-standard-token-abi'
|
||||
import { multiplyCurrencies } from '../helpers/utils/conversion-util'
|
||||
@ -58,6 +59,28 @@ export function getCurrentNetworkId (state) {
|
||||
return state.metamask.network
|
||||
}
|
||||
|
||||
export const getMetaMaskAccounts = createSelector(
|
||||
getMetaMaskAccountsRaw,
|
||||
getMetaMaskCachedBalances,
|
||||
(currentAccounts, cachedBalances) => Object.entries(currentAccounts).reduce((selectedAccounts, [accountID, account]) => {
|
||||
if (account.balance === null || account.balance === undefined) {
|
||||
return {
|
||||
...selectedAccounts,
|
||||
[accountID]: {
|
||||
...account,
|
||||
balance: cachedBalances && cachedBalances[accountID],
|
||||
},
|
||||
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
...selectedAccounts,
|
||||
[accountID]: account,
|
||||
}
|
||||
}
|
||||
}, {})
|
||||
)
|
||||
|
||||
export function getSelectedAddress (state) {
|
||||
const selectedAddress = state.metamask.selectedAddress || Object.keys(getMetaMaskAccounts(state))[0]
|
||||
|
||||
@ -80,25 +103,37 @@ export function getNumberOfTokens (state) {
|
||||
return tokens ? tokens.length : 0
|
||||
}
|
||||
|
||||
export function getMetaMaskAccounts (state) {
|
||||
const currentAccounts = state.metamask.accounts
|
||||
const cachedBalances = state.metamask.cachedBalances[state.metamask.network]
|
||||
const selectedAccounts = {}
|
||||
|
||||
Object.keys(currentAccounts).forEach(accountID => {
|
||||
const account = currentAccounts[accountID]
|
||||
if (account && account.balance === null || account.balance === undefined) {
|
||||
selectedAccounts[accountID] = {
|
||||
...account,
|
||||
balance: cachedBalances && cachedBalances[accountID],
|
||||
}
|
||||
} else {
|
||||
selectedAccounts[accountID] = account
|
||||
}
|
||||
})
|
||||
return selectedAccounts
|
||||
export function getMetaMaskKeyrings (state) {
|
||||
return state.metamask.keyrings
|
||||
}
|
||||
|
||||
export function getMetaMaskIdentities (state) {
|
||||
return state.metamask.identities
|
||||
}
|
||||
|
||||
export function getMetaMaskAccountsRaw (state) {
|
||||
return state.metamask.accounts
|
||||
}
|
||||
|
||||
export function getMetaMaskCachedBalances (state) {
|
||||
const network = getCurrentNetworkId(state)
|
||||
|
||||
return state.metamask.cachedBalances[network]
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ordered (by keyrings) accounts with identity and balance
|
||||
*/
|
||||
export const getMetaMaskAccountsOrdered = createSelector(
|
||||
getMetaMaskKeyrings,
|
||||
getMetaMaskIdentities,
|
||||
getMetaMaskAccounts,
|
||||
(keyrings, identities, accounts) => keyrings
|
||||
.reduce((list, keyring) => list.concat(keyring.accounts), [])
|
||||
.filter(address => !!identities[address])
|
||||
.map(address => ({ ...identities[address], ...accounts[address]}))
|
||||
)
|
||||
|
||||
export function isBalanceCached (state) {
|
||||
const selectedAccountBalance = state.metamask.accounts[getSelectedAddress(state)].balance
|
||||
const cachedBalance = getSelectedAccountCachedBalance(state)
|
||||
|
@ -1,8 +1,5 @@
|
||||
import assert from 'assert'
|
||||
import * as selectors from '../selectors'
|
||||
const {
|
||||
getAddressBook,
|
||||
} = selectors
|
||||
import { getAddressBook } from '../selectors.js'
|
||||
import mockState from './selectors-test-data'
|
||||
|
||||
describe('selectors', () => {
|
||||
|
Loading…
Reference in New Issue
Block a user