Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement NFTs V2 Arc endpoint #5973

Merged
merged 14 commits into from
Aug 15, 2024
104 changes: 104 additions & 0 deletions src/components/asset-list/RecyclerAssetList2/NFTEmptyState.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import React, { useCallback } from 'react';
import Animated from 'react-native-reanimated';
import { Box, Stack, Text, useColorMode } from '@/design-system';
import * as i18n from '@/languages';
import { TokenFamilyHeaderHeight } from './NFTLoadingSkeleton';
import { MINTS, useExperimentalFlag } from '@/config';
import { useRemoteConfig } from '@/model/remoteConfig';
import { IS_TEST } from '@/env';
import { useMints } from '@/resources/mints';
import { useAccountSettings } from '@/hooks';
import { GestureHandlerButton } from '@/__swaps__/screens/Swap/components/GestureHandlerButton';
import { StyleSheet } from 'react-native';
import { LIGHT_SEPARATOR_COLOR, SEPARATOR_COLOR } from '@/__swaps__/screens/Swap/constants';
import { analyticsV2 } from '@/analytics';
import { convertRawAmountToRoundedDecimal } from '@/helpers/utilities';
import { ethereumUtils } from '@/utils';
import { navigateToMintCollection } from '@/resources/reservoir/mints';

type LaunchFeaturedMintButtonProps = {
featuredMint: ReturnType<typeof useMints>['data']['featuredMint'];
};

const LaunchFeaturedMintButton = ({ featuredMint }: LaunchFeaturedMintButtonProps) => {
const { isDarkMode } = useColorMode();

const handlePress = useCallback(() => {
if (featuredMint) {
analyticsV2.track(analyticsV2.event.mintsPressedFeaturedMintCard, {
contractAddress: featuredMint.contractAddress,
chainId: featuredMint.chainId,
totalMints: featuredMint.totalMints,
mintsLastHour: featuredMint.totalMints,
priceInEth: convertRawAmountToRoundedDecimal(featuredMint.mintStatus.price, 18, 6),
});
const network = ethereumUtils.getNetworkFromChainId(featuredMint.chainId);
navigateToMintCollection(featuredMint.contract, featuredMint.mintStatus.price, network);
}
}, [featuredMint]);

return (
<Box style={{ alignItems: 'center', paddingTop: 12 }}>
<GestureHandlerButton onPressJS={handlePress} scaleTo={0.9}>
<Box as={Animated.View} alignItems="center" justifyContent="center" style={styles.buttonPadding}>
<Box
alignItems="center"
as={Animated.View}
borderRadius={15}
justifyContent="center"
paddingHorizontal="10px"
style={[{ backgroundColor: isDarkMode ? SEPARATOR_COLOR : LIGHT_SEPARATOR_COLOR }]}
>
<Text size="13pt" color={'label'} style={{ opacity: isDarkMode ? 0.6 : 0.75 }} weight="heavy">
{i18n.t(i18n.l.nfts.collect_now)}
</Text>
</Box>
</Box>
</GestureHandlerButton>
</Box>
);
};

export function NFTEmptyState() {
const { mints_enabled } = useRemoteConfig();
const { accountAddress } = useAccountSettings();

const {
data: { featuredMint },
} = useMints({ walletAddress: accountAddress });

const mintsEnabled = (useExperimentalFlag(MINTS) || mints_enabled) && !IS_TEST;

return (
<Box
alignItems="center"
as={Animated.View}
style={[{ alignSelf: 'center', flexDirection: 'row', height: TokenFamilyHeaderHeight * 5 }]}
>
<Box paddingHorizontal="44px">
<Stack space="16px">
<Text containsEmoji color="label" size="26pt" weight="bold" align="center">
🌟
</Text>

<Text color="labelTertiary" size="20pt" weight="semibold" align="center">
{i18n.t(i18n.l.nfts.empty)}
</Text>

<Text color="labelQuaternary" size="14px / 19px (Deprecated)" weight="regular" align="center">
{i18n.t(i18n.l.nfts.will_appear_here)}
</Text>

{mintsEnabled && featuredMint && <LaunchFeaturedMintButton featuredMint={featuredMint} />}
</Stack>
</Box>
</Box>
);
}

