Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63af1a160d | ||
|
|
85d52b3480 | ||
|
|
219de1f471 | ||
|
|
f110fa7732 | ||
|
|
513a1b0c4b | ||
|
|
96c9eede18 | ||
|
|
f4a97501e5 | ||
|
|
2ff5ce62db | ||
|
|
79176dfe79 |
17
.github/workflows/release.yaml
vendored
17
.github/workflows/release.yaml
vendored
@@ -1,12 +1,8 @@
|
||||
name: Daily Release
|
||||
name: Release
|
||||
# every morning
|
||||
#on:
|
||||
# schedule:
|
||||
# - cron: '0 12 * * *'
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- v2
|
||||
schedule:
|
||||
- cron: '0 12 * * *'
|
||||
jobs:
|
||||
create-release:
|
||||
name: Create Release
|
||||
@@ -48,6 +44,10 @@ jobs:
|
||||
pinata-api-key: ${{ secrets.PINATA_API_KEY }}
|
||||
pinata-secret-api-key: ${{ secrets.PINATA_API_SECRET_KEY }}
|
||||
|
||||
- name: Update DNS with new IPFS hash
|
||||
id: update_dns
|
||||
run: npx vercel --token ${{ secrets.VERCEL_TOKEN }} --scope uniswap dns add uniswap.org _dnslink.app TXT "dnslink=/ipfs/${{ steps.upload.outputs.hash }}"
|
||||
|
||||
- name: Create GitHub Release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1.1.0
|
||||
@@ -56,7 +56,6 @@ jobs:
|
||||
with:
|
||||
tag_name: ${{ steps.bump_version.outputs.new_tag }}
|
||||
release_name: Release ${{ steps.bump_version.outputs.new_tag }}
|
||||
draft: true
|
||||
body: |
|
||||
Release built from commit [`${{ github.sha }}`](https://github.com/Uniswap/uniswap-frontend/tree/${{ github.sha }})
|
||||
|
||||
@@ -68,6 +67,4 @@ jobs:
|
||||
- https://ipfs.io/ipfs/${{ steps.upload.outputs.hash }}/
|
||||
- https://dweb.link/ipfs/${{ steps.upload.outputs.hash }}/
|
||||
|
||||
Changes since the last release below.
|
||||
|
||||
${{ steps.bump_version.outputs.changelog }}
|
||||
26
README.md
26
README.md
@@ -3,6 +3,7 @@
|
||||
[](https://app.netlify.com/sites/uniswap/deploys)
|
||||
[](https://github.com/Uniswap/uniswap-frontend/actions?query=workflow%3ATests)
|
||||
[](https://prettier.io/)
|
||||
[](https://github.com/Uniswap/uniswap-frontend/actions?query=workflow%3ARelease)
|
||||
|
||||
An open source interface for Uniswap -- a protocol for decentralized exchange of Ethereum tokens.
|
||||
|
||||
@@ -13,19 +14,15 @@ An open source interface for Uniswap -- a protocol for decentralized exchange of
|
||||
- Email: [contact@uniswap.org](mailto:contact@uniswap.org)
|
||||
- Discord: [Uniswap](https://discord.gg/Y7TF6QA)
|
||||
- Whitepaper: [Link](https://hackmd.io/C-DvwDSfSxuh-Gd4WKE_ig)
|
||||
|
||||
## Run Uniswap Locally
|
||||
|
||||
1. Download and unzip the `build.zip` file from the latest release in the [Releases tab](https://github.com/Uniswap/uniswap-frontend/releases/latest).
|
||||
2. Serve the `build/` folder locally, and access the application via a browser.
|
||||
|
||||
For more information on running a local server see
|
||||
[https://developer.mozilla.org/en-US/docs/Learn/Common_questions/set_up_a_local_testing_server](https://developer.mozilla.org/en-US/docs/Learn/Common_questions/set_up_a_local_testing_server).
|
||||
## Accessing the frontend
|
||||
|
||||
This simple approach has one downside: refreshing the page will give a `404` because of how React handles client-side routing.
|
||||
To fix this issue, consider running `serve -s` courtesy of the [serve](https://github.com/zeit/serve) package.
|
||||
|
||||
## Develop Uniswap Locally
|
||||
The front end is deployed to IPFS as well as to [uniswap.exchange](https://uniswap.exchange).
|
||||
|
||||
To access the front end via IPFS, use a link from the
|
||||
[latest release](https://github.com/Uniswap/uniswap-frontend/releases/latest).
|
||||
|
||||
## Development
|
||||
|
||||
### Install Dependencies
|
||||
|
||||
@@ -50,12 +47,7 @@ change `REACT_APP_NETWORK_ID` to `"{yourNetworkId}"`, and change `REACT_APP_NETW
|
||||
Note that the front end only works properly on testnets where both
|
||||
[Uniswap V2](https://uniswap.org/docs/v2/smart-contracts/factory/) and
|
||||
[eth-scan](https://github.com/MyCryptoHQ/eth-scan) are deployed.
|
||||
The frontend is not expected to work with local testnets.
|
||||
|
||||
### Deployment
|
||||
|
||||
As a single page application, all routes that do not match an asset must be redirect to `/index.html`.
|
||||
See [create-react-app documentation.](https://create-react-app.dev/docs/deployment#notes-on-client-side-routing).
|
||||
The frontend will not work on other networks.
|
||||
|
||||
## Contributions
|
||||
|
||||
|
||||
56
package.json
56
package.json
@@ -3,20 +3,34 @@
|
||||
"description": "Uniswap Interface",
|
||||
"homepage": "https://uniswap.exchange",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"devDependencies": {
|
||||
"@ethersproject/address": "^5.0.0-beta.134",
|
||||
"@ethersproject/bignumber": "^5.0.0-beta.138",
|
||||
"@ethersproject/constants": "^5.0.0-beta.133",
|
||||
"@ethersproject/contracts": "^5.0.0-beta.151",
|
||||
"@ethersproject/experimental": "^5.0.0-beta.141",
|
||||
"@ethersproject/providers": "5.0.0-beta.162",
|
||||
"@ethersproject/strings": "^5.0.0-beta.136",
|
||||
"@ethersproject/units": "^5.0.0-beta.132",
|
||||
"@ethersproject/wallet": "^5.0.0-beta.141",
|
||||
"@material-ui/core": "^4.9.5",
|
||||
"@mycrypto/eth-scan": "^2.1.0",
|
||||
"@popperjs/core": "^2.4.0",
|
||||
"@reach/dialog": "^0.2.8",
|
||||
"@reach/tooltip": "^0.2.0",
|
||||
"@reduxjs/toolkit": "^1.3.5",
|
||||
"@types/jest": "^25.2.1",
|
||||
"@types/node": "^13.13.5",
|
||||
"@types/qs": "^6.9.2",
|
||||
"@types/react": "^16.9.34",
|
||||
"@types/react-dom": "^16.9.7",
|
||||
"@types/react-redux": "^7.1.8",
|
||||
"@types/react-router-dom": "^5.0.0",
|
||||
"@types/rebass": "^4.0.5",
|
||||
"@types/styled-components": "^4.2.0",
|
||||
"@types/testing-library__cypress": "^5.0.5",
|
||||
"@typescript-eslint/eslint-plugin": "^2.31.0",
|
||||
"@typescript-eslint/parser": "^2.31.0",
|
||||
"@uniswap/sdk": "^2.0.5",
|
||||
"@uniswap/v2-core": "1.0.0",
|
||||
"@uniswap/v2-periphery": "1.0.0-beta.0",
|
||||
@@ -29,12 +43,19 @@
|
||||
"@web3-react/walletlink-connector": "^6.0.9",
|
||||
"copy-to-clipboard": "^3.2.0",
|
||||
"cross-env": "^7.0.2",
|
||||
"cypress": "^4.5.0",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-prettier": "^6.11.0",
|
||||
"eslint-plugin-prettier": "^3.1.3",
|
||||
"eslint-plugin-react": "^7.19.0",
|
||||
"eslint-plugin-react-hooks": "^4.0.0",
|
||||
"history": "^4.9.0",
|
||||
"i18next": "^15.0.9",
|
||||
"i18next-browser-languagedetector": "^3.0.1",
|
||||
"i18next-xhr-backend": "^2.0.1",
|
||||
"jazzicon": "^1.5.0",
|
||||
"polished": "^3.3.2",
|
||||
"prettier": "^1.17.0",
|
||||
"qrcode.react": "^0.9.3",
|
||||
"qs": "^6.9.4",
|
||||
"react": "^16.13.1",
|
||||
@@ -51,35 +72,12 @@
|
||||
"react-use-gesture": "^6.0.14",
|
||||
"rebass": "^4.0.7",
|
||||
"redux-localstorage-simple": "^2.2.0",
|
||||
"styled-components": "^4.2.0",
|
||||
"swr": "0.1.18",
|
||||
"use-media": "^1.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ethersproject/experimental": "^5.0.0-beta.141",
|
||||
"@ethersproject/wallet": "^5.0.0-beta.141",
|
||||
"@types/jest": "^25.2.1",
|
||||
"@types/node": "^13.13.5",
|
||||
"@types/qs": "^6.9.2",
|
||||
"@types/react": "^16.9.34",
|
||||
"@types/react-dom": "^16.9.7",
|
||||
"@types/react-redux": "^7.1.8",
|
||||
"@types/react-router-dom": "^5.0.0",
|
||||
"@types/rebass": "^4.0.5",
|
||||
"@types/styled-components": "^4.2.0",
|
||||
"@types/testing-library__cypress": "^5.0.5",
|
||||
"@typescript-eslint/eslint-plugin": "^2.31.0",
|
||||
"@typescript-eslint/parser": "^2.31.0",
|
||||
"cypress": "^4.5.0",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-prettier": "^6.11.0",
|
||||
"eslint-plugin-prettier": "^3.1.3",
|
||||
"eslint-plugin-react": "^7.19.0",
|
||||
"eslint-plugin-react-hooks": "^4.0.0",
|
||||
"prettier": "^1.17.0",
|
||||
"serve": "^11.3.0",
|
||||
"start-server-and-test": "^1.11.0",
|
||||
"typescript": "^3.8.3"
|
||||
"styled-components": "^4.2.0",
|
||||
"swr": "0.1.18",
|
||||
"typescript": "^3.8.3",
|
||||
"use-media": "^1.4.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
@@ -109,4 +107,4 @@
|
||||
]
|
||||
},
|
||||
"license": "GPL-3.0-or-later"
|
||||
}
|
||||
}
|
||||
@@ -125,7 +125,6 @@ interface CurrencyInputPanelProps {
|
||||
onMax?: () => void
|
||||
showMaxButton: boolean
|
||||
label?: string
|
||||
urlAddedTokens?: Token[]
|
||||
onTokenSelection?: (tokenAddress: string) => void
|
||||
token?: Token | null
|
||||
disableTokenSelect?: boolean
|
||||
@@ -145,7 +144,6 @@ export default function CurrencyInputPanel({
|
||||
onMax,
|
||||
showMaxButton,
|
||||
label = 'Input',
|
||||
urlAddedTokens = [], // used
|
||||
onTokenSelection = null,
|
||||
token = null,
|
||||
disableTokenSelect = false,
|
||||
@@ -246,7 +244,6 @@ export default function CurrencyInputPanel({
|
||||
setModalOpen(false)
|
||||
}}
|
||||
filterType="tokens"
|
||||
urlAddedTokens={urlAddedTokens}
|
||||
onTokenSelect={onTokenSelection}
|
||||
showSendWithSwap={showSendWithSwap}
|
||||
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 styled, { ThemeContext } from 'styled-components'
|
||||
import { JSBI, Token, WETH } from '@uniswap/sdk'
|
||||
import { ChainId, JSBI, Token, WETH } from '@uniswap/sdk'
|
||||
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react'
|
||||
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 { 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 { 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 } 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 { 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`
|
||||
${({ 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<{}> {
|
||||
interface SearchModalProps extends RouteComponentProps {
|
||||
isOpen?: boolean
|
||||
onDismiss?: () => void
|
||||
filterType?: 'tokens'
|
||||
hiddenToken?: string
|
||||
showSendWithSwap?: boolean
|
||||
onTokenSelect?: (address: string) => void
|
||||
urlAddedTokens?: Token[]
|
||||
otherSelectedTokenAddress?: string
|
||||
otherSelectedText?: string
|
||||
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({
|
||||
history,
|
||||
isOpen,
|
||||
onDismiss,
|
||||
onTokenSelect,
|
||||
urlAddedTokens,
|
||||
filterType,
|
||||
hiddenToken,
|
||||
showSendWithSwap,
|
||||
@@ -183,17 +79,10 @@ function SearchModal({
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [invertSearchOrder, setInvertSearchOrder] = useState(false)
|
||||
|
||||
const userAddedTokens = useUserAddedTokens()
|
||||
const fetchTokenByAddress = useFetchTokenByAddress()
|
||||
const addToken = useAddUserToken()
|
||||
const removeTokenByAddress = useRemoveUserAddedToken()
|
||||
|
||||
// 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 [temporaryToken, setTemporaryToken] = useState<Token | null>()
|
||||
|
||||
// filters for ordering
|
||||
const [activeFilter, setActiveFilter] = useState(FILTERS.BALANCES)
|
||||
const searchQueryToken = useTokenByAddressAndAutomaticallyAdd(searchQuery)
|
||||
|
||||
// toggle specific token import view
|
||||
const [showTokenImport, setShowTokenImport] = useState(false)
|
||||
@@ -201,22 +90,6 @@ function SearchModal({
|
||||
// used to help scanning on results, put token found from input on left
|
||||
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
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
@@ -224,45 +97,24 @@ function SearchModal({
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
const tokenList = useMemo(() => {
|
||||
return Object.keys(allTokens)
|
||||
.sort((tokenAddressA, tokenAddressB): number => {
|
||||
// -1 = a is first
|
||||
// 1 = b is first
|
||||
const tokenComparator = useTokenComparator(invertSearchOrder)
|
||||
|
||||
// sort ETH first
|
||||
const a = allTokens[tokenAddressA]
|
||||
const b = allTokens[tokenAddressB]
|
||||
if (a.equals(WETH[chainId])) return -1
|
||||
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]
|
||||
const sortedTokenList = useMemo(() => {
|
||||
return Object.values(allTokens)
|
||||
.sort(tokenComparator)
|
||||
.map(token => {
|
||||
return {
|
||||
name: token.name,
|
||||
symbol: token.symbol,
|
||||
address: isAddress(tokenAddress) as string,
|
||||
balance: allBalances?.[account]?.[tokenAddress]
|
||||
address: token.address,
|
||||
balance: allBalances[account]?.[token.address]
|
||||
}
|
||||
})
|
||||
}, [allTokens, chainId, allBalances, account, invertSearchOrder])
|
||||
}, [allTokens, tokenComparator, allBalances, account])
|
||||
|
||||
const filteredTokenList = useMemo(() => {
|
||||
return tokenList.filter(tokenEntry => {
|
||||
const urlAdded = urlAddedTokens?.some(token => token.address === tokenEntry.address)
|
||||
const customAdded = userAddedTokens?.some(token => token.address === tokenEntry.address) && !urlAdded
|
||||
return sortedTokenList.filter(tokenEntry => {
|
||||
const customAdded = !isDefaultToken(tokenEntry.address, chainId)
|
||||
|
||||
// if token import page dont show preset list, else show all
|
||||
const include = !showTokenImport || (showTokenImport && customAdded && searchQuery !== '')
|
||||
@@ -285,7 +137,7 @@ function SearchModal({
|
||||
})
|
||||
return regexMatches.some(m => m)
|
||||
})
|
||||
}, [tokenList, urlAddedTokens, userAddedTokens, showTokenImport, searchQuery])
|
||||
}, [sortedTokenList, chainId, showTokenImport, searchQuery])
|
||||
|
||||
function _onTokenSelect(address) {
|
||||
setSearchQuery('')
|
||||
@@ -313,18 +165,14 @@ function SearchModal({
|
||||
// try to find an exact match by address
|
||||
if (searchQueryIsAddress) {
|
||||
const identifiedTokenByAddress = Object.values(allTokens).filter(token => {
|
||||
if (searchQueryIsAddress && token.address === isAddress(searchQuery)) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
return searchQueryIsAddress && token.address === isAddress(searchQuery)
|
||||
})
|
||||
if (identifiedTokenByAddress.length > 0) setIdentifiedToken(identifiedTokenByAddress[0])
|
||||
}
|
||||
// try to find an exact match by symbol
|
||||
else {
|
||||
const identifiedTokenBySymbol = Object.values(allTokens).filter(token => {
|
||||
if (token.symbol.slice(0, searchQuery.length).toLowerCase() === searchQuery.toLowerCase()) return true
|
||||
return false
|
||||
return token.symbol.slice(0, searchQuery.length).toLowerCase() === searchQuery.toLowerCase()
|
||||
})
|
||||
if (identifiedTokenBySymbol.length > 0) setIdentifiedToken(identifiedTokenBySymbol[0])
|
||||
}
|
||||
@@ -423,25 +271,22 @@ function SearchModal({
|
||||
function renderTokenList() {
|
||||
if (filteredTokenList.length === 0) {
|
||||
if (isAddress(searchQuery)) {
|
||||
if (temporaryToken === undefined) {
|
||||
return <TokenModalInfo>Searching for Token...</TokenModalInfo>
|
||||
} else if (temporaryToken === null) {
|
||||
return <TokenModalInfo>Address is not a valid ERC-20 token.</TokenModalInfo>
|
||||
if (!searchQueryToken) {
|
||||
return <TokenModalInfo>Searching...</TokenModalInfo>
|
||||
} else {
|
||||
// a user found a token by search that isn't yet added to localstorage
|
||||
return (
|
||||
<MenuItem
|
||||
key={temporaryToken.address}
|
||||
className={`temporary-token-${temporaryToken}`}
|
||||
key={searchQueryToken.address}
|
||||
className={`temporary-token-${searchQueryToken.address}`}
|
||||
onClick={() => {
|
||||
addToken(temporaryToken)
|
||||
_onTokenSelect(temporaryToken.address)
|
||||
_onTokenSelect(searchQueryToken.address)
|
||||
}}
|
||||
>
|
||||
<RowFixed>
|
||||
<TokenLogo address={temporaryToken.address} size={'24px'} style={{ marginRight: '14px' }} />
|
||||
<TokenLogo address={searchQueryToken.address} size={'24px'} style={{ marginRight: '14px' }} />
|
||||
<Column>
|
||||
<Text fontWeight={500}>{temporaryToken.symbol}</Text>
|
||||
<Text fontWeight={500}>{searchQueryToken.symbol}</Text>
|
||||
<FadedSpan>(Found by search)</FadedSpan>
|
||||
</Column>
|
||||
</RowFixed>
|
||||
@@ -453,8 +298,7 @@ function SearchModal({
|
||||
}
|
||||
} else {
|
||||
return filteredTokenList.map(({ address, symbol, balance }) => {
|
||||
const urlAdded = urlAddedTokens?.some(token => token.address === address)
|
||||
const customAdded = userAddedTokens?.some(token => token.address === address) && !urlAdded
|
||||
const customAdded = !isDefaultToken(address, chainId)
|
||||
|
||||
const zeroBalance = balance && JSBI.equal(JSBI.BigInt(0), balance.raw)
|
||||
|
||||
@@ -475,10 +319,7 @@ function SearchModal({
|
||||
{otherSelectedTokenAddress === address && <GreySpan> ({otherSelectedText})</GreySpan>}
|
||||
</Text>
|
||||
<FadedSpan>
|
||||
<TYPE.main fontWeight={500}>
|
||||
{urlAdded && 'Added by URL'}
|
||||
{customAdded && 'Added by user'}
|
||||
</TYPE.main>
|
||||
<TYPE.main fontWeight={500}>{customAdded && 'Added by user'}</TYPE.main>
|
||||
{customAdded && (
|
||||
<div
|
||||
onClick={event => {
|
||||
@@ -522,27 +363,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 (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
@@ -578,7 +398,7 @@ function SearchModal({
|
||||
<PaddedColumn gap="20px">
|
||||
<RowBetween>
|
||||
<Text fontWeight={500} fontSize={16}>
|
||||
{filterType === 'tokens' ? 'Select A Token' : 'Select A Pool'}
|
||||
{filterType === 'tokens' ? 'Select a token' : 'Select a pool'}
|
||||
</Text>
|
||||
<CloseIcon onClick={onDismiss} />
|
||||
</RowBetween>
|
||||
@@ -621,11 +441,13 @@ function SearchModal({
|
||||
<Text fontSize={14} fontWeight={500}>
|
||||
{filterType === 'tokens' ? 'Token Name' : 'Pool Name'}
|
||||
</Text>
|
||||
<Filter
|
||||
title={filterType === 'tokens' ? 'Your Balances' : ' '}
|
||||
filter={FILTERS.BALANCES}
|
||||
filterType={filterType}
|
||||
/>
|
||||
{filterType === 'tokens' && (
|
||||
<TokenSortButton
|
||||
invertSearchOrder={invertSearchOrder}
|
||||
toggleSortOrder={() => setInvertSearchOrder(iso => !iso)}
|
||||
title={filterType === 'tokens' ? 'Your Balances' : ' '}
|
||||
/>
|
||||
)}
|
||||
</RowBetween>
|
||||
</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'};
|
||||
`
|
||||
@@ -80,7 +80,7 @@ export default function CreatePool({ history, location }: RouteComponentProps) {
|
||||
{token0?.symbol}{' '}
|
||||
</Text>
|
||||
<TYPE.darkGray fontWeight={500} fontSize={16} marginLeft={'8px'}>
|
||||
{token0?.address === 'ETH' && '(default)'}
|
||||
{token0?.address === WETH[chainId]?.address && '(default)'}
|
||||
</TYPE.darkGray>
|
||||
</Row>
|
||||
</ButtonDropwdownLight>
|
||||
|
||||
Reference in New Issue
Block a user