feat: [ListV2] Add retry and remove logic to listings and collection approvals (#5923)

* add remove/retry buttons

* add retry logic functionality

* add scroll to active row

* properly update rejected status

* replace loadingicon with loader

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
This commit is contained in:
Charles Bachmeier 2023-02-06 12:55:05 -08:00 committed by GitHub
parent 7229637c4c
commit 5def0dd166
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 219 additions and 99 deletions

@ -0,0 +1,181 @@
import { Trans } from '@lingui/macro'
import Column from 'components/Column'
import Loader from 'components/Loader'
import Row from 'components/Row'
import { VerifiedIcon } from 'nft/components/icons'
import { AssetRow, CollectionRow, ListingStatus } from 'nft/types'
import { useEffect, useRef } from 'react'
import { Check, XOctagon } from 'react-feather'
import styled, { css, useTheme } from 'styled-components/macro'
import { ThemedText } from 'theme'
import { opacify } from 'theme/utils'
const ContentColumn = styled(Column)<{ failed: boolean }>`
background-color: ${({ theme, failed }) => failed && opacify(12, theme.accentCritical)};
border-radius: 12px;
padding-bottom: ${({ failed }) => failed && '16px'};
`
const ContentRowWrapper = styled(Row)<{ active: boolean; failed: boolean }>`
padding: 16px;
border: ${({ failed, theme }) => !failed && `1px solid ${theme.backgroundOutline}`};
border-radius: 12px;
opacity: ${({ active, failed }) => (active || failed ? '1' : '0.6')};
`
const CollectionIcon = styled.img`
border-radius: 100px;
height: 24px;
width: 24px;
z-index: 1;
`
const AssetIcon = styled.img`
border-radius: 4px;
height: 24px;
width: 24px;
z-index: 1;
`
const MarketplaceIcon = styled.img`
border-radius: 4px;
height: 24px;
width: 24px;
margin-left: -4px;
margin-right: 12px;
`
const ContentName = styled(ThemedText.SubHeaderSmall)`
color: ${({ theme }) => theme.textPrimary};
line-height: 20px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
max-width: 40%;
`
const ProceedText = styled.span`
font-weight: 600;
font-size: 12px;
line-height: 16px;
color: ${({ theme }) => theme.textSecondary};
`
const FailedText = styled.span`
font-weight: 600;
font-size: 10px;
line-height: 12px;
color: ${({ theme }) => theme.accentCritical};
margin-left: 4px;
`
const StyledVerifiedIcon = styled(VerifiedIcon)`
height: 16px;
width: 16px;
margin-left: 4px;
`
const IconWrapper = styled.div`
margin-left: auto;
margin-right: 0px;
`
const ButtonRow = styled(Row)`
padding: 0px 16px;
justify-content: space-between;
`
const failedButtonStyle = css`
width: 152px;
cursor: pointer;
padding: 8px 0px;
text-align: center;
font-weight: 600;
font-size: 14px;
line-height: 16px;
border-radius: 12px;
border: none;
&:hover {
opacity: 0.6;
}
`
const RemoveButton = styled.button`
background-color: ${({ theme }) => theme.accentCritical};
color: ${({ theme }) => theme.accentTextDarkPrimary};
${failedButtonStyle}
`
const RetryButton = styled.button`
background-color: ${({ theme }) => theme.backgroundInteractive};
color: ${({ theme }) => theme.textPrimary};
${failedButtonStyle}
`
export const ContentRow = ({
row,
isCollectionApprovalSection,
removeRow,
}: {
row: AssetRow
isCollectionApprovalSection: boolean
removeRow: (row: AssetRow) => void
}) => {
const theme = useTheme()
const rowRef = useRef<HTMLDivElement>()
const failed = row.status === ListingStatus.FAILED || row.status === ListingStatus.REJECTED
useEffect(() => {
row.status === ListingStatus.SIGNING && rowRef.current?.scroll
}, [row.status])
return (
<ContentColumn failed={failed}>
<ContentRowWrapper
active={row.status === ListingStatus.SIGNING || row.status === ListingStatus.APPROVED}
failed={failed}
ref={rowRef}
>
{isCollectionApprovalSection ? <CollectionIcon src={row.images[0]} /> : <AssetIcon src={row.images[0]} />}
<MarketplaceIcon src={row.images[1]} />
<ContentName>{row.name}</ContentName>
{isCollectionApprovalSection && (row as CollectionRow).isVerified && <StyledVerifiedIcon />}
<IconWrapper>
{row.status === ListingStatus.DEFINED || row.status === ListingStatus.PENDING ? (
<Loader
height="14px"
width="14px"
stroke={row.status === ListingStatus.PENDING ? theme.accentAction : theme.textTertiary}
/>
) : row.status === ListingStatus.SIGNING ? (
<ProceedText>
<Trans>Proceed in wallet</Trans>
</ProceedText>
) : row.status === ListingStatus.APPROVED ? (
<Check height="20" width="20" stroke={theme.accentSuccess} />
) : (
failed && (
<Row>
<XOctagon height="20" width="20" color={theme.accentCritical} />
<FailedText>
{row.status === ListingStatus.FAILED ? <Trans>Failed</Trans> : <Trans>Rejected</Trans>}
</FailedText>
</Row>
)
)}
</IconWrapper>
</ContentRowWrapper>
{failed && (
<ButtonRow justify="space-between">
<RemoveButton onClick={() => removeRow(row)}>
<Trans>Remove</Trans>
</RemoveButton>
<RetryButton onClick={row.callback}>
<Trans>Retry</Trans>
</RetryButton>
</ButtonRow>
)}
</ContentColumn>
)
}

@ -92,6 +92,11 @@ export const ListModal = ({ overlayClick }: { overlayClick: () => void }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [allCollectionsApproved])
// In the case that a user removes all listings via retry logic, close modal
useEffect(() => {
!listings.length && overlayClick()
}, [listings, overlayClick])
return (
<Portal>
<Trace modal={InterfaceModalName.NFT_LISTING}>

@ -3,21 +3,18 @@ import Column from 'components/Column'
import { ScrollBarStyles } from 'components/Common'
import Row from 'components/Row'
import { MouseoverTooltip } from 'components/Tooltip'
import {
ChevronUpIcon,
ListingModalWindowActive,
ListingModalWindowClosed,
LoadingIcon,
VerifiedIcon,
} from 'nft/components/icons'
import { AssetRow, CollectionRow, ListingStatus } from 'nft/types'
import { ChevronUpIcon, ListingModalWindowActive, ListingModalWindowClosed } from 'nft/components/icons'
import { useSellAsset } from 'nft/hooks'
import { AssetRow, CollectionRow, ListingRow, ListingStatus } from 'nft/types'
import { useMemo } from 'react'
import { Check, Info } from 'react-feather'
import { Info } from 'react-feather'
import styled, { useTheme } from 'styled-components/macro'
import { ThemedText } from 'theme'
import { colors } from 'theme/colors'
import { TRANSITION_DURATIONS } from 'theme/styles'
import { ContentRow } from './ContentRow'
const SectionHeader = styled(Row)`
justify-content: space-between;
`
@ -56,62 +53,7 @@ const StyledInfoIcon = styled(Info)`
const ContentRowContainer = styled(Column)`
gap: 8px;
`
const ContentRow = styled(Row)<{ active: boolean }>`
padding: 16px;
border: 1px solid ${({ theme }) => theme.backgroundOutline};
border-radius: 12px;
opacity: ${({ active }) => (active ? '1' : '0.6')};
`
const CollectionIcon = styled.img`
border-radius: 100px;
height: 24px;
width: 24px;
z-index: 1;
`
const AssetIcon = styled.img`
border-radius: 4px;
height: 24px;
width: 24px;
z-index: 1;
`
const MarketplaceIcon = styled.img`
border-radius: 4px;
height: 24px;
width: 24px;
margin-left: -4px;
margin-right: 12px;
`
const ContentName = styled(ThemedText.SubHeaderSmall)`
color: ${({ theme }) => theme.textPrimary};
line-height: 20px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
max-width: 50%;
`
const ProceedText = styled.span`
font-weight: 600;
font-size: 12px;
line-height: 16px;
color: ${({ theme }) => theme.textSecondary};
`
const StyledVerifiedIcon = styled(VerifiedIcon)`
height: 16px;
width: 16px;
margin-left: 4px;
`
const IconWrapper = styled.div`
margin-left: auto;
margin-right: 0px;
scroll-behavior: smooth;
`
export const enum Section {
@ -128,8 +70,24 @@ interface ListModalSectionProps {
export const ListModalSection = ({ sectionType, active, content, toggleSection }: ListModalSectionProps) => {
const theme = useTheme()
const sellAssets = useSellAsset((state) => state.sellAssets)
const removeAssetMarketplace = useSellAsset((state) => state.removeAssetMarketplace)
const allContentApproved = useMemo(() => !content.some((row) => row.status !== ListingStatus.APPROVED), [content])
const isCollectionApprovalSection = sectionType === Section.APPROVE
const removeRow = (row: AssetRow) => {
// collections
if (isCollectionApprovalSection) {
const collectionRow = row as CollectionRow
for (const asset of sellAssets)
if (asset.asset_contract.address === collectionRow.collectionAddress)
removeAssetMarketplace(asset, collectionRow.marketplace)
}
// listings
else {
const listingRow = row as ListingRow
removeAssetMarketplace(listingRow.asset, listingRow.marketplace)
}
}
return (
<Column>
<SectionHeader>
@ -174,40 +132,14 @@ export const ListModalSection = ({ sectionType, active, content, toggleSection }
</Row>
)}
<ContentRowContainer>
{content.map((row) => {
return (
<ContentRow
key={row.name}
active={row.status === ListingStatus.SIGNING || row.status === ListingStatus.APPROVED}
>
{isCollectionApprovalSection ? (
<CollectionIcon src={row.images[0]} />
) : (
<AssetIcon src={row.images[0]} />
)}
<MarketplaceIcon src={row.images[1]} />
<ContentName>{row.name}</ContentName>
{isCollectionApprovalSection && (row as CollectionRow).isVerified && <StyledVerifiedIcon />}
<IconWrapper>
{row.status === ListingStatus.DEFINED || row.status === ListingStatus.PENDING ? (
<LoadingIcon
height="14px"
width="14px"
stroke={row.status === ListingStatus.PENDING ? theme.accentAction : theme.textTertiary}
/>
) : row.status === ListingStatus.SIGNING ? (
<ProceedText>
<Trans>Proceed in wallet</Trans>
</ProceedText>
) : (
row.status === ListingStatus.APPROVED && (
<Check height="20" width="20" stroke={theme.accentSuccess} />
)
)}
</IconWrapper>
</ContentRow>
)
})}
{content.map((row: AssetRow) => (
<ContentRow
row={row}
key={row.name}
removeRow={removeRow}
isCollectionApprovalSection={isCollectionApprovalSection}
/>
))}
</ContentRowContainer>
</SectionBody>
)}

@ -47,6 +47,8 @@ export const useNFTList = create<NFTListState>()(
return ListingStatus.APPROVED
case ListingStatus.FAILED:
return listing.status === ListingStatus.SIGNING ? ListingStatus.SIGNING : ListingStatus.FAILED
case ListingStatus.REJECTED:
return listing.status === ListingStatus.SIGNING ? ListingStatus.SIGNING : ListingStatus.REJECTED
default:
return listing.status
}