Add react-window in token list for displaying tokens

This commit is contained in:
Mikhail Mikheev 2019-10-02 18:28:33 +04:00
parent 6bbfd04910
commit 49252889b0
8 changed files with 128 additions and 53 deletions

View File

@ -59,6 +59,7 @@
"react-qr-reader": "^2.2.1",
"react-redux": "7.1.1",
"react-router-dom": "5.1.2",
"react-window": "^1.8.5",
"recompose": "^0.30.0",
"redux": "4.0.4",
"redux-actions": "^2.6.5",
@ -114,7 +115,7 @@
"eslint-plugin-import": "2.18.2",
"eslint-plugin-jest": "22.17.0",
"eslint-plugin-jsx-a11y": "6.2.3",
"eslint-plugin-react": "7.15.0",
"eslint-plugin-react": "7.14.3",
"ethereumjs-abi": "0.6.8",
"extract-text-webpack-plugin": "^4.0.0-beta.0",
"file-loader": "4.2.0",

View File

@ -6,7 +6,11 @@ const fetchTokenList = () => {
const apiUrl = getRelayUrl()
const url = `${apiUrl}/tokens`
return axios.get(url)
return axios.get(url, {
params: {
limit: 300,
},
})
}
export default fetchTokenList

View File

@ -19,7 +19,7 @@ type Props = SelectorProps & Actions
export const loadSafe = async (
safeName: string,
safeAddress: string,
owners: Array,
owners: Array<*>,
addSafe: Function,
) => {
const safeProps = await buildSafe(safeAddress, safeName)

View File

@ -12,6 +12,7 @@ import SelectField from '~/components/forms/SelectField'
import { setImageToPlaceholder } from '~/routes/safe/components/Balances/utils'
import { required } from '~/components/forms/validator'
import { type Token } from '~/logic/tokens/store/model/token'
import { formatAmount } from '~/logic/tokens/utils/formatAmount'
import { selectedTokenStyles, selectStyles } from './style'
type SelectFieldProps = {
@ -35,7 +36,7 @@ const SelectedToken = ({ token, classes }: SelectedTokenProps) => (
<ListItemText
className={classes.tokenData}
primary={token.name}
secondary={`${token.balance} ${token.symbol}`}
secondary={`${formatAmount(token.balance)} ${token.symbol}`}
/>
</>
) : (
@ -54,7 +55,7 @@ const TokenSelectField = ({ tokens, classes, initialValue }: SelectFieldProps) =
const [initialToken, setInitialToken] = useState<InitialTokenType>('')
useEffect(() => {
const selectedToken = tokens.find(token => token.name === initialValue)
const selectedToken = tokens.find((token) => token.name === initialValue)
setInitialToken(selectedToken || '')
}, [initialValue])
@ -64,16 +65,16 @@ const TokenSelectField = ({ tokens, classes, initialValue }: SelectFieldProps) =
component={SelectField}
classes={{ selectMenu: classes.selectMenu }}
validate={required}
renderValue={token => <SelectedTokenStyled token={token} />}
renderValue={(token) => <SelectedTokenStyled token={token} />}
initialValue={initialToken}
displayEmpty
>
{tokens.map(token => (
{tokens.map((token) => (
<MenuItem key={token.address} value={token}>
<ListItemIcon>
<Img src={token.logoUri} height={28} alt={token.name} onError={setImageToPlaceholder} />
</ListItemIcon>
<ListItemText primary={token.name} secondary={`${token.balance} ${token.symbol}`} />
<ListItemText primary={token.name} secondary={`${formatAmount(token.balance)} ${token.symbol}`} />
</MenuItem>
))}
</Field>

View File

@ -0,0 +1,57 @@
// @flow
import React, { memo } from 'react'
import { List, Set } from 'immutable'
import { withStyles } from '@material-ui/core/styles'
import ListItem from '@material-ui/core/ListItem'
import ListItemIcon from '@material-ui/core/ListItemIcon'
import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'
import ListItemText from '@material-ui/core/ListItemText'
import Switch from '@material-ui/core/Switch'
import Img from '~/components/layout/Img'
import { setImageToPlaceholder } from '~/routes/safe/components/Balances/utils'
import { ETH_ADDRESS } from '~/logic/tokens/utils/tokenHelpers'
import { type Token } from '~/logic/tokens/store/model/token'
import { styles } from './style'
export const TOGGLE_TOKEN_TEST_ID = 'toggle-token-btn'
type Props = {
data: {
activeTokensAddresses: Set<string>,
tokens: List<Token>,
onSwitch: Function,
},
style: Object,
index: number,
classes: Object,
}
const TokenRow = memo(({
data, index, classes, style,
}: Props) => {
const { tokens, activeTokensAddresses, onSwitch } = data
const token: Token = tokens.get(index)
const isActive = activeTokensAddresses.has(token.address)
return (
<div style={style}>
<ListItem className={classes.token} classes={{ root: classes.tokenRoot }}>
<ListItemIcon className={classes.tokenIcon}>
<Img src={token.logoUri} height={28} alt={token.name} onError={setImageToPlaceholder} />
</ListItemIcon>
<ListItemText primary={token.symbol} secondary={token.name} />
{token.address !== ETH_ADDRESS && (
<ListItemSecondaryAction>
<Switch
onChange={onSwitch(token)}
checked={isActive}
inputProps={{ 'data-testid': `${token.symbol}_${TOGGLE_TOKEN_TEST_ID}` }}
/>
</ListItemSecondaryAction>
)}
</ListItem>
</div>
)
})
export default withStyles(styles)(TokenRow)

View File

@ -2,30 +2,23 @@
import * as React from 'react'
import { List, Set } from 'immutable'
import cn from 'classnames'
import { FixedSizeList } from 'react-window'
import SearchBar from 'material-ui-search-bar'
import { withStyles } from '@material-ui/core/styles'
import MuiList from '@material-ui/core/List'
import ListItem from '@material-ui/core/ListItem'
import ListItemIcon from '@material-ui/core/ListItemIcon'
import CircularProgress from '@material-ui/core/CircularProgress'
import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'
import ListItemText from '@material-ui/core/ListItemText'
import Switch from '@material-ui/core/Switch'
import Search from '@material-ui/icons/Search'
import Img from '~/components/layout/Img'
import Block from '~/components/layout/Block'
import Button from '~/components/layout/Button'
import Divider from '~/components/layout/Divider'
import Hairline from '~/components/layout/Hairline'
import Spacer from '~/components/Spacer'
import Row from '~/components/layout/Row'
import { ETH_ADDRESS } from '~/logic/tokens/utils/tokenHelpers'
import { type Token } from '~/logic/tokens/store/model/token'
import { setImageToPlaceholder } from '~/routes/safe/components/Balances/utils'
import TokenRow from './TokenRow'
import { styles } from './style'
export const ADD_CUSTOM_TOKEN_BUTTON_TEST_ID = 'add-custom-token-btn'
export const TOGGLE_TOKEN_TEST_ID = 'toggle-token-btn'
type Props = {
classes: Object,
@ -110,6 +103,18 @@ class Tokens extends React.Component<Props, State> {
}
}
createItemData = (tokens, activeTokensAddresses) => ({
tokens,
activeTokensAddresses,
onSwitch: this.onSwitch,
})
getItemKey = (index, { tokens }) => {
const token: Token = tokens.get(index)
return token.address
}
render() {
const { classes, tokens, setActiveScreen } = this.props
const { filter, activeTokensAddresses } = this.state
@ -122,6 +127,7 @@ class Tokens extends React.Component<Props, State> {
const switchToAddCustomTokenScreen = () => setActiveScreen('addCustomToken')
const filteredTokens = filterBy(filter, tokens)
const itemData = this.createItemData(filteredTokens, activeTokensAddresses)
return (
<>
@ -152,34 +158,25 @@ class Tokens extends React.Component<Props, State> {
</Row>
<Hairline />
</Block>
<MuiList className={classes.list}>
{!tokens.size && (
<Block justify="center" className={classes.progressContainer}>
<CircularProgress />
</Block>
)}
{filteredTokens.map((token: Token) => {
const isActive = activeTokensAddresses.has(token.address)
return (
<ListItem key={token.address} className={classes.token} classes={{ root: classes.tokenRoot }}>
<ListItemIcon className={classes.tokenIcon}>
<Img src={token.logoUri} height={28} alt={token.name} onError={setImageToPlaceholder} />
</ListItemIcon>
<ListItemText primary={token.symbol} secondary={token.name} />
{token.address !== ETH_ADDRESS && (
<ListItemSecondaryAction>
<Switch
onChange={this.onSwitch(token)}
checked={isActive}
inputProps={{ 'data-testid': `${token.symbol}_${TOGGLE_TOKEN_TEST_ID}` }}
/>
</ListItemSecondaryAction>
)}
</ListItem>
)
})}
</MuiList>
{!tokens.size && (
<Block justify="center" className={classes.progressContainer}>
<CircularProgress />
</Block>
)}
{tokens.size > 0 && (
<MuiList className={classes.list}>
<FixedSizeList
height={413}
width={500}
itemCount={filteredTokens.size}
itemData={itemData}
itemSize={51}
itemKey={this.getItemKey}
>
{TokenRow}
</FixedSizeList>
</MuiList>
)}
</>
)
}

View File

@ -56,6 +56,8 @@ export const styles = () => ({
},
tokenIcon: {
marginRight: md,
height: '28px',
width: '28px',
},
progressContainer: {
width: '100%',

View File

@ -7335,20 +7335,20 @@ eslint-plugin-jsx-a11y@6.2.3:
has "^1.0.3"
jsx-ast-utils "^2.2.1"
eslint-plugin-react@7.15.0:
version "7.15.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.15.0.tgz#4808b19cf7b4c439454099d4eb8f0cf0e9fe31dd"
integrity sha512-NbIh/yVXoltm8Df28PiPRanfCZAYubGqXU391MTCpW955Vum7S0nZdQYXGAvDh9ye4aNCmOR6YcYZsfMbEQZQA==
eslint-plugin-react@7.14.3:
version "7.14.3"
resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.14.3.tgz#911030dd7e98ba49e1b2208599571846a66bdf13"
integrity sha512-EzdyyBWC4Uz2hPYBiEJrKCUi2Fn+BJ9B/pJQcjw5X+x/H2Nm59S4MJIvL4O5NEE0+WbnQwEBxWY03oUk+Bc3FA==
dependencies:
array-includes "^3.0.3"
doctrine "^2.1.0"
has "^1.0.3"
jsx-ast-utils "^2.2.1"
jsx-ast-utils "^2.1.0"
object.entries "^1.1.0"
object.fromentries "^2.0.0"
object.values "^1.1.0"
prop-types "^15.7.2"
resolve "^1.12.0"
resolve "^1.10.1"
eslint-scope@^3.7.1:
version "3.7.3"
@ -11359,7 +11359,7 @@ jss@10.0.0-alpha.25:
is-in-browser "^1.1.3"
tiny-warning "^1.0.2"
jsx-ast-utils@^2.2.1:
jsx-ast-utils@^2.1.0, jsx-ast-utils@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.2.1.tgz#4d4973ebf8b9d2837ee91a8208cc66f3a2776cfb"
integrity sha512-v3FxCcAf20DayI+uxnCuw795+oOIkVu6EnJ1+kSzhqqTZHNkTZ7B66ZgLp4oLJ/gbA64cI0B7WRoHZMSRdyVRQ==
@ -12133,6 +12133,11 @@ memdown@~3.0.0:
ltgt "~2.2.0"
safe-buffer "~5.1.1"
"memoize-one@>=3.1.1 <6":
version "5.1.1"
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0"
integrity sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA==
memoize-one@^5.0.0:
version "5.0.5"
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.0.5.tgz#8cd3809555723a07684afafcd6f756072ac75d7e"
@ -14899,6 +14904,14 @@ react-transition-group@^4.3.0:
loose-envify "^1.4.0"
prop-types "^15.6.2"
react-window@^1.8.5:
version "1.8.5"
resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.5.tgz#a56b39307e79979721021f5d06a67742ecca52d1"
integrity sha512-HeTwlNa37AFa8MDZFZOKcNEkuF2YflA0hpGPiTT9vR7OawEt+GZbfM6wqkBahD3D3pUjIabQYzsnY/BSJbgq6Q==
dependencies:
"@babel/runtime" "^7.0.0"
memoize-one ">=3.1.1 <6"
react@16.10.1:
version "16.10.1"
resolved "https://registry.yarnpkg.com/react/-/react-16.10.1.tgz#967c1e71a2767dfa699e6ba702a00483e3b0573f"
@ -15592,7 +15605,7 @@ resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.11.0, resolve@^1.3.2
dependencies:
path-parse "^1.0.6"
resolve@^1.12.0:
resolve@^1.10.1, resolve@^1.12.0:
version "1.12.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.12.0.tgz#3fc644a35c84a48554609ff26ec52b66fa577df6"
integrity sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==