import React, { memo, useCallback, useMemo, useRef, useState, useEffect, } from "react"; import { Settings, CheckCircle } from "react-feather"; import { usePopper } from "react-popper"; import { useDispatch, useSelector } from "react-redux"; import styled from "styled-components/macro"; import { useFetchListCallback } from "../../hooks/useFetchListCallback"; import { useOnClickOutside } from "../../hooks/useOnClickOutside"; import { TokenList } from "@uniswap/token-lists"; import useToggle from "../../hooks/useToggle"; import { AppDispatch, AppState } from "../../state"; import { acceptListUpdate, removeList, disableList, enableList, } from "../../state/glists/actions"; import { useIsListActive, useAllLists, useActiveListUrls, } from "../../state/glists/hooks"; import { ExternalLink, LinkStyledButton, TYPE, IconWrapper } 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 ListToggle from "../Toggle/ListToggle"; import Card from "../Card"; import { CurrencyModalView } from "./CurrencySearchModal"; import { UNSUPPORTED_LIST_URLS } from "../../constants/lists"; import { useWeb3 } from "../../web3"; const Wrapper = styled(Column)` width: 100%; height: 100%; `; const UnpaddedLinkStyledButton = styled(LinkStyledButton)` padding: 0; font-size: 1rem; opacity: ${({ disabled }) => (disabled ? "0.4" : "1")}; `; const PopoverContainer = styled.div<{ show: boolean }>` z-index: 100; visibility: ${(props) => (props.show ? "visible" : "hidden")}; opacity: ${(props) => (props.show ? 1 : 0)}; transition: visibility 150ms linear, opacity 150ms linear; background: ${({ theme }) => theme.bg2}; border: 1px solid ${({ theme }) => theme.bg3}; box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.01), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04), 0px 24px 32px rgba(0, 0, 0, 0.01); color: ${({ theme }) => theme.text2}; border-radius: 0.5rem; padding: 1rem; display: grid; grid-template-rows: 1fr; grid-gap: 8px; font-size: 1rem; text-align: left; `; const StyledMenu = styled.div` display: flex; justify-content: center; align-items: center; position: relative; border: none; `; const StyledTitleText = styled.div<{ active: boolean }>` font-size: 16px; overflow: hidden; text-overflow: ellipsis; font-weight: 600; color: ${({ theme, active }) => (active ? theme.white : theme.text2)}; `; const StyledListUrlText = styled(TYPE.main)<{ active: boolean }>` font-size: 12px; color: ${({ theme, active }) => (active ? theme.white : theme.text2)}; `; const RowWrapper = styled(Row)<{ bgColor: string; active: boolean }>` background-color: ${({ bgColor, active, theme }) => active ? bgColor ?? "transparent" : theme.bg2}; transition: 200ms; align-items: center; padding: 1rem; border-radius: 20px; `; function listUrlRowHTMLId(listUrl: string) { return `list-row-${listUrl.replace(/\./g, "-")}`; } const ListRow = memo(function ListRow({ listUrl }: { listUrl: string }) { const listsByUrl = useSelector( (state) => state.glists.byUrl ); const dispatch = useDispatch(); const { current: list, pendingUpdate: pending } = listsByUrl[listUrl]; const theme = useTheme(); const listColor = useListColor(list?.logoURI); const isActive = useIsListActive(listUrl); const [open, toggle] = useToggle(false); const node = useRef(); const [referenceElement, setReferenceElement] = useState(); const [popperElement, setPopperElement] = useState(); const { styles, attributes } = usePopper(referenceElement, popperElement, { placement: "auto", strategy: "fixed", modifiers: [{ name: "offset", options: { offset: [8, 8] } }], }); useOnClickOutside(node, open ? toggle : undefined); const handleAcceptListUpdate = useCallback(() => { if (!pending) return; dispatch(acceptListUpdate(listUrl)); }, [dispatch, listUrl, pending]); const handleRemoveList = useCallback(() => { if ( window.prompt( `Please confirm you would like to remove this list by typing REMOVE` ) === `REMOVE` ) { dispatch(removeList(listUrl)); } }, [dispatch, listUrl]); const handleEnableList = useCallback(() => { dispatch(enableList(listUrl)); }, [dispatch, listUrl]); const handleDisableList = useCallback(() => { dispatch(disableList(listUrl)); }, [dispatch, listUrl]); if (!list) return null; return ( {list.logoURI ? ( ) : (
)} {list.name} {list.tokens.length} tokens {open && (
{list && listVersionLabel(list.version)}
View list Remove list {pending && ( Update list )}
)}
{ isActive ? handleDisableList() : handleEnableList(); }} /> ); }); const ListContainer = styled.div` padding: 1rem; height: 100%; overflow: auto; padding-bottom: 80px; `; export function ManageLists({ setModalView, setImportList, setListUrl, }: { setModalView: (view: CurrencyModalView) => void; setImportList: (list: TokenList) => void; setListUrl: (url: string) => void; }) { const theme = useTheme(); const [listUrlInput, setListUrlInput] = useState(""); const lists = useAllLists(); // 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); }, []); const fetchList = useFetchListCallback(); const validUrl: boolean = useMemo(() => { return ( uriToHttp(listUrlInput).length > 0 || Boolean(parseENSAddress(listUrlInput)) ); }, [listUrlInput]); const sortedLists = useMemo(() => { const listUrls = Object.keys(lists); return listUrls .filter((listUrl) => { // 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]; // first filter on active lists if (activeCopy?.includes(u1) && !activeCopy?.includes(u2)) { return -1; } if (!activeCopy?.includes(u1) && activeCopy?.includes(u2)) { return 1; } if (l1 && l2) { return l1.name.toLowerCase() < l2.name.toLowerCase() ? -1 : l1.name.toLowerCase() === l2.name.toLowerCase() ? 0 : 1; } if (l1) return -1; if (l2) return 1; return 0; }); }, [lists, activeCopy]); // temporary fetched list for import flow const [tempList, setTempList] = useState(); const [addError, setAddError] = useState(); const { library } = useWeb3(); useEffect(() => { async function fetchTempList() { if (!library) return; fetchList(library, listUrlInput, false) .then((list) => setTempList(list)) .catch(() => setAddError("Error importing list")); } // if valid url, fetch details for card if (validUrl) { fetchTempList(); } else { setTempList(undefined); listUrlInput !== "" && setAddError("Enter valid list location"); } // reset error if (listUrlInput === "") { setAddError(undefined); } }, [fetchList, listUrlInput, validUrl, library]); // check if list is already imported const isImported = Object.keys(lists).includes(listUrlInput); // set list values and have parent modal switch to import list view const handleImport = useCallback(() => { if (!tempList) return; setImportList(tempList); setModalView(CurrencyModalView.importList); setListUrl(listUrlInput); }, [listUrlInput, setImportList, setListUrl, setModalView, tempList]); return ( {addError ? ( {addError} ) : null} {tempList && ( {tempList.logoURI && ( )} {tempList.name} {tempList.tokens.length} tokens {isImported ? ( Loaded ) : ( Import )} )} {sortedLists.map((listUrl) => ( ))} ); }