feat: support kind 20
This commit is contained in:
597
package-lock.json
generated
597
package-lock.json
generated
@@ -9,7 +9,6 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nextui-org/image": "^2.2.3",
|
|
||||||
"@noble/hashes": "^1.6.1",
|
"@noble/hashes": "^1.6.1",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.4",
|
"@radix-ui/react-alert-dialog": "^1.1.4",
|
||||||
"@radix-ui/react-avatar": "^1.1.2",
|
"@radix-ui/react-avatar": "^1.1.2",
|
||||||
@@ -29,7 +28,7 @@
|
|||||||
"cmdk": "^1.0.0",
|
"cmdk": "^1.0.0",
|
||||||
"dataloader": "^2.2.3",
|
"dataloader": "^2.2.3",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"framer-motion": "^11.15.0",
|
"embla-carousel-react": "^8.5.1",
|
||||||
"i18next": "^24.2.0",
|
"i18next": "^24.2.0",
|
||||||
"lru-cache": "^11.0.2",
|
"lru-cache": "^11.0.2",
|
||||||
"lucide-react": "^0.469.0",
|
"lucide-react": "^0.469.0",
|
||||||
@@ -2105,57 +2104,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz",
|
||||||
"integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig=="
|
"integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig=="
|
||||||
},
|
},
|
||||||
"node_modules/@formatjs/ecma402-abstract": {
|
|
||||||
"version": "2.3.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.1.tgz",
|
|
||||||
"integrity": "sha512-Ip9uV+/MpLXWRk03U/GzeJMuPeOXpJBSB5V1tjA6kJhvqssye5J5LoYLc7Z5IAHb7nR62sRoguzrFiVCP/hnzw==",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@formatjs/fast-memoize": "2.2.5",
|
|
||||||
"@formatjs/intl-localematcher": "0.5.9",
|
|
||||||
"decimal.js": "10",
|
|
||||||
"tslib": "2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@formatjs/fast-memoize": {
|
|
||||||
"version": "2.2.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.5.tgz",
|
|
||||||
"integrity": "sha512-6PoewUMrrcqxSoBXAOJDiW1m+AmkrAj0RiXnOMD59GRaswjXhm3MDhgepXPBgonc09oSirAJTsAggzAGQf6A6g==",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": "2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@formatjs/icu-messageformat-parser": {
|
|
||||||
"version": "2.9.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.9.7.tgz",
|
|
||||||
"integrity": "sha512-cuEHyRM5VqLQobANOjtjlgU7+qmk9Q3fDQuBiRRJ3+Wp3ZoZhpUPtUfuimZXsir6SaI2TaAJ+SLo9vLnV5QcbA==",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@formatjs/ecma402-abstract": "2.3.1",
|
|
||||||
"@formatjs/icu-skeleton-parser": "1.8.11",
|
|
||||||
"tslib": "2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@formatjs/icu-skeleton-parser": {
|
|
||||||
"version": "1.8.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.11.tgz",
|
|
||||||
"integrity": "sha512-8LlHHE/yL/zVJZHAX3pbKaCjZKmBIO6aJY1mkVh4RMSEu/2WRZ4Ysvv3kKXJ9M8RJLBHdnk1/dUQFdod1Dt7Dw==",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@formatjs/ecma402-abstract": "2.3.1",
|
|
||||||
"tslib": "2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@formatjs/intl-localematcher": {
|
|
||||||
"version": "0.5.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.9.tgz",
|
|
||||||
"integrity": "sha512-8zkGu/sv5euxbjfZ/xmklqLyDGQSxsLqg8XOq88JW3cmJtzhCP8EtSJXlaKZnVO4beEaoiT9wj4eIoCQ9smwxA==",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": "2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@humanfs/core": {
|
"node_modules/@humanfs/core": {
|
||||||
"version": "0.19.1",
|
"version": "0.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||||
@@ -2217,43 +2165,6 @@
|
|||||||
"url": "https://github.com/sponsors/nzakas"
|
"url": "https://github.com/sponsors/nzakas"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@internationalized/date": {
|
|
||||||
"version": "3.6.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.6.0.tgz",
|
|
||||||
"integrity": "sha512-+z6ti+CcJnRlLHok/emGEsWQhe7kfSmEW+/6qCzvKY67YPh7YOBfvc7+/+NXq+zJlbArg30tYpqLjNgcAYv2YQ==",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@swc/helpers": "^0.5.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@internationalized/message": {
|
|
||||||
"version": "3.1.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/@internationalized/message/-/message-3.1.6.tgz",
|
|
||||||
"integrity": "sha512-JxbK3iAcTIeNr1p0WIFg/wQJjIzJt9l/2KNY/48vXV7GRGZSv3zMxJsce008fZclk2cDC8y0Ig3odceHO7EfNQ==",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@swc/helpers": "^0.5.0",
|
|
||||||
"intl-messageformat": "^10.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@internationalized/number": {
|
|
||||||
"version": "3.6.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@internationalized/number/-/number-3.6.0.tgz",
|
|
||||||
"integrity": "sha512-PtrRcJVy7nw++wn4W2OuePQQfTqDzfusSuY1QTtui4wa7r+rGVtR75pO8CyKvHvzyQYi3Q1uO5sY0AsB4e65Bw==",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@swc/helpers": "^0.5.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@internationalized/string": {
|
|
||||||
"version": "3.2.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@internationalized/string/-/string-3.2.5.tgz",
|
|
||||||
"integrity": "sha512-rKs71Zvl2OKOHM+mzAFMIyqR5hI1d1O6BBkMK2/lkfg3fkmVh9Eeg0awcA8W2WqYqDOv6a86DIOlFpggwLtbuw==",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@swc/helpers": "^0.5.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@isaacs/cliui": {
|
"node_modules/@isaacs/cliui": {
|
||||||
"version": "8.0.2",
|
"version": "8.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||||
@@ -2323,138 +2234,6 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@nextui-org/image": {
|
|
||||||
"version": "2.2.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@nextui-org/image/-/image-2.2.3.tgz",
|
|
||||||
"integrity": "sha512-erd+c7uA4FRoOOdwS6di97VVJSBbs8mXv9cOY2kZHU830e2//TKlNaE4nC7xR9ApFEAtfXjoRzSyUCIlyXmD9Q==",
|
|
||||||
"dependencies": {
|
|
||||||
"@nextui-org/react-utils": "2.1.1",
|
|
||||||
"@nextui-org/shared-utils": "2.1.1",
|
|
||||||
"@nextui-org/use-image": "2.1.1"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@nextui-org/system": ">=2.4.0",
|
|
||||||
"@nextui-org/theme": ">=2.4.0",
|
|
||||||
"react": ">=18 || >=19.0.0-rc.0",
|
|
||||||
"react-dom": ">=18 || >=19.0.0-rc.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@nextui-org/react-rsc-utils": {
|
|
||||||
"version": "2.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@nextui-org/react-rsc-utils/-/react-rsc-utils-2.1.1.tgz",
|
|
||||||
"integrity": "sha512-9uKH1XkeomTGaswqlGKt0V0ooUev8mPXtKJolR+6MnpvBUrkqngw1gUGF0bq/EcCCkks2+VOHXZqFT6x9hGkQQ==",
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": ">=18 || >=19.0.0-rc.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@nextui-org/react-utils": {
|
|
||||||
"version": "2.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@nextui-org/react-utils/-/react-utils-2.1.1.tgz",
|
|
||||||
"integrity": "sha512-cN3Z0b2bV6Nf0CYD4imsGdXbHMQqad8KivltpBv1ItbI1/FSTAv9AHTKSzDE15hd/UwOGYt3Qm7I6tWzqov55w==",
|
|
||||||
"dependencies": {
|
|
||||||
"@nextui-org/react-rsc-utils": "2.1.1",
|
|
||||||
"@nextui-org/shared-utils": "2.1.1"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": ">=18 || >=19.0.0-rc.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@nextui-org/shared-utils": {
|
|
||||||
"version": "2.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@nextui-org/shared-utils/-/shared-utils-2.1.1.tgz",
|
|
||||||
"integrity": "sha512-qE8gZO63GqUX1ljOi/4PlwGzE84dhUS3zFIq+10/N6ePAaNjM4DwtL4ocucG3abCz4iRUueYKLIxTO2+eYyAfw=="
|
|
||||||
},
|
|
||||||
"node_modules/@nextui-org/system": {
|
|
||||||
"version": "2.4.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@nextui-org/system/-/system-2.4.4.tgz",
|
|
||||||
"integrity": "sha512-ldlUYq7VprTEC2s3LaMxQh7S7Xeyy6DYoKkOML9XHJBgSgVXCMr5QyoxvIkO2XRl5nu6KWn2QA1vjtj2xiMjRw==",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@internationalized/date": "3.6.0",
|
|
||||||
"@nextui-org/react-utils": "2.1.1",
|
|
||||||
"@nextui-org/system-rsc": "2.3.4",
|
|
||||||
"@react-aria/i18n": "3.12.4",
|
|
||||||
"@react-aria/overlays": "3.24.0",
|
|
||||||
"@react-aria/utils": "3.26.0",
|
|
||||||
"@react-stately/utils": "3.10.5",
|
|
||||||
"@react-types/datepicker": "3.9.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"framer-motion": ">=11.5.6 || >=12.0.0-alpha.1",
|
|
||||||
"react": ">=18 || >=19.0.0-rc.0",
|
|
||||||
"react-dom": ">=18 || >=19.0.0-rc.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@nextui-org/system-rsc": {
|
|
||||||
"version": "2.3.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@nextui-org/system-rsc/-/system-rsc-2.3.4.tgz",
|
|
||||||
"integrity": "sha512-Y6OLFO7diYnUMe5ffDPt6sIqCaah7FOqRaJ3ZQ/We8gE8AgHnyNQxWllLtRzBqaCiIheHLo7dTMed1FFmb775A==",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@react-types/shared": "3.26.0",
|
|
||||||
"clsx": "^1.2.1"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@nextui-org/theme": ">=2.4.0",
|
|
||||||
"react": ">=18 || >=19.0.0-rc.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@nextui-org/system-rsc/node_modules/clsx": {
|
|
||||||
"version": "1.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
|
|
||||||
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
|
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@nextui-org/theme": {
|
|
||||||
"version": "2.4.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@nextui-org/theme/-/theme-2.4.3.tgz",
|
|
||||||
"integrity": "sha512-QH9ps5NpenWU966INdGbdvZOWWUEGqxrLM2vyqkSRq+A65YON4Jhg/x1xWcSX0SJECNhoNZLh5mt6jp3jH5k8Q==",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@nextui-org/shared-utils": "2.1.1",
|
|
||||||
"clsx": "^1.2.1",
|
|
||||||
"color": "^4.2.3",
|
|
||||||
"color2k": "^2.0.2",
|
|
||||||
"deepmerge": "4.3.1",
|
|
||||||
"flat": "^5.0.2",
|
|
||||||
"tailwind-merge": "^2.5.2",
|
|
||||||
"tailwind-variants": "^0.1.20"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"tailwindcss": ">=3.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@nextui-org/theme/node_modules/clsx": {
|
|
||||||
"version": "1.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
|
|
||||||
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
|
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@nextui-org/use-image": {
|
|
||||||
"version": "2.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@nextui-org/use-image/-/use-image-2.1.1.tgz",
|
|
||||||
"integrity": "sha512-Tsfy9pA4AQBAj7rFIEonB9L/hXGg7M5agaAZNBUVpdp47NjcEwLpcU2XncKh8AhkQku0p4JOyMC9usRGV3z06Q==",
|
|
||||||
"dependencies": {
|
|
||||||
"@nextui-org/use-safe-layout-effect": "2.1.1"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": ">=18 || >=19.0.0-rc.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@nextui-org/use-safe-layout-effect": {
|
|
||||||
"version": "2.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@nextui-org/use-safe-layout-effect/-/use-safe-layout-effect-2.1.1.tgz",
|
|
||||||
"integrity": "sha512-p0vezi2eujC3rxlMQmCLQlc8CNbp+GQgk6YcSm7Rk10isWVlUII5T1L3y+rcFYdgTPObCkCngPPciNQhD7Lf7g==",
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": ">=18 || >=19.0.0-rc.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@noble/ciphers": {
|
"node_modules/@noble/ciphers": {
|
||||||
"version": "0.5.3",
|
"version": "0.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.5.3.tgz",
|
||||||
@@ -3478,212 +3257,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz",
|
||||||
"integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg=="
|
"integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg=="
|
||||||
},
|
},
|
||||||
"node_modules/@react-aria/focus": {
|
|
||||||
"version": "3.19.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.19.0.tgz",
|
|
||||||
"integrity": "sha512-hPF9EXoUQeQl1Y21/rbV2H4FdUR2v+4/I0/vB+8U3bT1CJ+1AFj1hc/rqx2DqEwDlEwOHN+E4+mRahQmlybq0A==",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@react-aria/interactions": "^3.22.5",
|
|
||||||
"@react-aria/utils": "^3.26.0",
|
|
||||||
"@react-types/shared": "^3.26.0",
|
|
||||||
"@swc/helpers": "^0.5.0",
|
|
||||||
"clsx": "^2.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@react-aria/i18n": {
|
|
||||||
"version": "3.12.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@react-aria/i18n/-/i18n-3.12.4.tgz",
|
|
||||||
"integrity": "sha512-j9+UL3q0Ls8MhXV9gtnKlyozq4aM95YywXqnmJtzT1rYeBx7w28hooqrWkCYLfqr4OIryv1KUnPiCSLwC2OC7w==",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@internationalized/date": "^3.6.0",
|
|
||||||
"@internationalized/message": "^3.1.6",
|
|
||||||
"@internationalized/number": "^3.6.0",
|
|
||||||
"@internationalized/string": "^3.2.5",
|
|
||||||
"@react-aria/ssr": "^3.9.7",
|
|
||||||
"@react-aria/utils": "^3.26.0",
|
|
||||||
"@react-types/shared": "^3.26.0",
|
|
||||||
"@swc/helpers": "^0.5.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@react-aria/interactions": {
|
|
||||||
"version": "3.22.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.22.5.tgz",
|
|
||||||
"integrity": "sha512-kMwiAD9E0TQp+XNnOs13yVJghiy8ET8L0cbkeuTgNI96sOAp/63EJ1FSrDf17iD8sdjt41LafwX/dKXW9nCcLQ==",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@react-aria/ssr": "^3.9.7",
|
|
||||||
"@react-aria/utils": "^3.26.0",
|
|
||||||
"@react-types/shared": "^3.26.0",
|
|
||||||
"@swc/helpers": "^0.5.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@react-aria/overlays": {
|
|
||||||
"version": "3.24.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@react-aria/overlays/-/overlays-3.24.0.tgz",
|
|
||||||
"integrity": "sha512-0kAXBsMNTc/a3M07tK9Cdt/ea8CxTAEJ223g8YgqImlmoBBYAL7dl5G01IOj67TM64uWPTmZrOklBchHWgEm3A==",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@react-aria/focus": "^3.19.0",
|
|
||||||
"@react-aria/i18n": "^3.12.4",
|
|
||||||
"@react-aria/interactions": "^3.22.5",
|
|
||||||
"@react-aria/ssr": "^3.9.7",
|
|
||||||
"@react-aria/utils": "^3.26.0",
|
|
||||||
"@react-aria/visually-hidden": "^3.8.18",
|
|
||||||
"@react-stately/overlays": "^3.6.12",
|
|
||||||
"@react-types/button": "^3.10.1",
|
|
||||||
"@react-types/overlays": "^3.8.11",
|
|
||||||
"@react-types/shared": "^3.26.0",
|
|
||||||
"@swc/helpers": "^0.5.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
|
|
||||||
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@react-aria/ssr": {
|
|
||||||
"version": "3.9.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.7.tgz",
|
|
||||||
"integrity": "sha512-GQygZaGlmYjmYM+tiNBA5C6acmiDWF52Nqd40bBp0Znk4M4hP+LTmI0lpI1BuKMw45T8RIhrAsICIfKwZvi2Gg==",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@swc/helpers": "^0.5.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 12"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@react-aria/utils": {
|
|
||||||
"version": "3.26.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.26.0.tgz",
|
|
||||||
"integrity": "sha512-LkZouGSjjQ0rEqo4XJosS4L3YC/zzQkfRM3KoqK6fUOmUJ9t0jQ09WjiF+uOoG9u+p30AVg3TrZRUWmoTS+koQ==",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@react-aria/ssr": "^3.9.7",
|
|
||||||
"@react-stately/utils": "^3.10.5",
|
|
||||||
"@react-types/shared": "^3.26.0",
|
|
||||||
"@swc/helpers": "^0.5.0",
|
|
||||||
"clsx": "^2.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@react-aria/visually-hidden": {
|
|
||||||
"version": "3.8.18",
|
|
||||||
"resolved": "https://registry.npmjs.org/@react-aria/visually-hidden/-/visually-hidden-3.8.18.tgz",
|
|
||||||
"integrity": "sha512-l/0igp+uub/salP35SsNWq5mGmg3G5F5QMS1gDZ8p28n7CgjvzyiGhJbbca7Oxvaw1HRFzVl9ev+89I7moNnFQ==",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@react-aria/interactions": "^3.22.5",
|
|
||||||
"@react-aria/utils": "^3.26.0",
|
|
||||||
"@react-types/shared": "^3.26.0",
|
|
||||||
"@swc/helpers": "^0.5.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@react-stately/overlays": {
|
|
||||||
"version": "3.6.12",
|
|
||||||
"resolved": "https://registry.npmjs.org/@react-stately/overlays/-/overlays-3.6.12.tgz",
|
|
||||||
"integrity": "sha512-QinvZhwZgj8obUyPIcyURSCjTZlqZYRRCS60TF8jH8ZpT0tEAuDb3wvhhSXuYA3Xo9EHLwvLjEf3tQKKdAQArw==",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@react-stately/utils": "^3.10.5",
|
|
||||||
"@react-types/overlays": "^3.8.11",
|
|
||||||
"@swc/helpers": "^0.5.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@react-stately/utils": {
|
|
||||||
"version": "3.10.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.5.tgz",
|
|
||||||
"integrity": "sha512-iMQSGcpaecghDIh3mZEpZfoFH3ExBwTtuBEcvZ2XnGzCgQjeYXcMdIUwAfVQLXFTdHUHGF6Gu6/dFrYsCzySBQ==",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@swc/helpers": "^0.5.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@react-types/button": {
|
|
||||||
"version": "3.10.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@react-types/button/-/button-3.10.1.tgz",
|
|
||||||
"integrity": "sha512-XTtap8o04+4QjPNAshFWOOAusUTxQlBjU2ai0BTVLShQEjHhRVDBIWsI2B2FKJ4KXT6AZ25llaxhNrreWGonmA==",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@react-types/shared": "^3.26.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@react-types/calendar": {
|
|
||||||
"version": "3.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@react-types/calendar/-/calendar-3.5.0.tgz",
|
|
||||||
"integrity": "sha512-O3IRE7AGwAWYnvJIJ80cOy7WwoJ0m8GtX/qSmvXQAjC4qx00n+b5aFNBYAQtcyc3RM5QpW6obs9BfwGetFiI8w==",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@internationalized/date": "^3.6.0",
|
|
||||||
"@react-types/shared": "^3.26.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@react-types/datepicker": {
|
|
||||||
"version": "3.9.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@react-types/datepicker/-/datepicker-3.9.0.tgz",
|
|
||||||
"integrity": "sha512-dbKL5Qsm2MQwOTtVQdOcKrrphcXAqDD80WLlSQrBLg+waDuuQ7H+TrvOT0thLKloNBlFUGnZZfXGRHINpih/0g==",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@internationalized/date": "^3.6.0",
|
|
||||||
"@react-types/calendar": "^3.5.0",
|
|
||||||
"@react-types/overlays": "^3.8.11",
|
|
||||||
"@react-types/shared": "^3.26.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@react-types/overlays": {
|
|
||||||
"version": "3.8.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/@react-types/overlays/-/overlays-3.8.11.tgz",
|
|
||||||
"integrity": "sha512-aw7T0rwVI3EuyG5AOaEIk8j7dZJQ9m34XAztXJVZ/W2+4pDDkLDbJ/EAPnuo2xGYRGhowuNDn4tDju01eHYi+w==",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@react-types/shared": "^3.26.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@react-types/shared": {
|
|
||||||
"version": "3.26.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.26.0.tgz",
|
|
||||||
"integrity": "sha512-6FuPqvhmjjlpEDLTiYx29IJCbCNWPlsyO+ZUmCUXzhUv2ttShOXfw8CmeHWHftT/b2KweAWuzqSlfeXPR76jpw==",
|
|
||||||
"peer": true,
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@rollup/plugin-node-resolve": {
|
"node_modules/@rollup/plugin-node-resolve": {
|
||||||
"version": "15.3.1",
|
"version": "15.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz",
|
||||||
@@ -4103,15 +3676,6 @@
|
|||||||
"string.prototype.matchall": "^4.0.6"
|
"string.prototype.matchall": "^4.0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@swc/helpers": {
|
|
||||||
"version": "0.5.15",
|
|
||||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
|
||||||
"integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": "^2.8.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/babel__core": {
|
"node_modules/@types/babel__core": {
|
||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||||
@@ -5321,19 +4885,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/color": {
|
|
||||||
"version": "4.2.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
|
|
||||||
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"color-convert": "^2.0.1",
|
|
||||||
"color-string": "^1.9.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12.5.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
@@ -5350,22 +4901,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||||
},
|
},
|
||||||
"node_modules/color-string": {
|
|
||||||
"version": "1.9.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
|
|
||||||
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"color-name": "^1.0.0",
|
|
||||||
"simple-swizzle": "^0.2.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/color2k": {
|
|
||||||
"version": "2.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/color2k/-/color2k-2.0.3.tgz",
|
|
||||||
"integrity": "sha512-zW190nQTIoXcGCaU08DvVNFTmQhUpnJfVuAKfWqUQkflXKpaDdpaYoM0iluLS9lgJNHyBF58KKA2FBEwkD7wog==",
|
|
||||||
"peer": true
|
|
||||||
},
|
|
||||||
"node_modules/commander": {
|
"node_modules/commander": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
||||||
@@ -5525,12 +5060,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/decimal.js": {
|
|
||||||
"version": "10.4.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz",
|
|
||||||
"integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==",
|
|
||||||
"peer": true
|
|
||||||
},
|
|
||||||
"node_modules/deep-is": {
|
"node_modules/deep-is": {
|
||||||
"version": "0.1.4",
|
"version": "0.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
||||||
@@ -5541,6 +5070,7 @@
|
|||||||
"version": "4.3.1",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||||
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
|
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -5634,6 +5164,31 @@
|
|||||||
"integrity": "sha512-Lf3++DumRE/QmweGjU+ZcKqQ+3bKkU/qjaKYhIJKEOhgIO9Xs6IiAQFkfFoj+RhgDk4LUeNsLo6plExHqSyu6Q==",
|
"integrity": "sha512-Lf3++DumRE/QmweGjU+ZcKqQ+3bKkU/qjaKYhIJKEOhgIO9Xs6IiAQFkfFoj+RhgDk4LUeNsLo6plExHqSyu6Q==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/embla-carousel": {
|
||||||
|
"version": "8.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.5.1.tgz",
|
||||||
|
"integrity": "sha512-JUb5+FOHobSiWQ2EJNaueCNT/cQU9L6XWBbWmorWPQT9bkbk+fhsuLr8wWrzXKagO3oWszBO7MSx+GfaRk4E6A=="
|
||||||
|
},
|
||||||
|
"node_modules/embla-carousel-react": {
|
||||||
|
"version": "8.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-8.5.1.tgz",
|
||||||
|
"integrity": "sha512-z9Y0K84BJvhChXgqn2CFYbfEi6AwEr+FFVVKm/MqbTQ2zIzO1VQri6w67LcfpVF0AjbhwVMywDZqY4alYkjW5w==",
|
||||||
|
"dependencies": {
|
||||||
|
"embla-carousel": "8.5.1",
|
||||||
|
"embla-carousel-reactive-utils": "8.5.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/embla-carousel-reactive-utils": {
|
||||||
|
"version": "8.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.5.1.tgz",
|
||||||
|
"integrity": "sha512-n7VSoGIiiDIc4MfXF3ZRTO59KDp820QDuyBDGlt5/65+lumPHxX2JLz0EZ23hZ4eg4vZGUXwMkYv02fw2JVo/A==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"embla-carousel": "8.5.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/emoji-regex": {
|
"node_modules/emoji-regex": {
|
||||||
"version": "9.2.2",
|
"version": "9.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
||||||
@@ -6121,15 +5676,6 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/flat": {
|
|
||||||
"version": "5.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
|
|
||||||
"integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==",
|
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
|
||||||
"flat": "cli.js"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/flat-cache": {
|
"node_modules/flat-cache": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
|
||||||
@@ -6186,32 +5732,6 @@
|
|||||||
"url": "https://github.com/sponsors/rawify"
|
"url": "https://github.com/sponsors/rawify"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/framer-motion": {
|
|
||||||
"version": "11.15.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.15.0.tgz",
|
|
||||||
"integrity": "sha512-MLk8IvZntxOMg7lDBLw2qgTHHv664bYoYmnFTmE0Gm/FW67aOJk0WM3ctMcG+Xhcv+vh5uyyXwxvxhSeJzSe+w==",
|
|
||||||
"dependencies": {
|
|
||||||
"motion-dom": "^11.14.3",
|
|
||||||
"motion-utils": "^11.14.3",
|
|
||||||
"tslib": "^2.4.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@emotion/is-prop-valid": "*",
|
|
||||||
"react": "^18.0.0 || ^19.0.0",
|
|
||||||
"react-dom": "^18.0.0 || ^19.0.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@emotion/is-prop-valid": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"react": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"react-dom": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/fs-extra": {
|
"node_modules/fs-extra": {
|
||||||
"version": "9.1.0",
|
"version": "9.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
|
||||||
@@ -6646,18 +6166,6 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/intl-messageformat": {
|
|
||||||
"version": "10.7.10",
|
|
||||||
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.10.tgz",
|
|
||||||
"integrity": "sha512-hp7iejCBiJdW3zmOe18FdlJu8U/JsADSDiBPQhfdSeI8B9POtvPRvPh3nMlvhYayGMKLv6maldhR7y3Pf1vkpw==",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@formatjs/ecma402-abstract": "2.3.1",
|
|
||||||
"@formatjs/fast-memoize": "2.2.5",
|
|
||||||
"@formatjs/icu-messageformat-parser": "2.9.7",
|
|
||||||
"tslib": "2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/is-array-buffer": {
|
"node_modules/is-array-buffer": {
|
||||||
"version": "3.0.5",
|
"version": "3.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
||||||
@@ -6675,12 +6183,6 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/is-arrayish": {
|
|
||||||
"version": "0.3.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
|
|
||||||
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
|
|
||||||
"peer": true
|
|
||||||
},
|
|
||||||
"node_modules/is-async-function": {
|
"node_modules/is-async-function": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz",
|
||||||
@@ -7370,16 +6872,6 @@
|
|||||||
"node": ">=16 || 14 >=14.17"
|
"node": ">=16 || 14 >=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/motion-dom": {
|
|
||||||
"version": "11.14.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.14.3.tgz",
|
|
||||||
"integrity": "sha512-lW+D2wBy5vxLJi6aCP0xyxTxlTfiu+b+zcpVbGVFUxotwThqhdpPRSmX8xztAgtZMPMeU0WGVn/k1w4I+TbPqA=="
|
|
||||||
},
|
|
||||||
"node_modules/motion-utils": {
|
|
||||||
"version": "11.14.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.14.3.tgz",
|
|
||||||
"integrity": "sha512-Xg+8xnqIJTpr0L/cidfTTBFkvRw26ZtGGuIhA94J9PQ2p4mEa06Xx7QVYZH0BP+EpMSaDlu+q0I0mmvwADPsaQ=="
|
|
||||||
},
|
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
@@ -8535,15 +8027,6 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/simple-swizzle": {
|
|
||||||
"version": "0.2.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
|
|
||||||
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"is-arrayish": "^0.3.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/smob": {
|
"node_modules/smob": {
|
||||||
"version": "1.5.0",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz",
|
||||||
@@ -8855,32 +8338,6 @@
|
|||||||
"url": "https://github.com/sponsors/dcastil"
|
"url": "https://github.com/sponsors/dcastil"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tailwind-variants": {
|
|
||||||
"version": "0.1.20",
|
|
||||||
"resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-0.1.20.tgz",
|
|
||||||
"integrity": "sha512-AMh7x313t/V+eTySKB0Dal08RHY7ggYK0MSn/ad8wKWOrDUIzyiWNayRUm2PIJ4VRkvRnfNuyRuKbLV3EN+ewQ==",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"tailwind-merge": "^1.14.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=16.x",
|
|
||||||
"pnpm": ">=7.x"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"tailwindcss": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tailwind-variants/node_modules/tailwind-merge": {
|
|
||||||
"version": "1.14.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-1.14.0.tgz",
|
|
||||||
"integrity": "sha512-3mFKyCo/MBcgyOTlrY8T7odzZFx+w+qKSMAmdFzRvqBfLlSigU6TZnlFHK0lkMwj9Bj8OYU+9yW9lmGuS0QEnQ==",
|
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/dcastil"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tailwindcss": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "3.4.17",
|
"version": "3.4.17",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nextui-org/image": "^2.2.3",
|
|
||||||
"@noble/hashes": "^1.6.1",
|
"@noble/hashes": "^1.6.1",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.4",
|
"@radix-ui/react-alert-dialog": "^1.1.4",
|
||||||
"@radix-ui/react-avatar": "^1.1.2",
|
"@radix-ui/react-avatar": "^1.1.2",
|
||||||
@@ -39,7 +38,7 @@
|
|||||||
"cmdk": "^1.0.0",
|
"cmdk": "^1.0.0",
|
||||||
"dataloader": "^2.2.3",
|
"dataloader": "^2.2.3",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"framer-motion": "^11.15.0",
|
"embla-carousel-react": "^8.5.1",
|
||||||
"i18next": "^24.2.0",
|
"i18next": "^24.2.0",
|
||||||
"lru-cache": "^11.0.2",
|
"lru-cache": "^11.0.2",
|
||||||
"lucide-react": "^0.469.0",
|
"lucide-react": "^0.469.0",
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { isNsfwEvent } from '@/lib/event'
|
import { isNsfwEvent, isPictureEvent } from '@/lib/event'
|
||||||
|
import { extractImetaUrlFromTag } from '@/lib/tag'
|
||||||
|
import { isImage, isVideo } from '@/lib/url'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { Event } from 'nostr-tools'
|
import { Event } from 'nostr-tools'
|
||||||
import { memo } from 'react'
|
import { memo } from 'react'
|
||||||
@@ -14,6 +16,7 @@ import {
|
|||||||
import ImageGallery from '../ImageGallery'
|
import ImageGallery from '../ImageGallery'
|
||||||
import VideoPlayer from '../VideoPlayer'
|
import VideoPlayer from '../VideoPlayer'
|
||||||
import WebPreview from '../WebPreview'
|
import WebPreview from '../WebPreview'
|
||||||
|
import { URL_REGEX } from '@/constants'
|
||||||
|
|
||||||
const Content = memo(
|
const Content = memo(
|
||||||
({
|
({
|
||||||
@@ -25,7 +28,7 @@ const Content = memo(
|
|||||||
className?: string
|
className?: string
|
||||||
size?: 'normal' | 'small'
|
size?: 'normal' | 'small'
|
||||||
}) => {
|
}) => {
|
||||||
const { content, images, videos, embeddedNotes, lastNonMediaUrl } = preprocess(event.content)
|
const { content, images, videos, embeddedNotes, lastNonMediaUrl } = preprocess(event)
|
||||||
const isNsfw = isNsfwEvent(event)
|
const isNsfw = isNsfwEvent(event)
|
||||||
const nodes = embedded(content, [
|
const nodes = embedded(content, [
|
||||||
embeddedNormalUrlRenderer,
|
embeddedNormalUrlRenderer,
|
||||||
@@ -39,7 +42,7 @@ const Content = memo(
|
|||||||
if (images.length) {
|
if (images.length) {
|
||||||
nodes.push(
|
nodes.push(
|
||||||
<ImageGallery
|
<ImageGallery
|
||||||
className={`w-fit ${size === 'small' ? 'mt-1' : 'mt-2'}`}
|
className={`${size === 'small' ? 'mt-1' : 'mt-2'}`}
|
||||||
key={`image-gallery-${event.id}`}
|
key={`image-gallery-${event.id}`}
|
||||||
images={images}
|
images={images}
|
||||||
isNsfw={isNsfw}
|
isNsfw={isNsfw}
|
||||||
@@ -95,9 +98,9 @@ const Content = memo(
|
|||||||
Content.displayName = 'Content'
|
Content.displayName = 'Content'
|
||||||
export default Content
|
export default Content
|
||||||
|
|
||||||
function preprocess(content: string) {
|
function preprocess(event: Event) {
|
||||||
const urlRegex = /(https?:\/\/[^\s"']+)/g
|
const content = event.content
|
||||||
const urls = content.match(urlRegex) || []
|
const urls = content.match(URL_REGEX) || []
|
||||||
let lastNonMediaUrl: string | undefined
|
let lastNonMediaUrl: string | undefined
|
||||||
|
|
||||||
let c = content
|
let c = content
|
||||||
@@ -116,6 +119,15 @@ function preprocess(content: string) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (isPictureEvent(event)) {
|
||||||
|
event.tags.forEach((tag) => {
|
||||||
|
const imageUrl = extractImetaUrlFromTag(tag)
|
||||||
|
if (imageUrl) {
|
||||||
|
images.push(imageUrl)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const embeddedNotes: string[] = []
|
const embeddedNotes: string[] = []
|
||||||
const embeddedNoteRegex = /nostr:(note1[a-z0-9]{58}|nevent1[a-z0-9]+|naddr1[a-z0-9]+)/g
|
const embeddedNoteRegex = /nostr:(note1[a-z0-9]{58}|nevent1[a-z0-9]+|naddr1[a-z0-9]+)/g
|
||||||
;(c.match(embeddedNoteRegex) || []).forEach((note) => {
|
;(c.match(embeddedNoteRegex) || []).forEach((note) => {
|
||||||
@@ -123,23 +135,7 @@ function preprocess(content: string) {
|
|||||||
embeddedNotes.push(note)
|
embeddedNotes.push(note)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
c = c.replace(/\n{3,}/g, '\n\n').trim()
|
||||||
|
|
||||||
return { content: c, images, videos, embeddedNotes, lastNonMediaUrl }
|
return { content: c, images, videos, embeddedNotes, lastNonMediaUrl }
|
||||||
}
|
}
|
||||||
|
|
||||||
function isImage(url: string) {
|
|
||||||
try {
|
|
||||||
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.heic', '.svg']
|
|
||||||
return imageExtensions.some((ext) => new URL(url).pathname.toLowerCase().endsWith(ext))
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isVideo(url: string) {
|
|
||||||
try {
|
|
||||||
const videoExtensions = ['.mp4', '.webm', '.ogg', '.mov']
|
|
||||||
return videoExtensions.some((ext) => new URL(url).pathname.toLowerCase().endsWith(ext))
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
35
src/components/Image/index.tsx
Normal file
35
src/components/Image/index.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { Skeleton } from '@/components/ui/skeleton'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { HTMLAttributes, useState } from 'react'
|
||||||
|
|
||||||
|
export default function Image({
|
||||||
|
src,
|
||||||
|
alt,
|
||||||
|
className = '',
|
||||||
|
classNames = {},
|
||||||
|
...props
|
||||||
|
}: HTMLAttributes<HTMLDivElement> & {
|
||||||
|
src: string
|
||||||
|
alt?: string
|
||||||
|
classNames?: {
|
||||||
|
wrapper?: string
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('relative', classNames.wrapper ?? '')} {...props}>
|
||||||
|
{isLoading && <Skeleton className={cn('absolute inset-0', className)} />}
|
||||||
|
<img
|
||||||
|
src={src}
|
||||||
|
alt={alt}
|
||||||
|
className={cn(
|
||||||
|
'object-cover transition-opacity duration-700',
|
||||||
|
isLoading ? 'opacity-0' : 'opacity-100',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
onLoad={() => setIsLoading(false)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
43
src/components/ImageCarousel/index.tsx
Normal file
43
src/components/ImageCarousel/index.tsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { Carousel, CarouselContent, CarouselItem } from '@/components/ui/carousel'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import Lightbox from 'yet-another-react-lightbox'
|
||||||
|
import Zoom from 'yet-another-react-lightbox/plugins/zoom'
|
||||||
|
import Image from '../Image'
|
||||||
|
import NsfwOverlay from '../NsfwOverlay'
|
||||||
|
|
||||||
|
export function ImageCarousel({ images, isNsfw = false }: { images: string[]; isNsfw?: boolean }) {
|
||||||
|
const [index, setIndex] = useState(-1)
|
||||||
|
|
||||||
|
const handlePhotoClick = (event: React.MouseEvent, current: number) => {
|
||||||
|
event.preventDefault()
|
||||||
|
setIndex(current)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Carousel className="w-full">
|
||||||
|
<CarouselContent>
|
||||||
|
{images.map((url, index) => (
|
||||||
|
<CarouselItem key={index}>
|
||||||
|
<Image src={url} onClick={(e) => handlePhotoClick(e, index)} />
|
||||||
|
</CarouselItem>
|
||||||
|
))}
|
||||||
|
</CarouselContent>
|
||||||
|
</Carousel>
|
||||||
|
<Lightbox
|
||||||
|
index={index}
|
||||||
|
slides={images.map((src) => ({ src }))}
|
||||||
|
plugins={[Zoom]}
|
||||||
|
open={index >= 0}
|
||||||
|
close={() => setIndex(-1)}
|
||||||
|
controller={{
|
||||||
|
closeOnBackdropClick: true,
|
||||||
|
closeOnPullUp: true,
|
||||||
|
closeOnPullDown: true
|
||||||
|
}}
|
||||||
|
styles={{ toolbar: { paddingTop: '2.25rem' } }}
|
||||||
|
/>
|
||||||
|
{isNsfw && <NsfwOverlay className="rounded-lg" />}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Image } from '@nextui-org/image'
|
|
||||||
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { useState } from 'react'
|
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
||||||
|
import { ReactNode, useState } from 'react'
|
||||||
import Lightbox from 'yet-another-react-lightbox'
|
import Lightbox from 'yet-another-react-lightbox'
|
||||||
import Zoom from 'yet-another-react-lightbox/plugins/zoom'
|
import Zoom from 'yet-another-react-lightbox/plugins/zoom'
|
||||||
|
import Image from '../Image'
|
||||||
import NsfwOverlay from '../NsfwOverlay'
|
import NsfwOverlay from '../NsfwOverlay'
|
||||||
|
|
||||||
export default function ImageGallery({
|
export default function ImageGallery({
|
||||||
@@ -17,32 +17,70 @@ export default function ImageGallery({
|
|||||||
isNsfw?: boolean
|
isNsfw?: boolean
|
||||||
size?: 'normal' | 'small'
|
size?: 'normal' | 'small'
|
||||||
}) {
|
}) {
|
||||||
|
const { isSmallScreen } = useScreenSize()
|
||||||
const [index, setIndex] = useState(-1)
|
const [index, setIndex] = useState(-1)
|
||||||
|
|
||||||
const handlePhotoClick = (event: React.MouseEvent, current: number) => {
|
const handlePhotoClick = (event: React.MouseEvent, current: number) => {
|
||||||
|
event.stopPropagation()
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
setIndex(current)
|
setIndex(current)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
let imageContent: ReactNode | null = null
|
||||||
<div className={cn('relative', className)} onClick={(e) => e.stopPropagation()}>
|
if (images.length === 1) {
|
||||||
<ScrollArea className="w-full">
|
imageContent = (
|
||||||
<div className="flex space-x-2">
|
|
||||||
{images.map((src, index) => (
|
|
||||||
<Image
|
<Image
|
||||||
key={index}
|
key={index}
|
||||||
className={cn(
|
className={cn('rounded-lg cursor-pointer', size === 'small' ? 'h-[15vh]' : 'h-[30vh]')}
|
||||||
'rounded-lg cursor-pointer z-0 object-cover',
|
src={images[0]}
|
||||||
size === 'small' ? 'h-[15vh]' : 'h-[30vh]'
|
onClick={(e) => handlePhotoClick(e, 0)}
|
||||||
)}
|
/>
|
||||||
|
)
|
||||||
|
} else if (size === 'small') {
|
||||||
|
imageContent = (
|
||||||
|
<div className="grid grid-cols-4 gap-2">
|
||||||
|
{images.map((src, i) => (
|
||||||
|
<Image
|
||||||
|
key={i}
|
||||||
|
className="rounded-lg cursor-pointer aspect-square"
|
||||||
src={src}
|
src={src}
|
||||||
onClick={(e) => handlePhotoClick(e, index)}
|
onClick={(e) => handlePhotoClick(e, i)}
|
||||||
removeWrapper
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<ScrollBar orientation="horizontal" />
|
)
|
||||||
</ScrollArea>
|
} else if (isSmallScreen && (images.length === 2 || images.length === 4)) {
|
||||||
|
imageContent = (
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
{images.map((src, i) => (
|
||||||
|
<Image
|
||||||
|
key={i}
|
||||||
|
className="rounded-lg cursor-pointer aspect-square"
|
||||||
|
src={src}
|
||||||
|
onClick={(e) => handlePhotoClick(e, i)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
imageContent = (
|
||||||
|
<div className="grid grid-cols-3 gap-2">
|
||||||
|
{images.map((src, i) => (
|
||||||
|
<Image
|
||||||
|
key={i}
|
||||||
|
className="rounded-lg cursor-pointer aspect-square"
|
||||||
|
src={src}
|
||||||
|
onClick={(e) => handlePhotoClick(e, i)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('relative w-fit max-w-full', className)}>
|
||||||
|
{imageContent}
|
||||||
|
<div onClick={(e) => e.stopPropagation()}>
|
||||||
<Lightbox
|
<Lightbox
|
||||||
index={index}
|
index={index}
|
||||||
slides={images.map((src) => ({ src }))}
|
slides={images.map((src) => ({ src }))}
|
||||||
@@ -56,6 +94,7 @@ export default function ImageGallery({
|
|||||||
}}
|
}}
|
||||||
styles={{ toolbar: { paddingTop: '2.25rem' } }}
|
styles={{ toolbar: { paddingTop: '2.25rem' } }}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
{isNsfw && <NsfwOverlay className="rounded-lg" />}
|
{isNsfw && <NsfwOverlay className="rounded-lg" />}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
190
src/components/Nip22ReplyNoteList/index.tsx
Normal file
190
src/components/Nip22ReplyNoteList/index.tsx
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
import { Separator } from '@/components/ui/separator'
|
||||||
|
import { COMMENT_EVENT_KIND } from '@/constants'
|
||||||
|
import { tagNameEquals } from '@/lib/tag'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { useNostr } from '@/providers/NostrProvider'
|
||||||
|
import { useNoteStats } from '@/providers/NoteStatsProvider'
|
||||||
|
import client from '@/services/client.service'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { Event as NEvent } from 'nostr-tools'
|
||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import ReplyNote from '../ReplyNote'
|
||||||
|
import { isCommentEvent } from '@/lib/event'
|
||||||
|
|
||||||
|
const LIMIT = 100
|
||||||
|
|
||||||
|
export default function Nip22ReplyNoteList({
|
||||||
|
event,
|
||||||
|
className
|
||||||
|
}: {
|
||||||
|
event: NEvent
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { pubkey } = useNostr()
|
||||||
|
const [timelineKey, setTimelineKey] = useState<string | undefined>(undefined)
|
||||||
|
const [until, setUntil] = useState<number | undefined>(() => dayjs().unix())
|
||||||
|
const [replies, setReplies] = useState<NEvent[]>([])
|
||||||
|
const [replyMap, setReplyMap] = useState<
|
||||||
|
Record<string, { event: NEvent; level: number; parent?: NEvent } | undefined>
|
||||||
|
>({})
|
||||||
|
const [loading, setLoading] = useState<boolean>(false)
|
||||||
|
const [highlightReplyId, setHighlightReplyId] = useState<string | undefined>(undefined)
|
||||||
|
const { updateNoteReplyCount } = useNoteStats()
|
||||||
|
const replyRefs = useRef<Record<string, HTMLDivElement | null>>({})
|
||||||
|
const bottomRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleEventPublished = (data: Event) => {
|
||||||
|
const customEvent = data as CustomEvent<NEvent>
|
||||||
|
const evt = customEvent.detail
|
||||||
|
if (
|
||||||
|
isCommentEvent(evt) &&
|
||||||
|
evt.tags.some(([tagName, tagValue]) => tagName === 'E' && tagValue === event.id)
|
||||||
|
) {
|
||||||
|
onNewReply(evt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
client.addEventListener('eventPublished', handleEventPublished)
|
||||||
|
return () => {
|
||||||
|
client.removeEventListener('eventPublished', handleEventPublished)
|
||||||
|
}
|
||||||
|
}, [event])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (loading) return
|
||||||
|
|
||||||
|
const init = async () => {
|
||||||
|
setLoading(true)
|
||||||
|
setReplies([])
|
||||||
|
|
||||||
|
try {
|
||||||
|
const relayList = await client.fetchRelayList(event.pubkey)
|
||||||
|
const { closer, timelineKey } = await client.subscribeTimeline(
|
||||||
|
relayList.read.slice(0, 5),
|
||||||
|
{
|
||||||
|
'#E': [event.id],
|
||||||
|
kinds: [COMMENT_EVENT_KIND],
|
||||||
|
limit: LIMIT
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onEvents: (evts, eosed) => {
|
||||||
|
setReplies(evts.reverse())
|
||||||
|
if (eosed) {
|
||||||
|
setLoading(false)
|
||||||
|
setUntil(evts.length >= LIMIT ? evts[evts.length - 1].created_at - 1 : undefined)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onNew: (evt) => {
|
||||||
|
onNewReply(evt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
setTimelineKey(timelineKey)
|
||||||
|
return closer
|
||||||
|
} catch {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const promise = init()
|
||||||
|
return () => {
|
||||||
|
promise.then((closer) => closer?.())
|
||||||
|
}
|
||||||
|
}, [event])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateNoteReplyCount(event.id, replies.length)
|
||||||
|
|
||||||
|
const replyMap: Record<string, { event: NEvent; level: number; parent?: NEvent } | undefined> =
|
||||||
|
{}
|
||||||
|
for (const reply of replies) {
|
||||||
|
const parentEventId = reply.tags.find(tagNameEquals('e'))?.[1]
|
||||||
|
if (parentEventId && parentEventId !== event.id) {
|
||||||
|
const parentReplyInfo = replyMap[parentEventId]
|
||||||
|
const level = parentReplyInfo ? parentReplyInfo.level + 1 : 1
|
||||||
|
replyMap[reply.id] = { event: reply, level, parent: parentReplyInfo?.event }
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
replyMap[reply.id] = { event: reply, level: 1 }
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
setReplyMap(replyMap)
|
||||||
|
}, [replies, event.id, updateNoteReplyCount])
|
||||||
|
|
||||||
|
const loadMore = async () => {
|
||||||
|
if (loading || !until || !timelineKey) return
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
const events = await client.loadMoreTimeline(timelineKey, until, LIMIT)
|
||||||
|
const olderReplies = events.reverse()
|
||||||
|
if (olderReplies.length > 0) {
|
||||||
|
setReplies((pre) => [...olderReplies, ...pre])
|
||||||
|
}
|
||||||
|
setUntil(events.length ? events[events.length - 1].created_at - 1 : undefined)
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onNewReply = (evt: NEvent) => {
|
||||||
|
setReplies((pre) => {
|
||||||
|
if (pre.some((reply) => reply.id === evt.id)) return pre
|
||||||
|
return [...pre, evt]
|
||||||
|
})
|
||||||
|
if (evt.pubkey === pubkey) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (bottomRef.current) {
|
||||||
|
bottomRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
||||||
|
}
|
||||||
|
highlightReply(evt.id, false)
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const highlightReply = (eventId: string, scrollTo = true) => {
|
||||||
|
if (scrollTo) {
|
||||||
|
const ref = replyRefs.current[eventId]
|
||||||
|
if (ref) {
|
||||||
|
ref.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setHighlightReplyId(eventId)
|
||||||
|
setTimeout(() => {
|
||||||
|
setHighlightReplyId((pre) => (pre === eventId ? undefined : pre))
|
||||||
|
}, 1500)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={`text-sm text-center text-muted-foreground ${!loading ? 'hover:text-foreground cursor-pointer' : ''}`}
|
||||||
|
onClick={loadMore}
|
||||||
|
>
|
||||||
|
{loading ? t('loading...') : until ? t('load more older replies') : null}
|
||||||
|
</div>
|
||||||
|
{replies.length > 0 && (loading || until) && <Separator className="my-2" />}
|
||||||
|
<div className={cn('mb-4', className)}>
|
||||||
|
{replies.map((reply) => {
|
||||||
|
const info = replyMap[reply.id]
|
||||||
|
return (
|
||||||
|
<div ref={(el) => (replyRefs.current[reply.id] = el)} key={reply.id}>
|
||||||
|
<ReplyNote
|
||||||
|
event={reply}
|
||||||
|
parentEvent={info?.parent}
|
||||||
|
onClickParent={highlightReply}
|
||||||
|
highlight={highlightReplyId === reply.id}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{replies.length === 0 && !loading && !until && (
|
||||||
|
<div className="text-sm text-center text-muted-foreground">{t('no replies')}</div>
|
||||||
|
)}
|
||||||
|
<div ref={bottomRef} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -62,7 +62,7 @@ export default function Note({
|
|||||||
)}
|
)}
|
||||||
<Content className="mt-2" event={event} />
|
<Content className="mt-2" event={event} />
|
||||||
{!hideStats && (
|
{!hideStats && (
|
||||||
<NoteStats className="mt-3 sm:mt-4" event={event} fetchIfNotExisting={fetchNoteStats} />
|
<NoteStats className="mt-3" event={event} fetchIfNotExisting={fetchNoteStats} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { PICTURE_EVENT_KIND } from '@/constants'
|
||||||
import { useFetchRelayInfos } from '@/hooks'
|
import { useFetchRelayInfos } from '@/hooks'
|
||||||
import { isReplyNoteEvent } from '@/lib/event'
|
import { isReplyNoteEvent } from '@/lib/event'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { useNostr } from '@/providers/NostrProvider'
|
import { useNostr } from '@/providers/NostrProvider'
|
||||||
|
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
||||||
import client from '@/services/client.service'
|
import client from '@/services/client.service'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { Event, Filter, kinds } from 'nostr-tools'
|
import { Event, Filter, kinds } from 'nostr-tools'
|
||||||
@@ -10,10 +12,14 @@ import { useEffect, useMemo, useRef, useState } from 'react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import PullToRefresh from 'react-simple-pull-to-refresh'
|
import PullToRefresh from 'react-simple-pull-to-refresh'
|
||||||
import NoteCard from '../NoteCard'
|
import NoteCard from '../NoteCard'
|
||||||
|
import PictureNoteCard from '../PictureNoteCard'
|
||||||
|
import SimpleMasonryGrid from '../SimpleMasonryGrid'
|
||||||
|
|
||||||
const NORMAL_RELAY_LIMIT = 100
|
const NORMAL_RELAY_LIMIT = 100
|
||||||
const ALGO_RELAY_LIMIT = 500
|
const ALGO_RELAY_LIMIT = 500
|
||||||
|
|
||||||
|
type TListMode = 'posts' | 'postsAndReplies' | 'pictures'
|
||||||
|
|
||||||
export default function NoteList({
|
export default function NoteList({
|
||||||
relayUrls,
|
relayUrls,
|
||||||
filter = {},
|
filter = {},
|
||||||
@@ -24,6 +30,7 @@ export default function NoteList({
|
|||||||
className?: string
|
className?: string
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const { isSmallScreen } = useScreenSize()
|
||||||
const { signEvent, checkLogin } = useNostr()
|
const { signEvent, checkLogin } = useNostr()
|
||||||
const { isFetching: isFetchingRelayInfo, areAlgoRelays } = useFetchRelayInfos([...relayUrls])
|
const { isFetching: isFetchingRelayInfo, areAlgoRelays } = useFetchRelayInfos([...relayUrls])
|
||||||
const [refreshCount, setRefreshCount] = useState(0)
|
const [refreshCount, setRefreshCount] = useState(0)
|
||||||
@@ -32,15 +39,23 @@ export default function NoteList({
|
|||||||
const [newEvents, setNewEvents] = useState<Event[]>([])
|
const [newEvents, setNewEvents] = useState<Event[]>([])
|
||||||
const [hasMore, setHasMore] = useState<boolean>(true)
|
const [hasMore, setHasMore] = useState<boolean>(true)
|
||||||
const [refreshing, setRefreshing] = useState(true)
|
const [refreshing, setRefreshing] = useState(true)
|
||||||
const [displayReplies, setDisplayReplies] = useState(false)
|
const [listMode, setListMode] = useState<TListMode>('posts')
|
||||||
const bottomRef = useRef<HTMLDivElement | null>(null)
|
const bottomRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
const isPictures = useMemo(() => listMode === 'pictures', [listMode])
|
||||||
const noteFilter = useMemo(() => {
|
const noteFilter = useMemo(() => {
|
||||||
|
if (isPictures) {
|
||||||
return {
|
return {
|
||||||
kinds: [kinds.ShortTextNote, kinds.Repost],
|
kinds: [PICTURE_EVENT_KIND],
|
||||||
limit: areAlgoRelays ? ALGO_RELAY_LIMIT : NORMAL_RELAY_LIMIT,
|
limit: areAlgoRelays ? ALGO_RELAY_LIMIT : NORMAL_RELAY_LIMIT,
|
||||||
...filter
|
...filter
|
||||||
}
|
}
|
||||||
}, [JSON.stringify(filter), areAlgoRelays])
|
}
|
||||||
|
return {
|
||||||
|
kinds: [kinds.ShortTextNote, kinds.Repost, PICTURE_EVENT_KIND],
|
||||||
|
limit: areAlgoRelays ? ALGO_RELAY_LIMIT : NORMAL_RELAY_LIMIT,
|
||||||
|
...filter
|
||||||
|
}
|
||||||
|
}, [JSON.stringify(filter), areAlgoRelays, isPictures])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isFetchingRelayInfo || relayUrls.length === 0) return
|
if (isFetchingRelayInfo || relayUrls.length === 0) return
|
||||||
@@ -151,16 +166,7 @@ export default function NoteList({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('space-y-2 sm:space-y-2', className)}>
|
<div className={cn('space-y-2 sm:space-y-2', className)}>
|
||||||
<DisplayRepliesSwitch displayReplies={displayReplies} setDisplayReplies={setDisplayReplies} />
|
<ListModeSwitch listMode={listMode} setListMode={setListMode} />
|
||||||
<div className="space-y-2 sm:space-y-2">
|
|
||||||
{newEvents.filter((event) => displayReplies || !isReplyNoteEvent(event)).length > 0 && (
|
|
||||||
<div className="flex justify-center w-full max-sm:mt-2">
|
|
||||||
<Button size="lg" onClick={showNewEvents}>
|
|
||||||
{t('show new notes')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<PullToRefresh
|
<PullToRefresh
|
||||||
onRefresh={async () => {
|
onRefresh={async () => {
|
||||||
setRefreshCount((count) => count + 1)
|
setRefreshCount((count) => count + 1)
|
||||||
@@ -168,15 +174,32 @@ export default function NoteList({
|
|||||||
}}
|
}}
|
||||||
pullingContent=""
|
pullingContent=""
|
||||||
>
|
>
|
||||||
|
<div className="space-y-2 sm:space-y-2">
|
||||||
|
{newEvents.filter((event) => listMode !== 'posts' || !isReplyNoteEvent(event)).length >
|
||||||
|
0 && (
|
||||||
|
<div className="flex justify-center w-full max-sm:mt-2">
|
||||||
|
<Button size="lg" onClick={showNewEvents}>
|
||||||
|
{t('show new notes')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isPictures ? (
|
||||||
|
<SimpleMasonryGrid
|
||||||
|
className="px-2 sm:px-4"
|
||||||
|
columnCount={isSmallScreen ? 2 : 3}
|
||||||
|
items={events.map((event) => (
|
||||||
|
<PictureNoteCard key={event.id} className="w-full" event={event} />
|
||||||
|
))}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<div>
|
<div>
|
||||||
{events
|
{events
|
||||||
.filter((event) => displayReplies || !isReplyNoteEvent(event))
|
.filter((event) => listMode === 'postsAndReplies' || !isReplyNoteEvent(event))
|
||||||
.map((event) => (
|
.map((event) => (
|
||||||
<NoteCard key={event.id} className="w-full" event={event} />
|
<NoteCard key={event.id} className="w-full" event={event} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</PullToRefresh>
|
)}
|
||||||
</div>
|
|
||||||
<div className="text-center text-sm text-muted-foreground">
|
<div className="text-center text-sm text-muted-foreground">
|
||||||
{hasMore || refreshing ? (
|
{hasMore || refreshing ? (
|
||||||
<div ref={bottomRef}>{t('loading...')}</div>
|
<div ref={bottomRef}>{t('loading...')}</div>
|
||||||
@@ -191,15 +214,17 @@ export default function NoteList({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</PullToRefresh>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function DisplayRepliesSwitch({
|
function ListModeSwitch({
|
||||||
displayReplies,
|
listMode,
|
||||||
setDisplayReplies
|
setListMode
|
||||||
}: {
|
}: {
|
||||||
displayReplies: boolean
|
listMode: TListMode
|
||||||
setDisplayReplies: (value: boolean) => void
|
setListMode: (listMode: TListMode) => void
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
@@ -207,20 +232,26 @@ function DisplayRepliesSwitch({
|
|||||||
<div>
|
<div>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div
|
<div
|
||||||
className={`w-1/2 text-center py-2 font-semibold clickable cursor-pointer rounded-lg ${displayReplies ? 'text-muted-foreground' : ''}`}
|
className={`w-1/3 text-center py-2 font-semibold clickable cursor-pointer rounded-lg ${listMode === 'posts' ? '' : 'text-muted-foreground'}`}
|
||||||
onClick={() => setDisplayReplies(false)}
|
onClick={() => setListMode('posts')}
|
||||||
>
|
>
|
||||||
{t('Notes')}
|
{t('Notes')}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`w-1/2 text-center py-2 font-semibold clickable cursor-pointer rounded-lg ${displayReplies ? '' : 'text-muted-foreground'}`}
|
className={`w-1/3 text-center py-2 font-semibold clickable cursor-pointer rounded-lg ${listMode === 'postsAndReplies' ? '' : 'text-muted-foreground'}`}
|
||||||
onClick={() => setDisplayReplies(true)}
|
onClick={() => setListMode('postsAndReplies')}
|
||||||
>
|
>
|
||||||
{t('Notes & Replies')}
|
{t('Notes & Replies')}
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
className={`w-1/3 text-center py-2 font-semibold clickable cursor-pointer rounded-lg ${listMode === 'pictures' ? '' : 'text-muted-foreground'}`}
|
||||||
|
onClick={() => setListMode('pictures')}
|
||||||
|
>
|
||||||
|
{t('Pictures')}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`w-1/2 px-4 sm:px-6 transition-transform duration-500 ${displayReplies ? 'translate-x-full' : ''}`}
|
className={`w-1/3 px-4 sm:px-6 transition-transform duration-500 ${listMode === 'postsAndReplies' ? 'translate-x-full' : listMode === 'pictures' ? 'translate-x-[200%]' : ''} `}
|
||||||
>
|
>
|
||||||
<div className="w-full h-1 bg-primary rounded-full" />
|
<div className="w-full h-1 bg-primary rounded-full" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { COMMENT_EVENT_KIND, PICTURE_EVENT_KIND } from '@/constants'
|
||||||
import { useFetchEvent } from '@/hooks'
|
import { useFetchEvent } from '@/hooks'
|
||||||
import { toNote } from '@/lib/link'
|
import { toNote } from '@/lib/link'
|
||||||
import { tagNameEquals } from '@/lib/tag'
|
import { tagNameEquals } from '@/lib/tag'
|
||||||
@@ -5,7 +6,7 @@ import { useSecondaryPage } from '@/PageManager'
|
|||||||
import { useNostr } from '@/providers/NostrProvider'
|
import { useNostr } from '@/providers/NostrProvider'
|
||||||
import client from '@/services/client.service'
|
import client from '@/services/client.service'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { Heart, MessageCircle, Repeat } from 'lucide-react'
|
import { Heart, MessageCircle, Repeat, ThumbsUp } from 'lucide-react'
|
||||||
import { Event, kinds, nip19, validateEvent } from 'nostr-tools'
|
import { Event, kinds, nip19, validateEvent } from 'nostr-tools'
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@@ -41,7 +42,7 @@ export default function NotificationList() {
|
|||||||
: relayList.read.concat(client.getDefaultRelayUrls()).slice(0, 4),
|
: relayList.read.concat(client.getDefaultRelayUrls()).slice(0, 4),
|
||||||
{
|
{
|
||||||
'#p': [pubkey],
|
'#p': [pubkey],
|
||||||
kinds: [kinds.ShortTextNote, kinds.Repost, kinds.Reaction],
|
kinds: [kinds.ShortTextNote, kinds.Repost, kinds.Reaction, COMMENT_EVENT_KIND],
|
||||||
limit: LIMIT
|
limit: LIMIT
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -147,6 +148,9 @@ function NotificationItem({ notification }: { notification: Event }) {
|
|||||||
if (notification.kind === kinds.Repost) {
|
if (notification.kind === kinds.Repost) {
|
||||||
return <RepostNotification notification={notification} />
|
return <RepostNotification notification={notification} />
|
||||||
}
|
}
|
||||||
|
if (notification.kind === COMMENT_EVENT_KIND) {
|
||||||
|
return <CommentNotification notification={notification} />
|
||||||
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,7 +166,9 @@ function ReactionNotification({ notification }: { notification: Event }) {
|
|||||||
: undefined
|
: undefined
|
||||||
}, [notification])
|
}, [notification])
|
||||||
const { event } = useFetchEvent(bech32Id)
|
const { event } = useFetchEvent(bech32Id)
|
||||||
if (!event || !bech32Id || event.kind !== kinds.ShortTextNote) return null
|
if (!event || !bech32Id || ![kinds.ShortTextNote, PICTURE_EVENT_KIND].includes(event.kind)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -172,6 +178,7 @@ function ReactionNotification({ notification }: { notification: Event }) {
|
|||||||
<div className="flex gap-2 items-center flex-1">
|
<div className="flex gap-2 items-center flex-1">
|
||||||
<UserAvatar userId={notification.pubkey} size="small" />
|
<UserAvatar userId={notification.pubkey} size="small" />
|
||||||
<Heart size={24} className="text-red-400" />
|
<Heart size={24} className="text-red-400" />
|
||||||
|
<div>{notification.content === '+' ? <ThumbsUp size={14} /> : notification.content}</div>
|
||||||
<ContentPreview event={event} />
|
<ContentPreview event={event} />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-muted-foreground">
|
<div className="text-muted-foreground">
|
||||||
@@ -228,8 +235,37 @@ function RepostNotification({ notification }: { notification: Event }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function CommentNotification({ notification }: { notification: Event }) {
|
||||||
|
const { push } = useSecondaryPage()
|
||||||
|
const rootEventId = notification.tags.find(tagNameEquals('E'))?.[1]
|
||||||
|
const rootPubkey = notification.tags.find(tagNameEquals('P'))?.[1]
|
||||||
|
const rootKind = notification.tags.find(tagNameEquals('K'))?.[1]
|
||||||
|
if (
|
||||||
|
!rootEventId ||
|
||||||
|
!rootPubkey ||
|
||||||
|
!rootKind ||
|
||||||
|
![kinds.ShortTextNote, PICTURE_EVENT_KIND].includes(parseInt(rootKind))
|
||||||
|
) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex gap-2 items-center cursor-pointer py-2"
|
||||||
|
onClick={() => push(toNote({ id: rootEventId, pubkey: rootPubkey }))}
|
||||||
|
>
|
||||||
|
<UserAvatar userId={notification.pubkey} size="small" />
|
||||||
|
<MessageCircle size={24} className="text-blue-400" />
|
||||||
|
<ContentPreview event={notification} />
|
||||||
|
<div className="text-muted-foreground">
|
||||||
|
<FormattedTimestamp timestamp={notification.created_at} short />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function ContentPreview({ event }: { event?: Event }) {
|
function ContentPreview({ event }: { event?: Event }) {
|
||||||
if (!event || event.kind !== kinds.ShortTextNote) return null
|
if (!event) return null
|
||||||
|
|
||||||
return <div className="truncate flex-1 w-0">{event.content}</div>
|
return <div className="truncate flex-1 w-0">{event.content}</div>
|
||||||
}
|
}
|
||||||
|
|||||||
48
src/components/PictureContent/index.tsx
Normal file
48
src/components/PictureContent/index.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { extractImetaUrlFromTag } from '@/lib/tag'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { Event } from 'nostr-tools'
|
||||||
|
import { memo, ReactNode } from 'react'
|
||||||
|
import {
|
||||||
|
embedded,
|
||||||
|
embeddedHashtagRenderer,
|
||||||
|
embeddedNormalUrlRenderer,
|
||||||
|
embeddedNostrNpubRenderer,
|
||||||
|
embeddedNostrProfileRenderer,
|
||||||
|
embeddedWebsocketUrlRenderer
|
||||||
|
} from '../Embedded'
|
||||||
|
import { ImageCarousel } from '../ImageCarousel'
|
||||||
|
import { isNsfwEvent } from '@/lib/event'
|
||||||
|
|
||||||
|
const PictureContent = memo(({ event, className }: { event: Event; className?: string }) => {
|
||||||
|
const images: string[] = []
|
||||||
|
event.tags.forEach((tag) => {
|
||||||
|
const imageUrl = extractImetaUrlFromTag(tag)
|
||||||
|
if (imageUrl) {
|
||||||
|
images.push(imageUrl)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const isNsfw = isNsfwEvent(event)
|
||||||
|
|
||||||
|
const nodes: ReactNode[] = [
|
||||||
|
<ImageCarousel key={`image-gallery-${event.id}`} images={images} isNsfw={isNsfw} />
|
||||||
|
]
|
||||||
|
nodes.push(
|
||||||
|
<div className="px-4">
|
||||||
|
{embedded(event.content, [
|
||||||
|
embeddedNormalUrlRenderer,
|
||||||
|
embeddedWebsocketUrlRenderer,
|
||||||
|
embeddedHashtagRenderer,
|
||||||
|
embeddedNostrNpubRenderer,
|
||||||
|
embeddedNostrProfileRenderer
|
||||||
|
])}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('text-wrap break-words whitespace-pre-wrap space-y-2', className)}>
|
||||||
|
{nodes}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
PictureContent.displayName = 'PictureContent'
|
||||||
|
export default PictureContent
|
||||||
53
src/components/PictureNote/index.tsx
Normal file
53
src/components/PictureNote/index.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { getUsingClient } from '@/lib/event'
|
||||||
|
import { Event } from 'nostr-tools'
|
||||||
|
import { useMemo } from 'react'
|
||||||
|
import { FormattedTimestamp } from '../FormattedTimestamp'
|
||||||
|
import NoteStats from '../NoteStats'
|
||||||
|
import UserAvatar from '../UserAvatar'
|
||||||
|
import Username from '../Username'
|
||||||
|
import PictureContent from '../PictureContent'
|
||||||
|
|
||||||
|
export default function PictureNote({
|
||||||
|
event,
|
||||||
|
className,
|
||||||
|
hideStats = false,
|
||||||
|
fetchNoteStats = false
|
||||||
|
}: {
|
||||||
|
event: Event
|
||||||
|
className?: string
|
||||||
|
hideStats?: boolean
|
||||||
|
fetchNoteStats?: boolean
|
||||||
|
}) {
|
||||||
|
const usingClient = useMemo(() => getUsingClient(event), [event])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<div className="px-4 flex items-center space-x-2">
|
||||||
|
<UserAvatar userId={event.pubkey} />
|
||||||
|
<div className="flex-1 w-0">
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<Username
|
||||||
|
userId={event.pubkey}
|
||||||
|
className="font-semibold flex"
|
||||||
|
skeletonClassName="h-4"
|
||||||
|
/>
|
||||||
|
{usingClient && (
|
||||||
|
<div className="text-xs text-muted-foreground truncate">using {usingClient}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground line-clamp-1">
|
||||||
|
<FormattedTimestamp timestamp={event.created_at} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<PictureContent className="mt-2" event={event} />
|
||||||
|
{!hideStats && (
|
||||||
|
<NoteStats
|
||||||
|
className="px-4 mt-3 sm:mt-4"
|
||||||
|
event={event}
|
||||||
|
fetchIfNotExisting={fetchNoteStats}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
31
src/components/PictureNoteCard/index.tsx
Normal file
31
src/components/PictureNoteCard/index.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { extractFirstPictureFromPictureEvent } from '@/lib/event'
|
||||||
|
import { toNote } from '@/lib/link'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { useSecondaryPage } from '@/PageManager'
|
||||||
|
import { Event } from 'nostr-tools'
|
||||||
|
import Image from '../Image'
|
||||||
|
import UserAvatar from '../UserAvatar'
|
||||||
|
import Username from '../Username'
|
||||||
|
|
||||||
|
export default function PictureNoteCard({
|
||||||
|
event,
|
||||||
|
className
|
||||||
|
}: {
|
||||||
|
event: Event
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
const { push } = useSecondaryPage()
|
||||||
|
const firstImage = extractFirstPictureFromPictureEvent(event)
|
||||||
|
if (!firstImage) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('space-y-1 cursor-pointer', className)} onClick={() => push(toNote(event))}>
|
||||||
|
<Image className="rounded-lg w-full aspect-[6/8]" src={firstImage} />
|
||||||
|
<div className="line-clamp-2 px-2">{event.content}</div>
|
||||||
|
<div className="flex items-center gap-2 px-2">
|
||||||
|
<UserAvatar userId={event.pubkey} size="xSmall" />
|
||||||
|
<Username userId={event.pubkey} className="text-sm text-muted-foreground truncate" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -4,16 +4,21 @@ import { Switch } from '@/components/ui/switch'
|
|||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
import { StorageKey } from '@/constants'
|
import { StorageKey } from '@/constants'
|
||||||
import { useToast } from '@/hooks/use-toast'
|
import { useToast } from '@/hooks/use-toast'
|
||||||
import { createShortTextNoteDraftEvent } from '@/lib/draft-event'
|
import {
|
||||||
|
createCommentDraftEvent,
|
||||||
|
createPictureNoteDraftEvent,
|
||||||
|
createShortTextNoteDraftEvent
|
||||||
|
} from '@/lib/draft-event'
|
||||||
import { useNostr } from '@/providers/NostrProvider'
|
import { useNostr } from '@/providers/NostrProvider'
|
||||||
import client from '@/services/client.service'
|
import client from '@/services/client.service'
|
||||||
import { ChevronDown, LoaderCircle } from 'lucide-react'
|
import { ChevronDown, LoaderCircle } from 'lucide-react'
|
||||||
import { Event } from 'nostr-tools'
|
import { Event, kinds } from 'nostr-tools'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import Mentions from './Mentions'
|
import Mentions from './Mentions'
|
||||||
import Preview from './Preview'
|
import Preview from './Preview'
|
||||||
import Uploader from './Uploader'
|
import Uploader from './Uploader'
|
||||||
|
import { extractImagesFromContent } from '@/lib/event'
|
||||||
|
|
||||||
export default function PostContent({
|
export default function PostContent({
|
||||||
defaultContent = '',
|
defaultContent = '',
|
||||||
@@ -31,12 +36,19 @@ export default function PostContent({
|
|||||||
const [posting, setPosting] = useState(false)
|
const [posting, setPosting] = useState(false)
|
||||||
const [showMoreOptions, setShowMoreOptions] = useState(false)
|
const [showMoreOptions, setShowMoreOptions] = useState(false)
|
||||||
const [addClientTag, setAddClientTag] = useState(false)
|
const [addClientTag, setAddClientTag] = useState(false)
|
||||||
|
const [isPictureNote, setIsPictureNote] = useState(false)
|
||||||
|
const [hasImages, setHasImages] = useState(false)
|
||||||
const canPost = !!content && !posting
|
const canPost = !!content && !posting
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setAddClientTag(window.localStorage.getItem(StorageKey.ADD_CLIENT_TAG) === 'true')
|
setAddClientTag(window.localStorage.getItem(StorageKey.ADD_CLIENT_TAG) === 'true')
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const { images } = extractImagesFromContent(content)
|
||||||
|
setHasImages(!!images && images.length > 0)
|
||||||
|
}, [content])
|
||||||
|
|
||||||
const handleTextareaChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
const handleTextareaChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
setContent(e.target.value)
|
setContent(e.target.value)
|
||||||
}
|
}
|
||||||
@@ -56,7 +68,15 @@ export default function PostContent({
|
|||||||
const relayList = await client.fetchRelayList(parentEvent.pubkey)
|
const relayList = await client.fetchRelayList(parentEvent.pubkey)
|
||||||
additionalRelayUrls.push(...relayList.read.slice(0, 5))
|
additionalRelayUrls.push(...relayList.read.slice(0, 5))
|
||||||
}
|
}
|
||||||
const draftEvent = await createShortTextNoteDraftEvent(content, {
|
if (isPictureNote && !hasImages) {
|
||||||
|
throw new Error(t('Picture note requires images'))
|
||||||
|
}
|
||||||
|
const draftEvent =
|
||||||
|
isPictureNote && !parentEvent && hasImages
|
||||||
|
? await createPictureNoteDraftEvent(content, { addClientTag })
|
||||||
|
: parentEvent && parentEvent.kind !== kinds.ShortTextNote
|
||||||
|
? await createCommentDraftEvent(content, parentEvent, { addClientTag })
|
||||||
|
: await createShortTextNoteDraftEvent(content, {
|
||||||
parentEvent,
|
parentEvent,
|
||||||
addClientTag
|
addClientTag
|
||||||
})
|
})
|
||||||
@@ -151,6 +171,21 @@ export default function PostContent({
|
|||||||
<div className="text-muted-foreground text-xs">
|
<div className="text-muted-foreground text-xs">
|
||||||
{t('Show others this was sent via Jumble')}
|
{t('Show others this was sent via Jumble')}
|
||||||
</div>
|
</div>
|
||||||
|
{!parentEvent && (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Label htmlFor="picture-note">{t('Picture note')}</Label>
|
||||||
|
<Switch
|
||||||
|
id="picture-note"
|
||||||
|
checked={isPictureNote}
|
||||||
|
onCheckedChange={setIsPictureNote}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="text-muted-foreground text-xs">
|
||||||
|
{t('A special note for picture-first clients like Olas')}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex gap-2 items-center justify-around sm:hidden">
|
<div className="flex gap-2 items-center justify-around sm:hidden">
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Image } from '@nextui-org/image'
|
|
||||||
import { generateImageByPubkey } from '@/lib/pubkey'
|
import { generateImageByPubkey } from '@/lib/pubkey'
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
|
import Image from '../Image'
|
||||||
|
|
||||||
export default function ProfileBanner({
|
export default function ProfileBanner({
|
||||||
pubkey,
|
pubkey,
|
||||||
@@ -27,9 +26,8 @@ export default function ProfileBanner({
|
|||||||
<Image
|
<Image
|
||||||
src={bannerUrl}
|
src={bannerUrl}
|
||||||
alt={`${pubkey} banner`}
|
alt={`${pubkey} banner`}
|
||||||
className={cn('z-0', className)}
|
className={className}
|
||||||
onError={() => setBannerUrl(defaultBanner)}
|
onError={() => setBannerUrl(defaultBanner)}
|
||||||
removeWrapper
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
36
src/components/SimpleMasonryGrid/index.tsx
Normal file
36
src/components/SimpleMasonryGrid/index.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { useMemo, ReactNode } from 'react'
|
||||||
|
|
||||||
|
export default function SimpleMasonryGrid({
|
||||||
|
items,
|
||||||
|
columnCount,
|
||||||
|
className
|
||||||
|
}: {
|
||||||
|
items: ReactNode[]
|
||||||
|
columnCount: 2 | 3
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
const columns = useMemo(() => {
|
||||||
|
const newColumns: ReactNode[][] = Array.from({ length: columnCount }, () => [])
|
||||||
|
items.forEach((item, i) => {
|
||||||
|
newColumns[i % columnCount].push(item)
|
||||||
|
})
|
||||||
|
return newColumns
|
||||||
|
}, [items])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'grid',
|
||||||
|
columnCount === 2 ? 'grid-cols-2 gap-2' : 'grid-cols-3 gap-4',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{columns.map((column, i) => (
|
||||||
|
<div key={i} className={columnCount === 2 ? 'space-y-2' : 'space-y-4'}>
|
||||||
|
{column}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ const UserAvatarSizeCnMap = {
|
|||||||
big: 'w-16 h-16',
|
big: 'w-16 h-16',
|
||||||
normal: 'w-10 h-10',
|
normal: 'w-10 h-10',
|
||||||
small: 'w-7 h-7',
|
small: 'w-7 h-7',
|
||||||
|
xSmall: 'w-5 h-5',
|
||||||
tiny: 'w-4 h-4'
|
tiny: 'w-4 h-4'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,7 +25,7 @@ export default function UserAvatar({
|
|||||||
}: {
|
}: {
|
||||||
userId: string
|
userId: string
|
||||||
className?: string
|
className?: string
|
||||||
size?: 'large' | 'big' | 'normal' | 'small' | 'tiny'
|
size?: 'large' | 'big' | 'normal' | 'small' | 'xSmall' | 'tiny'
|
||||||
}) {
|
}) {
|
||||||
const { profile } = useFetchProfile(userId)
|
const { profile } = useFetchProfile(userId)
|
||||||
const defaultAvatar = useMemo(
|
const defaultAvatar = useMemo(
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useFetchWebMetadata } from '@/hooks/useFetchWebMetadata'
|
import { useFetchWebMetadata } from '@/hooks/useFetchWebMetadata'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
||||||
import { Image } from '@nextui-org/image'
|
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
|
import Image from '../Image'
|
||||||
|
|
||||||
export default function WebPreview({
|
export default function WebPreview({
|
||||||
url,
|
url,
|
||||||
@@ -30,7 +30,7 @@ export default function WebPreview({
|
|||||||
if (isSmallScreen && image) {
|
if (isSmallScreen && image) {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg border mt-2">
|
<div className="rounded-lg border mt-2">
|
||||||
<Image src={image} className="rounded-t-lg object-cover w-full h-44" removeWrapper />
|
<Image src={image} className="rounded-t-lg w-full h-44" />
|
||||||
<div className="bg-muted p-2 w-full rounded-b-lg">
|
<div className="bg-muted p-2 w-full rounded-b-lg">
|
||||||
<div className="text-xs text-muted-foreground">{hostname}</div>
|
<div className="text-xs text-muted-foreground">{hostname}</div>
|
||||||
<div className="font-semibold line-clamp-1">{title}</div>
|
<div className="font-semibold line-clamp-1">{title}</div>
|
||||||
@@ -48,11 +48,7 @@ export default function WebPreview({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{image && (
|
{image && (
|
||||||
<Image
|
<Image src={image} className={`rounded-l-lg ${size === 'normal' ? 'h-44' : 'h-24'}`} />
|
||||||
src={image}
|
|
||||||
className={`rounded-l-lg object-cover ${size === 'normal' ? 'h-44' : 'h-24'}`}
|
|
||||||
removeWrapper
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
<div className="flex-1 w-0 p-2">
|
<div className="flex-1 w-0 p-2">
|
||||||
<div className="text-xs text-muted-foreground">{hostname}</div>
|
<div className="text-xs text-muted-foreground">{hostname}</div>
|
||||||
|
|||||||
260
src/components/ui/carousel.tsx
Normal file
260
src/components/ui/carousel.tsx
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import useEmblaCarousel, {
|
||||||
|
type UseEmblaCarouselType,
|
||||||
|
} from "embla-carousel-react"
|
||||||
|
import { ArrowLeft, ArrowRight } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
|
||||||
|
type CarouselApi = UseEmblaCarouselType[1]
|
||||||
|
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
|
||||||
|
type CarouselOptions = UseCarouselParameters[0]
|
||||||
|
type CarouselPlugin = UseCarouselParameters[1]
|
||||||
|
|
||||||
|
type CarouselProps = {
|
||||||
|
opts?: CarouselOptions
|
||||||
|
plugins?: CarouselPlugin
|
||||||
|
orientation?: "horizontal" | "vertical"
|
||||||
|
setApi?: (api: CarouselApi) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
type CarouselContextProps = {
|
||||||
|
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
|
||||||
|
api: ReturnType<typeof useEmblaCarousel>[1]
|
||||||
|
scrollPrev: () => void
|
||||||
|
scrollNext: () => void
|
||||||
|
canScrollPrev: boolean
|
||||||
|
canScrollNext: boolean
|
||||||
|
} & CarouselProps
|
||||||
|
|
||||||
|
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
|
||||||
|
|
||||||
|
function useCarousel() {
|
||||||
|
const context = React.useContext(CarouselContext)
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useCarousel must be used within a <Carousel />")
|
||||||
|
}
|
||||||
|
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
const Carousel = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement> & CarouselProps
|
||||||
|
>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
orientation = "horizontal",
|
||||||
|
opts,
|
||||||
|
setApi,
|
||||||
|
plugins,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
const [carouselRef, api] = useEmblaCarousel(
|
||||||
|
{
|
||||||
|
...opts,
|
||||||
|
axis: orientation === "horizontal" ? "x" : "y",
|
||||||
|
},
|
||||||
|
plugins
|
||||||
|
)
|
||||||
|
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
|
||||||
|
const [canScrollNext, setCanScrollNext] = React.useState(false)
|
||||||
|
|
||||||
|
const onSelect = React.useCallback((api: CarouselApi) => {
|
||||||
|
if (!api) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setCanScrollPrev(api.canScrollPrev())
|
||||||
|
setCanScrollNext(api.canScrollNext())
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const scrollPrev = React.useCallback(() => {
|
||||||
|
api?.scrollPrev()
|
||||||
|
}, [api])
|
||||||
|
|
||||||
|
const scrollNext = React.useCallback(() => {
|
||||||
|
api?.scrollNext()
|
||||||
|
}, [api])
|
||||||
|
|
||||||
|
const handleKeyDown = React.useCallback(
|
||||||
|
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
if (event.key === "ArrowLeft") {
|
||||||
|
event.preventDefault()
|
||||||
|
scrollPrev()
|
||||||
|
} else if (event.key === "ArrowRight") {
|
||||||
|
event.preventDefault()
|
||||||
|
scrollNext()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[scrollPrev, scrollNext]
|
||||||
|
)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!api || !setApi) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setApi(api)
|
||||||
|
}, [api, setApi])
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!api) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelect(api)
|
||||||
|
api.on("reInit", onSelect)
|
||||||
|
api.on("select", onSelect)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
api?.off("select", onSelect)
|
||||||
|
}
|
||||||
|
}, [api, onSelect])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CarouselContext.Provider
|
||||||
|
value={{
|
||||||
|
carouselRef,
|
||||||
|
api: api,
|
||||||
|
opts,
|
||||||
|
orientation:
|
||||||
|
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
|
||||||
|
scrollPrev,
|
||||||
|
scrollNext,
|
||||||
|
canScrollPrev,
|
||||||
|
canScrollNext,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
onKeyDownCapture={handleKeyDown}
|
||||||
|
className={cn("relative", className)}
|
||||||
|
role="region"
|
||||||
|
aria-roledescription="carousel"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</CarouselContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Carousel.displayName = "Carousel"
|
||||||
|
|
||||||
|
const CarouselContent = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
const { carouselRef, orientation } = useCarousel()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={carouselRef} className="overflow-hidden">
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex",
|
||||||
|
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
CarouselContent.displayName = "CarouselContent"
|
||||||
|
|
||||||
|
const CarouselItem = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
const { orientation } = useCarousel()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
role="group"
|
||||||
|
aria-roledescription="slide"
|
||||||
|
className={cn(
|
||||||
|
"min-w-0 shrink-0 grow-0 basis-full",
|
||||||
|
orientation === "horizontal" ? "pl-4" : "pt-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
CarouselItem.displayName = "CarouselItem"
|
||||||
|
|
||||||
|
const CarouselPrevious = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
React.ComponentProps<typeof Button>
|
||||||
|
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
||||||
|
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
ref={ref}
|
||||||
|
variant={variant}
|
||||||
|
size={size}
|
||||||
|
className={cn(
|
||||||
|
"absolute h-8 w-8 rounded-full",
|
||||||
|
orientation === "horizontal"
|
||||||
|
? "-left-12 top-1/2 -translate-y-1/2"
|
||||||
|
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
disabled={!canScrollPrev}
|
||||||
|
onClick={scrollPrev}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ArrowLeft className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Previous slide</span>
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
CarouselPrevious.displayName = "CarouselPrevious"
|
||||||
|
|
||||||
|
const CarouselNext = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
React.ComponentProps<typeof Button>
|
||||||
|
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
||||||
|
const { orientation, scrollNext, canScrollNext } = useCarousel()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
ref={ref}
|
||||||
|
variant={variant}
|
||||||
|
size={size}
|
||||||
|
className={cn(
|
||||||
|
"absolute h-8 w-8 rounded-full",
|
||||||
|
orientation === "horizontal"
|
||||||
|
? "-right-12 top-1/2 -translate-y-1/2"
|
||||||
|
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
disabled={!canScrollNext}
|
||||||
|
onClick={scrollNext}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ArrowRight className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Next slide</span>
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
CarouselNext.displayName = "CarouselNext"
|
||||||
|
|
||||||
|
export {
|
||||||
|
type CarouselApi,
|
||||||
|
Carousel,
|
||||||
|
CarouselContent,
|
||||||
|
CarouselItem,
|
||||||
|
CarouselPrevious,
|
||||||
|
CarouselNext,
|
||||||
|
}
|
||||||
@@ -14,3 +14,8 @@ export const BIG_RELAY_URLS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
export const SEARCHABLE_RELAY_URLS = ['wss://relay.nostr.band/', 'wss://search.nos.today/']
|
export const SEARCHABLE_RELAY_URLS = ['wss://relay.nostr.band/', 'wss://search.nos.today/']
|
||||||
|
|
||||||
|
export const PICTURE_EVENT_KIND = 20
|
||||||
|
export const COMMENT_EVENT_KIND = 1111
|
||||||
|
|
||||||
|
export const URL_REGEX = /(https?:\/\/[^\s"']+)/g
|
||||||
|
|||||||
@@ -99,6 +99,11 @@ export default {
|
|||||||
Dark: 'Dark',
|
Dark: 'Dark',
|
||||||
Temporary: 'Temporary',
|
Temporary: 'Temporary',
|
||||||
'Choose a relay collection': 'Choose a relay collection',
|
'Choose a relay collection': 'Choose a relay collection',
|
||||||
'Switch account': 'Switch account'
|
'Switch account': 'Switch account',
|
||||||
|
Pictures: 'Pictures',
|
||||||
|
'Picture note': 'Picture note',
|
||||||
|
'A special note for picture-first clients like Olas':
|
||||||
|
'A special note for picture-first clients like Olas',
|
||||||
|
'Picture note requires images': 'Picture note requires images'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,6 +98,11 @@ export default {
|
|||||||
Dark: '深色',
|
Dark: '深色',
|
||||||
Temporary: '临时',
|
Temporary: '临时',
|
||||||
'Choose a relay collection': '选择一个服务器组',
|
'Choose a relay collection': '选择一个服务器组',
|
||||||
'Switch account': '切换账户'
|
'Switch account': '切换账户',
|
||||||
|
Pictures: '图片',
|
||||||
|
'Picture note': '图片笔记',
|
||||||
|
'A special note for picture-first clients like Olas':
|
||||||
|
'一种可以在图片优先客户端 (如 Olas) 中显示的特殊笔记',
|
||||||
|
'Picture note requires images': '图片笔记需要有图片'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
|
import { COMMENT_EVENT_KIND, PICTURE_EVENT_KIND } from '@/constants'
|
||||||
import { TDraftEvent } from '@/types'
|
import { TDraftEvent } from '@/types'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { Event, kinds } from 'nostr-tools'
|
import { Event, kinds } from 'nostr-tools'
|
||||||
import { extractHashtags, extractMentions, getEventCoordinate, isReplaceable } from './event'
|
import {
|
||||||
|
extractCommentMentions,
|
||||||
|
extractHashtags,
|
||||||
|
extractImagesFromContent,
|
||||||
|
extractMentions,
|
||||||
|
getEventCoordinate,
|
||||||
|
isReplaceable
|
||||||
|
} from './event'
|
||||||
|
|
||||||
// https://github.com/nostr-protocol/nips/blob/master/25.md
|
// https://github.com/nostr-protocol/nips/blob/master/25.md
|
||||||
export function createReactionDraftEvent(event: Event): TDraftEvent {
|
export function createReactionDraftEvent(event: Event): TDraftEvent {
|
||||||
@@ -73,3 +81,79 @@ export async function createShortTextNoteDraftEvent(
|
|||||||
created_at: dayjs().unix()
|
created_at: dayjs().unix()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function createPictureNoteDraftEvent(
|
||||||
|
content: string,
|
||||||
|
options: {
|
||||||
|
addClientTag?: boolean
|
||||||
|
} = {}
|
||||||
|
): Promise<TDraftEvent> {
|
||||||
|
const { pubkeys, quoteEventIds } = await extractMentions(content)
|
||||||
|
const hashtags = extractHashtags(content)
|
||||||
|
const { images, contentWithoutImages } = extractImagesFromContent(content)
|
||||||
|
if (!images || !images.length) {
|
||||||
|
throw new Error('No images found in content')
|
||||||
|
}
|
||||||
|
|
||||||
|
const tags = images
|
||||||
|
.map((image) => ['imeta', `url ${image}`])
|
||||||
|
.concat(pubkeys.map((pubkey) => ['p', pubkey]))
|
||||||
|
.concat(quoteEventIds.map((eventId) => ['q', eventId]))
|
||||||
|
.concat(hashtags.map((hashtag) => ['t', hashtag]))
|
||||||
|
|
||||||
|
if (options.addClientTag) {
|
||||||
|
tags.push(['client', 'jumble'])
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
kind: PICTURE_EVENT_KIND,
|
||||||
|
content: contentWithoutImages,
|
||||||
|
tags,
|
||||||
|
created_at: dayjs().unix()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createCommentDraftEvent(
|
||||||
|
content: string,
|
||||||
|
parentEvent: Event,
|
||||||
|
options: {
|
||||||
|
addClientTag?: boolean
|
||||||
|
} = {}
|
||||||
|
): Promise<TDraftEvent> {
|
||||||
|
const {
|
||||||
|
pubkeys,
|
||||||
|
quoteEventIds,
|
||||||
|
rootEventId,
|
||||||
|
rootEventKind,
|
||||||
|
rootEventPubkey,
|
||||||
|
parentEventId,
|
||||||
|
parentEventKind,
|
||||||
|
parentEventPubkey
|
||||||
|
} = await extractCommentMentions(content, parentEvent)
|
||||||
|
const hashtags = extractHashtags(content)
|
||||||
|
|
||||||
|
const tags = [
|
||||||
|
['E', rootEventId],
|
||||||
|
['K', rootEventKind.toString()],
|
||||||
|
['P', rootEventPubkey],
|
||||||
|
['e', parentEventId],
|
||||||
|
['k', parentEventKind.toString()],
|
||||||
|
['p', parentEventPubkey]
|
||||||
|
].concat(
|
||||||
|
pubkeys
|
||||||
|
.map((pubkey) => ['p', pubkey])
|
||||||
|
.concat(quoteEventIds.map((eventId) => ['q', eventId]))
|
||||||
|
.concat(hashtags.map((hashtag) => ['t', hashtag]))
|
||||||
|
)
|
||||||
|
|
||||||
|
if (options.addClientTag) {
|
||||||
|
tags.push(['client', 'jumble'])
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
kind: COMMENT_EVENT_KIND,
|
||||||
|
content,
|
||||||
|
tags,
|
||||||
|
created_at: dayjs().unix()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import { COMMENT_EVENT_KIND, PICTURE_EVENT_KIND } from '@/constants'
|
||||||
import client from '@/services/client.service'
|
import client from '@/services/client.service'
|
||||||
import { Event, kinds, nip19 } from 'nostr-tools'
|
import { Event, kinds, nip19 } from 'nostr-tools'
|
||||||
import { isReplyETag, isRootETag, tagNameEquals } from './tag'
|
import { extractImetaUrlFromTag, isReplyETag, isRootETag, tagNameEquals } from './tag'
|
||||||
|
|
||||||
export function isNsfwEvent(event: Event) {
|
export function isNsfwEvent(event: Event) {
|
||||||
return event.tags.some(
|
return event.tags.some(
|
||||||
@@ -26,6 +27,14 @@ export function isReplyNoteEvent(event: Event) {
|
|||||||
return hasETag && !hasMarker
|
return hasETag && !hasMarker
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isCommentEvent(event: Event) {
|
||||||
|
return event.kind === COMMENT_EVENT_KIND
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isPictureEvent(event: Event) {
|
||||||
|
return event.kind === PICTURE_EVENT_KIND
|
||||||
|
}
|
||||||
|
|
||||||
export function getParentEventId(event?: Event) {
|
export function getParentEventId(event?: Event) {
|
||||||
return event?.tags.find(isReplyETag)?.[1]
|
return event?.tags.find(isReplyETag)?.[1]
|
||||||
}
|
}
|
||||||
@@ -116,6 +125,54 @@ export async function extractMentions(content: string, parentEvent?: Event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function extractCommentMentions(content: string, parentEvent: Event) {
|
||||||
|
const pubkeySet = new Set<string>()
|
||||||
|
const quoteEventIdSet = new Set<string>()
|
||||||
|
const rootEventId = parentEvent.tags.find(tagNameEquals('E'))?.[1] ?? parentEvent.id
|
||||||
|
const rootEventKind = parentEvent.tags.find(tagNameEquals('K'))?.[1] ?? parentEvent.kind
|
||||||
|
const rootEventPubkey = parentEvent.tags.find(tagNameEquals('P'))?.[1] ?? parentEvent.pubkey
|
||||||
|
const parentEventId = parentEvent.id
|
||||||
|
const parentEventKind = parentEvent.kind
|
||||||
|
const parentEventPubkey = parentEvent.pubkey
|
||||||
|
|
||||||
|
const matches = content.match(
|
||||||
|
/nostr:(npub1[a-z0-9]{58}|nprofile1[a-z0-9]+|note1[a-z0-9]{58}|nevent1[a-z0-9]+)/g
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const m of matches || []) {
|
||||||
|
try {
|
||||||
|
const id = m.split(':')[1]
|
||||||
|
const { type, data } = nip19.decode(id)
|
||||||
|
if (type === 'nprofile') {
|
||||||
|
pubkeySet.add(data.pubkey)
|
||||||
|
} else if (type === 'npub') {
|
||||||
|
pubkeySet.add(data)
|
||||||
|
} else if (['nevent', 'note', 'naddr'].includes(type)) {
|
||||||
|
const event = await client.fetchEvent(id)
|
||||||
|
if (event) {
|
||||||
|
pubkeySet.add(event.pubkey)
|
||||||
|
quoteEventIdSet.add(event.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pubkeySet.add(parentEvent.pubkey)
|
||||||
|
|
||||||
|
return {
|
||||||
|
pubkeys: Array.from(pubkeySet),
|
||||||
|
quoteEventIds: Array.from(quoteEventIdSet),
|
||||||
|
rootEventId,
|
||||||
|
rootEventKind,
|
||||||
|
rootEventPubkey,
|
||||||
|
parentEventId,
|
||||||
|
parentEventKind,
|
||||||
|
parentEventPubkey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function extractHashtags(content: string) {
|
export function extractHashtags(content: string) {
|
||||||
const hashtags: string[] = []
|
const hashtags: string[] = []
|
||||||
const matches = content.match(/#[\p{L}\p{N}\p{M}]+/gu)
|
const matches = content.match(/#[\p{L}\p{N}\p{M}]+/gu)
|
||||||
@@ -127,3 +184,22 @@ export function extractHashtags(content: string) {
|
|||||||
})
|
})
|
||||||
return hashtags
|
return hashtags
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function extractFirstPictureFromPictureEvent(event: Event) {
|
||||||
|
if (!isPictureEvent(event)) return null
|
||||||
|
for (const tag of event.tags) {
|
||||||
|
const url = extractImetaUrlFromTag(tag)
|
||||||
|
if (url) return url
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractImagesFromContent(content: string) {
|
||||||
|
const images = content.match(/https?:\/\/[^\s"']+\.(jpg|jpeg|png|gif|webp|heic)/gi)
|
||||||
|
let contentWithoutImages = content
|
||||||
|
images?.forEach((url) => {
|
||||||
|
contentWithoutImages = contentWithoutImages.replace(url, '').trim()
|
||||||
|
})
|
||||||
|
contentWithoutImages = contentWithoutImages.replace(/\n{3,}/g, '\n\n').trim()
|
||||||
|
return { images, contentWithoutImages }
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Event, nip19 } from 'nostr-tools'
|
import { Event, nip19 } from 'nostr-tools'
|
||||||
|
|
||||||
export const toHome = () => '/'
|
export const toHome = () => '/'
|
||||||
export const toNote = (eventOrId: Event | string) => {
|
export const toNote = (eventOrId: Pick<Event, 'id' | 'pubkey'> | string) => {
|
||||||
if (typeof eventOrId === 'string') return `/notes/${eventOrId}`
|
if (typeof eventOrId === 'string') return `/notes/${eventOrId}`
|
||||||
const nevent = nip19.neventEncode({ id: eventOrId.id, author: eventOrId.pubkey })
|
const nevent = nip19.neventEncode({ id: eventOrId.id, author: eventOrId.pubkey })
|
||||||
return `/notes/${nevent}`
|
return `/notes/${nevent}`
|
||||||
|
|||||||
@@ -13,3 +13,10 @@ export function isRootETag([tagName, , , marker]: string[]) {
|
|||||||
export function isMentionETag([tagName, , , marker]: string[]) {
|
export function isMentionETag([tagName, , , marker]: string[]) {
|
||||||
return tagName === 'e' && marker === 'mention'
|
return tagName === 'e' && marker === 'mention'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function extractImetaUrlFromTag(tag: string[]) {
|
||||||
|
if (tag[0] !== 'imeta') return null
|
||||||
|
const urlItem = tag.find((item) => item.startsWith('url '))
|
||||||
|
const url = urlItem?.slice(4)
|
||||||
|
return url || null
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,3 +18,21 @@ export function normalizeUrl(url: string): string {
|
|||||||
export function simplifyUrl(url: string): string {
|
export function simplifyUrl(url: string): string {
|
||||||
return url.replace('wss://', '').replace('ws://', '').replace(/\/$/, '')
|
return url.replace('wss://', '').replace('ws://', '').replace(/\/$/, '')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isImage(url: string) {
|
||||||
|
try {
|
||||||
|
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.heic', '.svg']
|
||||||
|
return imageExtensions.some((ext) => new URL(url).pathname.toLowerCase().endsWith(ext))
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isVideo(url: string) {
|
||||||
|
try {
|
||||||
|
const videoExtensions = ['.mp4', '.webm', '.ogg', '.mov']
|
||||||
|
return videoExtensions.some((ext) => new URL(url).pathname.toLowerCase().endsWith(ext))
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { useSecondaryPage } from '@/PageManager'
|
import { useSecondaryPage } from '@/PageManager'
|
||||||
|
import Nip22ReplyNoteList from '@/components/Nip22ReplyNoteList'
|
||||||
import Note from '@/components/Note'
|
import Note from '@/components/Note'
|
||||||
|
import PictureNote from '@/components/PictureNote'
|
||||||
import ReplyNoteList from '@/components/ReplyNoteList'
|
import ReplyNoteList from '@/components/ReplyNoteList'
|
||||||
import UserAvatar from '@/components/UserAvatar'
|
import UserAvatar from '@/components/UserAvatar'
|
||||||
import Username from '@/components/Username'
|
import Username from '@/components/Username'
|
||||||
@@ -8,14 +10,16 @@ import { Separator } from '@/components/ui/separator'
|
|||||||
import { Skeleton } from '@/components/ui/skeleton'
|
import { Skeleton } from '@/components/ui/skeleton'
|
||||||
import { useFetchEvent } from '@/hooks'
|
import { useFetchEvent } from '@/hooks'
|
||||||
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
|
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
|
||||||
import { getParentEventId, getRootEventId } from '@/lib/event'
|
import { getParentEventId, getRootEventId, isPictureEvent } from '@/lib/event'
|
||||||
import { toNote } from '@/lib/link'
|
import { toNote } from '@/lib/link'
|
||||||
|
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import NotFoundPage from '../NotFoundPage'
|
import NotFoundPage from '../NotFoundPage'
|
||||||
|
|
||||||
export default function NotePage({ id, index }: { id?: string; index?: number }) {
|
export default function NotePage({ id, index }: { id?: string; index?: number }) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const { isSmallScreen } = useScreenSize()
|
||||||
const { event, isFetching } = useFetchEvent(id)
|
const { event, isFetching } = useFetchEvent(id)
|
||||||
const parentEventId = useMemo(() => getParentEventId(event), [event])
|
const parentEventId = useMemo(() => getParentEventId(event), [event])
|
||||||
const rootEventId = useMemo(() => getRootEventId(event), [event])
|
const rootEventId = useMemo(() => getRootEventId(event), [event])
|
||||||
@@ -31,6 +35,20 @@ export default function NotePage({ id, index }: { id?: string; index?: number })
|
|||||||
}
|
}
|
||||||
if (!event) return <NotFoundPage />
|
if (!event) return <NotFoundPage />
|
||||||
|
|
||||||
|
if (isPictureEvent(event) && isSmallScreen) {
|
||||||
|
return (
|
||||||
|
<SecondaryPageLayout index={index} titlebarContent={t('Note')} displayScrollToTopButton>
|
||||||
|
<PictureNote key={`note-${event.id}`} event={event} fetchNoteStats />
|
||||||
|
<Separator className="mb-2 mt-4" />
|
||||||
|
<Nip22ReplyNoteList
|
||||||
|
key={`nip22-reply-note-list-${event.id}`}
|
||||||
|
event={event}
|
||||||
|
className="px-2"
|
||||||
|
/>
|
||||||
|
</SecondaryPageLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SecondaryPageLayout index={index} titlebarContent={t('Note')} displayScrollToTopButton>
|
<SecondaryPageLayout index={index} titlebarContent={t('Note')} displayScrollToTopButton>
|
||||||
<div className="px-4">
|
<div className="px-4">
|
||||||
@@ -39,7 +57,15 @@ export default function NotePage({ id, index }: { id?: string; index?: number })
|
|||||||
<Note key={`note-${event.id}`} event={event} fetchNoteStats />
|
<Note key={`note-${event.id}`} event={event} fetchNoteStats />
|
||||||
</div>
|
</div>
|
||||||
<Separator className="mb-2 mt-4" />
|
<Separator className="mb-2 mt-4" />
|
||||||
|
{isPictureEvent(event) ? (
|
||||||
|
<Nip22ReplyNoteList
|
||||||
|
key={`nip22-reply-note-list-${event.id}`}
|
||||||
|
event={event}
|
||||||
|
className="px-2"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<ReplyNoteList key={`reply-note-list-${event.id}`} event={event} className="px-2" />
|
<ReplyNoteList key={`reply-note-list-${event.id}`} event={event} className="px-2" />
|
||||||
|
)}
|
||||||
</SecondaryPageLayout>
|
</SecondaryPageLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export default function ProfilePage({ id, index }: { id?: string; index?: number
|
|||||||
<ProfileBanner
|
<ProfileBanner
|
||||||
banner={banner}
|
banner={banner}
|
||||||
pubkey={pubkey}
|
pubkey={pubkey}
|
||||||
className="w-full h-full object-cover rounded-lg"
|
className="w-full aspect-video object-cover rounded-lg"
|
||||||
/>
|
/>
|
||||||
<Avatar className="w-24 h-24 absolute bottom-0 left-4 translate-y-1/2 border-4 border-background">
|
<Avatar className="w-24 h-24 absolute bottom-0 left-4 translate-y-1/2 border-4 border-background">
|
||||||
<AvatarImage src={avatar} className="object-cover object-center" />
|
<AvatarImage src={avatar} className="object-cover object-center" />
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ export default {
|
|||||||
content: [
|
content: [
|
||||||
'./index.html',
|
'./index.html',
|
||||||
'./src/**/*.{ts,tsx}',
|
'./src/**/*.{ts,tsx}',
|
||||||
'./node_modules/@nextui-org/theme/dist/components/image.js'
|
|
||||||
],
|
],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
|
|||||||
Reference in New Issue
Block a user