diff --git a/src/components/NetworkAlert/NetworkAlert.tsx b/src/components/NetworkAlert/NetworkAlert.tsx index 13d993f86..65348e9cc 100644 --- a/src/components/NetworkAlert/NetworkAlert.tsx +++ b/src/components/NetworkAlert/NetworkAlert.tsx @@ -48,7 +48,7 @@ export const OptimismWrapperBackgroundLightMode = css` background: radial-gradient(92% 105% at 50% 7%, rgba(255, 58, 212, 0.04) 0%, rgba(255, 255, 255, 0.03) 100%), radial-gradient(100% 97% at 0% 12%, rgba(235, 0, 255, 0.1) 0%, rgba(243, 19, 19, 0.1) 100%), hsla(0, 0%, 100%, 0.5); ` -const RootWrapper = styled.div<{ chainId: number; darkMode: boolean; logoUrl: string }>` +const RootWrapper = styled.div<{ chainId: SupportedChainId; darkMode: boolean; logoUrl: string }>` ${({ chainId, darkMode }) => [SupportedChainId.OPTIMISM, SupportedChainId.OPTIMISTIC_KOVAN].includes(chainId) ? darkMode diff --git a/src/components/SearchModal/ManageLists.tsx b/src/components/SearchModal/ManageLists.tsx index eed9e2c0a..f9e6f4e22 100644 --- a/src/components/SearchModal/ManageLists.tsx +++ b/src/components/SearchModal/ManageLists.tsx @@ -1,36 +1,34 @@ -import { memo, useCallback, useMemo, useRef, useState, useEffect } from 'react' -import { Settings, CheckCircle } from 'react-feather' +import { t, Trans } from '@lingui/macro' +import { TokenList } from '@uniswap/token-lists' +import Card from 'components/Card' +import { UNSUPPORTED_LIST_URLS } from 'constants/lists' +import { useListColor } from 'hooks/useColor' +import { useActiveWeb3React } from 'hooks/web3' +import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { CheckCircle, Settings } from 'react-feather' import ReactGA from 'react-ga' -import { useAppDispatch, useAppSelector } from 'state/hooks' import { usePopper } from 'react-popper' +import { useAppDispatch, useAppSelector } from 'state/hooks' import styled from 'styled-components/macro' import { useFetchListCallback } from '../../hooks/useFetchListCallback' import { useOnClickOutside } from '../../hooks/useOnClickOutside' -import { TokenList } from '@uniswap/token-lists' -import { t, Trans } from '@lingui/macro' - +import useTheme from '../../hooks/useTheme' import useToggle from '../../hooks/useToggle' -import { acceptListUpdate, removeList, disableList, enableList } from '../../state/lists/actions' -import { useIsListActive, useAllLists, useActiveListUrls } from '../../state/lists/hooks' -import { ExternalLink, LinkStyledButton, TYPE, IconWrapper } from '../../theme' +import { acceptListUpdate, disableList, enableList, removeList } from '../../state/lists/actions' +import { useActiveListUrls, useAllLists, useIsListActive } from '../../state/lists/hooks' +import { ExternalLink, IconWrapper, LinkStyledButton, TYPE } from '../../theme' import listVersionLabel from '../../utils/listVersionLabel' import { parseENSAddress } from '../../utils/parseENSAddress' import uriToHttp from '../../utils/uriToHttp' import { ButtonEmpty, ButtonPrimary } from '../Button' - import Column, { AutoColumn } from '../Column' import ListLogo from '../ListLogo' -import Row, { RowFixed, RowBetween } from '../Row' -import { PaddedColumn, SearchInput, Separator, SeparatorDark } from './styleds' -import { useListColor } from 'hooks/useColor' -import useTheme from '../../hooks/useTheme' +import Row, { RowBetween, RowFixed } from '../Row' import ListToggle from '../Toggle/ListToggle' -import Card from 'components/Card' import { CurrencyModalView } from './CurrencySearchModal' -import { UNSUPPORTED_LIST_URLS } from 'constants/lists' +import { PaddedColumn, SearchInput, Separator, SeparatorDark } from './styleds' const Wrapper = styled(Column)` - width: 100%; height: 100%; ` @@ -80,8 +78,9 @@ const StyledListUrlText = styled(TYPE.main)<{ active: boolean }>` color: ${({ theme, active }) => (active ? theme.white : theme.text2)}; ` -const RowWrapper = styled(Row)<{ bgColor: string; active: boolean }>` +const RowWrapper = styled(Row)<{ bgColor: string; active: boolean; hasActiveTokens: boolean }>` background-color: ${({ bgColor, active, theme }) => (active ? bgColor ?? 'transparent' : theme.bg2)}; + opacity: ${({ hasActiveTokens }) => (hasActiveTokens ? 1 : 0.4)}; transition: 200ms; align-items: center; padding: 1rem; @@ -93,10 +92,18 @@ function listUrlRowHTMLId(listUrl: string) { } const ListRow = memo(function ListRow({ listUrl }: { listUrl: string }) { + const { chainId } = useActiveWeb3React() const listsByUrl = useAppSelector((state) => state.lists.byUrl) const dispatch = useAppDispatch() const { current: list, pendingUpdate: pending } = listsByUrl[listUrl] + const activeTokensOnThisChain = useMemo(() => { + if (!list || !chainId) { + return 0 + } + return list.tokens.reduce((acc, cur) => (cur.chainId === chainId ? acc + 1 : acc), 0) + }, [chainId, list]) + const theme = useTheme() const listColor = useListColor(list?.logoURI) const isActive = useIsListActive(listUrl) @@ -130,7 +137,7 @@ const ListRow = memo(function ListRow({ listUrl }: { listUrl: string }) { action: 'Start Remove List', label: listUrl, }) - if (window.prompt(`Please confirm you would like to remove this list by typing REMOVE`) === `REMOVE`) { + if (window.prompt(t`Please confirm you would like to remove this list by typing REMOVE`) === `REMOVE`) { ReactGA.event({ category: 'Lists', action: 'Confirm Remove List', @@ -161,7 +168,13 @@ const ListRow = memo(function ListRow({ listUrl }: { listUrl: string }) { if (!list) return null return ( - + 0} + bgColor={listColor} + key={listUrl} + id={listUrlRowHTMLId(listUrl)} + > {list.logoURI ? ( ) : ( @@ -173,7 +186,7 @@ const ListRow = memo(function ListRow({ listUrl }: { listUrl: string }) { - {list.tokens.length} tokens + {activeTokensOnThisChain} tokens @@ -226,20 +239,29 @@ export function ManageLists({ setImportList: (list: TokenList) => void setListUrl: (url: string) => void }) { + const { chainId } = useActiveWeb3React() const theme = useTheme() const [listUrlInput, setListUrlInput] = useState('') const lists = useAllLists() + const tokenCountByListName = useMemo>( + () => + Object.values(lists).reduce((acc, { current: list }) => { + if (!list) { + return acc + } + return { + ...acc, + [list.name]: list.tokens.reduce((count: number, token) => (token.chainId === chainId ? count + 1 : count), 0), + } + }, {}), + [chainId, lists] + ) + // sort by active but only if not visible const activeListUrls = useActiveListUrls() - const [activeCopy, setActiveCopy] = useState() - useEffect(() => { - if (!activeCopy && activeListUrls) { - setActiveCopy(activeListUrls) - } - }, [activeCopy, activeListUrls]) const handleInput = useCallback((e) => { setListUrlInput(e.target.value) @@ -258,30 +280,36 @@ export function ManageLists({ // only show loaded lists, hide unsupported lists return Boolean(lists[listUrl].current) && !Boolean(UNSUPPORTED_LIST_URLS.includes(listUrl)) }) - .sort((u1, u2) => { - const { current: l1 } = lists[u1] - const { current: l2 } = lists[u2] + .sort((listUrlA, listUrlB) => { + const { current: listA } = lists[listUrlA] + const { current: listB } = lists[listUrlB] // first filter on active lists - if (activeCopy?.includes(u1) && !activeCopy?.includes(u2)) { + if (activeListUrls?.includes(listUrlA) && !activeListUrls?.includes(listUrlB)) { return -1 } - if (!activeCopy?.includes(u1) && activeCopy?.includes(u2)) { + if (!activeListUrls?.includes(listUrlA) && activeListUrls?.includes(listUrlB)) { return 1 } - if (l1 && l2) { - return l1.name.toLowerCase() < l2.name.toLowerCase() + if (listA && listB) { + if (tokenCountByListName[listA.name] > tokenCountByListName[listB.name]) { + return -1 + } + if (tokenCountByListName[listA.name] < tokenCountByListName[listB.name]) { + return 1 + } + return listA.name.toLowerCase() < listB.name.toLowerCase() ? -1 - : l1.name.toLowerCase() === l2.name.toLowerCase() + : listA.name.toLowerCase() === listB.name.toLowerCase() ? 0 : 1 } - if (l1) return -1 - if (l2) return 1 + if (listA) return -1 + if (listB) return 1 return 0 }) - }, [lists, activeCopy]) + }, [lists, activeListUrls, tokenCountByListName]) // temporary fetched list for import flow const [tempList, setTempList] = useState() diff --git a/src/constants/lists.ts b/src/constants/lists.ts index 96ca28c44..b25eb5dc1 100644 --- a/src/constants/lists.ts +++ b/src/constants/lists.ts @@ -19,7 +19,6 @@ export const UNSUPPORTED_LIST_URLS: string[] = [BA_LIST] export const DEFAULT_LIST_OF_LISTS: string[] = [ COMPOUND_LIST, AAVE_LIST, - OPTIMISM_LIST, CMC_ALL_LIST, CMC_STABLECOIN, UMA_LIST, @@ -28,6 +27,7 @@ export const DEFAULT_LIST_OF_LISTS: string[] = [ ROLL_LIST, COINGECKO_LIST, KLEROS_LIST, + OPTIMISM_LIST, GEMINI_LIST, ...UNSUPPORTED_LIST_URLS, // need to load unsupported tokens as well ] diff --git a/src/constants/routing.ts b/src/constants/routing.ts index a30be8dee..f5e992abf 100644 --- a/src/constants/routing.ts +++ b/src/constants/routing.ts @@ -112,7 +112,12 @@ export const COMMON_BASES: ChainCurrencyList = { ExtendedEther.onChain(SupportedChainId.ARBITRUM_ONE), WETH9_EXTENDED[SupportedChainId.ARBITRUM_ONE], ], + [SupportedChainId.ARBITRUM_RINKEBY]: [ + ExtendedEther.onChain(SupportedChainId.ARBITRUM_RINKEBY), + WETH9_EXTENDED[SupportedChainId.ARBITRUM_RINKEBY], + ], [SupportedChainId.OPTIMISM]: [ExtendedEther.onChain(SupportedChainId.OPTIMISM)], + [SupportedChainId.OPTIMISTIC_KOVAN]: [ExtendedEther.onChain(SupportedChainId.OPTIMISTIC_KOVAN)], } // used to construct the list of all pairs we consider by default in the frontend diff --git a/src/state/lists/hooks.ts b/src/state/lists/hooks.ts index 919e681ab..5207db503 100644 --- a/src/state/lists/hooks.ts +++ b/src/state/lists/hooks.ts @@ -46,14 +46,15 @@ export function useAllLists(): AppState['lists']['byUrl'] { return useAppSelector((state) => state.lists.byUrl) } -function combineMaps(map1: TokenAddressMap, map2: TokenAddressMap): TokenAddressMap { - return { - [1]: { ...map1[1], ...map2[1] }, - [4]: { ...map1[4], ...map2[4] }, - [3]: { ...map1[3], ...map2[3] }, - [42]: { ...map1[42], ...map2[42] }, - [5]: { ...map1[5], ...map2[5] }, - } +export function combineMaps(map1: TokenAddressMap, map2: TokenAddressMap): TokenAddressMap { + const chainIds = Object.keys({ ...map1, ...map2 }).map((id) => parseInt(id)) + return chainIds.reduce( + (acc, chainId) => ({ + ...acc, + [chainId]: { ...map2[chainId], ...map1[chainId] }, + }), + {} + ) } // merge tokens contained within lists from urls diff --git a/src/state/lists/updater.ts b/src/state/lists/updater.ts index 7d4d9ba8c..88e9de832 100644 --- a/src/state/lists/updater.ts +++ b/src/state/lists/updater.ts @@ -1,18 +1,18 @@ -import { useAllLists } from 'state/lists/hooks' import { getVersionUpgrade, minVersionBump, VersionUpgrade } from '@uniswap/token-lists' +import { SupportedChainId } from 'constants/chains' +import { OPTIMISM_LIST, UNSUPPORTED_LIST_URLS } from 'constants/lists' import { useCallback, useEffect } from 'react' - -import { useActiveWeb3React } from '../../hooks/web3' +import { useAppDispatch } from 'state/hooks' +import { useAllLists } from 'state/lists/hooks' import { useFetchListCallback } from '../../hooks/useFetchListCallback' import useInterval from '../../hooks/useInterval' import useIsWindowVisible from '../../hooks/useIsWindowVisible' -import { acceptListUpdate } from './actions' +import { useActiveWeb3React } from '../../hooks/web3' +import { acceptListUpdate, enableList } from './actions' import { useActiveListUrls } from './hooks' -import { UNSUPPORTED_LIST_URLS } from 'constants/lists' -import { useAppDispatch } from 'state/hooks' export default function Updater(): null { - const { library } = useActiveWeb3React() + const { chainId, library } = useActiveWeb3React() const dispatch = useAppDispatch() const isWindowVisible = useIsWindowVisible() @@ -28,6 +28,11 @@ export default function Updater(): null { ) }, [fetchList, isWindowVisible, lists]) + useEffect(() => { + if (chainId && [SupportedChainId.OPTIMISM, SupportedChainId.OPTIMISTIC_KOVAN].includes(chainId)) { + dispatch(enableList(OPTIMISM_LIST)) + } + }, [chainId, dispatch]) // fetch all lists every 10 minutes, but only after we initialize library useInterval(fetchAllListsCallback, library ? 1000 * 60 * 10 : null)