Merge pull request #88 from MergeMerc/main

Q-Tube with Support for new Qortal Core Features
This commit is contained in:
Ice
2025-06-18 11:01:33 -04:00
committed by GitHub
24 changed files with 851 additions and 273 deletions

257
package-lock.json generated
View File

@@ -10,16 +10,17 @@
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@mui/icons-material": "^6.3.0",
"@mui/lab": "^6.0.0-beta.21",
"@mui/material": "^6.3.0",
"@mui/icons-material": "^7.1.0",
"@mui/lab": "7.0.0-beta.12",
"@mui/material": "^7.1.0",
"@preact/signals-react": "^2.3.0",
"@reduxjs/toolkit": "^2.5.0",
"compressorjs": "^1.2.1",
"dompurify": "^3.2.3",
"jotai": "^2.12.4",
"localforage": "^1.10.0",
"moment": "^2.30.1",
"qapp-core": "^1.0.17",
"qapp-core": "^1.0.29",
"quill": "^2.0.2",
"quill-image-resize-module-react": "^3.0.0",
"react": "^19.0.0",
@@ -291,12 +292,10 @@
}
},
"node_modules/@babel/runtime": {
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz",
"integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz",
"integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
@@ -1063,40 +1062,6 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@floating-ui/core": {
"version": "1.6.8",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz",
"integrity": "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==",
"dependencies": {
"@floating-ui/utils": "^0.2.8"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.6.12",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.12.tgz",
"integrity": "sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w==",
"dependencies": {
"@floating-ui/core": "^1.6.0",
"@floating-ui/utils": "^0.2.8"
}
},
"node_modules/@floating-ui/react-dom": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz",
"integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==",
"dependencies": {
"@floating-ui/dom": "^1.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.8",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz",
"integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig=="
},
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@@ -1201,52 +1166,23 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@mui/base": {
"version": "5.0.0-beta.68",
"resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.68.tgz",
"integrity": "sha512-F1JMNeLS9Qhjj3wN86JUQYBtJoXyQvknxlzwNl6eS0ZABo1MiohMONj3/WQzYPSXIKC2bS/ZbyBzdHhi2GnEpA==",
"dependencies": {
"@babel/runtime": "^7.26.0",
"@floating-ui/react-dom": "^2.1.1",
"@mui/types": "^7.2.20",
"@mui/utils": "^6.3.0",
"@popperjs/core": "^2.11.8",
"clsx": "^2.1.1",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/core-downloads-tracker": {
"version": "6.4.10",
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.4.10.tgz",
"integrity": "sha512-cblGjlM6+xsptwyaALw8RbRIUoqmKxOqLxlk2LkTDhxqUuql1YSOKKLH3w+Yd2QLz28b7MR65sx1OjsRZUfOSQ==",
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.1.0.tgz",
"integrity": "sha512-E0OqhZv548Qdc0PwWhLVA2zmjJZSTvaL4ZhoswmI8NJEC1tpW2js6LLP827jrW9MEiXYdz3QS6+hask83w74yQ==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
}
},
"node_modules/@mui/icons-material": {
"version": "6.4.10",
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.4.10.tgz",
"integrity": "sha512-c2KdFl4KZ0QYC+JSDTMCNjcuOL2rVSdIx/beo7FwJDh2e9XqC1MoLCjw6L1Jo40zbArkgJyg3oFORbXcRfgZOA==",
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-7.1.0.tgz",
"integrity": "sha512-1mUPMAZ+Qk3jfgL5ftRR06ATH/Esi0izHl1z56H+df6cwIlCWG66RXciUqeJCttbOXOQ5y2DCjLZI/4t3Yg3LA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.0"
"@babel/runtime": "^7.27.1"
},
"engines": {
"node": ">=14.0.0"
@@ -1256,7 +1192,7 @@
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@mui/material": "^6.4.10",
"@mui/material": "^7.1.0",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
@@ -1267,15 +1203,15 @@
}
},
"node_modules/@mui/lab": {
"version": "6.0.0-beta.21",
"resolved": "https://registry.npmjs.org/@mui/lab/-/lab-6.0.0-beta.21.tgz",
"integrity": "sha512-hiFZgTwBNhJMUlEhmqfW4+5wy3C8UF9KFuzSOux6x4kgc9hsC0l+motXcF1Vyh+jhJYGeZ6yUoImqCf9RWzEvw==",
"version": "7.0.0-beta.12",
"resolved": "https://registry.npmjs.org/@mui/lab/-/lab-7.0.0-beta.12.tgz",
"integrity": "sha512-685MmzByCy3Vmb7xI6J8qOQm4l7yqfVTOLwxVmNV1EHBKuJiMuoX4/2vAAEGfNbDeEfWQsp7aBWanYpSWe1iRA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.0",
"@mui/base": "5.0.0-beta.68",
"@mui/system": "^6.3.0",
"@mui/types": "^7.2.20",
"@mui/utils": "^6.3.0",
"@babel/runtime": "^7.27.1",
"@mui/system": "^7.1.0",
"@mui/types": "^7.4.2",
"@mui/utils": "^7.1.0",
"clsx": "^2.1.1",
"prop-types": "^15.8.1"
},
@@ -1289,8 +1225,8 @@
"peerDependencies": {
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
"@mui/material": "^6.3.0",
"@mui/material-pigment-css": "^6.3.0",
"@mui/material": "^7.1.0",
"@mui/material-pigment-css": "^7.1.0",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
@@ -1311,21 +1247,22 @@
}
},
"node_modules/@mui/material": {
"version": "6.4.10",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-6.4.10.tgz",
"integrity": "sha512-L1B0+Vg9NFjo3NcfODH3bohl6fIkzjyDBHBHb3Al4QI7owaJrFm2sSDyfz++iatzICug6U6q5tHLQrCLO71xkg==",
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-7.1.0.tgz",
"integrity": "sha512-ahUJdrhEv+mCp4XHW+tHIEYzZMSRLg8z4AjUOsj44QpD1ZaMxQoVOG2xiHvLFdcsIPbgSRx1bg1eQSheHBgvtg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.0",
"@mui/core-downloads-tracker": "^6.4.10",
"@mui/system": "^6.4.10",
"@mui/types": "~7.2.24",
"@mui/utils": "^6.4.9",
"@babel/runtime": "^7.27.1",
"@mui/core-downloads-tracker": "^7.1.0",
"@mui/system": "^7.1.0",
"@mui/types": "^7.4.2",
"@mui/utils": "^7.1.0",
"@popperjs/core": "^2.11.8",
"@types/react-transition-group": "^4.4.12",
"clsx": "^2.1.1",
"csstype": "^3.1.3",
"prop-types": "^15.8.1",
"react-is": "^19.0.0",
"react-is": "^19.1.0",
"react-transition-group": "^4.4.5"
},
"engines": {
@@ -1338,7 +1275,7 @@
"peerDependencies": {
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
"@mui/material-pigment-css": "^6.4.10",
"@mui/material-pigment-css": "^7.1.0",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
@@ -1359,12 +1296,13 @@
}
},
"node_modules/@mui/private-theming": {
"version": "6.4.9",
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.4.9.tgz",
"integrity": "sha512-LktcVmI5X17/Q5SkwjCcdOLBzt1hXuc14jYa7NPShog0GBDCDvKtcnP0V7a2s6EiVRlv7BzbWEJzH6+l/zaCxw==",
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.1.0.tgz",
"integrity": "sha512-4Kck4jxhqF6YxNwJdSae1WgDfXVg0lIH6JVJ7gtuFfuKcQCgomJxPvUEOySTFRPz1IZzwz5OAcToskRdffElDA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.0",
"@mui/utils": "^6.4.9",
"@babel/runtime": "^7.27.1",
"@mui/utils": "^7.1.0",
"prop-types": "^15.8.1"
},
"engines": {
@@ -1385,11 +1323,12 @@
}
},
"node_modules/@mui/styled-engine": {
"version": "6.4.9",
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.4.9.tgz",
"integrity": "sha512-qZRWO0cT407NI4ZRjZcH+1SOu8f3JzLHqdMlg52GyEufM9pkSZFnf7xjpwnlvkixcGjco6wLlMD0VB43KRcBuA==",
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.1.0.tgz",
"integrity": "sha512-m0mJ0c6iRC+f9hMeRe0W7zZX1wme3oUX0+XTVHjPG7DJz6OdQ6K/ggEOq7ZdwilcpdsDUwwMfOmvO71qDkYd2w==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.0",
"@babel/runtime": "^7.27.1",
"@emotion/cache": "^11.13.5",
"@emotion/serialize": "^1.3.3",
"@emotion/sheet": "^1.4.0",
@@ -1418,15 +1357,16 @@
}
},
"node_modules/@mui/system": {
"version": "6.4.10",
"resolved": "https://registry.npmjs.org/@mui/system/-/system-6.4.10.tgz",
"integrity": "sha512-RyBGQwP3tgo4JEibK+RwVu1a6nQ6y8urMCNsb2aiN/nvTxxumq6P26aoG4GTUf8L4O1sthC4lMXlP4r8ixDkMg==",
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@mui/system/-/system-7.1.0.tgz",
"integrity": "sha512-iedAWgRJMCxeMHvkEhsDlbvkK+qKf9me6ofsf7twk/jfT4P1ImVf7Rwb5VubEA0sikrVL+1SkoZM41M4+LNAVA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.0",
"@mui/private-theming": "^6.4.9",
"@mui/styled-engine": "^6.4.9",
"@mui/types": "~7.2.24",
"@mui/utils": "^6.4.9",
"@babel/runtime": "^7.27.1",
"@mui/private-theming": "^7.1.0",
"@mui/styled-engine": "^7.1.0",
"@mui/types": "^7.4.2",
"@mui/utils": "^7.1.0",
"clsx": "^2.1.1",
"csstype": "^3.1.3",
"prop-types": "^15.8.1"
@@ -1457,9 +1397,13 @@
}
},
"node_modules/@mui/types": {
"version": "7.2.24",
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.24.tgz",
"integrity": "sha512-3c8tRt/CbWZ+pEg7QpSwbdxOk36EfmhbKf6AGZsD1EcLDLTSZoxxJ86FVtcjxvjuhdyBiWKSTGZFaXCnidO2kw==",
"version": "7.4.2",
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.2.tgz",
"integrity": "sha512-edRc5JcLPsrlNFYyTPxds+d5oUovuUxnnDtpJUbP6WMeV4+6eaX/mqai1ZIWT62lCOe0nlrON0s9HDiv5en5bA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.27.1"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
@@ -1470,16 +1414,17 @@
}
},
"node_modules/@mui/utils": {
"version": "6.4.9",
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.4.9.tgz",
"integrity": "sha512-Y12Q9hbK9g+ZY0T3Rxrx9m2m10gaphDuUMgWxyV5kNJevVxXYCLclYUCC9vXaIk1/NdNDTcW2Yfr2OGvNFNmHg==",
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.1.0.tgz",
"integrity": "sha512-/OM3S8kSHHmWNOP+NH9xEtpYSG10upXeQ0wLZnfDgmgadTAk5F4MQfFLyZ5FCRJENB3eRzltMmaNl6UtDnPovw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.0",
"@mui/types": "~7.2.24",
"@babel/runtime": "^7.27.1",
"@mui/types": "^7.4.2",
"@types/prop-types": "^15.7.14",
"clsx": "^2.1.1",
"prop-types": "^15.8.1",
"react-is": "^19.0.0"
"react-is": "^19.1.0"
},
"engines": {
"node": ">=14.0.0"
@@ -1962,7 +1907,8 @@
"node_modules/@types/prop-types": {
"version": "15.7.14",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
"integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ=="
"integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==",
"license": "MIT"
},
"node_modules/@types/react": {
"version": "19.0.2",
@@ -2700,6 +2646,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/dexie": {
"version": "4.0.11",
"resolved": "https://registry.npmjs.org/dexie/-/dexie-4.0.11.tgz",
"integrity": "sha512-SOKO002EqlvBYYKQSew3iymBoN2EQ4BDw/3yprjh7kAfFzjBYkaMNa/pZvcA7HSWlcKSQb9XhPe3wKyQ0x4A8A==",
"license": "Apache-2.0"
},
"node_modules/dom-helpers": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
@@ -3568,6 +3520,27 @@
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true
},
"node_modules/jotai": {
"version": "2.12.4",
"resolved": "https://registry.npmjs.org/jotai/-/jotai-2.12.4.tgz",
"integrity": "sha512-eFXLJol4oOLM8BS1+QV+XwaYQITG8n1tatBCFl4F5HE3zR5j2WIK8QpMt7VJIYmlogNUZfvB7wjwLoVk+umB9Q==",
"license": "MIT",
"engines": {
"node": ">=12.20.0"
},
"peerDependencies": {
"@types/react": ">=17.0.0",
"react": ">=17.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"react": {
"optional": true
}
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -4066,9 +4039,10 @@
}
},
"node_modules/qapp-core": {
"version": "1.0.17",
"resolved": "https://registry.npmjs.org/qapp-core/-/qapp-core-1.0.17.tgz",
"integrity": "sha512-c++yhwAIlg4HhJcp7J2UNBSo/+va7I8GbtmqZ91HNHN2uk5qrwnxIQvTdWXynLvaTVrq16pW7Ry92lTncumGHg==",
"version": "1.0.29",
"resolved": "https://registry.npmjs.org/qapp-core/-/qapp-core-1.0.29.tgz",
"integrity": "sha512-w1tBniw6yFxmOc+1H9584kCwL3mpHpIEtU31vv9FIAwltfRMF0hAidTC2tVcvFqNNYKn7rfvDuhhbgayI/elBQ==",
"license": "MIT",
"dependencies": {
"@tanstack/react-virtual": "^3.13.2",
"bloom-filters": "^3.0.4",
@@ -4076,6 +4050,7 @@
"compressorjs": "^1.2.1",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.13",
"dexie": "^4.0.11",
"dompurify": "^3.2.4",
"react-dropzone": "^14.3.8",
"react-hot-toast": "^2.5.2",
@@ -4086,8 +4061,8 @@
"peerDependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@mui/icons-material": "^6.4.7",
"@mui/material": "^6.4.7",
"@mui/icons-material": "^7.0.1",
"@mui/material": "^7.0.1",
"react": "^19.0.0"
}
},
@@ -4300,9 +4275,10 @@
}
},
"node_modules/react-is": {
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.0.0.tgz",
"integrity": "sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g=="
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.0.tgz",
"integrity": "sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==",
"license": "MIT"
},
"node_modules/react-quill-new": {
"version": "3.3.3",
@@ -4460,11 +4436,6 @@
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.14.tgz",
"integrity": "sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A=="
},
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
},
"node_modules/regexp.prototype.flags": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz",

View File

@@ -19,6 +19,7 @@
"@reduxjs/toolkit": "^2.5.0",
"compressorjs": "^1.2.1",
"dompurify": "^3.2.3",
"jotai": "^2.12.4",
"localforage": "^1.10.0",
"moment": "^2.30.1",
"qapp-core": "^1.0.29",

View File

@@ -3,7 +3,6 @@ import { ThemeProvider } from "@mui/material/styles";
import { useEffect, useState } from "react";
import { Provider, useSelector } from "react-redux";
import { Route, Routes } from "react-router-dom";
import { persistStore } from "redux-persist";
import { PersistGate } from "redux-persist/integration/react";
import { subscriptionListFilter } from "./App-Functions.ts";
import Notification from "./components/common/Notification/Notification";

View File

@@ -1,5 +1,4 @@
import { GlobalProvider } from 'qapp-core'
import React from 'react'
import { useSelector } from 'react-redux';
import { RootState } from './state/store';
@@ -7,16 +6,16 @@ export const QappCoreWrapper = ({children}) => {
const { user } = useSelector((state: RootState) => state.auth);
return (
<GlobalProvider
config={{
auth: {
authenticateOnMount: false,
userAccountInfo: {
address: user?.address,
publicKey: user?.publicKey
}
},
publicSalt: "usVbeM9YpjGCbLrTcc78YJS0ap1AxDkHAOMZrp3+wDY=",
appName: "Q-Tube",
config={{
auth: {
authenticateOnMount: false,
userAccountInfo: {
address: user?.address,
publicKey: user?.publicKey
}
},
publicSalt: "usVbeM9YpjGCbLrTcc78YJS0ap1AxDkHAOMZrp3+wDY=",
appName: "Q-Tube",
}}
>
{children}

View File

@@ -40,7 +40,7 @@ export const MultiplePublish = ({
const hasStarted = useRef(false);
const publish = useCallback(async (pub: any) => {
const lengthOfResources = pub?.resources?.length;
const lengthOfTimeout = lengthOfResources * 30000;
const lengthOfTimeout = lengthOfResources * 1200000; // Time out in QR, Seconds = 20 Minutes
return await qortalRequestWithTimeout(pub, lengthOfTimeout);
}, []);
const [isPublishing, setIsPublishing] = useState(true);

View File

@@ -0,0 +1,34 @@
import { Avatar, useTheme } from "@mui/material";
import { AccountCircleSVG } from "../assets/svgs/AccountCircleSVG";
import { menuIconSize } from "../constants/Misc";
import {
DropdownContainer,
DropdownText,
AvatarContainer
} from "./layout/Navbar/Navbar-styles";
interface UserDropDownProps {
userName: string;
handleMyChannelLink: (username: string) => void;
popMenuRef: React.RefObject<{ closePopover: () => void }>;
}
export const UserDropDown = ({ userName, handleMyChannelLink, popMenuRef }: UserDropDownProps) => {
const theme = useTheme();
const userAvatar = `/arbitrary/THUMBNAIL/${userName}/avatar?async=true`;
return (
<DropdownContainer
onClick={() => {
handleMyChannelLink(userName);
popMenuRef.current.closePopover();
}}
>
<Avatar src={userAvatar}>
{userName?.charAt(0).toUpperCase()}
</Avatar>
<DropdownText>{userName}</DropdownText>
</DropdownContainer>
)
}

View File

@@ -125,20 +125,11 @@ export default function FileElement({
try {
const { name, service, identifier } = fileInfo;
const url = `/arbitrary/${service}/${name}/${identifier}`;
fetch(url)
.then(response => response.blob())
.then(async blob => {
await qortalRequest({
action: "SAVE_FILE",
blob,
filename: filename,
mimeType,
});
})
.catch(error => {
console.error("Error fetching the video:", error);
});
await qortalRequest({
action: "SAVE_FILE",
location: fileInfo,
filename: filename,
});
} catch (error: any) {
let notificationObj: any = null;
if (typeof error === "string") {

View File

@@ -1,4 +1,4 @@
import { Popover, useMediaQuery, useTheme } from "@mui/material";
import { Popover, useMediaQuery, useTheme, Avatar } from "@mui/material";
import { AccountCircleSVG } from "../../../../assets/svgs/AccountCircleSVG.tsx";
import { headerIconSize, menuIconSize } from "../../../../constants/Misc.ts";
import { BlockedNamesModal } from "../../../common/BlockedNamesModal/BlockedNamesModal.tsx";
@@ -9,21 +9,27 @@ import {
NavbarName,
} from "../Navbar-styles.tsx";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import { useRef, useState } from "react";
import { useCallback, useRef, useState } from "react";
import PersonOffIcon from "@mui/icons-material/PersonOff";
import { RootState } from "../../../../state/store";
import { useDispatch } from "react-redux";
import { useNavigate } from "react-router-dom";
import { PopMenu, PopMenuRefType } from "../../../common/PopMenu.tsx";
import { UserDropDown } from "../../../UserDropDown.tsx";
import { Names } from "../../../../state/global/names.ts";
import { setName } from "../../../../state/features/authSlice.ts";
export interface NavBarMenuProps {
isShowMenu: boolean;
userAvatar: string;
userName: string | null;
allNames: Names;
}
export const UserMenu = ({
isShowMenu,
userAvatar,
userName,
allNames
}: NavBarMenuProps) => {
const isScreenSmall = !useMediaQuery(`(min-width:600px)`);
const theme = useTheme();
@@ -32,10 +38,12 @@ export const UserMenu = ({
useState<boolean>(false);
const popMenuRef = useRef<PopMenuRefType>(null);
const navigate = useNavigate();
const dispatch = useDispatch();
const handleMyChannelLink = () => {
navigate(`/channel/${userName}`);
};
const handleMyChannelLink = useCallback((switchToName: string) => {
dispatch(setName(switchToName));
navigate(`/channel/${switchToName}`);
}, [navigate]);
const onCloseBlockedNames = () => {
setIsOpenBlockedNamesModal(false);
@@ -43,58 +51,30 @@ export const UserMenu = ({
return (
<>
{isShowMenu && (
{isShowMenu && (
<>
<PopMenu
ref={popMenuRef}
MenuHeader={
<AvatarContainer>
{!isScreenSmall && <NavbarName>{userName}</NavbarName>}
{!userAvatar ? (
<AccountCircleSVG
color={theme.palette.text.primary}
width={headerIconSize}
height={headerIconSize}
/>
) : (
<img
src={userAvatar}
alt="User Avatar"
width={headerIconSize}
height={headerIconSize}
style={{
borderRadius: "50%",
}}
/>
)}
<Avatar src={userAvatar}>
{userName?.charAt(0).toUpperCase()}
</Avatar>
</AvatarContainer>
}
>
<DropdownContainer
onClick={() => {
handleMyChannelLink();
popMenuRef.current.closePopover();
}}
>
{!userAvatar ? (
<AccountCircleSVG
color={theme.palette.text.primary}
width={menuIconSize}
height={menuIconSize}
{
allNames.map((name) => (
<UserDropDown key={name.name}
userName={name.name}
handleMyChannelLink={handleMyChannelLink}
popMenuRef={popMenuRef}
/>
) : (
<img
src={userAvatar}
alt="User Avatar"
width={menuIconSize}
height={menuIconSize}
style={{
borderRadius: "50%",
}}
/>
)}
<DropdownText>{userName}</DropdownText>
</DropdownContainer>
))
}
<DropdownContainer
onClick={() => {
setIsOpenBlockedNamesModal(true);

View File

@@ -6,16 +6,18 @@ import { PublishMenu } from "./Components/PublishMenu.tsx";
import { QtubeLogo } from "./Components/QtubeLogo.tsx";
import { UserMenu } from "./Components/UserMenu.tsx";
import { CustomAppBar } from "./Navbar-styles";
import { Names } from "./../../../state/global/names.ts";
interface Props {
isAuthenticated: boolean;
userName: string | null;
allNames: Names;
userAvatar: string;
authenticate: () => void;
setTheme: (val: string) => void;
}
const NavBar: React.FC<Props> = ({ isAuthenticated, userName, userAvatar }) => {
const NavBar: React.FC<Props> = ({ isAuthenticated, userName, allNames, userAvatar }) => {
const isScreenSmall = !useMediaQuery(`(min-width:600px)`);
const isSecure = isAuthenticated && !!userName;
const gapSize = 10;
@@ -45,6 +47,7 @@ const NavBar: React.FC<Props> = ({ isAuthenticated, userName, userAvatar }) => {
isShowMenu={isSecure}
userAvatar={userAvatar}
userName={userName}
allNames={allNames}
/>
<PublishMenu isDisplayed={isSecure} />
</Box>

View File

@@ -6,7 +6,7 @@ export const minPriceSuperDislike = 1;
export const titleFormatter = /[\r\n]+/g;
export const titleFormatterOnSave = /[\r\n/<>:"'\\*|?]+/g;
export const videoMaxSize = 2147; // Size in Megabytes (decimal)
export const videoMaxSize = 2050; // Size in Megabytes (decimal)
export const maxSize = videoMaxSize * 1024 * 1024;
export const fontSizeExSmall = "60%";

6
src/global.d.ts vendored
View File

@@ -1,4 +1,9 @@
// src/global.d.ts
interface Location {
service: string;
name: string;
identifier?: string;
}
interface QortalRequestOptions {
action: string;
name?: string;
@@ -38,6 +43,7 @@ interface QortalRequestOptions {
excludeBlocked?: boolean;
exactMatchNames?: boolean;
nameListFilter?: string[];
location?: Location;
}
declare function qortalRequest(options: QortalRequestOptions): Promise<any>;

View File

@@ -0,0 +1,36 @@
import { useSetAtom } from 'jotai';
import { namesAtom } from '../state/global/names';
import { useCallback, useEffect } from 'react';
//import { useGlobal } from 'qapp-core';
import { useSelector } from 'react-redux';
import { RootState } from '../state/store';
export const useHandleNameData = () => {
const setNames = useSetAtom(namesAtom);
const user = useSelector((state: RootState) => state.auth.user);
const getMyNames = useCallback(async () => {
if (!user?.address) return;
try {
const res = await qortalRequest({
action: 'GET_ACCOUNT_NAMES',
address: user.address,
limit: 0,
offset: 0,
reverse: false,
});
setNames(res);
} catch (error) {
console.error(error);
}
}, [user?.address, setNames]);
// Initial fetch + interval
useEffect(() => {
getMyNames();
const interval = setInterval(getMyNames, 120_000); // every 2 minutes
return () => clearInterval(interval);
}, [getMyNames]);
return null;
};

View File

@@ -2,6 +2,7 @@ import ReactDOM from "react-dom/client";
import App from "./App";
import "./index.css";
import { BrowserRouter } from "react-router-dom";
interface CustomWindow extends Window {
_qdnBase: string;
}

View File

@@ -1,13 +1,19 @@
import { Box } from "@mui/material";
import React from "react";
import { Box, Tabs, Tab } from "@mui/material";
import React, { useState } from "react";
import { useParams } from "react-router-dom";
import { VideoListComponentLevel } from "../../Home/Components/VideoListComponentLevel.tsx";
import { PlayListComponentLevel } from "../../Home/Components/PlayListComponentLevel.tsx";
import { ChannelActions } from "../VideoContent/ChannelActions.tsx";
import { StyledCardHeaderComment } from "../VideoContent/VideoContent-styles.tsx";
import { HeaderContainer, ProfileContainer } from "./Profile-styles.tsx";
export const IndividualProfile = () => {
const { name: channelName } = useParams();
const [selectedTab, setSelectedTab] = useState(0);
const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => {
setSelectedTab(newValue);
};
return (
<ProfileContainer>
@@ -28,7 +34,19 @@ export const IndividualProfile = () => {
</StyledCardHeaderComment>
</Box>
</HeaderContainer>
<VideoListComponentLevel />
{/* Tabs Bar */}
<Box sx={{ borderBottom: 1, borderColor: "divider", mb: 2 }}>
<Tabs value={selectedTab} onChange={handleTabChange} aria-label="profile tabs">
<Tab label="Videos" />
<Tab label="Playlists" />
</Tabs>
</Box>
{/* Tab Content */}
{selectedTab === 0 && <VideoListComponentLevel />}
{selectedTab === 1 && <PlayListComponentLevel />}
</ProfileContainer>
);
};

View File

@@ -4,13 +4,19 @@ import { SubscribeButton } from "../../../components/common/ContentButtons/Subsc
import { RootState } from "../../../state/store.ts";
import { ChannelParams } from "./ChannelName.tsx";
import { StyledCardColComment } from "./VideoContent-styles.tsx";
import { namesAtom } from '../../../state/global/names';
import { useAtom } from "jotai";
export const ChannelButtons = ({ channelName, sx }: ChannelParams) => {
const userName = useSelector((state: RootState) => state.auth.user?.name);
const [names] = useAtom(namesAtom);
const isInNames = names.map((name) => name.name ).includes(channelName);
//const userName = useSelector((state: RootState) => state.auth.user?.name);
// We need to put a change in here to get the currentUser name, not from the Redux state
return (
<StyledCardColComment sx={{ alignItems: "center", ...sx }}>
{channelName !== userName && (
{!isInNames && (
<>
<SubscribeButton subscriberName={channelName} />
<FollowButton

View File

@@ -0,0 +1,183 @@
import { Box, useTheme } from "@mui/material";
import React, { useEffect, useRef, useState } from "react";
import { useSelector } from "react-redux";
import { useParams } from "react-router-dom";
import LazyLoad from "../../../components/common/LazyLoad.tsx";
import { useFetchVideos } from "../../../hooks/useFetchVideos.tsx";
import { Video } from "../../../state/features/videoSlice.ts";
import { RootState } from "../../../state/store.ts";
import { queue } from "../../../wrappers/GlobalWrapper.tsx";
import { VideoManagerRow } from "./VideoList-styles.tsx";
import { useSignal } from "@preact/signals-react";
import { PlayListList } from "./PlayListList.tsx";
interface VideoListProps {
mode?: string;
}
export const PlayListComponentLevel = ({ mode }: VideoListProps) => {
const { name: paramName } = useParams();
const firstFetch = useRef(false);
const afterFetch = useRef(false);
const hashMapVideos = useSelector(
(state: RootState) => state.video.hashMapVideos
);
const [videos, setVideos] = React.useState<Video[]>([]);
const isLoading = useSignal(true);
const { getVideo, checkAndUpdateVideo } = useFetchVideos();
// For Pagination
const pageRef = useRef(0);
const [hasMore, setHasMore] = useState(true);
const PAGE_SIZE = 20;
useEffect(() => {
firstFetch.current = false;
setVideos([]);
pageRef.current = 0;
setHasMore(true);
}, [paramName]);
// 16-May-2025: Includes Pagination for PlayLists
const getVideos = React.useCallback(async () => {
isLoading.value = true;
try {
// Query to get a users playlists
//'http://192.168.0.43:12391/arbitrary/resources/search?service=PLAYLIST&name=Ice&exactmatchnames=true&limit=20&reverse=true' \
const offset = pageRef.current * PAGE_SIZE;
const url = `/arbitrary/resources/search?mode=ALL&service=PLAYLIST&name=${encodeURIComponent(paramName)}&exactmatchnames=true&limit=20&reverse=true&offset=${offset}`;
const response = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
const responseData = await response.json();
/* This is the search result for playlists by a specific use
[
{
"name": "Ice",
"service": "PLAYLIST",
"identifier": "qtube_playlist_the-history-of-qortal_wZ9BtX",
"size": 11824,
"created": 1747327830726
}
]
*/
/* This is the result when getting the JSON infomration about a PLAYLIST
{ "title": "The History of Qortal",
"version": 1,
"description": "A timeline of Qortal according to Ice",
"htmlDescription": "<p>A timeline of Qortal according to Ice</p>",
"image": "data:image/webp;base64,UklGRugqAABXRUJQVlA4WAoAAAAgAAAASwIAFQAGSJuX2Ws5....YY9umRdt3kC5BWqZoIno3+DIAAA",
"videos": [
{ "identifier": "qtube_vid_q-tube-may-15_32Mf4d_metadata", "name": "Ice", "service": "DOCUMENT", "code": "GLzeC**<p>New In Qortal? 15-May 2025</p><p>Come see new enhancements specific to Q-Tube</p><p><br></p>" },
{ "identifier": "qtube_vid_11-why-crowetic-dedicated-to-q_nmHmNo_metadata", "name": "QortalNuggets", "service": "DOCUMENT", "code": "Q3fbb**<p>Ernest asks WHY. Jason answers.</p>" },
{ "identifier": "qtube_vid_qortal-conscious-soul-festival_55y2u0_metadata", "name": "ThanksToZen", "service": "DOCUMENT", "code": "Xvwjg**Footage from day 1 of 2" },
{ "identifier": "qtube_vid_qortal-at-web3-amsterdamvideo7_1lCJT4_metadata", "name": "igorcoin", "service": "DOCUMENT", "code": "IbRmX**qortal-at-web3-amsterdam_video_720p_eesti keelsete subtiitritega" }
],
"commentsId": "qtube_playlist__cm_wZ9BtX", "category": 26, "subcategory": "" }
*/
const structureData = responseData.map((video: any): Video => {
return {
title: video?.metadata?.title,
category: video?.metadata?.category,
categoryName: video?.metadata?.categoryName,
tags: video?.metadata?.tags || [],
description: video?.metadata?.description,
created: video?.created,
updated: video?.updated,
service: `PLAYLIST`,
user: video.name,
videoImage: "",
id: video.identifier,
};
});
// Pre-Pagination
//setVideos((prev) => {
// const copiedVideos: Video[] = [...prev];
// structureData.forEach((video: Video) => {
// const index = prev.findIndex((p) => p.id === video.id);
// if (index !== -1) {
// copiedVideos[index] = video;
// } else {
// copiedVideos.push(video);
// }
// });
// return copiedVideos;
//});
setVideos(prev => {
const updatedVideos = [...prev];
structureData.forEach(video => {
const exists = updatedVideos.some(v => v.id === video.id);
if (!exists) {
updatedVideos.push(video);
}
});
return updatedVideos;
});
// If fewer than PAGE_SIZE results, we've reached the end
if (structureData.length < PAGE_SIZE) {
setHasMore(false);
} else {
pageRef.current += 1;
}
for (const content of structureData) {
if (content.user && content.id) {
const res = checkAndUpdateVideo(content);
if (res) {
queue.push(() => getVideo(content.user, content.id, content));
}
}
}
isLoading.value = false;
} catch (error) {
console.log(error);
isLoading.value = false;
}
}, [checkAndUpdateVideo, getVideo, hashMapVideos, paramName]);
const getVideosHandlerMount = React.useCallback(async () => {
if (firstFetch.current) return;
firstFetch.current = true;
await getVideos();
afterFetch.current = true;
}, [getVideos]);
useEffect(() => {
if (!firstFetch.current) {
getVideosHandlerMount();
}
}, [getVideosHandlerMount]);
return (
<VideoManagerRow>
<Box
sx={{
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<PlayListList videos={videos} />
<LazyLoad
onLoadMore={hasMore ? getVideos : undefined}
isLoading={isLoading.value}
/>
</Box>
</VideoManagerRow>
);
};

View File

@@ -0,0 +1,311 @@
import BlockIcon from "@mui/icons-material/Block";
import EditIcon from "@mui/icons-material/Edit";
import { Avatar, Box, Tooltip, Typography, useTheme } from "@mui/material";
import { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
import { PlaylistSVG } from "../../../assets/svgs/PlaylistSVG.tsx";
import ResponsiveImage from "../../../components/ResponsiveImage.tsx";
import { fontSizeSmall, minDuration } from "../../../constants/Misc.ts";
import {
blockUser,
setEditPlaylist,
setEditVideo,
Video,
} from "../../../state/features/videoSlice.ts";
import { RootState } from "../../../state/store.ts";
import { formatTime } from "../../../utils/numberFunctions.ts";
import { formatDate } from "../../../utils/time.ts";
import { VideoCardImageContainer } from "./VideoCardImageContainer.tsx";
import {
BlockIconContainer,
BottomParent,
IconsBox,
NameContainer,
VideoCard,
VideoCardCol,
VideoCardContainer,
VideoCardName,
VideoCardTitle,
VideoUploadDate,
} from "./VideoList-styles.tsx";
import ContextMenuResource from '../../../components/common/ContextMenu/ContextMenuResource'
interface VideoListProps {
videos: Video[];
}
export const PlayListList = ({ videos }: VideoListProps) => {
const [showIcons, setShowIcons] = useState(null);
const hashMapVideos = useSelector(
(state: RootState) => state.video.hashMapVideos
);
// ToDo: This needs to be updated for names
const username = useSelector((state: RootState) => state.auth?.user?.name);
const navigate = useNavigate();
const dispatch = useDispatch();
const theme = useTheme();
const blockUserFunc = async (user: string) => {
if (user === "Q-Tube") return;
try {
const response = await qortalRequest({
action: "ADD_LIST_ITEMS",
list_name: "blockedNames",
items: [user],
});
if (response === true) {
dispatch(blockUser(user));
}
} catch (error) {
console.log(error);
}
};
return (
<VideoCardContainer>
{videos.map((video: any) => {
const fullId = video ? `${video.id}-${video.user}` : undefined;
const existingVideo = hashMapVideos[fullId];
let hasHash = false;
let videoObj = video;
if (existingVideo) {
videoObj = existingVideo;
hasHash = true;
}
// nb. this prevents showing metadata for a video which
// belongs to a different user
if (
videoObj?.user &&
videoObj?.videoReference?.name &&
videoObj.user != videoObj.videoReference.name
) {
return null;
}
if (hasHash && !videoObj?.videoImage && !videoObj?.image) {
return null;
}
const isPlaylist = videoObj?.service === "PLAYLIST";
if (isPlaylist) {
return (
<VideoCardCol
key={videoObj.id}
onMouseEnter={() => setShowIcons(videoObj.id)}
onMouseLeave={() => setShowIcons(null)}
>
<IconsBox
sx={{
opacity: showIcons === videoObj.id ? 1 : 0,
zIndex: 2,
}}
>
{videoObj?.user === username && (
<Tooltip title="Edit playlist" placement="top">
<BlockIconContainer>
<EditIcon
onClick={() => {
dispatch(setEditPlaylist(videoObj));
}}
/>
</BlockIconContainer>
</Tooltip>
)}
{videoObj?.user !== username && (
<Tooltip title="Block user content" placement="top">
<BlockIconContainer>
<BlockIcon
onClick={() => {
blockUserFunc(videoObj?.user);
}}
/>
</BlockIconContainer>
</Tooltip>
)}
</IconsBox>
<VideoCard
sx={{
cursor: !hasHash && "default",
}}
onClick={() => {
if (!hasHash) return;
navigate(`/playlist/${videoObj?.user}/${videoObj?.id}`);
}}
>
<ResponsiveImage
src={videoObj?.image}
width={266}
height={150}
style={{
maxHeight: "50%",
}}
/>
<VideoCardTitle>{videoObj?.title}</VideoCardTitle>
<BottomParent>
<NameContainer
onClick={e => {
e.stopPropagation();
navigate(`/channel/${videoObj?.user}`);
}}
>
<Avatar
sx={{ height: 24, width: 24 }}
src={`/arbitrary/THUMBNAIL/${videoObj?.user}/qortal_avatar`}
alt={`${videoObj?.user}'s avatar`}
/>
<VideoCardName
sx={{
":hover": {
textDecoration: "underline",
},
}}
>
{videoObj?.user}
</VideoCardName>
{videoObj?.created && (
<VideoUploadDate>
{formatDate(videoObj.created)}
</VideoUploadDate>
)}
</NameContainer>
<Box
sx={{
display: "flex",
position: "absolute",
bottom: "5px",
right: "5px",
}}
>
<PlaylistSVG
color={theme.palette.text.primary}
height="36px"
width="36px"
/>
</Box>
</BottomParent>
</VideoCard>
</VideoCardCol>
);
}
return (
<ContextMenuResource
name={video.user}
service="VIDEO"
identifier={video.id}
link={`qortal://APP/Q-Tube/video/${encodeURIComponent(video.user)}/${encodeURIComponent(video.id)}`}
>
<VideoCardCol
key={videoObj.id}
onMouseEnter={() => setShowIcons(videoObj.id)}
onMouseLeave={() => setShowIcons(null)}
>
<IconsBox
sx={{
opacity: showIcons === videoObj.id ? 1 : 0,
zIndex: 2,
}}
>
{videoObj?.user === username && (
<Tooltip title="Edit video properties" placement="top">
<BlockIconContainer>
<EditIcon
onClick={() => {
dispatch(setEditVideo(videoObj));
}}
/>
</BlockIconContainer>
</Tooltip>
)}
{videoObj?.user !== username && (
<Tooltip title="Block user content" placement="top">
<BlockIconContainer>
<BlockIcon
onClick={() => {
blockUserFunc(videoObj?.user);
}}
/>
</BlockIconContainer>
</Tooltip>
)}
</IconsBox>
<VideoCard
onClick={() => {
navigate(`/video/${videoObj?.user}/${videoObj?.id}`);
}}
>
{videoObj?.duration > minDuration && (
<Box
position="absolute"
right={0}
bottom={0}
bgcolor="#202020"
zIndex={999}
>
<Typography color="white">
{formatTime(videoObj.duration)}
</Typography>
</Box>
)}
<VideoCardImageContainer
width={266}
height={150}
videoImage={videoObj.videoImage}
frameImages={videoObj?.extracts || []}
/>
<Tooltip
title={videoObj.title}
placement="top"
slotProps={{ tooltip: { sx: { fontSize: fontSizeSmall } } }}
>
<VideoCardTitle>{videoObj.title}</VideoCardTitle>
</Tooltip>
<BottomParent>
<NameContainer
onClick={e => {
e.stopPropagation();
navigate(`/channel/${videoObj?.user}`);
}}
>
<Avatar
sx={{ height: 24, width: 24 }}
src={`/arbitrary/THUMBNAIL/${videoObj?.user}/qortal_avatar`}
alt={`${videoObj?.user}'s avatar`}
/>
<VideoCardName
sx={{
":hover": {
textDecoration: "underline",
},
}}
>
{videoObj?.user}
</VideoCardName>
</NameContainer>
{videoObj?.created && (
<Box sx={{ flexDirection: "row", width: "100%" }}>
<VideoUploadDate sx={{ display: "inline" }}>
{formatDate(videoObj.created)}
</VideoUploadDate>
</Box>
)}
</BottomParent>
</VideoCard>
</VideoCardCol>
</ContextMenuResource>
);
})}
</VideoCardContainer>
);
};
export default PlayListList;

View File

@@ -68,6 +68,7 @@ export const VideoList = ({ videos }: VideoListProps) => {
return (
<VideoCardContainer>
{videos.map((video: any) => {
//key = video.id;
const fullId = video ? `${video.id}-${video.user}` : undefined;
const existingVideo = hashMapVideos[fullId];
let hasHash = false;

View File

@@ -28,11 +28,23 @@ export const VideoListComponentLevel = ({ mode }: VideoListProps) => {
const [videos, setVideos] = React.useState<Video[]>([]);
const isLoading = useSignal(true);
const { getVideo, checkAndUpdateVideo } = useFetchVideos();
// For Pagination
const pageRef = useRef(0);
const [hasMore, setHasMore] = useState(true);
const PAGE_SIZE = 20;
useEffect(() => {
firstFetch.current = false;
setVideos([]);
pageRef.current = 0;
setHasMore(true);
}, [paramName]);
const getVideos = React.useCallback(async () => {
isLoading.value = true;
try {
const offset = videos.length;
const offset = pageRef.current * PAGE_SIZE;
console.log('getVideos ParamName:', paramName);
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&query=${QTUBE_VIDEO_BASE}&limit=20&includemetadata=false&reverse=true&excludeblocked=true&name=${paramName}&exactmatchnames=true&offset=${offset}`;
const response = await fetch(url, {
method: "GET",
@@ -57,16 +69,24 @@ export const VideoListComponentLevel = ({ mode }: VideoListProps) => {
};
});
const copiedVideos: Video[] = [...videos];
structureData.forEach((video: Video) => {
const index = videos.findIndex(p => p.id === video.id);
if (index !== -1) {
copiedVideos[index] = video;
} else {
copiedVideos.push(video);
}
setVideos(prev => {
const updatedVideos = [...prev];
structureData.forEach(video => {
const exists = updatedVideos.some(v => v.id === video.id);
if (!exists) {
updatedVideos.push(video);
}
});
return updatedVideos;
});
setVideos(copiedVideos);
// If fewer than PAGE_SIZE results, we've reached the end
if (structureData.length < PAGE_SIZE) {
setHasMore(false);
} else {
pageRef.current += 1;
}
for (const content of structureData) {
if (content.user && content.id) {
@@ -81,20 +101,20 @@ export const VideoListComponentLevel = ({ mode }: VideoListProps) => {
console.log(error);
isLoading.value = false;
}
}, [videos, hashMapVideos]);
const getVideosHandlerMount = React.useCallback(async () => {
if (firstFetch.current) return;
firstFetch.current = true;
await getVideos();
afterFetch.current = true;
}, [getVideos]);
}, [checkAndUpdateVideo, getVideo, hashMapVideos, paramName]);
useEffect(() => {
const fetchVideos = async () => {
firstFetch.current = true;
console.log("Running useEffect: " + paramName);
await getVideos();
afterFetch.current = true;
};
if (!firstFetch.current) {
getVideosHandlerMount();
fetchVideos();
}
}, [getVideosHandlerMount]);
}, [paramName, getVideos]);
return (
<VideoManagerRow>
@@ -107,7 +127,10 @@ export const VideoListComponentLevel = ({ mode }: VideoListProps) => {
}}
>
<VideoList videos={videos} />
<LazyLoad onLoadMore={getVideos} isLoading={isLoading.value}></LazyLoad>
<LazyLoad
onLoadMore={hasMore ? getVideos : undefined}
isLoading={isLoading.value}
/>
</Box>
</VideoManagerRow>
);

View File

@@ -19,9 +19,14 @@ export const authSlice = createSlice({
addUser: (state, action) => {
state.user = action.payload;
},
setName: (state, action) => {
state.user.name = action.payload;
},
},
});
export const { addUser } = authSlice.actions;
export const { setName } = authSlice.actions;
export default authSlice.reducer;

View File

@@ -0,0 +1,8 @@
import { atom } from 'jotai';
export interface Name {
name: string;
}
export type Names = Name[];
export const namesAtom = atom<Names>([]);

View File

@@ -41,6 +41,14 @@ export const getAccountNames = async (
return emptyNamesFilled.length > 0 ? emptyNamesFilled : [namelessAddress];
};
export const getPrimaryAccountName = async (address: string) => {
const primaryName = (await qortalRequest({
action: "GET_PRIMARY_NAME",
address,
})) as string | null;
return primaryName ?? "";
}
export const searchTransactions = async (params: TransactionSearchParams) => {
return (await qortalRequest({
action: "SEARCH_TRANSACTIONS",

View File

@@ -29,6 +29,10 @@ import ConsentModal from "../components/common/ConsentModal";
import { useFetchSuperLikes } from "../hooks/useFetchSuperLikes";
import { SUPER_LIKE_BASE } from "../constants/Identifiers.ts";
import { minPriceSuperLike } from "../constants/Misc.ts";
import { useHandleNameData } from './../hooks/useHandleNameData.tsx';
import { namesAtom } from './../state/global/names';
import { useAtom } from 'jotai';
import { getPrimaryAccountName } from "../utils/qortalRequestFunctions.ts";
interface Props {
children: React.ReactNode;
@@ -47,24 +51,26 @@ const GlobalWrapper: React.FC<Props> = ({ children, setTheme }) => {
const user = useSelector((state: RootState) => state.auth.user);
const { addSuperlikeRawDataGetToList } = useFetchSuperLikes();
const interval = useRef<any>(null);
useHandleNameData();
const videoPlaying = useSelector(
(state: RootState) => state.global.videoPlaying
);
const username = useMemo(() => {
if (!user?.name) return "";
return user.name;
}, [user]);
const [names] = useAtom(namesAtom);
const getAvatar = React.useCallback(
async (author: string) => {
try {
const url = await qortalRequest({
action: "GET_QDN_RESOURCE_URL",
name: author,
service: "THUMBNAIL",
identifier: "qortal_avatar",
});
const url = `/arbitrary/THUMBNAIL/${author}/qortal_avatar`;
if (url) {
setUserAvatar(url);
dispatch(
@@ -89,27 +95,13 @@ const GlobalWrapper: React.FC<Props> = ({ children, setTheme }) => {
const { isLoadingGlobal } = useSelector((state: RootState) => state.global);
async function getNameInfo(address: string) {
const response = await qortalRequest({
action: "GET_ACCOUNT_NAMES",
address: address,
});
const nameData = response;
if (nameData?.length > 0) {
return nameData[0].name;
} else {
return "";
}
}
const askForAccountInformation = React.useCallback(async () => {
try {
const account = await qortalRequest({
action: "GET_USER_ACCOUNT",
});
const name = await getNameInfo(account.address);
const name = await getPrimaryAccountName(account.address);
dispatch(addUser({ ...account, name }));
} catch (error) {
console.error(error);
@@ -223,6 +215,7 @@ const GlobalWrapper: React.FC<Props> = ({ children, setTheme }) => {
setTheme={(val: string) => setTheme(val)}
isAuthenticated={!!user?.name}
userName={user?.name || ""}
allNames={names}
userAvatar={userAvatar}
authenticate={askForAccountInformation}
/>

View File

@@ -1,12 +1,13 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
server: {
host: '0.0.0.0',
port: 3000
},
server: {
host: '0.0.0.0',
port: 3000
},
base: "",
});