wip
This commit is contained in:
18
.eslintrc
18
.eslintrc
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"parser": "@typescript-eslint/parser",
|
|
||||||
"plugins": ["@typescript-eslint", "react-refresh"],
|
|
||||||
"extends": [
|
|
||||||
"plugin:react/recommended",
|
|
||||||
"plugin:react/jsx-runtime",
|
|
||||||
"plugin:react-hooks/recommended",
|
|
||||||
"plugin:@typescript-eslint/recommended",
|
|
||||||
"prettier"
|
|
||||||
],
|
|
||||||
"rules": {
|
|
||||||
"@typescript-eslint/no-unused-vars": "error",
|
|
||||||
"@typescript-eslint/no-explicit-any": "warn",
|
|
||||||
"react-refresh/only-export-components": "error",
|
|
||||||
"react/no-unknown-property": ["error", { "ignore": ["fetchpriority"] }]
|
|
||||||
},
|
|
||||||
"ignorePatterns": ["dist", "**/*.js", "**/*.json", "node_modules"]
|
|
||||||
}
|
|
||||||
7
global.d.ts
vendored
7
global.d.ts
vendored
@@ -1,7 +0,0 @@
|
|||||||
import { AriaAttributes, DOMAttributes } from 'react';
|
|
||||||
|
|
||||||
declare module 'react' {
|
|
||||||
interface HTMLAttributes<T> extends AriaAttributes, DOMAttributes<T> {
|
|
||||||
fetchpriority?: 'high' | 'low' | 'auto';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
11
index.html
Normal file
11
index.html
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<html lang="en" class="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Lume</title>
|
||||||
|
</head>
|
||||||
|
<body class="cursor-default select-none overflow-hidden font-sans antialiased h-screen w-screen dark:bg-black dark:text-zinc-100">
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
10
package.json
10
package.json
@@ -16,11 +16,12 @@
|
|||||||
"@floating-ui/react": "^0.23.1",
|
"@floating-ui/react": "^0.23.1",
|
||||||
"@headlessui/react": "^1.7.15",
|
"@headlessui/react": "^1.7.15",
|
||||||
"@nostr-dev-kit/ndk": "^0.5.13",
|
"@nostr-dev-kit/ndk": "^0.5.13",
|
||||||
|
"@tanstack/react-query": "^4.29.15",
|
||||||
"@tanstack/react-virtual": "3.0.0-beta.54",
|
"@tanstack/react-virtual": "3.0.0-beta.54",
|
||||||
"@tauri-apps/api": "^1.4.0",
|
"@tauri-apps/api": "^1.4.0",
|
||||||
"@vidstack/react": "^0.4.5",
|
|
||||||
"dayjs": "^1.11.8",
|
"dayjs": "^1.11.8",
|
||||||
"destr": "^1.2.2",
|
"destr": "^1.2.2",
|
||||||
|
"framer-motion": "^10.12.17",
|
||||||
"get-urls": "^11.0.0",
|
"get-urls": "^11.0.0",
|
||||||
"immer": "^10.0.2",
|
"immer": "^10.0.2",
|
||||||
"light-bolt11-decoder": "^3.0.0",
|
"light-bolt11-decoder": "^3.0.0",
|
||||||
@@ -30,12 +31,12 @@
|
|||||||
"react-hook-form": "^7.45.0",
|
"react-hook-form": "^7.45.0",
|
||||||
"react-hotkeys-hook": "^4.4.0",
|
"react-hotkeys-hook": "^4.4.0",
|
||||||
"react-resizable-panels": "^0.0.48",
|
"react-resizable-panels": "^0.0.48",
|
||||||
|
"react-router-dom": "^6.14.0",
|
||||||
"react-string-replace": "^1.1.1",
|
"react-string-replace": "^1.1.1",
|
||||||
"react-virtuoso": "^4.3.10",
|
"react-virtuoso": "^4.3.11",
|
||||||
"slate": "^0.94.1",
|
"slate": "^0.94.1",
|
||||||
"slate-history": "^0.93.0",
|
"slate-history": "^0.93.0",
|
||||||
"slate-react": "^0.94.2",
|
"slate-react": "^0.94.2",
|
||||||
"swr": "^2.1.5",
|
|
||||||
"tailwind-merge": "^1.13.2",
|
"tailwind-merge": "^1.13.2",
|
||||||
"tauri-plugin-sql-api": "github:tauri-apps/tauri-plugin-sql",
|
"tauri-plugin-sql-api": "github:tauri-apps/tauri-plugin-sql",
|
||||||
"vidstack": "^0.4.5",
|
"vidstack": "^0.4.5",
|
||||||
@@ -45,7 +46,7 @@
|
|||||||
"@tailwindcss/typography": "^0.5.9",
|
"@tailwindcss/typography": "^0.5.9",
|
||||||
"@tauri-apps/cli": "^1.4.0",
|
"@tauri-apps/cli": "^1.4.0",
|
||||||
"@types/node": "^18.16.18",
|
"@types/node": "^18.16.18",
|
||||||
"@types/react": "^18.2.13",
|
"@types/react": "^18.2.14",
|
||||||
"@types/react-dom": "^18.2.6",
|
"@types/react-dom": "^18.2.6",
|
||||||
"@types/youtube-player": "^5.5.7",
|
"@types/youtube-player": "^5.5.7",
|
||||||
"@vitejs/plugin-react-swc": "^3.3.2",
|
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||||
@@ -61,7 +62,6 @@
|
|||||||
"tailwindcss": "^3.3.2",
|
"tailwindcss": "^3.3.2",
|
||||||
"typescript": "^4.9.5",
|
"typescript": "^4.9.5",
|
||||||
"vite": "^4.3.9",
|
"vite": "^4.3.9",
|
||||||
"vite-plugin-ssr": "^0.4.131",
|
|
||||||
"vite-plugin-top-level-await": "^1.3.1",
|
"vite-plugin-top-level-await": "^1.3.1",
|
||||||
"vite-tsconfig-paths": "^4.2.0"
|
"vite-tsconfig-paths": "^4.2.0"
|
||||||
}
|
}
|
||||||
|
|||||||
268
pnpm-lock.yaml
generated
268
pnpm-lock.yaml
generated
@@ -10,21 +10,24 @@ dependencies:
|
|||||||
'@nostr-dev-kit/ndk':
|
'@nostr-dev-kit/ndk':
|
||||||
specifier: ^0.5.13
|
specifier: ^0.5.13
|
||||||
version: 0.5.13(typescript@4.9.5)
|
version: 0.5.13(typescript@4.9.5)
|
||||||
|
'@tanstack/react-query':
|
||||||
|
specifier: ^4.29.15
|
||||||
|
version: 4.29.15(react-dom@18.2.0)(react@18.2.0)
|
||||||
'@tanstack/react-virtual':
|
'@tanstack/react-virtual':
|
||||||
specifier: 3.0.0-beta.54
|
specifier: 3.0.0-beta.54
|
||||||
version: 3.0.0-beta.54(react@18.2.0)
|
version: 3.0.0-beta.54(react@18.2.0)
|
||||||
'@tauri-apps/api':
|
'@tauri-apps/api':
|
||||||
specifier: ^1.4.0
|
specifier: ^1.4.0
|
||||||
version: 1.4.0
|
version: 1.4.0
|
||||||
'@vidstack/react':
|
|
||||||
specifier: ^0.4.5
|
|
||||||
version: 0.4.5(@types/react@18.2.13)(maverick.js@0.33.1)(media-icons@0.4.2)(react@18.2.0)(vidstack@0.4.5)
|
|
||||||
dayjs:
|
dayjs:
|
||||||
specifier: ^1.11.8
|
specifier: ^1.11.8
|
||||||
version: 1.11.8
|
version: 1.11.8
|
||||||
destr:
|
destr:
|
||||||
specifier: ^1.2.2
|
specifier: ^1.2.2
|
||||||
version: 1.2.2
|
version: 1.2.2
|
||||||
|
framer-motion:
|
||||||
|
specifier: ^10.12.17
|
||||||
|
version: 10.12.17(react-dom@18.2.0)(react@18.2.0)
|
||||||
get-urls:
|
get-urls:
|
||||||
specifier: ^11.0.0
|
specifier: ^11.0.0
|
||||||
version: 11.0.0
|
version: 11.0.0
|
||||||
@@ -52,12 +55,15 @@ dependencies:
|
|||||||
react-resizable-panels:
|
react-resizable-panels:
|
||||||
specifier: ^0.0.48
|
specifier: ^0.0.48
|
||||||
version: 0.0.48(react-dom@18.2.0)(react@18.2.0)
|
version: 0.0.48(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
react-router-dom:
|
||||||
|
specifier: ^6.14.0
|
||||||
|
version: 6.14.0(react-dom@18.2.0)(react@18.2.0)
|
||||||
react-string-replace:
|
react-string-replace:
|
||||||
specifier: ^1.1.1
|
specifier: ^1.1.1
|
||||||
version: 1.1.1
|
version: 1.1.1
|
||||||
react-virtuoso:
|
react-virtuoso:
|
||||||
specifier: ^4.3.10
|
specifier: ^4.3.11
|
||||||
version: 4.3.10(react-dom@18.2.0)(react@18.2.0)
|
version: 4.3.11(react-dom@18.2.0)(react@18.2.0)
|
||||||
slate:
|
slate:
|
||||||
specifier: ^0.94.1
|
specifier: ^0.94.1
|
||||||
version: 0.94.1
|
version: 0.94.1
|
||||||
@@ -67,9 +73,6 @@ dependencies:
|
|||||||
slate-react:
|
slate-react:
|
||||||
specifier: ^0.94.2
|
specifier: ^0.94.2
|
||||||
version: 0.94.2(react-dom@18.2.0)(react@18.2.0)(slate@0.94.1)
|
version: 0.94.2(react-dom@18.2.0)(react@18.2.0)(slate@0.94.1)
|
||||||
swr:
|
|
||||||
specifier: ^2.1.5
|
|
||||||
version: 2.1.5(react@18.2.0)
|
|
||||||
tailwind-merge:
|
tailwind-merge:
|
||||||
specifier: ^1.13.2
|
specifier: ^1.13.2
|
||||||
version: 1.13.2
|
version: 1.13.2
|
||||||
@@ -94,8 +97,8 @@ devDependencies:
|
|||||||
specifier: ^18.16.18
|
specifier: ^18.16.18
|
||||||
version: 18.16.18
|
version: 18.16.18
|
||||||
'@types/react':
|
'@types/react':
|
||||||
specifier: ^18.2.13
|
specifier: ^18.2.14
|
||||||
version: 18.2.13
|
version: 18.2.14
|
||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
specifier: ^18.2.6
|
specifier: ^18.2.6
|
||||||
version: 18.2.6
|
version: 18.2.6
|
||||||
@@ -141,9 +144,6 @@ devDependencies:
|
|||||||
vite:
|
vite:
|
||||||
specifier: ^4.3.9
|
specifier: ^4.3.9
|
||||||
version: 4.3.9(@types/node@18.16.18)
|
version: 4.3.9(@types/node@18.16.18)
|
||||||
vite-plugin-ssr:
|
|
||||||
specifier: ^0.4.131
|
|
||||||
version: 0.4.131(vite@4.3.9)
|
|
||||||
vite-plugin-top-level-await:
|
vite-plugin-top-level-await:
|
||||||
specifier: ^1.3.1
|
specifier: ^1.3.1
|
||||||
version: 1.3.1(vite@4.3.9)
|
version: 1.3.1(vite@4.3.9)
|
||||||
@@ -179,23 +179,18 @@ packages:
|
|||||||
js-tokens: 4.0.0
|
js-tokens: 4.0.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@brillout/import@0.2.3:
|
/@emotion/is-prop-valid@0.8.8:
|
||||||
resolution: {integrity: sha512-1T8WlD75eeFSMrptGy8jiLHmfHgMmSjWvLOIUvHmSVZt+6k0eQqYUoK4KbmE4T9pVLIfxvZSOm2D68VEqKRHRw==}
|
resolution: {integrity: sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==}
|
||||||
dev: true
|
requiresBuild: true
|
||||||
|
|
||||||
/@brillout/json-serializer@0.5.3:
|
|
||||||
resolution: {integrity: sha512-IxlOMD5gOM0WfFGdeR98jHKiC82Ad1tUnSjvLS5jnRkfMEKBI+YzHA32Umw8W3Ccp5N4fNEX229BW6RaRpxRWQ==}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@brillout/picocolors@1.0.4:
|
|
||||||
resolution: {integrity: sha512-rhZBVyrRCb53T9xIGoEjZQ6O4Um3XQWcQ1z2VL2eBQBtJYCsABUUNE/isqbnts3XD1sAkisDF2L3OjJeIgrznQ==}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@brillout/vite-plugin-import-build@0.2.18:
|
|
||||||
resolution: {integrity: sha512-sedZNrqIboHCeSnN7hwo34xRyP8egfMHcifixQ2YGNnQVb93884drTVE3b0vlSGz7LWumVDochKuHdWQljup9A==}
|
|
||||||
dependencies:
|
dependencies:
|
||||||
'@brillout/import': 0.2.3
|
'@emotion/memoize': 0.7.4
|
||||||
dev: true
|
dev: false
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
/@emotion/memoize@0.7.4:
|
||||||
|
resolution: {integrity: sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==}
|
||||||
|
dev: false
|
||||||
|
optional: true
|
||||||
|
|
||||||
/@esbuild/android-arm64@0.17.19:
|
/@esbuild/android-arm64@0.17.19:
|
||||||
resolution: {integrity: sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==}
|
resolution: {integrity: sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==}
|
||||||
@@ -533,8 +528,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==}
|
resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@maverick-js/signals@5.11.1:
|
/@maverick-js/signals@5.11.2:
|
||||||
resolution: {integrity: sha512-yr6ZIpQxupgbDl6364t1L7bOSJqK2xiwjf8hoxJ632SL+0nBL0bLNCQaiK4GFRQEhYhBwX2yYyA+eKgZLEJ6mg==}
|
resolution: {integrity: sha512-jKAyNE2O7H+xrigPoqdV0Iq2AeQ6cysfBf/b2jasJ4FfCUKjGyazgtp+pIspTW6skFvpPrvq40Qft+7HuR+Tlg==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@noble/curves@1.0.0:
|
/@noble/curves@1.0.0:
|
||||||
@@ -607,7 +602,7 @@ packages:
|
|||||||
resolution: {integrity: sha512-7kZUAaLscfgbwBQRbvdMYaZOWyMEcPTH/tJjnyAWJ/dvvs9Ef+CERx/qJb9GExJpl1qipaDGn7KqHnFGGixd0w==}
|
resolution: {integrity: sha512-7kZUAaLscfgbwBQRbvdMYaZOWyMEcPTH/tJjnyAWJ/dvvs9Ef+CERx/qJb9GExJpl1qipaDGn7KqHnFGGixd0w==}
|
||||||
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
||||||
dependencies:
|
dependencies:
|
||||||
semver: 7.5.2
|
semver: 7.5.3
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@pkgjs/parseargs@0.11.0:
|
/@pkgjs/parseargs@0.11.0:
|
||||||
@@ -617,9 +612,10 @@ packages:
|
|||||||
dev: false
|
dev: false
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@polka/url@1.0.0-next.21:
|
/@remix-run/router@1.7.0:
|
||||||
resolution: {integrity: sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==}
|
resolution: {integrity: sha512-Eu1V3kz3mV0wUpVTiFHuaT8UD1gj/0VnoFHQYX35xlslQUpe8CuYoKFn9d4WZFHm3yDywz6ALZuGdnUPKrNeAw==}
|
||||||
dev: true
|
engines: {node: '>=14'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@rollup/plugin-virtual@3.0.1:
|
/@rollup/plugin-virtual@3.0.1:
|
||||||
resolution: {integrity: sha512-fK8O0IL5+q+GrsMLuACVNk2x21g3yaw+sG2qn16SnUd3IlBsQyvWxLMGHmCmXRMecPjGRSZ/1LmZB4rjQm68og==}
|
resolution: {integrity: sha512-fK8O0IL5+q+GrsMLuACVNk2x21g3yaw+sG2qn16SnUd3IlBsQyvWxLMGHmCmXRMecPjGRSZ/1LmZB4rjQm68og==}
|
||||||
@@ -826,6 +822,28 @@ packages:
|
|||||||
tailwindcss: 3.3.2
|
tailwindcss: 3.3.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@tanstack/query-core@4.29.15:
|
||||||
|
resolution: {integrity: sha512-Recc1d5rjHesKhzlH3Aw66v+vQxtB9OHEXP/vxgEcEJ0DwEpfe3EQ4id20vuBJHY2XRjfgWGmUs6ZgK6PSsTXA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@tanstack/react-query@4.29.15(react-dom@18.2.0)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-1zDkv95ljuJ623hhbYU8YIprPW2x6774kh3IQNEuZav62+S+Zr26uUOrE2zGRp9I1uO5Liw/0uYB3dWXQP5+3Q==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
|
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
|
react-native: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
react-dom:
|
||||||
|
optional: true
|
||||||
|
react-native:
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@tanstack/query-core': 4.29.15
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
use-sync-external-store: 1.2.0(react@18.2.0)
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@tanstack/react-virtual@3.0.0-beta.54(react@18.2.0):
|
/@tanstack/react-virtual@3.0.0-beta.54(react@18.2.0):
|
||||||
resolution: {integrity: sha512-D1mDMf4UPbrtHRZZriCly5bXTBMhylslm4dhcHqTtDJ6brQcgGmk8YD9JdWBGWfGSWPKoh2x1H3e7eh+hgPXtQ==}
|
resolution: {integrity: sha512-D1mDMf4UPbrtHRZZriCly5bXTBMhylslm4dhcHqTtDJ6brQcgGmk8YD9JdWBGWfGSWPKoh2x1H3e7eh+hgPXtQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1001,22 +1019,25 @@ packages:
|
|||||||
|
|
||||||
/@types/prop-types@15.7.5:
|
/@types/prop-types@15.7.5:
|
||||||
resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==}
|
resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/react-dom@18.2.6:
|
/@types/react-dom@18.2.6:
|
||||||
resolution: {integrity: sha512-2et4PDvg6PVCyS7fuTc4gPoksV58bW0RwSxWKcPRcHZf0PRUGq03TKcD/rUHe3azfV6/5/biUBJw+HhCQjaP0A==}
|
resolution: {integrity: sha512-2et4PDvg6PVCyS7fuTc4gPoksV58bW0RwSxWKcPRcHZf0PRUGq03TKcD/rUHe3azfV6/5/biUBJw+HhCQjaP0A==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/react': 18.2.13
|
'@types/react': 18.2.14
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@types/react@18.2.13:
|
/@types/react@18.2.14:
|
||||||
resolution: {integrity: sha512-vJ+zElvi/Zn9cVXB5slX2xL8PZodPCwPRDpittQdw43JR2AJ5k3vKdgJJyneV/cYgIbLQUwXa9JVDvUZXGba+Q==}
|
resolution: {integrity: sha512-A0zjq+QN/O0Kpe30hA1GidzyFjatVvrpIvWLxD+xv67Vt91TWWgco9IvrJBkeyHm1trGaFS/FSGqPlhyeZRm0g==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/prop-types': 15.7.5
|
'@types/prop-types': 15.7.5
|
||||||
'@types/scheduler': 0.16.3
|
'@types/scheduler': 0.16.3
|
||||||
csstype: 3.1.2
|
csstype: 3.1.2
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/scheduler@0.16.3:
|
/@types/scheduler@0.16.3:
|
||||||
resolution: {integrity: sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==}
|
resolution: {integrity: sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/semver@7.5.0:
|
/@types/semver@7.5.0:
|
||||||
resolution: {integrity: sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==}
|
resolution: {integrity: sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==}
|
||||||
@@ -1047,7 +1068,7 @@ packages:
|
|||||||
grapheme-splitter: 1.0.4
|
grapheme-splitter: 1.0.4
|
||||||
ignore: 5.2.4
|
ignore: 5.2.4
|
||||||
natural-compare-lite: 1.4.0
|
natural-compare-lite: 1.4.0
|
||||||
semver: 7.5.2
|
semver: 7.5.3
|
||||||
tsutils: 3.21.0(typescript@4.9.5)
|
tsutils: 3.21.0(typescript@4.9.5)
|
||||||
typescript: 4.9.5
|
typescript: 4.9.5
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -1121,7 +1142,7 @@ packages:
|
|||||||
debug: 4.3.4
|
debug: 4.3.4
|
||||||
globby: 11.1.0
|
globby: 11.1.0
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
semver: 7.5.2
|
semver: 7.5.3
|
||||||
tsutils: 3.21.0(typescript@4.9.5)
|
tsutils: 3.21.0(typescript@4.9.5)
|
||||||
typescript: 4.9.5
|
typescript: 4.9.5
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -1142,7 +1163,7 @@ packages:
|
|||||||
'@typescript-eslint/typescript-estree': 5.60.0(typescript@4.9.5)
|
'@typescript-eslint/typescript-estree': 5.60.0(typescript@4.9.5)
|
||||||
eslint: 8.43.0
|
eslint: 8.43.0
|
||||||
eslint-scope: 5.1.1
|
eslint-scope: 5.1.1
|
||||||
semver: 7.5.2
|
semver: 7.5.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
- typescript
|
- typescript
|
||||||
@@ -1156,23 +1177,6 @@ packages:
|
|||||||
eslint-visitor-keys: 3.4.1
|
eslint-visitor-keys: 3.4.1
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@vidstack/react@0.4.5(@types/react@18.2.13)(maverick.js@0.33.1)(media-icons@0.4.2)(react@18.2.0)(vidstack@0.4.5):
|
|
||||||
resolution: {integrity: sha512-spcim3+p1fMzkhHRKn5PS54YQjfThW5M3F2+R8tCT+wpsxbbCDa/TGdLBoIy2oC0LNziPkn0vlBWIZko9F5iig==}
|
|
||||||
engines: {node: '>=16'}
|
|
||||||
peerDependencies:
|
|
||||||
'@types/react': ^18.0.0
|
|
||||||
maverick.js: 0.33.1
|
|
||||||
media-icons: ^0.4.2
|
|
||||||
react: ^18.0.0
|
|
||||||
vidstack: 0.4.5
|
|
||||||
dependencies:
|
|
||||||
'@types/react': 18.2.13
|
|
||||||
maverick.js: 0.33.1
|
|
||||||
media-icons: 0.4.2
|
|
||||||
react: 18.2.0
|
|
||||||
vidstack: 0.4.5
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@vitejs/plugin-react-swc@3.3.2(vite@4.3.9):
|
/@vitejs/plugin-react-swc@3.3.2(vite@4.3.9):
|
||||||
resolution: {integrity: sha512-VJFWY5sfoZerQRvJrh518h3AcQt6f/yTuWn4/TRB+dqmYU0NX1qz7qM5Wfd+gOQqUzQW4gxKqKN3KpE/P3+zrA==}
|
resolution: {integrity: sha512-VJFWY5sfoZerQRvJrh518h3AcQt6f/yTuWn4/TRB+dqmYU0NX1qz7qM5Wfd+gOQqUzQW4gxKqKN3KpE/P3+zrA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1200,6 +1204,7 @@ packages:
|
|||||||
resolution: {integrity: sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==}
|
resolution: {integrity: sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==}
|
||||||
engines: {node: '>=0.4.0'}
|
engines: {node: '>=0.4.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
dev: false
|
||||||
|
|
||||||
/agent-base@6.0.2:
|
/agent-base@6.0.2:
|
||||||
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
|
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
|
||||||
@@ -1373,7 +1378,7 @@ packages:
|
|||||||
postcss: ^8.1.0
|
postcss: ^8.1.0
|
||||||
dependencies:
|
dependencies:
|
||||||
browserslist: 4.21.9
|
browserslist: 4.21.9
|
||||||
caniuse-lite: 1.0.30001506
|
caniuse-lite: 1.0.30001507
|
||||||
fraction.js: 4.2.0
|
fraction.js: 4.2.0
|
||||||
normalize-range: 0.1.2
|
normalize-range: 0.1.2
|
||||||
picocolors: 1.0.0
|
picocolors: 1.0.0
|
||||||
@@ -1417,8 +1422,8 @@ packages:
|
|||||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
dependencies:
|
dependencies:
|
||||||
caniuse-lite: 1.0.30001506
|
caniuse-lite: 1.0.30001507
|
||||||
electron-to-chromium: 1.4.435
|
electron-to-chromium: 1.4.440
|
||||||
node-releases: 2.0.12
|
node-releases: 2.0.12
|
||||||
update-browserslist-db: 1.0.11(browserslist@4.21.9)
|
update-browserslist-db: 1.0.11(browserslist@4.21.9)
|
||||||
dev: true
|
dev: true
|
||||||
@@ -1431,18 +1436,13 @@ packages:
|
|||||||
node-gyp-build: 4.6.0
|
node-gyp-build: 4.6.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/cac@6.7.14:
|
|
||||||
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
|
|
||||||
engines: {node: '>=8'}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/cacache@17.1.3:
|
/cacache@17.1.3:
|
||||||
resolution: {integrity: sha512-jAdjGxmPxZh0IipMdR7fK/4sDSrHMLUV0+GvVUsjwyGNKHsh79kW/otg+GkbXwl6Uzvy9wsvHOX4nUoWldeZMg==}
|
resolution: {integrity: sha512-jAdjGxmPxZh0IipMdR7fK/4sDSrHMLUV0+GvVUsjwyGNKHsh79kW/otg+GkbXwl6Uzvy9wsvHOX4nUoWldeZMg==}
|
||||||
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@npmcli/fs': 3.1.0
|
'@npmcli/fs': 3.1.0
|
||||||
fs-minipass: 3.0.2
|
fs-minipass: 3.0.2
|
||||||
glob: 10.2.7
|
glob: 10.3.0
|
||||||
lru-cache: 7.18.3
|
lru-cache: 7.18.3
|
||||||
minipass: 5.0.0
|
minipass: 5.0.0
|
||||||
minipass-collect: 1.0.2
|
minipass-collect: 1.0.2
|
||||||
@@ -1485,8 +1485,8 @@ packages:
|
|||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/caniuse-lite@1.0.30001506:
|
/caniuse-lite@1.0.30001507:
|
||||||
resolution: {integrity: sha512-6XNEcpygZMCKaufIcgpQNZNf00GEqc7VQON+9Rd0K1bMYo8xhMZRAo5zpbnbMNizi4YNgIDAFrdykWsvY3H4Hw==}
|
resolution: {integrity: sha512-SFpUDoSLCaE5XYL2jfqe9ova/pbQHEmbheDf5r4diNwbAgR3qxM9NQtfsiSscjqoya5K7kFcHPUQ+VsUkIJR4A==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/chalk@2.4.2:
|
/chalk@2.4.2:
|
||||||
@@ -1615,7 +1615,7 @@ packages:
|
|||||||
resolution: {integrity: sha512-cllzD6IU/mzXBs5OdQVWL3+ne5Elpu3Wdm7h5OldMbGXk76yr9XzHlQXWJ4zfs0ZAibe26rkbs4KvMAJm7fIZA==}
|
resolution: {integrity: sha512-cllzD6IU/mzXBs5OdQVWL3+ne5Elpu3Wdm7h5OldMbGXk76yr9XzHlQXWJ4zfs0ZAibe26rkbs4KvMAJm7fIZA==}
|
||||||
engines: {node: '>=14.x'}
|
engines: {node: '>=14.x'}
|
||||||
dependencies:
|
dependencies:
|
||||||
semver: 7.5.2
|
semver: 7.5.3
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/cross-env@7.0.3:
|
/cross-env@7.0.3:
|
||||||
@@ -1653,6 +1653,7 @@ packages:
|
|||||||
|
|
||||||
/csstype@3.1.2:
|
/csstype@3.1.2:
|
||||||
resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==}
|
resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/d@1.0.1:
|
/d@1.0.1:
|
||||||
resolution: {integrity: sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==}
|
resolution: {integrity: sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==}
|
||||||
@@ -1783,8 +1784,8 @@ packages:
|
|||||||
/eastasianwidth@0.2.0:
|
/eastasianwidth@0.2.0:
|
||||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||||
|
|
||||||
/electron-to-chromium@1.4.435:
|
/electron-to-chromium@1.4.440:
|
||||||
resolution: {integrity: sha512-B0CBWVFhvoQCW/XtjRzgrmqcgVWg6RXOEM/dK59+wFV93BFGR6AeNKc4OyhM+T3IhJaOOG8o/V+33Y2mwJWtzw==}
|
resolution: {integrity: sha512-r6dCgNpRhPwiWlxbHzZQ/d9swfPaEJGi8ekqRBwQYaR3WmA5VkqQfBWSDDjuJU1ntO+W9tHx8OHV/96Q8e0dVw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/emoji-regex@8.0.0:
|
/emoji-regex@8.0.0:
|
||||||
@@ -1853,10 +1854,6 @@ packages:
|
|||||||
which-typed-array: 1.1.9
|
which-typed-array: 1.1.9
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/es-module-lexer@0.10.5:
|
|
||||||
resolution: {integrity: sha512-+7IwY/kiGAacQfY+YBhKMvEmyAJnw5grTUgjG85Pe7vcUI/6b7pZjZG8nQ7+48YhzEAEqrEgD2dCz/JIK+AYvw==}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/es-set-tostringtag@2.0.1:
|
/es-set-tostringtag@2.0.1:
|
||||||
resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==}
|
resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -2127,7 +2124,7 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
create-esm-loader: 0.2.3
|
create-esm-loader: 0.2.3
|
||||||
npm-run-all: 4.1.5
|
npm-run-all: 4.1.5
|
||||||
semver: 7.5.2
|
semver: 7.5.3
|
||||||
typescript: 5.1.3
|
typescript: 5.1.3
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
@@ -2299,6 +2296,24 @@ packages:
|
|||||||
resolution: {integrity: sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==}
|
resolution: {integrity: sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/framer-motion@10.12.17(react-dom@18.2.0)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-IR+aAYntsyu6ofyxqQV4QYotmOqzcuKxhqNpfc3DXJjNWOPpOeSyH0A+In3IEBu49Yx/+PNht+YMeZSdCNaYbw==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^18.0.0
|
||||||
|
react-dom: ^18.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
react-dom:
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
tslib: 2.5.3
|
||||||
|
optionalDependencies:
|
||||||
|
'@emotion/is-prop-valid': 0.8.8
|
||||||
|
dev: false
|
||||||
|
|
||||||
/fs-minipass@2.1.0:
|
/fs-minipass@2.1.0:
|
||||||
resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==}
|
resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
@@ -2400,14 +2415,14 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
|
|
||||||
/glob@10.2.7:
|
/glob@10.3.0:
|
||||||
resolution: {integrity: sha512-jTKehsravOJo8IJxUGfZILnkvVJM/MOfHRs8QcXolVef2zNI9Tqyy5+SeuOAZd3upViEZQLyFpQhYiHLrMUNmA==}
|
resolution: {integrity: sha512-AQ1/SB9HH0yCx1jXAT4vmCbTOPe5RQ+kCurjbel5xSCGhebumUv+GJZfa1rEqor3XIViqwSEmlkZCQD43RWrBg==}
|
||||||
engines: {node: '>=16 || 14 >=14.17'}
|
engines: {node: '>=16 || 14 >=14.17'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
dependencies:
|
dependencies:
|
||||||
foreground-child: 3.1.1
|
foreground-child: 3.1.1
|
||||||
jackspeak: 2.2.1
|
jackspeak: 2.2.1
|
||||||
minimatch: 9.0.1
|
minimatch: 9.0.2
|
||||||
minipass: 5.0.0
|
minipass: 5.0.0
|
||||||
path-scurry: 1.9.2
|
path-scurry: 1.9.2
|
||||||
dev: false
|
dev: false
|
||||||
@@ -3078,7 +3093,7 @@ packages:
|
|||||||
resolution: {integrity: sha512-p8L5V62CV6TmHAngmRAopp231oJKeH77mJja5SsKOfvzrPRoThT/Jo9U0jMRB5iMykqkvyg2J5V5Agn6FPXDWQ==}
|
resolution: {integrity: sha512-p8L5V62CV6TmHAngmRAopp231oJKeH77mJja5SsKOfvzrPRoThT/Jo9U0jMRB5iMykqkvyg2J5V5Agn6FPXDWQ==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@maverick-js/signals': 5.11.1
|
'@maverick-js/signals': 5.11.2
|
||||||
type-fest: 3.12.0
|
type-fest: 3.12.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
@@ -3150,8 +3165,8 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion: 1.1.11
|
brace-expansion: 1.1.11
|
||||||
|
|
||||||
/minimatch@9.0.1:
|
/minimatch@9.0.2:
|
||||||
resolution: {integrity: sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==}
|
resolution: {integrity: sha512-PZOT9g5v2ojiTL7r1xF6plNHLtOeTpSlDI007As2NlA2aYBMfVom17yqa6QzhmDP8QOhn7LjHTg7DFCVSSa6yg==}
|
||||||
engines: {node: '>=16 || 14 >=14.17'}
|
engines: {node: '>=16 || 14 >=14.17'}
|
||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion: 2.0.1
|
brace-expansion: 2.0.1
|
||||||
@@ -3235,11 +3250,6 @@ packages:
|
|||||||
hasBin: true
|
hasBin: true
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/mrmime@1.0.1:
|
|
||||||
resolution: {integrity: sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/ms@2.0.0:
|
/ms@2.0.0:
|
||||||
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
|
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -3322,7 +3332,7 @@ packages:
|
|||||||
nopt: 6.0.0
|
nopt: 6.0.0
|
||||||
npmlog: 6.0.2
|
npmlog: 6.0.2
|
||||||
rimraf: 3.0.2
|
rimraf: 3.0.2
|
||||||
semver: 7.5.2
|
semver: 7.5.3
|
||||||
tar: 6.1.15
|
tar: 6.1.15
|
||||||
which: 2.0.2
|
which: 2.0.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -3356,7 +3366,7 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
hosted-git-info: 4.1.0
|
hosted-git-info: 4.1.0
|
||||||
is-core-module: 2.12.1
|
is-core-module: 2.12.1
|
||||||
semver: 7.5.2
|
semver: 7.5.3
|
||||||
validate-npm-package-license: 3.0.4
|
validate-npm-package-license: 3.0.4
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
@@ -3817,13 +3827,36 @@ packages:
|
|||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/react-router-dom@6.14.0(react-dom@18.2.0)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-YEwlApKwzMMMbGbhh+Q7MsloTldcwMgHxUY/1g0uA62+B1hZo2jsybCWIDCL8zvIDB1FA0pBKY9chHbZHt+2dQ==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=16.8'
|
||||||
|
react-dom: '>=16.8'
|
||||||
|
dependencies:
|
||||||
|
'@remix-run/router': 1.7.0
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
react-router: 6.14.0(react@18.2.0)
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/react-router@6.14.0(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-OD+vkrcGbvlwkspUFDgMzsu1RXwdjNh83YgG/28lBnDzgslhCgxIqoExLlxsfTpIygp7fc+Hd3esloNwzkm2xA==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=16.8'
|
||||||
|
dependencies:
|
||||||
|
'@remix-run/router': 1.7.0
|
||||||
|
react: 18.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/react-string-replace@1.1.1:
|
/react-string-replace@1.1.1:
|
||||||
resolution: {integrity: sha512-26TUbLzLfHQ5jO5N7y3Mx88eeKo0Ml0UjCQuX4BMfOd/JX+enQqlKpL1CZnmjeBRvQE8TR+ds9j1rqx9CxhKHQ==}
|
resolution: {integrity: sha512-26TUbLzLfHQ5jO5N7y3Mx88eeKo0Ml0UjCQuX4BMfOd/JX+enQqlKpL1CZnmjeBRvQE8TR+ds9j1rqx9CxhKHQ==}
|
||||||
engines: {node: '>=0.12.0'}
|
engines: {node: '>=0.12.0'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/react-virtuoso@4.3.10(react-dom@18.2.0)(react@18.2.0):
|
/react-virtuoso@4.3.11(react-dom@18.2.0)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-/LDICLCmPRDMOheCKmuHj/U7CGjLT/WtMWZGfOothhhubQeWGbR6mtGyd+uD80Yw/n3ICZtYwERQZnTM8eC0ag==}
|
resolution: {integrity: sha512-0YrCvQ5GsIKRcN34GxrzhSJGuMNI+hGxWci5cTVuPQ8QWTEsrKfCyqm7YNBMmV3pu7onG1YVUBo86CyCXdejXg==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: '>=16 || >=17 || >= 18'
|
react: '>=16 || >=17 || >= 18'
|
||||||
@@ -4018,8 +4051,8 @@ packages:
|
|||||||
hasBin: true
|
hasBin: true
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/semver@7.5.2:
|
/semver@7.5.3:
|
||||||
resolution: {integrity: sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==}
|
resolution: {integrity: sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -4072,15 +4105,6 @@ packages:
|
|||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/sirv@2.0.3:
|
|
||||||
resolution: {integrity: sha512-O9jm9BsID1P+0HOi81VpXPoDxYP374pkOLzACAoyUQ/3OUVndNpsz6wMnY2z+yOxzbllCKZrM+9QrWsv4THnyA==}
|
|
||||||
engines: {node: '>= 10'}
|
|
||||||
dependencies:
|
|
||||||
'@polka/url': 1.0.0-next.21
|
|
||||||
mrmime: 1.0.1
|
|
||||||
totalist: 3.0.1
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/slash@3.0.0:
|
/slash@3.0.0:
|
||||||
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
|
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -4343,15 +4367,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
/swr@2.1.5(react@18.2.0):
|
|
||||||
resolution: {integrity: sha512-/OhfZMcEpuz77KavXST5q6XE9nrOBOVcBLWjMT+oAE/kQHyE3PASrevXCtQDZ8aamntOfFkbVJp7Il9tNBQWrw==}
|
|
||||||
peerDependencies:
|
|
||||||
react: ^16.11.0 || ^17.0.0 || ^18.0.0
|
|
||||||
dependencies:
|
|
||||||
react: 18.2.0
|
|
||||||
use-sync-external-store: 1.2.0(react@18.2.0)
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/tabbable@6.1.2:
|
/tabbable@6.1.2:
|
||||||
resolution: {integrity: sha512-qCN98uP7i9z0fIS4amQ5zbGBOq+OSigYeGvPy7NDk8Y9yncqDZ9pRPgfsc2PJIVM9RrJj7GIfuRgmjoUU9zTHQ==}
|
resolution: {integrity: sha512-qCN98uP7i9z0fIS4amQ5zbGBOq+OSigYeGvPy7NDk8Y9yncqDZ9pRPgfsc2PJIVM9RrJj7GIfuRgmjoUU9zTHQ==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -4444,11 +4459,6 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-number: 7.0.0
|
is-number: 7.0.0
|
||||||
|
|
||||||
/totalist@3.0.1:
|
|
||||||
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
|
|
||||||
engines: {node: '>=6'}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/trim-newlines@3.0.1:
|
/trim-newlines@3.0.1:
|
||||||
resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==}
|
resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -4684,30 +4694,6 @@ packages:
|
|||||||
type-fest: 3.12.0
|
type-fest: 3.12.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/vite-plugin-ssr@0.4.131(vite@4.3.9):
|
|
||||||
resolution: {integrity: sha512-M8ay5UuQUEMBHhg87BVhXGjPV+/xO3PZWzZ63S8J8rX4Xv+sQbnDswGYm/TJ+Ga/9NTgrFDAKqUfEb3pEpd+Aw==}
|
|
||||||
engines: {node: '>=12.19.0'}
|
|
||||||
hasBin: true
|
|
||||||
peerDependencies:
|
|
||||||
react-streaming: '>=0.3.5'
|
|
||||||
vite: '>=3.1.0'
|
|
||||||
peerDependenciesMeta:
|
|
||||||
react-streaming:
|
|
||||||
optional: true
|
|
||||||
dependencies:
|
|
||||||
'@brillout/import': 0.2.3
|
|
||||||
'@brillout/json-serializer': 0.5.3
|
|
||||||
'@brillout/picocolors': 1.0.4
|
|
||||||
'@brillout/vite-plugin-import-build': 0.2.18
|
|
||||||
acorn: 8.9.0
|
|
||||||
cac: 6.7.14
|
|
||||||
es-module-lexer: 0.10.5
|
|
||||||
esbuild: 0.17.19
|
|
||||||
fast-glob: 3.2.12
|
|
||||||
sirv: 2.0.3
|
|
||||||
vite: 4.3.9(@types/node@18.16.18)
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/vite-plugin-top-level-await@1.3.1(vite@4.3.9):
|
/vite-plugin-top-level-await@1.3.1(vite@4.3.9):
|
||||||
resolution: {integrity: sha512-55M1h4NAwkrpxPNOJIBzKZFihqLUzIgnElLSmPNPMR2Fn9+JHKaNg3sVX1Fq+VgvuBksQYxiD3OnwQAUu7kaPQ==}
|
resolution: {integrity: sha512-55M1h4NAwkrpxPNOJIBzKZFihqLUzIgnElLSmPNPMR2Fn9+JHKaNg3sVX1Fq+VgvuBksQYxiD3OnwQAUu7kaPQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"beforeDevCommand": "pnpm dev",
|
"beforeDevCommand": "pnpm dev",
|
||||||
"beforeBuildCommand": "pnpm build",
|
"beforeBuildCommand": "pnpm build",
|
||||||
"devPath": "http://localhost:3000",
|
"devPath": "http://localhost:3000",
|
||||||
"distDir": "../dist/client",
|
"distDir": "../dist",
|
||||||
"withGlobalTauri": true
|
"withGlobalTauri": true
|
||||||
},
|
},
|
||||||
"package": {
|
"package": {
|
||||||
|
|||||||
91
src/app.tsx
Normal file
91
src/app.tsx
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import "./index.css";
|
||||||
|
import { AuthCreateScreen } from "@app/auth/create";
|
||||||
|
import { CreateStep1Screen } from "@app/auth/create/step-1";
|
||||||
|
import { CreateStep2Screen } from "@app/auth/create/step-2";
|
||||||
|
import { CreateStep3Screen } from "@app/auth/create/step-3";
|
||||||
|
import { CreateStep4Screen } from "@app/auth/create/step-4";
|
||||||
|
import { AuthImportScreen } from "@app/auth/import";
|
||||||
|
import { ImportStep1Screen } from "@app/auth/import/step-1";
|
||||||
|
import { ImportStep2Screen } from "@app/auth/import/step-2";
|
||||||
|
import { OnboardingScreen } from "@app/auth/onboarding";
|
||||||
|
import { WelcomeScreen } from "@app/auth/welcome";
|
||||||
|
import { ChannelScreen } from "@app/channel";
|
||||||
|
import { ChatScreen } from "@app/chat";
|
||||||
|
import { ErrorScreen } from "@app/error";
|
||||||
|
import { Root } from "@app/root";
|
||||||
|
import { SpaceScreen } from "@app/space";
|
||||||
|
import { TrendingScreen } from "@app/trending";
|
||||||
|
import { AppLayout } from "@shared/appLayout";
|
||||||
|
import { AuthLayout } from "@shared/authLayout";
|
||||||
|
import { Protected } from "@shared/protected";
|
||||||
|
import { RelayProvider } from "@shared/relayProvider";
|
||||||
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
import { RouterProvider, createBrowserRouter } from "react-router-dom";
|
||||||
|
|
||||||
|
const router = createBrowserRouter([
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
element: (
|
||||||
|
<Protected>
|
||||||
|
<Root />
|
||||||
|
</Protected>
|
||||||
|
),
|
||||||
|
errorElement: <ErrorScreen />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/auth",
|
||||||
|
element: <AuthLayout />,
|
||||||
|
children: [
|
||||||
|
{ path: "welcome", element: <WelcomeScreen /> },
|
||||||
|
{ path: "onboarding", element: <OnboardingScreen /> },
|
||||||
|
{
|
||||||
|
path: "import",
|
||||||
|
element: <AuthImportScreen />,
|
||||||
|
children: [
|
||||||
|
{ path: "", element: <ImportStep1Screen /> },
|
||||||
|
{ path: "step-2", element: <ImportStep2Screen /> },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "create",
|
||||||
|
element: <AuthCreateScreen />,
|
||||||
|
children: [
|
||||||
|
{ path: "", element: <CreateStep1Screen /> },
|
||||||
|
{ path: "step-2", element: <CreateStep2Screen /> },
|
||||||
|
{ path: "step-3", element: <CreateStep3Screen /> },
|
||||||
|
{ path: "step-4", element: <CreateStep4Screen /> },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/app",
|
||||||
|
element: (
|
||||||
|
<Protected>
|
||||||
|
<AppLayout />
|
||||||
|
</Protected>
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
{ path: "space", element: <SpaceScreen /> },
|
||||||
|
{ path: "trending", element: <TrendingScreen /> },
|
||||||
|
{ path: "chat/:pubkey", element: <ChatScreen /> },
|
||||||
|
{ path: "channel/:id", element: <ChannelScreen /> },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
return (
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<RelayProvider>
|
||||||
|
<RouterProvider
|
||||||
|
router={router}
|
||||||
|
fallbackElement={<p>Loading..</p>}
|
||||||
|
future={{ v7_startTransition: true }}
|
||||||
|
/>
|
||||||
|
</RelayProvider>
|
||||||
|
</QueryClientProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { LayoutOnboarding as Layout } from "./layout";
|
|
||||||
@@ -4,39 +4,39 @@ import { useProfile } from "@utils/hooks/useProfile";
|
|||||||
import { shortenKey } from "@utils/shortenKey";
|
import { shortenKey } from "@utils/shortenKey";
|
||||||
|
|
||||||
export function User({ pubkey }: { pubkey: string }) {
|
export function User({ pubkey }: { pubkey: string }) {
|
||||||
const { user } = useProfile(pubkey);
|
const { status, user } = useProfile(pubkey);
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="relative h-11 w-11 shrink-0 rounded-md bg-zinc-800 animate-pulse" />
|
|
||||||
<div className="flex w-full flex-1 flex-col items-start gap-1 text-start">
|
|
||||||
<span className="w-full h-4 rounded bg-zinc-800 animate-pulse" />
|
|
||||||
<span className="w-1/2 h-3 rounded bg-zinc-800 animate-pulse" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="relative h-11 w-11 shrink rounded-md">
|
{status === "loading" ? (
|
||||||
<Image
|
<>
|
||||||
src={user.image}
|
<div className="relative h-11 w-11 shrink-0 rounded-md bg-zinc-800 animate-pulse" />
|
||||||
fallback={DEFAULT_AVATAR}
|
<div className="flex w-full flex-1 flex-col items-start gap-1 text-start">
|
||||||
alt={pubkey}
|
<span className="w-1/2 h-4 rounded bg-zinc-800 animate-pulse" />
|
||||||
className="h-11 w-11 rounded-md object-cover"
|
<span className="w-1/3 h-3 rounded bg-zinc-800 animate-pulse" />
|
||||||
decoding="async"
|
</div>
|
||||||
/>
|
</>
|
||||||
</div>
|
) : (
|
||||||
<div className="flex w-full flex-1 flex-col items-start text-start">
|
<>
|
||||||
<span className="truncate font-medium leading-tight text-zinc-100">
|
<div className="relative h-11 w-11 shrink rounded-md">
|
||||||
{user.displayName || user.name}
|
<Image
|
||||||
</span>
|
src={user.image}
|
||||||
<span className="text-base leading-tight text-zinc-400">
|
fallback={DEFAULT_AVATAR}
|
||||||
{user.nip05?.toLowerCase() || shortenKey(pubkey)}
|
alt={pubkey}
|
||||||
</span>
|
className="h-11 w-11 rounded-md object-cover"
|
||||||
</div>
|
decoding="async"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full flex-1 flex-col items-start text-start">
|
||||||
|
<span className="truncate font-medium leading-tight text-zinc-100">
|
||||||
|
{user.displayName || user.name}
|
||||||
|
</span>
|
||||||
|
<span className="text-base leading-tight text-zinc-400">
|
||||||
|
{user.nip05?.toLowerCase() || shortenKey(pubkey)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
9
src/app/auth/create/index.tsx
Normal file
9
src/app/auth/create/index.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { Outlet } from "react-router-dom";
|
||||||
|
|
||||||
|
export function AuthCreateScreen() {
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
105
src/app/auth/create/step-1.tsx
Normal file
105
src/app/auth/create/step-1.tsx
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import { createAccount } from "@libs/storage";
|
||||||
|
import { Button } from "@shared/button";
|
||||||
|
import { EyeOffIcon, EyeOnIcon } from "@shared/icons";
|
||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { generatePrivateKey, getPublicKey, nip19 } from "nostr-tools";
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
export function CreateStep1Screen() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const [type, setType] = useState("password");
|
||||||
|
|
||||||
|
const privkey = useMemo(() => generatePrivateKey(), []);
|
||||||
|
const pubkey = getPublicKey(privkey);
|
||||||
|
const npub = nip19.npubEncode(pubkey);
|
||||||
|
const nsec = nip19.nsecEncode(privkey);
|
||||||
|
|
||||||
|
// toggle private key
|
||||||
|
const showPrivateKey = () => {
|
||||||
|
if (type === "password") {
|
||||||
|
setType("text");
|
||||||
|
} else {
|
||||||
|
setType("password");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const account = useMutation({
|
||||||
|
mutationFn: (data: any) =>
|
||||||
|
createAccount(data.npub, data.pubkey, data.privkey, null, 1),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["currentAccount"] });
|
||||||
|
// redirect to next step
|
||||||
|
navigate("/auth/create/step-2", { replace: true });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
account.mutate({
|
||||||
|
npub,
|
||||||
|
pubkey,
|
||||||
|
privkey,
|
||||||
|
follows: null,
|
||||||
|
is_active: 1,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto w-full max-w-md">
|
||||||
|
<div className="mb-8 text-center">
|
||||||
|
<h1 className="text-xl font-semibold text-zinc-100">
|
||||||
|
Lume is auto-generated key for you
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label className="text-base font-semibold text-zinc-400">
|
||||||
|
Public Key
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
readOnly
|
||||||
|
value={npub}
|
||||||
|
className="relative w-full rounded-lg py-3 pl-3.5 pr-11 !outline-none placeholder:text-zinc-400 bg-zinc-800 text-zinc-100"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label className="text-base font-semibold text-zinc-400">
|
||||||
|
Private Key
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
readOnly
|
||||||
|
type={type}
|
||||||
|
value={nsec}
|
||||||
|
className="relative w-full rounded-lg py-3 pl-3.5 pr-11 !outline-none placeholder:text-zinc-400 bg-zinc-800 text-zinc-100"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => showPrivateKey()}
|
||||||
|
className="group absolute right-2 top-1/2 -translate-y-1/2 transform rounded p-1 hover:bg-zinc-700"
|
||||||
|
>
|
||||||
|
{type === "password" ? (
|
||||||
|
<EyeOffIcon
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
className="text-zinc-500 group-hover:text-zinc-100"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<EyeOnIcon
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
className="text-zinc-500 group-hover:text-zinc-100"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button preset="large" onClick={() => submit()}>
|
||||||
|
Continue →
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
112
src/app/auth/create/step-2.tsx
Normal file
112
src/app/auth/create/step-2.tsx
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import { AvatarUploader } from "@shared/avatarUploader";
|
||||||
|
import { LoaderIcon } from "@shared/icons";
|
||||||
|
import { Image } from "@shared/image";
|
||||||
|
import { DEFAULT_AVATAR } from "@stores/constants";
|
||||||
|
import { useOnboarding } from "@stores/onboarding";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
export function CreateStep2Screen() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const createProfile = useOnboarding((state: any) => state.createProfile);
|
||||||
|
|
||||||
|
const [image, setImage] = useState(DEFAULT_AVATAR);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
setValue,
|
||||||
|
formState: { isDirty, isValid },
|
||||||
|
} = useForm();
|
||||||
|
|
||||||
|
const onSubmit = (data: any) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const profile = { ...data, name: data.displayName };
|
||||||
|
createProfile(profile);
|
||||||
|
// redirect to step 3
|
||||||
|
navigate("/auth/create/step-3");
|
||||||
|
} catch {
|
||||||
|
console.log("error");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setValue("picture", image);
|
||||||
|
}, [setValue, image]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto w-full max-w-md">
|
||||||
|
<div className="mb-8 text-center">
|
||||||
|
<h1 className="text-xl font-semibold text-zinc-100">
|
||||||
|
Create your profile
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div className="w-full rounded-xl border-t border-zinc-800/50 bg-zinc-900 p-5">
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-4">
|
||||||
|
<input
|
||||||
|
type={"hidden"}
|
||||||
|
{...register("picture")}
|
||||||
|
value={image}
|
||||||
|
className="relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-100 dark:shadow-black/10 dark:placeholder:text-zinc-500"
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label className="text-sm font-semibold uppercase tracking-wider text-zinc-400">
|
||||||
|
Avatar
|
||||||
|
</label>
|
||||||
|
<div className="relative inline-flex h-36 w-full items-center justify-center overflow-hidden rounded-lg border border-zinc-900 bg-zinc-950">
|
||||||
|
<Image
|
||||||
|
src={image}
|
||||||
|
fallback={DEFAULT_AVATAR}
|
||||||
|
alt="avatar"
|
||||||
|
className="relative z-10 h-11 w-11 rounded-md"
|
||||||
|
/>
|
||||||
|
<div className="absolute bottom-3 right-3 z-10">
|
||||||
|
<AvatarUploader valueState={setImage} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label className="text-sm font-semibold uppercase tracking-wider text-zinc-400">
|
||||||
|
Display Name *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type={"text"}
|
||||||
|
{...register("displayName", {
|
||||||
|
required: true,
|
||||||
|
minLength: 4,
|
||||||
|
})}
|
||||||
|
spellCheck={false}
|
||||||
|
className="relative h-10 w-full rounded-lg px-3 py-2 !outline-none bg-zinc-800 text-zinc-100 placeholder:text-zinc-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label className="text-sm font-semibold uppercase tracking-wider text-zinc-400">
|
||||||
|
Bio
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
{...register("about")}
|
||||||
|
spellCheck={false}
|
||||||
|
className="resize-none relative h-20 w-full rounded-lg px-3 py-2 !outline-none bg-zinc-800 text-zinc-100 placeholder:text-zinc-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={!isDirty || !isValid}
|
||||||
|
className="inline-flex items-center justify-center h-11 w-full bg-fuchsia-500 rounded-md font-medium text-zinc-100 hover:bg-fuchsia-600"
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
|
||||||
|
) : (
|
||||||
|
"Continue →"
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
95
src/app/auth/create/step-3.tsx
Normal file
95
src/app/auth/create/step-3.tsx
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
||||||
|
import { Button } from "@shared/button";
|
||||||
|
import { LoaderIcon } from "@shared/icons";
|
||||||
|
import { RelayContext } from "@shared/relayProvider";
|
||||||
|
import { useOnboarding } from "@stores/onboarding";
|
||||||
|
import { Body, fetch } from "@tauri-apps/api/http";
|
||||||
|
import { useAccount } from "@utils/hooks/useAccount";
|
||||||
|
import { useContext, useState } from "react";
|
||||||
|
|
||||||
|
export function CreateStep3Screen() {
|
||||||
|
const ndk = useContext(RelayContext);
|
||||||
|
const profile = useOnboarding((state: any) => state.profile);
|
||||||
|
const { account } = useAccount();
|
||||||
|
|
||||||
|
const [username, setUsername] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const createNIP05 = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const response = await fetch("https://lume.nu/api/user-create", {
|
||||||
|
method: "POST",
|
||||||
|
timeout: 30,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
|
},
|
||||||
|
body: Body.json({
|
||||||
|
username: username,
|
||||||
|
pubkey: account.pubkey,
|
||||||
|
lightningAddress: "",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = { ...profile, nip05: `${username}@lume.nu` };
|
||||||
|
|
||||||
|
const signer = new NDKPrivateKeySigner(account.privkey);
|
||||||
|
ndk.signer = signer;
|
||||||
|
|
||||||
|
const event = new NDKEvent(ndk);
|
||||||
|
// build event
|
||||||
|
event.content = JSON.stringify(data);
|
||||||
|
event.kind = 0;
|
||||||
|
event.pubkey = account.pubkey;
|
||||||
|
event.tags = [];
|
||||||
|
// publish event
|
||||||
|
event.publish();
|
||||||
|
|
||||||
|
// redirect to step 4
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setLoading(false);
|
||||||
|
console.error("Error:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto w-full max-w-md">
|
||||||
|
<div className="mb-8 text-center">
|
||||||
|
<h1 className="text-xl font-semibold text-zinc-100">
|
||||||
|
Create your Lume ID
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div className="w-full flex flex-col justify-center items-center gap-4">
|
||||||
|
<div className="w-full inline-flex items-center justify-center gap-2 rounded-lg bg-zinc-800">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
autoCapitalize="false"
|
||||||
|
autoCorrect="none"
|
||||||
|
spellCheck="false"
|
||||||
|
placeholder="satoshi"
|
||||||
|
className="relative w-full py-3 pl-3.5 !outline-none placeholder:text-zinc-500 bg-transparent text-zinc-100"
|
||||||
|
/>
|
||||||
|
<span className="text-fuchsia-500 font-semibold pr-3.5">
|
||||||
|
@lume.nu
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
preset="large"
|
||||||
|
onClick={() => createNIP05()}
|
||||||
|
disabled={username.length === 0}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
|
||||||
|
) : (
|
||||||
|
"Continue →"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
import { User } from "@app/auth/components/user";
|
import { User } from "@app/auth/components/user";
|
||||||
|
import { updateAccount } from "@libs/storage";
|
||||||
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
||||||
import { CheckCircleIcon, LoaderIcon } from "@shared/icons";
|
import { CheckCircleIcon, LoaderIcon } from "@shared/icons";
|
||||||
import { RelayContext } from "@shared/relayProvider";
|
import { RelayContext } from "@shared/relayProvider";
|
||||||
import { useActiveAccount } from "@stores/accounts";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { useAccount } from "@utils/hooks/useAccount";
|
||||||
import { arrayToNIP02 } from "@utils/transform";
|
import { arrayToNIP02 } from "@utils/transform";
|
||||||
import { useContext, useState } from "react";
|
import { useContext, useState } from "react";
|
||||||
import { navigate } from "vite-plugin-ssr/client/router";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
const initialList = [
|
const initialList = [
|
||||||
{
|
{
|
||||||
@@ -106,15 +108,15 @@ const initialList = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function Page() {
|
export function CreateStep4Screen() {
|
||||||
const ndk = useContext(RelayContext);
|
const ndk = useContext(RelayContext);
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [follows, setFollows] = useState([]);
|
const [follows, setFollows] = useState([]);
|
||||||
const [account, updateFollows] = useActiveAccount((state: any) => [
|
|
||||||
state.account,
|
const { account } = useAccount();
|
||||||
state.updateFollows,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// toggle follow state
|
// toggle follow state
|
||||||
const toggleFollow = (pubkey: string) => {
|
const toggleFollow = (pubkey: string) => {
|
||||||
@@ -124,6 +126,16 @@ export function Page() {
|
|||||||
setFollows(arr);
|
setFollows(arr);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const update = useMutation({
|
||||||
|
mutationFn: (follows: any) =>
|
||||||
|
updateAccount("follows", follows, account.pubkey),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["currentAccount"] });
|
||||||
|
// redirect to next step
|
||||||
|
navigate("/auth/onboarding", { replace: true });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// save follows to database then broadcast
|
// save follows to database then broadcast
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -142,75 +154,64 @@ export function Page() {
|
|||||||
// publish event
|
// publish event
|
||||||
event.publish();
|
event.publish();
|
||||||
|
|
||||||
// update account follows
|
// update
|
||||||
updateFollows(follows);
|
update.mutate(follows);
|
||||||
|
|
||||||
// redirect to onboarding
|
|
||||||
setTimeout(
|
|
||||||
() =>
|
|
||||||
navigate("/app/onboarding", {
|
|
||||||
overwriteLastHistoryEntry: true,
|
|
||||||
}),
|
|
||||||
2000,
|
|
||||||
);
|
|
||||||
} catch {
|
} catch {
|
||||||
console.log("error");
|
console.log("error");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
<div className="mx-auto w-full max-w-md">
|
||||||
<div className="mx-auto w-full max-w-md">
|
<div className="mb-8 text-center">
|
||||||
<div className="mb-8 text-center">
|
<h1 className="text-xl font-semibold text-zinc-100">
|
||||||
<h1 className="text-xl font-semibold text-zinc-100">
|
Personalized your newsfeed
|
||||||
Personalized your newsfeed
|
</h1>
|
||||||
</h1>
|
</div>
|
||||||
</div>
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="w-full rounded-xl border-t border-zinc-800/50 bg-zinc-900 overflow-hidden">
|
||||||
<div className="w-full rounded-xl border-t border-zinc-800/50 bg-zinc-900 overflow-hidden">
|
<div className="inline-flex h-10 w-full items-center gap-1 border-b border-zinc-800 px-4 text-base font-medium text-zinc-400">
|
||||||
<div className="inline-flex h-10 w-full items-center gap-1 border-b border-zinc-800 px-4 text-base font-medium text-zinc-400">
|
Follow at least
|
||||||
Follow at least
|
<span className="text-fuchsia-500 font-semibold">
|
||||||
<span className="text-fuchsia-500 font-semibold">
|
{follows.length}/10
|
||||||
{follows.length}/10
|
</span>{" "}
|
||||||
</span>{" "}
|
plebs
|
||||||
plebs
|
</div>
|
||||||
</div>
|
<div className="scrollbar-hide flex h-96 flex-col overflow-y-auto py-2">
|
||||||
<div className="scrollbar-hide flex h-96 flex-col overflow-y-auto py-2">
|
{initialList.map((item: { pubkey: string }, index: number) => (
|
||||||
{initialList.map((item: { pubkey: string }, index: number) => (
|
<button
|
||||||
<button
|
key={`item-${index}`}
|
||||||
key={`item-${index}`}
|
type="button"
|
||||||
type="button"
|
onClick={() => toggleFollow(item.pubkey)}
|
||||||
onClick={() => toggleFollow(item.pubkey)}
|
className="inline-flex transform items-center justify-between bg-zinc-900 px-4 py-2 hover:bg-zinc-800 active:translate-y-1"
|
||||||
className="inline-flex transform items-center justify-between bg-zinc-900 px-4 py-2 hover:bg-zinc-800 active:translate-y-1"
|
>
|
||||||
>
|
<User pubkey={item.pubkey} />
|
||||||
<User pubkey={item.pubkey} />
|
{follows.includes(item.pubkey) && (
|
||||||
{follows.includes(item.pubkey) && (
|
<div>
|
||||||
<div>
|
<CheckCircleIcon
|
||||||
<CheckCircleIcon
|
width={16}
|
||||||
width={16}
|
height={16}
|
||||||
height={16}
|
className="text-green-400"
|
||||||
className="text-green-400"
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</button>
|
||||||
</button>
|
))}
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{follows.length >= 10 && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => submit()}
|
|
||||||
className="inline-flex items-center justify-center h-11 w-full bg-fuchsia-500 rounded-md font-medium text-zinc-100 hover:bg-fuchsia-600"
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
|
|
||||||
) : (
|
|
||||||
"Finish →"
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
{follows.length >= 10 && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => submit()}
|
||||||
|
className="inline-flex items-center justify-center h-11 w-full bg-fuchsia-500 rounded-md font-medium text-zinc-100 hover:bg-fuchsia-600"
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
|
||||||
|
) : (
|
||||||
|
"Finish →"
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
9
src/app/auth/import/index.tsx
Normal file
9
src/app/auth/import/index.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { Outlet } from "react-router-dom";
|
||||||
|
|
||||||
|
export function AuthImportScreen() {
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
111
src/app/auth/import/step-1.tsx
Normal file
111
src/app/auth/import/step-1.tsx
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import { createAccount } from "@libs/storage";
|
||||||
|
import { LoaderIcon } from "@shared/icons";
|
||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { getPublicKey, nip19 } from "nostr-tools";
|
||||||
|
import { Resolver, useForm } from "react-hook-form";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
type FormValues = {
|
||||||
|
key: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolver: Resolver<FormValues> = async (values) => {
|
||||||
|
return {
|
||||||
|
values: values.key ? values : {},
|
||||||
|
errors: !values.key
|
||||||
|
? {
|
||||||
|
key: {
|
||||||
|
type: "required",
|
||||||
|
message: "This is required.",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ImportStep1Screen() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const account = useMutation({
|
||||||
|
mutationFn: (data: any) =>
|
||||||
|
createAccount(data.npub, data.pubkey, data.privkey, null, 1),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["currentAccount"] });
|
||||||
|
// redirect to next step
|
||||||
|
navigate("/auth/import/step-2", { replace: true });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
setError,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { errors, isDirty, isValid, isSubmitting },
|
||||||
|
} = useForm<FormValues>({ resolver });
|
||||||
|
|
||||||
|
const onSubmit = async (data: any) => {
|
||||||
|
try {
|
||||||
|
let privkey = data["key"];
|
||||||
|
|
||||||
|
if (privkey.substring(0, 4) === "nsec") {
|
||||||
|
privkey = nip19.decode(privkey).data;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof getPublicKey(privkey) === "string") {
|
||||||
|
const pubkey = getPublicKey(privkey);
|
||||||
|
const npub = nip19.npubEncode(pubkey);
|
||||||
|
|
||||||
|
// update
|
||||||
|
account.mutate({
|
||||||
|
npub,
|
||||||
|
pubkey,
|
||||||
|
privkey,
|
||||||
|
follows: null,
|
||||||
|
is_active: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setError("key", {
|
||||||
|
type: "custom",
|
||||||
|
message: "Private Key is invalid, please check again",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto w-full max-w-md">
|
||||||
|
<div className="mb-8 text-center">
|
||||||
|
<h1 className="text-xl font-semibold text-zinc-100">Import your key</h1>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-3">
|
||||||
|
<div className="flex flex-col gap-0.5">
|
||||||
|
<input
|
||||||
|
{...register("key", { required: true, minLength: 32 })}
|
||||||
|
type={"password"}
|
||||||
|
placeholder="Paste private key here..."
|
||||||
|
className="relative w-full rounded-lg px-3 py-3 !outline-none bg-zinc-800 text-zinc-100 placeholder:text-zinc-500"
|
||||||
|
/>
|
||||||
|
<span className="text-base text-red-400">
|
||||||
|
{errors.key && <p>{errors.key.message}</p>}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={!isDirty || !isValid}
|
||||||
|
className="inline-flex items-center justify-center h-11 w-full bg-fuchsia-500 rounded-md font-medium text-zinc-100 hover:bg-fuchsia-600"
|
||||||
|
>
|
||||||
|
{isSubmitting ? (
|
||||||
|
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
|
||||||
|
) : (
|
||||||
|
"Continue →"
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
81
src/app/auth/import/step-2.tsx
Normal file
81
src/app/auth/import/step-2.tsx
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import { User } from "@app/auth/components/user";
|
||||||
|
import { updateAccount } from "@libs/storage";
|
||||||
|
import { Button } from "@shared/button";
|
||||||
|
import { LoaderIcon } from "@shared/icons";
|
||||||
|
import { RelayContext } from "@shared/relayProvider";
|
||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { useAccount } from "@utils/hooks/useAccount";
|
||||||
|
import { setToArray } from "@utils/transform";
|
||||||
|
import { useContext, useState } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
export function ImportStep2Screen() {
|
||||||
|
const ndk = useContext(RelayContext);
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const { status, account } = useAccount();
|
||||||
|
|
||||||
|
const update = useMutation({
|
||||||
|
mutationFn: (follows: any) =>
|
||||||
|
updateAccount("follows", follows, account.pubkey),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["currentAccount"] });
|
||||||
|
// redirect to next step
|
||||||
|
navigate("/auth/onboarding", { replace: true });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
try {
|
||||||
|
// show loading indicator
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const user = ndk.getUser({ hexpubkey: account.pubkey });
|
||||||
|
const follows = await user.follows();
|
||||||
|
|
||||||
|
// follows as list
|
||||||
|
const followsList = setToArray(follows);
|
||||||
|
|
||||||
|
// update
|
||||||
|
update.mutate(followsList);
|
||||||
|
} catch {
|
||||||
|
console.log("error");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto w-full max-w-md">
|
||||||
|
<div className="mb-8 text-center">
|
||||||
|
<h1 className="text-xl font-semibold">
|
||||||
|
{loading ? "Creating..." : "Continue with"}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div className="w-full rounded-xl border-t border-zinc-800/50 bg-zinc-900 p-4">
|
||||||
|
{status === "loading" ? (
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="h-11 w-11 animate-pulse rounded-lg bg-zinc-800" />
|
||||||
|
<div>
|
||||||
|
<h3 className="mb-1 h-4 w-16 animate-pulse rounded bg-zinc-800" />
|
||||||
|
<p className="h-3 w-36 animate-pulse rounded bg-zinc-800" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<User pubkey={account.pubkey} />
|
||||||
|
<Button preset="large" onClick={() => submit()}>
|
||||||
|
{loading ? (
|
||||||
|
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
|
||||||
|
) : (
|
||||||
|
"Continue →"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
import { ArrowLeftIcon, ArrowRightIcon } from "@shared/icons";
|
|
||||||
import useSWR from "swr";
|
|
||||||
|
|
||||||
const fetcher = async () => {
|
|
||||||
const { platform } = await import("@tauri-apps/api/os");
|
|
||||||
return await platform();
|
|
||||||
};
|
|
||||||
|
|
||||||
export function LayoutOnboarding({ children }: { children: React.ReactNode }) {
|
|
||||||
const { data: platform } = useSWR("platform", fetcher);
|
|
||||||
|
|
||||||
const goBack = () => {
|
|
||||||
window.history.back();
|
|
||||||
};
|
|
||||||
|
|
||||||
const goForward = () => {
|
|
||||||
window.history.forward();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="h-screen w-screen bg-zinc-50 text-zinc-900 dark:bg-zinc-950 dark:text-zinc-100">
|
|
||||||
<div className="flex h-screen w-full flex-col">
|
|
||||||
<div
|
|
||||||
data-tauri-drag-region
|
|
||||||
className="relative h-9 shrink-0 border border-zinc-100 bg-white dark:border-zinc-900 dark:bg-black"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
data-tauri-drag-region
|
|
||||||
className="flex h-full w-full flex-1 items-center px-2"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`flex h-full items-center gap-2 ${
|
|
||||||
platform === "darwin" ? "pl-[68px]" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => goBack()}
|
|
||||||
className="group inline-flex h-6 w-6 items-center justify-center rounded-md hover:bg-zinc-900"
|
|
||||||
>
|
|
||||||
<ArrowLeftIcon
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
className="text-zinc-500 group-hover:text-zinc-300"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => goForward()}
|
|
||||||
className="group inline-flex h-6 w-6 items-center justify-center rounded-md hover:bg-zinc-900"
|
|
||||||
>
|
|
||||||
<ArrowRightIcon
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
className="text-zinc-500 group-hover:text-zinc-300"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="relative flex min-h-0 w-full flex-1">{children}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,26 +1,17 @@
|
|||||||
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
||||||
import { ArrowRightCircleIcon } from "@shared/icons/arrowRightCircle";
|
import { ArrowRightCircleIcon } from "@shared/icons/arrowRightCircle";
|
||||||
import { Link } from "@shared/link";
|
|
||||||
import { RelayContext } from "@shared/relayProvider";
|
import { RelayContext } from "@shared/relayProvider";
|
||||||
import { User } from "@shared/user";
|
import { User } from "@shared/user";
|
||||||
import { useActiveAccount } from "@stores/accounts";
|
|
||||||
import { dateToUnix } from "@utils/date";
|
import { dateToUnix } from "@utils/date";
|
||||||
import { useContext, useEffect } from "react";
|
import { useAccount } from "@utils/hooks/useAccount";
|
||||||
import { navigate } from "vite-plugin-ssr/client/router";
|
import { useContext } from "react";
|
||||||
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
export function Page() {
|
export function OnboardingScreen() {
|
||||||
const ndk = useContext(RelayContext);
|
const ndk = useContext(RelayContext);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [account, fetchAccount] = useActiveAccount((state: any) => [
|
const { status, account } = useAccount();
|
||||||
state.account,
|
|
||||||
state.fetch,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (account === null) {
|
|
||||||
fetchAccount();
|
|
||||||
}
|
|
||||||
}, [fetchAccount]);
|
|
||||||
|
|
||||||
const publish = async () => {
|
const publish = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -39,10 +30,7 @@ export function Page() {
|
|||||||
event.publish();
|
event.publish();
|
||||||
|
|
||||||
// redirect to home
|
// redirect to home
|
||||||
setTimeout(
|
navigate("/", { replace: true });
|
||||||
() => navigate("/", { overwriteLastHistoryEntry: true }),
|
|
||||||
2000,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
}
|
}
|
||||||
@@ -64,7 +52,7 @@ export function Page() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="w-full border-t border-zinc-800/50 bg-zinc-900 rounded-xl">
|
<div className="w-full border-t border-zinc-800/50 bg-zinc-900 rounded-xl">
|
||||||
<div className="h-min w-full px-5 py-3">
|
<div className="h-min w-full px-5 py-3">
|
||||||
{account && (
|
{status === "success" && (
|
||||||
<User
|
<User
|
||||||
pubkey={account.pubkey}
|
pubkey={account.pubkey}
|
||||||
time={Math.floor(Date.now() / 1000)}
|
time={Math.floor(Date.now() / 1000)}
|
||||||
@@ -97,7 +85,7 @@ export function Page() {
|
|||||||
<ArrowRightCircleIcon className="w-5 h-5" />
|
<ArrowRightCircleIcon className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
<Link
|
<Link
|
||||||
href="/"
|
to="/"
|
||||||
className="inline-flex h-10 w-full items-center justify-center gap-2 rounded-lg px-6 text-sm font-medium text-zinc-200"
|
className="inline-flex h-10 w-full items-center justify-center gap-2 rounded-lg px-6 text-sm font-medium text-zinc-200"
|
||||||
>
|
>
|
||||||
Skip for now
|
Skip for now
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
import { Button } from "@shared/button";
|
|
||||||
import { EyeOffIcon, EyeOnIcon } from "@shared/icons";
|
|
||||||
import { useActiveAccount } from "@stores/accounts";
|
|
||||||
import { generatePrivateKey, getPublicKey, nip19 } from "nostr-tools";
|
|
||||||
import { useMemo, useState } from "react";
|
|
||||||
import { navigate } from "vite-plugin-ssr/client/router";
|
|
||||||
|
|
||||||
export function Page() {
|
|
||||||
const createAccount = useActiveAccount((state: any) => state.create);
|
|
||||||
|
|
||||||
const [type, setType] = useState("password");
|
|
||||||
const privkey = useMemo(() => generatePrivateKey(), []);
|
|
||||||
|
|
||||||
const pubkey = getPublicKey(privkey);
|
|
||||||
const npub = nip19.npubEncode(pubkey);
|
|
||||||
const nsec = nip19.nsecEncode(privkey);
|
|
||||||
|
|
||||||
// toggle private key
|
|
||||||
const showPrivateKey = () => {
|
|
||||||
if (type === "password") {
|
|
||||||
setType("text");
|
|
||||||
} else {
|
|
||||||
setType("password");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const submit = async () => {
|
|
||||||
createAccount(npub, pubkey, privkey, null, 1);
|
|
||||||
navigate("/app/auth/create/step-2");
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
|
||||||
<div className="mx-auto w-full max-w-md">
|
|
||||||
<div className="mb-8 text-center">
|
|
||||||
<h1 className="text-xl font-semibold text-zinc-100">
|
|
||||||
Lume is auto-generated key for you
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<label className="text-base font-semibold text-zinc-400">
|
|
||||||
Public Key
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
readOnly
|
|
||||||
value={npub}
|
|
||||||
className="relative w-full rounded-lg py-3 pl-3.5 pr-11 !outline-none placeholder:text-zinc-400 bg-zinc-800 text-zinc-100"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<label className="text-base font-semibold text-zinc-400">
|
|
||||||
Private Key
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<input
|
|
||||||
readOnly
|
|
||||||
type={type}
|
|
||||||
value={nsec}
|
|
||||||
className="relative w-full rounded-lg py-3 pl-3.5 pr-11 !outline-none placeholder:text-zinc-400 bg-zinc-800 text-zinc-100"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => showPrivateKey()}
|
|
||||||
className="group absolute right-2 top-1/2 -translate-y-1/2 transform rounded p-1 hover:bg-zinc-700"
|
|
||||||
>
|
|
||||||
{type === "password" ? (
|
|
||||||
<EyeOffIcon
|
|
||||||
width={20}
|
|
||||||
height={20}
|
|
||||||
className="text-zinc-500 group-hover:text-zinc-100"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<EyeOnIcon
|
|
||||||
width={20}
|
|
||||||
height={20}
|
|
||||||
className="text-zinc-500 group-hover:text-zinc-100"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button preset="large" onClick={() => submit()}>
|
|
||||||
Continue →
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
import { AvatarUploader } from "@shared/avatarUploader";
|
|
||||||
import { LoaderIcon } from "@shared/icons";
|
|
||||||
import { Image } from "@shared/image";
|
|
||||||
import { useActiveAccount } from "@stores/accounts";
|
|
||||||
import { DEFAULT_AVATAR } from "@stores/constants";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { navigate } from "vite-plugin-ssr/client/router";
|
|
||||||
|
|
||||||
export function Page() {
|
|
||||||
const createTempProfile = useActiveAccount(
|
|
||||||
(state: any) => state.createTempProfile,
|
|
||||||
);
|
|
||||||
|
|
||||||
const [image, setImage] = useState(DEFAULT_AVATAR);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
setValue,
|
|
||||||
formState: { isDirty, isValid },
|
|
||||||
} = useForm();
|
|
||||||
|
|
||||||
const onSubmit = (data: any) => {
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const profile = { ...data, name: data.displayName };
|
|
||||||
createTempProfile(profile);
|
|
||||||
// redirect to step 3
|
|
||||||
navigate("/app/auth/create/step-3", {
|
|
||||||
overwriteLastHistoryEntry: true,
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
console.log("error");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setValue("picture", image);
|
|
||||||
}, [setValue, image]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
|
||||||
<div className="mx-auto w-full max-w-md">
|
|
||||||
<div className="mb-8 text-center">
|
|
||||||
<h1 className="text-xl font-semibold text-zinc-100">
|
|
||||||
Create your profile
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
<div className="w-full rounded-xl border-t border-zinc-800/50 bg-zinc-900 p-5">
|
|
||||||
<form
|
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
|
||||||
className="flex flex-col gap-4"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type={"hidden"}
|
|
||||||
{...register("picture")}
|
|
||||||
value={image}
|
|
||||||
className="relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-100 dark:shadow-black/10 dark:placeholder:text-zinc-500"
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<label className="text-sm font-semibold uppercase tracking-wider text-zinc-400">
|
|
||||||
Avatar
|
|
||||||
</label>
|
|
||||||
<div className="relative inline-flex h-36 w-full items-center justify-center overflow-hidden rounded-lg border border-zinc-900 bg-zinc-950">
|
|
||||||
<Image
|
|
||||||
src={image}
|
|
||||||
fallback={DEFAULT_AVATAR}
|
|
||||||
alt="avatar"
|
|
||||||
className="relative z-10 h-11 w-11 rounded-md"
|
|
||||||
/>
|
|
||||||
<div className="absolute bottom-3 right-3 z-10">
|
|
||||||
<AvatarUploader valueState={setImage} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<label className="text-sm font-semibold uppercase tracking-wider text-zinc-400">
|
|
||||||
Display Name *
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type={"text"}
|
|
||||||
{...register("displayName", {
|
|
||||||
required: true,
|
|
||||||
minLength: 4,
|
|
||||||
})}
|
|
||||||
spellCheck={false}
|
|
||||||
className="relative h-10 w-full rounded-lg px-3 py-2 !outline-none bg-zinc-800 text-zinc-100 placeholder:text-zinc-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<label className="text-sm font-semibold uppercase tracking-wider text-zinc-400">
|
|
||||||
Bio
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
{...register("about")}
|
|
||||||
spellCheck={false}
|
|
||||||
className="resize-none relative h-20 w-full rounded-lg px-3 py-2 !outline-none bg-zinc-800 text-zinc-100 placeholder:text-zinc-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={!isDirty || !isValid}
|
|
||||||
className="inline-flex items-center justify-center h-11 w-full bg-fuchsia-500 rounded-md font-medium text-zinc-100 hover:bg-fuchsia-600"
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
|
|
||||||
) : (
|
|
||||||
"Continue →"
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
|
||||||
import { Button } from "@shared/button";
|
|
||||||
import { LoaderIcon } from "@shared/icons";
|
|
||||||
import { RelayContext } from "@shared/relayProvider";
|
|
||||||
import { useActiveAccount } from "@stores/accounts";
|
|
||||||
import { Body, fetch } from "@tauri-apps/api/http";
|
|
||||||
import { useContext, useState } from "react";
|
|
||||||
import { navigate } from "vite-plugin-ssr/client/router";
|
|
||||||
|
|
||||||
export function Page() {
|
|
||||||
const ndk = useContext(RelayContext);
|
|
||||||
const [account, tempProfile] = useActiveAccount((state: any) => [
|
|
||||||
state.account,
|
|
||||||
state.tempProfile,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const [username, setUsername] = useState("");
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const createNIP05 = async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
const response = await fetch("https://lume.nu/api/user-create", {
|
|
||||||
method: "POST",
|
|
||||||
timeout: 30,
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json; charset=utf-8",
|
|
||||||
},
|
|
||||||
body: Body.json({
|
|
||||||
username: username,
|
|
||||||
pubkey: account.pubkey,
|
|
||||||
lightningAddress: "",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const profile = { ...tempProfile, nip05: `${username}@lume.nu` };
|
|
||||||
|
|
||||||
const signer = new NDKPrivateKeySigner(account.privkey);
|
|
||||||
ndk.signer = signer;
|
|
||||||
|
|
||||||
const event = new NDKEvent(ndk);
|
|
||||||
// build event
|
|
||||||
event.content = JSON.stringify(profile);
|
|
||||||
event.kind = 0;
|
|
||||||
event.pubkey = account.pubkey;
|
|
||||||
event.tags = [];
|
|
||||||
// publish event
|
|
||||||
event.publish();
|
|
||||||
|
|
||||||
// redirect to step 4
|
|
||||||
navigate("/app/auth/create/step-4", {
|
|
||||||
overwriteLastHistoryEntry: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
setLoading(false);
|
|
||||||
console.error("Error:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
|
||||||
<div className="mx-auto w-full max-w-md">
|
|
||||||
<div className="mb-8 text-center">
|
|
||||||
<h1 className="text-xl font-semibold text-zinc-100">
|
|
||||||
Create your Lume ID
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
<div className="w-full flex flex-col justify-center items-center gap-4">
|
|
||||||
<div className="w-full inline-flex items-center justify-center gap-2 rounded-lg bg-zinc-800">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={username}
|
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
|
||||||
autoCapitalize="false"
|
|
||||||
autoCorrect="none"
|
|
||||||
spellCheck="false"
|
|
||||||
placeholder="satoshi"
|
|
||||||
className="relative w-full py-3 pl-3.5 !outline-none placeholder:text-zinc-500 bg-transparent text-zinc-100"
|
|
||||||
/>
|
|
||||||
<span className="text-fuchsia-500 font-semibold pr-3.5">
|
|
||||||
@lume.nu
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
preset="large"
|
|
||||||
onClick={() => createNIP05()}
|
|
||||||
disabled={username.length === 0}
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
|
|
||||||
) : (
|
|
||||||
"Continue →"
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
import { LoaderIcon } from "@shared/icons";
|
|
||||||
import { useActiveAccount } from "@stores/accounts";
|
|
||||||
import { getPublicKey, nip19 } from "nostr-tools";
|
|
||||||
import { Resolver, useForm } from "react-hook-form";
|
|
||||||
import { navigate } from "vite-plugin-ssr/client/router";
|
|
||||||
|
|
||||||
type FormValues = {
|
|
||||||
key: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const resolver: Resolver<FormValues> = async (values) => {
|
|
||||||
return {
|
|
||||||
values: values.key ? values : {},
|
|
||||||
errors: !values.key
|
|
||||||
? {
|
|
||||||
key: {
|
|
||||||
type: "required",
|
|
||||||
message: "This is required.",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export function Page() {
|
|
||||||
const createAccount = useActiveAccount((state: any) => state.create);
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
setError,
|
|
||||||
handleSubmit,
|
|
||||||
formState: { errors, isDirty, isValid, isSubmitting },
|
|
||||||
} = useForm<FormValues>({ resolver });
|
|
||||||
|
|
||||||
const onSubmit = async (data: any) => {
|
|
||||||
try {
|
|
||||||
let privkey = data["key"];
|
|
||||||
|
|
||||||
if (privkey.substring(0, 4) === "nsec") {
|
|
||||||
privkey = nip19.decode(privkey).data;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof getPublicKey(privkey) === "string") {
|
|
||||||
const pubkey = getPublicKey(privkey);
|
|
||||||
const npub = nip19.npubEncode(pubkey);
|
|
||||||
|
|
||||||
createAccount(npub, pubkey, privkey, null, 1);
|
|
||||||
navigate("/app/auth/import/step-2");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
setError("key", {
|
|
||||||
type: "custom",
|
|
||||||
message: "Private Key is invalid, please check again",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
|
||||||
<div className="mx-auto w-full max-w-md">
|
|
||||||
<div className="mb-8 text-center">
|
|
||||||
<h1 className="text-xl font-semibold text-zinc-100">
|
|
||||||
Import your key
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<form
|
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
|
||||||
className="flex flex-col gap-3"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-0.5">
|
|
||||||
<input
|
|
||||||
{...register("key", { required: true, minLength: 32 })}
|
|
||||||
type={"password"}
|
|
||||||
placeholder="Paste private key here..."
|
|
||||||
className="relative w-full rounded-lg px-3 py-3 !outline-none bg-zinc-800 text-zinc-100 placeholder:text-zinc-500"
|
|
||||||
/>
|
|
||||||
<span className="text-base text-red-400">
|
|
||||||
{errors.key && <p>{errors.key.message}</p>}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-center">
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={!isDirty || !isValid}
|
|
||||||
className="inline-flex items-center justify-center h-11 w-full bg-fuchsia-500 rounded-md font-medium text-zinc-100 hover:bg-fuchsia-600"
|
|
||||||
>
|
|
||||||
{isSubmitting ? (
|
|
||||||
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
|
|
||||||
) : (
|
|
||||||
"Continue →"
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
import { User } from "@app/auth/components/user";
|
|
||||||
import { Button } from "@shared/button";
|
|
||||||
import { LoaderIcon } from "@shared/icons";
|
|
||||||
import { RelayContext } from "@shared/relayProvider";
|
|
||||||
import { useActiveAccount } from "@stores/accounts";
|
|
||||||
import { setToArray } from "@utils/transform";
|
|
||||||
import { useContext, useState } from "react";
|
|
||||||
import { navigate } from "vite-plugin-ssr/client/router";
|
|
||||||
|
|
||||||
export function Page() {
|
|
||||||
const ndk = useContext(RelayContext);
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [account, updateFollows] = useActiveAccount((state: any) => [
|
|
||||||
state.account,
|
|
||||||
state.updateFollows,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const submit = async () => {
|
|
||||||
// show loading indicator
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const user = ndk.getUser({ hexpubkey: account.pubkey });
|
|
||||||
const follows = await user.follows();
|
|
||||||
|
|
||||||
// follows as list
|
|
||||||
const followsList = setToArray(follows);
|
|
||||||
|
|
||||||
// update account follows in store
|
|
||||||
updateFollows(followsList);
|
|
||||||
|
|
||||||
// redirect to onboarding
|
|
||||||
setTimeout(
|
|
||||||
() => navigate("/app/onboarding", { overwriteLastHistoryEntry: true }),
|
|
||||||
2000,
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
console.log("error");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
|
||||||
<div className="mx-auto w-full max-w-md">
|
|
||||||
<div className="mb-8 text-center">
|
|
||||||
<h1 className="text-xl font-semibold">
|
|
||||||
{loading ? "Creating..." : "Continue with"}
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
<div className="w-full rounded-xl border-t border-zinc-800/50 bg-zinc-900 p-4">
|
|
||||||
{!account ? (
|
|
||||||
<div className="w-full">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="h-11 w-11 animate-pulse rounded-lg bg-zinc-800" />
|
|
||||||
<div>
|
|
||||||
<h3 className="mb-1 h-4 w-16 animate-pulse rounded bg-zinc-800" />
|
|
||||||
<p className="h-3 w-36 animate-pulse rounded bg-zinc-800" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
<User pubkey={account.pubkey} />
|
|
||||||
<Button preset="large" onClick={() => submit()}>
|
|
||||||
{loading ? (
|
|
||||||
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
|
|
||||||
) : (
|
|
||||||
"Continue →"
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ArrowRightCircleIcon } from "@shared/icons/arrowRightCircle";
|
import { ArrowRightCircleIcon } from "@shared/icons/arrowRightCircle";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
export function Page() {
|
export function WelcomeScreen() {
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full grid grid-cols-12 gap-4 px-4 py-4">
|
<div className="w-full h-full grid grid-cols-12 gap-4 px-4 py-4">
|
||||||
<div className="col-span-5 border-t border-zinc-800/50 bg-zinc-900 rounded-xl flex flex-col">
|
<div className="col-span-5 border-t border-zinc-800/50 bg-zinc-900 rounded-xl flex flex-col">
|
||||||
@@ -19,20 +20,20 @@ export function Page() {
|
|||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-auto w-full flex flex-col gap-2 px-4 py-4">
|
<div className="mt-auto w-full flex flex-col gap-2 px-4 py-4">
|
||||||
<a
|
<Link
|
||||||
href="/app/auth/import"
|
to="/auth/import"
|
||||||
className="inline-flex h-12 w-full items-center justify-between gap-2 rounded-lg px-6 font-medium text-zinc-100 bg-fuchsia-500 hover:bg-fuchsia-600"
|
className="inline-flex h-12 w-full items-center justify-between gap-2 rounded-lg px-6 font-medium text-zinc-100 bg-fuchsia-500 hover:bg-fuchsia-600"
|
||||||
>
|
>
|
||||||
<span className="w-5" />
|
<span className="w-5" />
|
||||||
<span>Login with private key</span>
|
<span>Login with private key</span>
|
||||||
<ArrowRightCircleIcon className="w-5 h-5" />
|
<ArrowRightCircleIcon className="w-5 h-5" />
|
||||||
</a>
|
</Link>
|
||||||
<a
|
<Link
|
||||||
href="/app/auth/create"
|
to="/auth/create"
|
||||||
className="inline-flex h-12 w-full items-center justify-center gap-2 rounded-lg px-6 font-medium text-zinc-200 bg-zinc-800 hover:bg-zinc-700"
|
className="inline-flex h-12 w-full items-center justify-center gap-2 rounded-lg px-6 font-medium text-zinc-200 bg-zinc-800 hover:bg-zinc-700"
|
||||||
>
|
>
|
||||||
Create new key
|
Create new key
|
||||||
</a>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { DefaultLayout as Layout } from "@shared/layout";
|
|
||||||
@@ -5,20 +5,24 @@ import { AvatarUploader } from "@shared/avatarUploader";
|
|||||||
import { CancelIcon, LoaderIcon, PlusIcon } from "@shared/icons";
|
import { CancelIcon, LoaderIcon, PlusIcon } from "@shared/icons";
|
||||||
import { Image } from "@shared/image";
|
import { Image } from "@shared/image";
|
||||||
import { RelayContext } from "@shared/relayProvider";
|
import { RelayContext } from "@shared/relayProvider";
|
||||||
import { useActiveAccount } from "@stores/accounts";
|
|
||||||
import { DEFAULT_AVATAR } from "@stores/constants";
|
import { DEFAULT_AVATAR } from "@stores/constants";
|
||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { dateToUnix } from "@utils/date";
|
import { dateToUnix } from "@utils/date";
|
||||||
|
import { useAccount } from "@utils/hooks/useAccount";
|
||||||
import { Fragment, useContext, useEffect, useState } from "react";
|
import { Fragment, useContext, useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { navigate } from "vite-plugin-ssr/client/router";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
export function ChannelCreateModal() {
|
export function ChannelCreateModal() {
|
||||||
const ndk = useContext(RelayContext);
|
const ndk = useContext(RelayContext);
|
||||||
const account = useActiveAccount((state: any) => state.account);
|
const queryClient = useQueryClient();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [image, setImage] = useState(DEFAULT_AVATAR);
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [image, setImage] = useState(DEFAULT_AVATAR);
|
||||||
|
|
||||||
|
const { account } = useAccount();
|
||||||
|
|
||||||
const closeModal = () => {
|
const closeModal = () => {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
@@ -36,6 +40,21 @@ export function ChannelCreateModal() {
|
|||||||
formState: { isDirty, isValid },
|
formState: { isDirty, isValid },
|
||||||
} = useForm();
|
} = useForm();
|
||||||
|
|
||||||
|
const addChannel = useMutation({
|
||||||
|
mutationFn: (event: any) =>
|
||||||
|
createChannel(
|
||||||
|
event.id,
|
||||||
|
event.pubkey,
|
||||||
|
event.name,
|
||||||
|
event.picture,
|
||||||
|
event.about,
|
||||||
|
event.created_at,
|
||||||
|
),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["channels"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const onSubmit = (data: any) => {
|
const onSubmit = (data: any) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
@@ -55,7 +74,12 @@ export function ChannelCreateModal() {
|
|||||||
event.publish();
|
event.publish();
|
||||||
|
|
||||||
// insert to database
|
// insert to database
|
||||||
createChannel(event.id, event.pubkey, event.content, event.created_at);
|
addChannel.mutate({
|
||||||
|
...event,
|
||||||
|
name: data.name,
|
||||||
|
picture: data.picture,
|
||||||
|
about: data.about,
|
||||||
|
});
|
||||||
|
|
||||||
// reset form
|
// reset form
|
||||||
reset();
|
reset();
|
||||||
@@ -64,7 +88,7 @@ export function ChannelCreateModal() {
|
|||||||
// close modal
|
// close modal
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
// redirect to channel page
|
// redirect to channel page
|
||||||
navigate(`/app/channel?id=${event.id}`);
|
navigate(`/app/channel/${event.id}`);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("error: ", e);
|
console.log("error: ", e);
|
||||||
|
|||||||
@@ -1,29 +1,20 @@
|
|||||||
import { useChannelProfile } from "@app/channel/hooks/useChannelProfile";
|
import { useChannelProfile } from "@app/channel/hooks/useChannelProfile";
|
||||||
import { Link } from "@shared/link";
|
import { NavLink } from "react-router-dom";
|
||||||
import { usePageContext } from "@utils/hooks/usePageContext";
|
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
export function ChannelsListItem({ data }: { data: any }) {
|
export function ChannelsListItem({ data }: { data: any }) {
|
||||||
const channel: any = useChannelProfile(data.event_id);
|
const channel = useChannelProfile(data.event_id);
|
||||||
const pageContext = usePageContext();
|
|
||||||
|
|
||||||
const searchParams: any = pageContext.urlParsed.search;
|
|
||||||
const pageID = searchParams.id;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<NavLink
|
||||||
href={`/app/channel?id=${data.event_id}`}
|
to={`/app/channel/${data.event_id}`}
|
||||||
className={twMerge(
|
className={({ isActive }) =>
|
||||||
"inline-flex h-9 items-center gap-2.5 rounded-md px-2.5",
|
twMerge(
|
||||||
pageID === data.event_id ? "bg-zinc-900 text-zinc-100" : "",
|
"inline-flex h-9 items-center gap-2.5 rounded-md px-2.5",
|
||||||
)}
|
isActive ? "bg-zinc-900/50 text-zinc-100" : "",
|
||||||
|
)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div className="inline-flex shrink-0 h-6 w-6 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
|
||||||
className={twMerge(
|
|
||||||
"inline-flex shrink-0 h-6 w-6 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900",
|
|
||||||
pageID === data.event_id ? "bg-zinc-800" : "",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span className="text-xs text-zinc-100">#</span>
|
<span className="text-xs text-zinc-100">#</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full inline-flex items-center justify-between">
|
<div className="w-full inline-flex items-center justify-between">
|
||||||
@@ -36,6 +27,6 @@ export function ChannelsListItem({ data }: { data: any }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</NavLink>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,36 @@
|
|||||||
import { ChannelCreateModal } from "@app/channel/components/createModal";
|
import { ChannelCreateModal } from "@app/channel/components/createModal";
|
||||||
import { ChannelsListItem } from "@app/channel/components/item";
|
import { ChannelsListItem } from "@app/channel/components/item";
|
||||||
import { useChannels } from "@stores/channels";
|
import { getChannels } from "@libs/storage";
|
||||||
import { useEffect } from "react";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
export function ChannelsList() {
|
export function ChannelsList() {
|
||||||
const channels = useChannels((state: any) => state.channels);
|
const {
|
||||||
const fetchChannels = useChannels((state: any) => state.fetch);
|
status,
|
||||||
|
data: channels,
|
||||||
useEffect(() => {
|
isFetching,
|
||||||
fetchChannels();
|
} = useQuery(
|
||||||
}, [fetchChannels]);
|
["channels"],
|
||||||
|
async () => {
|
||||||
|
return await getChannels();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
refetchOnMount: false,
|
||||||
|
refetchOnReconnect: false,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
{!channels ? (
|
{status === "loading" ? (
|
||||||
<>
|
<>
|
||||||
<div className="inline-flex h-9 items-center gap-2 rounded-md px-2.5">
|
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5">
|
||||||
<div className="relative h-5 w-5 shrink-0 animate-pulse rounded bg-zinc-800" />
|
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" />
|
||||||
<div className="h-3 w-full animate-pulse rounded-sm bg-zinc-800" />
|
<div className="h-3.5 w-full animate-pulse rounded-sm bg-zinc-800" />
|
||||||
</div>
|
</div>
|
||||||
<div className="inline-flex h-9 items-center gap-2 rounded-md px-2.5">
|
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5">
|
||||||
<div className="relative h-5 w-5 shrink-0 animate-pulse rounded bg-zinc-800" />
|
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" />
|
||||||
<div className="h-3 w-full animate-pulse rounded-sm bg-zinc-800" />
|
<div className="h-3.5 w-full animate-pulse rounded-sm bg-zinc-800" />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@@ -29,6 +38,12 @@ export function ChannelsList() {
|
|||||||
<ChannelsListItem key={item.event_id} data={item} />
|
<ChannelsListItem key={item.event_id} data={item} />
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
{isFetching && (
|
||||||
|
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5">
|
||||||
|
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" />
|
||||||
|
<div className="h-3.5 w-full animate-pulse rounded-sm bg-zinc-800" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<ChannelCreateModal />
|
<ChannelCreateModal />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import { Member } from "@app/channel/components/member";
|
import { Member } from "@app/channel/components/member";
|
||||||
import { getChannelUsers } from "@libs/storage";
|
import { getChannelUsers } from "@libs/storage";
|
||||||
import useSWR from "swr";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
const fetcher = ([, id]) => getChannelUsers(id);
|
|
||||||
|
|
||||||
export function ChannelMembers({ id }: { id: string }) {
|
export function ChannelMembers({ id }: { id: string }) {
|
||||||
const { data, isLoading }: any = useSWR(["channel-members", id], fetcher);
|
const { status, data, isFetching } = useQuery(
|
||||||
|
["channel-members", id],
|
||||||
|
async () => {
|
||||||
|
return await getChannelUsers(id);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-3">
|
<div className="mt-3">
|
||||||
@@ -13,8 +16,7 @@ export function ChannelMembers({ id }: { id: string }) {
|
|||||||
Members
|
Members
|
||||||
</h5>
|
</h5>
|
||||||
<div className="mt-3 w-full flex flex-wrap gap-1.5">
|
<div className="mt-3 w-full flex flex-wrap gap-1.5">
|
||||||
{isLoading && <p>Loading...</p>}
|
{status === "loading" || isFetching ? (
|
||||||
{!data ? (
|
|
||||||
<p>Loading...</p>
|
<p>Loading...</p>
|
||||||
) : (
|
) : (
|
||||||
data.map((member: { pubkey: string }) => (
|
data.map((member: { pubkey: string }) => (
|
||||||
|
|||||||
@@ -3,14 +3,13 @@ import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
|||||||
import { CancelIcon, EnterIcon } from "@shared/icons";
|
import { CancelIcon, EnterIcon } from "@shared/icons";
|
||||||
import { MediaUploader } from "@shared/mediaUploader";
|
import { MediaUploader } from "@shared/mediaUploader";
|
||||||
import { RelayContext } from "@shared/relayProvider";
|
import { RelayContext } from "@shared/relayProvider";
|
||||||
import { useActiveAccount } from "@stores/accounts";
|
|
||||||
import { useChannelMessages } from "@stores/channels";
|
import { useChannelMessages } from "@stores/channels";
|
||||||
import { dateToUnix } from "@utils/date";
|
import { dateToUnix } from "@utils/date";
|
||||||
|
import { useAccount } from "@utils/hooks/useAccount";
|
||||||
import { useContext, useState } from "react";
|
import { useContext, useState } from "react";
|
||||||
|
|
||||||
export function ChannelMessageForm({ channelID }: { channelID: string }) {
|
export function ChannelMessageForm({ channelID }: { channelID: string }) {
|
||||||
const ndk = useContext(RelayContext);
|
const ndk = useContext(RelayContext);
|
||||||
const account = useActiveAccount((state: any) => state.account);
|
|
||||||
|
|
||||||
const [value, setValue] = useState("");
|
const [value, setValue] = useState("");
|
||||||
const [replyTo, closeReply] = useChannelMessages((state: any) => [
|
const [replyTo, closeReply] = useChannelMessages((state: any) => [
|
||||||
@@ -18,6 +17,8 @@ export function ChannelMessageForm({ channelID }: { channelID: string }) {
|
|||||||
state.closeReply,
|
state.closeReply,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const { account } = useAccount();
|
||||||
|
|
||||||
const submit = () => {
|
const submit = () => {
|
||||||
let tags: string[][];
|
let tags: string[][];
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,15 @@
|
|||||||
import { getChannel, updateChannelMetadata } from "@libs/storage";
|
import { getChannel, updateChannelMetadata } from "@libs/storage";
|
||||||
import { RelayContext } from "@shared/relayProvider";
|
import { RelayContext } from "@shared/relayProvider";
|
||||||
import { useContext } from "react";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import useSWR from "swr";
|
import { useContext, useEffect } from "react";
|
||||||
import useSWRSubscription from "swr/subscription";
|
|
||||||
|
|
||||||
const fetcher = async ([, id]) => {
|
|
||||||
const result = await getChannel(id);
|
|
||||||
if (result) {
|
|
||||||
return result;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export function useChannelProfile(id: string) {
|
export function useChannelProfile(id: string) {
|
||||||
const ndk = useContext(RelayContext);
|
const ndk = useContext(RelayContext);
|
||||||
const { data, mutate } = useSWR(["channel-metadata", id], fetcher);
|
const { data } = useQuery(["channel-metadata", id], async () => {
|
||||||
|
return await getChannel(id);
|
||||||
|
});
|
||||||
|
|
||||||
useSWRSubscription(data ? ["channel-metadata", id] : null, () => {
|
useEffect(() => {
|
||||||
// subscribe to channel
|
// subscribe to channel
|
||||||
const sub = ndk.subscribe(
|
const sub = ndk.subscribe(
|
||||||
{
|
{
|
||||||
@@ -32,14 +24,12 @@ export function useChannelProfile(id: string) {
|
|||||||
sub.addListener("event", (event: { content: string }) => {
|
sub.addListener("event", (event: { content: string }) => {
|
||||||
// update in local database
|
// update in local database
|
||||||
updateChannelMetadata(id, event.content);
|
updateChannelMetadata(id, event.content);
|
||||||
// revaildate
|
|
||||||
mutate();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
sub.stop();
|
sub.stop();
|
||||||
};
|
};
|
||||||
});
|
}, []);
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
import { ChannelMessageItem } from "../components/messages/item";
|
import { ChannelMessageItem } from "./components/messages/item";
|
||||||
import { ChannelMembers } from "@app/channel/components/members";
|
import { ChannelMembers } from "@app/channel/components/members";
|
||||||
import { ChannelMessageForm } from "@app/channel/components/messages/form";
|
import { ChannelMessageForm } from "@app/channel/components/messages/form";
|
||||||
import { ChannelMetadata } from "@app/channel/components/metadata";
|
import { ChannelMetadata } from "@app/channel/components/metadata";
|
||||||
import { RelayContext } from "@shared/relayProvider";
|
import { RelayContext } from "@shared/relayProvider";
|
||||||
import { useChannelMessages } from "@stores/channels";
|
import { useChannelMessages } from "@stores/channels";
|
||||||
import { dateToUnix, getHourAgo } from "@utils/date";
|
import { dateToUnix, getHourAgo } from "@utils/date";
|
||||||
import { usePageContext } from "@utils/hooks/usePageContext";
|
|
||||||
import { LumeEvent } from "@utils/types";
|
import { LumeEvent } from "@utils/types";
|
||||||
import { useCallback, useContext, useEffect, useRef } from "react";
|
import {
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useLayoutEffect,
|
||||||
|
useRef,
|
||||||
|
} from "react";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
import { Virtuoso } from "react-virtuoso";
|
import { Virtuoso } from "react-virtuoso";
|
||||||
import useSWRSubscription from "swr/subscription";
|
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
@@ -42,13 +47,11 @@ const Empty = (
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
export function Page() {
|
export function ChannelScreen() {
|
||||||
const ndk = useContext(RelayContext);
|
const ndk = useContext(RelayContext);
|
||||||
const pageContext = usePageContext();
|
|
||||||
const virtuosoRef = useRef(null);
|
const virtuosoRef = useRef(null);
|
||||||
|
|
||||||
const searchParams: any = pageContext.urlParsed.search;
|
const { id } = useParams();
|
||||||
const channelID = searchParams.id;
|
|
||||||
|
|
||||||
const [messages, fetchMessages, addMessage, clearMessages] =
|
const [messages, fetchMessages, addMessage, clearMessages] =
|
||||||
useChannelMessages((state: any) => [
|
useChannelMessages((state: any) => [
|
||||||
@@ -58,36 +61,30 @@ export function Page() {
|
|||||||
state.clear,
|
state.clear,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useSWRSubscription(
|
useLayoutEffect(() => {
|
||||||
channelID ? ["channelMessagesSubscribe", channelID] : null,
|
fetchMessages(id);
|
||||||
() => {
|
}, [fetchMessages]);
|
||||||
// subscribe to channel
|
|
||||||
const sub = ndk.subscribe(
|
|
||||||
{
|
|
||||||
"#e": [channelID],
|
|
||||||
kinds: [42],
|
|
||||||
since: dateToUnix(),
|
|
||||||
},
|
|
||||||
{ closeOnEose: false },
|
|
||||||
);
|
|
||||||
|
|
||||||
sub.addListener("event", (event: LumeEvent) => {
|
|
||||||
addMessage(channelID, event);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
sub.stop();
|
|
||||||
};
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchMessages(channelID);
|
// subscribe to channel
|
||||||
|
const sub = ndk.subscribe(
|
||||||
|
{
|
||||||
|
"#e": [id],
|
||||||
|
kinds: [42],
|
||||||
|
since: dateToUnix(),
|
||||||
|
},
|
||||||
|
{ closeOnEose: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
sub.addListener("event", (event: LumeEvent) => {
|
||||||
|
addMessage(id, event);
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
clearMessages();
|
clearMessages();
|
||||||
|
sub.stop();
|
||||||
};
|
};
|
||||||
}, [fetchMessages]);
|
}, []);
|
||||||
|
|
||||||
const itemContent: any = useCallback(
|
const itemContent: any = useCallback(
|
||||||
(index: string | number) => {
|
(index: string | number) => {
|
||||||
@@ -135,7 +132,7 @@ export function Page() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="w-full inline-flex shrink-0 px-5 py-3 border-t border-zinc-800">
|
<div className="w-full inline-flex shrink-0 px-5 py-3 border-t border-zinc-800">
|
||||||
<ChannelMessageForm channelID={channelID} />
|
<ChannelMessageForm channelID={id} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -146,8 +143,8 @@ export function Page() {
|
|||||||
className="h-11 w-full shrink-0 inline-flex items-center justify-center border-b border-zinc-900"
|
className="h-11 w-full shrink-0 inline-flex items-center justify-center border-b border-zinc-900"
|
||||||
/>
|
/>
|
||||||
<div className="p-3 flex flex-col gap-3">
|
<div className="p-3 flex flex-col gap-3">
|
||||||
<ChannelMetadata id={channelID} />
|
<ChannelMetadata id={id} />
|
||||||
<ChannelMembers id={channelID} />
|
<ChannelMembers id={id} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { DefaultLayout as Layout } from "@shared/layout";
|
|
||||||
@@ -1,22 +1,16 @@
|
|||||||
import { Image } from "@shared/image";
|
import { Image } from "@shared/image";
|
||||||
import { Link } from "@shared/link";
|
|
||||||
import { DEFAULT_AVATAR } from "@stores/constants";
|
import { DEFAULT_AVATAR } from "@stores/constants";
|
||||||
import { usePageContext } from "@utils/hooks/usePageContext";
|
|
||||||
import { useProfile } from "@utils/hooks/useProfile";
|
import { useProfile } from "@utils/hooks/useProfile";
|
||||||
import { shortenKey } from "@utils/shortenKey";
|
import { shortenKey } from "@utils/shortenKey";
|
||||||
|
import { NavLink } from "react-router-dom";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
export function ChatsListItem({ data }: { data: any }) {
|
export function ChatsListItem({ data }: { data: any }) {
|
||||||
const pageContext = usePageContext();
|
const { status, user, isFetching } = useProfile(data.sender_pubkey);
|
||||||
const searchParams: any = pageContext.urlParsed.search;
|
|
||||||
const pagePubkey = searchParams.pubkey;
|
|
||||||
|
|
||||||
const { user, isError, isLoading } = useProfile(data.sender_pubkey);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isError && <div>error</div>}
|
{status === "loading" && isFetching ? (
|
||||||
{isLoading && !user ? (
|
|
||||||
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5">
|
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5">
|
||||||
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" />
|
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" />
|
||||||
<div>
|
<div>
|
||||||
@@ -24,14 +18,14 @@ export function ChatsListItem({ data }: { data: any }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Link
|
<NavLink
|
||||||
href={`/app/chat?pubkey=${data.sender_pubkey}`}
|
to={`/app/chat/${data.sender_pubkey}`}
|
||||||
className={twMerge(
|
className={({ isActive }) =>
|
||||||
"inline-flex h-9 items-center gap-2.5 rounded-md px-2.5",
|
twMerge(
|
||||||
pagePubkey === data.sender_pubkey
|
"inline-flex h-9 items-center gap-2.5 rounded-md px-2.5",
|
||||||
? "bg-zinc-900 text-zinc-100"
|
isActive ? "bg-zinc-900/50 text-zinc-100" : "",
|
||||||
: "",
|
)
|
||||||
)}
|
}
|
||||||
>
|
>
|
||||||
<div className="inline-flex shrink-0 h-6 w-6 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
|
<div className="inline-flex shrink-0 h-6 w-6 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
|
||||||
<Image
|
<Image
|
||||||
@@ -57,7 +51,7 @@ export function ChatsListItem({ data }: { data: any }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</NavLink>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,44 +1,47 @@
|
|||||||
import { ChatsListItem } from "@app/chat/components/item";
|
import { ChatsListItem } from "@app/chat/components/item";
|
||||||
import { NewMessageModal } from "@app/chat/components/modal";
|
import { NewMessageModal } from "@app/chat/components/modal";
|
||||||
import { ChatsListSelfItem } from "@app/chat/components/self";
|
import { ChatsListSelfItem } from "@app/chat/components/self";
|
||||||
import { useActiveAccount } from "@stores/accounts";
|
import { getChatsByPubkey } from "@libs/storage";
|
||||||
import { useChats } from "@stores/chats";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useEffect } from "react";
|
import { useAccount } from "@utils/hooks/useAccount";
|
||||||
|
|
||||||
export function ChatsList() {
|
export function ChatsList() {
|
||||||
const account = useActiveAccount((state: any) => state.account);
|
const { account } = useAccount();
|
||||||
const chats = useChats((state: any) => state.chats);
|
|
||||||
const fetchChats = useChats((state: any) => state.fetch);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const {
|
||||||
if (!account) return;
|
status,
|
||||||
fetchChats(account.pubkey);
|
data: chats,
|
||||||
}, [fetchChats]);
|
isFetching,
|
||||||
|
} = useQuery(
|
||||||
if (!account)
|
["chats"],
|
||||||
return (
|
async () => {
|
||||||
<div className="flex flex-col">
|
return await getChatsByPubkey(account.pubkey);
|
||||||
<div className="inline-flex h-9 items-center gap-2 rounded-md px-2.5">
|
},
|
||||||
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" />
|
{
|
||||||
<div className="h-3 w-full rounded-sm animate-pulse bg-zinc-800" />
|
enabled: account ? true : false,
|
||||||
</div>
|
refetchOnWindowFocus: false,
|
||||||
<div className="inline-flex h-9 items-center gap-2 rounded-md px-2.5">
|
refetchOnMount: false,
|
||||||
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" />
|
refetchOnReconnect: false,
|
||||||
<div className="h-3 w-full rounded-sm animate-pulse bg-zinc-800" />
|
},
|
||||||
</div>
|
);
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<ChatsListSelfItem data={account} />
|
{account ? (
|
||||||
{!chats ? (
|
<ChatsListSelfItem data={account} />
|
||||||
|
) : (
|
||||||
|
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5">
|
||||||
|
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" />
|
||||||
|
<div className="h-3 w-full rounded-sm animate-pulse bg-zinc-800" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{status === "loading" ? (
|
||||||
<>
|
<>
|
||||||
<div className="inline-flex h-9 items-center gap-2 rounded-md px-2.5">
|
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5">
|
||||||
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" />
|
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" />
|
||||||
<div className="h-3 w-full rounded-sm animate-pulse bg-zinc-800" />
|
<div className="h-3 w-full rounded-sm animate-pulse bg-zinc-800" />
|
||||||
</div>
|
</div>
|
||||||
<div className="inline-flex h-9 items-center gap-2 rounded-md px-2.5">
|
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5">
|
||||||
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" />
|
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" />
|
||||||
<div className="h-3 w-full rounded-sm animate-pulse bg-zinc-800" />
|
<div className="h-3 w-full rounded-sm animate-pulse bg-zinc-800" />
|
||||||
</div>
|
</div>
|
||||||
@@ -50,6 +53,12 @@ export function ChatsList() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
)}
|
)}
|
||||||
|
{isFetching && (
|
||||||
|
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5">
|
||||||
|
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" />
|
||||||
|
<div className="h-3 w-full rounded-sm animate-pulse bg-zinc-800" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<NewMessageModal />
|
<NewMessageModal />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
import { ChatMessageItem } from "@app/chat/components/messages/item";
|
|
||||||
import { useActiveAccount } from "@stores/accounts";
|
|
||||||
import { useChatMessages } from "@stores/chats";
|
|
||||||
import { getHourAgo } from "@utils/date";
|
|
||||||
import { useCallback, useRef } from "react";
|
|
||||||
import { Virtuoso } from "react-virtuoso";
|
|
||||||
|
|
||||||
const now = new Date();
|
|
||||||
|
|
||||||
const Header = (
|
|
||||||
<div className="relative py-4">
|
|
||||||
<div className="absolute inset-0 flex items-center">
|
|
||||||
<div className="w-full border-t border-zinc-800" />
|
|
||||||
</div>
|
|
||||||
<div className="relative flex justify-center">
|
|
||||||
<div className="inline-flex items-center gap-x-1.5 rounded-full bg-zinc-900 px-3 py-1.5 text-sm font-medium text-zinc-400 shadow-sm ring-1 ring-inset ring-zinc-800">
|
|
||||||
{getHourAgo(24, now).toLocaleDateString("en-US", {
|
|
||||||
weekday: "long",
|
|
||||||
year: "numeric",
|
|
||||||
month: "long",
|
|
||||||
day: "numeric",
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const Empty = (
|
|
||||||
<div className="flex flex-col gap-1 text-center">
|
|
||||||
<h3 className="text-base font-semibold leading-none text-white">
|
|
||||||
Nothing to see here yet
|
|
||||||
</h3>
|
|
||||||
<p className="text-base leading-none text-zinc-400">
|
|
||||||
You two didn't talk yet, let's send first message
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export function ChatMessageList() {
|
|
||||||
const account = useActiveAccount((state: any) => state.account);
|
|
||||||
const messages = useChatMessages((state: any) => state.messages);
|
|
||||||
|
|
||||||
const virtuosoRef = useRef(null);
|
|
||||||
|
|
||||||
const itemContent: any = useCallback(
|
|
||||||
(index: string | number) => {
|
|
||||||
return (
|
|
||||||
<ChatMessageItem
|
|
||||||
data={messages[index]}
|
|
||||||
userPubkey={account.pubkey}
|
|
||||||
userPrivkey={account.privkey}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[account.privkey, account.pubkey, messages],
|
|
||||||
);
|
|
||||||
|
|
||||||
const computeItemKey = useCallback(
|
|
||||||
(index: string | number) => {
|
|
||||||
return messages[index].id;
|
|
||||||
},
|
|
||||||
[messages],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="h-full w-full">
|
|
||||||
<Virtuoso
|
|
||||||
ref={virtuosoRef}
|
|
||||||
data={messages}
|
|
||||||
itemContent={itemContent}
|
|
||||||
computeItemKey={computeItemKey}
|
|
||||||
initialTopMostItemIndex={messages.length - 1}
|
|
||||||
alignToBottom={true}
|
|
||||||
followOutput={true}
|
|
||||||
overscan={50}
|
|
||||||
increaseViewportBy={{ top: 200, bottom: 200 }}
|
|
||||||
className="scrollbar-hide h-full w-full overflow-y-auto"
|
|
||||||
components={{
|
|
||||||
Header: () => Header,
|
|
||||||
EmptyPlaceholder: () => Empty,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -2,16 +2,19 @@ import { Dialog, Transition } from "@headlessui/react";
|
|||||||
import { getPlebs } from "@libs/storage";
|
import { getPlebs } from "@libs/storage";
|
||||||
import { CancelIcon, PlusIcon } from "@shared/icons";
|
import { CancelIcon, PlusIcon } from "@shared/icons";
|
||||||
import { DEFAULT_AVATAR } from "@stores/constants";
|
import { DEFAULT_AVATAR } from "@stores/constants";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { nip19 } from "nostr-tools";
|
import { nip19 } from "nostr-tools";
|
||||||
import { Fragment, useState } from "react";
|
import { Fragment, useState } from "react";
|
||||||
import useSWR from "swr";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { navigate } from "vite-plugin-ssr/client/router";
|
|
||||||
|
|
||||||
const fetcher = () => getPlebs();
|
|
||||||
|
|
||||||
export function NewMessageModal() {
|
export function NewMessageModal() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const { status, data, isFetching }: any = useQuery(["plebs"], async () => {
|
||||||
|
return await getPlebs();
|
||||||
|
});
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const { data, isLoading }: any = useSWR("plebs", fetcher);
|
|
||||||
|
|
||||||
const closeModal = () => {
|
const closeModal = () => {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
@@ -23,7 +26,7 @@ export function NewMessageModal() {
|
|||||||
|
|
||||||
const openChat = (npub: string) => {
|
const openChat = (npub: string) => {
|
||||||
const pubkey = nip19.decode(npub).data;
|
const pubkey = nip19.decode(npub).data;
|
||||||
navigate(`/app/chat?pubkey=${pubkey}`);
|
navigate(`/app/chat/${pubkey}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -92,8 +95,7 @@ export function NewMessageModal() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-[500px] flex flex-col pb-5 overflow-y-auto">
|
<div className="h-[500px] flex flex-col pb-5 overflow-y-auto">
|
||||||
{isLoading && <p>Loading...</p>}
|
{status === "loading" || isFetching ? (
|
||||||
{!data ? (
|
|
||||||
<p>Loading...</p>
|
<p>Loading...</p>
|
||||||
) : (
|
) : (
|
||||||
data.map((pleb) => (
|
data.map((pleb) => (
|
||||||
|
|||||||
@@ -1,21 +1,16 @@
|
|||||||
import { Image } from "@shared/image";
|
import { Image } from "@shared/image";
|
||||||
import { Link } from "@shared/link";
|
|
||||||
import { DEFAULT_AVATAR } from "@stores/constants";
|
import { DEFAULT_AVATAR } from "@stores/constants";
|
||||||
import { usePageContext } from "@utils/hooks/usePageContext";
|
|
||||||
import { useProfile } from "@utils/hooks/useProfile";
|
import { useProfile } from "@utils/hooks/useProfile";
|
||||||
import { shortenKey } from "@utils/shortenKey";
|
import { shortenKey } from "@utils/shortenKey";
|
||||||
|
import { NavLink } from "react-router-dom";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
export function ChatsListSelfItem({ data }: { data: any }) {
|
export function ChatsListSelfItem({ data }: { data: any }) {
|
||||||
const pageContext = usePageContext();
|
const { status, user, isFetching } = useProfile(data.pubkey);
|
||||||
const searchParams: any = pageContext.urlParsed.search;
|
|
||||||
const pagePubkey = searchParams.pubkey;
|
|
||||||
|
|
||||||
const { user, isLoading } = useProfile(data.pubkey);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isLoading && !user ? (
|
{status === "loading" && isFetching ? (
|
||||||
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5">
|
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5">
|
||||||
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" />
|
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" />
|
||||||
<div>
|
<div>
|
||||||
@@ -23,12 +18,14 @@ export function ChatsListSelfItem({ data }: { data: any }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Link
|
<NavLink
|
||||||
href={`/app/chat?pubkey=${data.pubkey}`}
|
to={`/app/chat/${data.pubkey}`}
|
||||||
className={twMerge(
|
className={({ isActive }) =>
|
||||||
"inline-flex h-9 items-center gap-2.5 rounded-md px-2.5",
|
twMerge(
|
||||||
pagePubkey === data.pubkey ? "bg-zinc-900 text-zinc-100" : "",
|
"inline-flex h-9 items-center gap-2.5 rounded-md px-2.5",
|
||||||
)}
|
isActive ? "bg-zinc-900/50 text-zinc-100" : "",
|
||||||
|
)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
|
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
|
||||||
<Image
|
<Image
|
||||||
@@ -44,7 +41,7 @@ export function ChatsListSelfItem({ data }: { data: any }) {
|
|||||||
</h5>
|
</h5>
|
||||||
<span className="text-zinc-500">(you)</span>
|
<span className="text-zinc-500">(you)</span>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</NavLink>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,14 +3,15 @@ import { DEFAULT_AVATAR } from "@stores/constants";
|
|||||||
import { useProfile } from "@utils/hooks/useProfile";
|
import { useProfile } from "@utils/hooks/useProfile";
|
||||||
import { shortenKey } from "@utils/shortenKey";
|
import { shortenKey } from "@utils/shortenKey";
|
||||||
import { nip19 } from "nostr-tools";
|
import { nip19 } from "nostr-tools";
|
||||||
import { navigate } from "vite-plugin-ssr/client/router";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
export function ChatSidebar({ pubkey }: { pubkey: string }) {
|
export function ChatSidebar({ pubkey }: { pubkey: string }) {
|
||||||
|
const navigate = useNavigate();
|
||||||
const { user } = useProfile(pubkey);
|
const { user } = useProfile(pubkey);
|
||||||
|
|
||||||
const viewProfile = () => {
|
const viewProfile = () => {
|
||||||
const pubkey = nip19.decode(user.npub).data;
|
const pubkey = nip19.decode(user.npub).data;
|
||||||
navigate(`/app/user?pubkey=${pubkey}`);
|
navigate(`/app/user/${pubkey}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
110
src/app/chat/index.tsx
Normal file
110
src/app/chat/index.tsx
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import { ChatMessageForm } from "@app/chat/components/messages/form";
|
||||||
|
import { ChatMessageItem } from "@app/chat/components/messages/item";
|
||||||
|
import { ChatSidebar } from "@app/chat/components/sidebar";
|
||||||
|
import { getChatMessages } from "@libs/storage";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { useAccount } from "@utils/hooks/useAccount";
|
||||||
|
import { useCallback, useRef } from "react";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
import { Virtuoso } from "react-virtuoso";
|
||||||
|
|
||||||
|
export function ChatScreen() {
|
||||||
|
const virtuosoRef = useRef(null);
|
||||||
|
|
||||||
|
const { pubkey } = useParams();
|
||||||
|
const { account } = useAccount();
|
||||||
|
const { status, data } = useQuery(
|
||||||
|
["chat", pubkey],
|
||||||
|
async () => {
|
||||||
|
return await getChatMessages(account.pubkey, pubkey);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: account ? true : false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const itemContent: any = useCallback(
|
||||||
|
(index: string | number) => {
|
||||||
|
return (
|
||||||
|
<ChatMessageItem
|
||||||
|
data={data[index]}
|
||||||
|
userPubkey={account.pubkey}
|
||||||
|
userPrivkey={account.privkey}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[data],
|
||||||
|
);
|
||||||
|
|
||||||
|
const computeItemKey = useCallback(
|
||||||
|
(index: string | number) => {
|
||||||
|
return data[index].id;
|
||||||
|
},
|
||||||
|
[data],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full w-full grid grid-cols-3">
|
||||||
|
<div className="col-span-2 flex flex-col justify-between border-r border-zinc-900">
|
||||||
|
<div
|
||||||
|
data-tauri-drag-region
|
||||||
|
className="h-11 w-full shrink-0 inline-flex items-center justify-center border-b border-zinc-900"
|
||||||
|
>
|
||||||
|
<h3 className="font-semibold text-zinc-100">Encrypted Chat</h3>
|
||||||
|
</div>
|
||||||
|
<div className="w-full flex-1 p-3">
|
||||||
|
{account && (
|
||||||
|
<div className="flex h-full flex-col justify-between rounded-md bg-zinc-900">
|
||||||
|
{status === "loading" ? (
|
||||||
|
<p>Loading...</p>
|
||||||
|
) : (
|
||||||
|
<div className="h-full w-full">
|
||||||
|
<Virtuoso
|
||||||
|
ref={virtuosoRef}
|
||||||
|
data={data}
|
||||||
|
itemContent={itemContent}
|
||||||
|
computeItemKey={computeItemKey}
|
||||||
|
initialTopMostItemIndex={data.length - 1}
|
||||||
|
alignToBottom={true}
|
||||||
|
followOutput={true}
|
||||||
|
overscan={50}
|
||||||
|
increaseViewportBy={{ top: 200, bottom: 200 }}
|
||||||
|
className="scrollbar-hide h-full w-full overflow-y-auto"
|
||||||
|
components={{
|
||||||
|
EmptyPlaceholder: () => Empty,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="shrink-0 px-5 p-3 border-t border-zinc-800">
|
||||||
|
<ChatMessageForm
|
||||||
|
receiverPubkey={pubkey}
|
||||||
|
userPubkey={account.pubkey}
|
||||||
|
userPrivkey={account.privkey}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-1">
|
||||||
|
<div
|
||||||
|
data-tauri-drag-region
|
||||||
|
className="h-11 w-full shrink-0 inline-flex items-center justify-center border-b border-zinc-900"
|
||||||
|
/>
|
||||||
|
{pubkey && <ChatSidebar pubkey={pubkey} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Empty = (
|
||||||
|
<div className="flex flex-col gap-1 text-center">
|
||||||
|
<h3 className="text-base font-semibold leading-none text-white">
|
||||||
|
Nothing to see here yet
|
||||||
|
</h3>
|
||||||
|
<p className="text-base leading-none text-zinc-400">
|
||||||
|
You two didn't talk yet, let's send first message
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
import { ChatSidebar } from "../components/sidebar";
|
|
||||||
import { ChatMessageList } from "@app/chat/components/messageList";
|
|
||||||
import { ChatMessageForm } from "@app/chat/components/messages/form";
|
|
||||||
import { RelayContext } from "@shared/relayProvider";
|
|
||||||
import { useActiveAccount } from "@stores/accounts";
|
|
||||||
import { useChatMessages } from "@stores/chats";
|
|
||||||
import { dateToUnix } from "@utils/date";
|
|
||||||
import { usePageContext } from "@utils/hooks/usePageContext";
|
|
||||||
import { LumeEvent } from "@utils/types";
|
|
||||||
import { useContext, useEffect } from "react";
|
|
||||||
import useSWRSubscription from "swr/subscription";
|
|
||||||
|
|
||||||
export function Page() {
|
|
||||||
const ndk = useContext(RelayContext);
|
|
||||||
const account = useActiveAccount((state: any) => state.account);
|
|
||||||
|
|
||||||
const pageContext = usePageContext();
|
|
||||||
const searchParams: any = pageContext.urlParsed.search;
|
|
||||||
const pubkey = searchParams.pubkey;
|
|
||||||
|
|
||||||
const [add, fetchMessages, clear] = useChatMessages((state: any) => [
|
|
||||||
state.add,
|
|
||||||
state.fetch,
|
|
||||||
state.clear,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useSWRSubscription(account !== pubkey ? ["chat", pubkey] : null, () => {
|
|
||||||
const sub = ndk.subscribe({
|
|
||||||
kinds: [4],
|
|
||||||
authors: [pubkey],
|
|
||||||
"#p": [account.pubkey],
|
|
||||||
since: dateToUnix(),
|
|
||||||
});
|
|
||||||
|
|
||||||
sub.addListener("event", (event: LumeEvent) => {
|
|
||||||
add(account.pubkey, event);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
sub.stop();
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchMessages(account.pubkey, pubkey);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
clear();
|
|
||||||
};
|
|
||||||
}, [pubkey, fetchMessages]);
|
|
||||||
|
|
||||||
if (!account) return <div>Fuck SSR</div>;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="h-full w-full grid grid-cols-3">
|
|
||||||
<div className="col-span-2 flex flex-col justify-between border-r border-zinc-900">
|
|
||||||
<div
|
|
||||||
data-tauri-drag-region
|
|
||||||
className="h-11 w-full shrink-0 inline-flex items-center justify-center border-b border-zinc-900"
|
|
||||||
>
|
|
||||||
<h3 className="font-semibold text-zinc-100">Encrypted Chat</h3>
|
|
||||||
</div>
|
|
||||||
<div className="w-full flex-1 p-3">
|
|
||||||
<div className="flex h-full flex-col justify-between rounded-md bg-zinc-900">
|
|
||||||
<ChatMessageList />
|
|
||||||
<div className="shrink-0 px-5 p-3 border-t border-zinc-800">
|
|
||||||
<ChatMessageForm
|
|
||||||
receiverPubkey={pubkey}
|
|
||||||
userPubkey={account.pubkey}
|
|
||||||
userPrivkey={account.privkey}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="col-span-1">
|
|
||||||
<div
|
|
||||||
data-tauri-drag-region
|
|
||||||
className="h-11 w-full shrink-0 inline-flex items-center justify-center border-b border-zinc-900"
|
|
||||||
/>
|
|
||||||
<ChatSidebar pubkey={pubkey} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
17
src/app/error.tsx
Normal file
17
src/app/error.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { useRouteError } from "react-router-dom";
|
||||||
|
|
||||||
|
export function ErrorScreen() {
|
||||||
|
const error: any = useRouteError();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full flex items-center justify-center">
|
||||||
|
<div>
|
||||||
|
<h1>Oops!</h1>
|
||||||
|
<p>Sorry, an unexpected error has occurred.</p>
|
||||||
|
<p>
|
||||||
|
<i>{error.statusText || error.message}</i>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1 +0,0 @@
|
|||||||
export const filesystemRoutingRoot = "/";
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import { useActiveAccount } from "@stores/accounts";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import { navigate } from "vite-plugin-ssr/client/router";
|
|
||||||
|
|
||||||
export function Page() {
|
|
||||||
const fetchLastLogin = useActiveAccount((state: any) => state.fetchLastLogin);
|
|
||||||
const fetchAccount = useActiveAccount((state: any) => state.fetch);
|
|
||||||
const account = useActiveAccount((state: any) => state.account);
|
|
||||||
const lastLogin = useActiveAccount((state: any) => state.lastLogin);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (account === null) {
|
|
||||||
fetchAccount();
|
|
||||||
}
|
|
||||||
if (lastLogin === null) {
|
|
||||||
fetchLastLogin();
|
|
||||||
}
|
|
||||||
if (!account) {
|
|
||||||
navigate("/app/auth", { overwriteLastHistoryEntry: true });
|
|
||||||
}
|
|
||||||
if (account) {
|
|
||||||
navigate("/app/prefetch", { overwriteLastHistoryEntry: true });
|
|
||||||
}
|
|
||||||
}, [fetchAccount, fetchLastLogin, account, lastLogin]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="h-screen w-screen bg-zinc-50 text-zinc-900 dark:bg-black dark:text-zinc-100" />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { LayoutOnboarding as Layout } from "./layout";
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
export function Page() {
|
|
||||||
return (
|
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
|
||||||
<div className="mx-auto w-full max-w-md">
|
|
||||||
<div className="mb-8 text-center">
|
|
||||||
<h1 className="text-xl font-semibold text-zinc-100"># TODO</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -5,29 +5,25 @@ import {
|
|||||||
createChat,
|
createChat,
|
||||||
createNote,
|
createNote,
|
||||||
getChannels,
|
getChannels,
|
||||||
|
getLastLogin,
|
||||||
} from "@libs/storage";
|
} from "@libs/storage";
|
||||||
import { NDKFilter } from "@nostr-dev-kit/ndk";
|
import { NDKFilter } from "@nostr-dev-kit/ndk";
|
||||||
import { LumeIcon } from "@shared/icons";
|
import { LumeIcon } from "@shared/icons";
|
||||||
import { RelayContext } from "@shared/relayProvider";
|
import { RelayContext } from "@shared/relayProvider";
|
||||||
import { useActiveAccount } from "@stores/accounts";
|
|
||||||
import { dateToUnix, getHourAgo } from "@utils/date";
|
import { dateToUnix, getHourAgo } from "@utils/date";
|
||||||
|
import { useAccount } from "@utils/hooks/useAccount";
|
||||||
import { useContext, useEffect, useRef } from "react";
|
import { useContext, useEffect, useRef } from "react";
|
||||||
import { navigate } from "vite-plugin-ssr/client/router";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
let totalNotes: number;
|
const totalNotes = await countTotalNotes();
|
||||||
|
const lastLogin = await getLastLogin();
|
||||||
|
|
||||||
if (typeof window !== "undefined") {
|
export function Root() {
|
||||||
totalNotes = await countTotalNotes();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Page() {
|
|
||||||
const ndk = useContext(RelayContext);
|
const ndk = useContext(RelayContext);
|
||||||
const now = useRef(new Date());
|
const now = useRef(new Date());
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [account, lastLogin] = useActiveAccount((state: any) => [
|
const { status, account } = useAccount();
|
||||||
state.account,
|
|
||||||
state.lastLogin,
|
|
||||||
]);
|
|
||||||
|
|
||||||
async function fetchNotes() {
|
async function fetchNotes() {
|
||||||
try {
|
try {
|
||||||
@@ -150,12 +146,15 @@ export function Page() {
|
|||||||
const chats = await fetchChats();
|
const chats = await fetchChats();
|
||||||
const channels = await fetchChannelMessages();
|
const channels = await fetchChannelMessages();
|
||||||
if (chats && channels) {
|
if (chats && channels) {
|
||||||
navigate("/app/space", { overwriteLastHistoryEntry: true });
|
navigate("/app/space", { replace: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
prefetch();
|
|
||||||
}, []);
|
if (status === "success" && account) {
|
||||||
|
prefetch();
|
||||||
|
}
|
||||||
|
}, [status]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen w-screen bg-zinc-50 text-zinc-900 dark:bg-black dark:text-zinc-100">
|
<div className="h-screen w-screen bg-zinc-50 text-zinc-900 dark:bg-black dark:text-zinc-100">
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { DefaultLayout as Layout } from "@shared/layout";
|
|
||||||
@@ -1,19 +1,15 @@
|
|||||||
import { getNotesByAuthor } from "@libs/storage";
|
import { getNotesByAuthor } from "@libs/storage";
|
||||||
import { CancelIcon } from "@shared/icons";
|
|
||||||
import { Note } from "@shared/notes/note";
|
import { Note } from "@shared/notes/note";
|
||||||
import { NoteSkeleton } from "@shared/notes/skeleton";
|
import { NoteSkeleton } from "@shared/notes/skeleton";
|
||||||
import { TitleBar } from "@shared/titleBar";
|
import { TitleBar } from "@shared/titleBar";
|
||||||
import { useActiveAccount } from "@stores/accounts";
|
import { useActiveAccount } from "@stores/accounts";
|
||||||
|
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||||
import { useEffect, useMemo, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import useSWRInfinite from "swr/infinite";
|
|
||||||
|
|
||||||
const ITEM_PER_PAGE = 10;
|
const ITEM_PER_PAGE = 10;
|
||||||
const TIME = Math.floor(Date.now() / 1000);
|
const TIME = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
const fetcher = async ([pubkey, offset]) =>
|
|
||||||
getNotesByAuthor(pubkey, TIME, ITEM_PER_PAGE, offset);
|
|
||||||
|
|
||||||
export function FeedBlock({ params }: { params: any }) {
|
export function FeedBlock({ params }: { params: any }) {
|
||||||
const removeBlock = useActiveAccount((state: any) => state.removeBlock);
|
const removeBlock = useActiveAccount((state: any) => state.removeBlock);
|
||||||
|
|
||||||
@@ -21,26 +17,36 @@ export function FeedBlock({ params }: { params: any }) {
|
|||||||
removeBlock(params.id, true);
|
removeBlock(params.id, true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getKey = (pageIndex, previousPageData) => {
|
const {
|
||||||
if (previousPageData && !previousPageData.data) return null;
|
status,
|
||||||
if (pageIndex === 0) return [params.content, 0];
|
data,
|
||||||
return [params.content, previousPageData.nextCursor];
|
fetchNextPage,
|
||||||
};
|
hasNextPage,
|
||||||
|
isFetching,
|
||||||
const { data, isLoading, size, setSize } = useSWRInfinite(getKey, fetcher);
|
isFetchingNextPage,
|
||||||
|
}: any = useInfiniteQuery({
|
||||||
const notes = useMemo(
|
queryKey: ["newsfeed", params.content],
|
||||||
() => (data ? data.flatMap((d) => d.data) : []),
|
queryFn: async ({ pageParam = 0 }) => {
|
||||||
[data],
|
return await getNotesByAuthor(
|
||||||
);
|
params.content,
|
||||||
|
TIME,
|
||||||
|
ITEM_PER_PAGE,
|
||||||
|
pageParam,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
||||||
|
});
|
||||||
|
|
||||||
|
const notes = data ? data.pages.flatMap((d: { data: any }) => d.data) : [];
|
||||||
const parentRef = useRef();
|
const parentRef = useRef();
|
||||||
|
|
||||||
const rowVirtualizer = useVirtualizer({
|
const rowVirtualizer = useVirtualizer({
|
||||||
count: notes.length,
|
count: hasNextPage ? notes.length + 1 : notes.length,
|
||||||
getScrollElement: () => parentRef.current,
|
getScrollElement: () => parentRef.current,
|
||||||
estimateSize: () => 400,
|
estimateSize: () => 500,
|
||||||
overscan: 2,
|
overscan: 2,
|
||||||
});
|
});
|
||||||
|
|
||||||
const itemsVirtualizer = rowVirtualizer.getVirtualItems();
|
const itemsVirtualizer = rowVirtualizer.getVirtualItems();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -50,10 +56,25 @@ export function FeedBlock({ params }: { params: any }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastItem.index >= notes.length - 1) {
|
if (
|
||||||
setSize(size + 1);
|
lastItem.index >= notes.length - 1 &&
|
||||||
|
hasNextPage &&
|
||||||
|
!isFetchingNextPage
|
||||||
|
) {
|
||||||
|
fetchNextPage();
|
||||||
}
|
}
|
||||||
}, [notes.length, rowVirtualizer.getVirtualItems()]);
|
}, [notes.length, fetchNextPage, rowVirtualizer.getVirtualItems()]);
|
||||||
|
|
||||||
|
const renderItem = (index: string | number) => {
|
||||||
|
const note = notes[index];
|
||||||
|
|
||||||
|
if (!note) return;
|
||||||
|
return (
|
||||||
|
<div key={index} data-index={index} ref={rowVirtualizer.measureElement}>
|
||||||
|
<Note event={note} block={params.id} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="shrink-0 w-[400px] border-r border-zinc-900">
|
<div className="shrink-0 w-[400px] border-r border-zinc-900">
|
||||||
@@ -63,7 +84,7 @@ export function FeedBlock({ params }: { params: any }) {
|
|||||||
className="scrollbar-hide flex w-full h-full flex-col justify-between gap-1.5 pt-1.5 pb-20 overflow-y-auto"
|
className="scrollbar-hide flex w-full h-full flex-col justify-between gap-1.5 pt-1.5 pb-20 overflow-y-auto"
|
||||||
style={{ contain: "strict" }}
|
style={{ contain: "strict" }}
|
||||||
>
|
>
|
||||||
{!data || isLoading ? (
|
{status === "loading" || isFetching ? (
|
||||||
<div className="px-3 py-1.5">
|
<div className="px-3 py-1.5">
|
||||||
<div className="rounded-md border border-zinc-800 bg-zinc-900 px-3 py-3 shadow-input shadow-black/20">
|
<div className="rounded-md border border-zinc-800 bg-zinc-900 px-3 py-3 shadow-input shadow-black/20">
|
||||||
<NoteSkeleton />
|
<NoteSkeleton />
|
||||||
@@ -85,20 +106,9 @@ export function FeedBlock({ params }: { params: any }) {
|
|||||||
}px)`,
|
}px)`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
|
{rowVirtualizer
|
||||||
const note = notes[virtualRow.index];
|
.getVirtualItems()
|
||||||
if (note) {
|
.map((virtualRow) => renderItem(virtualRow.index))}
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={virtualRow.index}
|
|
||||||
data-index={virtualRow.index}
|
|
||||||
ref={rowVirtualizer.measureElement}
|
|
||||||
>
|
|
||||||
<Note event={note} block={params.id} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,68 +1,44 @@
|
|||||||
import { createNote, getNotes } from "@libs/storage";
|
import { createNote, getNotes } from "@libs/storage";
|
||||||
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
import { NDKEvent, NDKFilter, NDKSubscription } from "@nostr-dev-kit/ndk";
|
||||||
import { Note } from "@shared/notes/note";
|
import { Note } from "@shared/notes/note";
|
||||||
import { NoteSkeleton } from "@shared/notes/skeleton";
|
import { NoteSkeleton } from "@shared/notes/skeleton";
|
||||||
import { RelayContext } from "@shared/relayProvider";
|
import { RelayContext } from "@shared/relayProvider";
|
||||||
import { TitleBar } from "@shared/titleBar";
|
import { TitleBar } from "@shared/titleBar";
|
||||||
import { useActiveAccount } from "@stores/accounts";
|
import { useActiveAccount } from "@stores/accounts";
|
||||||
|
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||||
import { dateToUnix } from "@utils/date";
|
import { dateToUnix } from "@utils/date";
|
||||||
import { useContext, useEffect, useMemo, useRef } from "react";
|
import { useAccount } from "@utils/hooks/useAccount";
|
||||||
import useSWRInfinite from "swr/infinite";
|
import { useContext, useEffect, useRef } from "react";
|
||||||
import useSWRSubscription from "swr/subscription";
|
|
||||||
|
|
||||||
const ITEM_PER_PAGE = 10;
|
const ITEM_PER_PAGE = 10;
|
||||||
const TIME = Math.floor(Date.now() / 1000);
|
const TIME = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
const fetcher = async ([, offset]) => getNotes(TIME, ITEM_PER_PAGE, offset);
|
|
||||||
|
|
||||||
export function FollowingBlock({ block }: { block: number }) {
|
export function FollowingBlock({ block }: { block: number }) {
|
||||||
const ndk = useContext(RelayContext);
|
const ndk = useContext(RelayContext);
|
||||||
const account = useActiveAccount((state: any) => state.account);
|
|
||||||
|
|
||||||
const getKey = (pageIndex, previousPageData) => {
|
const { account } = useAccount();
|
||||||
if (previousPageData && !previousPageData.data) return null;
|
|
||||||
if (pageIndex === 0) return ["following", 0];
|
|
||||||
return ["following", previousPageData.nextCursor];
|
|
||||||
};
|
|
||||||
|
|
||||||
// fetch initial notes
|
const {
|
||||||
const { data, isLoading, size, setSize } = useSWRInfinite(getKey, fetcher);
|
status,
|
||||||
// fetch live notes
|
data,
|
||||||
useSWRSubscription(account ? "eventCollector" : null, () => {
|
fetchNextPage,
|
||||||
const follows = JSON.parse(account.follows);
|
hasNextPage,
|
||||||
const sub = ndk.subscribe({
|
isFetching,
|
||||||
kinds: [1, 6],
|
isFetchingNextPage,
|
||||||
authors: follows,
|
}: any = useInfiniteQuery({
|
||||||
since: dateToUnix(),
|
queryKey: ["newsfeed-circle"],
|
||||||
});
|
queryFn: async ({ pageParam = 0 }) => {
|
||||||
|
return await getNotes(TIME, ITEM_PER_PAGE, pageParam);
|
||||||
sub.addListener("event", (event: NDKEvent) => {
|
},
|
||||||
// save note
|
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
||||||
createNote(
|
|
||||||
event.id,
|
|
||||||
event.pubkey,
|
|
||||||
event.kind,
|
|
||||||
event.tags,
|
|
||||||
event.content,
|
|
||||||
event.created_at,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
sub.stop();
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const notes = useMemo(
|
const notes = data ? data.pages.flatMap((d: { data: any }) => d.data) : [];
|
||||||
() => (data ? data.flatMap((d) => d.data) : []),
|
|
||||||
[data],
|
|
||||||
);
|
|
||||||
|
|
||||||
const parentRef = useRef();
|
const parentRef = useRef();
|
||||||
|
|
||||||
const rowVirtualizer = useVirtualizer({
|
const rowVirtualizer = useVirtualizer({
|
||||||
count: notes.length,
|
count: hasNextPage ? notes.length + 1 : notes.length,
|
||||||
getScrollElement: () => parentRef.current,
|
getScrollElement: () => parentRef.current,
|
||||||
estimateSize: () => 500,
|
estimateSize: () => 500,
|
||||||
overscan: 2,
|
overscan: 2,
|
||||||
@@ -77,10 +53,43 @@ export function FollowingBlock({ block }: { block: number }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastItem.index >= notes.length - 1) {
|
if (
|
||||||
setSize(size + 1);
|
lastItem.index >= notes.length - 1 &&
|
||||||
|
hasNextPage &&
|
||||||
|
!isFetchingNextPage
|
||||||
|
) {
|
||||||
|
fetchNextPage();
|
||||||
}
|
}
|
||||||
}, [notes.length, rowVirtualizer.getVirtualItems()]);
|
}, [notes.length, fetchNextPage, rowVirtualizer.getVirtualItems()]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let sub: NDKSubscription;
|
||||||
|
|
||||||
|
if (account) {
|
||||||
|
const follows = JSON.parse(account.follows);
|
||||||
|
const filter: NDKFilter = {
|
||||||
|
kinds: [1, 6],
|
||||||
|
authors: follows,
|
||||||
|
since: dateToUnix(),
|
||||||
|
};
|
||||||
|
|
||||||
|
sub = ndk.subscribe(filter);
|
||||||
|
sub.addListener("event", (event: NDKEvent) => {
|
||||||
|
createNote(
|
||||||
|
event.id,
|
||||||
|
event.pubkey,
|
||||||
|
event.kind,
|
||||||
|
event.tags,
|
||||||
|
event.content,
|
||||||
|
event.created_at,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
sub.stop();
|
||||||
|
};
|
||||||
|
}, [account]);
|
||||||
|
|
||||||
const renderItem = (index: string | number) => {
|
const renderItem = (index: string | number) => {
|
||||||
const note = notes[index];
|
const note = notes[index];
|
||||||
@@ -101,7 +110,7 @@ export function FollowingBlock({ block }: { block: number }) {
|
|||||||
className="scrollbar-hide flex w-full h-full flex-col justify-between gap-1.5 pt-1.5 pb-20 overflow-y-auto"
|
className="scrollbar-hide flex w-full h-full flex-col justify-between gap-1.5 pt-1.5 pb-20 overflow-y-auto"
|
||||||
style={{ contain: "strict" }}
|
style={{ contain: "strict" }}
|
||||||
>
|
>
|
||||||
{!data || isLoading ? (
|
{status === "loading" ? (
|
||||||
<div className="px-3 py-1.5">
|
<div className="px-3 py-1.5">
|
||||||
<div className="rounded-md bg-zinc-900 px-3 py-3 shadow-input shadow-black/20">
|
<div className="rounded-md bg-zinc-900 px-3 py-3 shadow-input shadow-black/20">
|
||||||
<NoteSkeleton />
|
<NoteSkeleton />
|
||||||
@@ -129,6 +138,13 @@ export function FollowingBlock({ block }: { block: number }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{isFetching && !isFetchingNextPage && (
|
||||||
|
<div className="px-3 py-1.5">
|
||||||
|
<div className="rounded-md bg-zinc-900 px-3 py-3 shadow-input shadow-black/20">
|
||||||
|
<NoteSkeleton />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { getNoteByID } from "@libs/storage";
|
import { getNoteByID } from "@libs/storage";
|
||||||
import { ArrowLeftIcon } from "@shared/icons";
|
|
||||||
import { Kind1 } from "@shared/notes/contents/kind1";
|
import { Kind1 } from "@shared/notes/contents/kind1";
|
||||||
import { Kind1063 } from "@shared/notes/contents/kind1063";
|
import { Kind1063 } from "@shared/notes/contents/kind1063";
|
||||||
import { NoteMetadata } from "@shared/notes/metadata";
|
import { NoteMetadata } from "@shared/notes/metadata";
|
||||||
@@ -9,15 +8,19 @@ import { NoteSkeleton } from "@shared/notes/skeleton";
|
|||||||
import { TitleBar } from "@shared/titleBar";
|
import { TitleBar } from "@shared/titleBar";
|
||||||
import { User } from "@shared/user";
|
import { User } from "@shared/user";
|
||||||
import { useActiveAccount } from "@stores/accounts";
|
import { useActiveAccount } from "@stores/accounts";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { parser } from "@utils/parser";
|
import { parser } from "@utils/parser";
|
||||||
import useSWR from "swr";
|
|
||||||
|
|
||||||
const fetcher = ([, id]) => getNoteByID(id);
|
|
||||||
|
|
||||||
export function ThreadBlock({ params }: { params: any }) {
|
export function ThreadBlock({ params }: { params: any }) {
|
||||||
const { data } = useSWR(["thread", params.content], fetcher);
|
const { status, data, isFetching } = useQuery(
|
||||||
const removeBlock = useActiveAccount((state: any) => state.removeBlock);
|
["thread", params.content],
|
||||||
|
async () => {
|
||||||
|
return await getNoteByID(params.content);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const content = data ? parser(data) : null;
|
const content = data ? parser(data) : null;
|
||||||
|
const removeBlock = useActiveAccount((state: any) => state.removeBlock);
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
removeBlock(params.id, false);
|
removeBlock(params.id, false);
|
||||||
@@ -27,7 +30,7 @@ export function ThreadBlock({ params }: { params: any }) {
|
|||||||
<div className="shrink-0 w-[400px] border-r border-zinc-900">
|
<div className="shrink-0 w-[400px] border-r border-zinc-900">
|
||||||
<TitleBar title={params.title} onClick={() => close()} />
|
<TitleBar title={params.title} onClick={() => close()} />
|
||||||
<div className="scrollbar-hide flex w-full h-full flex-col gap-1.5 pt-1.5 pb-20 overflow-y-auto">
|
<div className="scrollbar-hide flex w-full h-full flex-col gap-1.5 pt-1.5 pb-20 overflow-y-auto">
|
||||||
{!data ? (
|
{status === "loading" || isFetching ? (
|
||||||
<div className="px-3 py-1.5">
|
<div className="px-3 py-1.5">
|
||||||
<div className="rounded-md bg-zinc-900 px-3 py-3 shadow-input shadow-black/20">
|
<div className="rounded-md bg-zinc-900 px-3 py-3 shadow-input shadow-black/20">
|
||||||
<NoteSkeleton />
|
<NoteSkeleton />
|
||||||
|
|||||||
@@ -3,18 +3,11 @@ import { FeedBlock } from "@app/space/components/blocks/feed";
|
|||||||
import { FollowingBlock } from "@app/space/components/blocks/following";
|
import { FollowingBlock } from "@app/space/components/blocks/following";
|
||||||
import { ImageBlock } from "@app/space/components/blocks/image";
|
import { ImageBlock } from "@app/space/components/blocks/image";
|
||||||
import { ThreadBlock } from "@app/space/components/blocks/thread";
|
import { ThreadBlock } from "@app/space/components/blocks/thread";
|
||||||
import { useActiveAccount } from "@stores/accounts";
|
import { getBlocks } from "@libs/storage";
|
||||||
import { useEffect } from "react";
|
|
||||||
|
|
||||||
export function Page() {
|
const blocks = await getBlocks();
|
||||||
const blocks = useActiveAccount((state: any) => state.blocks);
|
|
||||||
const fetchBlocks = useActiveAccount((state: any) => state.fetchBlocks);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (blocks !== null) return;
|
|
||||||
fetchBlocks();
|
|
||||||
}, [fetchBlocks]);
|
|
||||||
|
|
||||||
|
export function SpaceScreen() {
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full flex flex-nowrap overflow-x-auto overflow-y-hidden scrollbar-hide">
|
<div className="h-full w-full flex flex-nowrap overflow-x-auto overflow-y-hidden scrollbar-hide">
|
||||||
<FollowingBlock block={1} />
|
<FollowingBlock block={1} />
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { DefaultLayout as Layout } from "@shared/layout";
|
|
||||||
@@ -1,16 +1,20 @@
|
|||||||
import { Image } from "@shared/image";
|
import { Image } from "@shared/image";
|
||||||
import { DEFAULT_AVATAR } from "@stores/constants";
|
import { DEFAULT_AVATAR } from "@stores/constants";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { compactNumber } from "@utils/number";
|
import { compactNumber } from "@utils/number";
|
||||||
import { shortenKey } from "@utils/shortenKey";
|
import { shortenKey } from "@utils/shortenKey";
|
||||||
import useSWR from "swr";
|
|
||||||
|
|
||||||
const fetcher = (url: string) => fetch(url).then((r) => r.json());
|
|
||||||
|
|
||||||
export function Profile({ data }: { data: any }) {
|
export function Profile({ data }: { data: any }) {
|
||||||
const { data: userStats, error } = useSWR(
|
const {
|
||||||
`https://api.nostr.band/v0/stats/profile/${data.pubkey}`,
|
status,
|
||||||
fetcher,
|
data: userStats,
|
||||||
);
|
isFetching,
|
||||||
|
} = useQuery(["user-stats", data.pubkey], async () => {
|
||||||
|
const res = await fetch(
|
||||||
|
`https://api.nostr.band/v0/stats/profile/${data.pubkey}`,
|
||||||
|
);
|
||||||
|
return res.json();
|
||||||
|
});
|
||||||
|
|
||||||
const embedProfile = data.profile ? JSON.parse(data.profile.content) : null;
|
const embedProfile = data.profile ? JSON.parse(data.profile.content) : null;
|
||||||
const profile = embedProfile;
|
const profile = embedProfile;
|
||||||
@@ -47,8 +51,7 @@ export function Profile({ data }: { data: any }) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
{error && <p>Failed to fetch user stats</p>}
|
{status === "loading" || isFetching ? (
|
||||||
{!userStats ? (
|
|
||||||
<p>Loading...</p>
|
<p>Loading...</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-full flex items-center gap-8">
|
<div className="w-full flex items-center gap-8">
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
import { Note } from "@shared/notes/note";
|
import { Note } from "@shared/notes/note";
|
||||||
import { NoteSkeleton } from "@shared/notes/skeleton";
|
import { NoteSkeleton } from "@shared/notes/skeleton";
|
||||||
import { TitleBar } from "@shared/titleBar";
|
import { TitleBar } from "@shared/titleBar";
|
||||||
import useSWR from "swr";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
const fetcher = (url: string) => fetch(url).then((r) => r.json());
|
|
||||||
|
|
||||||
export function TrendingNotes() {
|
export function TrendingNotes() {
|
||||||
const { data, error } = useSWR(
|
const { status, data, isFetching } = useQuery(
|
||||||
"https://api.nostr.band/v0/trending/notes",
|
["trending-notes"],
|
||||||
fetcher,
|
async () => {
|
||||||
|
const res = await fetch("https://api.nostr.band/v0/trending/notes");
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="shrink-0 w-[360px] flex-col flex border-r border-zinc-900">
|
<div className="shrink-0 w-[360px] flex-col flex border-r border-zinc-900">
|
||||||
<TitleBar title="Trending Posts" />
|
<TitleBar title="Trending Posts" />
|
||||||
<div className="scrollbar-hide flex w-full h-full flex-col justify-between gap-1.5 pt-1.5 pb-20 overflow-y-auto">
|
<div className="scrollbar-hide flex w-full h-full flex-col justify-between gap-1.5 pt-1.5 pb-20 overflow-y-auto">
|
||||||
{error && <p>Failed to load...</p>}
|
{status === "loading" || isFetching ? (
|
||||||
{!data ? (
|
|
||||||
<div className="px-3 py-1.5">
|
<div className="px-3 py-1.5">
|
||||||
<div className="rounded-md bg-zinc-900 px-3 py-3 shadow-input shadow-black/20">
|
<div className="rounded-md bg-zinc-900 px-3 py-3 shadow-input shadow-black/20">
|
||||||
<NoteSkeleton />
|
<NoteSkeleton />
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
import { Profile } from "@app/trending/components/profile";
|
import { Profile } from "@app/trending/components/profile";
|
||||||
import { NoteSkeleton } from "@shared/notes/skeleton";
|
import { NoteSkeleton } from "@shared/notes/skeleton";
|
||||||
import { TitleBar } from "@shared/titleBar";
|
import { TitleBar } from "@shared/titleBar";
|
||||||
import useSWR from "swr";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
const fetcher = (url: string) => fetch(url).then((r) => r.json());
|
|
||||||
|
|
||||||
export function TrendingProfiles() {
|
export function TrendingProfiles() {
|
||||||
const { data, error } = useSWR(
|
const { status, data, isFetching } = useQuery(
|
||||||
"https://api.nostr.band/v0/trending/profiles",
|
["trending-profiles"],
|
||||||
fetcher,
|
async () => {
|
||||||
|
const res = await fetch("https://api.nostr.band/v0/trending/profiles");
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="shrink-0 w-[360px] flex-col flex border-r border-zinc-900">
|
<div className="shrink-0 w-[360px] flex-col flex border-r border-zinc-900">
|
||||||
<TitleBar title="Trending Profiles" />
|
<TitleBar title="Trending Profiles" />
|
||||||
<div className="scrollbar-hide flex w-full h-full flex-col justify-between gap-1.5 pt-1.5 pb-20 overflow-y-auto">
|
<div className="scrollbar-hide flex w-full h-full flex-col justify-between gap-1.5 pt-1.5 pb-20 overflow-y-auto">
|
||||||
{error && <p>Failed to load...</p>}
|
{status === "loading" || isFetching ? (
|
||||||
{!data ? (
|
|
||||||
<div className="px-3 py-1.5">
|
<div className="px-3 py-1.5">
|
||||||
<div className="rounded-md bg-zinc-900 px-3 py-3 shadow-input shadow-black/20">
|
<div className="rounded-md bg-zinc-900 px-3 py-3 shadow-input shadow-black/20">
|
||||||
<NoteSkeleton />
|
<NoteSkeleton />
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { TrendingNotes } from "@app/trending/components/trendingNotes";
|
import { TrendingNotes } from "@app/trending/components/trendingNotes";
|
||||||
import { TrendingProfiles } from "@app/trending/components/trendingProfiles";
|
import { TrendingProfiles } from "@app/trending/components/trendingProfiles";
|
||||||
|
|
||||||
export function Page() {
|
export function TrendingScreen() {
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full flex flex-nowrap overflow-x-auto overflow-y-hidden scrollbar-hide">
|
<div className="h-full w-full flex flex-nowrap overflow-x-auto overflow-y-hidden scrollbar-hide">
|
||||||
<TrendingProfiles />
|
<TrendingProfiles />
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { DefaultLayout as Layout } from "@shared/layout";
|
|
||||||
@@ -3,27 +3,30 @@ import { Image } from "@shared/image";
|
|||||||
import { RelayContext } from "@shared/relayProvider";
|
import { RelayContext } from "@shared/relayProvider";
|
||||||
import { useActiveAccount } from "@stores/accounts";
|
import { useActiveAccount } from "@stores/accounts";
|
||||||
import { DEFAULT_AVATAR } from "@stores/constants";
|
import { DEFAULT_AVATAR } from "@stores/constants";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { dateToUnix } from "@utils/date";
|
import { dateToUnix } from "@utils/date";
|
||||||
import { usePageContext } from "@utils/hooks/usePageContext";
|
import { usePageContext } from "@utils/hooks/usePageContext";
|
||||||
import { useProfile } from "@utils/hooks/useProfile";
|
import { useProfile } from "@utils/hooks/useProfile";
|
||||||
import { compactNumber } from "@utils/number";
|
import { compactNumber } from "@utils/number";
|
||||||
import { shortenKey } from "@utils/shortenKey";
|
import { shortenKey } from "@utils/shortenKey";
|
||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
import useSWR from "swr";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
const fetcher = (url: string) => fetch(url).then((r) => r.json());
|
export function UserScreen() {
|
||||||
|
|
||||||
export function Page() {
|
|
||||||
const ndk = useContext(RelayContext);
|
const ndk = useContext(RelayContext);
|
||||||
const pageContext = usePageContext();
|
const pageContext = usePageContext();
|
||||||
const searchParams: any = pageContext.urlParsed.search;
|
const searchParams: any = pageContext.urlParsed.search;
|
||||||
const pubkey = searchParams.pubkey || "";
|
const pubkey = searchParams.pubkey || "";
|
||||||
|
|
||||||
const { user } = useProfile(pubkey);
|
const { user } = useProfile(pubkey);
|
||||||
const { data: userStats, error } = useSWR(
|
const { data: userStats, error } = useQuery(["user", pubkey], async () => {
|
||||||
`https://api.nostr.band/v0/stats/profile/${pubkey}`,
|
const res = await fetch(
|
||||||
fetcher,
|
`https://api.nostr.band/v0/stats/profile/${pubkey}`,
|
||||||
);
|
);
|
||||||
|
if (res.ok) {
|
||||||
|
return await res.json();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const account = useActiveAccount((state: any) => state.account);
|
const account = useActiveAccount((state: any) => state.account);
|
||||||
const follows = account ? JSON.parse(account.follows) : [];
|
const follows = account ? JSON.parse(account.follows) : [];
|
||||||
@@ -180,12 +183,12 @@ export function Page() {
|
|||||||
Follow
|
Follow
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<a
|
<Link
|
||||||
href={`/app/chat?pubkey=${pubkey}`}
|
to={`/app/chat/${pubkey}`}
|
||||||
className="inline-flex w-44 h-10 items-center justify-center rounded-md bg-zinc-900 hover:bg-fuchsia-500 text-sm font-medium"
|
className="inline-flex w-44 h-10 items-center justify-center rounded-md bg-zinc-900 hover:bg-fuchsia-500 text-sm font-medium"
|
||||||
>
|
>
|
||||||
Message
|
Message
|
||||||
</a>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -18,8 +18,14 @@ export async function connect(): Promise<Database> {
|
|||||||
// get active account
|
// get active account
|
||||||
export async function getActiveAccount() {
|
export async function getActiveAccount() {
|
||||||
const db = await connect();
|
const db = await connect();
|
||||||
const result = await db.select("SELECT * FROM accounts WHERE is_active = 1;");
|
const result: any = await db.select(
|
||||||
return result[0];
|
"SELECT * FROM accounts WHERE is_active = 1;",
|
||||||
|
);
|
||||||
|
if (result.length > 0) {
|
||||||
|
return result[0];
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// get all accounts
|
// get all accounts
|
||||||
@@ -189,9 +195,10 @@ export async function createNote(
|
|||||||
// get note replies
|
// get note replies
|
||||||
export async function getReplies(parent_id: string) {
|
export async function getReplies(parent_id: string) {
|
||||||
const db = await connect();
|
const db = await connect();
|
||||||
return await db.select(
|
const result: any = await db.select(
|
||||||
`SELECT * FROM replies WHERE parent_id = "${parent_id}" ORDER BY created_at DESC;`,
|
`SELECT * FROM replies WHERE parent_id = "${parent_id}" ORDER BY created_at DESC;`,
|
||||||
);
|
);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// create reply note
|
// create reply note
|
||||||
@@ -214,7 +221,10 @@ export async function createReplyNote(
|
|||||||
// get all channels
|
// get all channels
|
||||||
export async function getChannels() {
|
export async function getChannels() {
|
||||||
const db = await connect();
|
const db = await connect();
|
||||||
return await db.select("SELECT * FROM channels ORDER BY created_at DESC;");
|
const result: any = await db.select(
|
||||||
|
"SELECT * FROM channels ORDER BY created_at DESC;",
|
||||||
|
);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// get channel by id
|
// get channel by id
|
||||||
@@ -230,13 +240,15 @@ export async function getChannel(id: string) {
|
|||||||
export async function createChannel(
|
export async function createChannel(
|
||||||
event_id: string,
|
event_id: string,
|
||||||
pubkey: string,
|
pubkey: string,
|
||||||
metadata: string,
|
name: string,
|
||||||
|
picture: string,
|
||||||
|
about: string,
|
||||||
created_at: number,
|
created_at: number,
|
||||||
) {
|
) {
|
||||||
const db = await connect();
|
const db = await connect();
|
||||||
return await db.execute(
|
return await db.execute(
|
||||||
"INSERT OR IGNORE INTO channels (event_id, pubkey, metadata, created_at) VALUES (?, ?, ?, ?);",
|
"INSERT OR IGNORE INTO channels (event_id, pubkey, name, picture, about, created_at) VALUES (?, ?, ?, ?, ?, ?);",
|
||||||
[event_id, pubkey, metadata, created_at],
|
[event_id, pubkey, name, picture, about, created_at],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,17 +291,19 @@ export async function getChannelMessages(channel_id: string) {
|
|||||||
// get channel users
|
// get channel users
|
||||||
export async function getChannelUsers(channel_id: string) {
|
export async function getChannelUsers(channel_id: string) {
|
||||||
const db = await connect();
|
const db = await connect();
|
||||||
return await db.select(
|
const result: any = await db.select(
|
||||||
`SELECT DISTINCT pubkey FROM channel_messages WHERE channel_id = "${channel_id}";`,
|
`SELECT DISTINCT pubkey FROM channel_messages WHERE channel_id = "${channel_id}";`,
|
||||||
);
|
);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// get all chats by pubkey
|
// get all chats by pubkey
|
||||||
export async function getChatsByPubkey(pubkey: string) {
|
export async function getChatsByPubkey(pubkey: string) {
|
||||||
const db = await connect();
|
const db = await connect();
|
||||||
return await db.select(
|
const result: any = await db.select(
|
||||||
`SELECT DISTINCT sender_pubkey FROM chats WHERE receiver_pubkey = "${pubkey}" ORDER BY created_at DESC;`,
|
`SELECT DISTINCT sender_pubkey FROM chats WHERE receiver_pubkey = "${pubkey}" ORDER BY created_at DESC;`,
|
||||||
);
|
);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// get chat messages
|
// get chat messages
|
||||||
@@ -390,11 +404,13 @@ export async function updateItemInBlacklist(content: string, status: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// get all blocks
|
// get all blocks
|
||||||
export async function getBlocks(account_id: number) {
|
export async function getBlocks() {
|
||||||
const db = await connect();
|
const db = await connect();
|
||||||
return await db.select(
|
const activeAccount = await getActiveAccount();
|
||||||
`SELECT * FROM blocks WHERE account_id <= "${account_id}";`,
|
const result: any = await db.select(
|
||||||
|
`SELECT * FROM blocks WHERE account_id <= "${activeAccount.id}";`,
|
||||||
);
|
);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// create block
|
// create block
|
||||||
|
|||||||
6
src/main.tsx
Normal file
6
src/main.tsx
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import App from "./app";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
|
||||||
|
const container = document.getElementById("root");
|
||||||
|
const root = createRoot(container);
|
||||||
|
root.render(<App />);
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import "./index.css";
|
|
||||||
import { Shell } from "./shell";
|
|
||||||
import { PageContextClient } from "./types";
|
|
||||||
import { StrictMode } from "react";
|
|
||||||
import { Root, createRoot, hydrateRoot } from "react-dom/client";
|
|
||||||
import "vidstack/styles/defaults.css";
|
|
||||||
|
|
||||||
export const clientRouting = true;
|
|
||||||
export const hydrationCanBeAborted = true;
|
|
||||||
|
|
||||||
let root: Root;
|
|
||||||
|
|
||||||
export async function render(pageContext: PageContextClient) {
|
|
||||||
const { Page, pageProps } = pageContext;
|
|
||||||
|
|
||||||
if (!Page)
|
|
||||||
throw new Error(
|
|
||||||
"Client-side render() hook expects pageContext.Page to be defined",
|
|
||||||
);
|
|
||||||
|
|
||||||
const page = (
|
|
||||||
<StrictMode>
|
|
||||||
<Shell pageContext={pageContext}>
|
|
||||||
<Page {...pageProps} />
|
|
||||||
</Shell>
|
|
||||||
</StrictMode>
|
|
||||||
);
|
|
||||||
|
|
||||||
const container = document.getElementById("app");
|
|
||||||
// SPA
|
|
||||||
if (container.innerHTML === "" || !pageContext.isHydration) {
|
|
||||||
if (!root) {
|
|
||||||
root = createRoot(container);
|
|
||||||
}
|
|
||||||
root.render(page);
|
|
||||||
// SSR
|
|
||||||
} else {
|
|
||||||
root = hydrateRoot(container, page);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import { Shell } from "./shell";
|
|
||||||
import { PageContextServer } from "./types";
|
|
||||||
import { StrictMode } from "react";
|
|
||||||
import ReactDOMServer from "react-dom/server";
|
|
||||||
import { dangerouslySkipEscape, escapeInject } from "vite-plugin-ssr/server";
|
|
||||||
|
|
||||||
export const passToClient = ["pageProps"];
|
|
||||||
|
|
||||||
export function render(pageContext: PageContextServer) {
|
|
||||||
let pageHtml: string;
|
|
||||||
|
|
||||||
if (!pageContext.Page) {
|
|
||||||
// SPA
|
|
||||||
pageHtml = "";
|
|
||||||
} else {
|
|
||||||
// SSR / HTML-only
|
|
||||||
const { Page, pageProps } = pageContext;
|
|
||||||
if (!Page)
|
|
||||||
throw new Error(
|
|
||||||
"My render() hook expects pageContext.Page to be defined",
|
|
||||||
);
|
|
||||||
|
|
||||||
pageHtml = ReactDOMServer.renderToString(
|
|
||||||
<StrictMode>
|
|
||||||
<Shell pageContext={pageContext}>
|
|
||||||
<Page {...pageProps} />
|
|
||||||
</Shell>
|
|
||||||
</StrictMode>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return escapeInject`<!DOCTYPE html>
|
|
||||||
<html lang="en" class="dark" suppressHydrationWarning>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
</head>
|
|
||||||
<body class="cursor-default select-none overflow-hidden font-sans antialiased bg-zinc-50 text-zinc-900 dark:bg-zinc-950 dark:text-zinc-100">
|
|
||||||
<div id="app">${dangerouslySkipEscape(pageHtml)}</div>
|
|
||||||
</body>
|
|
||||||
</html>`;
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
export function Page({ is404 }: { is404: boolean }) {
|
|
||||||
if (is404) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h1>404 Page Not Found</h1>
|
|
||||||
<p>This page could not be found.</p>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h1>500 Internal Server Error</h1>
|
|
||||||
<p>Something went wrong.</p>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export function LayoutDefault({ children }: { children: React.ReactNode }) {
|
|
||||||
return (
|
|
||||||
<div className="h-screen w-screen bg-zinc-50 text-zinc-900 dark:bg-black dark:text-zinc-100">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import { LayoutDefault } from "./layoutDefault";
|
|
||||||
import { PageContext } from "./types";
|
|
||||||
import { updateLastLogin } from "@libs/storage";
|
|
||||||
import { RelayProvider } from "@shared/relayProvider";
|
|
||||||
import { dateToUnix } from "@utils/date";
|
|
||||||
import { PageContextProvider } from "@utils/hooks/usePageContext";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
|
|
||||||
export function Shell({
|
|
||||||
children,
|
|
||||||
pageContext,
|
|
||||||
}: { children: React.ReactNode; pageContext: PageContext }) {
|
|
||||||
const Layout =
|
|
||||||
(pageContext.exports.Layout as React.ElementType) ||
|
|
||||||
(LayoutDefault as React.ElementType);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function initWindowEvent() {
|
|
||||||
const { TauriEvent } = await import("@tauri-apps/api/event");
|
|
||||||
const { appWindow, getCurrent } = await import("@tauri-apps/api/window");
|
|
||||||
|
|
||||||
// listen window close event
|
|
||||||
getCurrent().listen(TauriEvent.WINDOW_CLOSE_REQUESTED, () => {
|
|
||||||
// update last login time
|
|
||||||
updateLastLogin(dateToUnix());
|
|
||||||
// close window
|
|
||||||
appWindow.close();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
initWindowEvent().catch(console.error);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageContextProvider pageContext={pageContext}>
|
|
||||||
<RelayProvider>
|
|
||||||
<Layout>{children}</Layout>
|
|
||||||
</RelayProvider>
|
|
||||||
</PageContextProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import type {
|
|
||||||
PageContextBuiltIn,
|
|
||||||
/*
|
|
||||||
// When using Client Routing https://vite-plugin-ssr.com/clientRouting
|
|
||||||
PageContextBuiltInClientWithClientRouting as PageContextBuiltInClient
|
|
||||||
/*/
|
|
||||||
// When using Server Routing
|
|
||||||
PageContextBuiltInClientWithServerRouting as PageContextBuiltInClient, //*/
|
|
||||||
} from "vite-plugin-ssr/types";
|
|
||||||
|
|
||||||
export type { PageContextServer };
|
|
||||||
export type { PageContextClient };
|
|
||||||
export type { PageContext };
|
|
||||||
export type { PageProps };
|
|
||||||
|
|
||||||
type Page = (pageProps: PageProps) => React.ReactElement;
|
|
||||||
type PageProps = Record<string, never>;
|
|
||||||
|
|
||||||
export type PageContextCustom = {
|
|
||||||
Page: Page;
|
|
||||||
pageProps?: PageProps;
|
|
||||||
redirectTo?: string;
|
|
||||||
urlPathname: string;
|
|
||||||
exports: {
|
|
||||||
documentProps?: {
|
|
||||||
title?: string;
|
|
||||||
description?: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
type PageContextServer = PageContextBuiltIn<Page> & PageContextCustom;
|
|
||||||
type PageContextClient = PageContextBuiltInClient<Page> & PageContextCustom;
|
|
||||||
|
|
||||||
type PageContext = PageContextClient | PageContextServer;
|
|
||||||
@@ -1,26 +1,25 @@
|
|||||||
|
import { getLastLogin } from "@libs/storage";
|
||||||
import { Image } from "@shared/image";
|
import { Image } from "@shared/image";
|
||||||
import { NetworkStatusIndicator } from "@shared/networkStatusIndicator";
|
import { NetworkStatusIndicator } from "@shared/networkStatusIndicator";
|
||||||
import { RelayContext } from "@shared/relayProvider";
|
import { RelayContext } from "@shared/relayProvider";
|
||||||
import { useActiveAccount } from "@stores/accounts";
|
|
||||||
import { useChannels } from "@stores/channels";
|
import { useChannels } from "@stores/channels";
|
||||||
import { useChatMessages, useChats } from "@stores/chats";
|
import { useChats } from "@stores/chats";
|
||||||
import { DEFAULT_AVATAR } from "@stores/constants";
|
import { DEFAULT_AVATAR } from "@stores/constants";
|
||||||
import { useProfile } from "@utils/hooks/useProfile";
|
import { useProfile } from "@utils/hooks/useProfile";
|
||||||
import { sendNativeNotification } from "@utils/notification";
|
import { sendNativeNotification } from "@utils/notification";
|
||||||
import { useContext } from "react";
|
import { useContext, useEffect } from "react";
|
||||||
import useSWRSubscription from "swr/subscription";
|
|
||||||
|
const lastLogin = await getLastLogin();
|
||||||
|
|
||||||
export function ActiveAccount({ data }: { data: any }) {
|
export function ActiveAccount({ data }: { data: any }) {
|
||||||
const ndk = useContext(RelayContext);
|
const ndk = useContext(RelayContext);
|
||||||
|
|
||||||
const lastLogin = useActiveAccount((state: any) => state.lastLogin);
|
|
||||||
const notifyChat = useChats((state: any) => state.add);
|
const notifyChat = useChats((state: any) => state.add);
|
||||||
const saveChat = useChatMessages((state: any) => state.add);
|
|
||||||
const notifyChannel = useChannels((state: any) => state.add);
|
const notifyChannel = useChannels((state: any) => state.add);
|
||||||
|
|
||||||
const { user } = useProfile(data.pubkey);
|
const { status, user } = useProfile(data.pubkey);
|
||||||
|
|
||||||
useSWRSubscription(user ? ["activeAccount", data.pubkey] : null, () => {
|
useEffect(() => {
|
||||||
const since = lastLogin > 0 ? lastLogin : Math.floor(Date.now() / 1000);
|
const since = lastLogin > 0 ? lastLogin : Math.floor(Date.now() / 1000);
|
||||||
// subscribe to channel
|
// subscribe to channel
|
||||||
const sub = ndk.subscribe(
|
const sub = ndk.subscribe(
|
||||||
@@ -41,8 +40,6 @@ export function ActiveAccount({ data }: { data: any }) {
|
|||||||
sendNativeNotification("Someone mention you");
|
sendNativeNotification("Someone mention you");
|
||||||
break;
|
break;
|
||||||
case 4:
|
case 4:
|
||||||
// save
|
|
||||||
saveChat(data.pubkey, event);
|
|
||||||
// update state
|
// update state
|
||||||
notifyChat(event.pubkey);
|
notifyChat(event.pubkey);
|
||||||
// send native notifiation
|
// send native notifiation
|
||||||
@@ -62,16 +59,20 @@ export function ActiveAccount({ data }: { data: any }) {
|
|||||||
return () => {
|
return () => {
|
||||||
sub.stop();
|
sub.stop();
|
||||||
};
|
};
|
||||||
});
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button type="button" className="relative inline-block h-9 w-9">
|
<button type="button" className="relative inline-block h-9 w-9">
|
||||||
<Image
|
{status === "loading" ? (
|
||||||
src={user?.image}
|
<div className="w-9 h-9 rounded bg-zinc-800 animate-pulse" />
|
||||||
fallback={DEFAULT_AVATAR}
|
) : (
|
||||||
alt={data.npub}
|
<Image
|
||||||
className="h-9 w-9 rounded object-cover"
|
src={user.image}
|
||||||
/>
|
fallback={DEFAULT_AVATAR}
|
||||||
|
alt={data.npub}
|
||||||
|
className="h-9 w-9 rounded object-cover"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<NetworkStatusIndicator />
|
<NetworkStatusIndicator />
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
import { Link } from "@shared/link";
|
|
||||||
import { usePageContext } from "@utils/hooks/usePageContext";
|
|
||||||
import { twMerge } from "tailwind-merge";
|
|
||||||
|
|
||||||
export function ActiveLink({
|
|
||||||
href,
|
|
||||||
className,
|
|
||||||
activeClassName,
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
href: string;
|
|
||||||
className: string;
|
|
||||||
activeClassName: string;
|
|
||||||
children: React.ReactNode;
|
|
||||||
}) {
|
|
||||||
const pageContext = usePageContext();
|
|
||||||
const pathName = pageContext.urlPathname;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
href={href}
|
|
||||||
className={twMerge(className, href === pathName ? activeClassName : "")}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,15 @@
|
|||||||
import { ArrowLeftIcon, ArrowRightIcon } from "@shared/icons";
|
import { ArrowLeftIcon, ArrowRightIcon } from "@shared/icons";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
export function AppHeader({ reverse }: { reverse?: boolean }) {
|
export function AppHeader({ reverse }: { reverse?: boolean }) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const goBack = () => {
|
const goBack = () => {
|
||||||
window.history.back();
|
navigate(-1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const goForward = () => {
|
const goForward = () => {
|
||||||
window.history.forward();
|
navigate(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
16
src/shared/appLayout.tsx
Normal file
16
src/shared/appLayout.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { Navigation } from "@shared/navigation";
|
||||||
|
import { Outlet, ScrollRestoration } from "react-router-dom";
|
||||||
|
|
||||||
|
export function AppLayout() {
|
||||||
|
return (
|
||||||
|
<div className="flex w-screen h-screen">
|
||||||
|
<div className="relative flex flex-row shrink-0">
|
||||||
|
<Navigation />
|
||||||
|
</div>
|
||||||
|
<div className="w-full h-full">
|
||||||
|
<Outlet />
|
||||||
|
<ScrollRestoration />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,20 +1,18 @@
|
|||||||
import { ArrowLeftIcon, ArrowRightIcon } from "@shared/icons";
|
import { ArrowLeftIcon, ArrowRightIcon } from "@shared/icons";
|
||||||
import useSWR from "swr";
|
import { platform } from "@tauri-apps/api/os";
|
||||||
|
import { Outlet, useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
const fetcher = async () => {
|
const platformName = await platform();
|
||||||
const { platform } = await import("@tauri-apps/api/os");
|
|
||||||
return await platform();
|
|
||||||
};
|
|
||||||
|
|
||||||
export function LayoutOnboarding({ children }: { children: React.ReactNode }) {
|
export function AuthLayout() {
|
||||||
const { data: platform } = useSWR("platform", fetcher);
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const goBack = () => {
|
const goBack = () => {
|
||||||
window.history.back();
|
navigate(-1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const goForward = () => {
|
const goForward = () => {
|
||||||
window.history.forward();
|
navigate(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -22,7 +20,7 @@ export function LayoutOnboarding({ children }: { children: React.ReactNode }) {
|
|||||||
<div className="flex h-screen w-full flex-col">
|
<div className="flex h-screen w-full flex-col">
|
||||||
<div
|
<div
|
||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
className="relative h-9 shrink-0 border border-zinc-100 bg-white dark:border-zinc-900 dark:bg-black"
|
className="relative h-11 shrink-0 border border-zinc-100 bg-white dark:border-zinc-900 dark:bg-black"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
@@ -30,7 +28,7 @@ export function LayoutOnboarding({ children }: { children: React.ReactNode }) {
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`flex h-full items-center gap-2 ${
|
className={`flex h-full items-center gap-2 ${
|
||||||
platform === "darwin" ? "pl-[68px]" : ""
|
platformName === "darwin" ? "pl-[68px]" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@@ -58,7 +56,9 @@ export function LayoutOnboarding({ children }: { children: React.ReactNode }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative flex min-h-0 w-full flex-1">{children}</div>
|
<div className="relative flex min-h-0 w-full flex-1">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -15,7 +15,8 @@ import { Fragment } from "react";
|
|||||||
import { useHotkeys } from "react-hotkeys-hook";
|
import { useHotkeys } from "react-hotkeys-hook";
|
||||||
|
|
||||||
export function Composer() {
|
export function Composer() {
|
||||||
const account = useActiveAccount((state: any) => state.account);
|
const account = useActiveAccount((state) => state.account);
|
||||||
|
|
||||||
const [toggle, open] = useComposer((state: any) => [
|
const [toggle, open] = useComposer((state: any) => [
|
||||||
state.toggleModal,
|
state.toggleModal,
|
||||||
state.open,
|
state.open,
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
import { Navigation } from "@shared/navigation";
|
|
||||||
import useSWR from "swr";
|
|
||||||
|
|
||||||
const fetcher = async () => {
|
|
||||||
const { platform } = await import("@tauri-apps/api/os");
|
|
||||||
return platform();
|
|
||||||
};
|
|
||||||
|
|
||||||
export function DefaultLayout({ children }: { children: React.ReactNode }) {
|
|
||||||
const { data: platform } = useSWR(
|
|
||||||
typeof window !== "undefined" ? "platform" : null,
|
|
||||||
fetcher,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex w-screen h-screen">
|
|
||||||
<div className="relative flex flex-row shrink-0">
|
|
||||||
<Navigation reverse={platform !== "darwin"} />
|
|
||||||
</div>
|
|
||||||
<div className="w-full h-full">{children}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { ReactNode } from "react";
|
|
||||||
import { navigate } from "vite-plugin-ssr/client/router";
|
|
||||||
|
|
||||||
export function Link({
|
|
||||||
href,
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
}: { href: string; className?: string; children: ReactNode }) {
|
|
||||||
const goto = () => {
|
|
||||||
navigate(href, { keepScrollPosition: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button type="button" onClick={() => goto()} className={className}>
|
|
||||||
{children}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,18 +1,19 @@
|
|||||||
import { Transition } from "@headlessui/react";
|
import { Transition } from "@headlessui/react";
|
||||||
import { getAccounts, getActiveAccount } from "@libs/storage";
|
import { getActiveAccount } from "@libs/storage";
|
||||||
import { ActiveAccount } from "@shared/accounts/active";
|
import { ActiveAccount } from "@shared/accounts/active";
|
||||||
import { InactiveAccount } from "@shared/accounts/inactive";
|
import { VerticalDotsIcon } from "@shared/icons";
|
||||||
import { PlusIcon, VerticalDotsIcon } from "@shared/icons";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { Link } from "@shared/link";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import useSWR from "swr";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
const allFetcher = () => getAccounts();
|
|
||||||
const fetcher = () => getActiveAccount();
|
|
||||||
|
|
||||||
export function MultiAccounts() {
|
export function MultiAccounts() {
|
||||||
const { data: accounts }: any = useSWR("allAccounts", allFetcher);
|
const {
|
||||||
const { data: activeAccount }: any = useSWR("activeAccount", fetcher);
|
status,
|
||||||
|
data: activeAccount,
|
||||||
|
isFetching,
|
||||||
|
} = useQuery(["activeAccount"], async () => {
|
||||||
|
return await getActiveAccount();
|
||||||
|
});
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
@@ -24,28 +25,11 @@ export function MultiAccounts() {
|
|||||||
<div className="flex flex-col gap-2 rounded-xl p-2 border-t border-zinc-800/50 bg-zinc-900/80 backdrop-blur-md">
|
<div className="flex flex-col gap-2 rounded-xl p-2 border-t border-zinc-800/50 bg-zinc-900/80 backdrop-blur-md">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{!activeAccount ? (
|
{status === "loading" || isFetching ? (
|
||||||
<div className="group relative flex h-9 w-9 shrink animate-pulse items-center justify-center rounded-lg bg-zinc-900" />
|
<div className="group relative flex h-9 w-9 shrink animate-pulse items-center justify-center rounded-lg bg-zinc-900" />
|
||||||
) : (
|
) : (
|
||||||
<ActiveAccount data={activeAccount} />
|
<ActiveAccount data={activeAccount} />
|
||||||
)}
|
)}
|
||||||
{!accounts ? (
|
|
||||||
<div className="group relative flex h-9 w-9 shrink animate-pulse items-center justify-center rounded-lg bg-zinc-900" />
|
|
||||||
) : (
|
|
||||||
accounts.map((account: { is_active: number; pubkey: string }) => (
|
|
||||||
<InactiveAccount key={account.pubkey} data={account} />
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="group relative flex h-9 w-9 shrink items-center justify-center rounded border border-dashed border-zinc-600 hover:border-zinc-400"
|
|
||||||
>
|
|
||||||
<PlusIcon
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
className="text-zinc-400 group-hover:text-zinc-100"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -66,13 +50,13 @@ export function MultiAccounts() {
|
|||||||
className="flex flex-col items-start justify-start gap-1 pt-1.5 border-t border-zinc-800 transform"
|
className="flex flex-col items-start justify-start gap-1 pt-1.5 border-t border-zinc-800 transform"
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
href="/app/settings"
|
to="/app/settings"
|
||||||
className="w-full py-2 px-2 rounded hover:bg-zinc-800 text-zinc-100 text-start text-sm"
|
className="w-full py-2 px-2 rounded hover:bg-zinc-800 text-zinc-100 text-start text-sm"
|
||||||
>
|
>
|
||||||
Settings
|
Settings
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href="/app/logout"
|
to="/app/logout"
|
||||||
className="w-full py-2 px-2 rounded hover:bg-zinc-800 text-zinc-100 text-start text-sm"
|
className="w-full py-2 px-2 rounded hover:bg-zinc-800 text-zinc-100 text-start text-sm"
|
||||||
>
|
>
|
||||||
Logout
|
Logout
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { ChannelsList } from "@app/channel/components/list";
|
import { ChannelsList } from "@app/channel/components/list";
|
||||||
import { ChatsList } from "@app/chat/components/list";
|
import { ChatsList } from "@app/chat/components/list";
|
||||||
import { Disclosure } from "@headlessui/react";
|
import { Disclosure } from "@headlessui/react";
|
||||||
import { ActiveLink } from "@shared/activeLink";
|
|
||||||
import { AppHeader } from "@shared/appHeader";
|
import { AppHeader } from "@shared/appHeader";
|
||||||
import { Composer } from "@shared/composer/modal";
|
import { Composer } from "@shared/composer/modal";
|
||||||
import { NavArrowDownIcon, SpaceIcon, TrendingIcon } from "@shared/icons";
|
import { NavArrowDownIcon, SpaceIcon, TrendingIcon } from "@shared/icons";
|
||||||
import { MultiAccounts } from "@shared/multiAccounts";
|
import { MultiAccounts } from "@shared/multiAccounts";
|
||||||
|
import { NavLink } from "react-router-dom";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
export function Navigation({ reverse }: { reverse?: boolean }) {
|
export function Navigation({ reverse = false }: { reverse?: boolean }) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`relative flex w-[232px] flex-col gap-3 ${
|
className={`relative flex w-[232px] flex-col gap-3 ${
|
||||||
@@ -27,20 +28,28 @@ export function Navigation({ reverse }: { reverse?: boolean }) {
|
|||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<ActiveLink
|
<NavLink
|
||||||
href="/app/space"
|
to="/app/space"
|
||||||
className="flex h-9 items-center gap-2.5 rounded-md px-2.5 text-zinc-200"
|
className={({ isActive }) =>
|
||||||
activeClassName="bg-zinc-900/50"
|
twMerge(
|
||||||
|
"flex h-9 items-center gap-2.5 rounded-md px-2.5 text-zinc-200",
|
||||||
|
isActive ? "bg-zinc-900/50" : "",
|
||||||
|
)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<span className="inline-flex h-6 w-6 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
|
<span className="inline-flex h-6 w-6 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
|
||||||
<SpaceIcon width={12} height={12} className="text-zinc-100" />
|
<SpaceIcon width={12} height={12} className="text-zinc-100" />
|
||||||
</span>
|
</span>
|
||||||
<span className="font-medium">Spaces</span>
|
<span className="font-medium">Spaces</span>
|
||||||
</ActiveLink>
|
</NavLink>
|
||||||
<ActiveLink
|
<NavLink
|
||||||
href="/app/trending"
|
to="/app/trending"
|
||||||
className="flex h-9 items-center gap-2.5 rounded-md px-2.5 text-zinc-200"
|
className={({ isActive }) =>
|
||||||
activeClassName="bg-zinc-900/50"
|
twMerge(
|
||||||
|
"flex h-9 items-center gap-2.5 rounded-md px-2.5 text-zinc-200",
|
||||||
|
isActive ? "bg-zinc-900/50" : "",
|
||||||
|
)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<span className="inline-flex h-6 w-6 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
|
<span className="inline-flex h-6 w-6 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
|
||||||
<TrendingIcon
|
<TrendingIcon
|
||||||
@@ -50,7 +59,7 @@ export function Navigation({ reverse }: { reverse?: boolean }) {
|
|||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<span className="font-medium">Trending</span>
|
<span className="font-medium">Trending</span>
|
||||||
</ActiveLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Channels */}
|
{/* Channels */}
|
||||||
|
|||||||
@@ -3,18 +3,19 @@ import { Kind1063 } from "@shared/notes/contents/kind1063";
|
|||||||
import { NoteSkeleton } from "@shared/notes/skeleton";
|
import { NoteSkeleton } from "@shared/notes/skeleton";
|
||||||
import { User } from "@shared/user";
|
import { User } from "@shared/user";
|
||||||
import { useEvent } from "@utils/hooks/useEvent";
|
import { useEvent } from "@utils/hooks/useEvent";
|
||||||
import { parser } from "@utils/parser";
|
|
||||||
import { memo } from "react";
|
import { memo } from "react";
|
||||||
|
|
||||||
export const MentionNote = memo(function MentionNote({ id }: { id: string }) {
|
export const MentionNote = memo(function MentionNote({ id }: { id: string }) {
|
||||||
const data = useEvent(id);
|
const { status, data, isFetching } = useEvent(id);
|
||||||
|
|
||||||
const kind1 = data?.kind === 1 ? parser(data) : null;
|
const kind1 = data?.kind === 1 ? data.content : null;
|
||||||
const kind1063 = data?.kind === 1063 ? data.tags : null;
|
const kind1063 = data?.kind === 1063 ? data.tags : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-3 rounded-lg border border-zinc-800 px-3 py-3">
|
<div className="mt-3 rounded-lg border border-zinc-800 px-3 py-3">
|
||||||
{data ? (
|
{isFetching || status === "loading" ? (
|
||||||
|
<NoteSkeleton />
|
||||||
|
) : (
|
||||||
<>
|
<>
|
||||||
<User pubkey={data.pubkey} time={data.created_at} size="small" />
|
<User pubkey={data.pubkey} time={data.created_at} size="small" />
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
@@ -37,8 +38,6 @@ export const MentionNote = memo(function MentionNote({ id }: { id: string }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
|
||||||
<NoteSkeleton />
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { Link } from "@shared/link";
|
|
||||||
import { useProfile } from "@utils/hooks/useProfile";
|
import { useProfile } from "@utils/hooks/useProfile";
|
||||||
import { shortenKey } from "@utils/shortenKey";
|
import { shortenKey } from "@utils/shortenKey";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
export function MentionUser({ pubkey }: { pubkey: string }) {
|
export function MentionUser({ pubkey }: { pubkey: string }) {
|
||||||
const { user } = useProfile(pubkey);
|
const { user } = useProfile(pubkey);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
href={`/app/user?pubkey=${pubkey}`}
|
to={`/app/user/${pubkey}`}
|
||||||
className="text-fuchsia-500 hover:text-fuchsia-600 no-underline font-normal"
|
className="text-fuchsia-500 hover:text-fuchsia-600 no-underline font-normal"
|
||||||
>
|
>
|
||||||
@{user?.name || user?.displayName || shortenKey(pubkey)}
|
@{user?.name || user?.displayName || shortenKey(pubkey)}
|
||||||
|
|||||||
@@ -5,57 +5,9 @@ import { NoteReply } from "@shared/notes/metadata/reply";
|
|||||||
import { NoteRepost } from "@shared/notes/metadata/repost";
|
import { NoteRepost } from "@shared/notes/metadata/repost";
|
||||||
import { NoteZap } from "@shared/notes/metadata/zap";
|
import { NoteZap } from "@shared/notes/metadata/zap";
|
||||||
import { RelayContext } from "@shared/relayProvider";
|
import { RelayContext } from "@shared/relayProvider";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { decode } from "light-bolt11-decoder";
|
import { decode } from "light-bolt11-decoder";
|
||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
import useSWR from "swr";
|
|
||||||
|
|
||||||
const fetcher = async ([, ndk, id]) => {
|
|
||||||
let replies = 0;
|
|
||||||
let reposts = 0;
|
|
||||||
let zap = 0;
|
|
||||||
|
|
||||||
const filter: NDKFilter = {
|
|
||||||
"#e": [id],
|
|
||||||
kinds: [1, 6, 9735],
|
|
||||||
};
|
|
||||||
|
|
||||||
const events = await ndk.fetchEvents(filter);
|
|
||||||
events.forEach((event: NDKEvent) => {
|
|
||||||
switch (event.kind) {
|
|
||||||
case 1:
|
|
||||||
replies += 1;
|
|
||||||
createReplyNote(
|
|
||||||
id,
|
|
||||||
event.id,
|
|
||||||
event.pubkey,
|
|
||||||
event.kind,
|
|
||||||
event.tags,
|
|
||||||
event.content,
|
|
||||||
event.created_at,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case 6:
|
|
||||||
reposts += 1;
|
|
||||||
break;
|
|
||||||
case 9735: {
|
|
||||||
const bolt11 = event.tags.find((tag) => tag[0] === "bolt11")[1];
|
|
||||||
if (bolt11) {
|
|
||||||
const decoded = decode(bolt11);
|
|
||||||
const amount = decoded.sections.find(
|
|
||||||
(item) => item.name === "amount",
|
|
||||||
);
|
|
||||||
const sats = amount.value / 1000;
|
|
||||||
zap += sats;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return { replies, reposts, zap };
|
|
||||||
};
|
|
||||||
|
|
||||||
export function NoteMetadata({
|
export function NoteMetadata({
|
||||||
id,
|
id,
|
||||||
@@ -67,11 +19,60 @@ export function NoteMetadata({
|
|||||||
currentBlock?: number;
|
currentBlock?: number;
|
||||||
}) {
|
}) {
|
||||||
const ndk = useContext(RelayContext);
|
const ndk = useContext(RelayContext);
|
||||||
const { data, isLoading } = useSWR(["note-metadata", ndk, id], fetcher);
|
const { status, data, isFetching } = useQuery(
|
||||||
|
["note-metadata", id],
|
||||||
|
async () => {
|
||||||
|
let replies = 0;
|
||||||
|
let reposts = 0;
|
||||||
|
let zap = 0;
|
||||||
|
|
||||||
|
const filter: NDKFilter = {
|
||||||
|
"#e": [id],
|
||||||
|
kinds: [1, 6, 9735],
|
||||||
|
};
|
||||||
|
|
||||||
|
const events = await ndk.fetchEvents(filter);
|
||||||
|
events.forEach((event: NDKEvent) => {
|
||||||
|
switch (event.kind) {
|
||||||
|
case 1:
|
||||||
|
replies += 1;
|
||||||
|
createReplyNote(
|
||||||
|
id,
|
||||||
|
event.id,
|
||||||
|
event.pubkey,
|
||||||
|
event.kind,
|
||||||
|
event.tags,
|
||||||
|
event.content,
|
||||||
|
event.created_at,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 6:
|
||||||
|
reposts += 1;
|
||||||
|
break;
|
||||||
|
case 9735: {
|
||||||
|
const bolt11 = event.tags.find((tag) => tag[0] === "bolt11")[1];
|
||||||
|
if (bolt11) {
|
||||||
|
const decoded = decode(bolt11);
|
||||||
|
const amount = decoded.sections.find(
|
||||||
|
(item) => item.name === "amount",
|
||||||
|
);
|
||||||
|
const sats = amount.value / 1000;
|
||||||
|
zap += sats;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { replies, reposts, zap };
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="inline-flex items-center w-full h-12 mt-2">
|
<div className="inline-flex items-center w-full h-12 mt-2">
|
||||||
{!data || isLoading ? (
|
{status === "loading" || isFetching ? (
|
||||||
<>
|
<>
|
||||||
<div className="w-20 group inline-flex items-center gap-1.5">
|
<div className="w-20 group inline-flex items-center gap-1.5">
|
||||||
<ReplyIcon
|
<ReplyIcon
|
||||||
|
|||||||
@@ -4,21 +4,22 @@ import { NoteMetadata } from "@shared/notes/metadata";
|
|||||||
import { NoteSkeleton } from "@shared/notes/skeleton";
|
import { NoteSkeleton } from "@shared/notes/skeleton";
|
||||||
import { User } from "@shared/user";
|
import { User } from "@shared/user";
|
||||||
import { useEvent } from "@utils/hooks/useEvent";
|
import { useEvent } from "@utils/hooks/useEvent";
|
||||||
import { parser } from "@utils/parser";
|
|
||||||
|
|
||||||
export function NoteParent({
|
export function NoteParent({
|
||||||
id,
|
id,
|
||||||
currentBlock,
|
currentBlock,
|
||||||
}: { id: string; currentBlock: number }) {
|
}: { id: string; currentBlock: number }) {
|
||||||
const data = useEvent(id);
|
const { status, data, isFetching } = useEvent(id);
|
||||||
|
|
||||||
const kind1 = data?.kind === 1 ? parser(data) : null;
|
const kind1 = data?.kind === 1 ? data.content : null;
|
||||||
const kind1063 = data?.kind === 1063 ? data.tags : null;
|
const kind1063 = data?.kind === 1063 ? data.tags : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative overflow-hidden flex flex-col pb-6">
|
<div className="relative overflow-hidden flex flex-col pb-6">
|
||||||
<div className="absolute left-[18px] top-0 h-full w-0.5 bg-gradient-to-t from-zinc-800 to-zinc-600" />
|
<div className="absolute left-[18px] top-0 h-full w-0.5 bg-gradient-to-t from-zinc-800 to-zinc-600" />
|
||||||
{data ? (
|
{isFetching || status === "loading" ? (
|
||||||
|
<NoteSkeleton />
|
||||||
|
) : (
|
||||||
<>
|
<>
|
||||||
<User pubkey={data.pubkey} time={data.created_at} />
|
<User pubkey={data.pubkey} time={data.created_at} />
|
||||||
<div className="-mt-5 pl-[49px]">
|
<div className="-mt-5 pl-[49px]">
|
||||||
@@ -46,8 +47,6 @@ export function NoteParent({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
|
||||||
<NoteSkeleton />
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,16 +1,26 @@
|
|||||||
import { Image } from "@shared/image";
|
import { Image } from "@shared/image";
|
||||||
import { useOpenGraph } from "@utils/hooks/useOpenGraph";
|
import { useOpenGraph } from "@utils/hooks/useOpenGraph";
|
||||||
|
|
||||||
|
function isValidURL(string: string) {
|
||||||
|
let url: URL;
|
||||||
|
try {
|
||||||
|
url = new URL(string);
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
export function LinkPreview({ urls }: { urls: string[] }) {
|
export function LinkPreview({ urls }: { urls: string[] }) {
|
||||||
const domain = new URL(urls[0]);
|
const domain = new URL(urls[0]);
|
||||||
const { data, error, isLoading } = useOpenGraph(urls[0]);
|
const { status, data, error, isFetching } = useOpenGraph(urls[0]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-3 max-w-[420px] overflow-hidden rounded-lg bg-zinc-800">
|
<div className="mt-3 max-w-[420px] overflow-hidden rounded-lg bg-zinc-800">
|
||||||
{error && <p>failed to load</p>}
|
{error && <p>failed to load</p>}
|
||||||
{isLoading || !data ? (
|
{isFetching || status === "loading" ? (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="w-full h-16 bg-zinc-700 animate-pulse" />
|
<div className="w-full h-44 bg-zinc-700 animate-pulse" />
|
||||||
<div className="flex flex-col gap-2 px-3 py-3">
|
<div className="flex flex-col gap-2 px-3 py-3">
|
||||||
<div className="w-2/3 h-3 rounded bg-zinc-700 animate-pulse" />
|
<div className="w-2/3 h-3 rounded bg-zinc-700 animate-pulse" />
|
||||||
<div className="w-3/4 h-3 rounded bg-zinc-700 animate-pulse" />
|
<div className="w-3/4 h-3 rounded bg-zinc-700 animate-pulse" />
|
||||||
@@ -19,6 +29,20 @@ export function LinkPreview({ urls }: { urls: string[] }) {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : !data ? (
|
||||||
|
<a
|
||||||
|
className="flex flex-col px-3 py-3 rounded-lg border border-transparent hover:border-fuchsia-900"
|
||||||
|
href={urls[0]}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
<p className="leading-none text-sm text-zinc-400 line-clamp-3">
|
||||||
|
Can't fetch open graph, click to open website directly
|
||||||
|
</p>
|
||||||
|
<span className="mt-2.5 leading-none text-sm text-zinc-500">
|
||||||
|
{domain.hostname}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
) : (
|
) : (
|
||||||
<a
|
<a
|
||||||
className="flex flex-col rounded-lg border border-transparent hover:border-fuchsia-900"
|
className="flex flex-col rounded-lg border border-transparent hover:border-fuchsia-900"
|
||||||
@@ -26,13 +50,20 @@ export function LinkPreview({ urls }: { urls: string[] }) {
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
{data["og:image"] && (
|
{isValidURL(data["og:image"]) ? (
|
||||||
<Image
|
<Image
|
||||||
src={data["og:image"]}
|
src={data["og:image"]}
|
||||||
fallback="https://void.cat/d/XTmrMkpid8DGLjv1AzdvcW"
|
fallback="https://void.cat/d/XTmrMkpid8DGLjv1AzdvcW"
|
||||||
alt={urls[0]}
|
alt={urls[0]}
|
||||||
className="w-full h-44 object-cover rounded-t-lg bg-white"
|
className="w-full h-44 object-cover rounded-t-lg bg-white"
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<Image
|
||||||
|
src="https://void.cat/d/XTmrMkpid8DGLjv1AzdvcW"
|
||||||
|
fallback="https://void.cat/d/XTmrMkpid8DGLjv1AzdvcW"
|
||||||
|
alt={urls[0]}
|
||||||
|
className="w-full h-44 object-cover rounded-t-lg bg-white"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-col gap-2 px-3 py-3">
|
<div className="flex flex-col gap-2 px-3 py-3">
|
||||||
<h5 className="leading-none font-medium text-zinc-200">
|
<h5 className="leading-none font-medium text-zinc-200">
|
||||||
|
|||||||
@@ -1,17 +1,9 @@
|
|||||||
import { MediaOutlet, MediaPlayer } from "@vidstack/react";
|
|
||||||
|
|
||||||
export function VideoPreview({ urls }: { urls: string[] }) {
|
export function VideoPreview({ urls }: { urls: string[] }) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
onKeyDown={(e) => e.stopPropagation()}
|
onKeyDown={(e) => e.stopPropagation()}
|
||||||
className="relative mt-3 max-w-[420px] flex w-full flex-col overflow-hidden rounded-lg bg-zinc-950"
|
className="relative mt-3 max-w-[420px] flex w-full flex-col overflow-hidden rounded-lg bg-zinc-950"
|
||||||
>
|
/>
|
||||||
{urls.map((url: string) => (
|
|
||||||
<MediaPlayer key={url} src={urls[0]} poster="" controls>
|
|
||||||
<MediaOutlet />
|
|
||||||
</MediaPlayer>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ import { useContext, useState } from "react";
|
|||||||
|
|
||||||
export function NoteReplyForm({ id }: { id: string }) {
|
export function NoteReplyForm({ id }: { id: string }) {
|
||||||
const ndk = useContext(RelayContext);
|
const ndk = useContext(RelayContext);
|
||||||
const account = useActiveAccount((state: any) => state.account);
|
const account = useActiveAccount((state) => state.account);
|
||||||
const { user } = useProfile(account.npub);
|
|
||||||
|
const { status, user } = useProfile(account.npub);
|
||||||
|
|
||||||
const [value, setValue] = useState("");
|
const [value, setValue] = useState("");
|
||||||
|
|
||||||
@@ -46,35 +47,41 @@ export function NoteReplyForm({ id }: { id: string }) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="border-t border-zinc-800 w-full py-3 px-5">
|
<div className="border-t border-zinc-800 w-full py-3 px-5">
|
||||||
<div className="flex w-full items-center justify-between">
|
{status === "loading" ? (
|
||||||
<div className="inline-flex items-center gap-2">
|
<div>
|
||||||
<div className="relative h-9 w-9 shrink-0 rounded">
|
<p>Loading...</p>
|
||||||
<Image
|
</div>
|
||||||
src={user?.image}
|
) : (
|
||||||
fallback={DEFAULT_AVATAR}
|
<div className="flex w-full items-center justify-between">
|
||||||
alt={account.npub}
|
<div className="inline-flex items-center gap-2">
|
||||||
className="h-9 w-9 rounded-md bg-white object-cover"
|
<div className="relative h-9 w-9 shrink-0 rounded">
|
||||||
/>
|
<Image
|
||||||
|
src={user?.image}
|
||||||
|
fallback={DEFAULT_AVATAR}
|
||||||
|
alt={account.npub}
|
||||||
|
className="h-9 w-9 rounded-md bg-white object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="mb-px leading-none text-sm text-zinc-400">
|
||||||
|
Reply as
|
||||||
|
</p>
|
||||||
|
<p className="leading-none text-sm font-medium text-zinc-100">
|
||||||
|
{user?.nip05 || user?.name}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="flex items-center gap-2">
|
||||||
<p className="mb-px leading-none text-sm text-zinc-400">
|
<Button
|
||||||
Reply as
|
onClick={() => submitEvent()}
|
||||||
</p>
|
disabled={value.length === 0 ? true : false}
|
||||||
<p className="leading-none text-sm font-medium text-zinc-100">
|
preset="publish"
|
||||||
{user?.nip05 || user?.name}
|
>
|
||||||
</p>
|
Reply
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
)}
|
||||||
<Button
|
|
||||||
onClick={() => submitEvent()}
|
|
||||||
disabled={value.length === 0 ? true : false}
|
|
||||||
preset="publish"
|
|
||||||
>
|
|
||||||
Reply
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import { getReplies } from "@libs/storage";
|
|||||||
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
||||||
import { EmptyIcon } from "@shared/icons";
|
import { EmptyIcon } from "@shared/icons";
|
||||||
import { Reply } from "@shared/notes/replies/item";
|
import { Reply } from "@shared/notes/replies/item";
|
||||||
import useSWR from "swr";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
const fetcher = ([, id]) => getReplies(id);
|
|
||||||
|
|
||||||
export function RepliesList({ parent_id }: { parent_id: string }) {
|
export function RepliesList({ parent_id }: { parent_id: string }) {
|
||||||
const { data }: any = useSWR(["note-replies", parent_id], fetcher);
|
const { data } = useQuery(["replies", parent_id], async () => {
|
||||||
|
return await getReplies(parent_id);
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-5">
|
<div className="mt-5">
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { NoteMetadata } from "@shared/notes/metadata";
|
|||||||
import { NoteSkeleton } from "@shared/notes/skeleton";
|
import { NoteSkeleton } from "@shared/notes/skeleton";
|
||||||
import { User } from "@shared/user";
|
import { User } from "@shared/user";
|
||||||
import { useEvent } from "@utils/hooks/useEvent";
|
import { useEvent } from "@utils/hooks/useEvent";
|
||||||
import { parser } from "@utils/parser";
|
|
||||||
import { getRepostID } from "@utils/transform";
|
import { getRepostID } from "@utils/transform";
|
||||||
import { LumeEvent } from "@utils/types";
|
import { LumeEvent } from "@utils/types";
|
||||||
|
|
||||||
@@ -13,14 +12,16 @@ export function Repost({
|
|||||||
currentBlock,
|
currentBlock,
|
||||||
}: { event: LumeEvent; currentBlock?: number }) {
|
}: { event: LumeEvent; currentBlock?: number }) {
|
||||||
const repostID = getRepostID(event.tags);
|
const repostID = getRepostID(event.tags);
|
||||||
const data = useEvent(repostID);
|
const { status, data, isFetching } = useEvent(repostID);
|
||||||
|
|
||||||
const kind1 = data?.kind === 1 ? parser(data) : null;
|
const kind1 = data?.kind === 1 ? data.content : null;
|
||||||
const kind1063 = data?.kind === 1063 ? data.tags : null;
|
const kind1063 = data?.kind === 1063 ? data.tags : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative overflow-hidden flex flex-col mt-12">
|
<div className="relative overflow-hidden flex flex-col mt-12">
|
||||||
{data ? (
|
{isFetching || status === "loading" ? (
|
||||||
|
<NoteSkeleton />
|
||||||
|
) : (
|
||||||
<>
|
<>
|
||||||
<User pubkey={data.pubkey} time={data.created_at} />
|
<User pubkey={data.pubkey} time={data.created_at} />
|
||||||
<div className="-mt-5 pl-[49px]">
|
<div className="-mt-5 pl-[49px]">
|
||||||
@@ -48,8 +49,6 @@ export function Repost({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
|
||||||
<NoteSkeleton />
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
13
src/shared/protected.tsx
Normal file
13
src/shared/protected.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { useAccount } from "@utils/hooks/useAccount";
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
import { Navigate } from "react-router-dom";
|
||||||
|
|
||||||
|
export function Protected({ children }: { children: ReactNode }) {
|
||||||
|
const { status, account } = useAccount();
|
||||||
|
|
||||||
|
if (status === "success" && !account) {
|
||||||
|
return <Navigate to="/auth/welcome" replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return children;
|
||||||
|
}
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
import { initNDK } from "@libs/ndk";
|
import { initNDK } from "@libs/ndk";
|
||||||
import NDK from "@nostr-dev-kit/ndk";
|
import NDK from "@nostr-dev-kit/ndk";
|
||||||
|
import { FULL_RELAYS } from "@stores/constants";
|
||||||
import { createContext } from "react";
|
import { createContext } from "react";
|
||||||
|
|
||||||
export const RelayContext = createContext<NDK>(null);
|
export const RelayContext = createContext<NDK>(null);
|
||||||
const ndk = await initNDK();
|
|
||||||
|
const ndk = new NDK({ explicitRelayUrls: FULL_RELAYS });
|
||||||
|
await ndk.connect();
|
||||||
|
|
||||||
export function RelayProvider({ children }: { children: React.ReactNode }) {
|
export function RelayProvider({ children }: { children: React.ReactNode }) {
|
||||||
return <RelayContext.Provider value={ndk}>{children}</RelayContext.Provider>;
|
return <RelayContext.Provider value={ndk}>{children}</RelayContext.Provider>;
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { Popover, Transition } from "@headlessui/react";
|
import { Popover, Transition } from "@headlessui/react";
|
||||||
import { Image } from "@shared/image";
|
import { Image } from "@shared/image";
|
||||||
import { Link } from "@shared/link";
|
|
||||||
import { DEFAULT_AVATAR } from "@stores/constants";
|
import { DEFAULT_AVATAR } from "@stores/constants";
|
||||||
import { useProfile } from "@utils/hooks/useProfile";
|
import { useProfile } from "@utils/hooks/useProfile";
|
||||||
import { shortenKey } from "@utils/shortenKey";
|
import { shortenKey } from "@utils/shortenKey";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import relativeTime from "dayjs/plugin/relativeTime";
|
import relativeTime from "dayjs/plugin/relativeTime";
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
@@ -96,13 +96,13 @@ export function User({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 px-3 py-3">
|
<div className="flex items-center gap-2 px-3 py-3">
|
||||||
<Link
|
<Link
|
||||||
href={`/app/user?pubkey=${pubkey}`}
|
to={`/app/user/${pubkey}`}
|
||||||
className="inline-flex h-10 flex-1 items-center justify-center rounded-md bg-zinc-700 hover:bg-fuchsia-500 text-sm font-medium"
|
className="inline-flex h-10 flex-1 items-center justify-center rounded-md bg-zinc-700 hover:bg-fuchsia-500 text-sm font-medium"
|
||||||
>
|
>
|
||||||
View profile
|
View profile
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href={`/app/chat?pubkey=${pubkey}`}
|
to={`/app/chat/${pubkey}`}
|
||||||
className="inline-flex h-10 flex-1 items-center justify-center rounded-md bg-zinc-700 hover:bg-fuchsia-500 text-sm font-medium"
|
className="inline-flex h-10 flex-1 items-center justify-center rounded-md bg-zinc-700 hover:bg-fuchsia-500 text-sm font-medium"
|
||||||
>
|
>
|
||||||
Message
|
Message
|
||||||
|
|||||||
@@ -7,6 +7,5 @@ export const OPENGRAPH_KEY = "9EJG4SY-19Q4M5J-H8R29C9-091XPCC";
|
|||||||
export const FULL_RELAYS = [
|
export const FULL_RELAYS = [
|
||||||
"wss://relay.damus.io",
|
"wss://relay.damus.io",
|
||||||
"wss://relay.nostr.band/all",
|
"wss://relay.nostr.band/all",
|
||||||
"wss://relay.nostrich.land",
|
|
||||||
"wss://relay.nostrgraph.net",
|
"wss://relay.nostrgraph.net",
|
||||||
];
|
];
|
||||||
|
|||||||
17
src/stores/onboarding.tsx
Normal file
17
src/stores/onboarding.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { create } from "zustand";
|
||||||
|
import { createJSONStorage, persist } from "zustand/middleware";
|
||||||
|
|
||||||
|
export const useOnboarding = create(
|
||||||
|
persist(
|
||||||
|
(set) => ({
|
||||||
|
profile: {},
|
||||||
|
createProfile: (data) => {
|
||||||
|
set({ profile: data });
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: "onboarding",
|
||||||
|
storage: createJSONStorage(() => sessionStorage),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
11
src/utils/hooks/useAccount.tsx
Normal file
11
src/utils/hooks/useAccount.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { getActiveAccount } from "@libs/storage";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
export function useAccount() {
|
||||||
|
const { status, data: account } = useQuery(["currentAccount"], async () => {
|
||||||
|
const res = await getActiveAccount();
|
||||||
|
return res;
|
||||||
|
});
|
||||||
|
|
||||||
|
return { status, account };
|
||||||
|
}
|
||||||
@@ -1,31 +1,40 @@
|
|||||||
import { createNote, getNoteByID } from "@libs/storage";
|
import { createNote, getNoteByID } from "@libs/storage";
|
||||||
import { RelayContext } from "@shared/relayProvider";
|
import { RelayContext } from "@shared/relayProvider";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { parser } from "@utils/parser";
|
||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
import useSWR from "swr";
|
|
||||||
|
|
||||||
const fetcher = async ([, ndk, id]) => {
|
|
||||||
const result = await getNoteByID(id);
|
|
||||||
|
|
||||||
if (result) {
|
|
||||||
return result;
|
|
||||||
} else {
|
|
||||||
const event = await ndk.fetchEvent(id);
|
|
||||||
await createNote(
|
|
||||||
event.id,
|
|
||||||
event.pubkey,
|
|
||||||
event.kind,
|
|
||||||
event.tags,
|
|
||||||
event.content,
|
|
||||||
event.created_at,
|
|
||||||
);
|
|
||||||
event["event_id"] = event.id;
|
|
||||||
return event;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export function useEvent(id: string) {
|
export function useEvent(id: string) {
|
||||||
const ndk = useContext(RelayContext);
|
const ndk = useContext(RelayContext);
|
||||||
const { data } = useSWR(["note", ndk, id], fetcher);
|
const { status, data, error, isFetching } = useQuery(
|
||||||
|
["note", id],
|
||||||
|
async () => {
|
||||||
|
const result = await getNoteByID(id);
|
||||||
|
if (result) {
|
||||||
|
result["content"] = parser(result);
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
const event = await ndk.fetchEvent(id);
|
||||||
|
await createNote(
|
||||||
|
event.id,
|
||||||
|
event.pubkey,
|
||||||
|
event.kind,
|
||||||
|
event.tags,
|
||||||
|
event.content,
|
||||||
|
event.created_at,
|
||||||
|
);
|
||||||
|
event["event_id"] = event.id;
|
||||||
|
// @ts-ignore
|
||||||
|
event["content"] = parser(event);
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
refetchOnMount: false,
|
||||||
|
refetchOnReconnect: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
return data;
|
return { status, data, error, isFetching };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,49 @@
|
|||||||
import { OPENGRAPH_KEY } from "@stores/constants";
|
import { OPENGRAPH_KEY } from "@stores/constants";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { fetch } from "@tauri-apps/api/http";
|
import { fetch } from "@tauri-apps/api/http";
|
||||||
import useSWR from "swr";
|
|
||||||
|
|
||||||
const fetcher = async (url: string) => {
|
|
||||||
const result = await fetch(url, {
|
|
||||||
method: "GET",
|
|
||||||
timeout: 20,
|
|
||||||
});
|
|
||||||
if (result.ok) {
|
|
||||||
return result.data;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export function useOpenGraph(url: string) {
|
export function useOpenGraph(url: string) {
|
||||||
const { data, error, isLoading } = useSWR(
|
const { status, data, error, isFetching } = useQuery(
|
||||||
`https://skrape.dev/api/opengraph/?url=${url}&key=${OPENGRAPH_KEY}`,
|
["preview", url],
|
||||||
fetcher,
|
async () => {
|
||||||
|
const result = await fetch(
|
||||||
|
`https://skrape.dev/api/opengraph/?url=${url}&key=${OPENGRAPH_KEY}`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
timeout: 10,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (result.ok) {
|
||||||
|
if (Object.keys(result.data).length === 0) {
|
||||||
|
const origin = new URL(url).origin;
|
||||||
|
const result = await fetch(
|
||||||
|
`https://skrape.dev/api/opengraph/?url=${origin}&key=${OPENGRAPH_KEY}`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
timeout: 10,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (result.ok) {
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
refetchOnMount: false,
|
||||||
|
refetchOnReconnect: false,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: data,
|
status,
|
||||||
error: error,
|
data,
|
||||||
isLoading: isLoading,
|
error,
|
||||||
|
isFetching,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,44 +1,38 @@
|
|||||||
import { createPleb, getPleb } from "@libs/storage";
|
import { createPleb, getPleb } from "@libs/storage";
|
||||||
import NDK from "@nostr-dev-kit/ndk";
|
|
||||||
import { RelayContext } from "@shared/relayProvider";
|
import { RelayContext } from "@shared/relayProvider";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { nip19 } from "nostr-tools";
|
import { nip19 } from "nostr-tools";
|
||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
import useSWR from "swr";
|
|
||||||
|
|
||||||
const fetcher = async ([, ndk, key]) => {
|
export function useProfile(id: string) {
|
||||||
let npub: string;
|
|
||||||
|
|
||||||
if (key.substring(0, 4) === "npub") {
|
|
||||||
npub = key;
|
|
||||||
} else {
|
|
||||||
npub = nip19.npubEncode(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
const current = Math.floor(Date.now() / 1000);
|
|
||||||
const result = await getPleb(npub);
|
|
||||||
|
|
||||||
if (result && result.created_at + 86400 > current) {
|
|
||||||
return result;
|
|
||||||
} else {
|
|
||||||
const user = ndk.getUser({ npub });
|
|
||||||
await user.fetchProfile();
|
|
||||||
await createPleb(key, user.profile);
|
|
||||||
|
|
||||||
return user.profile;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export function useProfile(key: string) {
|
|
||||||
const ndk = useContext(RelayContext);
|
const ndk = useContext(RelayContext);
|
||||||
const { data, error, isLoading } = useSWR(["profile", ndk, key], fetcher, {
|
const {
|
||||||
revalidateIfStale: false,
|
status,
|
||||||
revalidateOnFocus: false,
|
data: user,
|
||||||
revalidateOnReconnect: false,
|
error,
|
||||||
|
isFetching,
|
||||||
|
} = useQuery(["user", id], async () => {
|
||||||
|
let npub: string;
|
||||||
|
|
||||||
|
if (id.substring(0, 4) === "npub") {
|
||||||
|
npub = id;
|
||||||
|
} else {
|
||||||
|
npub = nip19.npubEncode(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const current = Math.floor(Date.now() / 1000);
|
||||||
|
const result = await getPleb(npub);
|
||||||
|
|
||||||
|
if (result && result.created_at + 86400 > current) {
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
const user = ndk.getUser({ npub });
|
||||||
|
await user.fetchProfile();
|
||||||
|
await createPleb(id, user.profile);
|
||||||
|
|
||||||
|
return user.profile;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return { status, user, error, isFetching };
|
||||||
user: data,
|
|
||||||
isLoading,
|
|
||||||
isError: error,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Link } from "@shared/link";
|
|
||||||
import { MentionUser } from "@shared/notes/mentions/user";
|
import { MentionUser } from "@shared/notes/mentions/user";
|
||||||
import destr from "destr";
|
import destr from "destr";
|
||||||
import getUrls from "get-urls";
|
import getUrls from "get-urls";
|
||||||
import { parseReferences } from "nostr-tools";
|
import { parseReferences } from "nostr-tools";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
import reactStringReplace from "react-string-replace";
|
import reactStringReplace from "react-string-replace";
|
||||||
|
|
||||||
function isJsonString(str) {
|
function isJsonString(str) {
|
||||||
@@ -50,17 +50,33 @@ export function parser(event: any) {
|
|||||||
// image url
|
// image url
|
||||||
content.images.push(url);
|
content.images.push(url);
|
||||||
// remove url from original content
|
// remove url from original content
|
||||||
content.parsed = content.parsed.replace(url, "");
|
content.parsed = reactStringReplace(content.parsed, url, () => null);
|
||||||
} else if (url.match(/\.(mp4|webm|mov|ogv|avi|mp3)$/)) {
|
} else if (url.match(/\.(mp4|webm|mov|ogv|avi|mp3)$/)) {
|
||||||
// video
|
// video
|
||||||
content.videos.push(url);
|
content.videos.push(url);
|
||||||
// remove url from original content
|
// remove url from original content
|
||||||
content.parsed = content.parsed.replace(url, "");
|
content.parsed = reactStringReplace(content.parsed, url, () => null);
|
||||||
} else {
|
} else {
|
||||||
// push to store
|
if (content.links.length < 1) {
|
||||||
content.links.push(url);
|
// push to store
|
||||||
// remove url from original content
|
content.links.push(url);
|
||||||
content.parsed = content.parsed.replace(url, "");
|
// remove url from original content
|
||||||
|
content.parsed = reactStringReplace(content.parsed, url, () => null);
|
||||||
|
} else {
|
||||||
|
content.parsed = reactStringReplace(
|
||||||
|
content.parsed,
|
||||||
|
/#(\w+)/g,
|
||||||
|
(match, i) => (
|
||||||
|
<Link
|
||||||
|
key={match + i}
|
||||||
|
to={match}
|
||||||
|
className="text-fuchsia-500 hover:text-fuchsia-600 no-underline font-normal"
|
||||||
|
>
|
||||||
|
{match}
|
||||||
|
</Link>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -70,7 +86,11 @@ export function parser(event: any) {
|
|||||||
const event = item.event;
|
const event = item.event;
|
||||||
if (event) {
|
if (event) {
|
||||||
content.notes.push(event.id);
|
content.notes.push(event.id);
|
||||||
content.parsed = content.parsed.replace(item.text, "");
|
content.parsed = reactStringReplace(
|
||||||
|
content.parsed,
|
||||||
|
item.text,
|
||||||
|
() => null,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (profile) {
|
if (profile) {
|
||||||
content.parsed = reactStringReplace(
|
content.parsed = reactStringReplace(
|
||||||
@@ -85,7 +105,7 @@ export function parser(event: any) {
|
|||||||
content.parsed = reactStringReplace(content.parsed, /#(\w+)/g, (match, i) => (
|
content.parsed = reactStringReplace(content.parsed, /#(\w+)/g, (match, i) => (
|
||||||
<Link
|
<Link
|
||||||
key={match + i}
|
key={match + i}
|
||||||
href={`/search/${match}`}
|
to={`/search/${match}`}
|
||||||
className="text-fuchsia-500 hover:text-fuchsia-600 no-underline font-normal"
|
className="text-fuchsia-500 hover:text-fuchsia-600 no-underline font-normal"
|
||||||
>
|
>
|
||||||
#{match}
|
#{match}
|
||||||
|
|||||||
@@ -1,57 +1,10 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
content: ['./src/**/*.{js,ts,jsx,tsx}'],
|
content: ["./src/**/*.{js,ts,jsx,tsx}", "index.html"],
|
||||||
darkMode: 'class',
|
darkMode: "class",
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {},
|
||||||
boxShadow: {
|
},
|
||||||
input: `
|
plugins: [require("@tailwindcss/typography")],
|
||||||
0px 1px 0px -1px var(--tw-shadow-color),
|
|
||||||
0px 1px 1px -1px var(--tw-shadow-color),
|
|
||||||
0px 1px 2px -1px var(--tw-shadow-color),
|
|
||||||
0px 2px 4px -2px var(--tw-shadow-color),
|
|
||||||
0px 3px 6px -3px var(--tw-shadow-color)
|
|
||||||
`,
|
|
||||||
highlight: `
|
|
||||||
inset 0px 0px 0px 1px var(--tw-shadow-color),
|
|
||||||
inset 0px 1px 0px var(--tw-shadow-color)
|
|
||||||
`,
|
|
||||||
popover: `0px 0px 7px rgba(0,0,0,0.52)`,
|
|
||||||
inner: `
|
|
||||||
0 2px 2px rgb(4 4 7 / 45%),
|
|
||||||
0 8px 24px rgb(4 4 7 / 60%)
|
|
||||||
`,
|
|
||||||
button: `
|
|
||||||
rgba(74, 4, 78, 0.5) 0px 2px 8px,
|
|
||||||
rgb(74, 4, 78) 0px 2px 4px,
|
|
||||||
rgb(74, 4, 78) 0px 0px 0px 1px,
|
|
||||||
rgba(255, 255, 255, 0.2) 0px 0px 0px 1px inset
|
|
||||||
`,
|
|
||||||
'mini-button': `
|
|
||||||
rgba(13, 16, 23, 0.36) 0px 2px 8px,
|
|
||||||
rgba(13, 16, 23, 0.36) 0px 2px 4px,
|
|
||||||
rgba(13, 16, 23, 0.36) 0px 0px 0px 1px,
|
|
||||||
rgba(255, 255, 255, 0.1) 0px 0px 0px 1px inset
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
backgroundImage: {
|
|
||||||
fade: 'linear-gradient(120deg, #000, transparent 30%, transparent 70%, #000)',
|
|
||||||
},
|
|
||||||
keyframes: {
|
|
||||||
moveBg: {
|
|
||||||
'0%': { backgroundPosition: '50px' },
|
|
||||||
'20%': { backgroundPosition: '150px' },
|
|
||||||
'40%': { backgroundPosition: '250px' },
|
|
||||||
'60%': { backgroundPosition: '350px' },
|
|
||||||
'80%': { backgroundPosition: '450px' },
|
|
||||||
'100%': { backgroundPosition: '550px' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
animation: {
|
|
||||||
moveBg: 'moveBg 3s ease-in-out infinite alternate running forwards',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: [require('@tailwindcss/typography')],
|
|
||||||
};
|
};
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user