make token list a separate screen inside tokens list modal, so adding custom token may be implemented
This commit is contained in:
parent
e4453c7d0c
commit
399aa9f84b
|
@ -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
|
||||
|
|
|
@ -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,99 +22,22 @@ type Props = Actions & {
|
|||
}
|
||||
|
||||
type State = {
|
||||
filter: string,
|
||||
activeTokensAddresses: Set<string>,
|
||||
activeScreen: string,
|
||||
}
|
||||
|
||||
const filterBy = (filter: string, tokens: List<Token>): List<Token> => 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<Props, State> {
|
||||
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 (
|
||||
<React.Fragment>
|
||||
<Block className={classes.root}>
|
||||
<Row align="center" grow className={classes.heading}>
|
||||
<Paragraph className={classes.manage} noMargin>
|
||||
Manage Tokens
|
||||
|
@ -139,43 +47,15 @@ class Tokens extends React.Component<Props, State> {
|
|||
</IconButton>
|
||||
</Row>
|
||||
<Hairline />
|
||||
<Row align="center" className={classNames(classes.padding, classes.actions)}>
|
||||
<Search className={classes.search} />
|
||||
<SearchBar
|
||||
placeholder="Search by name or symbol"
|
||||
classes={searchClasses}
|
||||
searchIcon={<div />}
|
||||
onChange={this.onChangeSearchBar}
|
||||
onCancelSearch={this.onCancelSearch}
|
||||
{activeScreen === 'tokenList' && (
|
||||
<TokenList
|
||||
tokens={tokens}
|
||||
activeTokens={activeTokens}
|
||||
fetchTokens={fetchTokens}
|
||||
updateActiveTokens={updateActiveTokens}
|
||||
safeAddress={safeAddress}
|
||||
/>
|
||||
<Spacer />
|
||||
<Divider />
|
||||
<Spacer />
|
||||
<Button variant="contained" size="small" color="secondary" className={classes.add}>
|
||||
+ ADD CUSTOM TOKEN
|
||||
</Button>
|
||||
</Row>
|
||||
<Hairline />
|
||||
</Block>
|
||||
<MuiList className={classes.list}>
|
||||
{filteredTokens.map((token: Token) => {
|
||||
const isActive = activeTokensAddresses.has(token.address)
|
||||
|
||||
return (
|
||||
<ListItem key={token.address} className={classes.token}>
|
||||
<ListItemIcon>
|
||||
<Img src={token.logoUri} height={28} alt={token.name} onError={this.setImageToPlaceholder} />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={token.symbol} secondary={token.name} />
|
||||
{token.address !== ETH_ADDRESS && (
|
||||
<ListItemSecondaryAction>
|
||||
<Switch onChange={this.onSwitch(token)} checked={isActive} />
|
||||
</ListItemSecondaryAction>
|
||||
)}
|
||||
</ListItem>
|
||||
)
|
||||
})}
|
||||
</MuiList>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
// @flow
|
||||
|
||||
import React, { Component } from 'react'
|
||||
|
||||
export default class TokenLIst extends Component {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
token list
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
// @flow
|
||||
import TokenList from './TokenList'
|
||||
|
||||
export default TokenList
|
|
@ -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<Token>,
|
||||
safeAddress: string,
|
||||
activeTokens: List<Token>,
|
||||
fetchTokens: Function,
|
||||
updateActiveTokens: Function,
|
||||
}
|
||||
|
||||
type State = {
|
||||
filter: string,
|
||||
activeTokensAddresses: Set<string>,
|
||||
}
|
||||
|
||||
const filterBy = (filter: string, tokens: List<Token>): List<Token> => 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<Props, State> {
|
||||
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 (
|
||||
<React.Fragment>
|
||||
<Block className={classes.root}>
|
||||
<Row align="center" className={cn(classes.padding, classes.actions)}>
|
||||
<Search className={classes.search} />
|
||||
<SearchBar
|
||||
placeholder="Search by name or symbol"
|
||||
classes={searchClasses}
|
||||
searchIcon={<div />}
|
||||
onChange={this.onChangeSearchBar}
|
||||
onCancelSearch={this.onCancelSearch}
|
||||
/>
|
||||
<Spacer />
|
||||
<Divider />
|
||||
<Spacer />
|
||||
<Button variant="contained" size="small" color="secondary" className={classes.add}>
|
||||
+ ADD CUSTOM TOKEN
|
||||
</Button>
|
||||
</Row>
|
||||
<Hairline />
|
||||
</Block>
|
||||
<MuiList className={classes.list}>
|
||||
{filteredTokens.map((token: Token) => {
|
||||
const isActive = activeTokensAddresses.has(token.address)
|
||||
|
||||
return (
|
||||
<ListItem key={token.address} className={classes.token}>
|
||||
<ListItemIcon>
|
||||
<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} />
|
||||
</ListItemSecondaryAction>
|
||||
)}
|
||||
</ListItem>
|
||||
)
|
||||
})}
|
||||
</MuiList>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const TokenComponent = withStyles(styles)(Tokens)
|
||||
|
||||
export default TokenComponent
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
})
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
@ -0,0 +1,3 @@
|
|||
// @flow
|
||||
|
||||
export * from './setTokenImgToPlaceholder'
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue