diff --git a/package.json b/package.json index 55cd725b..c8dffd7d 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/logic/tokens/api/fetchTokenList.js b/src/logic/tokens/api/fetchTokenList.js index 63b3b9eb..82cbb7ba 100644 --- a/src/logic/tokens/api/fetchTokenList.js +++ b/src/logic/tokens/api/fetchTokenList.js @@ -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 diff --git a/src/routes/load/container/Load.jsx b/src/routes/load/container/Load.jsx index 181d0ec8..82b825e0 100644 --- a/src/routes/load/container/Load.jsx +++ b/src/routes/load/container/Load.jsx @@ -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) diff --git a/src/routes/safe/components/Balances/SendModal/screens/SendFunds/TokenSelectField/index.jsx b/src/routes/safe/components/Balances/SendModal/screens/SendFunds/TokenSelectField/index.jsx index 873df0d0..61264c67 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/SendFunds/TokenSelectField/index.jsx +++ b/src/routes/safe/components/Balances/SendModal/screens/SendFunds/TokenSelectField/index.jsx @@ -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) => ( ) : ( @@ -54,7 +55,7 @@ const TokenSelectField = ({ tokens, classes, initialValue }: SelectFieldProps) = const [initialToken, setInitialToken] = useState('') 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 => } + renderValue={(token) => } initialValue={initialToken} displayEmpty > - {tokens.map(token => ( + {tokens.map((token) => ( {token.name} - + ))} diff --git a/src/routes/safe/components/Balances/Tokens/screens/TokenList/TokenRow.jsx b/src/routes/safe/components/Balances/Tokens/screens/TokenList/TokenRow.jsx new file mode 100644 index 00000000..9d3e3b73 --- /dev/null +++ b/src/routes/safe/components/Balances/Tokens/screens/TokenList/TokenRow.jsx @@ -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, + tokens: List, + 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 ( +
+ + + {token.name} + + + {token.address !== ETH_ADDRESS && ( + + + + )} + +
+ ) +}) + +export default withStyles(styles)(TokenRow) diff --git a/src/routes/safe/components/Balances/Tokens/screens/TokenList/index.jsx b/src/routes/safe/components/Balances/Tokens/screens/TokenList/index.jsx index 8e6dc9a2..c2ae1f9e 100644 --- a/src/routes/safe/components/Balances/Tokens/screens/TokenList/index.jsx +++ b/src/routes/safe/components/Balances/Tokens/screens/TokenList/index.jsx @@ -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 { } } + 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 { 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 { - - {!tokens.size && ( - - - - )} - {filteredTokens.map((token: Token) => { - const isActive = activeTokensAddresses.has(token.address) - - return ( - - - {token.name} - - - {token.address !== ETH_ADDRESS && ( - - - - )} - - ) - })} - + {!tokens.size && ( + + + + )} + {tokens.size > 0 && ( + + + {TokenRow} + + + )} ) } diff --git a/src/routes/safe/components/Balances/Tokens/screens/TokenList/style.js b/src/routes/safe/components/Balances/Tokens/screens/TokenList/style.js index 578e72ef..4ba04d2a 100644 --- a/src/routes/safe/components/Balances/Tokens/screens/TokenList/style.js +++ b/src/routes/safe/components/Balances/Tokens/screens/TokenList/style.js @@ -56,6 +56,8 @@ export const styles = () => ({ }, tokenIcon: { marginRight: md, + height: '28px', + width: '28px', }, progressContainer: { width: '100%', diff --git a/yarn.lock b/yarn.lock index 4bb821b4..32a4e9d8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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==