diff --git a/package-lock.json b/package-lock.json index 3f6b0a935..4dbec598f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,7 +49,7 @@ "decentraland-experiments": "^1.0.2", "decentraland-transactions": "^2.24.1", "decentraland-ui": "^6.24.0", - "decentraland-ui2": "^0.44.1", + "decentraland-ui2": "^0.45.2", "ethers": "^5.6.8", "fast-equals": "^5.3.0", "file-saver": "^2.0.1", @@ -1062,6 +1062,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz", "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==", "dev": true, + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.23.5", @@ -1136,7 +1137,6 @@ "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/types": "^7.27.3" }, @@ -2627,6 +2627,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -3194,6 +3195,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -3237,10 +3239,11 @@ } }, "node_modules/@dcl/schemas": { - "version": "20.2.0", - "resolved": "https://registry.npmjs.org/@dcl/schemas/-/schemas-20.2.0.tgz", - "integrity": "sha512-7NIEDNvAybLrzUPcB700gMWs1zXlzsKOZCVKlph8/SLzloB84iT6gdyE1Zs0yyKmBx4+9G2B7ojf0sbQIqJ1XA==", + "version": "20.3.1", + "resolved": "https://registry.npmjs.org/@dcl/schemas/-/schemas-20.3.1.tgz", + "integrity": "sha512-B0eQMO3U64+rNzfZSDWnsWCRaRHBjR9nB1oApmwgxWrYzFcogDSri5ZX8vxqH0lYzcKgg+TElZi8BgVzr+9nEg==", "license": "Apache-2.0", + "peer": true, "dependencies": { "ajv": "^8.11.0", "ajv-errors": "^3.0.0", @@ -4268,6 +4271,7 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "peer": true, "dependencies": { "@ethersproject/address": "^5.7.0", "@ethersproject/bignumber": "^5.7.0", @@ -4744,6 +4748,7 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "peer": true, "dependencies": { "@ethersproject/abstract-provider": "^5.7.0", "@ethersproject/abstract-signer": "^5.7.0", @@ -5870,6 +5875,7 @@ "version": "27.0.0", "resolved": "https://registry.npmjs.org/@magic-sdk/provider/-/provider-27.0.0.tgz", "integrity": "sha512-k2haK2zhYkqf+cbMF+/HKvqa8qPWqdZbW10qgN6AdqaOE750ceC3GDQCUwAfkzdofmWrRtFgW5jVAVCVz5WklQ==", + "peer": true, "dependencies": { "@magic-sdk/types": "^23.0.0", "eventemitter3": "^4.0.4", @@ -5882,7 +5888,8 @@ "node_modules/@magic-sdk/types": { "version": "23.0.0", "resolved": "https://registry.npmjs.org/@magic-sdk/types/-/types-23.0.0.tgz", - "integrity": "sha512-NTjgGPJVYxoncSrHtDZLDiJzdSbxHV0jCy6J+YvT+LY7QUiGY3zzu+NhLqgLxE6KZODXERhSLYSimotcokZbmw==" + "integrity": "sha512-NTjgGPJVYxoncSrHtDZLDiJzdSbxHV0jCy6J+YvT+LY7QUiGY3zzu+NhLqgLxE6KZODXERhSLYSimotcokZbmw==", + "peer": true }, "node_modules/@messageformat/parser": { "version": "5.1.1", @@ -6046,6 +6053,7 @@ "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.18.0.tgz", "integrity": "sha512-bbH/HaJZpFtXGvWg3TsBWG4eyt3gah3E7nCNU8GLyRjVoWcA91Vm/T+sjHfUcwgJSw9iLtucfHBoq+qW/T30aA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.23.9", "@mui/core-downloads-tracker": "^5.18.0", @@ -6661,6 +6669,7 @@ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" @@ -6755,6 +6764,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/@redux-saga/is/-/is-1.1.3.tgz", "integrity": "sha512-naXrkETG1jLRfVfhOx/ZdLj0EyAzHYbgJWkXbB3qFliPcHKiWbv/ULQryOAEKyjrhiclmr6AMdgsXFyx7/yE6Q==", + "peer": true, "dependencies": { "@redux-saga/symbols": "^1.1.3", "@redux-saga/types": "^1.2.1" @@ -6763,7 +6773,8 @@ "node_modules/@redux-saga/symbols": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@redux-saga/symbols/-/symbols-1.1.3.tgz", - "integrity": "sha512-hCx6ZvU4QAEUojETnX8EVg4ubNLBFl1Lps4j2tX7o45x/2qg37m3c6v+kSp8xjDJY+2tJw4QB3j8o8dsl1FDXg==" + "integrity": "sha512-hCx6ZvU4QAEUojETnX8EVg4ubNLBFl1Lps4j2tX7o45x/2qg37m3c6v+kSp8xjDJY+2tJw4QB3j8o8dsl1FDXg==", + "peer": true }, "node_modules/@redux-saga/types": { "version": "1.2.1", @@ -7644,6 +7655,7 @@ "integrity": "sha512-++QPSPkFq2qELYVScxNHJC42hKQChjiTWS2P0QQ5JWT4NHb9lmNSfrc1ylFIyImwRnxsW2MTBALLYLf95EFAsg==", "dev": true, "hasInstallScript": true, + "peer": true, "dependencies": { "@swc/counter": "^0.1.1", "@swc/types": "^0.1.5" @@ -7868,6 +7880,7 @@ "version": "8.20.1", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.1.tgz", "integrity": "sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g==", + "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -8370,6 +8383,7 @@ "version": "20.12.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz", "integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==", + "peer": true, "dependencies": { "undici-types": "~5.26.4" } @@ -8441,6 +8455,7 @@ "version": "17.0.75", "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.75.tgz", "integrity": "sha512-MSA+NzEzXnQKrqpO63CYqNstFjsESgvJAdAyyJ1n6ZQq/GLgf6nOfIKwk+Twuz0L1N6xPe+qz5xRCJrbhMaLsw==", + "peer": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -8668,6 +8683,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.19.1.tgz", "integrity": "sha512-WEfX22ziAh6pRE9jnbkkLGp/4RhTpffr2ZK5bJ18M8mIfA8A+k97U9ZyaXCEJRlmMHh7R9MJZWXp/r73DzINVQ==", "dev": true, + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "6.19.1", "@typescript-eslint/types": "6.19.1", @@ -9649,6 +9665,7 @@ "version": "1.4.2", "resolved": "https://registry.npmjs.org/@well-known-components/interfaces/-/interfaces-1.4.2.tgz", "integrity": "sha512-1tkr/OhZ4UmnQiST2svKznEiJ86crV2S+AIhokhowR4MexAKWgYlmZpoP6VIcgDVPf7nu8hOtIPMQaCZ+COC0A==", + "peer": true, "dependencies": { "@types/node": "^20.3.1", "@types/node-fetch": "^2.5.12", @@ -9737,6 +9754,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.13.0.tgz", "integrity": "sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -9812,6 +9830,7 @@ "version": "8.12.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -10653,6 +10672,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "peer": true, "dependencies": { "@babel/runtime": "^7.12.5", "cosmiconfig": "^7.0.0", @@ -11120,6 +11140,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -11235,6 +11256,7 @@ "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.8.tgz", "integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==", "hasInstallScript": true, + "peer": true, "dependencies": { "node-gyp-build": "^4.3.0" }, @@ -12943,6 +12965,69 @@ "mitt": "^3.0.1" } }, + "node_modules/decentraland-dapps/node_modules/decentraland-ui2": { + "version": "0.44.1", + "resolved": "https://registry.npmjs.org/decentraland-ui2/-/decentraland-ui2-0.44.1.tgz", + "integrity": "sha512-tWhUqsiQc4FYElFfAJMmbemhPJgoasQhPqGMZfhfD4FV5RskovL6WOU1DsO4NNmkRJxVWCPvUGU05URMcjgJFw==", + "license": "Apache-2.0", + "dependencies": { + "@contentful/rich-text-react-renderer": "^16.0.1", + "@dcl/hooks": "^0.3.0", + "@dcl/schemas": "^20.2.0", + "@dcl/ui-env": "^1.5.1", + "@emotion/react": "^11.11.4", + "@emotion/styled": "^11.11.5", + "@mui/icons-material": "^5.16.0", + "@mui/material": "^5.16.0", + "autoprefixer": "^10.4.19", + "date-fns": "^3.6.0", + "deep-equal": "^2.2.3", + "ethereum-blockies": "^0.1.1", + "fp-future": "^1.0.1", + "lottie-react": "^2.4.0", + "mitt": "^3.0.1", + "radash": "^11.0.0", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "react-tile-map": "^0.4.1", + "uuid": "^11.1.0" + } + }, + "node_modules/decentraland-dapps/node_modules/decentraland-ui2/node_modules/@dcl/schemas": { + "version": "20.3.1", + "resolved": "https://registry.npmjs.org/@dcl/schemas/-/schemas-20.3.1.tgz", + "integrity": "sha512-B0eQMO3U64+rNzfZSDWnsWCRaRHBjR9nB1oApmwgxWrYzFcogDSri5ZX8vxqH0lYzcKgg+TElZi8BgVzr+9nEg==", + "license": "Apache-2.0", + "dependencies": { + "ajv": "^8.11.0", + "ajv-errors": "^3.0.0", + "ajv-keywords": "^5.1.0", + "mitt": "^3.0.1" + } + }, + "node_modules/decentraland-dapps/node_modules/decentraland-ui2/node_modules/date-fns": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/decentraland-dapps/node_modules/decentraland-ui2/node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/decentraland-dapps/node_modules/react-intl": { "version": "5.25.1", "resolved": "https://registry.npmjs.org/react-intl/-/react-intl-5.25.1.tgz", @@ -12977,21 +13062,6 @@ "node": ">= 4" } }, - "node_modules/decentraland-dapps/node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "license": "Apache-2.0", - "optional": true, - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, "node_modules/decentraland-dapps/node_modules/uuid": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", @@ -13008,6 +13078,7 @@ "version": "6.12.4-7784644013.commit-f770b3e", "resolved": "https://registry.npmjs.org/decentraland-ecs/-/decentraland-ecs-6.12.4-7784644013.commit-f770b3e.tgz", "integrity": "sha512-ItyASzOTryKqfrkxqposZ/dHR0kqBUxtPwnscAxXV8ONlJrhd5iT0wj49Cm3d+1Vq81tRWWy/gQuXdL4GJINmg==", + "peer": true, "dependencies": { "@dcl/amd": "7.0.0-7784644013.commit-f770b3e", "@dcl/build-ecs": "7.0.0-7784644013.commit-f770b3e", @@ -13275,14 +13346,14 @@ } }, "node_modules/decentraland-ui2": { - "version": "0.44.1", - "resolved": "https://registry.npmjs.org/decentraland-ui2/-/decentraland-ui2-0.44.1.tgz", - "integrity": "sha512-tWhUqsiQc4FYElFfAJMmbemhPJgoasQhPqGMZfhfD4FV5RskovL6WOU1DsO4NNmkRJxVWCPvUGU05URMcjgJFw==", + "version": "0.45.2", + "resolved": "https://registry.npmjs.org/decentraland-ui2/-/decentraland-ui2-0.45.2.tgz", + "integrity": "sha512-J0+s1EZkXi8gsQduvnjR4ezUF1XI0NN5BUsiLGJZhjAyBOch9JGICxx6YRxGFGvIZVxuFvKmFWPAc+EdbXsnbA==", "license": "Apache-2.0", "dependencies": { "@contentful/rich-text-react-renderer": "^16.0.1", "@dcl/hooks": "^0.3.0", - "@dcl/schemas": "^20.2.0", + "@dcl/schemas": "^20.2.1", "@dcl/ui-env": "^1.5.1", "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", @@ -14070,6 +14141,7 @@ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", "hasInstallScript": true, + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -14156,6 +14228,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -14593,6 +14666,7 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "peer": true, "dependencies": { "@ethersproject/abi": "5.7.0", "@ethersproject/abstract-provider": "5.7.0", @@ -15877,6 +15951,7 @@ "version": "15.8.0", "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.8.0.tgz", "integrity": "sha512-5gghUc24tP9HRznNpV2+FIoq3xKkj5dTQqf4v0CpdPbFVwFkWoxOM+o+2OC9ZSvjEMTjfmG9QT+gcvggTwW1zw==", + "peer": true, "engines": { "node": ">= 10.x" } @@ -16114,6 +16189,7 @@ "version": "4.10.1", "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", + "peer": true, "dependencies": { "@babel/runtime": "^7.1.2", "loose-envify": "^1.2.0", @@ -19058,6 +19134,7 @@ "version": "1.10.0", "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", + "peer": true, "dependencies": { "lie": "3.1.1" } @@ -21327,6 +21404,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.0.1", @@ -21636,6 +21714,7 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "peer": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -22288,6 +22367,7 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -22446,6 +22526,7 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" @@ -22482,6 +22563,7 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -22558,6 +22640,7 @@ "version": "7.2.9", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz", "integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==", + "peer": true, "dependencies": { "@babel/runtime": "^7.15.4", "@types/react-redux": "^7.1.20", @@ -22605,6 +22688,7 @@ "version": "5.3.4", "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz", "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==", + "peer": true, "dependencies": { "@babel/runtime": "^7.12.13", "history": "^4.9.0", @@ -22875,6 +22959,7 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "peer": true, "dependencies": { "@babel/runtime": "^7.9.2" } @@ -22926,6 +23011,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/redux-saga/-/redux-saga-1.3.0.tgz", "integrity": "sha512-J9RvCeAZXSTAibFY0kGw6Iy4EdyDNW7k6Q+liwX+bsck7QVsU78zz8vpBRweEfANxnnlG/xGGeOvf6r8UXzNJQ==", + "peer": true, "dependencies": { "@redux-saga/core": "^1.3.0" } @@ -23266,6 +23352,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.20.0.tgz", "integrity": "sha512-6rbWBChcnSGzIlXeIdNIZTopKYad8ZG8ajhl78lGRLsI2rX8IkaotQhVas2Ma+GPxJav19wrSzvRvuiv0YKzWw==", "dev": true, + "peer": true, "dependencies": { "@types/estree": "1.0.5" }, @@ -24682,6 +24769,7 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -25342,6 +25430,7 @@ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -25557,6 +25646,7 @@ "version": "8.3.2", "resolved": "https://registry.npmjs.org/typechain/-/typechain-8.3.2.tgz", "integrity": "sha512-x/sQYr5w9K7yv3es7jo4KTX05CLxOf7TRWwoHlrjRh8H82G64g+k7VuWPJlgMo6qrjfCulOdfBjiaDtmhFYD/Q==", + "peer": true, "dependencies": { "@types/prettier": "^2.1.1", "debug": "^4.3.1", @@ -25656,6 +25746,7 @@ "version": "5.7.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -26072,6 +26163,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -26175,6 +26267,7 @@ "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", "hasInstallScript": true, + "peer": true, "dependencies": { "node-gyp-build": "^4.3.0" }, @@ -26309,6 +26402,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.0.tgz", "integrity": "sha512-5xokfMX0PIiwCMCMb9ZJcMyh5wbBun0zUzKib+L65vAZ8GY9ePZMXxFrHbr/Kyll2+LSCY7xtERPpxkBDKngwg==", "dev": true, + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.40", @@ -27336,6 +27430,7 @@ "version": "4.47.0", "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.47.0.tgz", "integrity": "sha512-td7fYwgLSrky3fI1EuU5cneU4+pbH6GgOfuKNS1tNPcfdGinGELAqsb/BP4nnvZyKSG2i/xFGU7+n2PvZA8HJQ==", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.9.0", "@webassemblyjs/helper-module-context": "1.9.0", @@ -27412,6 +27507,7 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -27880,6 +27976,7 @@ "version": "8.17.1", "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "peer": true, "engines": { "node": ">=10.0.0" }, diff --git a/package.json b/package.json index 7c787420a..14b0fec40 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "decentraland-experiments": "^1.0.2", "decentraland-transactions": "^2.24.1", "decentraland-ui": "^6.24.0", - "decentraland-ui2": "^0.44.1", + "decentraland-ui2": "^0.45.2", "ethers": "^5.6.8", "fast-equals": "^5.3.0", "file-saver": "^2.0.1", diff --git a/src/components/ItemEditorPage/CenterPanel/CenterPanel.css b/src/components/ItemEditorPage/CenterPanel/CenterPanel.css index 4310f46d0..238096bf0 100644 --- a/src/components/ItemEditorPage/CenterPanel/CenterPanel.css +++ b/src/components/ItemEditorPage/CenterPanel/CenterPanel.css @@ -144,30 +144,31 @@ } .CenterPanel .emote-controls { - padding: 4px; + padding: 0; background: #242129; position: relative; } -.CenterPanel .emote-controls .ui.button.sound-control { - margin: 0 15px; -} - -.CenterPanel .EmoteControls { - padding: 0; -} - -.CenterPanel .EmoteControls .frame-control { - margin: 4px; - margin-left: 0; +/* Emote Controls Styles */ +.CenterPanel .emote-controls .MuiButtonBase-root.MuiButton-root.MuiButton-text.MuiButton-textPrimary, +.CenterPanel .emote-controls .MuiButtonBase-root.MuiButton-root.MuiButton-text.MuiButton-textPrimary:hover { + color: white; + background-color: unset; } -.CenterPanel .EmoteControls .ui.button.play-control { +/* Play Control Button */ +.CenterPanel .emote-controls .MuiButtonBase-root.MuiButton-root.MuiButton-text.MuiButton-textPrimary:nth-child(1) { width: 65px; height: 51px; border-radius: 0; } +/* Sound Control Button */ +.CenterPanel .emote-controls .MuiButtonBase-root.MuiButton-root.MuiButton-text.MuiButton-textPrimary:nth-child(4) { + margin: 0 5px; + width: 45px; +} + .CenterPanel .EmoteControls .ui.button.play-control:hover { transform: none; } diff --git a/src/components/ItemEditorPage/CenterPanel/CenterPanel.tsx b/src/components/ItemEditorPage/CenterPanel/CenterPanel.tsx index e8ee73ab8..f35ba1393 100644 --- a/src/components/ItemEditorPage/CenterPanel/CenterPanel.tsx +++ b/src/components/ItemEditorPage/CenterPanel/CenterPanel.tsx @@ -1,8 +1,8 @@ import * as React from 'react' import type { Wearable } from 'decentraland-ecs' import { BodyShape, PreviewEmote, WearableCategory } from '@dcl/schemas' -import { Dropdown, DropdownProps, Popup, Icon, Loader, Center, EmoteControls, DropdownItemProps, Button } from 'decentraland-ui' -import { AnimationControls, WearablePreview, ZoomControls } from 'decentraland-ui2' +import { Dropdown, DropdownProps, Popup, Icon, Loader, Center, DropdownItemProps, Button } from 'decentraland-ui' +import { AnimationControls, WearablePreview, EmoteControls, ZoomControls } from 'decentraland-ui2' import { SocialEmoteAnimation } from '@dcl/schemas/dist/dapps/preview/social-emote-animation' import { getAnalytics } from 'decentraland-dapps/dist/modules/analytics/utils' import { t } from 'decentraland-dapps/dist/modules/translation/utils' @@ -25,7 +25,8 @@ export default class CenterPanel extends React.PureComponent { showSceneBoundaries: false, isShowingAvatarAttributes: false, isLoading: false, - socialEmote: undefined + socialEmote: undefined, + hasBeenUpdatedWearablePreview: false } analytics = getAnalytics() @@ -67,6 +68,12 @@ export default class CenterPanel extends React.PureComponent { onSetWearablePreviewController(null) } + componentDidUpdate(_prevProps: Props, prevState: State) { + if (prevState.socialEmote !== this.state.socialEmote) { + this.setState({ hasBeenUpdatedWearablePreview: false }) + } + } + handleToggleShowingAvatarAttributes = () => { this.setState({ isShowingAvatarAttributes: !this.state.isShowingAvatarAttributes }) } @@ -246,7 +253,7 @@ export default class CenterPanel extends React.PureComponent { isImportFilesModalOpen, wearableController } = this.props - const { isShowingAvatarAttributes, showSceneBoundaries, isLoading, socialEmote } = this.state + const { isShowingAvatarAttributes, showSceneBoundaries, isLoading, socialEmote, hasBeenUpdatedWearablePreview } = this.state const isRenderingAnEmote = visibleItems.some(isEmote) && selectedItem?.type === ItemType.EMOTE const zoom = emote === PreviewEmote.JUMP ? 1 : undefined let _socialEmote = undefined @@ -279,18 +286,14 @@ export default class CenterPanel extends React.PureComponent { wheelZoom={1.5} wheelStart={100} dev={isDevelopment} - onUpdate={() => this.setState({ isLoading: true })} + onUpdate={() => this.setState({ isLoading: true, hasBeenUpdatedWearablePreview: true })} onLoad={this.handleWearablePreviewLoad} disableDefaultEmotes={isRenderingAnEmote} showSceneBoundaries={showSceneBoundaries} socialEmote={socialEmote || _socialEmote} /> - {isRenderingAnEmote && !isLoading && wearableController ? ( - + {isRenderingAnEmote && hasBeenUpdatedWearablePreview && wearableController ? ( + ) : null} {isLoading && (
@@ -298,7 +301,7 @@ export default class CenterPanel extends React.PureComponent {
)}
- {isRenderingAnEmote && !isLoading && wearableController ? ( + {isRenderingAnEmote && !isLoading && hasBeenUpdatedWearablePreview && wearableController ? (
{
{isRenderingAnEmote ? ( - !isLoading && wearableController ? ( + hasBeenUpdatedWearablePreview && wearableController ? ( = props => { address => dispatch(fetchOrphanItemRequest(address)), [dispatch] ) + const onResetEmoteToIdle = useCallback(() => dispatch(setEmote(PreviewEmote.IDLE)), [dispatch]) return ( = props => { onFetchCollections={onFetchCollections} onFetchOrphanItems={onFetchOrphanItems} onFetchOrphanItem={onFetchOrphanItem} + onResetEmoteToIdle={onResetEmoteToIdle} /> ) } diff --git a/src/components/ItemEditorPage/LeftPanel/LeftPanel.hooks.ts b/src/components/ItemEditorPage/LeftPanel/LeftPanel.hooks.ts new file mode 100644 index 000000000..08d082ac6 --- /dev/null +++ b/src/components/ItemEditorPage/LeftPanel/LeftPanel.hooks.ts @@ -0,0 +1,284 @@ +import { useCallback, useEffect, useRef, useState } from 'react' +import { Item, ItemType } from 'modules/item/types' +import { INITIAL_PAGE, LEFT_PANEL_PAGE_SIZE } from '../constants' +import { ItemEditorTabs, UseLeftPanelPaginationOptions, UseItemSelectionOptions, UseInitialDataFetchOptions } from './LeftPanel.types' + +/** + * Manages pagination state and logic for the LeftPanel component. + * + * Handles: + * - Current page tracking + * - Finding newly created items by iterating through pages + * - Loading random pages for reviewing items + * - Resetting pagination when collection/tab changes + */ +export const useLeftPanelPagination = (options: UseLeftPanelPaginationOptions) => { + const { + address, + selectedCollectionId, + selectedItemId, + orphanItems, + totalItems, + totalCollections, + isConnected, + currentTab, + onFetchResource, + onSetReviewedItems + } = options + + const [currentPage, setCurrentPage] = useState(INITIAL_PAGE) + const [visitedPages, setVisitedPages] = useState([INITIAL_PAGE]) + + // Track previous values to detect changes + const prevSelectedCollectionId = useRef(selectedCollectionId) + const prevIsConnected = useRef(isConnected) + const prevCurrentTab = useRef(currentTab) + + const isCollectionTabActive = currentTab === ItemEditorTabs.COLLECTIONS && !selectedCollectionId + + // Reset pagination when collection or tab changes + useEffect(() => { + const collectionChanged = prevSelectedCollectionId.current !== selectedCollectionId + const connectionChanged = isConnected && !prevIsConnected.current + const tabChanged = prevCurrentTab.current !== currentTab + + if (collectionChanged || connectionChanged || tabChanged) { + setCurrentPage(INITIAL_PAGE) + setVisitedPages([INITIAL_PAGE]) + } + + prevSelectedCollectionId.current = selectedCollectionId + prevIsConnected.current = isConnected + prevCurrentTab.current = currentTab + }, [selectedCollectionId, isConnected, currentTab]) + + /** + * When a newly created item redirects to the item editor, iterate over the pages + * until finding it. This handles the case where the new item might be on a different page. + */ + useEffect(() => { + if (selectedCollectionId || !address || !selectedItemId || !totalItems) { + return + } + + const itemExists = orphanItems.some(item => item.id === selectedItemId) + if (itemExists) { + return + } + + const totalPages = Math.ceil(totalItems / LEFT_PANEL_PAGE_SIZE) + const nextPage = Math.min(totalPages, currentPage + 1) + + if (!visitedPages.includes(nextPage) && nextPage !== currentPage) { + setCurrentPage(nextPage) + setVisitedPages(prev => [...prev, nextPage]) + onFetchResource(nextPage) + } + }, [address, selectedCollectionId, selectedItemId, orphanItems, totalItems, currentPage, visitedPages, onFetchResource]) + + /** + * Load a specific page + */ + const loadPage = useCallback( + (page: number) => { + setCurrentPage(page) + setVisitedPages([page]) + onFetchResource(page) + }, + [onFetchResource] + ) + + /** + * Load a random page for reviewing items. + * Used during third-party item reviews to get a random sample of items. + */ + const loadRandomPage = useCallback( + (currentItems: Item[]) => { + const totalResources = isCollectionTabActive ? totalCollections : totalItems + if (!totalResources) { + onSetReviewedItems(currentItems) + return + } + + const totalPages = Math.ceil(totalResources / LEFT_PANEL_PAGE_SIZE) + + if (totalPages > visitedPages.length) { + const availablePages = [...Array(totalPages).keys()].map(i => i + 1).filter(page => !visitedPages.includes(page)) + + if (availablePages.length > 0) { + const randomPage = availablePages[Math.floor(Math.random() * availablePages.length)] + setCurrentPage(randomPage) + setVisitedPages(prev => [...prev, randomPage]) + onFetchResource(randomPage) + } + } + + onSetReviewedItems(currentItems) + }, + [isCollectionTabActive, totalCollections, totalItems, visitedPages, onFetchResource, onSetReviewedItems] + ) + + /** + * Handle page change from CollectionProvider + */ + const handlePageChange = useCallback((page: number) => { + setCurrentPage(page) + setVisitedPages([page]) + }, []) + + return { + currentPage, + visitedPages, + loadPage, + loadRandomPage, + handlePageChange, + isCollectionTabActive + } +} + +/** + * Manages item selection and visibility for the LeftPanel component. + * + * Handles: + * - Preselecting items when clicking on a collection + * - Clearing visible items when changing collections + * - Managing emote visibility for auto-play functionality + * + * EMOTE HANDLING: + * - Emotes are added to visibleItems to trigger auto-play in the preview + * - Only one emote can be visible at a time (others are filtered out) + * - The Items component (Items.tsx:74-86) handles the play/pause toggle + * via wearableController when clicking on an already-visible emote + * - When switching to a non-emote item, emotes are filtered from visibleItems + */ +export const useItemSelection = (options: UseItemSelectionOptions) => { + const { selectedItem, selectedItemId, selectedCollectionId, visibleItems, onSetItems, onResetEmoteToIdle } = options + + // Track previous values to detect changes + const prevSelectedItem = useRef(selectedItem) + const prevSelectedItemId = useRef(selectedItemId) + const prevSelectedCollectionId = useRef(selectedCollectionId) + const isInitialMount = useRef(true) + + useEffect(() => { + // Skip initial mount - let componentDidMount equivalent handle it + if (isInitialMount.current) { + isInitialMount.current = false + prevSelectedItem.current = selectedItem + prevSelectedItemId.current = selectedItemId + prevSelectedCollectionId.current = selectedCollectionId + return + } + + const collectionChanged = prevSelectedCollectionId.current !== selectedCollectionId + + // Clear visible items and reset emote to idle when changing collection + if (collectionChanged) { + onSetItems([]) + onResetEmoteToIdle() + prevSelectedCollectionId.current = selectedCollectionId + prevSelectedItem.current = selectedItem + prevSelectedItemId.current = selectedItemId + return + } + + // Preselect item when clicking on a collection (new item appears) + // This includes emotes - adding them to visibleItems triggers auto-play + if (!prevSelectedItem.current && selectedItem) { + // For emotes, filter out any existing emotes first (only one can play at a time) + if (selectedItem.type === ItemType.EMOTE) { + const nonEmoteItems = visibleItems.filter(item => item.type !== ItemType.EMOTE) + onSetItems([...nonEmoteItems, selectedItem]) + } else { + onSetItems([selectedItem]) + } + } + + // When switching between items (both previous and current exist and are different) + const itemIdChanged = prevSelectedItemId.current && selectedItemId && prevSelectedItemId.current !== selectedItemId + if (itemIdChanged && selectedItem) { + if (selectedItem.type === ItemType.EMOTE) { + // Switching to an emote: filter out other emotes and add the new one + const nonEmoteItems = visibleItems.filter(item => item.type !== ItemType.EMOTE) + onSetItems([...nonEmoteItems, selectedItem]) + } else { + // Switching to a non-emote item: filter out emotes from visible items + const nonEmoteItems = visibleItems.filter(item => item.type !== ItemType.EMOTE) + onSetItems(nonEmoteItems) + } + } + + prevSelectedItem.current = selectedItem + prevSelectedItemId.current = selectedItemId + prevSelectedCollectionId.current = selectedCollectionId + }, [selectedItem, selectedItemId, selectedCollectionId, visibleItems, onSetItems]) +} + +/** + * Handles initial data fetching when the component mounts. + * Also handles fetching orphan items when the address changes. + */ +export const useInitialDataFetch = (options: UseInitialDataFetchOptions) => { + const { + address, + hasUserOrphanItems, + selectedItem, + isReviewing, + selectedCollectionId, + currentTab, + onFetchOrphanItem, + onFetchCollections, + onFetchOrphanItems, + onSetItems + } = options + + const hasInitializedRef = useRef(false) + const prevAddressRef = useRef(address) + + // Initial mount logic + useEffect(() => { + if (hasInitializedRef.current) { + return + } + hasInitializedRef.current = true + + // Fetch initial resources + if (address && !isReviewing && !selectedCollectionId) { + const isCollectionTab = currentTab === ItemEditorTabs.COLLECTIONS && !selectedCollectionId + const fetchFn = isCollectionTab ? onFetchCollections : onFetchOrphanItems + fetchFn(address, { limit: LEFT_PANEL_PAGE_SIZE, page: INITIAL_PAGE }) + } + + // TODO: Remove this call when there are no users with orphan items + if (address && hasUserOrphanItems === undefined) { + onFetchOrphanItem(address) + } + + // Set initial selected item + if (selectedItem) { + onSetItems([selectedItem]) + } + }, []) // Empty deps - only run on mount + + // Handle address changes for orphan items + useEffect(() => { + if (address && address !== prevAddressRef.current && hasUserOrphanItems === undefined) { + onFetchOrphanItem(address) + } + prevAddressRef.current = address + }, [address, hasUserOrphanItems, onFetchOrphanItem]) +} + +/** + * Cleanup hook that clears visible items when component unmounts. + */ +export const useCleanup = (onSetItems: (items: Item[]) => void) => { + const onSetItemsRef = useRef(onSetItems) + onSetItemsRef.current = onSetItems + + useEffect(() => { + return () => { + onSetItemsRef.current([]) + } + }, []) +} diff --git a/src/components/ItemEditorPage/LeftPanel/LeftPanel.tsx b/src/components/ItemEditorPage/LeftPanel/LeftPanel.tsx index 1f6d71c65..22770a9cd 100644 --- a/src/components/ItemEditorPage/LeftPanel/LeftPanel.tsx +++ b/src/components/ItemEditorPage/LeftPanel/LeftPanel.tsx @@ -1,289 +1,270 @@ -import * as React from 'react' +import React, { memo, useState, useCallback, useMemo } from 'react' import { Tabs } from 'decentraland-ui' import { t } from 'decentraland-dapps/dist/modules/translation/utils' import { Collection } from 'modules/collection/types' import { CurationStatus } from 'modules/curations/types' import { ItemCuration } from 'modules/curations/itemCuration/types' import { isThirdPartyCollection } from 'modules/collection/utils' -import { Item, ItemType } from 'modules/item/types' +import { Item } from 'modules/item/types' import CollectionProvider from 'components/CollectionProvider' import { ItemAddedToast } from './Toasts/ItemAdded' import Header from './Header' import Items from './Items' import Collections from './Collections' -import { LEFT_PANEL_PAGE_SIZE } from '../constants' -import { Props, State, ItemEditorTabs } from './LeftPanel.types' +import { INITIAL_PAGE, LEFT_PANEL_PAGE_SIZE } from '../constants' +import { Props, ItemEditorTabs } from './LeftPanel.types' +import { useLeftPanelPagination, useItemSelection, useInitialDataFetch, useCleanup } from './LeftPanel.hooks' import './LeftPanel.css' -const INITIAL_PAGE = 1 - -export default class LeftPanel extends React.PureComponent { - state: State = this.getInitialState() - - getInitialState() { - const { selectedCollectionId, selectedItemId } = this.props - let currentTab = ItemEditorTabs.COLLECTIONS - - if (!selectedCollectionId && selectedItemId) { - currentTab = ItemEditorTabs.ORPHAN_ITEMS - } - - return { - pages: [INITIAL_PAGE], - currentTab, - initialPage: INITIAL_PAGE, - showSamplesModalAgain: true - } - } - - handleToggleShowSamplesModalAgain = () => { - this.setState(prev => ({ showSamplesModalAgain: !prev.showSamplesModalAgain })) +function getInitialTab(selectedCollectionId: string | null, selectedItemId: string | null): ItemEditorTabs { + if (!selectedCollectionId && selectedItemId) { + return ItemEditorTabs.ORPHAN_ITEMS } + return ItemEditorTabs.COLLECTIONS +} - fetchResource = () => { - const { address, onFetchCollections, onFetchOrphanItems, isReviewing, selectedCollectionId } = this.props - const { pages } = this.state - // this is for the items editor base view, if a collection is selected, the logic will be handled in the collection provider - if (address && !isReviewing && !selectedCollectionId) { - const page = pages[pages.length - 1] // fetch new last page added, the previous ones were already fetched - const fetchFn = this.isCollectionTabActive() ? onFetchCollections : onFetchOrphanItems - fetchFn(address, { limit: LEFT_PANEL_PAGE_SIZE, page }) +function getDisplayItems( + selectedCollectionId: string | null, + collection: Collection | null, + collectionItems: Item[], + itemCurations: ItemCuration[] | null, + orphanItems: Item[], + isReviewing: boolean +): Item[] { + if (selectedCollectionId && collection) { + if (isThirdPartyCollection(collection) && isReviewing) { + return collectionItems.filter( + item => !!itemCurations?.find(curation => curation.itemId === item.id && curation.status === CurationStatus.PENDING) + ) } + return collectionItems } + return orphanItems +} - componentDidMount() { - const { address, hasUserOrphanItems, selectedItem, onFetchOrphanItem, onSetItems } = this.props - this.fetchResource() - // TODO: Remove this call when there are no users with orphan items - if (address && hasUserOrphanItems === undefined) { - onFetchOrphanItem(address) - } +function checkIsCollectionTabActive(currentTab: ItemEditorTabs, selectedCollectionId: string | null): boolean { + return currentTab === ItemEditorTabs.COLLECTIONS && !selectedCollectionId +} - if (selectedItem) { - onSetItems([selectedItem]) - } - } +/** + * LeftPanel is the main navigation component for the Item Editor. + * It displays collections and items in a sidebar format with tab navigation. + * + * Features: + * - Tab navigation between Collections and Orphan Items + * - Pagination for both collections and items + * - Random page sampling for third-party item reviews + * - Item selection and visibility management + * + * EMOTE HANDLING: + * Emotes are handled differently than wearables in this component: + * 1. When switching between non-emote items, emotes are filtered from visibleItems + * 2. This is because emotes have their own playback control via wearableController + * 3. The Items component (Items.tsx) manages emote selection and play/pause state + * 4. Only one emote can be active at a time + */ +const LeftPanel: React.FC = props => { + const { + address, + isConnected, + items: allItems, + totalItems, + totalCollections, + orphanItems, + collections, + selectedItemId, + selectedItem, + selectedCollectionId, + visibleItems, + reviewedItems, + isReviewing, + isPlayingEmote, + bodyShape, + wearableController, + isLoading: isLoadingOrphanItems, + hasUserOrphanItems, + onSetItems, + onFetchOrphanItems, + onFetchCollections, + onSetReviewedItems, + onFetchOrphanItem, + onResetReviewedItems, + onResetEmoteToIdle + } = props - componentDidUpdate(prevProps: Props, prevState: State) { - const { - isConnected, - address, - selectedCollectionId, - selectedItemId, - selectedItem, - orphanItems, - totalItems, - visibleItems, - hasUserOrphanItems, - onSetItems, - onFetchOrphanItem - } = this.props - const { initialPage, pages } = this.state - // when a newly created item redirects to the item editor, iterate over the pages until finding it - if ( - !selectedCollectionId && - address && - selectedItemId && - totalItems && - (initialPage === INITIAL_PAGE || prevState.initialPage < initialPage) - ) { - if (!orphanItems.find(item => item.id === selectedItemId)) { - const totalPages = Math.ceil(totalItems / LEFT_PANEL_PAGE_SIZE) - const page = pages[pages.length - 1] - const nextPage = Math.min(totalPages, page + 1) - if (!pages.includes(nextPage)) { - this.setState({ pages: [nextPage], initialPage: nextPage }, this.fetchResource) - } - } - } else if (prevProps.selectedItemId && selectedItemId && prevProps.selectedItemId !== selectedItemId) { - const items = visibleItems.filter(item => item.type !== ItemType.EMOTE) - onSetItems(items) - } else { - // fetch only if this was triggered by a connecting event or if th selectedCollection changes - if (address && isConnected && (isConnected !== prevProps.isConnected || (prevProps.selectedCollectionId && !selectedCollectionId))) { - this.setState({ pages: [INITIAL_PAGE] }, this.fetchResource) - } - if (prevProps.selectedCollectionId !== selectedCollectionId) { - this.setState({ pages: [INITIAL_PAGE] }) - } - // TODO: Remove this call when there are no users with orphan items - if (address && address !== prevProps.address && hasUserOrphanItems === undefined) { - onFetchOrphanItem(address) - } - } + const [currentTab, setCurrentTab] = useState(() => getInitialTab(selectedCollectionId, selectedItemId)) - // Preselect item when clicking on a collection - if (!prevProps.selectedItem && selectedItem && selectedItem.type !== ItemType.EMOTE) { - onSetItems([selectedItem]) - } + const [showSamplesModalAgain, setShowSamplesModalAgain] = useState(true) - // Clear visible items when changing collection - if (prevProps.selectedCollectionId !== selectedCollectionId) { - onSetItems([]) - } - } + const isCollectionTabActive = useMemo( + () => checkIsCollectionTabActive(currentTab, selectedCollectionId), + [currentTab, selectedCollectionId] + ) - componentWillUnmount(): void { - const { onSetItems } = this.props - onSetItems([]) - } + const showTabs = !selectedCollectionId && hasUserOrphanItems + const showCollections = isCollectionTabActive && !selectedCollectionId + const showItems = !isCollectionTabActive || selectedCollectionId - getItems(collection: Collection | null, collectionItems: Item[], itemCurations: ItemCuration[] | null) { - const { selectedCollectionId, orphanItems, isReviewing } = this.props - if (selectedCollectionId && collection) { - return isThirdPartyCollection(collection) && isReviewing - ? collectionItems.filter( - item => !!itemCurations?.find(curation => curation.itemId === item.id && curation.status === CurationStatus.PENDING) - ) - : collectionItems - } - return orphanItems - } + /** + * Fetches the appropriate resource (collections or items) based on current state. + * This is for the items editor base view - when a collection is selected, + * the CollectionProvider handles the fetching logic. + */ + const fetchResource = useCallback( + (page: number) => { + if (!address || isReviewing || selectedCollectionId) { + return + } - loadPage = (page: number) => { - this.setState({ pages: [page] }, () => this.fetchResource()) - } + const fetchFn = isCollectionTabActive ? onFetchCollections : onFetchOrphanItems + fetchFn(address, { limit: LEFT_PANEL_PAGE_SIZE, page }) + }, + [address, isReviewing, selectedCollectionId, isCollectionTabActive, onFetchCollections, onFetchOrphanItems] + ) - getRandomPage = (min: number, max: number) => { - return Math.floor(Math.random() * (max - min + 1) + min) - } + const { currentPage, loadPage, loadRandomPage, handlePageChange } = useLeftPanelPagination({ + address, + selectedCollectionId, + selectedItemId, + orphanItems, + totalItems, + totalCollections, + isConnected, + currentTab, + onFetchResource: fetchResource, + onSetReviewedItems + }) - loadRandomPage = (currentItems: Item[]) => { - const { pages } = this.state - const { totalItems, totalCollections, onSetReviewedItems } = this.props + // Handle item selection and visibility + useItemSelection({ + selectedItem, + selectedItemId, + selectedCollectionId, + visibleItems, + onSetItems, + onResetEmoteToIdle + }) - const totalResources = this.isCollectionTabActive() ? totalCollections : totalItems - const totalPages = Math.ceil(totalResources! / LEFT_PANEL_PAGE_SIZE) + // Handle initial data fetching + useInitialDataFetch({ + address, + hasUserOrphanItems, + selectedItem, + isReviewing, + selectedCollectionId, + currentTab, + onFetchOrphanItem, + onFetchCollections, + onFetchOrphanItems, + onSetItems + }) - if (totalPages > pages.length) { - const availablePages = [...Array(totalPages).keys()].map(i => i + 1).filter(page => !pages.includes(page)) + // Cleanup on unmount + useCleanup(onSetItems) - if (availablePages.length > 0) { - const randomPage = availablePages[Math.floor(Math.random() * availablePages.length)] + const handleToggleShowSamplesModalAgain = useCallback(() => { + setShowSamplesModalAgain(prev => !prev) + }, []) - this.setState(prevState => ({ pages: [...prevState.pages, randomPage] }), this.fetchResource) - } - } + const handleTabChange = useCallback( + (tab: ItemEditorTabs) => { + setCurrentTab(tab) + fetchResource(INITIAL_PAGE) + }, + [fetchResource] + ) - onSetReviewedItems(currentItems) - } + const handleLoadRandomPage = useCallback( + (items: Item[]) => { + loadRandomPage(items) + }, + [loadRandomPage] + ) - handleTabChange = (tab: ItemEditorTabs) => { - this.setState({ currentTab: tab, pages: [INITIAL_PAGE] }, this.fetchResource) + if (!isConnected) { + return
} - isCollectionTabActive = () => { - const { selectedCollectionId } = this.props - const { currentTab } = this.state - return currentTab === ItemEditorTabs.COLLECTIONS && !selectedCollectionId - } + return ( +
+ + {({ + paginatedCollections, + collection, + paginatedItems: collectionItems, + initialPage: collectionInitialPage, + isLoadingCollection, + isLoadingCollectionItems, + itemCurations + }) => { + const items = getDisplayItems(selectedCollectionId, collection, collectionItems, itemCurations, orphanItems, isReviewing) - render() { - const { - items: allItems, - totalItems, - totalCollections, - collections, - selectedItemId, - selectedCollectionId, - visibleItems, - bodyShape, - reviewedItems, - isReviewing, - isPlayingEmote, - isConnected, - wearableController, - isLoading: isLoadingOrphanItems, - hasUserOrphanItems, - onSetItems, - onSetReviewedItems, - onResetReviewedItems - } = this.props - const { pages, showSamplesModalAgain } = this.state - const showTabs = !selectedCollectionId && hasUserOrphanItems - const showCollections = this.isCollectionTabActive() && !selectedCollectionId - const showItems = !this.isCollectionTabActive() || selectedCollectionId - return ( -
- {isConnected ? ( - this.setState({ pages: [page] })} - > - {({ - paginatedCollections, - collection, - paginatedItems: collectionItems, - initialPage: collectionInitialPage, - isLoadingCollection, - isLoadingCollectionItems, - itemCurations - }) => { - const items = this.getItems(collection, collectionItems, itemCurations) - const isCollectionTab = this.isCollectionTabActive() - const initialPage = selectedCollectionId && collection ? collectionInitialPage : this.state.initialPage + const displayInitialPage = selectedCollectionId && collection ? collectionInitialPage : currentPage - return ( + return ( + <> +
+ {showTabs && ( + + handleTabChange(ItemEditorTabs.COLLECTIONS)}> + {t('collections_page.collections')} + + handleTabChange(ItemEditorTabs.ORPHAN_ITEMS)}> + {t('item_editor.left_panel.items')} + + + )} + {showCollections && ( + 0} + selectedCollectionId={selectedCollectionId} + onLoadPage={loadPage} + isLoading={isLoadingCollection} + /> + )} + {showItems && ( <> -
- {showTabs ? ( - - this.handleTabChange(ItemEditorTabs.COLLECTIONS)}> - {t('collections_page.collections')} - - this.handleTabChange(ItemEditorTabs.ORPHAN_ITEMS)}> - {t('item_editor.left_panel.items')} - - - ) : null} - {showCollections ? ( - 0} - selectedCollectionId={selectedCollectionId} - onLoadPage={this.loadPage} - isLoading={isLoadingCollection} - /> - ) : null} - {showItems ? ( - <> - 0} - selectedItemId={selectedItemId} - collection={collection} - isReviewing={isReviewing} - isPlayingEmote={isPlayingEmote} - visibleItems={visibleItems} - bodyShape={bodyShape} - onResetReviewedItems={onResetReviewedItems} - reviewedItems={reviewedItems} - onSetItems={onSetItems} - wearableController={wearableController} - initialPage={initialPage} - isLoading={isLoadingCollectionItems || isLoadingOrphanItems} - onToggleShowSamplesModalAgain={this.handleToggleShowSamplesModalAgain} - showSamplesModalAgain={showSamplesModalAgain} - onReviewItems={() => onSetReviewedItems(items)} - onLoadRandomPage={() => this.loadRandomPage(items)} - onLoadPage={this.loadPage} - /> - - - ) : null} + 0} + selectedItemId={selectedItemId} + collection={collection} + isReviewing={isReviewing} + isPlayingEmote={isPlayingEmote} + visibleItems={visibleItems} + bodyShape={bodyShape} + onResetReviewedItems={onResetReviewedItems} + reviewedItems={reviewedItems} + onSetItems={onSetItems} + wearableController={wearableController} + initialPage={displayInitialPage} + isLoading={isLoadingCollectionItems || isLoadingOrphanItems} + onToggleShowSamplesModalAgain={handleToggleShowSamplesModalAgain} + showSamplesModalAgain={showSamplesModalAgain} + onReviewItems={() => onSetReviewedItems(items)} + onLoadRandomPage={() => handleLoadRandomPage(items)} + onLoadPage={loadPage} + /> + - ) - }} - - ) : null} -
- ) - } + )} + + ) + }} +
+
+ ) } + +export default memo(LeftPanel) diff --git a/src/components/ItemEditorPage/LeftPanel/LeftPanel.types.ts b/src/components/ItemEditorPage/LeftPanel/LeftPanel.types.ts index 609263396..83699b12e 100644 --- a/src/components/ItemEditorPage/LeftPanel/LeftPanel.types.ts +++ b/src/components/ItemEditorPage/LeftPanel/LeftPanel.types.ts @@ -42,13 +42,7 @@ export type Props = { onSetReviewedItems: (itemIds: Item[]) => void onFetchOrphanItem: ActionFunction onResetReviewedItems: () => void -} - -export type State = { - currentTab: ItemEditorTabs - pages: number[] - initialPage: number - showSamplesModalAgain: boolean + onResetEmoteToIdle: () => void } export type MapStateProps = Pick< @@ -71,7 +65,10 @@ export type MapStateProps = Pick< | 'isPlayingEmote' | 'hasUserOrphanItems' > -export type MapDispatchProps = Pick +export type MapDispatchProps = Pick< + Props, + 'onSetItems' | 'onFetchOrphanItems' | 'onFetchCollections' | 'onFetchOrphanItem' | 'onResetEmoteToIdle' +> export type MapDispatch = Dispatch< | SetItemsAction | FetchCollectionItemsRequestAction @@ -82,3 +79,39 @@ export type MapDispatch = Dispatch< // New type for the functional component container export type LeftPanelContainerProps = Omit + +// Hook types +export type UseLeftPanelPaginationOptions = { + address?: string + selectedCollectionId: string | null + selectedItemId: string | null + orphanItems: Item[] + totalItems: number | null + totalCollections: number | null + isConnected: boolean + currentTab: ItemEditorTabs + onFetchResource: (page: number) => void + onSetReviewedItems: (items: Item[]) => void +} + +export type UseItemSelectionOptions = { + selectedItem: Item | null + selectedItemId: string | null + selectedCollectionId: string | null + visibleItems: Item[] + onSetItems: (items: Item[]) => void + onResetEmoteToIdle: () => void +} + +export type UseInitialDataFetchOptions = { + address?: string + hasUserOrphanItems: boolean | undefined + selectedItem: Item | null + isReviewing: boolean + selectedCollectionId: string | null + currentTab: ItemEditorTabs + onFetchOrphanItem: (address: string) => void + onFetchCollections: (address: string, params: { limit: number; page: number }) => void + onFetchOrphanItems: (address: string, params: { limit: number; page: number }) => void + onSetItems: (items: Item[]) => void +} diff --git a/src/components/ItemEditorPage/constants.ts b/src/components/ItemEditorPage/constants.ts index b24a3ecd0..f79271d64 100644 --- a/src/components/ItemEditorPage/constants.ts +++ b/src/components/ItemEditorPage/constants.ts @@ -1 +1,2 @@ export const LEFT_PANEL_PAGE_SIZE = 10 +export const INITIAL_PAGE = 1