diff --git a/src/routes/safe/component/Balances/AssetTableCell/AssetTableCell.js b/src/routes/safe/component/Balances/AssetTableCell/AssetTableCell.js index 40a31b7f..c30ec55b 100644 --- a/src/routes/safe/component/Balances/AssetTableCell/AssetTableCell.js +++ b/src/routes/safe/component/Balances/AssetTableCell/AssetTableCell.js @@ -3,7 +3,7 @@ import * as React from 'react' import Block from '~/components/layout/Block' import Img from '~/components/layout/Img' import Paragraph from '~/components/layout/Paragraph' -import TokenPlaceholder from '../Tokens/assets/token_placeholder.svg' +import TokenPlaceholder from '../assets/token_placeholder.svg' const setImageToPlaceholder = (e) => { e.target.onerror = null diff --git a/src/routes/safe/component/Balances/Tokens/index.jsx b/src/routes/safe/component/Balances/Tokens/index.jsx index 0fab6f03..89e4c855 100644 --- a/src/routes/safe/component/Balances/Tokens/index.jsx +++ b/src/routes/safe/component/Balances/Tokens/index.jsx @@ -1,31 +1,16 @@ // @flow import * as React from 'react' import { connect } from 'react-redux' -import { List, Set } from 'immutable' -import classNames from 'classnames/bind' -import SearchBar from 'material-ui-search-bar' +import { List } from 'immutable' import { withStyles } from '@material-ui/core/styles' -import MuiList from '@material-ui/core/List' -import Img from '~/components/layout/Img' -import Block from '~/components/layout/Block' -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 Close from '@material-ui/icons/Close' -import Search from '@material-ui/icons/Search' import IconButton from '@material-ui/core/IconButton' import Paragraph from '~/components/layout/Paragraph' -import Button from '~/components/layout/Button' -import Switch from '@material-ui/core/Switch' -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 TokenList from '~/routes/safe/component/Balances/Tokens/screens/tokenList' import { type Token } from '~/logic/tokens/store/model/token' import actions, { type Actions } from './actions' -import TokenPlaceholder from './assets/token_placeholder.svg' import { styles } from './style' type Props = Actions & { @@ -37,145 +22,40 @@ type Props = Actions & { } type State = { - filter: string, - activeTokensAddresses: Set, + activeScreen: string, } -const filterBy = (filter: string, tokens: List): List => tokens.filter( - (token: Token) => !filter - || token.symbol.toLowerCase().includes(filter.toLowerCase()) - || token.name.toLowerCase().includes(filter.toLowerCase()), -) - -// OPTIMIZATION IDEA (Thanks Andre) -// Calculate active tokens on component mount, store it in component state -// After user closes modal, dispatch an action so we dont have 100500 actions -// And selectors dont recalculate - class Tokens extends React.Component { state = { - activeScreen: '', - filter: '', - activeTokensAddresses: Set([]), - activeTokensCalculated: false, - } - - componentDidMount() { - const { fetchTokens, safeAddress } = this.props - - fetchTokens(safeAddress) - } - - static getDerivedStateFromProps(nextProps, prevState) { - // I moved this logic here because if placed in ComponentDidMount - // the user would see Switches switch and this method fires before the component mounts - - if (!prevState.activeTokensCalculated) { - const { activeTokens } = nextProps - - return { - activeTokensAddresses: Set(activeTokens.map(({ address }) => address)), - activeTokensCalculated: true, - } - } - return null - } - - componentWillUnmount() { - const { activeTokensAddresses } = this.state - const { updateActiveTokens, safeAddress } = this.props - - updateActiveTokens(safeAddress, activeTokensAddresses.toList()) - } - - onCancelSearch = () => { - this.setState(() => ({ filter: '' })) - } - - onChangeSearchBar = (value) => { - this.setState(() => ({ filter: value })) - } - - onSwitch = (token: Token) => () => { - const { activeTokensAddresses } = this.state - - if (activeTokensAddresses.has(token.address)) { - this.setState({ - activeTokensAddresses: activeTokensAddresses.remove(token.address), - }) - } else { - this.setState({ - activeTokensAddresses: activeTokensAddresses.add(token.address), - }) - } - } - - setImageToPlaceholder = (e) => { - e.target.onerror = null - e.target.src = TokenPlaceholder + activeScreen: 'tokenList', } render() { - const { onClose, classes, tokens } = this.props - const { filter, activeTokensAddresses } = this.state - const searchClasses = { - input: classes.searchInput, - root: classes.searchRoot, - iconButton: classes.searchIcon, - searchContainer: classes.searchContainer, - } - - const filteredTokens = filterBy(filter, tokens) + const { + onClose, classes, tokens, activeTokens, fetchTokens, updateActiveTokens, safeAddress, + } = this.props + const { activeScreen } = this.state return ( - - - - Manage Tokens - - - - - - - - - } - onChange={this.onChangeSearchBar} - onCancelSearch={this.onCancelSearch} - /> - - - - - - - - - {filteredTokens.map((token: Token) => { - const isActive = activeTokensAddresses.has(token.address) - - return ( - - - {token.name} - - - {token.address !== ETH_ADDRESS && ( - - - - )} - - ) - })} - + + + Manage Tokens + + + + + + + {activeScreen === 'tokenList' && ( + + )} ) } diff --git a/src/routes/safe/component/Balances/Tokens/screens/tokenList/TokenList.js b/src/routes/safe/component/Balances/Tokens/screens/tokenList/TokenList.js deleted file mode 100644 index dc33db22..00000000 --- a/src/routes/safe/component/Balances/Tokens/screens/tokenList/TokenList.js +++ /dev/null @@ -1,13 +0,0 @@ -// @flow - -import React, { Component } from 'react' - -export default class TokenLIst extends Component { - render() { - return ( -
- token list -
- ) - } -} diff --git a/src/routes/safe/component/Balances/Tokens/screens/tokenList/index.js b/src/routes/safe/component/Balances/Tokens/screens/tokenList/index.js deleted file mode 100644 index 7aa87181..00000000 --- a/src/routes/safe/component/Balances/Tokens/screens/tokenList/index.js +++ /dev/null @@ -1,4 +0,0 @@ -// @flow -import TokenList from './TokenList' - -export default TokenList diff --git a/src/routes/safe/component/Balances/Tokens/screens/tokenList/index.jsx b/src/routes/safe/component/Balances/Tokens/screens/tokenList/index.jsx new file mode 100644 index 00000000..21c7956f --- /dev/null +++ b/src/routes/safe/component/Balances/Tokens/screens/tokenList/index.jsx @@ -0,0 +1,167 @@ +// @flow +import * as React from 'react' +import { List, Set } from 'immutable' +import cn from 'classnames' +import SearchBar from 'material-ui-search-bar' +import { withStyles } from '@material-ui/core/styles' +import MuiList from '@material-ui/core/List' +import Img from '~/components/layout/Img' +import Block from '~/components/layout/Block' +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 Search from '@material-ui/icons/Search' +import Button from '~/components/layout/Button' +import Switch from '@material-ui/core/Switch' +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/component/Balances/utils' +import { styles } from './style' + +type Props = { + classes: Object, + tokens: List, + safeAddress: string, + activeTokens: List, + fetchTokens: Function, + updateActiveTokens: Function, +} + +type State = { + filter: string, + activeTokensAddresses: Set, +} + +const filterBy = (filter: string, tokens: List): List => tokens.filter( + (token: Token) => !filter + || token.symbol.toLowerCase().includes(filter.toLowerCase()) + || token.name.toLowerCase().includes(filter.toLowerCase()), +) + +// OPTIMIZATION IDEA (Thanks Andre) +// Calculate active tokens on component mount, store it in component state +// After user closes modal, dispatch an action so we dont have 100500 actions +// And selectors dont recalculate + +class Tokens extends React.Component { + state = { + filter: '', + activeTokensAddresses: Set([]), + activeTokensCalculated: false, + } + + componentDidMount() { + const { fetchTokens } = this.props + + fetchTokens() + } + + static getDerivedStateFromProps(nextProps, prevState) { + // I moved this logic here because if placed in ComponentDidMount + // the user would see Switches switch and this method fires before the component mounts + + if (!prevState.activeTokensCalculated) { + const { activeTokens } = nextProps + + return { + activeTokensAddresses: Set(activeTokens.map(({ address }) => address)), + activeTokensCalculated: true, + } + } + return null + } + + componentWillUnmount() { + const { activeTokensAddresses } = this.state + const { updateActiveTokens, safeAddress } = this.props + + updateActiveTokens(safeAddress, activeTokensAddresses.toList()) + } + + onCancelSearch = () => { + this.setState(() => ({ filter: '' })) + } + + onChangeSearchBar = (value) => { + this.setState(() => ({ filter: value })) + } + + onSwitch = (token: Token) => () => { + const { activeTokensAddresses } = this.state + + if (activeTokensAddresses.has(token.address)) { + this.setState({ + activeTokensAddresses: activeTokensAddresses.remove(token.address), + }) + } else { + this.setState({ + activeTokensAddresses: activeTokensAddresses.add(token.address), + }) + } + } + + render() { + const { classes, tokens } = this.props + const { filter, activeTokensAddresses } = this.state + const searchClasses = { + input: classes.searchInput, + root: classes.searchRoot, + iconButton: classes.searchIcon, + searchContainer: classes.searchContainer, + } + + const filteredTokens = filterBy(filter, tokens) + + return ( + + + + + } + onChange={this.onChangeSearchBar} + onCancelSearch={this.onCancelSearch} + /> + + + + + + + + + {filteredTokens.map((token: Token) => { + const isActive = activeTokensAddresses.has(token.address) + + return ( + + + {token.name} + + + {token.address !== ETH_ADDRESS && ( + + + + )} + + ) + })} + + + ) + } +} + +const TokenComponent = withStyles(styles)(Tokens) + +export default TokenComponent diff --git a/src/routes/safe/component/Balances/Tokens/screens/tokenList/style.js b/src/routes/safe/component/Balances/Tokens/screens/tokenList/style.js new file mode 100644 index 00000000..0089a081 --- /dev/null +++ b/src/routes/safe/component/Balances/Tokens/screens/tokenList/style.js @@ -0,0 +1,63 @@ +// @flow +import { + md, sm, xs, mediumFontSize, border, +} from '~/theme/variables' + +export const styles = () => ({ + root: { + minHeight: '48px', + }, + search: { + color: '#a2a8ba', + paddingLeft: sm, + }, + padding: { + padding: `0 ${md}`, + }, + add: { + fontWeight: 'normal', + paddingRight: md, + paddingLeft: md, + }, + list: { + overflow: 'hidden', + overflowY: 'scroll', + padding: 0, + height: '100%', + }, + token: { + minHeight: '50px', + borderBottom: `1px solid ${border}`, + }, + searchInput: { + backgroundColor: 'transparent', + lineHeight: 'initial', + fontSize: mediumFontSize, + padding: 0, + '& > input::placeholder': { + letterSpacing: '-0.5px', + fontSize: mediumFontSize, + color: 'black', + }, + '& > input': { + letterSpacing: '-0.5px', + }, + }, + searchContainer: { + width: '180px', + marginLeft: xs, + marginRight: xs, + }, + searchRoot: { + letterSpacing: '-0.5px', + fontFamily: 'Roboto Mono, monospace', + fontSize: mediumFontSize, + border: 'none', + boxShadow: 'none', + }, + searchIcon: { + '&:hover': { + backgroundColor: 'transparent !important', + }, + }, +}) diff --git a/src/routes/safe/component/Balances/Tokens/style.js b/src/routes/safe/component/Balances/Tokens/style.js index 6945334d..18980b13 100644 --- a/src/routes/safe/component/Balances/Tokens/style.js +++ b/src/routes/safe/component/Balances/Tokens/style.js @@ -1,76 +1,18 @@ // @flow -import { - lg, md, sm, xs, mediumFontSize, border, -} from '~/theme/variables' +import { lg, sm } from '~/theme/variables' export const styles = () => ({ - root: { - minHeight: '127px', - }, heading: { padding: `${sm} ${lg}`, justifyContent: 'space-between', + maxHeight: '75px', + boxSizing: 'border-box', }, manage: { fontSize: '24px', }, - actions: { - height: '50px', - }, close: { height: '35px', width: '35px', }, - search: { - color: '#a2a8ba', - paddingLeft: sm, - }, - padding: { - padding: `0 ${md}`, - }, - add: { - fontWeight: 'normal', - paddingRight: md, - paddingLeft: md, - }, - list: { - overflow: 'hidden', - overflowY: 'scroll', - padding: 0, - }, - token: { - minHeight: '50px', - borderBottom: `1px solid ${border}`, - }, - searchInput: { - backgroundColor: 'transparent', - lineHeight: 'initial', - fontSize: mediumFontSize, - padding: 0, - '& > input::placeholder': { - letterSpacing: '-0.5px', - fontSize: mediumFontSize, - color: 'black', - }, - '& > input': { - letterSpacing: '-0.5px', - }, - }, - searchContainer: { - width: '180px', - marginLeft: xs, - marginRight: xs, - }, - searchRoot: { - letterSpacing: '-0.5px', - fontFamily: 'Roboto Mono, monospace', - fontSize: mediumFontSize, - border: 'none', - boxShadow: 'none', - }, - searchIcon: { - '&:hover': { - backgroundColor: 'transparent !important', - }, - }, }) diff --git a/src/routes/safe/component/Balances/Tokens/assets/token_placeholder.svg b/src/routes/safe/component/Balances/assets/token_placeholder.svg similarity index 100% rename from src/routes/safe/component/Balances/Tokens/assets/token_placeholder.svg rename to src/routes/safe/component/Balances/assets/token_placeholder.svg diff --git a/src/routes/safe/component/Balances/utils/index.js b/src/routes/safe/component/Balances/utils/index.js new file mode 100644 index 00000000..daa91a8b --- /dev/null +++ b/src/routes/safe/component/Balances/utils/index.js @@ -0,0 +1,3 @@ +// @flow + +export * from './setTokenImgToPlaceholder' diff --git a/src/routes/safe/component/Balances/utils/setTokenImgToPlaceholder.js b/src/routes/safe/component/Balances/utils/setTokenImgToPlaceholder.js new file mode 100644 index 00000000..086c1609 --- /dev/null +++ b/src/routes/safe/component/Balances/utils/setTokenImgToPlaceholder.js @@ -0,0 +1,7 @@ +// @flow +import TokenPlaceholder from '~/routes/safe/component/Balances/assets/token_placeholder.svg' + +export const setImageToPlaceholder = (e) => { + e.target.onerror = null + e.target.src = TokenPlaceholder +}