const styles = StyleSheet.create({
buttonPadding: {
paddingHorizontal: 24,
paddingVertical: 12,
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';
import { useForegroundColor } from '@/design-system';
import { useTheme } from '@/theme';
import { opacity } from '@/__swaps__/utils/swaps';
import { deviceUtils } from '@/utils';

export const TokenFamilyHeaderHeight = 50;

const getRandomBetween = (min: number, max: number) => {
return Math.random() * (max - min) + min;
};

const NFTItem = () => {
const { colors } = useTheme();
const labelTertiary = useForegroundColor('labelTertiary');

return (
<View
style={[
sx.content,
{
backgroundColor: colors.white,
},
]}
>
<View
style={[
sx.image,
{
backgroundColor: opacity(labelTertiary, 0.04),
},
]}
/>
<View style={sx.textContainer}>
<View
style={[
sx.title,
{ backgroundColor: opacity(labelTertiary, 0.08), width: deviceUtils.dimensions.width / getRandomBetween(1.8, 3) },
]}
/>
</View>
</View>
);
};

const NFTLoadingSkeleton = ({ items = 5 }) => {
return (
<View style={sx.container}>
{[...Array(items)].map((_, index) => (
<NFTItem key={index} />
))}
</View>
);
};

const sx = StyleSheet.create({
container: {
flex: 1,
width: '100%',
gap: 5,
},
image: {
height: 30,
width: 30,
borderRadius: 15,
marginRight: 12,
},
textContainer: {
justifyContent: 'center',
},
title: {
width: deviceUtils.dimensions.width / 2,
height: 14,
borderRadius: 7,
paddingRight: 9,
},
content: {
flexDirection: 'row',
alignItems: 'center',
height: TokenFamilyHeaderHeight,
padding: 19,
width: '100%',
},
});

export default NFTLoadingSkeleton;
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,31 @@ import React from 'react';
import { Box, Inline, Text } from '@/design-system';
import * as i18n from '@/languages';
import { ListHeaderMenu } from '@/components/list/ListHeaderMenu';
import useNftSort, { CollectibleSortByOptions } from '@/hooks/useNFTsSortBy';
import { NftCollectionSortCriterion } from '@/graphql/__generated__/arc';
import useNftSort from '@/hooks/useNFTsSortBy';

const TokenFamilyHeaderHeight = 48;

const getIconForSortType = (selected: string) => {
const getIconForSortType = (selected: NftCollectionSortCriterion) => {
switch (selected) {
case CollectibleSortByOptions.ABC:
case NftCollectionSortCriterion.Abc:
return '􀋲';
case CollectibleSortByOptions.FLOOR_PRICE:
case NftCollectionSortCriterion.FloorPrice:
return '􀅺';
case CollectibleSortByOptions.MOST_RECENT:
case NftCollectionSortCriterion.MostRecent:
return '􀐫';
}
return '';
};

const getMenuItemIcon = (value: CollectibleSortByOptions) => {
const getMenuItemIcon = (value: NftCollectionSortCriterion) => {
switch (value) {
case CollectibleSortByOptions.ABC:
case NftCollectionSortCriterion.Abc:
return 'list.bullet';
case CollectibleSortByOptions.FLOOR_PRICE:
case NftCollectionSortCriterion.FloorPrice:
return 'plus.forwardslash.minus';
case CollectibleSortByOptions.MOST_RECENT:
case NftCollectionSortCriterion.MostRecent:
return 'clock';
}
return '';
};

const CollectiblesHeader = () => {
Expand All @@ -48,13 +47,13 @@ const CollectiblesHeader = () => {

<ListHeaderMenu
selected={nftSort}
menuItems={Object.entries(CollectibleSortByOptions).map(([key, value]) => ({
menuItems={Object.entries(NftCollectionSortCriterion).map(([key, value]) => ({
actionKey: value,
actionTitle: i18n.t(i18n.l.nfts.sort[value]),
icon: { iconType: 'SYSTEM', iconValue: getMenuItemIcon(value) },
menuState: nftSort === key ? 'on' : 'off',
}))}
selectItem={string => updateNFTSort(string as CollectibleSortByOptions)}
selectItem={string => updateNFTSort(string as NftCollectionSortCriterion)}
icon={getIconForSortType(nftSort)}
text={i18n.t(i18n.l.nfts.sort[nftSort])}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,10 @@ import { DiscoverMoreButton } from './DiscoverMoreButton';
import { RotatingLearnCard } from '@/components/cards/RotatingLearnCard';
import WrappedPosition from '../WrappedPosition';
import WrappedPositionsListHeader from '../WrappedPositionsListHeader';
import * as lang from '@/languages';
import { RemoteCardCarousel } from '@/components/cards/remote-cards';
import WrappedCollectiblesHeader from '../WrappedCollectiblesHeader';
import NFTLoadingSkeleton from '../NFTLoadingSkeleton';
import { NFTEmptyState } from '../NFTEmptyState';

function rowRenderer(type: CellType, { uid }: { uid: string }, _: unknown, extendedState: ExtendedState) {
const data = extendedState.additionalData[uid];
Expand Down Expand Up @@ -123,8 +124,13 @@ function rowRenderer(type: CellType, { uid }: { uid: string }, _: unknown, exten
);
case CellType.NFTS_HEADER:
return <WrappedCollectiblesHeader />;
case CellType.NFTS_LOADING:
return <NFTLoadingSkeleton />;
case CellType.NFTS_EMPTY:
return <NFTEmptyState />;
case CellType.FAMILY_HEADER: {
const { name, image, total } = data as NFTFamilyExtraData;

return (
<WrappedTokenFamilyHeader
image={image}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,15 @@ const ViewDimensions: Record<CellType, Dim> = {
[CellType.FAMILY_HEADER]: { height: TokenFamilyHeaderHeight },
[CellType.NFT]: {
// @ts-expect-error
height: UniqueTokenRow.cardSize + UniqueTokenRow.cardMargin,
height: UniqueTokenRow.height,
width: deviceUtils.dimensions.width / 2 - 0.1,
},
[CellType.NFTS_LOADING]: {
height: TokenFamilyHeaderHeight * 5,
},
[CellType.NFTS_EMPTY]: {
height: TokenFamilyHeaderHeight * 5,
},
[CellType.NFT_SPACE_AFTER]: { height: 5 },
[CellType.LOADING_ASSETS]: { height: AssetListItemSkeletonHeight },
[CellType.POSITIONS_HEADER]: { height: AssetListHeaderHeight },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export enum CellType {
PROFILE_NAME_ROW_SPACE_AFTER = 'PROFILE_NAME_ROW_SPACE_AFTER',
PROFILE_STICKY_HEADER = 'PROFILE_STICKY_HEADER',
NFTS_HEADER = 'NFTS_HEADER',
NFTS_LOADING = 'NFTS_LOADING',
NFTS_EMPTY = 'NFTS_EMPTY',
NFTS_HEADER_SPACE_BEFORE = 'NFTS_HEADER_SPACE_BEFORE',
NFTS_HEADER_SPACE_AFTER = 'NFTS_HEADER_SPACE_AFTER',
FAMILY_HEADER = 'FAMILY_HEADER',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { Dimension, Layout, LayoutManager, LayoutProvider } from 'recyclerlistvi
import ViewDimensions from './ViewDimensions';
import { BaseCellType, CellType } from './ViewTypes';
import { deviceUtils } from '@/utils';
import { TrimmedCard } from '@/resources/cards/cardCollectionQuery';

const getStyleOverridesForIndex = (indices: number[]) => (index: number) => {
if (indices.includes(index)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,6 @@ export default function useMemoBriefSectionData({
return false;
}

// removes NFTS_HEADER if wallet doesn't have NFTs
if (data.type === CellType.NFTS_HEADER && !arr[arrIndex + 2]) {
return false;
}

if (data.type === CellType.PROFILE_STICKY_HEADER) {
stickyHeaders.push(index);
}
Expand Down
4 changes: 2 additions & 2 deletions src/components/list/ListHeaderMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import ContextMenuButton from '@/components/native-context-menu/contextMenu';
import { ButtonPressAnimation } from '@/components/animations';
import { Bleed, Box, Inline, Text, useForegroundColor } from '@/design-system';
import { haptics } from '@/utils';
import { CollectibleSortByOptions } from '@/hooks/useNFTsSortBy';
import { NftCollectionSortCriterion } from '@/graphql/__generated__/arc';

type MenuItem = {
actionKey: string;
Expand All @@ -12,7 +12,7 @@ type MenuItem = {
};

type ListHeaderMenuProps = {
selected: CollectibleSortByOptions;
selected: NftCollectionSortCriterion;
menuItems: MenuItem[];
selectItem: (item: string) => void;
icon: string;
Expand Down
7 changes: 7 additions & 0 deletions src/components/skeleton/Skeleton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ export const FakeAvatar = styled.View({
borderRadius: 20,
});

// @ts-expect-error Property 'View' does not exist on type...
export const FakeNFT = styled.View({
...position.sizeAsObject(32),
backgroundColor: ({ theme: { colors }, color }: FakeItemProps) => color ?? colors.skeleton,
borderRadius: 16,
});

export const FakeRow = styled(Row).attrs({
align: 'flex-end',
flex: 0,
Expand Down
4 changes: 2 additions & 2 deletions src/graphql/queries/arc.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -298,8 +298,8 @@ fragment simpleHashPaymentToken on SimpleHashPaymentToken {
decimals
}

query getNFTs($walletAddress: String!) {
nfts(walletAddress: $walletAddress) {
query getNFTs($walletAddress: String!, $sortBy: NFTCollectionSortCriterion, $sortDirection: SortDirection) {
nftsV2(walletAddress: $walletAddress, sortBy: $sortBy, sortDirection: $sortDirection) {
nft_id
chain
contract_address
Expand Down
Loading
Loading