From 8cf0ab114ac6393526c57da386fd96a8dfb1924c Mon Sep 17 00:00:00 2001 From: Christopher Howard Date: Tue, 20 Feb 2024 12:20:30 -0500 Subject: [PATCH] [BX-1292] NFT Menus Refactor + Keyboard Shortcuts (#1334) Co-authored-by: Daniel Sinclair --- e2e/serial/send/3_nft-sendFlow.test.ts | 130 +++++++++++++++++ src/core/references/shortcuts.ts | 16 +++ src/entries/popup/hooks/useHomeShortcuts.ts | 15 +- .../popup/hooks/useKeyboardAnalytics.ts | 4 + src/entries/popup/hooks/useNftShortcuts.tsx | 94 +++++++++++++ .../popup/pages/home/NFTs/NFTContextMenu.tsx | 130 +++++++++++------ .../popup/pages/home/NFTs/NFTDetails.tsx | 23 ++- .../popup/pages/home/NFTs/NFTDropdownMenu.tsx | 133 ++++++++++++------ src/entries/popup/pages/home/NFTs/NFTs.tsx | 24 +++- static/json/languages/en_US.json | 4 +- 10 files changed, 475 insertions(+), 98 deletions(-) create mode 100644 e2e/serial/send/3_nft-sendFlow.test.ts create mode 100644 src/entries/popup/hooks/useNftShortcuts.tsx diff --git a/e2e/serial/send/3_nft-sendFlow.test.ts b/e2e/serial/send/3_nft-sendFlow.test.ts new file mode 100644 index 0000000000..e0cf6443d6 --- /dev/null +++ b/e2e/serial/send/3_nft-sendFlow.test.ts @@ -0,0 +1,130 @@ +import 'chromedriver'; +import 'geckodriver'; +import { WebDriver } from 'selenium-webdriver'; +import { afterAll, afterEach, beforeAll, beforeEach, expect, it } from 'vitest'; + +import { + delayTime, + doNotFindElementByTestId, + findElementById, + findElementByIdAndClick, + findElementByTestId, + findElementByTestIdAndClick, + findElementByText, + findElementByTextAndClick, + getExtensionIdByName, + getRootUrl, + goToPopup, + importWalletFlow, + initDriverWithOptions, + querySelector, + shortenAddress, + takeScreenshotOnFailure, + transactionStatus, + waitAndClick, +} from '../../helpers'; +import { TEST_VARIABLES } from '../../walletVariables'; + +let rootURL = getRootUrl(); +let driver: WebDriver; + +const browser = process.env.BROWSER || 'chrome'; +const os = process.env.OS || 'mac'; + +beforeAll(async () => { + driver = await initDriverWithOptions({ + browser, + os, + }); + const extensionId = await getExtensionIdByName(driver, 'Rainbow'); + if (!extensionId) throw new Error('Extension not found'); + rootURL += extensionId; +}); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +beforeEach(async (context: any) => { + context.driver = driver; +}); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +afterEach(async (context: any) => { + await takeScreenshotOnFailure(context); +}); + +afterAll(() => driver.quit()); + +it('should be able import a wallet via pk', async () => { + await importWalletFlow(driver, rootURL, TEST_VARIABLES.SEED_WALLET.PK); +}); + +it('should be able import a second wallet via pk then switch back to wallet 1', async () => { + await importWalletFlow( + driver, + rootURL, + TEST_VARIABLES.PRIVATE_KEY_WALLET.SECRET, + true, + ); + await findElementByIdAndClick({ id: 'header-account-name-shuffle', driver }); + await findElementByTestIdAndClick({ id: 'wallet-account-1', driver }); + const accountName = await findElementById({ + id: 'header-account-name-shuffle', + driver, + }); + expect(await accountName.getText()).toBe( + shortenAddress(TEST_VARIABLES.SEED_WALLET.ADDRESS), + ); +}); + +it('should be able to go to setings', async () => { + await goToPopup(driver, rootURL); + await findElementByTestIdAndClick({ id: 'home-page-header-right', driver }); + await findElementByTestIdAndClick({ id: 'settings-link', driver }); +}); + +it('should be able to connect to hardhat and go to send flow', async () => { + const btn = await querySelector(driver, '[data-testid="connect-to-hardhat"]'); + await waitAndClick(btn, driver); + const button = await findElementByText(driver, 'Disconnect from Hardhat'); + expect(button).toBeTruthy(); + await findElementByTestIdAndClick({ id: 'navbar-button-with-back', driver }); +}); + +it('should be able to filter nfts and make selection on send flow', async () => { + await findElementByTestIdAndClick({ id: 'bottom-tab-nfts', driver }); + await delayTime('very-long'); + await delayTime('very-long'); + await findElementByTestIdAndClick({ id: 'header-link-send', driver }); + const input = await findElementByTestId({ id: 'to-address-input', driver }); + await input.sendKeys('rainbowwallet.eth'); + + await findElementByTestIdAndClick({ + id: 'input-wrapper-dropdown-token-input', + driver, + }); + const assetInput = await findElementByTestId({ id: 'token-input', driver }); + await assetInput.click(); + await assetInput.sendKeys('uni'); + const uniswapV3PositionsSection = await findElementByTestId({ + id: 'nfts-collection-section-Uniswap V3 Positions', + driver, + }); + const learnWeb3Badges = await doNotFindElementByTestId({ + id: 'nfts-collection-section-LearnWeb3 Badges', + driver, + }); + expect(uniswapV3PositionsSection).toBeTruthy(); + expect(learnWeb3Badges).toBeFalsy(); + + await uniswapV3PositionsSection.click(); + await findElementByTextAndClick(driver, '#521552'); +}); + +it('should be able to go to review on send flow', async () => { + await findElementByTestIdAndClick({ id: 'send-review-button', driver }); +}); + +it('should be able to send transaction on review on send flow', async () => { + await findElementByTestIdAndClick({ id: 'review-confirm-button', driver }); + const sendTransaction = await transactionStatus(); + expect(await sendTransaction).toBe('success'); +}); diff --git a/src/core/references/shortcuts.ts b/src/core/references/shortcuts.ts index 36e0993e0e..b6ff77bf65 100644 --- a/src/core/references/shortcuts.ts +++ b/src/core/references/shortcuts.ts @@ -186,6 +186,22 @@ export const shortcuts = { display: 'R', key: 'r', }, + DOWNLOAD_NFT: { + display: 'D', + key: 'd', + }, + COPY_NFT_ID: { + display: 'C', + key: 'c', + }, + SEND_NFT: { + display: 'S', + key: 's', + }, + HIDE_NFT: { + display: 'H', + key: 'h', + }, }, wallets: { CHOOSE_WALLET_GROUP_NEW: { diff --git a/src/entries/popup/hooks/useHomeShortcuts.ts b/src/entries/popup/hooks/useHomeShortcuts.ts index f234027bed..b9f9b74c38 100644 --- a/src/entries/popup/hooks/useHomeShortcuts.ts +++ b/src/entries/popup/hooks/useHomeShortcuts.ts @@ -9,6 +9,7 @@ import { useCurrentHomeSheetStore } from '~/core/state/currentHomeSheet'; import { useDeveloperToolsEnabledStore } from '~/core/state/currentSettings/developerToolsEnabled'; import { useFeatureFlagsStore } from '~/core/state/currentSettings/featureFlags'; import { useTestnetModeStore } from '~/core/state/currentSettings/testnetMode'; +import { useSelectedNftStore } from '~/core/state/selectedNft'; import { useSelectedTokenStore } from '~/core/state/selectedToken'; import { useSelectedTransactionStore } from '~/core/state/selectedTransaction'; import { truncateAddress } from '~/core/utils/address'; @@ -52,6 +53,7 @@ export function useHomeShortcuts() { const { isWatchingWallet } = useWallets(); const { testnetMode, setTestnetMode } = useTestnetModeStore(); const { developerToolsEnabled } = useDeveloperToolsEnabledStore(); + const { selectedNft } = useSelectedNftStore(); const allowSend = useMemo( () => !isWatchingWallet || featureFlags.full_watching_wallets, @@ -112,11 +114,13 @@ export function useHomeShortcuts() { navigate(ROUTES.BUY); break; case shortcuts.home.COPY_ADDRESS.key: - trackShortcut({ - key: shortcuts.home.COPY_ADDRESS.display, - type: 'home.copyAddress', - }); - handleCopy(); + if (!selectedNft) { + trackShortcut({ + key: shortcuts.home.COPY_ADDRESS.display, + type: 'home.copyAddress', + }); + handleCopy(); + } break; case shortcuts.home.GO_TO_CONNECTED_APPS.key: trackShortcut({ @@ -228,6 +232,7 @@ export function useHomeShortcuts() { handleTestnetMode, alertWatchingWallet, disconnectFromApp, + selectedNft, ], ); useKeyboardShortcut({ diff --git a/src/entries/popup/hooks/useKeyboardAnalytics.ts b/src/entries/popup/hooks/useKeyboardAnalytics.ts index fd54d1f6ee..bb179379eb 100644 --- a/src/entries/popup/hooks/useKeyboardAnalytics.ts +++ b/src/entries/popup/hooks/useKeyboardAnalytics.ts @@ -40,6 +40,10 @@ export type KeyboardEventDescription = | 'navbar.goBack' | 'navigate.down' | 'navigate.up' + | 'nfts.copyId' + | 'nfts.download' + | 'nfts.hide' + | 'nfts.send' | 'radix.switchMenu.dismiss' | 'send.cancel' | 'send.copyContactAddress' diff --git a/src/entries/popup/hooks/useNftShortcuts.tsx b/src/entries/popup/hooks/useNftShortcuts.tsx new file mode 100644 index 0000000000..0521a5a4dd --- /dev/null +++ b/src/entries/popup/hooks/useNftShortcuts.tsx @@ -0,0 +1,94 @@ +import { useCallback, useRef } from 'react'; + +import { i18n } from '~/core/languages'; +import { shortcuts } from '~/core/references/shortcuts'; +import { useCurrentAddressStore } from '~/core/state'; +import { useNftsStore } from '~/core/state/nfts'; +import { useSelectedNftStore } from '~/core/state/selectedNft'; +import { UniqueAsset } from '~/core/types/nfts'; + +import { triggerToast } from '../components/Toast/Toast'; +import { ROUTES } from '../urls'; + +import useKeyboardAnalytics from './useKeyboardAnalytics'; +import { useKeyboardShortcut } from './useKeyboardShortcut'; +import { useRainbowNavigate } from './useRainbowNavigate'; + +export function useNftShortcuts(nft?: UniqueAsset | null) { + const { currentAddress: address } = useCurrentAddressStore(); + const { selectedNft, setSelectedNft } = useSelectedNftStore(); + const { trackShortcut } = useKeyboardAnalytics(); + const { toggleHideNFT } = useNftsStore(); + const navigate = useRainbowNavigate(); + const nftToFocus = nft ?? selectedNft; + const getNftIsSelected = useCallback(() => !!nftToFocus, [nftToFocus]); + const downloadLink = useRef(null); + + const handleCopyId = useCallback(() => { + if (nftToFocus) { + navigator.clipboard.writeText(nftToFocus.id); + triggerToast({ + title: i18n.t('nfts.details.copy_token_id'), + description: nftToFocus.id, + }); + } + }, [nftToFocus]); + + const handleDownload = useCallback(() => { + downloadLink.current?.click(); + const link = document.createElement('a'); + link.setAttribute('download', ''); + link.href = nftToFocus?.image_url || ''; + link.click(); + link.remove(); + }, [nftToFocus?.image_url]); + + const handleHideNft = useCallback(() => { + toggleHideNFT(address, nftToFocus?.uniqueId || ''); + }, [address, nftToFocus?.uniqueId, toggleHideNFT]); + + const handleSendNft = useCallback(() => { + if (nft) { + setSelectedNft(nft); + navigate(ROUTES.SEND, { replace: true }); + } + }, [navigate, nft, setSelectedNft]); + + const handleNftShortcuts = useCallback( + (e: KeyboardEvent) => { + if (e.key === shortcuts.nfts.DOWNLOAD_NFT.key) { + handleDownload(); + trackShortcut({ + key: shortcuts.nfts.DOWNLOAD_NFT.display, + type: 'nfts.download', + }); + } + if (e.key === shortcuts.nfts.COPY_NFT_ID.key) { + handleCopyId(); + trackShortcut({ + key: shortcuts.nfts.COPY_NFT_ID.display, + type: 'nfts.copyId', + }); + } + if (e.key === shortcuts.nfts.HIDE_NFT.key) { + handleHideNft(); + trackShortcut({ + key: shortcuts.nfts.HIDE_NFT.display, + type: 'nfts.hide', + }); + } + if (e.key === shortcuts.nfts.SEND_NFT.key) { + handleSendNft(); + trackShortcut({ + key: shortcuts.nfts.SEND_NFT.display, + type: 'nfts.send', + }); + } + }, + [handleCopyId, handleDownload, handleHideNft, handleSendNft, trackShortcut], + ); + useKeyboardShortcut({ + condition: getNftIsSelected, + handler: handleNftShortcuts, + }); +} diff --git a/src/entries/popup/pages/home/NFTs/NFTContextMenu.tsx b/src/entries/popup/pages/home/NFTs/NFTContextMenu.tsx index fc40b9b92a..07917035cc 100644 --- a/src/entries/popup/pages/home/NFTs/NFTContextMenu.tsx +++ b/src/entries/popup/pages/home/NFTs/NFTContextMenu.tsx @@ -1,8 +1,10 @@ import { ReactNode, useCallback, useRef } from 'react'; import { i18n } from '~/core/languages'; +import { shortcuts } from '~/core/references/shortcuts'; import { useCurrentAddressStore } from '~/core/state'; import { useNftsStore } from '~/core/state/nfts'; +import { useSelectedNftStore } from '~/core/state/selectedNft'; import { ChainName } from '~/core/types/chains'; import { UniqueAsset } from '~/core/types/nfts'; import { @@ -14,112 +16,160 @@ import { Box, Stack, Text, TextOverflow } from '~/design-system'; import { ContextMenuContent, ContextMenuItem, + ContextMenuSeparator, ContextMenuTrigger, } from '~/entries/popup/components/ContextMenu/ContextMenu'; import { DetailsMenuWrapper } from '~/entries/popup/components/DetailsMenu'; import { triggerToast } from '~/entries/popup/components/Toast/Toast'; +import { useRainbowNavigate } from '~/entries/popup/hooks/useRainbowNavigate'; +import { ROUTES } from '~/entries/popup/urls'; import { getOpenseaUrl } from './utils'; export default function NFTContextMenu({ children, nft, + offsetOverride, }: { children: ReactNode; nft?: UniqueAsset | null; + offsetOverride?: boolean; }) { const { currentAddress: address } = useCurrentAddressStore(); const { hidden, toggleHideNFT } = useNftsStore(); + const { selectedNft, setSelectedNft } = useSelectedNftStore(); + const navigate = useRainbowNavigate(); const hiddenNftsForAddress = hidden[address] || {}; - const hasContractAddress = !!nft?.asset_contract.address; - const hasNetwork = !!nft?.network; - const displayed = !hiddenNftsForAddress[nft?.uniqueId || '']; + const nftToFocus = selectedNft ?? nft; + const hasContractAddress = !!nftToFocus?.asset_contract.address; + const hasNetwork = !!nftToFocus?.network; + const displayed = !hiddenNftsForAddress[nftToFocus?.uniqueId || '']; const explorerTitle = - nft?.network === 'mainnet' ? 'Etherscan' : i18n.t('nfts.details.explorer'); + nftToFocus?.network === 'mainnet' + ? 'Etherscan' + : i18n.t('nfts.details.explorer'); const getBlockExplorerUrl = () => { - if (nft?.network === 'mainnet') { + if (nftToFocus?.network === 'mainnet') { return `https://${getBlockExplorerHostForChain( - chainIdFromChainName(nft?.network as ChainName), - )}/nft/${nft?.asset_contract.address}/${nft?.id}`; + chainIdFromChainName(nftToFocus?.network as ChainName), + )}/nft/${nftToFocus?.asset_contract.address}/${nft?.id}`; } else { return `https://${getBlockExplorerHostForChain( - chainIdFromChainName(nft?.network as ChainName), - )}/token/${nft?.asset_contract.address}?a=${nft?.id}`; + chainIdFromChainName(nftToFocus?.network as ChainName), + )}/token/${nftToFocus?.asset_contract.address}?a=${nft?.id}`; } }; - const openseaUrl = getOpenseaUrl({ nft }); + const openseaUrl = getOpenseaUrl({ nft: nftToFocus }); const downloadLink = useRef(null); - const copyId = useCallback(() => { - navigator.clipboard.writeText(nft?.id as string); + const handleCopyId = useCallback(() => { + navigator.clipboard.writeText(nftToFocus?.id as string); triggerToast({ title: i18n.t('nfts.details.id_copied'), - description: nft?.id, + description: nftToFocus?.id, }); - }, [nft?.id]); + }, [nftToFocus?.id]); + + const handleOpenChange = useCallback( + (isOpen: boolean) => { + if (isOpen && nft) { + setSelectedNft(nft); + } + }, + [nft, setSelectedNft], + ); + + const handleSendNft = useCallback(() => { + navigate(ROUTES.SEND); + }, [navigate]); return ( - + {children} - + - + - {i18n.t('nfts.details.copy_token_id')} + {i18n.t('nfts.details.send')} + + + + toggleHideNFT(address, nftToFocus?.uniqueId || '') + } + shortcut={shortcuts.nfts.HIDE_NFT.display} + > + + {displayed + ? i18n.t('nfts.details.hide') + : i18n.t('nfts.details.unhide')} - - {nft?.id} - - {nft?.image_url && ( + {nftToFocus?.image_url && ( downloadLink.current?.click()} + shortcut={shortcuts.nfts.DOWNLOAD_NFT.display} > - + {i18n.t('nfts.details.download')} )} + + + {i18n.t('nfts.details.copy_token_id')} + + + {nftToFocus?.id} + + + {hasContractAddress && hasNetwork && ( goToNewTab({ url: openseaUrl })} > - {'OpenSea'} + {i18n.t('nfts.details.view_on_opensea')} )} goToNewTab({ url: getBlockExplorerUrl() })} > - {explorerTitle} - - - toggleHideNFT(address, nft?.uniqueId || '')} - > - - {displayed - ? i18n.t('nfts.details.hide') - : i18n.t('nfts.details.unhide')} + {i18n.t('nfts.details.view_on_explorer', { explorerTitle })} diff --git a/src/entries/popup/pages/home/NFTs/NFTDetails.tsx b/src/entries/popup/pages/home/NFTs/NFTDetails.tsx index 97047b2d41..1b4656c652 100644 --- a/src/entries/popup/pages/home/NFTs/NFTDetails.tsx +++ b/src/entries/popup/pages/home/NFTs/NFTDetails.tsx @@ -75,12 +75,14 @@ import { } from '~/entries/popup/components/Navbar/Navbar'; import { useDominantColor } from '~/entries/popup/hooks/useDominantColor'; import { useEns } from '~/entries/popup/hooks/useEns'; +import { useNftShortcuts } from '~/entries/popup/hooks/useNftShortcuts'; import { useRainbowNavigate } from '~/entries/popup/hooks/useRainbowNavigate'; import { useUserChains } from '~/entries/popup/hooks/useUserChains'; import { ROUTES } from '~/entries/popup/urls'; import chunkLinks from '~/entries/popup/utils/chunkLinks'; import { BirdIcon } from './BirdIcon'; +import NFTContextMenu from './NFTContextMenu'; import NFTDropdownMenu from './NFTDropdownMenu'; import { getOpenseaUrl } from './utils'; @@ -146,6 +148,9 @@ export default function NFTDetails() { }); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + + useNftShortcuts(nft); + return ( - + + + diff --git a/src/entries/popup/pages/home/NFTs/NFTDropdownMenu.tsx b/src/entries/popup/pages/home/NFTs/NFTDropdownMenu.tsx index 57035ec400..55ae4b2632 100644 --- a/src/entries/popup/pages/home/NFTs/NFTDropdownMenu.tsx +++ b/src/entries/popup/pages/home/NFTs/NFTDropdownMenu.tsx @@ -1,8 +1,10 @@ import { ReactNode, useCallback, useRef } from 'react'; import { i18n } from '~/core/languages'; +import { shortcuts } from '~/core/references/shortcuts'; import { useCurrentAddressStore } from '~/core/state'; import { useNftsStore } from '~/core/state/nfts'; +import { useSelectedNftStore } from '~/core/state/selectedNft'; import { ChainName } from '~/core/types/chains'; import { UniqueAsset } from '~/core/types/nfts'; import { @@ -26,7 +28,10 @@ import { DropdownMenuTrigger, } from '~/entries/popup/components/DropdownMenu/DropdownMenu'; import { HomeMenuRow } from '~/entries/popup/components/HomeMenuRow/HomeMenuRow'; +import { ShortcutHint } from '~/entries/popup/components/ShortcutHint/ShortcutHint'; import { triggerToast } from '~/entries/popup/components/Toast/Toast'; +import { useRainbowNavigate } from '~/entries/popup/hooks/useRainbowNavigate'; +import { ROUTES } from '~/entries/popup/urls'; import { getOpenseaUrl } from './utils'; @@ -39,6 +44,8 @@ export default function NFTDropdownMenu({ }) { const { currentAddress: address } = useCurrentAddressStore(); const { hidden, toggleHideNFT } = useNftsStore(); + const { setSelectedNft } = useSelectedNftStore(); + const navigate = useRainbowNavigate(); const hiddenNftsForAddress = hidden[address] || {}; const displayed = !hiddenNftsForAddress[nft?.uniqueId || '']; const hasContractAddress = !!nft?.asset_contract.address; @@ -71,8 +78,15 @@ export default function NFTDropdownMenu({ }); }, [nft?.id]); + const handleSendNft = useCallback(() => { + if (nft) { + setSelectedNft(nft); + navigate(ROUTES.SEND, { replace: true }); + } + }, [navigate, nft, setSelectedNft]); + const onValueChange = ( - value: 'copy' | 'download' | 'opensea' | 'explorer' | 'hide', + value: 'send' | 'copy' | 'download' | 'opensea' | 'explorer' | 'hide', ) => { switch (value) { case 'copy': @@ -90,6 +104,9 @@ export default function NFTDropdownMenu({ case 'hide': toggleHideNFT(address, nft?.uniqueId || ''); break; + case 'send': + handleSendNft(); + break; } }; return ( @@ -109,36 +126,48 @@ export default function NFTDropdownMenu({ > - + } centerComponent={ - - {i18n.t('nfts.details.copy_token_id')} + + {i18n.t('nfts.details.send')} - - {nft?.id} - } + rightComponent={ + + } + /> + + + + } + centerComponent={ + + + {displayed + ? i18n.t('nfts.details.hide') + : i18n.t('nfts.details.unhide')} + + + } + rightComponent={ + + } /> {nft?.image_url && ( @@ -165,9 +194,50 @@ export default function NFTDropdownMenu({ } + rightComponent={ + + } /> )} + + + } + centerComponent={ + + + {i18n.t('nfts.details.copy_token_id')} + + + {nft?.id} + + + } + rightComponent={ + + } + /> + + {hasContractAddress && hasNetwork && ( - {'OpenSea'} + {i18n.t('nfts.details.view_on_opensea')} } @@ -204,7 +274,9 @@ export default function NFTDropdownMenu({ centerComponent={ - {explorerTitle} + {i18n.t('nfts.details.view_on_explorer', { + explorerTitle, + })} } @@ -218,25 +290,6 @@ export default function NFTDropdownMenu({ } /> - - - - {'🙈'} - - } - centerComponent={ - - - {displayed - ? i18n.t('nfts.details.hide') - : i18n.t('nfts.details.unhide')} - - - } - /> - diff --git a/src/entries/popup/pages/home/NFTs/NFTs.tsx b/src/entries/popup/pages/home/NFTs/NFTs.tsx index a2ceb4bba8..dfb473787c 100644 --- a/src/entries/popup/pages/home/NFTs/NFTs.tsx +++ b/src/entries/popup/pages/home/NFTs/NFTs.tsx @@ -4,7 +4,10 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { i18n } from '~/core/languages'; import { shortcuts } from '~/core/references/shortcuts'; -import { selectSortedNftCollections } from '~/core/resources/_selectors/nfts'; +import { + NFTCollectionSectionData, + selectSortedNftCollections, +} from '~/core/resources/_selectors/nfts'; import { useNfts } from '~/core/resources/nfts'; import { getNftCount } from '~/core/resources/nfts/nfts'; import { useCurrentAddressStore } from '~/core/state'; @@ -28,6 +31,7 @@ import { useContainerRef } from '~/design-system/components/AnimatedRoute/Animat import { Skeleton } from '~/design-system/components/Skeleton/Skeleton'; import { useCoolMode } from '~/entries/popup/hooks/useCoolMode'; import { useKeyboardShortcut } from '~/entries/popup/hooks/useKeyboardShortcut'; +import { useNftShortcuts } from '~/entries/popup/hooks/useNftShortcuts'; import { useRainbowNavigate } from '~/entries/popup/hooks/useRainbowNavigate'; import { useUserChains } from '~/entries/popup/hooks/useUserChains'; import { ROUTES } from '~/entries/popup/urls'; @@ -71,10 +75,18 @@ export function NFTs() { .flat() .filter((nft) => hiddenNftsForAddress[nft.uniqueId]); const allSections = sortedSections - .filter((section) => { - return section.assets.filter((nft) => !hiddenNftsForAddress[nft.uniqueId]) - .length; - }) + .reduce((cumulativeSections, currentSection) => { + const unhiddenAssets = currentSection.assets.filter( + (nft) => !hiddenNftsForAddress[nft.uniqueId], + ); + if (unhiddenAssets.length) { + cumulativeSections.push({ + ...currentSection, + assets: unhiddenAssets, + }); + } + return cumulativeSections; + }, [] as NFTCollectionSectionData[]) .concat( hiddenAssets.length ? { @@ -189,6 +201,8 @@ export function NFTs() { nftCount, ]); + useNftShortcuts(); + if (!isLoading && sortedSections.length === 0) { return ; } diff --git a/static/json/languages/en_US.json b/static/json/languages/en_US.json index 50b1d9e1ae..f238c36646 100644 --- a/static/json/languages/en_US.json +++ b/static/json/languages/en_US.json @@ -1319,7 +1319,9 @@ "floor_price_title": "Collection floor price", "floor_price_action_label": "Got it" }, - "send": "Send" + "send": "Send", + "view_on_opensea": "View on OpenSea", + "view_on_explorer": "View on %{explorerTitle}" } }, "points": {