Commit 18939aa8 authored by Charles Bachmeier's avatar Charles Bachmeier Committed by GitHub

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