perf(search modal): refactor before more dramatic changes
This commit is contained in:
parent
219de1f471
commit
85d52b3480
@ -125,7 +125,6 @@ interface CurrencyInputPanelProps {
|
|||||||
onMax?: () => void
|
onMax?: () => void
|
||||||
showMaxButton: boolean
|
showMaxButton: boolean
|
||||||
label?: string
|
label?: string
|
||||||
urlAddedTokens?: Token[]
|
|
||||||
onTokenSelection?: (tokenAddress: string) => void
|
onTokenSelection?: (tokenAddress: string) => void
|
||||||
token?: Token | null
|
token?: Token | null
|
||||||
disableTokenSelect?: boolean
|
disableTokenSelect?: boolean
|
||||||
@ -145,7 +144,6 @@ export default function CurrencyInputPanel({
|
|||||||
onMax,
|
onMax,
|
||||||
showMaxButton,
|
showMaxButton,
|
||||||
label = 'Input',
|
label = 'Input',
|
||||||
urlAddedTokens = [], // used
|
|
||||||
onTokenSelection = null,
|
onTokenSelection = null,
|
||||||
token = null,
|
token = null,
|
||||||
disableTokenSelect = false,
|
disableTokenSelect = false,
|
||||||
@ -246,7 +244,6 @@ export default function CurrencyInputPanel({
|
|||||||
setModalOpen(false)
|
setModalOpen(false)
|
||||||
}}
|
}}
|
||||||
filterType="tokens"
|
filterType="tokens"
|
||||||
urlAddedTokens={urlAddedTokens}
|
|
||||||
onTokenSelect={onTokenSelection}
|
onTokenSelect={onTokenSelection}
|
||||||
showSendWithSwap={showSendWithSwap}
|
showSendWithSwap={showSendWithSwap}
|
||||||
hiddenToken={token?.address}
|
hiddenToken={token?.address}
|
||||||
|
24
src/components/SearchModal/TokenSortButton.tsx
Normal file
24
src/components/SearchModal/TokenSortButton.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Text } from 'rebass'
|
||||||
|
import { FilterWrapper } from './styleds'
|
||||||
|
|
||||||
|
export function TokenSortButton({
|
||||||
|
title,
|
||||||
|
toggleSortOrder,
|
||||||
|
invertSearchOrder
|
||||||
|
}: {
|
||||||
|
title: string
|
||||||
|
toggleSortOrder: () => void
|
||||||
|
invertSearchOrder: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<FilterWrapper onClick={toggleSortOrder}>
|
||||||
|
<Text fontSize={14} fontWeight={500}>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
<Text fontSize={14} fontWeight={500}>
|
||||||
|
{!invertSearchOrder ? '↓' : '↑'}
|
||||||
|
</Text>
|
||||||
|
</FilterWrapper>
|
||||||
|
)
|
||||||
|
}
|
@ -1,170 +1,66 @@
|
|||||||
import React, { useState, useRef, useMemo, useEffect, useContext } from 'react'
|
|
||||||
import '@reach/tooltip/styles.css'
|
import '@reach/tooltip/styles.css'
|
||||||
import styled, { ThemeContext } from 'styled-components'
|
import { ChainId, JSBI, Token, WETH } from '@uniswap/sdk'
|
||||||
import { JSBI, Token, WETH } from '@uniswap/sdk'
|
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { isMobile } from 'react-device-detect'
|
import { isMobile } from 'react-device-detect'
|
||||||
import { RouteComponentProps, withRouter } from 'react-router-dom'
|
|
||||||
import { COMMON_BASES } from '../../constants'
|
|
||||||
import { useAllTokenBalancesTreatingWETHasETH } from '../../state/wallet/hooks'
|
|
||||||
import { Link as StyledLink } from '../../theme/components'
|
|
||||||
|
|
||||||
import Card from '../../components/Card'
|
|
||||||
import Modal from '../Modal'
|
|
||||||
import Circle from '../../assets/images/circle.svg'
|
|
||||||
import TokenLogo from '../TokenLogo'
|
|
||||||
import DoubleTokenLogo from '../DoubleLogo'
|
|
||||||
import Column, { AutoColumn } from '../Column'
|
|
||||||
import { Text } from 'rebass'
|
|
||||||
import { CursorPointer } from '../../theme'
|
|
||||||
import { ArrowLeft } from 'react-feather'
|
import { ArrowLeft } from 'react-feather'
|
||||||
import { CloseIcon } from '../../theme/components'
|
|
||||||
import { ButtonPrimary, ButtonSecondary } from '../../components/Button'
|
|
||||||
import { Spinner, TYPE } from '../../theme'
|
|
||||||
import { RowBetween, RowFixed, AutoRow } from '../Row'
|
|
||||||
|
|
||||||
import { isAddress, escapeRegExp } from '../../utils'
|
|
||||||
import { useActiveWeb3React } from '../../hooks'
|
|
||||||
import {
|
|
||||||
useAllDummyPairs,
|
|
||||||
useFetchTokenByAddress,
|
|
||||||
useAddUserToken,
|
|
||||||
useRemoveUserAddedToken,
|
|
||||||
useUserAddedTokens
|
|
||||||
} from '../../state/user/hooks'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useToken, useAllTokens } from '../../hooks/Tokens'
|
import { RouteComponentProps, withRouter } from 'react-router-dom'
|
||||||
|
import { Text } from 'rebass'
|
||||||
|
import { ThemeContext } from 'styled-components'
|
||||||
|
import Circle from '../../assets/images/circle.svg'
|
||||||
|
import Card from '../../components/Card'
|
||||||
|
import { COMMON_BASES } from '../../constants'
|
||||||
|
import { ALL_TOKENS } from '../../constants/tokens'
|
||||||
|
import { useActiveWeb3React } from '../../hooks'
|
||||||
|
import { useAllTokens, useTokenByAddressAndAutomaticallyAdd } from '../../hooks/Tokens'
|
||||||
|
import { useAllDummyPairs, useRemoveUserAddedToken, useUserAddedTokens } from '../../state/user/hooks'
|
||||||
|
import { useAllTokenBalancesTreatingWETHasETH } from '../../state/wallet/hooks'
|
||||||
|
import { CursorPointer, TYPE } from '../../theme'
|
||||||
|
import { CloseIcon, Link as StyledLink } from '../../theme/components'
|
||||||
|
import { escapeRegExp, isAddress } from '../../utils'
|
||||||
|
import { ButtonPrimary, ButtonSecondary } from '../Button'
|
||||||
|
import Column, { AutoColumn } from '../Column'
|
||||||
|
import DoubleTokenLogo from '../DoubleLogo'
|
||||||
|
import Modal from '../Modal'
|
||||||
import QuestionHelper from '../Question'
|
import QuestionHelper from '../Question'
|
||||||
|
import { AutoRow, RowBetween, RowFixed } from '../Row'
|
||||||
|
import TokenLogo from '../TokenLogo'
|
||||||
|
import { useTokenComparator } from './sorting'
|
||||||
|
import {
|
||||||
|
BaseWrapper,
|
||||||
|
FadedSpan,
|
||||||
|
GreySpan,
|
||||||
|
Input,
|
||||||
|
ItemList,
|
||||||
|
MenuItem,
|
||||||
|
PaddedColumn,
|
||||||
|
SpinnerWrapper,
|
||||||
|
TokenModalInfo
|
||||||
|
} from './styleds'
|
||||||
|
import { TokenSortButton } from './TokenSortButton'
|
||||||
|
|
||||||
const TokenModalInfo = styled.div`
|
interface SearchModalProps extends RouteComponentProps {
|
||||||
${({ theme }) => theme.flexRowNoWrap}
|
|
||||||
align-items: center;
|
|
||||||
padding: 1rem 1rem;
|
|
||||||
margin: 0.25rem 0.5rem;
|
|
||||||
justify-content: center;
|
|
||||||
user-select: none;
|
|
||||||
min-height: 200px;
|
|
||||||
`
|
|
||||||
|
|
||||||
const ItemList = styled.div`
|
|
||||||
flex-grow: 1;
|
|
||||||
height: 254px;
|
|
||||||
overflow-y: scroll;
|
|
||||||
-webkit-overflow-scrolling: touch;
|
|
||||||
`
|
|
||||||
|
|
||||||
const FadedSpan = styled(RowFixed)`
|
|
||||||
color: ${({ theme }) => theme.primary1};
|
|
||||||
font-size: 14px;
|
|
||||||
`
|
|
||||||
|
|
||||||
const GreySpan = styled.span`
|
|
||||||
color: ${({ theme }) => theme.text3};
|
|
||||||
font-weight: 400;
|
|
||||||
`
|
|
||||||
|
|
||||||
const SpinnerWrapper = styled(Spinner)`
|
|
||||||
margin: 0 0.25rem 0 0.25rem;
|
|
||||||
color: ${({ theme }) => theme.text4};
|
|
||||||
opacity: 0.6;
|
|
||||||
`
|
|
||||||
|
|
||||||
const Input = styled.input`
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
padding: 16px;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
white-space: nowrap;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
border-radius: 20px;
|
|
||||||
color: ${({ theme }) => theme.text1};
|
|
||||||
border-style: solid;
|
|
||||||
border: 1px solid ${({ theme }) => theme.bg3};
|
|
||||||
-webkit-appearance: none;
|
|
||||||
|
|
||||||
font-size: 18px;
|
|
||||||
|
|
||||||
::placeholder {
|
|
||||||
color: ${({ theme }) => theme.text3};
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const FilterWrapper = styled(RowFixed)`
|
|
||||||
padding: 8px;
|
|
||||||
background-color: ${({ selected, theme }) => selected && theme.bg2};
|
|
||||||
color: ${({ selected, theme }) => (selected ? theme.text1 : theme.text2)};
|
|
||||||
border-radius: 8px;
|
|
||||||
user-select: none;
|
|
||||||
& > * {
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const PaddedColumn = styled(AutoColumn)`
|
|
||||||
padding: 20px;
|
|
||||||
padding-bottom: 12px;
|
|
||||||
`
|
|
||||||
|
|
||||||
const PaddedItem = styled(RowBetween)`
|
|
||||||
padding: 4px 20px;
|
|
||||||
height: 56px;
|
|
||||||
`
|
|
||||||
|
|
||||||
const MenuItem = styled(PaddedItem)`
|
|
||||||
cursor: ${({ disabled }) => !disabled && 'pointer'};
|
|
||||||
pointer-events: ${({ disabled }) => disabled && 'none'};
|
|
||||||
:hover {
|
|
||||||
background-color: ${({ theme, disabled }) => !disabled && theme.bg2};
|
|
||||||
}
|
|
||||||
opacity: ${({ disabled, selected }) => (disabled || selected ? 0.5 : 1)};
|
|
||||||
`
|
|
||||||
|
|
||||||
const BaseWrapper = styled(AutoRow)<{ disable?: boolean }>`
|
|
||||||
border: 1px solid ${({ theme, disable }) => (disable ? 'transparent' : theme.bg3)};
|
|
||||||
padding: 0 6px;
|
|
||||||
border-radius: 10px;
|
|
||||||
width: 120px;
|
|
||||||
|
|
||||||
:hover {
|
|
||||||
cursor: ${({ disable }) => !disable && 'pointer'};
|
|
||||||
background-color: ${({ theme, disable }) => !disable && theme.bg2};
|
|
||||||
}
|
|
||||||
|
|
||||||
background-color: ${({ theme, disable }) => disable && theme.bg3};
|
|
||||||
opacity: ${({ disable }) => disable && '0.4'};
|
|
||||||
`
|
|
||||||
|
|
||||||
// filters on results
|
|
||||||
const FILTERS = {
|
|
||||||
VOLUME: 'VOLUME',
|
|
||||||
LIQUIDITY: 'LIQUIDITY',
|
|
||||||
BALANCES: 'BALANCES'
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SearchModalProps extends RouteComponentProps<{}> {
|
|
||||||
isOpen?: boolean
|
isOpen?: boolean
|
||||||
onDismiss?: () => void
|
onDismiss?: () => void
|
||||||
filterType?: 'tokens'
|
filterType?: 'tokens'
|
||||||
hiddenToken?: string
|
hiddenToken?: string
|
||||||
showSendWithSwap?: boolean
|
showSendWithSwap?: boolean
|
||||||
onTokenSelect?: (address: string) => void
|
onTokenSelect?: (address: string) => void
|
||||||
urlAddedTokens?: Token[]
|
|
||||||
otherSelectedTokenAddress?: string
|
otherSelectedTokenAddress?: string
|
||||||
otherSelectedText?: string
|
otherSelectedText?: string
|
||||||
showCommonBases?: boolean
|
showCommonBases?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isDefaultToken(tokenAddress: string, chainId?: number): boolean {
|
||||||
|
const address = isAddress(tokenAddress)
|
||||||
|
return Boolean(chainId && address && ALL_TOKENS[chainId as ChainId]?.[tokenAddress])
|
||||||
|
}
|
||||||
|
|
||||||
function SearchModal({
|
function SearchModal({
|
||||||
history,
|
history,
|
||||||
isOpen,
|
isOpen,
|
||||||
onDismiss,
|
onDismiss,
|
||||||
onTokenSelect,
|
onTokenSelect,
|
||||||
urlAddedTokens,
|
|
||||||
filterType,
|
filterType,
|
||||||
hiddenToken,
|
hiddenToken,
|
||||||
showSendWithSwap,
|
showSendWithSwap,
|
||||||
@ -184,16 +80,10 @@ function SearchModal({
|
|||||||
const [invertSearchOrder, setInvertSearchOrder] = useState(false)
|
const [invertSearchOrder, setInvertSearchOrder] = useState(false)
|
||||||
|
|
||||||
const userAddedTokens = useUserAddedTokens()
|
const userAddedTokens = useUserAddedTokens()
|
||||||
const fetchTokenByAddress = useFetchTokenByAddress()
|
|
||||||
const addToken = useAddUserToken()
|
|
||||||
const removeTokenByAddress = useRemoveUserAddedToken()
|
const removeTokenByAddress = useRemoveUserAddedToken()
|
||||||
|
|
||||||
// if the current input is an address, and we don't have the token in context, try to fetch it
|
// if the current input is an address, and we don't have the token in context, try to fetch it
|
||||||
const token = useToken(searchQuery)
|
const searchQueryToken = useTokenByAddressAndAutomaticallyAdd(searchQuery)
|
||||||
const [temporaryToken, setTemporaryToken] = useState<Token | null>()
|
|
||||||
|
|
||||||
// filters for ordering
|
|
||||||
const [activeFilter, setActiveFilter] = useState(FILTERS.BALANCES)
|
|
||||||
|
|
||||||
// toggle specific token import view
|
// toggle specific token import view
|
||||||
const [showTokenImport, setShowTokenImport] = useState(false)
|
const [showTokenImport, setShowTokenImport] = useState(false)
|
||||||
@ -201,22 +91,6 @@ function SearchModal({
|
|||||||
// used to help scanning on results, put token found from input on left
|
// used to help scanning on results, put token found from input on left
|
||||||
const [identifiedToken, setIdentifiedToken] = useState<Token>()
|
const [identifiedToken, setIdentifiedToken] = useState<Token>()
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const address = isAddress(searchQuery)
|
|
||||||
if (address && !token) {
|
|
||||||
let stale = false
|
|
||||||
fetchTokenByAddress(address).then(token => {
|
|
||||||
if (!stale) {
|
|
||||||
setTemporaryToken(token)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return () => {
|
|
||||||
stale = true
|
|
||||||
setTemporaryToken(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [searchQuery, token, fetchTokenByAddress])
|
|
||||||
|
|
||||||
// reset view on close
|
// reset view on close
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
@ -224,45 +98,24 @@ function SearchModal({
|
|||||||
}
|
}
|
||||||
}, [isOpen])
|
}, [isOpen])
|
||||||
|
|
||||||
const tokenList = useMemo(() => {
|
const tokenComparator = useTokenComparator(invertSearchOrder)
|
||||||
return Object.keys(allTokens)
|
|
||||||
.sort((tokenAddressA, tokenAddressB): number => {
|
|
||||||
// -1 = a is first
|
|
||||||
// 1 = b is first
|
|
||||||
|
|
||||||
// sort ETH first
|
const sortedTokenList = useMemo(() => {
|
||||||
const a = allTokens[tokenAddressA]
|
return Object.values(allTokens)
|
||||||
const b = allTokens[tokenAddressB]
|
.sort(tokenComparator)
|
||||||
if (a.equals(WETH[chainId])) return -1
|
.map(token => {
|
||||||
if (b.equals(WETH[chainId])) return 1
|
|
||||||
|
|
||||||
// sort by balances
|
|
||||||
const balanceA = allBalances[account]?.[tokenAddressA]
|
|
||||||
const balanceB = allBalances[account]?.[tokenAddressB]
|
|
||||||
if (balanceA?.greaterThan('0') && !balanceB?.greaterThan('0')) return !invertSearchOrder ? -1 : 1
|
|
||||||
if (!balanceA?.greaterThan('0') && balanceB?.greaterThan('0')) return !invertSearchOrder ? 1 : -1
|
|
||||||
if (balanceA?.greaterThan('0') && balanceB?.greaterThan('0')) {
|
|
||||||
return balanceA.greaterThan(balanceB) ? (!invertSearchOrder ? -1 : 1) : !invertSearchOrder ? 1 : -1
|
|
||||||
}
|
|
||||||
|
|
||||||
// sort by symbol
|
|
||||||
return a.symbol.toLowerCase() < b.symbol.toLowerCase() ? -1 : 1
|
|
||||||
})
|
|
||||||
.map(tokenAddress => {
|
|
||||||
const token = allTokens[tokenAddress]
|
|
||||||
return {
|
return {
|
||||||
name: token.name,
|
name: token.name,
|
||||||
symbol: token.symbol,
|
symbol: token.symbol,
|
||||||
address: isAddress(tokenAddress) as string,
|
address: token.address,
|
||||||
balance: allBalances?.[account]?.[tokenAddress]
|
balance: allBalances[account]?.[token.address]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [allTokens, chainId, allBalances, account, invertSearchOrder])
|
}, [allTokens, tokenComparator, allBalances, account])
|
||||||
|
|
||||||
const filteredTokenList = useMemo(() => {
|
const filteredTokenList = useMemo(() => {
|
||||||
return tokenList.filter(tokenEntry => {
|
return sortedTokenList.filter(tokenEntry => {
|
||||||
const urlAdded = urlAddedTokens?.some(token => token.address === tokenEntry.address)
|
const customAdded = !isDefaultToken(tokenEntry.address, chainId)
|
||||||
const customAdded = userAddedTokens?.some(token => token.address === tokenEntry.address) && !urlAdded
|
|
||||||
|
|
||||||
// if token import page dont show preset list, else show all
|
// if token import page dont show preset list, else show all
|
||||||
const include = !showTokenImport || (showTokenImport && customAdded && searchQuery !== '')
|
const include = !showTokenImport || (showTokenImport && customAdded && searchQuery !== '')
|
||||||
@ -285,7 +138,7 @@ function SearchModal({
|
|||||||
})
|
})
|
||||||
return regexMatches.some(m => m)
|
return regexMatches.some(m => m)
|
||||||
})
|
})
|
||||||
}, [tokenList, urlAddedTokens, userAddedTokens, showTokenImport, searchQuery])
|
}, [sortedTokenList, chainId, showTokenImport, searchQuery])
|
||||||
|
|
||||||
function _onTokenSelect(address) {
|
function _onTokenSelect(address) {
|
||||||
setSearchQuery('')
|
setSearchQuery('')
|
||||||
@ -313,18 +166,14 @@ function SearchModal({
|
|||||||
// try to find an exact match by address
|
// try to find an exact match by address
|
||||||
if (searchQueryIsAddress) {
|
if (searchQueryIsAddress) {
|
||||||
const identifiedTokenByAddress = Object.values(allTokens).filter(token => {
|
const identifiedTokenByAddress = Object.values(allTokens).filter(token => {
|
||||||
if (searchQueryIsAddress && token.address === isAddress(searchQuery)) {
|
return searchQueryIsAddress && token.address === isAddress(searchQuery)
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
})
|
||||||
if (identifiedTokenByAddress.length > 0) setIdentifiedToken(identifiedTokenByAddress[0])
|
if (identifiedTokenByAddress.length > 0) setIdentifiedToken(identifiedTokenByAddress[0])
|
||||||
}
|
}
|
||||||
// try to find an exact match by symbol
|
// try to find an exact match by symbol
|
||||||
else {
|
else {
|
||||||
const identifiedTokenBySymbol = Object.values(allTokens).filter(token => {
|
const identifiedTokenBySymbol = Object.values(allTokens).filter(token => {
|
||||||
if (token.symbol.slice(0, searchQuery.length).toLowerCase() === searchQuery.toLowerCase()) return true
|
return token.symbol.slice(0, searchQuery.length).toLowerCase() === searchQuery.toLowerCase()
|
||||||
return false
|
|
||||||
})
|
})
|
||||||
if (identifiedTokenBySymbol.length > 0) setIdentifiedToken(identifiedTokenBySymbol[0])
|
if (identifiedTokenBySymbol.length > 0) setIdentifiedToken(identifiedTokenBySymbol[0])
|
||||||
}
|
}
|
||||||
@ -423,25 +272,22 @@ function SearchModal({
|
|||||||
function renderTokenList() {
|
function renderTokenList() {
|
||||||
if (filteredTokenList.length === 0) {
|
if (filteredTokenList.length === 0) {
|
||||||
if (isAddress(searchQuery)) {
|
if (isAddress(searchQuery)) {
|
||||||
if (temporaryToken === undefined) {
|
if (!searchQueryToken) {
|
||||||
return <TokenModalInfo>Searching for Token...</TokenModalInfo>
|
return <TokenModalInfo>Searching...</TokenModalInfo>
|
||||||
} else if (temporaryToken === null) {
|
|
||||||
return <TokenModalInfo>Address is not a valid ERC-20 token.</TokenModalInfo>
|
|
||||||
} else {
|
} else {
|
||||||
// a user found a token by search that isn't yet added to localstorage
|
// a user found a token by search that isn't yet added to localstorage
|
||||||
return (
|
return (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
key={temporaryToken.address}
|
key={searchQueryToken.address}
|
||||||
className={`temporary-token-${temporaryToken}`}
|
className={`temporary-token-${searchQueryToken.address}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
addToken(temporaryToken)
|
_onTokenSelect(searchQueryToken.address)
|
||||||
_onTokenSelect(temporaryToken.address)
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<RowFixed>
|
<RowFixed>
|
||||||
<TokenLogo address={temporaryToken.address} size={'24px'} style={{ marginRight: '14px' }} />
|
<TokenLogo address={searchQueryToken.address} size={'24px'} style={{ marginRight: '14px' }} />
|
||||||
<Column>
|
<Column>
|
||||||
<Text fontWeight={500}>{temporaryToken.symbol}</Text>
|
<Text fontWeight={500}>{searchQueryToken.symbol}</Text>
|
||||||
<FadedSpan>(Found by search)</FadedSpan>
|
<FadedSpan>(Found by search)</FadedSpan>
|
||||||
</Column>
|
</Column>
|
||||||
</RowFixed>
|
</RowFixed>
|
||||||
@ -453,8 +299,7 @@ function SearchModal({
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return filteredTokenList.map(({ address, symbol, balance }) => {
|
return filteredTokenList.map(({ address, symbol, balance }) => {
|
||||||
const urlAdded = urlAddedTokens?.some(token => token.address === address)
|
const customAdded = !isDefaultToken(address, chainId)
|
||||||
const customAdded = userAddedTokens?.some(token => token.address === address) && !urlAdded
|
|
||||||
|
|
||||||
const zeroBalance = balance && JSBI.equal(JSBI.BigInt(0), balance.raw)
|
const zeroBalance = balance && JSBI.equal(JSBI.BigInt(0), balance.raw)
|
||||||
|
|
||||||
@ -475,10 +320,7 @@ function SearchModal({
|
|||||||
{otherSelectedTokenAddress === address && <GreySpan> ({otherSelectedText})</GreySpan>}
|
{otherSelectedTokenAddress === address && <GreySpan> ({otherSelectedText})</GreySpan>}
|
||||||
</Text>
|
</Text>
|
||||||
<FadedSpan>
|
<FadedSpan>
|
||||||
<TYPE.main fontWeight={500}>
|
<TYPE.main fontWeight={500}>{customAdded && 'Added by user'}</TYPE.main>
|
||||||
{urlAdded && 'Added by URL'}
|
|
||||||
{customAdded && 'Added by user'}
|
|
||||||
</TYPE.main>
|
|
||||||
{customAdded && (
|
{customAdded && (
|
||||||
<div
|
<div
|
||||||
onClick={event => {
|
onClick={event => {
|
||||||
@ -522,27 +364,6 @@ function SearchModal({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const Filter = ({ title, filter, filterType }: { title: string; filter: string; filterType: string }) => {
|
|
||||||
return (
|
|
||||||
<FilterWrapper
|
|
||||||
onClick={() => {
|
|
||||||
setActiveFilter(filter)
|
|
||||||
setInvertSearchOrder(invertSearchOrder => !invertSearchOrder)
|
|
||||||
}}
|
|
||||||
selected={filter === activeFilter}
|
|
||||||
>
|
|
||||||
<Text fontSize={14} fontWeight={500}>
|
|
||||||
{title}
|
|
||||||
</Text>
|
|
||||||
{filter === activeFilter && filterType === 'tokens' && (
|
|
||||||
<Text fontSize={14} fontWeight={500}>
|
|
||||||
{!invertSearchOrder ? '↓' : '↑'}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</FilterWrapper>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
@ -578,7 +399,7 @@ function SearchModal({
|
|||||||
<PaddedColumn gap="20px">
|
<PaddedColumn gap="20px">
|
||||||
<RowBetween>
|
<RowBetween>
|
||||||
<Text fontWeight={500} fontSize={16}>
|
<Text fontWeight={500} fontSize={16}>
|
||||||
{filterType === 'tokens' ? 'Select A Token' : 'Select A Pool'}
|
{filterType === 'tokens' ? 'Select a token' : 'Select a pool'}
|
||||||
</Text>
|
</Text>
|
||||||
<CloseIcon onClick={onDismiss} />
|
<CloseIcon onClick={onDismiss} />
|
||||||
</RowBetween>
|
</RowBetween>
|
||||||
@ -621,11 +442,13 @@ function SearchModal({
|
|||||||
<Text fontSize={14} fontWeight={500}>
|
<Text fontSize={14} fontWeight={500}>
|
||||||
{filterType === 'tokens' ? 'Token Name' : 'Pool Name'}
|
{filterType === 'tokens' ? 'Token Name' : 'Pool Name'}
|
||||||
</Text>
|
</Text>
|
||||||
<Filter
|
{filterType === 'tokens' && (
|
||||||
title={filterType === 'tokens' ? 'Your Balances' : ' '}
|
<TokenSortButton
|
||||||
filter={FILTERS.BALANCES}
|
invertSearchOrder={invertSearchOrder}
|
||||||
filterType={filterType}
|
toggleSortOrder={() => setInvertSearchOrder(iso => !iso)}
|
||||||
/>
|
title={filterType === 'tokens' ? 'Your Balances' : ' '}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</RowBetween>
|
</RowBetween>
|
||||||
</PaddedColumn>
|
</PaddedColumn>
|
||||||
)}
|
)}
|
||||||
|
41
src/components/SearchModal/sorting.ts
Normal file
41
src/components/SearchModal/sorting.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { Token, TokenAmount, WETH } from '@uniswap/sdk'
|
||||||
|
import { useMemo } from 'react'
|
||||||
|
import { useActiveWeb3React } from '../../hooks'
|
||||||
|
import { useAllTokenBalancesTreatingWETHasETH } from '../../state/wallet/hooks'
|
||||||
|
|
||||||
|
function getTokenComparator(
|
||||||
|
weth: Token | undefined,
|
||||||
|
balances: { [tokenAddress: string]: TokenAmount },
|
||||||
|
invertSearchOrder: boolean
|
||||||
|
): (tokenA: Token, tokenB: Token) => number {
|
||||||
|
return function sortTokens(tokenA: Token, tokenB: Token): number {
|
||||||
|
// -1 = a is first
|
||||||
|
// 1 = b is first
|
||||||
|
|
||||||
|
// sort ETH first
|
||||||
|
if (weth) {
|
||||||
|
if (tokenA.equals(weth)) return -1
|
||||||
|
if (tokenB.equals(weth)) return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort by balances
|
||||||
|
const balanceA = balances[tokenA.address]
|
||||||
|
const balanceB = balances[tokenB.address]
|
||||||
|
|
||||||
|
if (balanceA?.greaterThan('0') && !balanceB?.greaterThan('0')) return !invertSearchOrder ? -1 : 1
|
||||||
|
if (!balanceA?.greaterThan('0') && balanceB?.greaterThan('0')) return !invertSearchOrder ? 1 : -1
|
||||||
|
if (balanceA?.greaterThan('0') && balanceB?.greaterThan('0')) {
|
||||||
|
return balanceA.greaterThan(balanceB) ? (!invertSearchOrder ? -1 : 1) : !invertSearchOrder ? 1 : -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort by symbol
|
||||||
|
return tokenA.symbol.toLowerCase() < tokenB.symbol.toLowerCase() ? -1 : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTokenComparator(inverted: boolean): (tokenA: Token, tokenB: Token) => number {
|
||||||
|
const { account, chainId } = useActiveWeb3React()
|
||||||
|
const weth = WETH[chainId]
|
||||||
|
const balances = useAllTokenBalancesTreatingWETHasETH()
|
||||||
|
return useMemo(() => getTokenComparator(weth, balances[account] ?? {}, inverted), [account, balances, inverted, weth])
|
||||||
|
}
|
108
src/components/SearchModal/styleds.tsx
Normal file
108
src/components/SearchModal/styleds.tsx
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import styled from 'styled-components'
|
||||||
|
import { Spinner } from '../../theme'
|
||||||
|
import { AutoColumn } from '../Column'
|
||||||
|
import { AutoRow, RowBetween, RowFixed } from '../Row'
|
||||||
|
|
||||||
|
export const TokenModalInfo = styled.div`
|
||||||
|
${({ theme }) => theme.flexRowNoWrap}
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem 1rem;
|
||||||
|
margin: 0.25rem 0.5rem;
|
||||||
|
justify-content: center;
|
||||||
|
user-select: none;
|
||||||
|
min-height: 200px;
|
||||||
|
`
|
||||||
|
|
||||||
|
export const ItemList = styled.div`
|
||||||
|
flex-grow: 1;
|
||||||
|
height: 254px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
`
|
||||||
|
|
||||||
|
export const FadedSpan = styled(RowFixed)`
|
||||||
|
color: ${({ theme }) => theme.primary1};
|
||||||
|
font-size: 14px;
|
||||||
|
`
|
||||||
|
|
||||||
|
export const GreySpan = styled.span`
|
||||||
|
color: ${({ theme }) => theme.text3};
|
||||||
|
font-weight: 400;
|
||||||
|
`
|
||||||
|
|
||||||
|
export const SpinnerWrapper = styled(Spinner)`
|
||||||
|
margin: 0 0.25rem 0 0.25rem;
|
||||||
|
color: ${({ theme }) => theme.text4};
|
||||||
|
opacity: 0.6;
|
||||||
|
`
|
||||||
|
|
||||||
|
export const Input = styled.input`
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
padding: 16px;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
white-space: nowrap;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
border-radius: 20px;
|
||||||
|
color: ${({ theme }) => theme.text1};
|
||||||
|
border-style: solid;
|
||||||
|
border: 1px solid ${({ theme }) => theme.bg3};
|
||||||
|
-webkit-appearance: none;
|
||||||
|
|
||||||
|
font-size: 18px;
|
||||||
|
|
||||||
|
::placeholder {
|
||||||
|
color: ${({ theme }) => theme.text3};
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const FilterWrapper = styled(RowFixed)`
|
||||||
|
padding: 8px;
|
||||||
|
background-color: ${({ selected, theme }) => selected && theme.bg2};
|
||||||
|
color: ${({ selected, theme }) => (selected ? theme.text1 : theme.text2)};
|
||||||
|
border-radius: 8px;
|
||||||
|
user-select: none;
|
||||||
|
& > * {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const PaddedColumn = styled(AutoColumn)`
|
||||||
|
padding: 20px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const PaddedItem = styled(RowBetween)`
|
||||||
|
padding: 4px 20px;
|
||||||
|
height: 56px;
|
||||||
|
`
|
||||||
|
|
||||||
|
export const MenuItem = styled(PaddedItem)`
|
||||||
|
cursor: ${({ disabled }) => !disabled && 'pointer'};
|
||||||
|
pointer-events: ${({ disabled }) => disabled && 'none'};
|
||||||
|
:hover {
|
||||||
|
background-color: ${({ theme, disabled }) => !disabled && theme.bg2};
|
||||||
|
}
|
||||||
|
opacity: ${({ disabled, selected }) => (disabled || selected ? 0.5 : 1)};
|
||||||
|
`
|
||||||
|
|
||||||
|
export const BaseWrapper = styled(AutoRow)<{ disable?: boolean }>`
|
||||||
|
border: 1px solid ${({ theme, disable }) => (disable ? 'transparent' : theme.bg3)};
|
||||||
|
padding: 0 6px;
|
||||||
|
border-radius: 10px;
|
||||||
|
width: 120px;
|
||||||
|
|
||||||
|
:hover {
|
||||||
|
cursor: ${({ disable }) => !disable && 'pointer'};
|
||||||
|
background-color: ${({ theme, disable }) => !disable && theme.bg2};
|
||||||
|
}
|
||||||
|
|
||||||
|
background-color: ${({ theme, disable }) => disable && theme.bg3};
|
||||||
|
opacity: ${({ disable }) => disable && '0.4'};
|
||||||
|
`
|
Loading…
Reference in New Issue
Block a user