1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00

Refactor token list (#8233)

* Refactor token list into standard container/component modules

The token list has been moved into its own directory and split into
separate container and component modules. Additional updates have been
made to simplify the component logic as well.

* Update token-list to use new React Context API
This commit is contained in:
Mark Stacey 2020-03-25 21:43:25 -03:00 committed by GitHub
parent ade737e2a8
commit 966c38cd99
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 192 additions and 190 deletions

View File

@ -1,190 +0,0 @@
import PropTypes from 'prop-types'
import React, { Component } from 'react'
import TokenCell from './token-cell'
import TokenTracker from 'eth-token-tracker'
import { connect } from 'react-redux'
import { getSelectedAddress } from '../../selectors/selectors'
import log from 'loglevel'
function mapStateToProps (state) {
return {
network: state.metamask.network,
tokens: state.metamask.tokens,
userAddress: getSelectedAddress(state),
assetImages: state.metamask.assetImages,
}
}
const defaultTokens = []
import contracts from 'eth-contract-metadata'
for (const address in contracts) {
const contract = contracts[address]
if (contract.erc20) {
contract.address = address
defaultTokens.push(contract)
}
}
class TokenList extends Component {
static contextTypes = {
t: PropTypes.func,
}
static propTypes = {
tokens: PropTypes.array.isRequired,
userAddress: PropTypes.string.isRequired,
network: PropTypes.string.isRequired,
assetImages: PropTypes.object.isRequired,
onTokenClick: PropTypes.func.isRequired,
}
state = {
tokens: [],
isLoading: true,
}
createFreshTokenTracker () {
if (this.tracker) {
// Clean up old trackers when refreshing:
this.tracker.stop()
this.tracker.removeListener('update', this.balanceUpdater)
this.tracker.removeListener('error', this.showError)
}
if (!global.ethereumProvider) {
return
}
const { userAddress } = this.props
this.tracker = new TokenTracker({
userAddress,
provider: global.ethereumProvider,
tokens: this.props.tokens,
pollingInterval: 8000,
})
// Set up listener instances for cleaning up
this.balanceUpdater = this.updateBalances.bind(this)
this.showError = (error) => {
this.setState({ error, isLoading: false })
}
this.tracker.on('update', this.balanceUpdater)
this.tracker.on('error', this.showError)
this.tracker.updateBalances()
}
updateBalances = function (tokens) {
this.setState({ tokens, isLoading: false })
}
componentDidMount () {
this.createFreshTokenTracker()
}
componentDidUpdate (prevProps) {
const {
network: oldNet,
userAddress: oldAddress,
tokens,
} = prevProps
const {
network: newNet,
userAddress: newAddress,
tokens: newTokens,
} = this.props
const isLoading = newNet === 'loading'
const missingInfo = !oldNet || !newNet || !oldAddress || !newAddress
const sameUserAndNetwork = oldAddress === newAddress && oldNet === newNet
const shouldUpdateTokens = isLoading || missingInfo || sameUserAndNetwork
const oldTokensLength = tokens ? tokens.length : 0
const tokensLengthUnchanged = oldTokensLength === newTokens.length
if (tokensLengthUnchanged && shouldUpdateTokens) {
return
}
this.setState({ isLoading: true })
this.createFreshTokenTracker()
}
componentWillUnmount () {
if (!this.tracker) {
return
}
this.tracker.stop()
this.tracker.removeListener('update', this.balanceUpdater)
this.tracker.removeListener('error', this.showError)
}
render () {
const { userAddress, assetImages, onTokenClick } = this.props
const { tokens, isLoading, error } = this.state
if (isLoading) {
return (
<div
style={{
display: 'flex',
height: '250px',
alignItems: 'center',
justifyContent: 'center',
padding: '30px',
}}
>
{this.context.t('loadingTokens')}
</div>
)
}
if (error) {
log.error(error)
return (
<div
className="hotFix"
style={{
padding: '80px',
}}
>
{this.context.t('troubleTokenBalances')}
<span
className="hotFix"
style={{
color: 'rgba(247, 134, 28, 1)',
cursor: 'pointer',
}}
onClick={() => {
global.platform.openWindow({
url: `https://ethplorer.io/address/${userAddress}`,
})
}}
>
{this.context.t('here')}
</span>
</div>
)
}
return (
<div>
{tokens.map((tokenData, index) => {
tokenData.image = assetImages[tokenData.address]
return (
<TokenCell key={index} {...tokenData} onClick={onTokenClick} />
)
})}
</div>
)
}
}
const TokenListContainer = connect(mapStateToProps)(TokenList)
TokenListContainer.propTypes = {
onTokenClick: PropTypes.func.isRequired,
}
export default TokenListContainer

View File

@ -0,0 +1 @@
export { default } from './token-list.container'

View File

@ -0,0 +1,170 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import TokenTracker from 'eth-token-tracker'
import { isEqual } from 'lodash'
import contracts from 'eth-contract-metadata'
import { I18nContext } from '../../../contexts/i18n'
import TokenCell from '../token-cell'
const defaultTokens = []
for (const address in contracts) {
const contract = contracts[address]
if (contract.erc20) {
contract.address = address
defaultTokens.push(contract)
}
}
class TokenList extends Component {
static contextType = I18nContext
static propTypes = {
assetImages: PropTypes.object.isRequired,
network: PropTypes.string.isRequired,
onTokenClick: PropTypes.func.isRequired,
tokens: PropTypes.array.isRequired,
userAddress: PropTypes.string.isRequired,
}
constructor () {
super()
this.state = {
error: null,
tokensLoading: false,
tokensWithBalances: [],
}
}
constructTokenTracker () {
const { network, tokens, userAddress } = this.props
if (!tokens || !tokens.length) {
this.setState({
tokensLoading: false,
tokensWithBalances: [],
})
return
}
this.setState({ tokensLoading: true })
if (!userAddress || network === 'loading' || !global.ethereumProvider) {
return
}
const updateBalances = (tokensWithBalances) => {
this.setState({
error: null,
tokensLoading: false,
tokensWithBalances,
})
}
const showError = (error) => {
this.setState({
error,
tokensLoading: false,
})
}
this.tokenTracker = new TokenTracker({
userAddress,
provider: global.ethereumProvider,
tokens: tokens,
pollingInterval: 8000,
})
this.tokenTracker.on('update', updateBalances)
this.tokenTracker.on('error', showError)
this.tokenTracker.updateBalances()
}
stopTokenTracker () {
if (this.tokenTracker) {
this.tokenTracker.stop()
this.tokenTracker.removeAllListeners('update')
this.tokenTracker.removeAllListeners('error')
}
}
componentDidMount () {
this.constructTokenTracker()
}
componentDidUpdate (prevProps) {
const { network, tokens, userAddress } = this.props
if (
isEqual(tokens, prevProps.tokens) &&
userAddress === prevProps.userAddress &&
network === prevProps.network
) {
return
}
this.stopTokenTracker()
this.constructTokenTracker()
}
componentWillUnmount () {
this.stopTokenTracker()
}
render () {
const t = this.context
const { error, tokensLoading, tokensWithBalances } = this.state
const { assetImages, network, onTokenClick, userAddress } = this.props
if (network === 'loading' || tokensLoading) {
return (
<div
style={{
display: 'flex',
height: '250px',
alignItems: 'center',
justifyContent: 'center',
padding: '30px',
}}
>
{t('loadingTokens')}
</div>
)
}
if (error) {
return (
<div
className="hotFix"
style={{
padding: '80px',
}}
>
{t('troubleTokenBalances')}
<span
className="hotFix"
style={{
color: 'rgba(247, 134, 28, 1)',
cursor: 'pointer',
}}
onClick={() => {
global.platform.openWindow({
url: `https://ethplorer.io/address/${userAddress}`,
})
}}
>
{t('here')}
</span>
</div>
)
}
return (
<div>
{tokensWithBalances.map((tokenData, index) => {
tokenData.image = assetImages[tokenData.address]
return (
<TokenCell key={index} {...tokenData} onClick={onTokenClick} />
)
})}
</div>
)
}
}
export default TokenList

View File

@ -0,0 +1,21 @@
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
import { getSelectedAddress } from '../../../selectors/selectors'
import TokenList from './token-list.component'
function mapStateToProps (state) {
return {
network: state.metamask.network,
tokens: state.metamask.tokens,
userAddress: getSelectedAddress(state),
assetImages: state.metamask.assetImages,
}
}
const TokenListContainer = connect(mapStateToProps)(TokenList)
TokenListContainer.propTypes = {
onTokenClick: PropTypes.func.isRequired,
}
export default TokenListContainer