From b4798fcb766233551e67da4d8c665f5495b0c82d Mon Sep 17 00:00:00 2001 From: Nikita Yutanov Date: Fri, 29 Mar 2024 02:38:34 +0300 Subject: [PATCH] Migrate to `react-query`, drop cache and subscriptions (#33) * Handle messages success on block finalization * Migrate to react-query, drop cache and subscriptions --- frontend/package.json | 4 +- frontend/pnpm-lock.yaml | 168 +++--------------- frontend/src/context/index.ts | 3 +- frontend/src/context/indexer.tsx | 81 --------- frontend/src/context/marketplace/context.tsx | 5 +- .../components/mint-nft/mint-nft.tsx | 9 +- .../src/hooks/use-send-message-with-reply.ts | 23 +-- frontend/src/pages/collection/collection.tsx | 32 +--- frontend/src/pages/collection/hooks.ts | 19 +- frontend/src/pages/create-collection/hooks.ts | 15 +- frontend/src/pages/lists/hooks.ts | 70 ++++---- frontend/src/pages/lists/lists.tsx | 3 + frontend/src/pages/nft/hooks.ts | 11 +- frontend/src/providers.tsx | 16 +- 14 files changed, 143 insertions(+), 316 deletions(-) delete mode 100644 frontend/src/context/indexer.tsx diff --git a/frontend/package.json b/frontend/package.json index 88bb28e..c3d0f80 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,14 +11,14 @@ "codegen": "graphql-codegen" }, "dependencies": { - "@apollo/client": "3.9.6", "@gear-js/api": "0.36.3", "@gear-js/react-hooks": "0.10.3", "@gear-js/vara-ui": "0.0.7", "@hookform/resolvers": "3.3.3", "@polkadot/api": "10.11.2", "@polkadot/react-identicon": "3.6.4", - "graphql-ws": "5.15.0", + "@tanstack/react-query": "5.28.9", + "graphql-request": "6.1.0", "react": "18.2.0", "react-dom": "18.2.0", "react-hook-form": "7.49.2", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 72ba3c4..e8db0fa 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -5,9 +5,6 @@ settings: excludeLinksFromLockfile: false dependencies: - '@apollo/client': - specifier: 3.9.6 - version: 3.9.6(@types/react@18.2.43)(graphql-ws@5.15.0)(graphql@16.8.1)(react-dom@18.2.0)(react@18.2.0) '@gear-js/api': specifier: 0.36.3 version: 0.36.3(@polkadot/api@10.11.2)(@polkadot/wasm-crypto@7.3.2)(rxjs@7.8.1) @@ -26,9 +23,12 @@ dependencies: '@polkadot/react-identicon': specifier: 3.6.4 version: 3.6.4(@polkadot/keyring@12.6.2)(@polkadot/networks@12.6.2)(@polkadot/util-crypto@12.6.2)(@polkadot/util@12.6.2)(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0) - graphql-ws: - specifier: 5.15.0 - version: 5.15.0(graphql@16.8.1) + '@tanstack/react-query': + specifier: 5.28.9 + version: 5.28.9(react@18.2.0) + graphql-request: + specifier: 6.1.0 + version: 6.1.0(graphql@16.8.1) react: specifier: 18.2.0 version: 18.2.0 @@ -134,46 +134,6 @@ packages: '@jridgewell/trace-mapping': 0.3.25 dev: true - /@apollo/client@3.9.6(@types/react@18.2.43)(graphql-ws@5.15.0)(graphql@16.8.1)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-+zpddcnZ4G2VZ0xIEnvIHFsLqeopNOnWuE2ZVbRuetLLpj/biLPNN719B/iofdd1/iHRclKfv0XaAmX6PBhYKA==} - peerDependencies: - graphql: ^15.0.0 || ^16.0.0 - graphql-ws: ^5.5.5 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - subscriptions-transport-ws: ^0.9.0 || ^0.11.0 - peerDependenciesMeta: - graphql-ws: - optional: true - react: - optional: true - react-dom: - optional: true - subscriptions-transport-ws: - optional: true - dependencies: - '@graphql-typed-document-node/core': 3.2.0(graphql@16.8.1) - '@wry/caches': 1.0.1 - '@wry/equality': 0.5.7 - '@wry/trie': 0.5.0 - graphql: 16.8.1 - graphql-tag: 2.12.6(graphql@16.8.1) - graphql-ws: 5.15.0(graphql@16.8.1) - hoist-non-react-statics: 3.3.2 - optimism: 0.18.0 - prop-types: 15.8.1 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - rehackt: 0.0.6(@types/react@18.2.43)(react@18.2.0) - response-iterator: 0.2.6 - symbol-observable: 4.0.0 - ts-invariant: 0.10.3 - tslib: 2.6.2 - zen-observable-ts: 1.2.5 - transitivePeerDependencies: - - '@types/react' - dev: false - /@ardatan/relay-compiler@12.0.0(graphql@16.8.1): resolution: {integrity: sha512-9anThAaj1dQr6IGmzBMcfzOQKTa5artjuPmw8NYK/fiGEMjADbSguBY2FMDykt+QhilR3wc9VA/3yVju7JHg7Q==} hasBin: true @@ -2924,6 +2884,19 @@ packages: - supports-color dev: true + /@tanstack/query-core@5.28.9: + resolution: {integrity: sha512-hNlfCiqZevr3GRVPXS3MhaGW5hjcxvCsIQ4q6ff7EPlvFwYZaS+0d9EIIgofnegDaU2BbCDlyURoYfRl5rmzow==} + dev: false + + /@tanstack/react-query@5.28.9(react@18.2.0): + resolution: {integrity: sha512-vwifBkGXsydsLxFOBMe3+f8kvtDoqDRDwUNjPHVDDt+FoBetCbOWAUHgZn4k+CVeZgLmy7bx6aKeDbe3e8koOQ==} + peerDependencies: + react: ^18.0.0 + dependencies: + '@tanstack/query-core': 5.28.9 + react: 18.2.0 + dev: false + /@types/babel__core@7.20.5: resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} dependencies: @@ -2985,6 +2958,7 @@ packages: /@types/prop-types@15.7.11: resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==} + dev: true /@types/react-dom@18.2.17: resolution: {integrity: sha512-rvrT/M7Df5eykWFxn6MYt5Pem/Dbyc1N8Y0S9Mrkw2WFCRiqUgw9P7ul2NpwsXCSM1DVdENzdG9J5SreqfAIWg==} @@ -2998,9 +2972,11 @@ packages: '@types/prop-types': 15.7.11 '@types/scheduler': 0.16.8 csstype: 3.1.3 + dev: true /@types/scheduler@0.16.8: resolution: {integrity: sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==} + dev: true /@types/semver@7.5.8: resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} @@ -3215,41 +3191,6 @@ packages: tslib: 2.6.2 dev: true - /@wry/caches@1.0.1: - resolution: {integrity: sha512-bXuaUNLVVkD20wcGBWRyo7j9N3TxePEWFZj2Y+r9OoUzfqmavM84+mFykRicNsBqatba5JLay1t48wxaXaWnlA==} - engines: {node: '>=8'} - dependencies: - tslib: 2.6.2 - dev: false - - /@wry/context@0.7.4: - resolution: {integrity: sha512-jmT7Sb4ZQWI5iyu3lobQxICu2nC/vbUhP0vIdd6tHC9PTfenmRmuIFqktc6GH9cgi+ZHnsLWPvfSvc4DrYmKiQ==} - engines: {node: '>=8'} - dependencies: - tslib: 2.6.2 - dev: false - - /@wry/equality@0.5.7: - resolution: {integrity: sha512-BRFORjsTuQv5gxcXsuDXx6oGRhuVsEGwZy6LOzRRfgu+eSfxbhUQ9L9YtSEIuIjY/o7g3iWFjrc5eSY1GXP2Dw==} - engines: {node: '>=8'} - dependencies: - tslib: 2.6.2 - dev: false - - /@wry/trie@0.4.3: - resolution: {integrity: sha512-I6bHwH0fSf6RqQcnnXLJKhkSXG45MFral3GxPaY4uAl0LYDZM+YDVDAiU9bYwjTuysy1S0IeecWtmq1SZA3M1w==} - engines: {node: '>=8'} - dependencies: - tslib: 2.6.2 - dev: false - - /@wry/trie@0.5.0: - resolution: {integrity: sha512-FNoYzHawTMk/6KMQoEG5O4PuioX19UbwdQKF44yw0nLfOypfQdjtfZzo/UIJWAJ23sNIFbD1Ug9lbaDGMwbqQA==} - engines: {node: '>=8'} - dependencies: - tslib: 2.6.2 - dev: false - /acorn-jsx@5.3.2(acorn@8.11.3): resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -4000,7 +3941,6 @@ packages: node-fetch: 2.7.0 transitivePeerDependencies: - encoding - dev: true /cross-inspect@1.0.0: resolution: {integrity: sha512-4PFfn4b5ZN6FMNGSZlyb7wUhuN8wvj8t/VQHZdM4JsDcruGJ8L2kf9zao98QIrBPFCpdk27qst/AGTl7pL3ypQ==} @@ -5035,7 +4975,6 @@ packages: graphql: 16.8.1 transitivePeerDependencies: - encoding - dev: true /graphql-tag@2.12.6(graphql@16.8.1): resolution: {integrity: sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==} @@ -5045,6 +4984,7 @@ packages: dependencies: graphql: 16.8.1 tslib: 2.6.2 + dev: true /graphql-ws@5.15.0(graphql@16.8.1): resolution: {integrity: sha512-xWGAtm3fig9TIhSaNsg0FaDZ8Pyn/3re3RFlP4rhQcmjRDIPpk1EhRuNB+YSJtLzttyuToaDiNhwT1OMoGnJnw==} @@ -5053,6 +4993,7 @@ packages: graphql: '>=0.11 <=16' dependencies: graphql: 16.8.1 + dev: true /graphql@16.8.1: resolution: {integrity: sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==} @@ -5141,12 +5082,6 @@ packages: minimalistic-crypto-utils: 1.0.1 dev: true - /hoist-non-react-statics@3.3.2: - resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} - dependencies: - react-is: 16.13.1 - dev: false - /http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -5923,7 +5858,6 @@ packages: optional: true dependencies: whatwg-url: 5.0.0 - dev: true /node-fetch@3.3.2: resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} @@ -6086,15 +6020,6 @@ packages: mimic-fn: 2.1.0 dev: true - /optimism@0.18.0: - resolution: {integrity: sha512-tGn8+REwLRNFnb9WmcY5IfpOqeX2kpaYJ1s6Ae3mn12AeydLkR3j+jSCmVQFoXqU8D41PAJ1RG1rCRNWmNZVmQ==} - dependencies: - '@wry/caches': 1.0.1 - '@wry/context': 0.7.4 - '@wry/trie': 0.4.3 - tslib: 2.6.2 - dev: false - /optionator@0.9.3: resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} engines: {node: '>= 0.8.0'} @@ -6562,21 +6487,6 @@ packages: set-function-name: 2.0.2 dev: true - /rehackt@0.0.6(@types/react@18.2.43)(react@18.2.0): - resolution: {integrity: sha512-l3WEzkt4ntlEc/IB3/mF6SRgNHA6zfQR7BlGOgBTOmx7IJJXojDASav+NsgXHFjHn+6RmwqsGPFgZpabWpeOdw==} - peerDependencies: - '@types/react': '*' - react: '*' - peerDependenciesMeta: - '@types/react': - optional: true - react: - optional: true - dependencies: - '@types/react': 18.2.43 - react: 18.2.0 - dev: false - /relay-runtime@12.0.0: resolution: {integrity: sha512-QU6JKr1tMsry22DXNy9Whsq5rmvwr3LSZiiWV/9+DFpuTWvp+WFhobWMc8TC4OjKFfNhEZy7mOiqUAn5atQtug==} dependencies: @@ -6640,11 +6550,6 @@ packages: supports-preserve-symlinks-flag: 1.0.0 dev: true - /response-iterator@0.2.6: - resolution: {integrity: sha512-pVzEEzrsg23Sh053rmDUvLSkGXluZio0qu8VT6ukrYuvtjVfCbDZH9d6PGXb8HZfzdNZt8feXv/jvUzlhRgLnw==} - engines: {node: '>=0.8'} - dev: false - /restore-cursor@3.1.0: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} engines: {node: '>=8'} @@ -7099,11 +7004,6 @@ packages: tslib: 2.6.2 dev: true - /symbol-observable@4.0.0: - resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==} - engines: {node: '>=0.10'} - dev: false - /tapable@2.2.1: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} engines: {node: '>=6'} @@ -7159,7 +7059,6 @@ packages: /tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - dev: true /ts-api-utils@1.3.0(typescript@5.2.2): resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} @@ -7170,13 +7069,6 @@ packages: typescript: 5.2.2 dev: true - /ts-invariant@0.10.3: - resolution: {integrity: sha512-uivwYcQaxAucv1CzRp2n/QdYPo4ILf9VXgH19zEIjFx2EJufV16P0JtJVpYHy89DItG6Kwj2oIUjrcK5au+4tQ==} - engines: {node: '>=8'} - dependencies: - tslib: 2.6.2 - dev: false - /ts-log@2.2.5: resolution: {integrity: sha512-PGcnJoTBnVGy6yYNFxWVNkdcAuAMstvutN9MgDJIV6L0oG8fB+ZNNy1T+wJzah8RPGor1mZuPQkVfXNDpy9eHA==} dev: true @@ -7548,14 +7440,12 @@ packages: /webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - dev: true /whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} dependencies: tr46: 0.0.3 webidl-conversions: 3.0.1 - dev: true /which-boxed-primitive@1.0.2: resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} @@ -7732,16 +7622,6 @@ packages: engines: {node: '>=10'} dev: true - /zen-observable-ts@1.2.5: - resolution: {integrity: sha512-QZWQekv6iB72Naeake9hS1KxHlotfRpe+WGNbNx5/ta+R3DNjVO2bswf63gXlWDcs+EMd7XY8HfVQyP1X6T4Zg==} - dependencies: - zen-observable: 0.8.15 - dev: false - - /zen-observable@0.8.15: - resolution: {integrity: sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==} - dev: false - /zod@3.22.4: resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} dev: false diff --git a/frontend/src/context/index.ts b/frontend/src/context/index.ts index 659a777..8767730 100644 --- a/frontend/src/context/index.ts +++ b/frontend/src/context/index.ts @@ -1,4 +1,3 @@ -import { IndexerProvider } from './indexer'; import { MarketplaceProvider, useMarketplace } from './marketplace'; -export { MarketplaceProvider, useMarketplace, IndexerProvider }; +export { MarketplaceProvider, useMarketplace }; diff --git a/frontend/src/context/indexer.tsx b/frontend/src/context/indexer.tsx deleted file mode 100644 index b56e9dc..0000000 --- a/frontend/src/context/indexer.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { HttpLink, split, ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client'; -import { GraphQLWsLink } from '@apollo/client/link/subscriptions'; -import { getMainDefinition } from '@apollo/client/utilities'; -import { ProviderProps } from '@gear-js/react-hooks'; -import { Kind, OperationTypeNode } from 'graphql'; -import { createClient } from 'graphql-ws'; - -import { ADDRESS } from '@/consts'; - -const httpLink = new HttpLink({ uri: ADDRESS.INDEXER }); -const wsLink = new GraphQLWsLink(createClient({ url: ADDRESS.INDEXER_WS })); - -const splitLink = split( - ({ query }) => { - const definition = getMainDefinition(query); - return definition.kind === Kind.OPERATION_DEFINITION && definition.operation === OperationTypeNode.SUBSCRIPTION; - }, - wsLink, - httpLink, -); - -const client = new ApolloClient({ - link: splitLink, - cache: new InMemoryCache({ - typePolicies: { - Query: { - fields: { - nfts: { - keyArgs: ['where', ['owner_eq', 'collection', ['id_eq']]], - merge: (existing: unknown[] = [], incoming: unknown[], { args }: { args: unknown }) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const offset = args.offset as number; - - // Slicing is necessary because the existing data is - // immutable, and frozen in development. - const merged = existing ? existing.slice(0) : []; - - for (let i = 0; i < incoming.length; ++i) { - merged[offset + i] = incoming[i]; - } - - return merged; - }, - }, - - // TODO: make it less mess - collections: { - keyArgs: ['where', ['admin_contains', 'admin_eq']], // admin_eq is for create collection page - merge: (existing: unknown[] = [], incoming: unknown[], { args }: { args: unknown }) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const offset = args.offset as number; - - // Slicing is necessary because the existing data is - // immutable, and frozen in development. - const merged = existing ? existing.slice(0) : []; - - for (let i = 0; i < incoming.length; ++i) { - merged[offset + i] = incoming[i]; - } - - return merged; - }, - }, - }, - }, - }, - }), - - defaultOptions: { - query: { notifyOnNetworkStatusChange: true, fetchPolicy: 'network-only' }, - watchQuery: { notifyOnNetworkStatusChange: true, fetchPolicy: 'network-only' }, - }, -}); - -function IndexerProvider({ children }: ProviderProps) { - return {children}; -} - -export { IndexerProvider }; diff --git a/frontend/src/context/marketplace/context.tsx b/frontend/src/context/marketplace/context.tsx index bfff3fa..2abb5a0 100644 --- a/frontend/src/context/marketplace/context.tsx +++ b/frontend/src/context/marketplace/context.tsx @@ -1,6 +1,7 @@ -import { useQuery } from '@apollo/client'; import { ProgramMetadata } from '@gear-js/api'; import { ProviderProps, useAlert } from '@gear-js/react-hooks'; +import { useQuery } from '@tanstack/react-query'; +import request from 'graphql-request'; import { createContext, useContext, useEffect, useState } from 'react'; import { ADDRESS } from '@/consts'; @@ -16,7 +17,7 @@ const useMarketplace = () => useContext(MarketplaceContext); function MarketplaceProvider({ children }: ProviderProps) { const alert = useAlert(); - const { data } = useQuery(MARKETPLACE_QUERY); + const { data } = useQuery({ queryKey: ['marketplace'], queryFn: () => request(ADDRESS.INDEXER, MARKETPLACE_QUERY) }); const marketplace = data?.marketplaceById; const [marketplaceMetadata, setMarketplaceMetadata] = useState(); diff --git a/frontend/src/features/collections/components/mint-nft/mint-nft.tsx b/frontend/src/features/collections/components/mint-nft/mint-nft.tsx index ca88f51..805806c 100644 --- a/frontend/src/features/collections/components/mint-nft/mint-nft.tsx +++ b/frontend/src/features/collections/components/mint-nft/mint-nft.tsx @@ -11,9 +11,10 @@ type Props = Pick< 'id' | 'tokensLimit' | 'paymentForMint' | 'userMintLimit' | 'permissionToMint' | 'admin' > & { nfts: Pick[]; + refetch: () => void; }; -function Component({ id, tokensLimit, paymentForMint, userMintLimit, permissionToMint, admin, nfts }: Props) { +function Component({ id, tokensLimit, paymentForMint, userMintLimit, permissionToMint, admin, nfts, refetch }: Props) { const sendMessage = useMarketplaceMessage(); const alert = useAlert(); const [isLoading, enableLoading, disableLoading] = useLoading(); @@ -42,7 +43,11 @@ function Component({ id, tokensLimit, paymentForMint, userMintLimit, permissionT const percentage = (BigInt(paymentForMint) * BigInt(200)) / BigInt(FEE_MULTIPLIER); const value = (BigInt(paymentForMint) + percentage).toString(); - const onSuccess = () => alert.success('NFT minted'); + const onSuccess = () => { + refetch(); + alert.success('NFT minted'); + }; + const onFinally = disableLoading; sendMessage({ payload, value, onSuccess, onFinally }); diff --git a/frontend/src/hooks/use-send-message-with-reply.ts b/frontend/src/hooks/use-send-message-with-reply.ts index 70a8a92..8453ada 100644 --- a/frontend/src/hooks/use-send-message-with-reply.ts +++ b/frontend/src/hooks/use-send-message-with-reply.ts @@ -58,6 +58,7 @@ const useSendMessageWithReply = (programId: HexString, metadata: ProgramMetadata if (!account) throw new Error('Account is not found'); let unsub: UnsubscribePromise | undefined = undefined; + let replyPayload: Reply | undefined = undefined; const _onFinally = () => { onFinally(); @@ -68,6 +69,13 @@ const useSendMessageWithReply = (programId: HexString, metadata: ProgramMetadata unsub.then((unsubCallback) => unsubCallback()).catch((error: Error) => alert.error(error.message)); }; + const _onSuccess = () => { + _onFinally(); + + if (!replyPayload) return; + onSuccess(replyPayload); + }; + const handleUserMessageSent = ({ data }: UserMessageSent) => { try { if (!metadata) throw new Error('Failed to get transaction result: metadata is not found'); @@ -87,25 +95,20 @@ const useSendMessageWithReply = (programId: HexString, metadata: ProgramMetadata const decodedPayload = metadata.createType(typeIndex, payload).toJSON(); if (!isObject(decodedPayload)) throw new Error('Failed to get transaction result: payload is not an object'); + if ('err' in decodedPayload) throw new Error(decodedPayload.err?.toString()); + if (!('ok' in decodedPayload)) throw new Error('Failed to get transaction result: ok property is not found'); - const isErrorPayload = Object.prototype.hasOwnProperty.call(decodedPayload, 'err'); - if (isErrorPayload) throw new Error(decodedPayload.err?.toString()); - - const isSuccessPayload = Object.prototype.hasOwnProperty.call(decodedPayload, 'ok'); - if (!isSuccessPayload) throw new Error('Failed to get transaction result: ok property is not found'); - - onSuccess(decodedPayload.ok as Reply); + replyPayload = decodedPayload.ok as Reply; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); alert.error(errorMessage); + _onFinally(); } - - _onFinally(); }; unsub = api.gearEvents.subscribeToGearEvent('UserMessageSent', handleUserMessageSent); - sendMessage({ ...sendMessageArgs, onError: _onFinally }); + sendMessage({ ...sendMessageArgs, onError: _onFinally, onSuccess: _onSuccess }); }; }; diff --git a/frontend/src/pages/collection/collection.tsx b/frontend/src/pages/collection/collection.tsx index 3a39228..ced9b3c 100644 --- a/frontend/src/pages/collection/collection.tsx +++ b/frontend/src/pages/collection/collection.tsx @@ -8,20 +8,13 @@ import NFTCardSkeletonSVG from '@/features/collections/assets/nft-card-skeleton. import { AccountFilter, GRID_SIZE, GridSize, useAccountFilter, useGridSize } from '@/features/lists'; import { getIpfsLink } from '@/utils'; +import { useNFTs } from '../lists/hooks'; // TODO: shared folder + import UserSVG from './assets/user.svg?react'; import styles from './collection.module.scss'; import { SOCIAL_ICON } from './consts'; import { useCollection } from './hooks'; -// function useSearchQuery() { -// const { handleSubmit, register } = useForm({ defaultValues: { query: '' } }); -// const [query, setQuery] = useState(''); - -// const onSubmit = handleSubmit((values) => setQuery(values.query.trim().toLocaleLowerCase())); - -// return { query, onSubmit, register }; -// } - type Params = { id: string; }; @@ -32,12 +25,10 @@ function Collection() { const { gridSize, setGridSize } = useGridSize(); const { accountFilterValue, accountFilterAddress, setAccountFilterValue } = useAccountFilter(); - const [collection, nfts, totalNFTsCount, hasMoreNFTs, isCollectionQueryReady, fetchNFTs] = useCollection( - id, - accountFilterAddress, - ); - const { name, additionalLinks } = collection || {}; + const [nfts, nftsCount, hasMoreNFTs, isNFTsQueryReady, fetchNFTs, refetchNFTs] = useNFTs(accountFilterAddress, id); + const [collection, isCollectionQueryReady] = useCollection(id); + const { name, additionalLinks } = collection || {}; const socialEntries = Object.entries(additionalLinks || {}).filter(([key]) => !key.startsWith('__')); const renderSocials = () => @@ -59,7 +50,7 @@ function Collection() { - {collection ? ( + {isCollectionQueryReady && collection ? (
@@ -67,7 +58,7 @@ function Collection() {
- +
@@ -80,7 +71,7 @@ function Collection() {
    {renderSocials()}
- +
@@ -92,11 +83,6 @@ function Collection() {
- {/* TODO: search */} - {/*
- - */} -
@@ -112,7 +98,7 @@ function Collection() { isMoreItems={hasMoreNFTs} skeleton={{ rowsCount: 2, - isVisible: !isCollectionQueryReady, + isVisible: !isNFTsQueryReady, renderItem: (index) => (
  • diff --git a/frontend/src/pages/collection/hooks.ts b/frontend/src/pages/collection/hooks.ts index b97a223..546bbe5 100644 --- a/frontend/src/pages/collection/hooks.ts +++ b/frontend/src/pages/collection/hooks.ts @@ -1,19 +1,20 @@ -import { useQuery } from '@apollo/client'; +import { useQuery } from '@tanstack/react-query'; +import request from 'graphql-request'; -// TODO: reusing temporary solution for nfts subscriptiption -import { useNFTs } from '../lists/hooks'; +import { ADDRESS } from '@/consts'; import { COLLECTION_QUERY } from './consts'; -function useCollection(id: string, owner: string) { - const [nfts, nftsCount, hasMoreNFTs, isNFTsQueryReady, fetchNFTs] = useNFTs(owner, id); +function useCollection(id: string) { + const { data, isFetching } = useQuery({ + queryKey: ['collection', id], + queryFn: () => request(ADDRESS.INDEXER, COLLECTION_QUERY, { id }), + }); - // subscription to handle update in case of newly created collection - const { data, loading } = useQuery(COLLECTION_QUERY, { variables: { id } }); const collection = data?.collectionById; - const isCollectionQueryReady = !loading && isNFTsQueryReady; + const isCollectionQueryReady = !isFetching; - return [collection, nfts, nftsCount, hasMoreNFTs, isCollectionQueryReady, fetchNFTs] as const; + return [collection, isCollectionQueryReady] as const; } export { useCollection }; diff --git a/frontend/src/pages/create-collection/hooks.ts b/frontend/src/pages/create-collection/hooks.ts index df8946e..1710b80 100644 --- a/frontend/src/pages/create-collection/hooks.ts +++ b/frontend/src/pages/create-collection/hooks.ts @@ -1,6 +1,8 @@ -import { useQuery } from '@apollo/client'; import { useAccount } from '@gear-js/react-hooks'; +import { useQuery } from '@tanstack/react-query'; +import request from 'graphql-request'; +import { ADDRESS } from '@/consts'; import { useMarketplace } from '@/context'; import { useCountdown } from '@/hooks'; @@ -10,16 +12,13 @@ function useLastCollection() { const { account } = useAccount(); const admin = account?.decodedAddress || ''; - const { data, loading } = useQuery(LAST_CREATED_COLLECTION_QUERY, { - variables: { admin }, - // TODO: better to use cache-and-network, but result.fetching is not getting updated immediately - // maybe should be easy to fix via result.stale - // ref: https://github.com/urql-graphql/urql/issues/2002 - fetchPolicy: 'network-only', + const { data, isFetching } = useQuery({ + queryKey: ['lastCreatedCollection', admin], + queryFn: () => request(ADDRESS.INDEXER, LAST_CREATED_COLLECTION_QUERY, { admin }), }); const lastCollection = data?.collections[0]; - const isLastCollectionReady = !loading; + const isLastCollectionReady = !isFetching; return { lastCollection, isLastCollectionReady }; } diff --git a/frontend/src/pages/lists/hooks.ts b/frontend/src/pages/lists/hooks.ts index 0cbb020..6aa1c8a 100644 --- a/frontend/src/pages/lists/hooks.ts +++ b/frontend/src/pages/lists/hooks.ts @@ -1,6 +1,8 @@ -import { useQuery } from '@apollo/client'; -import { useCallback, useMemo } from 'react'; +import { useInfiniteQuery, useQuery } from '@tanstack/react-query'; +import request from 'graphql-request'; +import { useMemo } from 'react'; +import { ADDRESS } from '@/consts'; import { CollectionWhereInput, NftWhereInput } from '@/graphql/graphql'; import { @@ -14,30 +16,41 @@ import { import { getCollectionFilters, getNftFilters } from './utils'; function useCollectionsNFTsCount(ids: string[]) { - const { data } = useQuery(COLLECTIONS_NFTS_COUNT_QUERY, { variables: { ids }, skip: !ids.length }); + const { data } = useQuery({ + queryKey: ['collectionsNFTsCount', ids], + queryFn: () => request(ADDRESS.INDEXER, COLLECTIONS_NFTS_COUNT_QUERY, { ids }), + enabled: Boolean(ids.length), + }); return data?.nftsInCollection || []; } function useTotalCollectionsCount(where: CollectionWhereInput) { - const { data, loading } = useQuery(COLLECTIONS_CONNECTION_QUERY, { - variables: { ...DEFAULT_VARIABLES.COLLECTIONS, where }, + const { data, isFetching } = useQuery({ + queryKey: ['collectionsCount', where], + queryFn: () => request(ADDRESS.INDEXER, COLLECTIONS_CONNECTION_QUERY, { ...DEFAULT_VARIABLES.COLLECTIONS, where }), }); + const isReady = !isFetching; const totalCount = data?.collectionsConnection.totalCount || 0; - return [totalCount, !loading] as const; + return [totalCount, isReady] as const; } function useCollections(admin: string) { const where = useMemo(() => getCollectionFilters(admin), [admin]); const [totalCount, isTotalCountReady] = useTotalCollectionsCount(where); - const { data, loading, fetchMore } = useQuery(COLLECTIONS_QUERY, { - variables: { ...DEFAULT_VARIABLES.COLLECTIONS, where }, + const { data, isFetching, fetchNextPage } = useInfiniteQuery({ + queryKey: ['collections', admin], + queryFn: ({ pageParam: offset }) => + request(ADDRESS.INDEXER, COLLECTIONS_QUERY, { ...DEFAULT_VARIABLES.COLLECTIONS, offset, where }), + initialPageParam: 0, + // TODO: take a look, works for now cuz counter and hasMore are calculated below + getNextPageParam: (_lastPage, pages) => DEFAULT_VARIABLES.COLLECTIONS.limit * pages.length, }); - const collections = useMemo(() => data?.collections || [], [data]); + const collections = useMemo(() => data?.pages.flatMap((result) => result.collections) || [], [data]); const collectionsCount = collections.length; const collectionIds = useMemo(() => collections.map(({ id }) => id), [collections]); @@ -51,48 +64,45 @@ function useCollections(admin: string) { }); }, [collections, nftsCounts]); - const isReady = !loading && isTotalCountReady; + const isReady = !isFetching && isTotalCountReady; const hasMore = totalCount && collectionsCount ? collectionsCount < totalCount : false; - const fetchCollections = useCallback(() => { - const offset = collectionsCount; - - fetchMore({ variables: { offset } }).catch(console.error); - }, [collectionsCount, fetchMore]); - - return [collectionsWithCounts, totalCount, hasMore, isReady, fetchCollections] as const; + return [collectionsWithCounts, totalCount, hasMore, isReady, fetchNextPage] as const; } function useTotalNFTsCount(where: NftWhereInput) { - const { data, loading } = useQuery(NFTS_CONNECTION_QUERY, { variables: { where } }); + const { data, isFetching } = useQuery({ + queryKey: ['nftsCount', where], + queryFn: () => request(ADDRESS.INDEXER, NFTS_CONNECTION_QUERY, { where }), + }); + const isReady = !isFetching; const totalCount = data?.nftsConnection?.totalCount || 0; - return [totalCount, !loading] as const; + return [totalCount, isReady] as const; } function useNFTs(owner: string, collectionId?: string) { const where = useMemo(() => getNftFilters(owner, collectionId), [owner, collectionId]); const [totalCount, isTotalCountReady] = useTotalNFTsCount(where); - const { data, loading, fetchMore } = useQuery(NFTS_QUERY, { - variables: { ...DEFAULT_VARIABLES.NFTS, where }, + const { data, isFetching, fetchNextPage, refetch } = useInfiniteQuery({ + queryKey: ['nfts', where], + queryFn: ({ pageParam: offset }) => + request(ADDRESS.INDEXER, NFTS_QUERY, { ...DEFAULT_VARIABLES.NFTS, offset, where }), + initialPageParam: 0, + // TODO: take a look, works for now cuz counter and hasMore are calculated below + getNextPageParam: (_lastPage, pages) => DEFAULT_VARIABLES.NFTS.limit * pages.length, }); - const nfts = data?.nfts || []; + const nfts = useMemo(() => data?.pages.flatMap((result) => result.nfts) || [], [data]); const nftsCount = nfts.length; // TODO: if new nfts would be minted, totalCount will remain the same const hasMoreNFTs = totalCount && nftsCount ? nftsCount < totalCount : false; - const isNFTsQueryReady = !loading && isTotalCountReady; - - const fetchNFTs = useCallback(() => { - const offset = nftsCount; - - fetchMore({ variables: { offset } }).catch(console.error); - }, [fetchMore, nftsCount]); + const isNFTsQueryReady = !isFetching && isTotalCountReady; - return [nfts, totalCount, hasMoreNFTs, isNFTsQueryReady, fetchNFTs] as const; + return [nfts, totalCount, hasMoreNFTs, isNFTsQueryReady, fetchNextPage, refetch] as const; } export { useNFTs, useCollections }; diff --git a/frontend/src/pages/lists/lists.tsx b/frontend/src/pages/lists/lists.tsx index 72a6b01..751c92e 100644 --- a/frontend/src/pages/lists/lists.tsx +++ b/frontend/src/pages/lists/lists.tsx @@ -77,6 +77,9 @@ function Lists() { items={collections} itemsPerRow={gridSize === GRID_SIZE.SMALL ? 3 : 2} emptyText="Create collections" + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment renderItem={(collection) => } fetchItems={fetchCollections} isMoreItems={hasMoreCollections} diff --git a/frontend/src/pages/nft/hooks.ts b/frontend/src/pages/nft/hooks.ts index a322141..75b96e4 100644 --- a/frontend/src/pages/nft/hooks.ts +++ b/frontend/src/pages/nft/hooks.ts @@ -1,10 +1,17 @@ -import { useQuery, useSubscription } from '@apollo/client'; +import { useQuery } from '@tanstack/react-query'; +import request from 'graphql-request'; + +import { ADDRESS } from '@/consts'; import { NFT_QUERY } from './consts'; function useNFT(collectionId: string, idInCollection: string) { const id = `${collectionId}-${idInCollection}`; - const { data } = useQuery(NFT_QUERY, { variables: { id } }); + + const { data } = useQuery({ + queryKey: ['nft', id], + queryFn: () => request(ADDRESS.INDEXER, NFT_QUERY, { id }), + }); return data?.nftById; } diff --git a/frontend/src/providers.tsx b/frontend/src/providers.tsx index afe1b7d..706cbc7 100644 --- a/frontend/src/providers.tsx +++ b/frontend/src/providers.tsx @@ -5,10 +5,11 @@ import { ProviderProps, } from '@gear-js/react-hooks'; import { Alert, alertStyles } from '@gear-js/vara-ui'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ComponentType } from 'react'; import { ADDRESS } from './consts'; -import { IndexerProvider, MarketplaceProvider } from './context'; +import { MarketplaceProvider } from './context'; function ApiProvider({ children }: ProviderProps) { return {children}; @@ -22,6 +23,19 @@ function AlertProvider({ children }: ProviderProps) { ); } +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + gcTime: 0, + staleTime: Infinity, + }, + }, +}); + +function IndexerProvider({ children }: ProviderProps) { + return {children}; +} + const providers = [ApiProvider, AccountProvider, AlertProvider, IndexerProvider, MarketplaceProvider]; const withProviders = (Component: ComponentType) => () =>