Changeset View
Changeset View
Standalone View
Standalone View
cashtab/src/components/Agora/index.tsx
| // Copyright (c) 2024 The Bitcoin developers | // Copyright (c) 2024 The Bitcoin developers | ||||
| // Distributed under the MIT software license, see the accompanying | // Distributed under the MIT software license, see the accompanying | ||||
| // file COPYING or http://www.opensource.org/licenses/mit-license.php. | // file COPYING or http://www.opensource.org/licenses/mit-license.php. | ||||
| import React, { useRef, useState, useEffect, useContext } from 'react'; | import React, { useState, useEffect, useContext, useRef } from 'react'; | ||||
| import { Link } from 'react-router'; | |||||
| import { WalletContext, isWalletContextLoaded } from 'wallet/context'; | import { WalletContext, isWalletContextLoaded } from 'wallet/context'; | ||||
| import { toHex } from 'ecash-lib'; | import { toHex } from 'ecash-lib'; | ||||
| import { Alert } from 'components/Common/Atoms'; | import { Alert } from 'components/Common/Atoms'; | ||||
| import Spinner from 'components/Common/Spinner'; | import Spinner from 'components/Common/Spinner'; | ||||
| import { getTokenGenesisInfo } from 'chronik'; | import { getTokenGenesisInfo } from 'chronik'; | ||||
| import { | import { | ||||
| ActiveOffers, | ActiveOffers, | ||||
| OfferTitle, | TokenList, | ||||
| OfferTable, | TokenRow, | ||||
| AgoraHeader, | TokenRowName, | ||||
| TokenRowTicker, | |||||
| SectionTitle, | |||||
| SortSwitch, | SortSwitch, | ||||
| ManageSwitch, | Wrapper, | ||||
| } from './styled'; | } from './styled'; | ||||
| import { getUserLocale } from 'helpers'; | import { getUserLocale } from 'helpers'; | ||||
| import OrderBook, { OrderBookInfo } from './OrderBook'; | |||||
| import { token as tokenConfig } from 'config/token'; | import { token as tokenConfig } from 'config/token'; | ||||
| import TokenIcon from 'components/Etokens/TokenIcon'; | |||||
| import CashtabCache, { CashtabCachedTokenInfo } from 'config/CashtabCache'; | import CashtabCache, { CashtabCachedTokenInfo } from 'config/CashtabCache'; | ||||
| import { InlineLoader } from 'components/Common/Spinner'; | import { InlineLoader } from 'components/Common/Spinner'; | ||||
| import PrimaryButton from 'components/Common/Buttons'; | import PrimaryButton from 'components/Common/Buttons'; | ||||
| import Modal from 'components/Common/Modal'; | import Modal from 'components/Common/Modal'; | ||||
| import { FIRMA } from 'constants/tokens'; | import ActionButtonRow from 'components/Common/ActionButtonRow'; | ||||
| interface CashtabActiveOffers { | interface CashtabActiveOffers { | ||||
| offeredFungibleTokenIds: string[]; | offeredFungibleTokenIds: string[]; | ||||
| offeredFungibleTokenIdsThisWallet: string[]; | offeredFungibleTokenIdsThisWallet: string[]; | ||||
| } | } | ||||
| interface ServerBlacklistResponse { | interface ServerBlacklistResponse { | ||||
| status: string; | status: string; | ||||
| ▲ Show 20 Lines • Show All 43 Lines • ▼ Show 20 Lines | const Agora: React.FC = () => { | ||||
| const { cashtabCache } = cashtabState; | const { cashtabCache } = cashtabState; | ||||
| if (!ecashWallet) { | if (!ecashWallet) { | ||||
| return null; | return null; | ||||
| } | } | ||||
| // Note that wallets must be a non-empty array of CashtabWallet[] here, because | // Note that wallets must be a non-empty array of CashtabWallet[] here, because | ||||
| // context is loaded, and App component only renders Onboarding screen if user has no wallet | // context is loaded, and App component only renders Onboarding screen if user has no wallet | ||||
| const pk = toHex(ecashWallet.pk); | const pk = toHex(ecashWallet.pk); | ||||
| // Use a state param to keep track of how many orderbooks we load at once | // Use a state param to keep track of how many token rows we load at once (when loading all offers) | ||||
| const [loadedOrderBooksCount, setLoadedOrderBooksCount] = | const [loadedOrderBooksCount, setLoadedOrderBooksCount] = | ||||
| useState(POLITE_REQUEST_LIMIT); | useState(POLITE_REQUEST_LIMIT); | ||||
| // active agora partial offers organized for rendering this screen | // active agora partial offers organized for rendering this screen | ||||
| const [activeOffersCashtab, setActiveOffersCashtab] = | const [activeOffersCashtab, setActiveOffersCashtab] = | ||||
| useState<null | CashtabActiveOffers>(null); | useState<null | CashtabActiveOffers>(null); | ||||
| const [chronikQueryError, setChronikQueryError] = useState<boolean>(false); | const [chronikQueryError, setChronikQueryError] = useState<boolean>(false); | ||||
| const [manageMyOffers, setManageMyOffers] = useState<boolean>(false); | |||||
| const orderBookInfoMapRef = useRef<Map<string, OrderBookInfo>>(new Map()); | |||||
| const [allOrderBooksLoaded, setAllOrderBooksLoaded] = | |||||
| useState<boolean>(false); | |||||
| // Agora by default loads a limited whitelist | // Agora by default loads a limited whitelist | ||||
| // But it is possible to load all available offers, if the user is determined | // But it is possible to load all available offers, if the user is determined | ||||
| // and patient | // and patient | ||||
| const [loadAllZeOffers, setLoadAllZeOffers] = useState<boolean>(false); | const [loadAllZeOffers, setLoadAllZeOffers] = useState<boolean>(false); | ||||
| // State variable to hold the token orderbooks we present to the user | // State variable to hold the token orderbooks we present to the user | ||||
| const [renderedTokenIds, setRenderedTokenIds] = useState<null | string[]>( | const [renderedTokenIds, setRenderedTokenIds] = useState<null | string[]>( | ||||
| null, | null, | ||||
| ); | ); | ||||
| // Boolean to show a confirmation modal for alarming decision to loadAllZeOffers | // Boolean to show a confirmation modal for alarming decision to loadAllZeOffers | ||||
| const [showConfirmLoadAllModal, setShowConfirmLoadAllModal] = | const [showConfirmLoadAllModal, setShowConfirmLoadAllModal] = | ||||
| useState<boolean>(false); | useState<boolean>(false); | ||||
| // On load, assume all whitelisted tokens have active offers | // On load, assume all whitelisted tokens have active offers | ||||
| const [whitelistedTokensWithOffers, setWhitelistedTokensWithOffers] = | const [whitelistedTokensWithOffers, setWhitelistedTokensWithOffers] = | ||||
| useState<string[]>(tokenConfig.whitelist); | useState<string[]>(tokenConfig.whitelist); | ||||
| const orderBookInfoMapRef = useRef<Map<string, { offerCount: number }>>( | |||||
| new Map(), | |||||
| ); | |||||
| const [allOrderBooksLoaded, setAllOrderBooksLoaded] = | |||||
| useState<boolean>(false); | |||||
| interface SortSwitches { | interface SortSwitches { | ||||
| byOfferCount: boolean; | byOfferCount: boolean; | ||||
| byTokenId: boolean; | byTokenId: boolean; | ||||
| } | } | ||||
| const sortSwitchesOff: SortSwitches = { | const sortSwitchesOff: SortSwitches = { | ||||
| byOfferCount: false, | byOfferCount: false, | ||||
| byTokenId: false, | byTokenId: false, | ||||
| }; | }; | ||||
| // App loads with offers sorted by token ID, as this is the only sort available before | |||||
| // we analyze data available from agora API calls | |||||
| const [switches, setSwitches] = useState<SortSwitches>({ | const [switches, setSwitches] = useState<SortSwitches>({ | ||||
| ...sortSwitchesOff, | ...sortSwitchesOff, | ||||
| byTokenId: true, | byTokenId: true, | ||||
| }); | }); | ||||
| const sortOrderBooksByOfferCount = () => { | const sortTokenIdsByOfferCount = () => { | ||||
| if (!orderBookInfoMapRef.current || renderedTokenIds === null) { | if (!orderBookInfoMapRef.current || renderedTokenIds === null) { | ||||
| return; | return; | ||||
| } | } | ||||
| const sortedTokenIds = [...renderedTokenIds].sort((a, b) => { | const sortedTokenIds = [...renderedTokenIds].sort((a, b) => { | ||||
| const offerCountA = | const offerCountA = | ||||
| orderBookInfoMapRef.current.get(a)?.offerCount || 0; | orderBookInfoMapRef.current.get(a)?.offerCount || 0; | ||||
| const offerCountB = | const offerCountB = | ||||
| orderBookInfoMapRef.current.get(b)?.offerCount || 0; | orderBookInfoMapRef.current.get(b)?.offerCount || 0; | ||||
| return offerCountB - offerCountA; // Sort by descending offer count | return offerCountB - offerCountA; | ||||
| }); | }); | ||||
| // Update the state with sorted token IDs | |||||
| setRenderedTokenIds(sortedTokenIds); | setRenderedTokenIds(sortedTokenIds); | ||||
| }; | }; | ||||
| const sortOrderBooksByTokenId = () => { | const sortTokenIdsByTokenId = () => { | ||||
| if (!orderBookInfoMapRef.current || renderedTokenIds === null) { | if (renderedTokenIds === null) { | ||||
| return; | return; | ||||
| } | } | ||||
| const sortedTokenIds = [...renderedTokenIds].sort(); | const sortedTokenIds = [...renderedTokenIds].sort(); | ||||
| // Update the state with sorted token IDs | |||||
| setRenderedTokenIds(sortedTokenIds); | setRenderedTokenIds(sortedTokenIds); | ||||
| }; | }; | ||||
| useEffect(() => { | useEffect(() => { | ||||
| if (allOrderBooksLoaded) { | if (renderedTokenIds === null || activeOffersCashtab === null) { | ||||
| if (switches.byOfferCount) { | return; | ||||
| sortOrderBooksByOfferCount(); | |||||
| } else if (switches.byTokenId) { | |||||
| sortOrderBooksByTokenId(); | |||||
| } | |||||
| } | } | ||||
| }, [allOrderBooksLoaded, switches]); | if (renderedTokenIds.length === 0) { | ||||
| setAllOrderBooksLoaded(true); | |||||
| useEffect(() => { | |||||
| if ( | |||||
| activeOffersCashtab === null || | |||||
| allOrderBooksLoaded || | |||||
| !loadAllZeOffers | |||||
| ) { | |||||
| // If we have not yet loaded any offers | |||||
| // Or if we have loaded all the offers | |||||
| // Or if the user is not trying to loadAllZeOffers | |||||
| // Then we do not enter this useEffect | |||||
| return; | return; | ||||
| } | } | ||||
| setAllOrderBooksLoaded(false); | |||||
| orderBookInfoMapRef.current = new Map(); | |||||
| const loadMoreOrderBooks = () => { | const fetchOfferCounts = async () => { | ||||
| setLoadedOrderBooksCount(prevCount => { | for (const tokenId of renderedTokenIds) { | ||||
| const newCount = prevCount + POLITE_REQUEST_LIMIT; | try { | ||||
| // Only increase if there are more to load | const offers = await agora.activeOffersByTokenId(tokenId); | ||||
| if ( | const count = Array.isArray(offers) ? offers.length : 0; | ||||
| newCount <= | orderBookInfoMapRef.current.set(tokenId, { | ||||
| activeOffersCashtab.offeredFungibleTokenIds.length | offerCount: count, | ||||
| ) { | }); | ||||
| return newCount; | } catch { | ||||
| } | orderBookInfoMapRef.current.set(tokenId, { | ||||
| // Clear the interval when all are loaded | offerCount: 0, | ||||
| clearInterval(intervalId); | |||||
| // Use the total when we get there | |||||
| return activeOffersCashtab.offeredFungibleTokenIds.length; | |||||
| }); | }); | ||||
| } | |||||
| } | |||||
| setAllOrderBooksLoaded(true); | |||||
| }; | }; | ||||
| fetchOfferCounts(); | |||||
| }, [ | |||||
| renderedTokenIds?.length, | |||||
| activeOffersCashtab?.offeredFungibleTokenIds.length, | |||||
| ]); | |||||
| const intervalId = setInterval(loadMoreOrderBooks, POLITE_INTERVAL_MS); | useEffect(() => { | ||||
| if (!allOrderBooksLoaded || renderedTokenIds === null) { | |||||
| // Clean up the interval when component unmounts or when all order books are loaded | return; | ||||
| return () => clearInterval(intervalId); | } | ||||
| }, [activeOffersCashtab, allOrderBooksLoaded, loadAllZeOffers]); | if (switches.byOfferCount) { | ||||
| sortTokenIdsByOfferCount(); | |||||
| } else if (switches.byTokenId) { | |||||
| sortTokenIdsByTokenId(); | |||||
| } | |||||
| }, [allOrderBooksLoaded, switches]); | |||||
| useEffect(() => { | useEffect(() => { | ||||
| if (activeOffersCashtab === null) { | if (activeOffersCashtab === null) { | ||||
| return; | return; | ||||
| } | } | ||||
| // If the user wants to loadAllZeOffers, we do polite loading of activeOffersCashtab.offeredFungibleTokenIds | // If the user wants to loadAllZeOffers, we do polite loading of activeOffersCashtab.offeredFungibleTokenIds | ||||
| // If the user has not manually agreed to loadAllZeOffers, we only show whitelisted tokenIds that have active offers | // If the user has not manually agreed to loadAllZeOffers, we only show whitelisted tokenIds that have active offers | ||||
| ▲ Show 20 Lines • Show All 199 Lines • ▼ Show 20 Lines | useEffect(() => { | ||||
| // Do nothing if pk has not yet been set | // Do nothing if pk has not yet been set | ||||
| return; | return; | ||||
| } | } | ||||
| // Reset when pk changes | // Reset when pk changes | ||||
| setActiveOffersCashtab(null); | setActiveOffersCashtab(null); | ||||
| getListedTokens(); | getListedTokens(); | ||||
| }, [pk]); | }, [pk]); | ||||
| const intervalIdRef = useRef<NodeJS.Timeout | null>(null); | |||||
| useEffect(() => { | useEffect(() => { | ||||
| if (activeOffersCashtab === null || renderedTokenIds === null) { | if (activeOffersCashtab === null || !loadAllZeOffers) { | ||||
| // Do nothing until we have loaded tokens to render | |||||
| return; | return; | ||||
| } | } | ||||
| if (!loadAllZeOffers) { | const loadMoreTokens = () => { | ||||
| // If we are not loading all of the offers, create the timeout based on renderedTokenIds | setLoadedOrderBooksCount(prevCount => { | ||||
| intervalIdRef.current = setInterval(() => { | const newCount = prevCount + POLITE_REQUEST_LIMIT; | ||||
| const currentSize = orderBookInfoMapRef.current.size; | |||||
| if (currentSize === renderedTokenIds.length) { | |||||
| // We have loaded all offer info, can enable search using this info | |||||
| setAllOrderBooksLoaded(true); | |||||
| // Clear the interval when orderBookInfoMap is loaded | |||||
| if (intervalIdRef.current !== null) { | |||||
| clearInterval(intervalIdRef.current); | |||||
| intervalIdRef.current = null; // Reset the ref to null | |||||
| } | |||||
| } | |||||
| // Check every 5 seconds. For whitelisted tokenIds, we do not expect to do this more than once | |||||
| // But the whitelist is expected to grow, so mb we will get there | |||||
| }, 5000); | |||||
| // Cleanup the interval when the component unmounts | |||||
| return () => { | |||||
| if (intervalIdRef.current) { | |||||
| clearInterval(intervalIdRef.current); | |||||
| } | |||||
| }; | |||||
| } | |||||
| if ( | |||||
| allOrderBooksLoaded == true && | |||||
| orderBookInfoMapRef.current.size < | |||||
| activeOffersCashtab.offeredFungibleTokenIds.length | |||||
| ) { | |||||
| // Reset allOrderBooksLoaded if all order books are in fact not loaded | |||||
| // Expected behavior if the user elects to loadAllZeOffers | |||||
| setAllOrderBooksLoaded(false); | |||||
| } | |||||
| // Start the interval when the component mounts | |||||
| intervalIdRef.current = setInterval(() => { | |||||
| const currentSize = orderBookInfoMapRef.current.size; | |||||
| if ( | if ( | ||||
| currentSize === | newCount <= | ||||
| activeOffersCashtab.offeredFungibleTokenIds.length | activeOffersCashtab.offeredFungibleTokenIds.length | ||||
| ) { | ) { | ||||
| // We have loaded all offer info, can enable search using this info | return newCount; | ||||
| setAllOrderBooksLoaded(true); | |||||
| // Clear the interval when orderBookInfoMap is loaded | |||||
| if (intervalIdRef.current !== null) { | |||||
| clearInterval(intervalIdRef.current); | |||||
| intervalIdRef.current = null; // Reset the ref to null | |||||
| } | |||||
| } | |||||
| }, 5000); // Check every 5 seconds. This can take > 30s. | |||||
| // Cleanup the interval when the component unmounts | |||||
| return () => { | |||||
| if (intervalIdRef.current) { | |||||
| clearInterval(intervalIdRef.current); | |||||
| } | } | ||||
| return activeOffersCashtab.offeredFungibleTokenIds.length; | |||||
| }); | |||||
| }; | }; | ||||
| }, [activeOffersCashtab, loadAllZeOffers, renderedTokenIds]); | |||||
| const intervalId = setInterval(loadMoreTokens, POLITE_INTERVAL_MS); | |||||
| return () => clearInterval(intervalId); | |||||
| }, [activeOffersCashtab, loadAllZeOffers]); | |||||
| return ( | return ( | ||||
| <> | <Wrapper> | ||||
| <ActionButtonRow variant="agora" activeIndex={0} /> | |||||
| {chronikQueryError ? ( | {chronikQueryError ? ( | ||||
| <ActiveOffers title="Chronik Query Error"> | <ActiveOffers title="Chronik Query Error"> | ||||
| <Alert> | <Alert> | ||||
| Error querying listed tokens. Please try again later. | Error querying listed tokens. Please try again later. | ||||
| </Alert> | </Alert> | ||||
| </ActiveOffers> | </ActiveOffers> | ||||
| ) : ( | ) : ( | ||||
| <> | <> | ||||
| {renderedTokenIds === null ? ( | {renderedTokenIds === null ? ( | ||||
| <Spinner title="Loading active offers" /> | <Spinner title="Loading active offers" /> | ||||
| ) : ( | ) : ( | ||||
| <> | <> | ||||
| {showConfirmLoadAllModal && | {showConfirmLoadAllModal && | ||||
| activeOffersCashtab !== null && ( | activeOffersCashtab !== null && ( | ||||
| <Modal | <Modal | ||||
| title={`Load all agora offers?`} | title={`Load all agora offers?`} | ||||
| description={`We have ${activeOffersCashtab.offeredFungibleTokenIds.length.toLocaleString( | description={`We have ${activeOffersCashtab.offeredFungibleTokenIds.length.toLocaleString( | ||||
| userLocale, | userLocale, | ||||
| { | { | ||||
| minimumFractionDigits: 0, | minimumFractionDigits: 0, | ||||
| maximumFractionDigits: 0, | maximumFractionDigits: 0, | ||||
| }, | }, | ||||
| )} listings. This will take a long time and the screen will be slow.`} | )} listings. This will take a long time and the screen will be slow.`} | ||||
| handleOk={() => { | handleOk={() => { | ||||
| // Manually update the sort switch to sort by tokenId | |||||
| // We need to update orderbookMap with all of the offers | |||||
| // before we can offer sort by other params | |||||
| setSwitches({ | |||||
| ...sortSwitchesOff, | |||||
| byTokenId: true, | |||||
| }); | |||||
| // Start loading all the offers | |||||
| setLoadAllZeOffers(true); | setLoadAllZeOffers(true); | ||||
| // Hide this modal | |||||
| setShowConfirmLoadAllModal(false); | setShowConfirmLoadAllModal(false); | ||||
| }} | }} | ||||
| handleCancel={() => | handleCancel={() => | ||||
| setShowConfirmLoadAllModal(false) | setShowConfirmLoadAllModal(false) | ||||
| } | } | ||||
| /> | /> | ||||
| )} | )} | ||||
| <AgoraHeader> | <ActiveOffers title="Active Offers"> | ||||
| <h2>Token Offers</h2> | {renderedTokenIds === null || | ||||
| <div> | activeOffersCashtab === null ? ( | ||||
| {!manageMyOffers && ( | <InlineLoader /> | ||||
| ) : ( | |||||
| (() => { | |||||
| const myOfferIds = | |||||
| activeOffersCashtab.offeredFungibleTokenIdsThisWallet; | |||||
| const otherTokenIds = | |||||
| renderedTokenIds.filter( | |||||
| id => !myOfferIds.includes(id), | |||||
| ); | |||||
| const renderTokenRow = ( | |||||
| tokenId: string, | |||||
| ) => { | |||||
| const cached = | |||||
| cashtabCache.tokens.get( | |||||
| tokenId, | |||||
| ); | |||||
| const name = | |||||
| cached?.genesisInfo | |||||
| ?.tokenName ?? tokenId; | |||||
| const ticker = | |||||
| cached?.genesisInfo | |||||
| ?.tokenTicker ?? '—'; | |||||
| return ( | |||||
| <TokenRow | |||||
| key={tokenId} | |||||
| as={Link} | |||||
| to={`/token/${tokenId}`} | |||||
| > | |||||
| <TokenIcon | |||||
| size={64} | |||||
| tokenId={tokenId} | |||||
| /> | |||||
| <TokenRowName> | |||||
| {name} | |||||
| <TokenRowTicker> | |||||
| {ticker} | |||||
| </TokenRowTicker> | |||||
| </TokenRowName> | |||||
| </TokenRow> | |||||
| ); | |||||
| }; | |||||
| return ( | |||||
| <> | |||||
| {myOfferIds.length > 0 && ( | |||||
| <> | <> | ||||
| <SectionTitle> | |||||
| Your Listings | |||||
| </SectionTitle> | |||||
| <TokenList> | |||||
| {myOfferIds.map( | |||||
| renderTokenRow, | |||||
| )} | |||||
| </TokenList> | |||||
| </> | |||||
| )} | |||||
| <div | |||||
| style={{ | |||||
| marginTop: | |||||
| myOfferIds.length > | |||||
| 0 | |||||
| ? 24 | |||||
| : 0, | |||||
| }} | |||||
| > | |||||
| <div | |||||
| style={{ | |||||
| display: 'flex', | |||||
| flexWrap: 'wrap', | |||||
| alignItems: | |||||
| 'center', | |||||
| gap: 8, | |||||
| marginBottom: 8, | |||||
| }} | |||||
| > | |||||
| <SectionTitle | |||||
| style={{ | |||||
| margin: 0, | |||||
| }} | |||||
| > | |||||
| Token Offers | |||||
| </SectionTitle> | |||||
| <span | |||||
| style={{ | |||||
| display: 'flex', | |||||
| alignItems: | |||||
| 'center', | |||||
| gap: 8, | |||||
| }} | |||||
| > | |||||
| Sort by: | Sort by: | ||||
| <SortSwitch | <SortSwitch | ||||
| disabled={false} | disabled={false} | ||||
| active={switches.byTokenId} | active={ | ||||
| switches.byTokenId | |||||
| } | |||||
| title="Sort by TokenId" | title="Sort by TokenId" | ||||
| onClick={() => | onClick={() => | ||||
| setSwitches({ | setSwitches( | ||||
| { | |||||
| ...sortSwitchesOff, | ...sortSwitchesOff, | ||||
| byTokenId: true, | byTokenId: true, | ||||
| }) | }, | ||||
| ) | |||||
| } | } | ||||
| > | > | ||||
| TokenID | TokenID | ||||
| </SortSwitch> | </SortSwitch> | ||||
| <SortSwitch | <SortSwitch | ||||
| disabled={!allOrderBooksLoaded} | disabled={ | ||||
| active={switches.byOfferCount} | !allOrderBooksLoaded | ||||
| } | |||||
| active={ | |||||
| switches.byOfferCount | |||||
| } | |||||
| title="Sort by Offer Count" | title="Sort by Offer Count" | ||||
| onClick={ | onClick={ | ||||
| allOrderBooksLoaded | allOrderBooksLoaded | ||||
| ? () => | ? () => | ||||
| setSwitches({ | setSwitches( | ||||
| { | |||||
| ...sortSwitchesOff, | ...sortSwitchesOff, | ||||
| byOfferCount: true, | byOfferCount: true, | ||||
| }) | }, | ||||
| ) | |||||
| : undefined | : undefined | ||||
| } | } | ||||
| > | > | ||||
| Offers | Offers | ||||
| {!allOrderBooksLoaded && ( | {!allOrderBooksLoaded && ( | ||||
| <div> | |||||
| <InlineLoader title="Loading OrderBook info..." /> | <InlineLoader title="Loading OrderBook info..." /> | ||||
| </div> | |||||
| )} | )} | ||||
| </SortSwitch> | </SortSwitch> | ||||
| </> | </span> | ||||
| )} | |||||
| <ManageSwitch | |||||
| title="Toggle Active Offers" | |||||
| onClick={() => { | |||||
| setManageMyOffers( | |||||
| () => !manageMyOffers, | |||||
| ); | |||||
| }} | |||||
| > | |||||
| {!manageMyOffers | |||||
| ? 'My Listings' | |||||
| : 'All Offers'} | |||||
| </ManageSwitch> | |||||
| </div> | </div> | ||||
| </AgoraHeader> | {otherTokenIds.length > | ||||
| <ActiveOffers title="Active Offers"> | 0 ? ( | ||||
| {manageMyOffers ? ( | <TokenList> | ||||
| <> | {otherTokenIds.map( | ||||
| <OfferTitle> | renderTokenRow, | ||||
| Manage your listings | |||||
| </OfferTitle> | |||||
| {activeOffersCashtab === null ? ( | |||||
| <InlineLoader /> | |||||
| ) : activeOffersCashtab | |||||
| .offeredFungibleTokenIdsThisWallet | |||||
| .length > 0 ? ( | |||||
| <OfferTable> | |||||
| {activeOffersCashtab.offeredFungibleTokenIdsThisWallet.map( | |||||
| offeredTokenId => { | |||||
| return ( | |||||
| <OrderBook | |||||
| key={ | |||||
| offeredTokenId | |||||
| } | |||||
| tokenId={ | |||||
| offeredTokenId | |||||
| } | |||||
| userLocale={ | |||||
| userLocale | |||||
| } | |||||
| /> | |||||
| ); | |||||
| }, | |||||
| )} | |||||
| </OfferTable> | |||||
| ) : ( | |||||
| <p> | |||||
| You do not have any listed | |||||
| tokens | |||||
| </p> | |||||
| )} | |||||
| </> | |||||
| ) : ( | |||||
| <> | |||||
| {renderedTokenIds !== null && | |||||
| renderedTokenIds.length > 0 ? ( | |||||
| <OfferTable | |||||
| renderedOfferCount={ | |||||
| renderedTokenIds.length | |||||
| } | |||||
| > | |||||
| {renderedTokenIds.map( | |||||
| offeredTokenId => { | |||||
| return ( | |||||
| <OrderBook | |||||
| key={ | |||||
| offeredTokenId | |||||
| } | |||||
| tokenId={ | |||||
| offeredTokenId | |||||
| } | |||||
| userLocale={ | |||||
| userLocale | |||||
| } | |||||
| orderBookInfoMap={ | |||||
| orderBookInfoMapRef.current | |||||
| } | |||||
| /** | |||||
| * We want to use Websockets | |||||
| * - When we manage our own offers (see above) | |||||
| * - When we only show whitelisted offers | |||||
| * | |||||
| * We DO NOT want to use websockets | |||||
| * - When we display all offers | |||||
| * | |||||
| * We may be able to use websockets for all | |||||
| * rendered OrderBooks after we get lazy loading right | |||||
| */ | |||||
| noWebsocket={ | |||||
| loadAllZeOffers | |||||
| } | |||||
| priceInFiat={ | |||||
| offeredTokenId === | |||||
| FIRMA.tokenId | |||||
| } | |||||
| /> | |||||
| ); | |||||
| }, | |||||
| )} | )} | ||||
| </OfferTable> | </TokenList> | ||||
| ) : ( | ) : !loadAllZeOffers ? ( | ||||
| <> | |||||
| {!loadAllZeOffers ? ( | |||||
| <p> | <p> | ||||
| No whitelisted tokens | No whitelisted | ||||
| are currently listed for | tokens are currently | ||||
| sale. Try loading all | listed for sale. Try | ||||
| offers. | loading all offers. | ||||
| </p> | </p> | ||||
| ) : ( | ) : ( | ||||
| <p> | <p> | ||||
| No tokens are currently | No tokens are | ||||
| listed for sale. | currently listed for | ||||
| sale. | |||||
| </p> | </p> | ||||
| )} | )} | ||||
| </div> | |||||
| </> | </> | ||||
| )} | ); | ||||
| </> | })() | ||||
| )} | )} | ||||
| </ActiveOffers> | </ActiveOffers> | ||||
| {!loadAllZeOffers && !manageMyOffers && ( | {!loadAllZeOffers && ( | ||||
| <PrimaryButton | <PrimaryButton | ||||
| style={{ | style={{ | ||||
| marginTop: '12px', | marginTop: '12px', | ||||
| }} | }} | ||||
| disabled={activeOffersCashtab === null} | disabled={activeOffersCashtab === null} | ||||
| onClick={() => | onClick={() => | ||||
| setShowConfirmLoadAllModal(true) | setShowConfirmLoadAllModal(true) | ||||
| } | } | ||||
| > | > | ||||
| {activeOffersCashtab === null ? ( | {activeOffersCashtab === null ? ( | ||||
| <InlineLoader /> | <InlineLoader /> | ||||
| ) : ( | ) : ( | ||||
| 'Load all offers' | 'Load all offers' | ||||
| )} | )} | ||||
| </PrimaryButton> | </PrimaryButton> | ||||
| )} | )} | ||||
| </> | </> | ||||
| )} | )} | ||||
| </> | </> | ||||
| )} | )} | ||||
| </> | </Wrapper> | ||||
| ); | ); | ||||
| }; | }; | ||||
| export default Agora; | export default Agora; | ||||