feat: Migrate NFT Activity to GraphQL (#6103)
* add new activity query * add nftactivity hook and fix query * converted activity type to accept optionals, hook should return activity type * single asset activity * relaystylepagination * working on local endpoint * working timestamps * updated endpoint * use correct env var * undo asset testing * forgot last * undo more testing * don't break on null address * working loading state * better loading states * handle marketplace type update * remove types * properly format price and reduce redundancy * handle null price --------- Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
This commit is contained in:
parent
bc251230da
commit
18939aa871
@ -19,6 +19,7 @@ export const apolloClient = new ApolloClient({
|
||||
fields: {
|
||||
nftBalances: relayStylePagination(),
|
||||
nftAssets: relayStylePagination(),
|
||||
nftActivity: relayStylePagination(),
|
||||
// tell apollo client how to reference Token items in the cache after being fetched by queries that return Token[]
|
||||
token: {
|
||||
read(_, { args, toReference }): Reference | undefined {
|
||||
|
140
src/graphql/data/nft/NftActivity.ts
Normal file
140
src/graphql/data/nft/NftActivity.ts
Normal file
@ -0,0 +1,140 @@
|
||||
import { WatchQueryFetchPolicy } from '@apollo/client'
|
||||
import { useNftGraphqlEnabled } from 'featureFlags/flags/nftlGraphql'
|
||||
import gql from 'graphql-tag'
|
||||
import { ActivityEvent } from 'nft/types'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
|
||||
import { NftActivityFilterInput, useNftActivityQuery } from '../__generated__/types-and-hooks'
|
||||
|
||||
gql`
|
||||
query NftActivity($filter: NftActivityFilterInput, $after: String, $first: Int) {
|
||||
nftActivity(filter: $filter, after: $after, first: $first) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
address
|
||||
tokenId
|
||||
asset {
|
||||
id
|
||||
metadataUrl
|
||||
image {
|
||||
id
|
||||
url
|
||||
}
|
||||
smallImage {
|
||||
id
|
||||
url
|
||||
}
|
||||
name
|
||||
rarities {
|
||||
id
|
||||
provider
|
||||
rank
|
||||
score
|
||||
}
|
||||
suspiciousFlag
|
||||
nftContract {
|
||||
id
|
||||
standard
|
||||
}
|
||||
collection {
|
||||
id
|
||||
image {
|
||||
id
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
type
|
||||
marketplace
|
||||
fromAddress
|
||||
toAddress
|
||||
transactionHash
|
||||
price {
|
||||
id
|
||||
value
|
||||
}
|
||||
orderStatus
|
||||
quantity
|
||||
url
|
||||
timestamp
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
endCursor
|
||||
hasNextPage
|
||||
hasPreviousPage
|
||||
startCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export function useNftActivity(filter: NftActivityFilterInput, first?: number, fetchPolicy?: WatchQueryFetchPolicy) {
|
||||
const isNftGraphqlEnabled = useNftGraphqlEnabled()
|
||||
const { data, loading, fetchMore, error } = useNftActivityQuery({
|
||||
variables: {
|
||||
filter,
|
||||
first,
|
||||
},
|
||||
skip: !isNftGraphqlEnabled,
|
||||
fetchPolicy,
|
||||
})
|
||||
|
||||
const hasNext = data?.nftActivity?.pageInfo?.hasNextPage
|
||||
const loadMore = useCallback(
|
||||
() =>
|
||||
fetchMore({
|
||||
variables: {
|
||||
after: data?.nftActivity?.pageInfo?.endCursor,
|
||||
},
|
||||
}),
|
||||
[data, fetchMore]
|
||||
)
|
||||
|
||||
const nftActivity: ActivityEvent[] | undefined = useMemo(
|
||||
() =>
|
||||
data?.nftActivity?.edges?.map((queryActivity) => {
|
||||
const activity = queryActivity?.node
|
||||
const asset = activity?.asset
|
||||
return {
|
||||
collectionAddress: activity.address,
|
||||
tokenId: activity.tokenId,
|
||||
tokenMetadata: {
|
||||
name: asset?.name,
|
||||
imageUrl: asset?.image?.url,
|
||||
smallImageUrl: asset?.smallImage?.url,
|
||||
metadataUrl: asset?.metadataUrl,
|
||||
rarity: {
|
||||
primaryProvider: 'Rarity Sniper', // TODO update when backend adds more providers
|
||||
providers: asset?.rarities?.map((rarity) => {
|
||||
return {
|
||||
...rarity,
|
||||
provider: 'Rarity Sniper',
|
||||
}
|
||||
}),
|
||||
},
|
||||
suspiciousFlag: asset?.suspiciousFlag,
|
||||
standard: asset?.nftContract?.standard,
|
||||
},
|
||||
eventType: activity.type,
|
||||
marketplace: activity.marketplace,
|
||||
fromAddress: activity.fromAddress,
|
||||
toAddress: activity.toAddress,
|
||||
transactionHash: activity.transactionHash,
|
||||
orderStatus: activity.orderStatus,
|
||||
price: activity.price?.value.toString(),
|
||||
symbol: asset?.collection?.image?.url,
|
||||
quantity: activity.quantity,
|
||||
url: activity.url,
|
||||
eventTimestamp: activity.timestamp * 1000,
|
||||
}
|
||||
}),
|
||||
[data]
|
||||
)
|
||||
|
||||
return useMemo(
|
||||
() => ({ nftActivity, hasNext, loadMore, loading, error }),
|
||||
[hasNext, loadMore, loading, nftActivity, error]
|
||||
)
|
||||
}
|
@ -1,4 +1,7 @@
|
||||
import { OpacityHoverState } from 'components/Common'
|
||||
import { useNftGraphqlEnabled } from 'featureFlags/flags/nftlGraphql'
|
||||
import { NftActivityType } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { useNftActivity } from 'graphql/data/nft/NftActivity'
|
||||
import { Box } from 'nft/components/Box'
|
||||
import { Column, Row } from 'nft/components/Flex'
|
||||
import { themeVars, vars } from 'nft/css/sprinkles.css'
|
||||
@ -6,7 +9,7 @@ import { useBag, useIsMobile } from 'nft/hooks'
|
||||
import { ActivityFetcher } from 'nft/queries/genie/ActivityFetcher'
|
||||
import { ActivityEvent, ActivityEventResponse, ActivityEventType } from 'nft/types'
|
||||
import { fetchPrice } from 'nft/utils/fetchPrice'
|
||||
import { useCallback, useEffect, useMemo, useReducer, useState } from 'react'
|
||||
import { useCallback, useEffect, useReducer, useState } from 'react'
|
||||
import InfiniteScroll from 'react-infinite-scroll-component'
|
||||
import { useInfiniteQuery } from 'react-query'
|
||||
import { useIsDarkMode } from 'state/user/hooks'
|
||||
@ -63,6 +66,7 @@ export const reduceFilters = (state: typeof initialFilterState, action: { eventT
|
||||
const baseHref = (event: ActivityEvent) => `/#/nfts/asset/${event.collectionAddress}/${event.tokenId}?origin=activity`
|
||||
|
||||
export const Activity = ({ contractAddress, rarityVerified, collectionName, chainId }: ActivityProps) => {
|
||||
const isNftGraphqlEnabled = useNftGraphqlEnabled()
|
||||
const [activeFilters, filtersDispatch] = useReducer(reduceFilters, initialFilterState)
|
||||
|
||||
const {
|
||||
@ -102,11 +106,33 @@ export const Activity = ({ contractAddress, rarityVerified, collectionName, chai
|
||||
}
|
||||
)
|
||||
|
||||
const events = useMemo(
|
||||
() => (isSuccess ? eventsData?.pages.map((page) => page.events).flat() : null),
|
||||
[isSuccess, eventsData]
|
||||
const {
|
||||
nftActivity: gqlEventsData,
|
||||
hasNext,
|
||||
loadMore,
|
||||
loading,
|
||||
} = useNftActivity(
|
||||
{
|
||||
activityTypes: Object.keys(activeFilters)
|
||||
.map((key) => key as NftActivityType)
|
||||
.filter((key) => activeFilters[key]),
|
||||
address: contractAddress,
|
||||
},
|
||||
25
|
||||
)
|
||||
|
||||
const { events, gatedHasNext, gatedLoadMore, gatedLoading, gatedIsLoadingMore } = {
|
||||
events: isNftGraphqlEnabled
|
||||
? gqlEventsData
|
||||
: isSuccess
|
||||
? eventsData?.pages.map((page) => page.events).flat()
|
||||
: undefined,
|
||||
gatedHasNext: isNftGraphqlEnabled ? hasNext : hasNextPage,
|
||||
gatedLoadMore: isNftGraphqlEnabled ? loadMore : fetchNextPage,
|
||||
gatedLoading: isNftGraphqlEnabled ? loading : isLoading,
|
||||
gatedIsLoadingMore: isNftGraphqlEnabled ? hasNext && gqlEventsData?.length : isFetchingNextPage,
|
||||
}
|
||||
|
||||
const itemsInBag = useBag((state) => state.itemsInBag)
|
||||
const addAssetsToBag = useBag((state) => state.addAssetsToBag)
|
||||
const removeAssetsFromBag = useBag((state) => state.removeAssetsFromBag)
|
||||
@ -147,51 +173,63 @@ export const Activity = ({ contractAddress, rarityVerified, collectionName, chai
|
||||
<Filter eventType={ActivityEventType.Sale} />
|
||||
<Filter eventType={ActivityEventType.Transfer} />
|
||||
</Row>
|
||||
{isLoading && <ActivityLoader />}
|
||||
{events && (
|
||||
<Column marginTop="36">
|
||||
<HeaderRow />
|
||||
<InfiniteScroll
|
||||
next={fetchNextPage}
|
||||
hasMore={!!hasNextPage}
|
||||
loader={isFetchingNextPage ? <ActivityPageLoader rowCount={2} /> : null}
|
||||
dataLength={events?.length ?? 0}
|
||||
style={{ overflow: 'unset' }}
|
||||
>
|
||||
{events.map((event, i) => (
|
||||
<Box as="a" data-testid="nft-activity-row" href={baseHref(event)} className={styles.eventRow} key={i}>
|
||||
<ItemCell
|
||||
event={event}
|
||||
rarityVerified={rarityVerified}
|
||||
collectionName={collectionName}
|
||||
eventTimestamp={event.eventTimestamp}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
<EventCell
|
||||
eventType={event.eventType}
|
||||
eventTimestamp={event.eventTimestamp}
|
||||
eventTransactionHash={event.transactionHash}
|
||||
price={event.price}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
<PriceCell marketplace={event.marketplace} price={event.price} />
|
||||
<AddressCell address={event.fromAddress} chainId={chainId} />
|
||||
<AddressCell address={event.toAddress} chainId={chainId} desktopLBreakpoint />
|
||||
<BuyCell
|
||||
event={event}
|
||||
collectionName={collectionName}
|
||||
selectAsset={addAssetsToBag}
|
||||
removeAsset={removeAssetsFromBag}
|
||||
itemsInBag={itemsInBag}
|
||||
cartExpanded={cartExpanded}
|
||||
toggleCart={toggleCart}
|
||||
isMobile={isMobile}
|
||||
ethPriceInUSD={ethPriceInUSD}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
</InfiniteScroll>
|
||||
</Column>
|
||||
{gatedLoading ? (
|
||||
<ActivityLoader />
|
||||
) : (
|
||||
events && (
|
||||
<Column marginTop="36">
|
||||
<HeaderRow />
|
||||
<InfiniteScroll
|
||||
next={gatedLoadMore}
|
||||
hasMore={!!gatedHasNext}
|
||||
loader={gatedIsLoadingMore ? <ActivityPageLoader rowCount={2} /> : null}
|
||||
dataLength={events?.length ?? 0}
|
||||
style={{ overflow: 'unset' }}
|
||||
>
|
||||
{events.map(
|
||||
(event, i) =>
|
||||
event.eventType && (
|
||||
<Box
|
||||
as="a"
|
||||
data-testid="nft-activity-row"
|
||||
href={baseHref(event)}
|
||||
className={styles.eventRow}
|
||||
key={i}
|
||||
>
|
||||
<ItemCell
|
||||
event={event}
|
||||
rarityVerified={rarityVerified}
|
||||
collectionName={collectionName}
|
||||
eventTimestamp={event.eventTimestamp}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
<EventCell
|
||||
eventType={event.eventType}
|
||||
eventTimestamp={event.eventTimestamp}
|
||||
eventTransactionHash={event.transactionHash}
|
||||
price={event.price}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
<PriceCell marketplace={event.marketplace} price={event.price} />
|
||||
<AddressCell address={event.fromAddress} chainId={chainId} />
|
||||
<AddressCell address={event.toAddress} chainId={chainId} desktopLBreakpoint />
|
||||
<BuyCell
|
||||
event={event}
|
||||
collectionName={collectionName}
|
||||
selectAsset={addAssetsToBag}
|
||||
removeAsset={removeAssetsFromBag}
|
||||
itemsInBag={itemsInBag}
|
||||
cartExpanded={cartExpanded}
|
||||
toggleCart={toggleCart}
|
||||
isMobile={isMobile}
|
||||
ethPriceInUSD={ethPriceInUSD}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
)}
|
||||
</InfiniteScroll>
|
||||
</Column>
|
||||
)
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
|
@ -2,6 +2,8 @@ import { sendAnalyticsEvent, useTrace } from '@uniswap/analytics'
|
||||
import { InterfacePageName, NFTEventName } from '@uniswap/analytics-events'
|
||||
import { ChainId } from '@uniswap/smart-order-router'
|
||||
import { MouseoverTooltip } from 'components/Tooltip'
|
||||
import { useNftGraphqlEnabled } from 'featureFlags/flags/nftlGraphql'
|
||||
import { NftActivityType, OrderStatus } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { Box } from 'nft/components/Box'
|
||||
import { Column, Row } from 'nft/components/Flex'
|
||||
import {
|
||||
@ -14,18 +16,17 @@ import {
|
||||
} from 'nft/components/icons'
|
||||
import {
|
||||
ActivityEvent,
|
||||
ActivityEventType,
|
||||
ActivityEventTypeDisplay,
|
||||
BagItem,
|
||||
GenieAsset,
|
||||
Markets,
|
||||
OrderStatus,
|
||||
Rarity,
|
||||
TokenMetadata,
|
||||
TokenRarity,
|
||||
} from 'nft/types'
|
||||
import { shortenAddress } from 'nft/utils/address'
|
||||
import { buildActivityAsset } from 'nft/utils/buildActivityAsset'
|
||||
import { formatEthPrice } from 'nft/utils/currency'
|
||||
import { formatEth, formatEthPrice } from 'nft/utils/currency'
|
||||
import { getTimeDifference, isValidDate } from 'nft/utils/date'
|
||||
import { putCommas } from 'nft/utils/putCommas'
|
||||
import { fallbackProvider, getRarityProviderLogo } from 'nft/utils/rarity'
|
||||
@ -59,14 +60,16 @@ const AddressLink = styled(ExternalLink)`
|
||||
|
||||
const formatListingStatus = (status: OrderStatus): string => {
|
||||
switch (status) {
|
||||
case OrderStatus.EXECUTED:
|
||||
case OrderStatus.Executed:
|
||||
return 'Sold'
|
||||
case OrderStatus.CANCELLED:
|
||||
case OrderStatus.Cancelled:
|
||||
return 'Cancelled'
|
||||
case OrderStatus.EXPIRED:
|
||||
case OrderStatus.Expired:
|
||||
return 'Expired'
|
||||
case OrderStatus.VALID:
|
||||
case OrderStatus.Valid:
|
||||
return 'Add to Bag'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
@ -93,9 +96,10 @@ export const BuyCell = ({
|
||||
isMobile,
|
||||
ethPriceInUSD,
|
||||
}: BuyCellProps) => {
|
||||
const isNftGraphqlEnabled = useNftGraphqlEnabled()
|
||||
const asset = useMemo(
|
||||
() => buildActivityAsset(event, collectionName, ethPriceInUSD),
|
||||
[event, collectionName, ethPriceInUSD]
|
||||
() => buildActivityAsset(event, collectionName, ethPriceInUSD, isNftGraphqlEnabled),
|
||||
[event, collectionName, ethPriceInUSD, isNftGraphqlEnabled]
|
||||
)
|
||||
const isSelected = useMemo(() => {
|
||||
return itemsInBag.some((item) => asset.tokenId === item.asset.tokenId && asset.address === item.asset.address)
|
||||
@ -112,19 +116,19 @@ export const BuyCell = ({
|
||||
|
||||
return (
|
||||
<Column display={{ sm: 'none', lg: 'flex' }} height="full" justifyContent="center" marginX="auto">
|
||||
{event.eventType === ActivityEventType.Listing && event.orderStatus ? (
|
||||
{event.eventType === NftActivityType.Listing && event.orderStatus ? (
|
||||
<Box
|
||||
as="button"
|
||||
className={event.orderStatus === OrderStatus.VALID && isSelected ? styles.removeCell : styles.buyCell}
|
||||
className={event.orderStatus === OrderStatus.Valid && isSelected ? styles.removeCell : styles.buyCell}
|
||||
onClick={(e: MouseEvent) => {
|
||||
e.preventDefault()
|
||||
isSelected ? removeAsset([asset]) : selectAsset([asset])
|
||||
!isSelected && !cartExpanded && !isMobile && toggleCart()
|
||||
!isSelected && sendAnalyticsEvent(NFTEventName.NFT_BUY_ADDED, { eventProperties })
|
||||
}}
|
||||
disabled={event.orderStatus !== OrderStatus.VALID}
|
||||
disabled={event.orderStatus !== OrderStatus.Valid}
|
||||
>
|
||||
{event.orderStatus === OrderStatus.VALID ? (
|
||||
{event.orderStatus === OrderStatus.Valid ? (
|
||||
<>{`${isSelected ? 'Remove' : 'Add to bag'}`}</>
|
||||
) : (
|
||||
<>{`${formatListingStatus(event.orderStatus)}`}</>
|
||||
@ -159,12 +163,12 @@ export const AddressCell = ({ address, desktopLBreakpoint, chainId }: AddressCel
|
||||
)
|
||||
}
|
||||
|
||||
export const MarketplaceIcon = ({ marketplace }: { marketplace: Markets }) => {
|
||||
export const MarketplaceIcon = ({ marketplace }: { marketplace: Markets | string }) => {
|
||||
return (
|
||||
<Box
|
||||
as="img"
|
||||
alt={marketplace}
|
||||
src={`/nft/svgs/marketplaces/${marketplace}.svg`}
|
||||
src={`/nft/svgs/marketplaces/${marketplace.toLowerCase()}.svg`}
|
||||
className={styles.marketplaceIcon}
|
||||
/>
|
||||
)
|
||||
@ -183,8 +187,17 @@ const PriceTooltip = ({ price }: { price: string }) => (
|
||||
</MouseoverTooltip>
|
||||
)
|
||||
|
||||
export const PriceCell = ({ marketplace, price }: { marketplace?: Markets; price?: string }) => {
|
||||
const formattedPrice = useMemo(() => (price ? putCommas(formatEthPrice(price))?.toString() : null), [price])
|
||||
export const PriceCell = ({ marketplace, price }: { marketplace?: Markets | string; price?: string | number }) => {
|
||||
const isNftGraphqlEnabled = useNftGraphqlEnabled()
|
||||
const formattedPrice = useMemo(
|
||||
() =>
|
||||
price
|
||||
? isNftGraphqlEnabled
|
||||
? formatEth(parseFloat(price?.toString()))
|
||||
: putCommas(formatEthPrice(price.toString()))?.toString()
|
||||
: null,
|
||||
[isNftGraphqlEnabled, price]
|
||||
)
|
||||
|
||||
return (
|
||||
<Row display={{ sm: 'none', md: 'flex' }} gap="8">
|
||||
@ -203,23 +216,23 @@ export const PriceCell = ({ marketplace, price }: { marketplace?: Markets; price
|
||||
}
|
||||
|
||||
interface EventCellProps {
|
||||
eventType: ActivityEventType
|
||||
eventType: NftActivityType
|
||||
eventTimestamp?: number
|
||||
eventTransactionHash?: string
|
||||
eventOnly?: boolean
|
||||
price?: string
|
||||
price?: string | number
|
||||
isMobile?: boolean
|
||||
}
|
||||
|
||||
const renderEventIcon = (eventType: ActivityEventType) => {
|
||||
const renderEventIcon = (eventType: NftActivityType) => {
|
||||
switch (eventType) {
|
||||
case ActivityEventType.Listing:
|
||||
case NftActivityType.Listing:
|
||||
return <ActivityListingIcon width={16} height={16} />
|
||||
case ActivityEventType.Sale:
|
||||
case NftActivityType.Sale:
|
||||
return <ActivitySaleIcon width={16} height={16} />
|
||||
case ActivityEventType.Transfer:
|
||||
case NftActivityType.Transfer:
|
||||
return <ActivityTransferIcon width={16} height={16} />
|
||||
case ActivityEventType.CancelListing:
|
||||
case NftActivityType.CancelListing:
|
||||
return <CancelListingIcon width={16} height={16} />
|
||||
default:
|
||||
return null
|
||||
@ -237,12 +250,12 @@ const ExternalLinkIcon = ({ transactionHash }: { transactionHash: string }) => (
|
||||
</Row>
|
||||
)
|
||||
|
||||
const eventColors = (eventType: ActivityEventType) => {
|
||||
const eventColors = (eventType: NftActivityType) => {
|
||||
const activityEvents = {
|
||||
[ActivityEventType.Listing]: 'gold',
|
||||
[ActivityEventType.Sale]: 'green',
|
||||
[ActivityEventType.Transfer]: 'violet',
|
||||
[ActivityEventType.CancelListing]: 'accentFailure',
|
||||
[NftActivityType.Listing]: 'gold',
|
||||
[NftActivityType.Sale]: 'green',
|
||||
[NftActivityType.Transfer]: 'violet',
|
||||
[NftActivityType.CancelListing]: 'accentFailure',
|
||||
}
|
||||
|
||||
return activityEvents[eventType] as 'gold' | 'green' | 'violet' | 'accentFailure'
|
||||
@ -256,16 +269,25 @@ export const EventCell = ({
|
||||
price,
|
||||
isMobile,
|
||||
}: EventCellProps) => {
|
||||
const formattedPrice = useMemo(() => (price ? putCommas(formatEthPrice(price))?.toString() : null), [price])
|
||||
const isNftGraphqlEnabled = useNftGraphqlEnabled()
|
||||
const formattedPrice = useMemo(
|
||||
() =>
|
||||
price
|
||||
? isNftGraphqlEnabled
|
||||
? formatEth(parseFloat(price?.toString()))
|
||||
: putCommas(formatEthPrice(price.toString()))?.toString()
|
||||
: null,
|
||||
[isNftGraphqlEnabled, price]
|
||||
)
|
||||
return (
|
||||
<Column height="full" justifyContent="center" gap="4">
|
||||
<Row className={styles.eventDetail} color={eventColors(eventType)}>
|
||||
{renderEventIcon(eventType)}
|
||||
{ActivityEventTypeDisplay[eventType]}
|
||||
</Row>
|
||||
{eventTimestamp && isValidDate(eventTimestamp) && !isMobile && !eventOnly && (
|
||||
{eventTimestamp && (isValidDate(eventTimestamp) || isNftGraphqlEnabled) && !isMobile && !eventOnly && (
|
||||
<Row className={styles.eventTime}>
|
||||
{getTimeDifference(eventTimestamp.toString())}
|
||||
{getTimeDifference(eventTimestamp.toString(), isNftGraphqlEnabled)}
|
||||
{eventTransactionHash && <ExternalLinkIcon transactionHash={eventTransactionHash} />}
|
||||
</Row>
|
||||
)}
|
||||
@ -310,14 +332,18 @@ const NoContentContainer = () => (
|
||||
)
|
||||
|
||||
interface RankingProps {
|
||||
rarity: TokenRarity
|
||||
rarity: TokenRarity | Rarity
|
||||
collectionName: string
|
||||
rarityVerified: boolean
|
||||
details?: boolean
|
||||
}
|
||||
|
||||
const Ranking = ({ rarity, collectionName, rarityVerified }: RankingProps) => {
|
||||
const rarityProviderLogo = getRarityProviderLogo(rarity.source)
|
||||
const source = (rarity as TokenRarity).source || (rarity as Rarity).primaryProvider
|
||||
const rank = (rarity as TokenRarity).rank || (rarity as Rarity).providers?.[0].rank
|
||||
const rarityProviderLogo = getRarityProviderLogo(source)
|
||||
|
||||
if (!rank) return null
|
||||
|
||||
return (
|
||||
<Box>
|
||||
@ -330,7 +356,7 @@ const Ranking = ({ rarity, collectionName, rarityVerified }: RankingProps) => {
|
||||
<Box width="full" fontSize="14">
|
||||
{rarityVerified
|
||||
? `Verified by ${collectionName}`
|
||||
: `Ranking by ${rarity.source === 'Genie' ? fallbackProvider : rarity.source}`}
|
||||
: `Ranking by ${source === 'Genie' ? fallbackProvider : source}`}
|
||||
</Box>
|
||||
</Row>
|
||||
}
|
||||
@ -338,7 +364,7 @@ const Ranking = ({ rarity, collectionName, rarityVerified }: RankingProps) => {
|
||||
>
|
||||
<Box className={styles.rarityInfo}>
|
||||
<Box paddingTop="2" paddingBottom="2" display="flex">
|
||||
{putCommas(rarity.rank)}
|
||||
{putCommas(rank)}
|
||||
</Box>
|
||||
|
||||
<Box display="flex" height="16">
|
||||
@ -357,6 +383,7 @@ const getItemImage = (tokenMetadata?: TokenMetadata): string | undefined => {
|
||||
export const ItemCell = ({ event, rarityVerified, collectionName, eventTimestamp, isMobile }: ItemCellProps) => {
|
||||
const [loaded, setLoaded] = useState(false)
|
||||
const [noContent, setNoContent] = useState(!getItemImage(event.tokenMetadata))
|
||||
const isNftGraphqlEnabled = useNftGraphqlEnabled()
|
||||
|
||||
return (
|
||||
<Row gap="16" overflow="hidden" whiteSpace="nowrap">
|
||||
@ -385,7 +412,10 @@ export const ItemCell = ({ event, rarityVerified, collectionName, eventTimestamp
|
||||
collectionName={collectionName}
|
||||
/>
|
||||
)}
|
||||
{isMobile && eventTimestamp && isValidDate(eventTimestamp) && getTimeDifference(eventTimestamp.toString())}
|
||||
{isMobile &&
|
||||
eventTimestamp &&
|
||||
(isValidDate(eventTimestamp) || isNftGraphqlEnabled) &&
|
||||
getTimeDifference(eventTimestamp.toString(), isNftGraphqlEnabled)}
|
||||
</Column>
|
||||
</Row>
|
||||
)
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { OpacityHoverState, ScrollBarStyles } from 'components/Common'
|
||||
import { LoadingBubble } from 'components/Tokens/loading'
|
||||
import { useNftGraphqlEnabled } from 'featureFlags/flags/nftlGraphql'
|
||||
import { EventCell, MarketplaceIcon } from 'nft/components/collection/ActivityCells'
|
||||
import { ActivityEventResponse } from 'nft/types'
|
||||
import { ActivityEvent } from 'nft/types'
|
||||
import { shortenAddress } from 'nft/utils/address'
|
||||
import { formatEthPrice } from 'nft/utils/currency'
|
||||
import { formatEth, formatEthPrice } from 'nft/utils/currency'
|
||||
import { getTimeDifference } from 'nft/utils/date'
|
||||
import { putCommas } from 'nft/utils/putCommas'
|
||||
import { ReactNode } from 'react'
|
||||
@ -147,14 +148,19 @@ export const LoadingAssetActivity = ({ rowCount }: { rowCount: number }) => {
|
||||
)
|
||||
}
|
||||
|
||||
const AssetActivity = ({ eventsData }: { eventsData: ActivityEventResponse | undefined }) => {
|
||||
const AssetActivity = ({ events }: { events: ActivityEvent[] | undefined }) => {
|
||||
const isNftGraphqlEnabled = useNftGraphqlEnabled()
|
||||
return (
|
||||
<ActivityTable>
|
||||
{eventsData?.events &&
|
||||
eventsData.events.map((event, index) => {
|
||||
{events &&
|
||||
events.map((event, index) => {
|
||||
const { eventTimestamp, eventType, fromAddress, marketplace, price, toAddress, transactionHash } = event
|
||||
const formattedPrice = price ? putCommas(formatEthPrice(price)).toString() : null
|
||||
|
||||
const formattedPrice = price
|
||||
? isNftGraphqlEnabled
|
||||
? formatEth(parseFloat(price ?? ''))
|
||||
: putCommas(formatEthPrice(price)).toString()
|
||||
: null
|
||||
if (!eventType) return null
|
||||
return (
|
||||
<TR key={index}>
|
||||
<TD>
|
||||
@ -189,7 +195,7 @@ const AssetActivity = ({ eventsData }: { eventsData: ActivityEventResponse | und
|
||||
</Link>
|
||||
)}
|
||||
</TD>
|
||||
<TD>{eventTimestamp && getTimeDifference(eventTimestamp.toString())}</TD>
|
||||
<TD>{eventTimestamp && getTimeDifference(eventTimestamp.toString(), isNftGraphqlEnabled)}</TD>
|
||||
</TR>
|
||||
)
|
||||
})}
|
||||
|
@ -1,6 +1,9 @@
|
||||
import { OpacityHoverState, ScrollBarStyles } from 'components/Common'
|
||||
import Resource from 'components/Tokens/TokenDetails/Resource'
|
||||
import { MouseoverTooltip } from 'components/Tooltip/index'
|
||||
import { useNftGraphqlEnabled } from 'featureFlags/flags/nftlGraphql'
|
||||
import { NftActivityType } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { useNftActivity } from 'graphql/data/nft/NftActivity'
|
||||
import { Box } from 'nft/components/Box'
|
||||
import { reduceFilters } from 'nft/components/collection/Activity'
|
||||
import { LoadingSparkle } from 'nft/components/common/Loading/LoadingSparkle'
|
||||
@ -10,7 +13,7 @@ import { themeVars, vars } from 'nft/css/sprinkles.css'
|
||||
import { ActivityFetcher } from 'nft/queries/genie/ActivityFetcher'
|
||||
import { ActivityEventResponse, ActivityEventType, CollectionInfoForAsset, GenieAsset } from 'nft/types'
|
||||
import { shortenAddress } from 'nft/utils/address'
|
||||
import { formatEthPrice } from 'nft/utils/currency'
|
||||
import { formatEth, formatEthPrice } from 'nft/utils/currency'
|
||||
import { isAudio } from 'nft/utils/isAudio'
|
||||
import { isVideo } from 'nft/utils/isVideo'
|
||||
import { putCommas } from 'nft/utils/putCommas'
|
||||
@ -244,6 +247,7 @@ interface AssetDetailsProps {
|
||||
}
|
||||
|
||||
export const AssetDetails = ({ asset, collection }: AssetDetailsProps) => {
|
||||
const isNftGraphqlEnabled = useNftGraphqlEnabled()
|
||||
const [dominantColor] = useState<[number, number, number]>([0, 0, 0])
|
||||
|
||||
const { rarityProvider } = useMemo(
|
||||
@ -299,10 +303,26 @@ export const AssetDetails = ({ asset, collection }: AssetDetailsProps) => {
|
||||
refetchOnMount: false,
|
||||
}
|
||||
)
|
||||
const { nftActivity: gqlPriceData } = useNftActivity(
|
||||
{
|
||||
activityTypes: [NftActivityType.Sale],
|
||||
address: contractAddress,
|
||||
tokenId: token_id,
|
||||
},
|
||||
1,
|
||||
'no-cache'
|
||||
)
|
||||
|
||||
const lastSalePrice = priceData?.events[0]?.price ?? null
|
||||
const formattedEthprice = formatEthPrice(lastSalePrice ?? '') || 0
|
||||
const formattedPrice = lastSalePrice ? putCommas(formattedEthprice).toString() : null
|
||||
// TODO simplify typecasting when removing graphql flag
|
||||
const lastSalePrice = isNftGraphqlEnabled ? gqlPriceData?.[0]?.price : priceData?.events[0]?.price
|
||||
const formattedEthprice = isNftGraphqlEnabled
|
||||
? formatEth(parseFloat(lastSalePrice ?? ''))
|
||||
: formatEthPrice(lastSalePrice) || 0
|
||||
const formattedPrice = isNftGraphqlEnabled
|
||||
? formattedEthprice
|
||||
: lastSalePrice
|
||||
? putCommas(parseFloat(formattedEthprice.toString())).toString()
|
||||
: null
|
||||
const [activeFilters, filtersDispatch] = useReducer(reduceFilters, initialFilterState)
|
||||
|
||||
const Filter = useCallback(
|
||||
@ -365,13 +385,48 @@ export const AssetDetails = ({ asset, collection }: AssetDetailsProps) => {
|
||||
}
|
||||
)
|
||||
|
||||
const rarity = asset?.rarity?.providers?.length ? asset?.rarity?.providers?.[0] : undefined
|
||||
const {
|
||||
nftActivity: gqlEventsData,
|
||||
hasNext,
|
||||
loadMore,
|
||||
loading,
|
||||
error,
|
||||
} = useNftActivity(
|
||||
{
|
||||
activityTypes: Object.keys(activeFilters)
|
||||
.map((key) => key as NftActivityType)
|
||||
.filter((key) => activeFilters[key]),
|
||||
address: contractAddress,
|
||||
tokenId: token_id,
|
||||
},
|
||||
25
|
||||
)
|
||||
|
||||
const { events, gatedHasNext, gatedLoadMore, gatedLoading, gatedSuccess } = useMemo(() => {
|
||||
return {
|
||||
events: isNftGraphqlEnabled ? gqlEventsData : eventsData?.pages.map((page) => page.events).flat(),
|
||||
gatedHasNext: isNftGraphqlEnabled ? hasNext : hasNextPage,
|
||||
gatedLoadMore: isNftGraphqlEnabled ? loadMore : fetchNextPage,
|
||||
gatedLoading: isNftGraphqlEnabled ? loading : isActivityLoading,
|
||||
gatedSuccess: isNftGraphqlEnabled ? !error : isSuccess,
|
||||
}
|
||||
}, [
|
||||
error,
|
||||
eventsData?.pages,
|
||||
fetchNextPage,
|
||||
gqlEventsData,
|
||||
hasNext,
|
||||
hasNextPage,
|
||||
isActivityLoading,
|
||||
isNftGraphqlEnabled,
|
||||
isSuccess,
|
||||
loadMore,
|
||||
loading,
|
||||
])
|
||||
|
||||
const rarity = asset?.rarity?.providers?.[0]
|
||||
const [showHolder, setShowHolder] = useState(false)
|
||||
const rarityProviderLogo = getRarityProviderLogo(rarity?.provider)
|
||||
const events = useMemo(
|
||||
() => (isSuccess ? eventsData?.pages.map((page) => page.events).flat() : null),
|
||||
[isSuccess, eventsData]
|
||||
)
|
||||
|
||||
return (
|
||||
<Column>
|
||||
@ -433,11 +488,12 @@ export const AssetDetails = ({ asset, collection }: AssetDetailsProps) => {
|
||||
<Filter eventType={ActivityEventType.Transfer} />
|
||||
<Filter eventType={ActivityEventType.CancelListing} />
|
||||
</ActivitySelectContainer>
|
||||
{isActivityLoading && <LoadingAssetActivity rowCount={10} />}
|
||||
{events && events.length > 0 ? (
|
||||
{gatedLoading ? (
|
||||
<LoadingAssetActivity rowCount={10} />
|
||||
) : events && events.length > 0 ? (
|
||||
<InfiniteScroll
|
||||
next={fetchNextPage}
|
||||
hasMore={!!hasNextPage}
|
||||
next={gatedLoadMore}
|
||||
hasMore={!!gatedHasNext}
|
||||
loader={
|
||||
isFetchingNextPage && (
|
||||
<Center>
|
||||
@ -448,11 +504,11 @@ export const AssetDetails = ({ asset, collection }: AssetDetailsProps) => {
|
||||
dataLength={events?.length ?? 0}
|
||||
scrollableTarget="activityContainer"
|
||||
>
|
||||
<AssetActivity eventsData={{ events }} />
|
||||
<AssetActivity events={events} />
|
||||
</InfiniteScroll>
|
||||
) : (
|
||||
<>
|
||||
{!isActivityLoading && (
|
||||
{gatedSuccess && events && (
|
||||
<EmptyActivitiesContainer>
|
||||
<div>No activities yet</div>
|
||||
<Link to={`/nfts/collection/${asset.address}`}>View collection items</Link>{' '}
|
||||
|
@ -1,4 +1,6 @@
|
||||
import { Markets, TokenType } from '../common'
|
||||
import { NftActivityType, NftStandard, OrderStatus } from 'graphql/data/__generated__/types-and-hooks'
|
||||
|
||||
import { Markets, Rarity, TokenType } from '../common'
|
||||
export interface AssetPayload {
|
||||
filters: {
|
||||
traits?: Record<string, string[]>
|
||||
@ -57,13 +59,6 @@ export enum ActivityEventTypeDisplay {
|
||||
'CANCEL_LISTING' = 'Cancellation',
|
||||
}
|
||||
|
||||
export enum OrderStatus {
|
||||
VALID = 'VALID',
|
||||
EXECUTED = 'EXECUTED',
|
||||
CANCELLED = 'CANCELLED',
|
||||
EXPIRED = 'EXPIRED',
|
||||
}
|
||||
|
||||
export interface ActivityFilter {
|
||||
collectionAddress?: string
|
||||
eventTypes?: ActivityEventType[]
|
||||
@ -83,31 +78,29 @@ export interface TokenRarity {
|
||||
}
|
||||
|
||||
export interface TokenMetadata {
|
||||
name: string
|
||||
imageUrl: string
|
||||
smallImageUrl: string
|
||||
metadataUrl: string
|
||||
rarity: TokenRarity
|
||||
suspiciousFlag: boolean
|
||||
suspiciousFlaggedBy: string
|
||||
standard: TokenType
|
||||
name?: string
|
||||
imageUrl?: string
|
||||
smallImageUrl?: string
|
||||
metadataUrl?: string
|
||||
rarity?: TokenRarity | Rarity
|
||||
suspiciousFlag?: boolean
|
||||
standard?: TokenType | NftStandard
|
||||
}
|
||||
|
||||
// TODO when deprecating activity query, remove all outdated types (former in optional fields)
|
||||
export interface ActivityEvent {
|
||||
collectionAddress: string
|
||||
collectionAddress?: string
|
||||
tokenId?: string
|
||||
tokenMetadata?: TokenMetadata
|
||||
eventType: ActivityEventType
|
||||
marketplace?: Markets
|
||||
fromAddress: string
|
||||
eventType?: NftActivityType
|
||||
marketplace?: Markets | string
|
||||
fromAddress?: string
|
||||
toAddress?: string
|
||||
transactionHash?: string
|
||||
orderHash?: string
|
||||
orderStatus?: OrderStatus
|
||||
price?: string
|
||||
symbol?: string
|
||||
quantity?: number
|
||||
auctionType?: string
|
||||
url?: string
|
||||
eventTimestamp?: number
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ import { isAddress } from '@ethersproject/address'
|
||||
*/
|
||||
export function shortenAddress(address: string, charsStart = 4, charsEnd?: number): string {
|
||||
const parsed = isAddress(address)
|
||||
if (!parsed) throw Error(`Invalid 'address' parameter '${address}'.`)
|
||||
if (!parsed) return ''
|
||||
|
||||
return `${address.substring(0, charsStart + 2)}...${address.substring(42 - (charsEnd || charsStart))}`
|
||||
}
|
||||
|
@ -1,16 +1,28 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { formatEther } from '@ethersproject/units'
|
||||
import { parseEther } from 'ethers/lib/utils'
|
||||
import { ActivityEvent, GenieAsset } from 'nft/types'
|
||||
|
||||
export const buildActivityAsset = (event: ActivityEvent, collectionName: string, ethPriceInUSD: number): GenieAsset => {
|
||||
import { formatEth } from './currency'
|
||||
|
||||
export const buildActivityAsset = (
|
||||
event: ActivityEvent,
|
||||
collectionName: string,
|
||||
ethPriceInUSD: number,
|
||||
isNftGraphqlEnabled: boolean
|
||||
): GenieAsset => {
|
||||
const assetUsdPrice = event.price
|
||||
? formatEther(
|
||||
BigNumber.from(event.price)
|
||||
.mul(BigNumber.from(Math.trunc(ethPriceInUSD * 100)))
|
||||
.div(100)
|
||||
)
|
||||
? isNftGraphqlEnabled
|
||||
? formatEth(parseFloat(event.price) * ethPriceInUSD)
|
||||
: formatEther(
|
||||
BigNumber.from(event.price)
|
||||
.mul(BigNumber.from(Math.trunc(ethPriceInUSD * 100)))
|
||||
.div(100)
|
||||
)
|
||||
: '0'
|
||||
|
||||
const weiPrice = isNftGraphqlEnabled ? (event.price ? parseEther(event.price) : '') : event.price
|
||||
|
||||
return {
|
||||
address: event.collectionAddress,
|
||||
collectionName,
|
||||
@ -23,8 +35,8 @@ export const buildActivityAsset = (event: ActivityEvent, collectionName: string,
|
||||
collectionSymbol: event.symbol,
|
||||
priceInfo: {
|
||||
USDPrice: assetUsdPrice,
|
||||
ETHPrice: event.price,
|
||||
basePrice: event.price,
|
||||
ETHPrice: weiPrice,
|
||||
basePrice: weiPrice,
|
||||
baseAsset: 'ETH',
|
||||
},
|
||||
tokenType: event.tokenMetadata?.standard,
|
||||
|
@ -3,8 +3,8 @@ export const isValidDate = (date: number): boolean => {
|
||||
return isNaN(d) ? false : true
|
||||
}
|
||||
|
||||
export const getTimeDifference = (eventTimestamp: string) => {
|
||||
const date = new Date(eventTimestamp).getTime()
|
||||
export const getTimeDifference = (eventTimestamp: string, isNftGraphqlEnabled: boolean) => {
|
||||
const date = isNftGraphqlEnabled ? parseFloat(eventTimestamp) : new Date(eventTimestamp).getTime()
|
||||
const diff = new Date().getTime() - date
|
||||
|
||||
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
|
||||
|
Loading…
Reference in New Issue
Block a user