From 63bbec8b3d3b0dd48cec7ff9b8e2a07c21554838 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 17 Jan 2024 13:57:00 +0200 Subject: [PATCH] TW-1125: [epic] Assets rework (#1006) * TW-683: Tokens rework. Account's tokens -> Redux * TW-683: Assets rework. Tokens. + Whitelist * TW-683: Assets rework. Tokens. 'Add Asset' page * TW-683: Assets rework. Collectibles. Redux * TW-683: Assets rework. Tokens. Manage page. Fix: listing other account tokens too * TW-683: Assets rework. Tokens. Refactor * TW-683: Assets rework. Collectibles. Optimized loading * TW-683: Assets rework. Collectibles. Optimized loading * TW-683: Assets rework. Refactor * TW-683: Assets rework. Optimized balances loading * TW-683: Assets rework. Collectibles. Fix details loading from OBJKT * TW-683: Assets rework. Migration from IndexedDB * TW-683: Assets rework. Refactor * TW-683: Assets rework. Refactor * TW-683: Assets rework. Refactor * TW-683: Assets rework. + ESLint no-cycle rule * TW-683: Assets rework. Refactor * TW-683: Assets rework. Migrations made async * TW-683: Assets rework. Refactor * TW-683: Assets rework. Refactor * TW-683: Assets rework. Not keeping metadata from TZKT * TW-683: Assets rework. Refactor * TW-683: Assets rework. + __TEMPORARY_COLLECTIBLES_LOAD_LIMIT__ * TW-683: Assets rework. Fix tokens re-render on account switch * TW-683: Assets rework. Fix page crash, when switching to Ghostnet * TW-993: Assets rework. Collectibles pagination logic (#1018) * TW-993: Assets rework. NFTs pagination. + buildCollectibleImagesStack * TW-993: Assets rework. NFTs pagination. WIP. Not sending all NFTs. Not loading all NFTs meta * TW-993: Assets rework. NFTs pagination. WIP. + type StoredCollectible * TW-993: Assets rework. Collectibles pagination. Rolled back to not saving & * TW-993: Assets rework. Collectibles pagination. WIP. Grid. Without search * TW-993: Assets rework. Collectibles pagination. WIP. Grid. + Search * TW-993: Assets rework. Collectibles pagination. WIP. Tuned meta refresh * TW-993: Assets rework. Collectibles pagination. + Search in grid * TW-993: Assets rework. Collectibles pagination. Improved paginated loading * TW-993: Assets rework. Collectibles pagination. Manage assets page. Refactor * TW-993: Assets rework. + Manage Collectibles component * TW-993: Assets rework. Pagination & listing logic. Refactor * TW-993: Assets rework. Pagination logic. Cleared logs * TW-993: Assets rework. Pagination logic. WIP. + ReduxStoreState.collectiblesMetadata * TW-993: Assets rework. Pagination logic. Collectibles meta. WIP. + Slice size check * TW-993: Assets rework. Pagination logic. Metadata refactor * TW-993: Assets rework. Pagination logic. Metadata refactor. + Migration * TW-993: Assets rework. Pagination logic. Refactor * TW-993: Assets rework. Pagination logic. Refactor * TW-993: Assets rework. Pagination logic. ++ To known meta when loading assets * TW-993: Assets rework. Pagination logic. Manage assets page. Refactor * TW-993: Assets rework. Pagination logic. Fix paginated list update + Fix search * TW-993: Assets rework. Pagination logic. Using stored balances * TW-993: Assets rework. Pagination logic. Refactor * TW-993: Assets rework. Pagination logic. ++ * TW-993: Assets rework. Pagination logic. ++ useIntersectionDetection() * TW-993: Assets rework. Pagination logic. ++ * TW-993: Assets rework. Pagination logic. ++ Persistance logic * TW-993: Assets rework. Pagination logic. Refactor * TW-993: Assets rework. Pagination logic. + Kept page items amount in URL * TW-993: Assets rework. Pagination logic. + Map type to Redux store * TW-993: Assets rework. Pagination logic. + * TW-993: Assets rework. Pagination logic. Minor fixes * TW-993: Assets rework. Pagination logic. Minor fixes * TW-993: Assets rework. Pagination logic. ++ Performance * TW-993: Assets rework. Pagination logic. ++ Performance. Removed bottleneck of reducing collectibles in Store * TW-993: Assets rework. Pagination logic. Performance. Refactor * TW-993: Assets rework. Pagination logic. Performance. Refactor * TW-993: Assets rework. Pagination logic. Refactor. -- 'mem' * TW-993: Assets rework. Pagination logic. Tiny fix * TW-993: Fix unit tests * TW-993: Assets rework. Pagination logic. Fix adding token * TW-993: Assets rework. Pagination logic. Fix removing token * TW-993: Fix unit tests. -- Send NFT * TW-993: Assets rework. Clean-up deps * TW-993: Assets rework. NFTs pagination. Loading 30+50 at a time * TW-993: Assets rework. NFTs pagination. reduced scrollThreshold * TW-993: Assets rework. NFTs pagination. Rearranged NFTs images sources * TW-993: Assets rework. NFTs pagination. Fix gas balance loading * TW-993: Assets rework. NFTs pagination. Overcome OBJKT API byte-size payload limit * TW-993: Assets rework. NFTs pagination. Fix lost sorting in SendForm * TW-993: Assets rework. NFTs pagination. Fix areStringArraysEqual * TW-1225: [epic] Assets rework. Refactor --- .eslintrc | 7 +- e2e/src/features/send.feature | 16 - .../pages/manage-assets-collectibles.page.ts | 2 +- .../pages/manage-assets-tokens.page.ts | 2 +- e2e/src/utils/input-data.utils.ts | 6 +- jest.setup.js | 6 - package.json | 7 +- src/app/ConfirmPage.tsx | 3 +- src/app/ErrorBoundary.tsx | 2 +- src/app/PageRouter.tsx | 2 +- src/app/WithDataLoading.tsx | 21 +- src/app/atoms/SimpleInfiniteScroll.tsx | 36 + src/app/atoms/useTabSlug.tsx | 4 +- .../hooks/AliceBob/use-disabled-proceed.ts | 3 +- src/app/hooks/use-assets-loading.ts | 42 + src/app/hooks/use-assets-migrations.ts | 18 + src/app/hooks/use-balances-loading.ts | 27 + .../hooks/use-balances-with-decimals.hook.ts | 35 - .../hooks/use-collectibles-details-loading.ts | 21 +- .../hooks/use-collectibles-listing-logic.ts | 72 ++ .../use-collectibles-pagination-logic.ts | 78 ++ src/app/hooks/use-metadata-loading.ts | 49 +- src/app/hooks/use-metadata-refresh.ts | 43 +- src/app/hooks/use-tokens-listing-logic.ts | 90 ++ src/app/hooks/use-tokens-loading.ts | 9 - .../Header/AccountDropdown/index.tsx | 3 +- .../ShortcutAccountSwitchOverlay/index.tsx | 3 +- src/app/pages/AddAsset/AddAsset.tsx | 41 +- .../CollectiblePage/CollectiblePageImage.tsx | 13 +- .../CollectiblePage/PropertiesItems.tsx | 3 +- .../Collectibles/CollectiblePage/index.tsx | 35 +- .../CollectiblesTab/CollectibleItem.tsx | 86 +- .../CollectiblesTab/CollectibleItemImage.tsx | 61 +- .../Collectibles/CollectiblesTab/index.tsx | 122 +- .../components/AudioCollectible.tsx | 4 +- .../components/CollectibleImageFallback.tsx | 8 +- .../hooks/use-collectible-selling.hook.ts | 4 +- src/app/pages/Home/ActionButtonsBar.tsx | 148 +++ src/app/pages/Home/Home.tsx | 179 +-- .../OtherComponents/BakingHistoryItem.tsx | 3 +- .../Home/OtherComponents/BakingSection.tsx | 3 +- .../pages/Home/OtherComponents/MainBanner.tsx | 17 +- .../Home/OtherComponents/Tokens/Tokens.tsx | 51 +- .../Tokens/components/ListItem.tsx | 127 +- .../components/TokenTag/DelegateTag.tsx | 6 +- .../pages/ManageAssets/AssetsPlaceholder.tsx | 41 + src/app/pages/ManageAssets/ListItem.tsx | 73 ++ .../ManageAssets/ManageAssets.module.css | 3 - src/app/pages/ManageAssets/ManageAssets.tsx | 220 ---- .../ManageAssets/ManageAssetsContent.tsx | 122 ++ .../pages/ManageAssets/ManageCollectibles.tsx | 62 + src/app/pages/ManageAssets/ManageTokens.tsx | 55 + src/app/pages/ManageAssets/index.tsx | 32 + ...ManageAssets.selectors.ts => selectors.ts} | 0 src/app/store/ab-testing/epics.ts | 7 +- src/app/store/advertising/epics.ts | 7 +- src/app/store/assets/actions.ts | 40 + src/app/store/assets/epics.ts | 91 ++ src/app/store/assets/reducer.ts | 158 +++ src/app/store/assets/selectors.ts | 21 + src/app/store/assets/state.mock.ts | 9 + src/app/store/assets/state.ts | 34 + src/app/store/assets/utils.ts | 161 +++ src/app/store/balances/actions.ts | 32 +- src/app/store/balances/epics.ts | 73 +- src/app/store/balances/reducers.ts | 49 +- src/app/store/balances/selectors.ts | 13 +- src/app/store/balances/state.ts | 2 +- src/app/store/balances/utils.ts | 40 + src/app/store/buy-with-credit-card/epics.ts | 8 +- src/app/store/buy-with-credit-card/utils.ts | 2 +- .../store/collectibles-metadata/actions.ts | 15 + src/app/store/collectibles-metadata/epics.ts | 26 + .../store/collectibles-metadata/reducer.ts | 96 ++ .../store/collectibles-metadata/selectors.ts | 10 + .../store/collectibles-metadata/state.mock.ts | 8 + src/app/store/collectibles-metadata/state.ts | 22 + src/app/store/collectibles/epics.ts | 22 +- src/app/store/collectibles/reducer.ts | 2 + src/app/store/collectibles/selectors.ts | 2 +- src/app/store/collectibles/state.ts | 1 - src/app/store/collectibles/utils.ts | 1 - src/app/store/currency/epics.ts | 7 +- src/app/store/index.ts | 48 +- src/app/store/migrations.ts | 51 + src/app/store/partners-promotion/epics.ts | 5 +- src/app/store/root-state.epics.ts | 8 +- src/app/store/root-state.mock.ts | 4 + src/app/store/root-state.reducer.ts | 4 + src/app/store/swap/epics.ts | 9 +- src/app/store/tokens-metadata/actions.ts | 33 +- src/app/store/tokens-metadata/epics.ts | 77 +- src/app/store/tokens-metadata/reducers.ts | 95 +- src/app/store/tokens-metadata/selectors.ts | 2 +- src/app/store/tokens-metadata/state.ts | 15 +- src/app/store/tokens-metadata/utils.ts | 12 +- src/app/templates/AccountBanner.tsx | 2 +- .../AdditionalFeeInput/AdditionalFeeInput.tsx | 2 +- src/app/templates/AssetImage.tsx | 84 +- src/app/templates/AssetInfo.tsx | 12 +- .../{AssetSelect => }/AssetSelect.tsx | 89 +- src/app/templates/AssetSelect/interfaces.ts | 3 - src/app/templates/AssetSelect/utils.ts | 5 - src/app/templates/Balance.tsx | 2 +- src/app/templates/DAppsList.tsx | 9 +- src/app/templates/DAppsList/DAppIcon.tsx | 12 +- src/app/templates/DelegateForm.tsx | 5 +- .../templates/ExpensesView/ExpensesView.tsx | 3 +- src/app/templates/InternalConfirmation.tsx | 3 +- .../PaymentProviderOption.tsx | 6 +- src/app/templates/SearchAssetField.tsx | 11 +- src/app/templates/SearchField.tsx | 2 +- src/app/templates/SendForm/FeeSection.tsx | 3 +- src/app/templates/SendForm/Form.tsx | 3 +- src/app/templates/SendForm/SendErrorAlert.tsx | 2 +- src/app/templates/SendForm/index.tsx | 69 +- .../SwapExchangeRate/SwapExchangeRate.tsx | 2 +- src/app/templates/SwapForm/SwapForm.tsx | 8 +- .../SwapFormInput/AssetsMenu/AssetOption.tsx | 3 +- .../SwapForm/SwapFormInput/SwapFormInput.tsx | 19 +- .../templates/TopUpInput/CurrencyOption.tsx | 6 +- .../use-page-router-analytics.hook.ts | 4 +- src/lib/apis/apollo.ts | 35 +- src/lib/apis/objkt/constants.ts | 12 +- src/lib/apis/objkt/index.ts | 20 +- src/lib/apis/objkt/queries.ts | 27 +- src/lib/apis/objkt/types.ts | 23 +- .../temple/endpoints/get-exchange-rates.ts | 2 +- src/lib/apis/temple/metadata.ts | 34 +- src/lib/apis/temple/whitelist-tokens.ts | 42 +- src/lib/apis/tzkt/api.ts | 97 +- src/lib/apis/tzkt/index.ts | 7 +- src/lib/apis/tzkt/types.ts | 52 +- src/lib/apis/yupana.ts | 6 +- src/lib/assets/contract.utils.ts | 13 +- src/lib/assets/hooks/collectibles.ts | 49 + src/lib/assets/hooks/index.ts | 30 + src/lib/assets/hooks/tokens.ts | 127 ++ src/lib/assets/hooks/utils.ts | 13 + src/lib/assets/index.ts | 2 +- src/lib/assets/known-tokens.ts | 67 +- src/lib/assets/migrations.ts | 37 + src/lib/assets/search.utils.ts | 34 +- src/lib/assets/types.ts | 11 +- src/lib/assets/use-filtered.ts | 108 -- src/lib/assets/use-sorting.ts | 47 + src/lib/assets/utils.ts | 12 +- src/lib/balances/fetch.ts | 4 +- src/lib/balances/hooks.ts | 78 +- src/lib/balances/index.ts | 4 +- src/lib/balances/utils.ts | 2 - src/lib/fixed-times.ts | 4 +- src/lib/images-uri.ts | 164 +++ src/lib/local-storage/index.ts | 19 + src/lib/local-storage/migrator.ts | 4 +- src/lib/metadata/fetch.ts | 63 +- src/lib/metadata/index.ts | 129 +- src/lib/metadata/on-chain/fetch.ts | 10 +- src/lib/metadata/types.ts | 7 +- src/lib/metadata/utils.ts | 41 +- src/lib/migrator.ts | 6 +- src/lib/notifications/store/epics.ts | 4 +- src/lib/route3/constants.ts | 4 - src/lib/store/epics.utils.ts | 6 + src/lib/store/index.ts | 4 + src/lib/store/persist.utils.ts | 57 + src/lib/store/state.utils.ts | 4 +- src/lib/swr/index.ts | 2 +- src/lib/temple/activity-new/fetch.ts | 5 +- src/lib/temple/assets/accountTokens.ts | 47 - src/lib/temple/assets/index.ts | 1 - src/lib/temple/front/assets.ts | 194 --- src/lib/temple/front/baking.ts | 2 +- src/lib/temple/front/balance.ts | 48 - src/lib/temple/front/chain.ts | 2 +- src/lib/temple/front/image-uri.ts | 106 -- src/lib/temple/front/index.ts | 17 - src/lib/temple/front/load-balances.ts | 24 - src/lib/temple/front/provider.tsx | 9 +- src/lib/temple/front/sync-tokens.ts | 104 -- .../temple/front/use-total-balance.hook.ts | 30 +- src/lib/temple/repo.ts | 6 +- src/lib/temple/types.ts | 2 +- src/lib/types.d.ts | 2 + src/lib/ui/Image.tsx | 64 - src/lib/ui/ImageStacked.tsx | 65 + src/lib/ui/Popper.tsx | 7 +- .../{temple/front => ui}/get-online-status.ts | 0 src/lib/ui/hooks/useCallbackRef.ts | 9 + src/lib/ui/hooks/useDidMount.ts | 8 +- src/lib/ui/hooks/useDidUpdate.ts | 22 +- src/lib/ui/hooks/useInterval.ts | 11 +- src/lib/ui/use-images-stack-loading.ts | 51 + src/lib/ui/use-intersection-detection.ts | 59 +- src/lib/utils/get-transfer-permissions.ts | 2 +- src/lib/utils/numbers.ts | 4 + src/lib/utils/search-items.ts | 6 +- src/lib/utils/string.ts | 8 + src/lib/utils/swap.utils.ts | 4 +- src/lib/woozie/index.ts | 16 + tailwind.config.js | 5 - yarn.lock | 1062 +---------------- 202 files changed, 3906 insertions(+), 3436 deletions(-) create mode 100644 src/app/atoms/SimpleInfiniteScroll.tsx create mode 100644 src/app/hooks/use-assets-loading.ts create mode 100644 src/app/hooks/use-assets-migrations.ts create mode 100644 src/app/hooks/use-balances-loading.ts delete mode 100644 src/app/hooks/use-balances-with-decimals.hook.ts create mode 100644 src/app/hooks/use-collectibles-listing-logic.ts create mode 100644 src/app/hooks/use-collectibles-pagination-logic.ts create mode 100644 src/app/hooks/use-tokens-listing-logic.ts delete mode 100644 src/app/hooks/use-tokens-loading.ts create mode 100644 src/app/pages/Home/ActionButtonsBar.tsx create mode 100644 src/app/pages/ManageAssets/AssetsPlaceholder.tsx create mode 100644 src/app/pages/ManageAssets/ListItem.tsx delete mode 100644 src/app/pages/ManageAssets/ManageAssets.module.css delete mode 100644 src/app/pages/ManageAssets/ManageAssets.tsx create mode 100644 src/app/pages/ManageAssets/ManageAssetsContent.tsx create mode 100644 src/app/pages/ManageAssets/ManageCollectibles.tsx create mode 100644 src/app/pages/ManageAssets/ManageTokens.tsx create mode 100644 src/app/pages/ManageAssets/index.tsx rename src/app/pages/ManageAssets/{ManageAssets.selectors.ts => selectors.ts} (100%) create mode 100644 src/app/store/assets/actions.ts create mode 100644 src/app/store/assets/epics.ts create mode 100644 src/app/store/assets/reducer.ts create mode 100644 src/app/store/assets/selectors.ts create mode 100644 src/app/store/assets/state.mock.ts create mode 100644 src/app/store/assets/state.ts create mode 100644 src/app/store/assets/utils.ts create mode 100644 src/app/store/balances/utils.ts create mode 100644 src/app/store/collectibles-metadata/actions.ts create mode 100644 src/app/store/collectibles-metadata/epics.ts create mode 100644 src/app/store/collectibles-metadata/reducer.ts create mode 100644 src/app/store/collectibles-metadata/selectors.ts create mode 100644 src/app/store/collectibles-metadata/state.mock.ts create mode 100644 src/app/store/collectibles-metadata/state.ts create mode 100644 src/app/store/migrations.ts rename src/app/templates/{AssetSelect => }/AssetSelect.tsx (57%) delete mode 100644 src/app/templates/AssetSelect/interfaces.ts delete mode 100644 src/app/templates/AssetSelect/utils.ts create mode 100644 src/lib/assets/hooks/collectibles.ts create mode 100644 src/lib/assets/hooks/index.ts create mode 100644 src/lib/assets/hooks/tokens.ts create mode 100644 src/lib/assets/hooks/utils.ts create mode 100644 src/lib/assets/migrations.ts delete mode 100644 src/lib/assets/use-filtered.ts create mode 100644 src/lib/assets/use-sorting.ts create mode 100644 src/lib/images-uri.ts create mode 100644 src/lib/store/epics.utils.ts create mode 100644 src/lib/store/persist.utils.ts delete mode 100644 src/lib/temple/assets/accountTokens.ts delete mode 100644 src/lib/temple/assets/index.ts delete mode 100644 src/lib/temple/front/assets.ts delete mode 100644 src/lib/temple/front/balance.ts delete mode 100644 src/lib/temple/front/image-uri.ts delete mode 100644 src/lib/temple/front/load-balances.ts delete mode 100644 src/lib/temple/front/sync-tokens.ts delete mode 100644 src/lib/ui/Image.tsx create mode 100644 src/lib/ui/ImageStacked.tsx rename src/lib/{temple/front => ui}/get-online-status.ts (100%) create mode 100644 src/lib/ui/hooks/useCallbackRef.ts create mode 100644 src/lib/ui/use-images-stack-loading.ts create mode 100644 src/lib/utils/string.ts diff --git a/.eslintrc b/.eslintrc index 79a1d4c11b..add7c54d8d 100644 --- a/.eslintrc +++ b/.eslintrc @@ -5,7 +5,7 @@ "plugin:import/warnings", "plugin:import/typescript" ], - "plugins": ["import", "prettier"], + "plugins": ["import", "prettier", "no-type-assertion"], "parser": "@typescript-eslint/parser", "overrides": [{ "files": ["*.ts", "*.tsx"], @@ -51,9 +51,12 @@ "newlines-between": "always" } ], + "no-type-assertion/no-type-assertion": "warn", "@typescript-eslint/await-thenable": "error", "@typescript-eslint/no-non-null-assertion": "warn", "react-hooks/rules-of-hooks": "warn", - "react-hooks/exhaustive-deps": "warn" + "react-hooks/exhaustive-deps": ["warn", { + "additionalHooks": "(useMemoWithCompare|useDidUpdate)" + }] } } diff --git a/e2e/src/features/send.feature b/e2e/src/features/send.feature index dec266b455..39211d49a0 100644 --- a/e2e/src/features/send.feature +++ b/e2e/src/features/send.feature @@ -52,19 +52,3 @@ Feature: Send And I'm waiting for 'success ✓' operation status And I am on the Send page -# Send NFT - And I press Asset Drop-down on the Send Form page - And I clear Asset Drop-down Search Input value on the Send Form page - And I enter OBJKTCOM into Asset Drop-down Search Input on the Send Form page - And I select OBJKTCOM token in the token drop-down list on the Send page - And I enter watchOnlyPublicKey into Recipient Input on the Send Form page - And I enter amount_1 into Amount Input on the Send Form page - And I press Send Button on the Send Form page - - And I am on the InternalConfirmation page - And I press Confirm Button on the Internal Confirmation page - - And I am on the OperationStatusAlert page - And I'm waiting for 'success ✓' operation status - - Then I am on the Send page diff --git a/e2e/src/page-objects/pages/manage-assets-collectibles.page.ts b/e2e/src/page-objects/pages/manage-assets-collectibles.page.ts index bcc471e951..30fbb1c663 100644 --- a/e2e/src/page-objects/pages/manage-assets-collectibles.page.ts +++ b/e2e/src/page-objects/pages/manage-assets-collectibles.page.ts @@ -1,4 +1,4 @@ -import { ManageAssetsSelectors } from 'src/app/pages/ManageAssets/ManageAssets.selectors'; +import { ManageAssetsSelectors } from 'src/app/pages/ManageAssets/selectors'; import { Page } from '../../classes/page.class'; import { createPageElement } from '../../utils/search.utils'; diff --git a/e2e/src/page-objects/pages/manage-assets-tokens.page.ts b/e2e/src/page-objects/pages/manage-assets-tokens.page.ts index de8dca3daf..62bdefb0fa 100644 --- a/e2e/src/page-objects/pages/manage-assets-tokens.page.ts +++ b/e2e/src/page-objects/pages/manage-assets-tokens.page.ts @@ -1,5 +1,5 @@ import retry from 'async-retry'; -import { ManageAssetsSelectors } from 'src/app/pages/ManageAssets/ManageAssets.selectors'; +import { ManageAssetsSelectors } from 'src/app/pages/ManageAssets/selectors'; import { RETRY_OPTIONS, SHORT_TIMEOUT, VERY_SHORT_TIMEOUT } from 'e2e/src/utils/timing.utils'; diff --git a/e2e/src/utils/input-data.utils.ts b/e2e/src/utils/input-data.utils.ts index caaf634e9d..198697dc38 100644 --- a/e2e/src/utils/input-data.utils.ts +++ b/e2e/src/utils/input-data.utils.ts @@ -72,8 +72,7 @@ export const iEnterValues = { kUSD: 'kUSD', uUSD: 'uUSD', WTZ: 'WTZ', - wUSDT: 'wUSDT', - OBJKTCOM: 'Temple NFT' + wUSDT: 'wUSDT' }; export type IEnterValuesKey = keyof typeof iEnterValues; @@ -82,8 +81,7 @@ export const iSelectTokenSlugs = { kUSD: 'KT1K9gCRgaLRFKTErYt1wVxA3Frb9FjasjTV_0', uUSD: 'KT1XRPEPXbZK25r3Htzp2o1x7xdMMmfocKNW_0', WTZ: 'KT1PnUZCp3u2KzWr93pn4DD7HAJnm3rWVrgn_0', - wUSDT: 'KT18fp5rcTW7mbWDmzFwjLDUhs5MeJmagDSZ_18', - OBJKTCOM: 'KT1DGbb333QNo3e2cpN3YGL5aRwWzkADcPA3_2' // 'Temple NFT' + wUSDT: 'KT18fp5rcTW7mbWDmzFwjLDUhs5MeJmagDSZ_18' }; export const clearDataFromCurrentInput = async () => { diff --git a/jest.setup.js b/jest.setup.js index ac2dd57e5b..c5d9b3d76e 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -5,12 +5,6 @@ Object.assign(global, { CryptoKey }); -jest.mock('mem', () => { - return function memoize(fn) { - return fn; - }; -}); - jest.mock('lib/temple/repo', () => ({ db: { delete: jest.fn(), diff --git a/package.json b/package.json index 41bc11bbae..d6a20bf967 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,6 @@ "@types/react-modal": "3.13.1", "@types/react-text-mask": "^5.4.11", "@types/react-transition-group": "4.4.5", - "@types/react-virtualized": "^9.21.21", "@types/resolve": "^1.20.2", "@types/scryptsy": "^2.0.0", "@types/typedarray-to-buffer": "^4.0.0", @@ -127,6 +126,7 @@ "eslint-config-react-app": "^7.0.1", "eslint-import-resolver-typescript": "^3", "eslint-plugin-import": "^2.29.0", + "eslint-plugin-no-type-assertion": "^1.3.0", "eslint-plugin-prettier": "^4", "eslint-webpack-plugin": "^3.2.0", "fast-glob": "^3.2.12", @@ -142,7 +142,6 @@ "libsodium": "0.7.8", "libsodium-wrappers": "0.7.8", "lodash": "4.17.21", - "mem": "^9.0.2", "micro-memoize": "4.0.9", "mini-css-extract-plugin": "2.6.1", "nanoid": "3.1.31", @@ -163,7 +162,6 @@ "react-dev-utils": "^12", "react-dom": "18.2.0", "react-hook-form": "5.3.1", - "react-image-fallback": "^8.0.0", "react-infinite-scroll-component": "^6.1.0", "react-json-view": "1.21.3", "react-modal": "3.15.1", @@ -171,7 +169,6 @@ "react-redux": "^8.0.2", "react-text-mask": "^5.5.0", "react-transition-group": "4.4.5", - "react-virtualized": "^9.22.3", "redux-observable": "^2.0.0", "redux-persist": "^6.0.0", "regexparam": "1.3.0", @@ -216,7 +213,7 @@ "@taquito/contracts-library": "17.0.0", "@taquito/tzip16": "17.0.0", "bignumber.js": "9.1.0", - "eslint-plugin-import": "2.29.0", + "eslint-plugin-import": "^2.29.0", "graphql-request": "^6.1.0", "json5": "^2.2.2", "follow-redirects": "^1.15.4" diff --git a/src/app/ConfirmPage.tsx b/src/app/ConfirmPage.tsx index 9d797cf5f8..7d03630bb7 100644 --- a/src/app/ConfirmPage.tsx +++ b/src/app/ConfirmPage.tsx @@ -21,9 +21,10 @@ import { ModifyFeeAndLimit } from 'app/templates/ExpensesView/ExpensesView'; import NetworkBanner from 'app/templates/NetworkBanner'; import OperationView from 'app/templates/OperationView'; import { CustomRpcContext } from 'lib/analytics'; +import { useGasToken } from 'lib/assets/hooks'; import { T, t } from 'lib/i18n'; import { useRetryableSWR } from 'lib/swr'; -import { useTempleClient, useAccount, useRelevantAccounts, useCustomChainId, useGasToken } from 'lib/temple/front'; +import { useTempleClient, useAccount, useRelevantAccounts, useCustomChainId } from 'lib/temple/front'; import { TempleAccountType, TempleDAppPayload, TempleAccount, TempleChainId } from 'lib/temple/types'; import { useSafeState } from 'lib/ui/hooks'; import { delay } from 'lib/utils'; diff --git a/src/app/ErrorBoundary.tsx b/src/app/ErrorBoundary.tsx index daee741658..b792f88dc6 100644 --- a/src/app/ErrorBoundary.tsx +++ b/src/app/ErrorBoundary.tsx @@ -4,7 +4,7 @@ import classNames from 'clsx'; import { ReactComponent as DangerIcon } from 'app/icons/danger.svg'; import { t, T } from 'lib/i18n'; -import { getOnlineStatus } from 'lib/temple/front/get-online-status'; +import { getOnlineStatus } from 'lib/ui/get-online-status'; interface ErrorBoundaryProps extends React.PropsWithChildren { className?: string; diff --git a/src/app/PageRouter.tsx b/src/app/PageRouter.tsx index 90518bc605..841e7021ce 100644 --- a/src/app/PageRouter.tsx +++ b/src/app/PageRouter.tsx @@ -13,7 +13,7 @@ import DApps from 'app/pages/DApps'; import Delegate from 'app/pages/Delegate'; import Home from 'app/pages/Home/Home'; import ImportAccount from 'app/pages/ImportAccount'; -import ManageAssets from 'app/pages/ManageAssets/ManageAssets'; +import ManageAssets from 'app/pages/ManageAssets'; import { CreateWallet } from 'app/pages/NewWallet/CreateWallet'; import { ImportWallet } from 'app/pages/NewWallet/ImportWallet'; import AttentionPage from 'app/pages/Onboarding/pages/AttentionPage'; diff --git a/src/app/WithDataLoading.tsx b/src/app/WithDataLoading.tsx index 56adac5b3a..c691ab0a94 100644 --- a/src/app/WithDataLoading.tsx +++ b/src/app/WithDataLoading.tsx @@ -2,20 +2,23 @@ import React, { FC, useEffect } from 'react'; import { useDispatch } from 'react-redux'; -import { useAdvertisingLoading } from 'app/hooks/use-advertising.hook'; -import { useCollectiblesDetailsLoading } from 'app/hooks/use-collectibles-details-loading'; -import { useTokensApyLoading } from 'app/hooks/use-load-tokens-apy.hook'; -import { useLongRefreshLoading } from 'app/hooks/use-long-refresh-loading.hook'; -import { useMetadataLoading } from 'app/hooks/use-metadata-loading'; -import { useStorageAnalytics } from 'app/hooks/use-storage-analytics'; -import { useTokensLoading } from 'app/hooks/use-tokens-loading'; import { loadSwapDexesAction, loadSwapTokensAction } from 'app/store/swap/actions'; -import { useBalancesLoading } from 'lib/temple/front/load-balances'; +import { useAdvertisingLoading } from './hooks/use-advertising.hook'; +import { useAssetsLoading } from './hooks/use-assets-loading'; +import { useAssetsMigrations } from './hooks/use-assets-migrations'; +import { useBalancesLoading } from './hooks/use-balances-loading'; +import { useCollectiblesDetailsLoading } from './hooks/use-collectibles-details-loading'; +import { useTokensApyLoading } from './hooks/use-load-tokens-apy.hook'; +import { useLongRefreshLoading } from './hooks/use-long-refresh-loading.hook'; +import { useMetadataLoading } from './hooks/use-metadata-loading'; import { useMetadataRefresh } from './hooks/use-metadata-refresh'; +import { useStorageAnalytics } from './hooks/use-storage-analytics'; export const WithDataLoading: FC = ({ children }) => { - useTokensLoading(); + useAssetsMigrations(); + + useAssetsLoading(); useMetadataLoading(); useMetadataRefresh(); useBalancesLoading(); diff --git a/src/app/atoms/SimpleInfiniteScroll.tsx b/src/app/atoms/SimpleInfiniteScroll.tsx new file mode 100644 index 0000000000..6d75535f4f --- /dev/null +++ b/src/app/atoms/SimpleInfiniteScroll.tsx @@ -0,0 +1,36 @@ +import React, { memo, PropsWithChildren, useCallback, useState } from 'react'; + +import InfiniteScroll from 'react-infinite-scroll-component'; + +interface Props { + loadNext: EmptyFn; +} + +export const SimpleInfiniteScroll = memo>(({ loadNext, children }) => { + const [seedForLoadNext, setSeedForLoadNext] = useState(0); + + const loadNextLocal = useCallback(() => { + setSeedForLoadNext(val => (val % 2) + 1); + loadNext(); + }, [loadNext]); + + return ( + + {children} + + ); +}); diff --git a/src/app/atoms/useTabSlug.tsx b/src/app/atoms/useTabSlug.tsx index bf042e590c..e854c343b5 100644 --- a/src/app/atoms/useTabSlug.tsx +++ b/src/app/atoms/useTabSlug.tsx @@ -5,10 +5,8 @@ import { useLocation } from 'lib/woozie'; export const useTabSlug = () => { const { search } = useLocation(); - const tabSlug = useMemo(() => { + return useMemo(() => { const usp = new URLSearchParams(search); return usp.get('tab'); }, [search]); - - return useMemo(() => tabSlug, [tabSlug]); }; diff --git a/src/app/hooks/AliceBob/use-disabled-proceed.ts b/src/app/hooks/AliceBob/use-disabled-proceed.ts index 5b602004c8..34cc6a2845 100644 --- a/src/app/hooks/AliceBob/use-disabled-proceed.ts +++ b/src/app/hooks/AliceBob/use-disabled-proceed.ts @@ -2,7 +2,8 @@ import { useMemo } from 'react'; import BigNumber from 'bignumber.js'; -import { useAccount, useBalance } from 'lib/temple/front'; +import { useBalance } from 'lib/balances'; +import { useAccount } from 'lib/temple/front'; export const useDisabledProceed = ( inputAmount: number | undefined, diff --git a/src/app/hooks/use-assets-loading.ts b/src/app/hooks/use-assets-loading.ts new file mode 100644 index 0000000000..616b3eca2a --- /dev/null +++ b/src/app/hooks/use-assets-loading.ts @@ -0,0 +1,42 @@ +import { useEffect } from 'react'; + +import { dispatch } from 'app/store'; +import { + loadAccountTokensActions, + loadTokensWhitelistActions, + loadAccountCollectiblesActions +} from 'app/store/assets/actions'; +import { useAreAssetsLoading } from 'app/store/assets/selectors'; +import { ASSETS_SYNC_INTERVAL } from 'lib/fixed-times'; +import { useAccount, useChainId } from 'lib/temple/front'; +import { TempleChainId } from 'lib/temple/types'; +import { useInterval } from 'lib/ui/hooks'; + +export const useAssetsLoading = () => { + const chainId = useChainId()!; + const { publicKeyHash } = useAccount(); + + useEffect(() => { + if (chainId === TempleChainId.Mainnet) dispatch(loadTokensWhitelistActions.submit()); + }, [chainId]); + + const tokensAreLoading = useAreAssetsLoading('tokens'); + + useInterval( + () => { + if (!tokensAreLoading) dispatch(loadAccountTokensActions.submit({ account: publicKeyHash, chainId })); + }, + ASSETS_SYNC_INTERVAL, + [chainId, publicKeyHash] + ); + + const collectiblesAreLoading = useAreAssetsLoading('collectibles'); + + useInterval( + () => { + if (!collectiblesAreLoading) dispatch(loadAccountCollectiblesActions.submit({ account: publicKeyHash, chainId })); + }, + ASSETS_SYNC_INTERVAL, + [chainId, publicKeyHash] + ); +}; diff --git a/src/app/hooks/use-assets-migrations.ts b/src/app/hooks/use-assets-migrations.ts new file mode 100644 index 0000000000..0131e972bd --- /dev/null +++ b/src/app/hooks/use-assets-migrations.ts @@ -0,0 +1,18 @@ +import { useAllTokensMetadataSelector } from 'app/store/tokens-metadata/selectors'; +import { migrateFromIndexedDB } from 'lib/assets/migrations'; +import { migrate } from 'lib/local-storage/migrator'; +import { useDidMount } from 'lib/ui/hooks'; + +export const useAssetsMigrations = () => { + const allMetadatas = useAllTokensMetadataSelector(); + + useDidMount( + () => + void migrate([ + { + name: 'assets-migrations@1.18.2', + up: () => migrateFromIndexedDB(allMetadatas) + } + ]) + ); +}; diff --git a/src/app/hooks/use-balances-loading.ts b/src/app/hooks/use-balances-loading.ts new file mode 100644 index 0000000000..5b037c21ad --- /dev/null +++ b/src/app/hooks/use-balances-loading.ts @@ -0,0 +1,27 @@ +import { useDispatch } from 'react-redux'; + +import { loadGasBalanceActions, loadAssetsBalancesActions } from 'app/store/balances/actions'; +import { BALANCES_SYNC_INTERVAL } from 'lib/fixed-times'; +import { useAccount, useChainId } from 'lib/temple/front'; +import { useInterval } from 'lib/ui/hooks'; + +export const useBalancesLoading = () => { + const chainId = useChainId(true)!; + const { publicKeyHash } = useAccount(); + + const dispatch = useDispatch(); + + useInterval( + () => void dispatch(loadGasBalanceActions.submit({ publicKeyHash, chainId })), + BALANCES_SYNC_INTERVAL, + [chainId, publicKeyHash], + true + ); + + useInterval( + () => void dispatch(loadAssetsBalancesActions.submit({ publicKeyHash, chainId })), + BALANCES_SYNC_INTERVAL, + [chainId, publicKeyHash], + false // Not calling immediately, because balances are also loaded via assets loading + ); +}; diff --git a/src/app/hooks/use-balances-with-decimals.hook.ts b/src/app/hooks/use-balances-with-decimals.hook.ts deleted file mode 100644 index 794916ae80..0000000000 --- a/src/app/hooks/use-balances-with-decimals.hook.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { useMemo } from 'react'; - -import { BigNumber } from 'bignumber.js'; - -import { useBalancesSelector } from 'app/store/balances/selectors'; -import { useTokensMetadataSelector } from 'app/store/tokens-metadata/selectors'; -import { TEZ_TOKEN_SLUG } from 'lib/assets'; -import { useAccount, useChainId, useGasToken } from 'lib/temple/front'; -import { atomsToTokens } from 'lib/temple/helpers'; - -export const useBalancesWithDecimals = () => { - const { publicKeyHash } = useAccount(); - const chainId = useChainId(true)!; - - const balancesRaw = useBalancesSelector(publicKeyHash, chainId); - const allTokensMetadata = useTokensMetadataSelector(); - const { metadata: gasTokenMetadata } = useGasToken(); - - return useMemo(() => { - const balancesBN: Record = {}; - - for (const tokenSlug in balancesRaw) { - const metadata = allTokensMetadata[tokenSlug]; - - if (tokenSlug === TEZ_TOKEN_SLUG) { - balancesBN[tokenSlug] = atomsToTokens(new BigNumber(balancesRaw[tokenSlug]), gasTokenMetadata.decimals); - } else { - if (metadata) { - balancesBN[tokenSlug] = atomsToTokens(new BigNumber(balancesRaw[tokenSlug]), metadata.decimals); - } - } - } - return balancesBN; - }, [balancesRaw, allTokensMetadata, gasTokenMetadata]); -}; diff --git a/src/app/hooks/use-collectibles-details-loading.ts b/src/app/hooks/use-collectibles-details-loading.ts index c99b173c54..1cbe8fdff4 100644 --- a/src/app/hooks/use-collectibles-details-loading.ts +++ b/src/app/hooks/use-collectibles-details-loading.ts @@ -1,30 +1,25 @@ import { isEqual } from 'lodash'; -import { useDispatch } from 'react-redux'; +import { dispatch } from 'app/store'; import { loadCollectiblesDetailsActions } from 'app/store/collectibles/actions'; +import { useAccountCollectibles } from 'lib/assets/hooks'; import { COLLECTIBLES_DETAILS_SYNC_INTERVAL } from 'lib/fixed-times'; -import { useAccount, useChainId, useCollectibleTokens } from 'lib/temple/front'; +import { useAccount, useChainId } from 'lib/temple/front'; import { useInterval, useMemoWithCompare } from 'lib/ui/hooks'; export const useCollectiblesDetailsLoading = () => { const chainId = useChainId()!; const { publicKeyHash } = useAccount(); - const { data: collectibles } = useCollectibleTokens(chainId, publicKeyHash); - const dispatch = useDispatch(); + const collectibles = useAccountCollectibles(publicKeyHash, chainId); - const slugs = useMemoWithCompare( - () => collectibles.map(({ tokenSlug }) => tokenSlug).sort(), - [collectibles], - isEqual - ); + const slugs = useMemoWithCompare(() => collectibles.map(({ slug }) => slug).sort(), [collectibles], isEqual); useInterval( () => { - if (slugs.length < 1) return; - - dispatch(loadCollectiblesDetailsActions.submit(slugs)); + // Is it necessary for collectibles on non-Mainnet networks too? + if (slugs.length) dispatch(loadCollectiblesDetailsActions.submit(slugs)); }, COLLECTIBLES_DETAILS_SYNC_INTERVAL, - [slugs, dispatch] + [slugs] ); }; diff --git a/src/app/hooks/use-collectibles-listing-logic.ts b/src/app/hooks/use-collectibles-listing-logic.ts new file mode 100644 index 0000000000..e605d94383 --- /dev/null +++ b/src/app/hooks/use-collectibles-listing-logic.ts @@ -0,0 +1,72 @@ +import { useMemo, useState } from 'react'; + +import { useDebounce } from 'use-debounce'; + +import { useAreAssetsLoading } from 'app/store/assets/selectors'; +import { useCollectiblesMetadataLoadingSelector } from 'app/store/collectibles-metadata/selectors'; +import { searchAssetsWithNoMeta } from 'lib/assets/search.utils'; +import { useCollectiblesMetadataPresenceCheck, useGetCollectibleMetadata } from 'lib/metadata'; +import { isSearchStringApplicable } from 'lib/utils/search-items'; +import { createLocationState } from 'lib/woozie/location'; + +import { ITEMS_PER_PAGE, useCollectiblesPaginationLogic } from './use-collectibles-pagination-logic'; + +export const useCollectiblesListingLogic = (allSlugsSorted: string[]) => { + const initialAmount = useMemo(() => { + const { search } = createLocationState(); + const usp = new URLSearchParams(search); + const amount = usp.get('amount'); + return amount ? Number(amount) : 0; + }, []); + + const { + slugs: paginatedSlugs, + isLoading: pageIsLoading, + loadNext + } = useCollectiblesPaginationLogic(allSlugsSorted, initialAmount); + + const assetsAreLoading = useAreAssetsLoading('collectibles'); + const metadatasLoading = useCollectiblesMetadataLoadingSelector(); + + const [searchValue, setSearchValue] = useState(''); + const [searchValueDebounced] = useDebounce(searchValue, 500); + + const isInSearchMode = isSearchStringApplicable(searchValueDebounced); + + const isSyncing = isInSearchMode ? assetsAreLoading || metadatasLoading : assetsAreLoading || pageIsLoading; + + // In `isInSearchMode === false` there might be a glitch after `assetsAreLoading` & before `pageIsLoading` + // of `isSyncing === false`. Debouncing to preserve `true` for a while. + const [isSyncingDebounced] = useDebounce(isSyncing, 500); + + const metaToCheckAndLoad = useMemo(() => { + // Search is not paginated. This is how all needed meta is loaded + if (isInSearchMode) return allSlugsSorted; + + // In pagination, loading meta for the following pages in advance, + // while not required in current page + return pageIsLoading ? undefined : allSlugsSorted.slice(paginatedSlugs.length + ITEMS_PER_PAGE * 2); + }, [isInSearchMode, pageIsLoading, allSlugsSorted, paginatedSlugs.length]); + + useCollectiblesMetadataPresenceCheck(metaToCheckAndLoad); + + const getCollectibleMeta = useGetCollectibleMetadata(); + + const displayedSlugs = useMemo( + () => + isInSearchMode + ? searchAssetsWithNoMeta(searchValueDebounced, allSlugsSorted, getCollectibleMeta, slug => slug) + : paginatedSlugs, + [isInSearchMode, paginatedSlugs, searchValueDebounced, allSlugsSorted, getCollectibleMeta] + ); + + return { + isInSearchMode, + displayedSlugs, + paginatedSlugs, + isSyncing: isSyncing || isSyncingDebounced, + loadNext, + searchValue, + setSearchValue + }; +}; diff --git a/src/app/hooks/use-collectibles-pagination-logic.ts b/src/app/hooks/use-collectibles-pagination-logic.ts new file mode 100644 index 0000000000..d63ab51c80 --- /dev/null +++ b/src/app/hooks/use-collectibles-pagination-logic.ts @@ -0,0 +1,78 @@ +import { useCallback, useState } from 'react'; + +import { useDispatch } from 'react-redux'; + +import { putCollectiblesMetadataAction } from 'app/store/collectibles-metadata/actions'; +import { useAllCollectiblesMetadataSelector } from 'app/store/collectibles-metadata/selectors'; +import { loadTokensMetadata } from 'lib/metadata/fetch'; +import { useNetwork } from 'lib/temple/front'; +import { useDidMount, useDidUpdate } from 'lib/ui/hooks'; +import { setNavigateSearchParams } from 'lib/woozie'; + +export const ITEMS_PER_PAGE = 30; + +export const useCollectiblesPaginationLogic = (allSlugsSorted: string[], initialSize: number) => { + const allMeta = useAllCollectiblesMetadataSelector(); + + const { rpcBaseURL: rpcUrl } = useNetwork(); + const dispatch = useDispatch(); + + const [slugs, setSlugs] = useState(() => allSlugsSorted.slice(0, initialSize)); + + const initialIsLoading = initialSize ? false : Boolean(allSlugsSorted.length); + const [isLoading, setIsLoading] = useState(initialIsLoading); + + const _load = useCallback( + async (size: number) => { + setIsLoading(true); + + const nextSlugs = allSlugsSorted.slice(0, size); + + const slugsWithoutMeta = nextSlugs + // Not checking metadata of loaded items + .slice(slugs.length) + .filter(slug => !allMeta.get(slug)); + + if (slugsWithoutMeta.length) + await loadTokensMetadata(rpcUrl, slugsWithoutMeta) + .then( + records => { + dispatch(putCollectiblesMetadataAction({ records })); + setSlugs(nextSlugs); + }, + error => { + console.error(error); + } + ) + .finally(() => setIsLoading(false)); + else { + setSlugs(nextSlugs); + setIsLoading(false); + } + + setNavigateSearchParams({ amount: String(size) }); + }, + [allSlugsSorted, slugs.length, allMeta, rpcUrl, dispatch] + ); + + useDidMount(() => { + if (initialIsLoading) _load(ITEMS_PER_PAGE); + }); + + useDidUpdate(() => { + if (isLoading) return; + + if (slugs.length) _load(slugs.length); + else if (allSlugsSorted.length) _load(ITEMS_PER_PAGE); + }, [allSlugsSorted]); + + const loadNext = useCallback(() => { + if (isLoading || slugs.length === allSlugsSorted.length) return; + + const size = (Math.floor(slugs.length / ITEMS_PER_PAGE) + 1) * ITEMS_PER_PAGE; + + _load(size); + }, [_load, isLoading, slugs.length, allSlugsSorted.length]); + + return { slugs, isLoading, loadNext }; +}; diff --git a/src/app/hooks/use-metadata-loading.ts b/src/app/hooks/use-metadata-loading.ts index 76bd1b5bfe..48e0ba87e1 100644 --- a/src/app/hooks/use-metadata-loading.ts +++ b/src/app/hooks/use-metadata-loading.ts @@ -1,39 +1,19 @@ -import { useEffect } from 'react'; +import { useEffect, useMemo } from 'react'; -import { isDefined } from '@rnw-community/shared'; -import { isEqual } from 'lodash'; import { useDispatch } from 'react-redux'; -import { - loadTokensMetadataAction, - loadWhitelistAction, - resetTokensMetadataLoadingAction -} from 'app/store/tokens-metadata/actions'; -import { useTokensMetadataSelector } from 'app/store/tokens-metadata/selectors'; -import { METADATA_SYNC_INTERVAL } from 'lib/fixed-times'; -import { useChainId, useTezos } from 'lib/temple/front'; -import { useAllStoredTokensSlugs } from 'lib/temple/front/assets'; -import { TempleChainId } from 'lib/temple/types'; -import { useInterval, useMemoWithCompare } from 'lib/ui/hooks'; +import { useAccountTokensSelector } from 'app/store/assets/selectors'; +import { resetTokensMetadataLoadingAction } from 'app/store/tokens-metadata/actions'; +import { useTokensMetadataPresenceCheck } from 'lib/metadata'; +import { useAccount, useChainId } from 'lib/temple/front'; export const useMetadataLoading = () => { const chainId = useChainId(true)!; + const { publicKeyHash: account } = useAccount(); const dispatch = useDispatch(); - const tezos = useTezos(); - const tokensMetadata = useTokensMetadataSelector(); - - const { data: tokensSlugs } = useAllStoredTokensSlugs(chainId); - - const slugsWithoutMetadata = useMemoWithCompare( - () => tokensSlugs?.filter(slug => !isDefined(tokensMetadata[slug])).sort(), - [tokensSlugs, tokensMetadata], - isEqual - ); - - useEffect(() => { - if (chainId === TempleChainId.Mainnet) dispatch(loadWhitelistAction.submit()); - }, [chainId]); + const tokens = useAccountTokensSelector(account, chainId); + const slugs = useMemo(() => Object.keys(tokens), [tokens]); useEffect(() => { dispatch(resetTokensMetadataLoadingAction()); @@ -41,15 +21,6 @@ export const useMetadataLoading = () => { return () => void dispatch(resetTokensMetadataLoadingAction()); }, []); - useInterval( - () => { - if (!slugsWithoutMetadata || slugsWithoutMetadata.length < 1) return; - - const rpcUrl = tezos.rpc.getRpcUrl(); - - dispatch(loadTokensMetadataAction({ rpcUrl, slugs: slugsWithoutMetadata })); - }, - METADATA_SYNC_INTERVAL, - [tezos, slugsWithoutMetadata] - ); + // TODO: Should there be a time interval? + useTokensMetadataPresenceCheck(slugs); }; diff --git a/src/app/hooks/use-metadata-refresh.ts b/src/app/hooks/use-metadata-refresh.ts index e2b715df8a..b2aea0087d 100644 --- a/src/app/hooks/use-metadata-refresh.ts +++ b/src/app/hooks/use-metadata-refresh.ts @@ -3,10 +3,10 @@ import { useEffect, useMemo } from 'react'; import { useDispatch } from 'react-redux'; import { refreshTokensMetadataAction } from 'app/store/tokens-metadata/actions'; -import { useTokensMetadataSelector } from 'app/store/tokens-metadata/selectors'; +import { useAllTokensMetadataSelector } from 'app/store/tokens-metadata/selectors'; import { fetchTokensMetadata } from 'lib/apis/temple'; -import { TokenMetadata } from 'lib/metadata'; -import { buildTokenMetadataFromFetched } from 'lib/metadata/utils'; +import { ALL_PREDEFINED_METADATAS_RECORD } from 'lib/assets/known-tokens'; +import { reduceToMetadataRecord } from 'lib/metadata/fetch'; import { useChainId } from 'lib/temple/front'; import { TempleChainId } from 'lib/temple/types'; import { useLocalStorage } from 'lib/ui/local-storage'; @@ -23,8 +23,11 @@ export const useMetadataRefresh = () => { const [records, setRecords] = useLocalStorage(STORAGE_KEY, {}); - const tokensMetadata = useTokensMetadataSelector(); - const slugsOnAppLoad = useMemo(() => Object.keys(tokensMetadata), []); + const tokensMetadata = useAllTokensMetadataSelector(); + const slugsOnAppLoad = useMemo( + () => Object.keys(tokensMetadata).filter(slug => !ALL_PREDEFINED_METADATAS_RECORD[slug]), + [] + ); useEffect(() => { const lastVersion = records[chainId]; @@ -38,27 +41,15 @@ export const useMetadataRefresh = () => { return; } - if (!needToSetVersion) return; + if (!needToSetVersion || chainId !== TempleChainId.Mainnet) return; - if (chainId === TempleChainId.Mainnet) { - fetchTokensMetadata(chainId, slugsOnAppLoad) - .then(data => - data.reduce((acc, token, index) => { - const slug = slugsOnAppLoad[index]!; - const [address, id] = slug.split('_'); - - const metadata = buildTokenMetadataFromFetched(token, address, Number(id)); - - return metadata ? acc.concat(metadata) : acc; - }, []) - ) - .then( - data => { - if (data.length) dispatch(refreshTokensMetadataAction(data)); - setLastVersion(); - }, - error => console.error(error) - ); - } + fetchTokensMetadata(chainId, slugsOnAppLoad).then( + data => { + const record = reduceToMetadataRecord(slugsOnAppLoad, data); + dispatch(refreshTokensMetadataAction(record)); + setLastVersion(); + }, + error => console.error(error) + ); }, [chainId]); }; diff --git a/src/app/hooks/use-tokens-listing-logic.ts b/src/app/hooks/use-tokens-listing-logic.ts new file mode 100644 index 0000000000..99174aafc6 --- /dev/null +++ b/src/app/hooks/use-tokens-listing-logic.ts @@ -0,0 +1,90 @@ +import { useCallback, useMemo, useState } from 'react'; + +import { isDefined } from '@rnw-community/shared'; +import { isEqual } from 'lodash'; +import { useDebounce } from 'use-debounce'; + +import { toTokenSlug } from 'lib/assets'; +import { searchAssetsWithNoMeta } from 'lib/assets/search.utils'; +import { useTokensSortPredicate } from 'lib/assets/use-sorting'; +import { useCurrentAccountBalances } from 'lib/balances'; +import { useGetTokenOrGasMetadata } from 'lib/metadata'; +import { useMemoWithCompare } from 'lib/ui/hooks'; +import { isSearchStringApplicable } from 'lib/utils/search-items'; + +export const useTokensListingLogic = ( + assetsSlugs: string[], + filterZeroBalances = false, + leadingAssets?: string[], + leadingAssetsAreFilterable = false +) => { + const nonLeadingAssets = useMemo( + () => (leadingAssets?.length ? assetsSlugs.filter(slug => !leadingAssets.includes(slug)) : assetsSlugs), + [assetsSlugs, leadingAssets] + ); + + const balances = useCurrentAccountBalances(); + const isNonZeroBalance = useCallback( + (slug: string) => { + const balance = balances[slug]; + return isDefined(balance) && balance !== '0'; + }, + [balances] + ); + + const sourceArray = useMemo( + () => (filterZeroBalances ? nonLeadingAssets.filter(isNonZeroBalance) : nonLeadingAssets), + [filterZeroBalances, nonLeadingAssets, isNonZeroBalance] + ); + + const [searchValue, setSearchValue] = useState(''); + const [tokenId, setTokenId] = useState(); + const [searchValueDebounced] = useDebounce(tokenId ? toTokenSlug(searchValue, String(tokenId)) : searchValue, 300); + + const assetsSortPredicate = useTokensSortPredicate(); + const getMetadata = useGetTokenOrGasMetadata(); + + const searchedSlugs = useMemo( + () => + isSearchStringApplicable(searchValueDebounced) + ? searchAssetsWithNoMeta(searchValueDebounced, sourceArray, getMetadata, slug => slug) + : [...sourceArray].sort(assetsSortPredicate), + [searchValueDebounced, sourceArray, getMetadata, assetsSortPredicate] + ); + + const filteredAssets = useMemoWithCompare( + () => { + if (!isDefined(leadingAssets) || !leadingAssets.length) return searchedSlugs; + + const filteredLeadingSlugs = + leadingAssetsAreFilterable && filterZeroBalances ? leadingAssets.filter(isNonZeroBalance) : leadingAssets; + + const searchedLeadingSlugs = searchAssetsWithNoMeta( + searchValueDebounced, + filteredLeadingSlugs, + getMetadata, + slug => slug + ); + + return searchedLeadingSlugs.length ? searchedLeadingSlugs.concat(searchedSlugs) : searchedSlugs; + }, + [ + leadingAssets, + leadingAssetsAreFilterable, + filterZeroBalances, + isNonZeroBalance, + searchedSlugs, + searchValueDebounced, + getMetadata + ], + isEqual + ); + + return { + filteredAssets, + searchValue, + setSearchValue, + tokenId, + setTokenId + }; +}; diff --git a/src/app/hooks/use-tokens-loading.ts b/src/app/hooks/use-tokens-loading.ts deleted file mode 100644 index 7179c1b9ff..0000000000 --- a/src/app/hooks/use-tokens-loading.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { TOKENS_SYNC_INTERVAL } from 'lib/fixed-times'; -import { useSyncTokens } from 'lib/temple/front/sync-tokens'; -import { useInterval } from 'lib/ui/hooks'; - -export const useTokensLoading = () => { - const { syncTokens } = useSyncTokens(); - - useInterval(syncTokens, TOKENS_SYNC_INTERVAL, [syncTokens]); -}; diff --git a/src/app/layouts/PageLayout/Header/AccountDropdown/index.tsx b/src/app/layouts/PageLayout/Header/AccountDropdown/index.tsx index ead6b411bb..59b3d2eb58 100644 --- a/src/app/layouts/PageLayout/Header/AccountDropdown/index.tsx +++ b/src/app/layouts/PageLayout/Header/AccountDropdown/index.tsx @@ -15,9 +15,10 @@ import { ReactComponent as MaximiseIcon } from 'app/icons/maximise.svg'; import { ReactComponent as SadSearchIcon } from 'app/icons/sad-search.svg'; import { ReactComponent as SettingsIcon } from 'app/icons/settings.svg'; import SearchField from 'app/templates/SearchField'; +import { useGasToken } from 'lib/assets/hooks'; import { searchHotkey } from 'lib/constants'; import { T, t } from 'lib/i18n'; -import { useAccount, useRelevantAccounts, useSetAccountPkh, useTempleClient, useGasToken } from 'lib/temple/front'; +import { useAccount, useRelevantAccounts, useSetAccountPkh, useTempleClient } from 'lib/temple/front'; import { PopperRenderProps } from 'lib/ui/Popper'; import { searchAndFilterItems } from 'lib/utils/search-items'; import { HistoryAction, navigate } from 'lib/woozie'; diff --git a/src/app/layouts/PageLayout/ShortcutAccountSwitchOverlay/index.tsx b/src/app/layouts/PageLayout/ShortcutAccountSwitchOverlay/index.tsx index 99a39257fe..b4d9f60026 100644 --- a/src/app/layouts/PageLayout/ShortcutAccountSwitchOverlay/index.tsx +++ b/src/app/layouts/PageLayout/ShortcutAccountSwitchOverlay/index.tsx @@ -9,9 +9,10 @@ import { useAccountSelectShortcut } from 'app/hooks/use-account-select-shortcut' import { useModalScrollLock } from 'app/hooks/use-modal-scroll-lock'; import { ReactComponent as SadSearchIcon } from 'app/icons/sad-search.svg'; import SearchField from 'app/templates/SearchField'; +import { useGasToken } from 'lib/assets/hooks'; import { searchHotkey } from 'lib/constants'; import { T, t } from 'lib/i18n'; -import { useAccount, useRelevantAccounts, useSetAccountPkh, useGasToken } from 'lib/temple/front'; +import { useAccount, useRelevantAccounts, useSetAccountPkh } from 'lib/temple/front'; import Portal from 'lib/ui/Portal'; import { searchAndFilterItems } from 'lib/utils/search-items'; import { HistoryAction, navigate } from 'lib/woozie'; diff --git a/src/app/pages/AddAsset/AddAsset.tsx b/src/app/pages/AddAsset/AddAsset.tsx index 8acbac26e7..83cc345c9c 100644 --- a/src/app/pages/AddAsset/AddAsset.tsx +++ b/src/app/pages/AddAsset/AddAsset.tsx @@ -1,4 +1,4 @@ -import React, { FC, ReactNode, useCallback, useEffect, useRef, useMemo } from 'react'; +import React, { FC, memo, ReactNode, useCallback, useEffect, useRef, useMemo } from 'react'; import { ContractAbstraction, ContractProvider, Wallet } from '@taquito/taquito'; import classNames from 'clsx'; @@ -11,7 +11,9 @@ import { Alert, FormField, FormSubmitButton, NoSpaceField } from 'app/atoms'; import Spinner from 'app/atoms/Spinner/Spinner'; import { ReactComponent as AddIcon } from 'app/icons/add.svg'; import PageLayout from 'app/layouts/PageLayout'; -import { addTokensMetadataAction } from 'app/store/tokens-metadata/actions'; +import { putTokensAsIsAction, putCollectiblesAsIsAction } from 'app/store/assets/actions'; +import { putCollectiblesMetadataAction } from 'app/store/collectibles-metadata/actions'; +import { putTokensMetadataAction } from 'app/store/tokens-metadata/actions'; import { useFormAnalytics } from 'lib/analytics'; import { TokenMetadataResponse } from 'lib/apis/temple'; import { toTokenSlug } from 'lib/assets'; @@ -23,12 +25,11 @@ import { } from 'lib/assets/standards'; import { getBalanceSWRKey } from 'lib/balances'; import { T, t } from 'lib/i18n'; -import type { TokenMetadata } from 'lib/metadata'; +import { isCollectible, TokenMetadata } from 'lib/metadata'; import { fetchOneTokenMetadata } from 'lib/metadata/fetch'; import { TokenMetadataNotFoundError } from 'lib/metadata/on-chain'; import { loadContract } from 'lib/temple/contract'; import { useTezos, useNetwork, useChainId, useAccount, validateContractAddress } from 'lib/temple/front'; -import * as Repo from 'lib/temple/repo'; import { useSafeState } from 'lib/ui/hooks'; import { delay } from 'lib/utils'; import { navigate } from 'lib/woozie'; @@ -75,7 +76,7 @@ const INITIAL_STATE: ComponentState = { class ContractNotFoundError extends Error {} -const Form: FC = () => { +const Form = memo(() => { const tezos = useTezos(); const { id: networkId } = useNetwork(); const chainId = useChainId(true)!; @@ -132,7 +133,7 @@ const Form: FC = () => { if (tokenStandard === 'fa2') await assertFa2TokenDefined(tezos, contract, tokenId); const rpcUrl = tezos.rpc.getRpcUrl(); - const metadata = await fetchOneTokenMetadata(rpcUrl, contractAddress, tokenId); + const metadata = await fetchOneTokenMetadata(rpcUrl, contractAddress, String(tokenId)); if (metadata) { metadataRef.current = metadata; @@ -207,21 +208,23 @@ const Form: FC = () => { const tokenMetadata: TokenMetadata = { ...baseMetadata, address: contractAddress, - id: tokenId + id: String(tokenId) }; - dispatch(addTokensMetadataAction([tokenMetadata])); + const assetIsCollectible = isCollectible(tokenMetadata); - await Repo.accountTokens.put( - { - chainId, - account: accountPkh, - tokenSlug, - status: Repo.ITokenStatus.Enabled, - addedAt: Date.now() - }, - Repo.toAccountTokenKey(chainId, accountPkh, tokenSlug) - ); + const actionPayload = { records: { [tokenSlug]: tokenMetadata } }; + if (assetIsCollectible) dispatch(putCollectiblesMetadataAction(actionPayload)); + else dispatch(putTokensMetadataAction(actionPayload)); + + const asset = { + chainId, + account: accountPkh, + slug: tokenSlug, + status: 'enabled' as const + }; + + dispatch(assetIsCollectible ? putCollectiblesAsIsAction([asset]) : putTokensAsIsAction([asset])); swrCache.delete(unstable_serialize(getBalanceSWRKey(tezos, tokenSlug, accountPkh))); @@ -326,7 +329,7 @@ const Form: FC = () => { )} ); -}; +}); type BottomSectionProps = Pick & { submitError?: ReactNode; diff --git a/src/app/pages/Collectibles/CollectiblePage/CollectiblePageImage.tsx b/src/app/pages/Collectibles/CollectiblePage/CollectiblePageImage.tsx index 6724a2c0a4..3908f1fabb 100644 --- a/src/app/pages/Collectibles/CollectiblePage/CollectiblePageImage.tsx +++ b/src/app/pages/Collectibles/CollectiblePage/CollectiblePageImage.tsx @@ -2,9 +2,8 @@ import React, { memo, useCallback, useEffect, useState } from 'react'; import { Model3DViewer } from 'app/atoms/Model3DViewer'; import { AssetImage } from 'app/templates/AssetImage'; +import { isSvgDataUriInUtf8Encoding, buildObjktCollectibleArtifactUri } from 'lib/images-uri'; import { TokenMetadata } from 'lib/metadata'; -import { isSvgDataUriInUtf8Encoding, buildObjktCollectibleArtifactUri } from 'lib/temple/front'; -import { Image } from 'lib/ui/Image'; import { AudioCollectible } from '../components/AudioCollectible'; import { CollectibleBlur } from '../components/CollectibleBlur'; @@ -42,15 +41,7 @@ export const CollectiblePageImage = memo( if (objktArtifactUri && !isRenderFailedOnce) { if (isSvgDataUriInUtf8Encoding(objktArtifactUri)) { - return ( - {metadata?.name}} - onError={handleError} - className={className} - /> - ); + return {metadata?.name}; } if (mime) { diff --git a/src/app/pages/Collectibles/CollectiblePage/PropertiesItems.tsx b/src/app/pages/Collectibles/CollectiblePage/PropertiesItems.tsx index 5753643449..990dabdb7f 100644 --- a/src/app/pages/Collectibles/CollectiblePage/PropertiesItems.tsx +++ b/src/app/pages/Collectibles/CollectiblePage/PropertiesItems.tsx @@ -5,8 +5,9 @@ import BigNumber from 'bignumber.js'; import { HashChip, ExternalLinkChip } from 'app/atoms'; import type { CollectibleDetails } from 'app/store/collectibles/state'; import { fromFa2TokenSlug } from 'lib/assets/utils'; +import { useBalance } from 'lib/balances'; import { formatDate } from 'lib/i18n'; -import { useBalance, useExplorerBaseUrls } from 'lib/temple/front'; +import { useExplorerBaseUrls } from 'lib/temple/front'; interface PropertiesItemsProps { assetSlug: string; diff --git a/src/app/pages/Collectibles/CollectiblePage/index.tsx b/src/app/pages/Collectibles/CollectiblePage/index.tsx index 32cdfa8e1c..5cdc2ec224 100644 --- a/src/app/pages/Collectibles/CollectiblePage/index.tsx +++ b/src/app/pages/Collectibles/CollectiblePage/index.tsx @@ -11,20 +11,22 @@ import { useAllCollectiblesDetailsLoadingSelector, useCollectibleDetailsSelector } from 'app/store/collectibles/selectors'; -import { useTokenMetadataSelector } from 'app/store/tokens-metadata/selectors'; +import { useCollectibleMetadataSelector } from 'app/store/collectibles-metadata/selectors'; import AddressChip from 'app/templates/AddressChip'; import OperationStatus from 'app/templates/OperationStatus'; import { TabsBar } from 'app/templates/TabBar'; -import { objktCurrencies } from 'lib/apis/objkt'; +import { fetchCollectibleExtraDetails, objktCurrencies } from 'lib/apis/objkt'; +import { fromAssetSlug } from 'lib/assets'; import { BLOCK_DURATION } from 'lib/fixed-times'; import { t, T } from 'lib/i18n'; +import { buildTokenImagesStack } from 'lib/images-uri'; import { getAssetName } from 'lib/metadata'; +import { useRetryableSWR } from 'lib/swr'; import { useAccount } from 'lib/temple/front'; -import { formatTcInfraImgUri } from 'lib/temple/front/image-uri'; import { atomsToTokens } from 'lib/temple/helpers'; import { TempleAccountType } from 'lib/temple/types'; import { useInterval } from 'lib/ui/hooks'; -import { Image } from 'lib/ui/Image'; +import { ImageStacked } from 'lib/ui/ImageStacked'; import { navigate } from 'lib/woozie'; import { useCollectibleSelling } from '../hooks/use-collectible-selling.hook'; @@ -41,12 +43,23 @@ interface Props { } const CollectiblePage = memo(({ assetSlug }) => { - const metadata = useTokenMetadataSelector(assetSlug); + const metadata = useCollectibleMetadataSelector(assetSlug); // Loaded only, if shown in grid for now const details = useCollectibleDetailsSelector(assetSlug); const areAnyCollectiblesDetailsLoading = useAllCollectiblesDetailsLoadingSelector(); const account = useAccount(); + const [contractAddress, tokenId] = fromAssetSlug(assetSlug); + + const { data: extraDetails } = useRetryableSWR( + ['fetchCollectibleExtraDetails', contractAddress, tokenId], + () => (tokenId ? fetchCollectibleExtraDetails(contractAddress, tokenId) : Promise.resolve(null)), + { + refreshInterval: DETAILS_SYNC_INTERVAL + } + ); + const offers = extraDetails?.offers_active; + const { publicKeyHash } = account; const accountCanSign = account.type !== TempleAccountType.WatchOnly; @@ -58,7 +71,7 @@ const CollectiblePage = memo(({ assetSlug }) => { () => details && { title: details.galleries[0]?.title ?? details.fa.name, - logo: [formatTcInfraImgUri(details.fa.logo, 'small'), formatTcInfraImgUri(details.fa.logo, 'medium')] + logo: buildTokenImagesStack(details.fa.logo) }, [details] ); @@ -66,8 +79,8 @@ const CollectiblePage = memo(({ assetSlug }) => { const creators = details?.creators ?? []; const takableOffer = useMemo( - () => details?.offers.find(({ buyer_address }) => buyer_address !== publicKeyHash), - [details, publicKeyHash] + () => offers?.find(({ buyer_address }) => buyer_address !== publicKeyHash), + [offers, publicKeyHash] ); const { @@ -86,7 +99,7 @@ const CollectiblePage = memo(({ assetSlug }) => { ]); const displayedOffer = useMemo(() => { - const highestOffer = details?.offers[0]; + const highestOffer = offers?.[0]; if (!isDefined(highestOffer)) return null; const offer = takableOffer ?? highestOffer; @@ -99,7 +112,7 @@ const CollectiblePage = memo(({ assetSlug }) => { const price = atomsToTokens(offer.price, currency.decimals); return { price, symbol: currency.symbol, buyerIsMe }; - }, [details?.offers, takableOffer, publicKeyHash]); + }, [offers, takableOffer, publicKeyHash]); const sellButtonTooltipStr = useMemo(() => { if (!displayedOffer) return; @@ -162,7 +175,7 @@ const CollectiblePage = memo(({ assetSlug }) => { {collection && (
- +
{collection?.title ?? ''}
diff --git a/src/app/pages/Collectibles/CollectiblesTab/CollectibleItem.tsx b/src/app/pages/Collectibles/CollectiblesTab/CollectibleItem.tsx index b5587ffdf9..0ef4e717b1 100644 --- a/src/app/pages/Collectibles/CollectiblesTab/CollectibleItem.tsx +++ b/src/app/pages/Collectibles/CollectiblesTab/CollectibleItem.tsx @@ -1,21 +1,20 @@ -import React, { memo, useCallback, useRef, useState, useMemo } from 'react'; +import React, { memo, useRef, useMemo } from 'react'; import { isDefined } from '@rnw-community/shared'; import clsx from 'clsx'; import Money from 'app/atoms/Money'; import { useAppEnv } from 'app/env'; +import { useBalanceSelector } from 'app/store/balances/selectors'; import { useAllCollectiblesDetailsLoadingSelector, useCollectibleDetailsSelector } from 'app/store/collectibles/selectors'; -import { useTokenMetadataSelector } from 'app/store/tokens-metadata/selectors'; +import { useCollectibleMetadataSelector } from 'app/store/collectibles-metadata/selectors'; import { objktCurrencies } from 'lib/apis/objkt'; import { T } from 'lib/i18n'; import { getAssetName } from 'lib/metadata'; -import { useBalance } from 'lib/temple/front'; import { atomsToTokens } from 'lib/temple/helpers'; -import { useIntersectionDetection } from 'lib/ui/use-intersection-detection'; import { Link } from 'lib/woozie'; import { CollectibleItemImage } from './CollectibleItemImage'; @@ -23,15 +22,23 @@ import { CollectibleItemImage } from './CollectibleItemImage'; interface Props { assetSlug: string; accountPkh: string; + chainId: string; areDetailsShown: boolean; + hideWithoutMeta?: boolean; } -export const CollectibleItem = memo(({ assetSlug, accountPkh, areDetailsShown }) => { +export const CollectibleItem = memo(({ assetSlug, accountPkh, chainId, areDetailsShown, hideWithoutMeta }) => { const { popup } = useAppEnv(); - const metadata = useTokenMetadataSelector(assetSlug); - const toDisplayRef = useRef(null); - const [displayed, setDisplayed] = useState(true); - const { data: balance } = useBalance(assetSlug, accountPkh, { displayed, suspense: false }); + const metadata = useCollectibleMetadataSelector(assetSlug); + const wrapperElemRef = useRef(null); + const balanceAtomic = useBalanceSelector(accountPkh, chainId, assetSlug); + + const decimals = metadata?.decimals; + + const balance = useMemo( + () => (isDefined(decimals) && balanceAtomic ? atomsToTokens(balanceAtomic, decimals) : null), + [balanceAtomic, decimals] + ); const areDetailsLoading = useAllCollectiblesDetailsLoadingSelector(); const details = useCollectibleDetailsSelector(assetSlug); @@ -45,34 +52,56 @@ export const CollectibleItem = memo(({ assetSlug, accountPkh, areDetailsS if (!isDefined(currency)) return null; - return { floorPrice, decimals: currency.decimals, symbol: currency.symbol }; - }, [details]); + return { floorPrice: atomsToTokens(floorPrice, currency.decimals).toString(), symbol: currency.symbol }; + }, [details?.listing]); + + // Fixed sizes to improve large grid performance + const [style, imgWrapStyle] = useMemo(() => { + const size = popup ? 106 : 125; - const handleIntersection = useCallback(() => void setDisplayed(true), []); + const style = popup + ? { + width: size, + height: areDetailsShown ? 152 : size + } + : { + width: size, + height: areDetailsShown ? 171 : size + }; - useIntersectionDetection(toDisplayRef, handleIntersection, !displayed); + const imgWrapStyle = { + height: size - 2 + }; + + return [style, imgWrapStyle]; + }, [areDetailsShown, popup]); + + if (hideWithoutMeta && !metadata) return null; const assetName = getAssetName(metadata); return ( - +
- {displayed && ( - - )} + {areDetailsShown && balance ? (
@@ -82,16 +111,17 @@ export const CollectibleItem = memo(({ assetSlug, accountPkh, areDetailsS
{areDetailsShown && ( -
+
{assetName}
-
+
:{' '} + {isDefined(listing) ? ( <> - {atomsToTokens(listing.floorPrice, listing.decimals)} + {listing.floorPrice} {listing.symbol} diff --git a/src/app/pages/Collectibles/CollectiblesTab/CollectibleItemImage.tsx b/src/app/pages/Collectibles/CollectiblesTab/CollectibleItemImage.tsx index 23df78127d..c05c033362 100644 --- a/src/app/pages/Collectibles/CollectiblesTab/CollectibleItemImage.tsx +++ b/src/app/pages/Collectibles/CollectiblesTab/CollectibleItemImage.tsx @@ -1,10 +1,13 @@ -import React, { memo, useMemo } from 'react'; +import React, { memo, useMemo, useState } from 'react'; import { isDefined } from '@rnw-community/shared'; +import { debounce } from 'lodash'; import { useCollectibleIsAdultSelector } from 'app/store/collectibles/selectors'; -import { AssetImage } from 'app/templates/AssetImage'; +import { buildCollectibleImagesStack } from 'lib/images-uri'; import type { TokenMetadata } from 'lib/metadata'; +import { ImageStacked } from 'lib/ui/ImageStacked'; +import { useIntersectionDetection } from 'lib/ui/use-intersection-detection'; import { CollectibleBlur } from '../components/CollectibleBlur'; import { CollectibleImageFallback } from '../components/CollectibleImageFallback'; @@ -15,27 +18,39 @@ interface Props { metadata?: TokenMetadata; areDetailsLoading: boolean; mime?: string | null; + containerElemRef: React.RefObject; } -export const CollectibleItemImage = memo(({ assetSlug, metadata, areDetailsLoading, mime }) => { - const isAdultContent = useCollectibleIsAdultSelector(assetSlug); - const isAdultFlagLoading = areDetailsLoading && !isDefined(isAdultContent); - - const isAudioCollectible = useMemo(() => Boolean(mime && mime.startsWith('audio')), [mime]); - - if (isAdultFlagLoading) { - return ; +export const CollectibleItemImage = memo( + ({ assetSlug, metadata, areDetailsLoading, mime, containerElemRef }) => { + const isAdultContent = useCollectibleIsAdultSelector(assetSlug); + const isAdultFlagLoading = areDetailsLoading && !isDefined(isAdultContent); + + const sources = useMemo(() => (metadata ? buildCollectibleImagesStack(metadata) : []), [metadata]); + + const isAudioCollectible = useMemo(() => Boolean(mime && mime.startsWith('audio')), [mime]); + + const [isInViewport, setIsInViewport] = useState(false); + const handleIntersection = useMemo(() => debounce(setIsInViewport, 500), []); + + useIntersectionDetection(containerElemRef, handleIntersection, true, 800); + + return ( +
+ {isAdultFlagLoading ? ( + + ) : isAdultContent ? ( + + ) : ( + } + fallback={} + /> + )} +
+ ); } - - if (isAdultContent) { - return ; - } - - return ( - } - fallback={} - /> - ); -}); +); diff --git a/src/app/pages/Collectibles/CollectiblesTab/index.tsx b/src/app/pages/Collectibles/CollectiblesTab/index.tsx index 4ffd45ad9c..46a20286e4 100644 --- a/src/app/pages/Collectibles/CollectiblesTab/index.tsx +++ b/src/app/pages/Collectibles/CollectiblesTab/index.tsx @@ -1,4 +1,4 @@ -import React, { FC, memo, useCallback, useEffect } from 'react'; +import React, { FC, memo, useCallback, useEffect, useMemo } from 'react'; import clsx from 'clsx'; import { isEqual } from 'lodash'; @@ -7,20 +7,21 @@ import { SyncSpinner } from 'app/atoms'; import Checkbox from 'app/atoms/Checkbox'; import Divider from 'app/atoms/Divider'; import DropdownWrapper from 'app/atoms/DropdownWrapper'; +import { SimpleInfiniteScroll } from 'app/atoms/SimpleInfiniteScroll'; import { useAppEnv } from 'app/env'; +import { useCollectiblesListingLogic } from 'app/hooks/use-collectibles-listing-logic'; import { ReactComponent as EditingIcon } from 'app/icons/editing.svg'; import { AssetsSelectors } from 'app/pages/Home/OtherComponents/Assets.selectors'; -import { useTokensMetadataLoadingSelector } from 'app/store/tokens-metadata/selectors'; import { ButtonForManageDropdown } from 'app/templates/ManageDropdown'; import SearchAssetField from 'app/templates/SearchAssetField'; +import { useEnabledAccountCollectiblesSlugs } from 'lib/assets/hooks'; import { AssetTypesEnum } from 'lib/assets/types'; -import { useFilteredAssetsSlugs } from 'lib/assets/use-filtered'; +import { useCollectiblesSortPredicate } from 'lib/assets/use-sorting'; import { T, t } from 'lib/i18n'; -import { useAccount, useChainId, useCollectibleTokens } from 'lib/temple/front'; -import { useSyncTokens } from 'lib/temple/front/sync-tokens'; +import { useAccount, useChainId } from 'lib/temple/front'; import { useMemoWithCompare } from 'lib/ui/hooks'; import { useLocalStorage } from 'lib/ui/local-storage'; -import Popper, { PopperRenderProps } from 'lib/ui/Popper'; +import Popper, { PopperChildren, PopperPopup, PopperRenderProps } from 'lib/ui/Popper'; import { Link } from 'lib/woozie'; import { CollectibleItem } from './CollectibleItem'; @@ -32,39 +33,74 @@ interface Props { } export const CollectiblesTab = memo(({ scrollToTheTabsBar }) => { - const chainId = useChainId(true)!; const { popup } = useAppEnv(); const { publicKeyHash } = useAccount(); - const { isSyncing: tokensAreSyncing } = useSyncTokens(); - const metadatasLoading = useTokensMetadataLoadingSelector(); + const chainId = useChainId()!; const [areDetailsShown, setDetailsShown] = useLocalStorage(LOCAL_STORAGE_TOGGLE_KEY, false); - const toggleDetailsShown = useCallback(() => void setDetailsShown(val => !val), [setDetailsShown]); - const { data: collectibles = [], isValidating: readingCollectibles } = useCollectibleTokens( - chainId, - publicKeyHash, - true - ); + const allSlugs = useEnabledAccountCollectiblesSlugs(); + + const assetsSortPredicate = useCollectiblesSortPredicate(); - const collectiblesSlugs = useMemoWithCompare( - () => collectibles.map(collectible => collectible.tokenSlug).sort(), - [collectibles], + const allSlugsSorted = useMemoWithCompare( + () => [...allSlugs].sort(assetsSortPredicate), + [allSlugs, assetsSortPredicate], isEqual ); - const { filteredAssets, searchValue, setSearchValue } = useFilteredAssetsSlugs(collectiblesSlugs, false); + const { isInSearchMode, displayedSlugs, paginatedSlugs, isSyncing, loadNext, searchValue, setSearchValue } = + useCollectiblesListingLogic(allSlugsSorted); + + const shouldScrollToTheTabsBar = paginatedSlugs.length > 0; + useEffect(() => { + if (shouldScrollToTheTabsBar) void scrollToTheTabsBar(); + }, [shouldScrollToTheTabsBar, scrollToTheTabsBar]); + + const contentElement = useMemo( + () => ( +
+ {displayedSlugs.map(slug => ( + + ))} +
+ ), + [displayedSlugs, publicKeyHash, chainId, areDetailsShown, isInSearchMode] + ); - const shouldScrollToTheTabsBar = collectibles.length > 0; - useEffect(() => void scrollToTheTabsBar(), [shouldScrollToTheTabsBar, scrollToTheTabsBar]); + const renderManageDropdown = useCallback( + props => ( + + ), + [areDetailsShown, toggleDetailsShown] + ); - const isSyncing = tokensAreSyncing || metadatasLoading || readingCollectibles; + const renderManageButton = useCallback( + ({ ref, opened, toggleOpened }) => ( + + ), + [] + ); return (
-
+
(({ scrollToTheTabsBar }) => { testID={AssetsSelectors.searchAssetsInputCollectibles} /> - ( - - )} - > - {({ ref, opened, toggleOpened }) => ( - - )} + + {renderManageButton}
- {filteredAssets.length === 0 ? ( + {displayedSlugs.length === 0 ? ( buildEmptySection(isSyncing) ) : ( <> -
- {filteredAssets.map(slug => ( - - ))} -
+ {isInSearchMode ? ( + contentElement + ) : ( + {contentElement} + )} {isSyncing && } diff --git a/src/app/pages/Collectibles/components/AudioCollectible.tsx b/src/app/pages/Collectibles/components/AudioCollectible.tsx index 2344aa221e..ffb20ab4be 100644 --- a/src/app/pages/Collectibles/components/AudioCollectible.tsx +++ b/src/app/pages/Collectibles/components/AudioCollectible.tsx @@ -42,8 +42,8 @@ export const AudioCollectible: FC = ({ uri, metadata, className, style, l fallback={} className={className} style={style} - onLoad={handleImageLoaded} - onError={handleImageLoaded} + onStackLoaded={handleImageLoaded} + onStackFailed={handleImageLoaded} /> {!ready && loader} diff --git a/src/app/pages/Collectibles/components/CollectibleImageFallback.tsx b/src/app/pages/Collectibles/components/CollectibleImageFallback.tsx index c3597db114..1805b95a27 100644 --- a/src/app/pages/Collectibles/components/CollectibleImageFallback.tsx +++ b/src/app/pages/Collectibles/components/CollectibleImageFallback.tsx @@ -1,14 +1,14 @@ -import React, { FC } from 'react'; +import React, { memo } from 'react'; import { ReactComponent as BrokenImageSvg } from 'app/icons/broken-image.svg'; import { ReactComponent as MusicSvg } from 'app/icons/music.svg'; -interface ImageFallbackProps { +interface Props { large?: boolean; isAudioCollectible?: boolean; } -export const CollectibleImageFallback: FC = ({ large = false, isAudioCollectible = false }) => { +export const CollectibleImageFallback = memo(({ large = false, isAudioCollectible = false }) => { const height = large ? '23%' : '32%'; return ( @@ -16,4 +16,4 @@ export const CollectibleImageFallback: FC = ({ large = false {isAudioCollectible ? : }
); -}; +}); diff --git a/src/app/pages/Collectibles/hooks/use-collectible-selling.hook.ts b/src/app/pages/Collectibles/hooks/use-collectible-selling.hook.ts index c7a79fc0a3..7bd16ed2db 100644 --- a/src/app/pages/Collectibles/hooks/use-collectible-selling.hook.ts +++ b/src/app/pages/Collectibles/hooks/use-collectible-selling.hook.ts @@ -3,9 +3,9 @@ import { useCallback, useState } from 'react'; import type { WalletOperation } from '@taquito/taquito'; import BigNumber from 'bignumber.js'; -import type { CollectibleDetails } from 'app/store/collectibles/state'; import { useFormAnalytics } from 'lib/analytics'; import { getObjktMarketplaceContract } from 'lib/apis/objkt'; +import type { ObjktOffer } from 'lib/apis/objkt/types'; import { fromFa2TokenSlug } from 'lib/assets/utils'; import { useAccount, useTezos } from 'lib/temple/front'; import { getTransferPermissions } from 'lib/utils/get-transfer-permissions'; @@ -13,7 +13,7 @@ import { parseTransferParamsToParamsWithKind } from 'lib/utils/parse-transfer-pa const DEFAULT_OBJKT_STORAGE_LIMIT = 350; -export const useCollectibleSelling = (assetSlug: string, offer?: CollectibleDetails['offers'][number]) => { +export const useCollectibleSelling = (assetSlug: string, offer?: ObjktOffer) => { const tezos = useTezos(); const { publicKeyHash } = useAccount(); const [isSelling, setIsSelling] = useState(false); diff --git a/src/app/pages/Home/ActionButtonsBar.tsx b/src/app/pages/Home/ActionButtonsBar.tsx new file mode 100644 index 0000000000..4cdfca97ed --- /dev/null +++ b/src/app/pages/Home/ActionButtonsBar.tsx @@ -0,0 +1,148 @@ +import React, { memo, FunctionComponent, SVGProps, useMemo } from 'react'; + +import clsx from 'clsx'; +import { Props as TippyProps } from 'tippy.js'; + +import { Anchor } from 'app/atoms'; +import { ReactComponent as BuyIcon } from 'app/icons/buy.svg'; +import { ReactComponent as ReceiveIcon } from 'app/icons/receive.svg'; +import { ReactComponent as SendIcon } from 'app/icons/send-alt.svg'; +import { ReactComponent as SwapIcon } from 'app/icons/swap.svg'; +import { ReactComponent as WithdrawIcon } from 'app/icons/withdraw.svg'; +import { TestIDProps } from 'lib/analytics'; +import { TID, T, t } from 'lib/i18n'; +import { useAccount, useNetwork } from 'lib/temple/front'; +import { TempleAccountType, TempleNetworkType } from 'lib/temple/types'; +import useTippy from 'lib/ui/useTippy'; +import { createUrl, Link, To } from 'lib/woozie'; +import { createLocationState } from 'lib/woozie/location'; + +import { HomeSelectors } from './Home.selectors'; + +const tippyPropsMock = { + trigger: 'mouseenter', + hideOnClick: false, + content: t('disabledForWatchOnlyAccount'), + animation: 'shift-away-subtle' +}; + +const NETWORK_TYPES_WITH_BUY_BUTTON: TempleNetworkType[] = ['main', 'dcp']; + +interface Props { + assetSlug: string | nullish; +} + +export const ActionButtonsBar = memo(({ assetSlug }) => { + const account = useAccount(); + const network = useNetwork(); + + const canSend = account.type !== TempleAccountType.WatchOnly; + const sendLink = assetSlug ? `/send/${assetSlug}` : '/send'; + + const swapLink = useMemo( + () => ({ + pathname: '/swap', + search: `from=${assetSlug ?? ''}` + }), + [assetSlug] + ); + + return ( +
+ + + + + + +
+ ); +}); + +interface ActionButtonProps extends TestIDProps { + labelI18nKey: TID; + Icon: FunctionComponent>; + to: To; + disabled?: boolean; + isAnchor?: boolean; + tippyProps?: Partial; +} + +const ActionButton = memo( + ({ labelI18nKey, Icon, to, disabled, isAnchor, tippyProps = {}, testID, testIDProperties }) => { + const buttonRef = useTippy({ + ...tippyProps, + content: disabled && !tippyProps.content ? t('disabled') : tippyProps.content + }); + + const commonButtonProps = useMemo( + () => ({ + className: `flex flex-col items-center`, + type: 'button' as const, + children: ( + <> +
+ +
+ + + + + + ) + }), + [disabled, Icon, labelI18nKey] + ); + + if (disabled) { + return
); -}; +}); diff --git a/src/app/pages/Home/OtherComponents/Tokens/Tokens.tsx b/src/app/pages/Home/OtherComponents/Tokens/Tokens.tsx index daa927f6a6..fe43ff8e2e 100644 --- a/src/app/pages/Home/OtherComponents/Tokens/Tokens.tsx +++ b/src/app/pages/Home/OtherComponents/Tokens/Tokens.tsx @@ -1,28 +1,25 @@ -import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; +import React, { FC, memo, useCallback, useEffect, useMemo, useState } from 'react'; import { ChainIds } from '@taquito/taquito'; -import { BigNumber } from 'bignumber.js'; import clsx from 'clsx'; -import { isEqual } from 'lodash'; import { SyncSpinner, Divider, Checkbox } from 'app/atoms'; import DropdownWrapper from 'app/atoms/DropdownWrapper'; import { PartnersPromotion, PartnersPromotionVariant } from 'app/atoms/partners-promotion'; import { useAppEnv } from 'app/env'; -import { useBalancesWithDecimals } from 'app/hooks/use-balances-with-decimals.hook'; import { useLoadPartnersPromo } from 'app/hooks/use-load-partners-promo'; +import { useTokensListingLogic } from 'app/hooks/use-tokens-listing-logic'; import { ReactComponent as EditingIcon } from 'app/icons/editing.svg'; import { ReactComponent as SearchIcon } from 'app/icons/search.svg'; +import { useAreAssetsLoading } from 'app/store/assets/selectors'; import { ButtonForManageDropdown } from 'app/templates/ManageDropdown'; import SearchAssetField from 'app/templates/SearchAssetField'; import { setTestID } from 'lib/analytics'; import { OptimalPromoVariantEnum } from 'lib/apis/optimal'; -import { TEMPLE_TOKEN_SLUG, TEZ_TOKEN_SLUG } from 'lib/assets'; -import { useFilteredAssetsSlugs } from 'lib/assets/use-filtered'; +import { TEZ_TOKEN_SLUG, TEMPLE_TOKEN_SLUG } from 'lib/assets'; +import { useEnabledAccountTokensSlugs } from 'lib/assets/hooks'; import { T, t } from 'lib/i18n'; -import { useAccount, useChainId, useDisplayedFungibleTokens } from 'lib/temple/front'; -import { useSyncTokens } from 'lib/temple/front/sync-tokens'; -import { useMemoWithCompare } from 'lib/ui/hooks'; +import { useChainId } from 'lib/temple/front'; import { useLocalStorage } from 'lib/ui/local-storage'; import Popper, { PopperRenderProps } from 'lib/ui/Popper'; import { Link, navigate } from 'lib/woozie'; @@ -36,15 +33,14 @@ import { toExploreAssetLink } from './utils'; const LOCAL_STORAGE_TOGGLE_KEY = 'tokens-list:hide-zero-balances'; const svgIconClassName = 'w-4 h-4 stroke-current fill-current text-gray-600'; -export const TokensTab: FC = () => { +export const TokensTab = memo(() => { const chainId = useChainId(true)!; - const balances = useBalancesWithDecimals(); - const { publicKeyHash } = useAccount(); - const { isSyncing } = useSyncTokens(); const { popup } = useAppEnv(); - const { data: tokens = [] } = useDisplayedFungibleTokens(chainId, publicKeyHash); + const isSyncing = useAreAssetsLoading('tokens'); + + const slugs = useEnabledAccountTokensSlugs(); const [isZeroBalancesHidden, setIsZeroBalancesHidden] = useLocalStorage(LOCAL_STORAGE_TOGGLE_KEY, false); @@ -53,14 +49,12 @@ export const TokensTab: FC = () => { [setIsZeroBalancesHidden] ); - const slugs = useMemoWithCompare(() => tokens.map(({ tokenSlug }) => tokenSlug).sort(), [tokens], isEqual); - const leadingAssets = useMemo( () => (chainId === ChainIds.MAINNET ? [TEZ_TOKEN_SLUG, TEMPLE_TOKEN_SLUG] : [TEZ_TOKEN_SLUG]), [chainId] ); - const { filteredAssets, searchValue, setSearchValue } = useFilteredAssetsSlugs( + const { filteredAssets, searchValue, setSearchValue } = useTokensListingLogic( slugs, isZeroBalancesHidden, leadingAssets @@ -74,30 +68,27 @@ export const TokensTab: FC = () => { return searchFocused && searchValueExist && filteredAssets[activeIndex] ? filteredAssets[activeIndex] : null; }, [filteredAssets, searchFocused, searchValueExist, activeIndex]); - const tokensView = useMemo>(() => { + const tokensView = useMemo(() => { const tokensJsx = filteredAssets.map(assetSlug => ( )); + const promoJsx = ( + + ); + if (filteredAssets.length < 5) { - tokensJsx.push( - - ); + tokensJsx.push(promoJsx); } else { - tokensJsx.splice( - 1, - 0, - - ); + tokensJsx.splice(1, 0, promoJsx); } return tokensJsx; - }, [filteredAssets, activeAssetSlug, balances]); + }, [filteredAssets, activeAssetSlug]); useLoadPartnersPromo(OptimalPromoVariantEnum.Token); @@ -136,7 +127,7 @@ export const TokensTab: FC = () => { return (
-
+
{ {isSyncing && }
); -}; +}); interface ManageButtonDropdownProps extends PopperRenderProps { isZeroBalancesHidden: boolean; diff --git a/src/app/pages/Home/OtherComponents/Tokens/components/ListItem.tsx b/src/app/pages/Home/OtherComponents/Tokens/components/ListItem.tsx index d87677c562..7f7819cd2c 100644 --- a/src/app/pages/Home/OtherComponents/Tokens/components/ListItem.tsx +++ b/src/app/pages/Home/OtherComponents/Tokens/components/ListItem.tsx @@ -1,12 +1,16 @@ import React, { memo, useMemo } from 'react'; +import { isDefined } from '@rnw-community/shared'; import BigNumber from 'bignumber.js'; import classNames from 'clsx'; import { useTokenApyInfo } from 'app/hooks/use-token-apy.hook'; import { AssetIcon } from 'app/templates/AssetIcon'; import { setAnotherSelector } from 'lib/analytics'; -import { useAssetMetadata, getAssetName, getAssetSymbol } from 'lib/metadata'; +import { useCurrentAccountAssetBalance } from 'lib/balances/hooks'; +import { getAssetName, getAssetSymbol, useGetTokenOrGasMetadata } from 'lib/metadata'; +import { atomsToTokens } from 'lib/temple/helpers'; +import { ZERO } from 'lib/utils/numbers'; import { Link } from 'lib/woozie'; import { AssetsSelectors } from '../../Assets.selectors'; @@ -17,78 +21,71 @@ import { CryptoBalance, FiatBalance } from './Balance'; import { TokenTag } from './TokenTag'; interface Props { - active: boolean; assetSlug: string; - balance: BigNumber; + active: boolean; } -export const ListItem = memo( - ({ active, assetSlug, balance }) => { - const metadata = useAssetMetadata(assetSlug); +export const ListItem = memo(({ assetSlug, active }) => { + const metadata = useGetTokenOrGasMetadata()(assetSlug); - const apyInfo = useTokenApyInfo(assetSlug); + const balance = useCurrentAccountAssetBalance(assetSlug); - const classNameMemo = useMemo( - () => - classNames( - 'relative block w-full overflow-hidden flex items-center px-4 py-3 rounded', - 'hover:bg-gray-200 text-gray-700 transition ease-in-out duration-200 focus:outline-none', - active && 'focus:bg-gray-200' - ), - [active] - ); + const decimals = metadata?.decimals; - if (metadata == null) return null; + const balanceWithDecimals = useMemo( + () => (balance && isDefined(decimals) ? atomsToTokens(new BigNumber(balance), decimals) : ZERO), + [balance, decimals] + ); - const assetSymbol = getAssetSymbol(metadata); - const assetName = getAssetName(metadata); + const apyInfo = useTokenApyInfo(assetSlug); - return ( - - + const classNameMemo = useMemo( + () => + classNames( + 'relative block w-full overflow-hidden flex items-center px-4 py-3 rounded', + 'hover:bg-gray-200 text-gray-700 transition ease-in-out duration-200 focus:outline-none', + active && 'focus:bg-gray-200' + ), + [active] + ); -
-
-
-
{assetSymbol}
- -
- -
-
-
{assetName}
- + if (metadata == null) return null; + + const assetSymbol = getAssetSymbol(metadata); + const assetName = getAssetName(metadata); + + return ( + + + +
+
+
+
{assetSymbol}
+
+
- - ); - }, - (prevProps, nextProps) => { - if (prevProps.active !== nextProps.active) { - return false; - } - if (prevProps.assetSlug !== nextProps.assetSlug) { - return false; - } - if (prevProps.balance.toFixed() !== nextProps.balance.toFixed()) { - return false; - } - - return true; - } -); +
+
{assetName}
+ +
+
+ + ); +}); diff --git a/src/app/pages/Home/OtherComponents/Tokens/components/TokenTag/DelegateTag.tsx b/src/app/pages/Home/OtherComponents/Tokens/components/TokenTag/DelegateTag.tsx index 551e15fc67..f4528c704e 100644 --- a/src/app/pages/Home/OtherComponents/Tokens/components/TokenTag/DelegateTag.tsx +++ b/src/app/pages/Home/OtherComponents/Tokens/components/TokenTag/DelegateTag.tsx @@ -1,4 +1,4 @@ -import React, { FC, useCallback, useMemo } from 'react'; +import React, { memo, useCallback, useMemo } from 'react'; import classNames from 'clsx'; @@ -12,7 +12,7 @@ import { navigate } from 'lib/woozie'; import { AssetsSelectors } from '../../../Assets.selectors'; import modStyles from '../../Tokens.module.css'; -export const DelegateTezosTag: FC = () => { +export const DelegateTezosTag = memo(() => { const acc = useAccount(); const { data: myBakerPkh } = useDelegate(acc.publicKeyHash); const { trackEvent } = useAnalytics(); @@ -54,4 +54,4 @@ export const DelegateTezosTag: FC = () => { ); return myBakerPkh ? TezosDelegated : NotDelegatedButton; -}; +}); diff --git a/src/app/pages/ManageAssets/AssetsPlaceholder.tsx b/src/app/pages/ManageAssets/AssetsPlaceholder.tsx new file mode 100644 index 0000000000..a3fe2ea13d --- /dev/null +++ b/src/app/pages/ManageAssets/AssetsPlaceholder.tsx @@ -0,0 +1,41 @@ +import React, { memo } from 'react'; + +import { SyncSpinner } from 'app/atoms'; +import { ReactComponent as SearchIcon } from 'app/icons/search.svg'; +import { setTestID } from 'lib/analytics'; +import { T } from 'lib/i18n'; + +import { ManageAssetsSelectors } from './selectors'; + +interface Props { + isInSearchMode: boolean; + ofCollectibles?: boolean; + isLoading?: boolean; +} + +export const AssetsPlaceholder = memo(({ isInSearchMode, ofCollectibles, isLoading }) => { + if (isLoading) return ; + + return ( +
+

+ {isInSearchMode && } + + + + +

+ +

+ + + + ]} + /> +

+
+ ); +}); diff --git a/src/app/pages/ManageAssets/ListItem.tsx b/src/app/pages/ManageAssets/ListItem.tsx new file mode 100644 index 0000000000..8e766e4c46 --- /dev/null +++ b/src/app/pages/ManageAssets/ListItem.tsx @@ -0,0 +1,73 @@ +import React, { memo, useCallback } from 'react'; + +import clsx from 'clsx'; + +import Checkbox from 'app/atoms/Checkbox'; +import { ReactComponent as CloseIcon } from 'app/icons/close.svg'; +import { ManageAssetsSelectors } from 'app/pages/ManageAssets/selectors'; +import { AssetIcon } from 'app/templates/AssetIcon'; +import { setAnotherSelector, setTestID } from 'lib/analytics'; +import { t } from 'lib/i18n'; +import { getAssetName, getAssetSymbol, AssetMetadataBase } from 'lib/metadata'; + +type Props = { + assetSlug: string; + metadata?: AssetMetadataBase; + last: boolean; + checked: boolean; + onToggle: (slug: string, newState: boolean) => void; + onRemove: (slug: string) => void; +}; + +export const ListItem = memo(({ assetSlug, metadata, last, checked, onToggle, onRemove }) => { + const onCheckboxChange = useCallback((checked: boolean) => void onToggle(assetSlug, !checked), [assetSlug, onToggle]); + + const onRemoveBtnClick = useCallback>( + event => { + event.preventDefault(); + onRemove(assetSlug); + }, + [assetSlug, onRemove] + ); + + return ( +