Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f2b1458bd2 | |||
| 0d43c13928 | |||
| a42a2788ea | |||
| e30274dab3 | |||
|
|
740b7588bc | ||
| 24c2ed4eb2 | |||
| 4006c0010e | |||
| 7decf264d7 | |||
| 482b218f74 | |||
| e06b760e41 | |||
| 7efc35f622 | |||
| 8795923443 | |||
| 4093821fd0 | |||
| b19637bdb7 | |||
| 21e758ec13 | |||
| 48ab123850 | |||
| a401070031 | |||
| e5e4109e79 | |||
|
|
d62c814f33 | ||
| 2a92b7c202 | |||
| 255dcb43fe | |||
| a528b646e3 | |||
| fc35745c0d | |||
| 9ddf3471ce | |||
| 8355ad6863 | |||
| 217ac490b1 | |||
| 092cf49227 | |||
| 5318f6c4cb | |||
| 80f675cb54 | |||
| 6f68c2762b | |||
| f4390b29e2 | |||
| 00e4f9d357 | |||
| d28d183620 | |||
| 3c6c9c86d1 | |||
| bcd079c88e | |||
|
|
d989d6ffad | ||
| 5229458746 | |||
| 2bfa1db816 | |||
| 8439428ce1 | |||
| 34dceef4a3 | |||
|
|
619bfb8dff | ||
| 7759851541 | |||
| 9112c1c24a | |||
| 24b21a9451 | |||
| 31a53b9c48 | |||
| dc229f40cb | |||
| 54ad1e6e1d | |||
|
|
065ccbbea4 | ||
| 74738c36cd | |||
|
|
2fdf437789 | ||
| 731c72535c | |||
| 628102087e | |||
| 536ea30ed2 | |||
| 8ee38cdb42 |
@@ -11,7 +11,6 @@
|
|||||||
"^@app/(.*)$",
|
"^@app/(.*)$",
|
||||||
"^@libs/(.*)$",
|
"^@libs/(.*)$",
|
||||||
"^@shared/(.*)$",
|
"^@shared/(.*)$",
|
||||||
"^@stores/(.*)$",
|
|
||||||
"^@utils/(.*)$",
|
"^@utils/(.*)$",
|
||||||
"^[./]"
|
"^[./]"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Lume</title>
|
<title>Lume</title>
|
||||||
</head>
|
</head>
|
||||||
<body class="relative cursor-default select-none overflow-hidden font-sans antialiased h-screen w-screen text-neutral-950 dark:text-neutral-50">
|
<body
|
||||||
|
class="relative h-screen w-screen cursor-default select-none overflow-hidden font-sans text-neutral-950 antialiased dark:text-neutral-50"
|
||||||
|
>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/main.jsx"></script>
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
75
package.json
75
package.json
@@ -2,7 +2,7 @@
|
|||||||
"name": "lume",
|
"name": "lume",
|
||||||
"description": "the communication app",
|
"description": "the communication app",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2.1.2",
|
"version": "2.2.3",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
@@ -18,10 +18,11 @@
|
|||||||
"**/*.{ts, tsx, css, md, html, json}": "prettier --cache --write"
|
"**/*.{ts, tsx, css, md, html, json}": "prettier --cache --write"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@evilmartians/harmony": "^1.1.0",
|
"@evilmartians/harmony": "^1.2.0",
|
||||||
"@getalby/sdk": "^2.6.0",
|
"@getalby/sdk": "^2.7.0",
|
||||||
"@nostr-dev-kit/ndk": "^2.0.5",
|
"@nostr-dev-kit/ndk": "^2.2.0",
|
||||||
"@nostr-fetch/adapter-ndk": "^0.13.1",
|
"@nostr-fetch/adapter-ndk": "^0.13.1",
|
||||||
|
"@radix-ui/react-accordion": "^1.1.2",
|
||||||
"@radix-ui/react-alert-dialog": "^1.0.5",
|
"@radix-ui/react-alert-dialog": "^1.0.5",
|
||||||
"@radix-ui/react-avatar": "^1.0.4",
|
"@radix-ui/react-avatar": "^1.0.4",
|
||||||
"@radix-ui/react-collapsible": "^1.0.3",
|
"@radix-ui/react-collapsible": "^1.0.3",
|
||||||
@@ -32,7 +33,8 @@
|
|||||||
"@radix-ui/react-switch": "^1.0.3",
|
"@radix-ui/react-switch": "^1.0.3",
|
||||||
"@radix-ui/react-toolbar": "^1.0.4",
|
"@radix-ui/react-toolbar": "^1.0.4",
|
||||||
"@radix-ui/react-tooltip": "^1.0.7",
|
"@radix-ui/react-tooltip": "^1.0.7",
|
||||||
"@tanstack/react-query": "^5.8.4",
|
"@tanstack/react-query": "^5.12.2",
|
||||||
|
"@tanstack/react-query-devtools": "^5.12.2",
|
||||||
"@tauri-apps/api": "2.0.0-alpha.11",
|
"@tauri-apps/api": "2.0.0-alpha.11",
|
||||||
"@tauri-apps/cli": "2.0.0-alpha.17",
|
"@tauri-apps/cli": "2.0.0-alpha.17",
|
||||||
"@tauri-apps/plugin-autostart": "2.0.0-alpha.3",
|
"@tauri-apps/plugin-autostart": "2.0.0-alpha.3",
|
||||||
@@ -47,27 +49,26 @@
|
|||||||
"@tauri-apps/plugin-sql": "2.0.0-alpha.3",
|
"@tauri-apps/plugin-sql": "2.0.0-alpha.3",
|
||||||
"@tauri-apps/plugin-updater": "2.0.0-alpha.3",
|
"@tauri-apps/plugin-updater": "2.0.0-alpha.3",
|
||||||
"@tauri-apps/plugin-upload": "2.0.0-alpha.3",
|
"@tauri-apps/plugin-upload": "2.0.0-alpha.3",
|
||||||
"@tiptap/extension-character-count": "^2.1.12",
|
"@tiptap/extension-character-count": "^2.1.13",
|
||||||
"@tiptap/extension-document": "^2.1.12",
|
"@tiptap/extension-document": "^2.1.13",
|
||||||
"@tiptap/extension-image": "^2.1.12",
|
"@tiptap/extension-image": "^2.1.13",
|
||||||
"@tiptap/extension-mention": "^2.1.12",
|
"@tiptap/extension-mention": "^2.1.13",
|
||||||
"@tiptap/extension-paragraph": "^2.1.12",
|
"@tiptap/extension-paragraph": "^2.1.13",
|
||||||
"@tiptap/extension-placeholder": "^2.1.12",
|
"@tiptap/extension-placeholder": "^2.1.13",
|
||||||
"@tiptap/extension-text": "^2.1.12",
|
"@tiptap/extension-text": "^2.1.13",
|
||||||
"@tiptap/pm": "^2.1.12",
|
"@tiptap/pm": "^2.1.13",
|
||||||
"@tiptap/react": "^2.1.12",
|
"@tiptap/react": "^2.1.13",
|
||||||
"@tiptap/starter-kit": "^2.1.12",
|
"@tiptap/starter-kit": "^2.1.13",
|
||||||
"@tiptap/suggestion": "^2.1.12",
|
"@tiptap/suggestion": "^2.1.13",
|
||||||
|
"@vidstack/react": "^1.8.3",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"framer-motion": "^10.16.5",
|
"framer-motion": "^10.16.12",
|
||||||
"html-to-text": "^9.0.5",
|
"html-to-text": "^9.0.5",
|
||||||
"idb-keyval": "^6.2.1",
|
|
||||||
"light-bolt11-decoder": "^3.0.0",
|
"light-bolt11-decoder": "^3.0.0",
|
||||||
"lru-cache": "^10.0.3",
|
"lru-cache": "^10.1.0",
|
||||||
"markdown-to-jsx": "^7.3.2",
|
"markdown-to-jsx": "^7.3.2",
|
||||||
"media-chrome": "^1.5.3",
|
|
||||||
"minidenticons": "^4.2.0",
|
"minidenticons": "^4.2.0",
|
||||||
"nanoid": "^5.0.3",
|
"nanoid": "^5.0.4",
|
||||||
"nostr-fetch": "^0.13.1",
|
"nostr-fetch": "^0.13.1",
|
||||||
"nostr-tools": "^1.17.0",
|
"nostr-tools": "^1.17.0",
|
||||||
"qrcode.react": "^3.1.0",
|
"qrcode.react": "^3.1.0",
|
||||||
@@ -77,48 +78,48 @@
|
|||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hook-form": "^7.48.2",
|
"react-hook-form": "^7.48.2",
|
||||||
"react-hotkeys-hook": "^4.4.1",
|
"react-hotkeys-hook": "^4.4.1",
|
||||||
"react-router-dom": "^6.19.0",
|
"react-router-dom": "^6.20.1",
|
||||||
"react-string-replace": "^1.1.1",
|
"react-string-replace": "^1.1.1",
|
||||||
"reactflow": "^11.10.1",
|
"reactflow": "^11.10.1",
|
||||||
"sonner": "^1.2.2",
|
"sonner": "^1.2.4",
|
||||||
"tailwind-scrollbar": "^3.0.5",
|
|
||||||
"tauri-controls": "github:reyamir/tauri-controls",
|
"tauri-controls": "github:reyamir/tauri-controls",
|
||||||
"tippy.js": "^6.3.7",
|
"tippy.js": "^6.3.7",
|
||||||
"tiptap-markdown": "^0.8.4",
|
"tiptap-markdown": "^0.8.8",
|
||||||
"virtua": "^0.16.5",
|
"virtua": "^0.17.4",
|
||||||
"zustand": "^4.4.6"
|
"zustand": "^4.4.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@tailwindcss/forms": "^0.5.7",
|
||||||
"@tailwindcss/typography": "^0.5.10",
|
"@tailwindcss/typography": "^0.5.10",
|
||||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||||
"@types/html-to-text": "^9.0.4",
|
"@types/html-to-text": "^9.0.4",
|
||||||
"@types/node": "^20.9.4",
|
"@types/node": "^20.10.3",
|
||||||
"@types/react": "^18.2.38",
|
"@types/react": "^18.2.41",
|
||||||
"@types/react-dom": "^18.2.17",
|
"@types/react-dom": "^18.2.17",
|
||||||
"@types/youtube-player": "^5.5.11",
|
"@typescript-eslint/eslint-plugin": "^6.13.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.12.0",
|
"@typescript-eslint/parser": "^6.13.1",
|
||||||
"@typescript-eslint/parser": "^6.12.0",
|
|
||||||
"@vitejs/plugin-react-swc": "^3.5.0",
|
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||||
"autoprefixer": "^10.4.16",
|
"autoprefixer": "^10.4.16",
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"csstype": "^3.1.2",
|
|
||||||
"encoding": "^0.1.13",
|
"encoding": "^0.1.13",
|
||||||
"eslint": "^8.54.0",
|
"eslint": "^8.55.0",
|
||||||
"eslint-config-prettier": "^9.0.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.8.0",
|
"eslint-plugin-jsx-a11y": "^6.8.0",
|
||||||
"eslint-plugin-react": "^7.33.2",
|
"eslint-plugin-react": "^7.33.2",
|
||||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
"eslint-plugin-simple-import-sort": "^10.0.0",
|
||||||
"husky": "^8.0.3",
|
"husky": "^8.0.3",
|
||||||
"lint-staged": "^15.1.0",
|
"lint-staged": "^15.2.0",
|
||||||
"postcss": "^8.4.31",
|
"postcss": "^8.4.32",
|
||||||
"prettier": "^3.1.0",
|
"prettier": "^3.1.0",
|
||||||
"prettier-plugin-tailwindcss": "^0.5.7",
|
"prettier-plugin-tailwindcss": "^0.5.7",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"tailwind-merge": "^1.14.0",
|
"tailwind-merge": "^1.14.0",
|
||||||
|
"tailwind-scrollbar": "^3.0.5",
|
||||||
"tailwindcss": "^3.3.5",
|
"tailwindcss": "^3.3.5",
|
||||||
"typescript": "^5.3.2",
|
"typescript": "^5.3.2",
|
||||||
"vite": "^4.5.0",
|
"vite": "^4.5.0",
|
||||||
|
"vite-plugin-top-level-await": "^1.3.1",
|
||||||
"vite-tsconfig-paths": "^4.2.1"
|
"vite-tsconfig-paths": "^4.2.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1757
pnpm-lock.yaml
generated
1757
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
312
src-tauri/Cargo.lock
generated
312
src-tauri/Cargo.lock
generated
@@ -183,12 +183,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-channel"
|
name = "async-channel"
|
||||||
version = "2.1.0"
|
version = "2.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d37875bd9915b7d67c2f117ea2c30a0989874d0b2cb694fe25403c85763c0c9e"
|
checksum = "1ca33f4bc4ed1babef42cad36cc1f51fa88be00420404e5b1e80ab1b18f7678c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"concurrent-queue",
|
"concurrent-queue",
|
||||||
"event-listener 3.1.0",
|
"event-listener 4.0.0",
|
||||||
"event-listener-strategy",
|
"event-listener-strategy",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
@@ -196,11 +196,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-executor"
|
name = "async-executor"
|
||||||
version = "1.7.2"
|
version = "1.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fc5ea910c42e5ab19012bab31f53cb4d63d54c3a27730f9a833a88efcf4bb52d"
|
checksum = "17ae5ebefcc48e7452b4987947920dac9450be1110cadf34d1b8c116bdbaf97c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-lock 3.1.1",
|
"async-lock 3.1.2",
|
||||||
"async-task",
|
"async-task",
|
||||||
"concurrent-queue",
|
"concurrent-queue",
|
||||||
"fastrand 2.0.1",
|
"fastrand 2.0.1",
|
||||||
@@ -242,22 +242,21 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-io"
|
name = "async-io"
|
||||||
version = "2.2.0"
|
version = "2.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "41ed9d5715c2d329bf1b4da8d60455b99b187f27ba726df2883799af9af60997"
|
checksum = "d6d3b15875ba253d1110c740755e246537483f152fa334f91abd7fe84c88b3ff"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-lock 3.1.1",
|
"async-lock 3.1.2",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"concurrent-queue",
|
"concurrent-queue",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
"futures-lite 2.0.1",
|
"futures-lite 2.0.1",
|
||||||
"parking",
|
"parking",
|
||||||
"polling 3.3.0",
|
"polling 3.3.1",
|
||||||
"rustix 0.38.25",
|
"rustix 0.38.26",
|
||||||
"slab",
|
"slab",
|
||||||
"tracing",
|
"tracing",
|
||||||
"waker-fn",
|
"windows-sys 0.52.0",
|
||||||
"windows-sys 0.48.0",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -271,11 +270,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-lock"
|
name = "async-lock"
|
||||||
version = "3.1.1"
|
version = "3.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "655b9c7fe787d3b25cc0f804a1a8401790f0c5bc395beb5a64dc77d8de079105"
|
checksum = "dea8b3453dd7cc96711834b75400d671b73e3656975fa68d9f277163b7f7e316"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"event-listener 3.1.0",
|
"event-listener 4.0.0",
|
||||||
"event-listener-strategy",
|
"event-listener-strategy",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
@@ -293,7 +292,7 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
"event-listener 3.1.0",
|
"event-listener 3.1.0",
|
||||||
"futures-lite 1.13.0",
|
"futures-lite 1.13.0",
|
||||||
"rustix 0.38.25",
|
"rustix 0.38.26",
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -314,13 +313,13 @@ version = "0.2.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9e47d90f65a225c4527103a8d747001fc56e375203592b25ad103e1ca13124c5"
|
checksum = "9e47d90f65a225c4527103a8d747001fc56e375203592b25ad103e1ca13124c5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-io 2.2.0",
|
"async-io 2.2.1",
|
||||||
"async-lock 2.8.0",
|
"async-lock 2.8.0",
|
||||||
"atomic-waker",
|
"atomic-waker",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
"rustix 0.38.25",
|
"rustix 0.38.26",
|
||||||
"signal-hook-registry",
|
"signal-hook-registry",
|
||||||
"slab",
|
"slab",
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
@@ -381,6 +380,16 @@ version = "1.1.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "atomic-write-file"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "edcdbedc2236483ab103a53415653d6b4442ea6141baf1ffa85df29635e88436"
|
||||||
|
dependencies = [
|
||||||
|
"nix 0.27.1",
|
||||||
|
"rand 0.8.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "auto-launch"
|
name = "auto-launch"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@@ -501,7 +510,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118"
|
checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-channel",
|
"async-channel",
|
||||||
"async-lock 3.1.1",
|
"async-lock 3.1.2",
|
||||||
"async-task",
|
"async-task",
|
||||||
"fastrand 2.0.1",
|
"fastrand 2.0.1",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
@@ -722,9 +731,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.4.8"
|
version = "4.4.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2275f18819641850fa26c89acc84d465c1bf91ce57bc2748b28c420473352f64"
|
checksum = "41fffed7514f420abec6d183b1d3acfd9099c79c3a10a06ade4f8203f1411272"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap_builder",
|
"clap_builder",
|
||||||
"clap_derive",
|
"clap_derive",
|
||||||
@@ -732,9 +741,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_builder"
|
name = "clap_builder"
|
||||||
version = "4.4.8"
|
version = "4.4.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "07cdf1b148b25c1e1f7a42225e30a0d99a615cd4637eae7365548dd4529b95bc"
|
checksum = "63361bae7eef3771745f02d8d892bec2fee5f6e34af316ba556e7f97a7069ff1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstream",
|
"anstream",
|
||||||
"anstyle",
|
"anstyle",
|
||||||
@@ -881,9 +890,9 @@ checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation"
|
name = "core-foundation"
|
||||||
version = "0.9.3"
|
version = "0.9.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146"
|
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"core-foundation-sys",
|
"core-foundation-sys",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -891,9 +900,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation-sys"
|
name = "core-foundation-sys"
|
||||||
version = "0.8.4"
|
version = "0.8.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
|
checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-graphics"
|
name = "core-graphics"
|
||||||
@@ -923,9 +932,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-graphics-types"
|
name = "core-graphics-types"
|
||||||
version = "0.1.2"
|
version = "0.1.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2bb142d41022986c1d8ff29103a1411c8a3dfad3552f87a4f8dc50d61d4f4e33"
|
checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 1.3.2",
|
"bitflags 1.3.2",
|
||||||
"core-foundation",
|
"core-foundation",
|
||||||
@@ -1108,9 +1117,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "data-url"
|
name = "data-url"
|
||||||
version = "0.3.0"
|
version = "0.3.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "41b319d1b62ffbd002e057f36bebd1f42b9f97927c9577461d855f3513c4289f"
|
checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "der"
|
name = "der"
|
||||||
@@ -1321,12 +1330,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "errno"
|
name = "errno"
|
||||||
version = "0.3.7"
|
version = "0.3.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f258a7194e7f7c2a7837a8913aeab7fd8c383457034fa20ce4dd3dcb813e8eb8"
|
checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1368,12 +1377,23 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "event-listener-strategy"
|
name = "event-listener"
|
||||||
version = "0.3.0"
|
version = "4.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d96b852f1345da36d551b9473fa1e2b1eb5c5195585c6c018118bc92a8d91160"
|
checksum = "770d968249b5d99410d61f5bf89057f3199a077a04d087092f58e7d10692baae"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"event-listener 3.1.0",
|
"concurrent-queue",
|
||||||
|
"parking",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "event-listener-strategy"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3"
|
||||||
|
dependencies = [
|
||||||
|
"event-listener 4.0.0",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1399,7 +1419,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "ef033ed5e9bad94e55838ca0ca906db0e043f517adda0c8b79c7a8c66c93c1b5"
|
checksum = "ef033ed5e9bad94e55838ca0ca906db0e043f517adda0c8b79c7a8c66c93c1b5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"rustix 0.38.25",
|
"rustix 0.38.26",
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1511,9 +1531,9 @@ checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "form_urlencoded"
|
name = "form_urlencoded"
|
||||||
version = "1.2.0"
|
version = "1.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652"
|
checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
@@ -1818,9 +1838,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gimli"
|
name = "gimli"
|
||||||
version = "0.28.0"
|
version = "0.28.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0"
|
checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gio"
|
name = "gio"
|
||||||
@@ -2068,9 +2088,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.14.2"
|
version = "0.14.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156"
|
checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash",
|
||||||
"allocator-api2",
|
"allocator-api2",
|
||||||
@@ -2082,7 +2102,7 @@ version = "0.8.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7"
|
checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hashbrown 0.14.2",
|
"hashbrown 0.14.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2259,9 +2279,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "0.4.0"
|
version = "0.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c"
|
checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-bidi",
|
"unicode-bidi",
|
||||||
"unicode-normalization",
|
"unicode-normalization",
|
||||||
@@ -2300,7 +2320,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f"
|
checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown 0.14.2",
|
"hashbrown 0.14.3",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2450,9 +2470,9 @@ checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.65"
|
version = "0.3.66"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8"
|
checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
@@ -2575,9 +2595,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libsqlite3-sys"
|
name = "libsqlite3-sys"
|
||||||
version = "0.26.0"
|
version = "0.27.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326"
|
checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"pkg-config",
|
"pkg-config",
|
||||||
@@ -2623,9 +2643,9 @@ checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
version = "0.4.11"
|
version = "0.4.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829"
|
checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
@@ -2660,7 +2680,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lume"
|
name = "lume"
|
||||||
version = "2.1.2"
|
version = "2.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"keyring",
|
"keyring",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -2678,7 +2698,6 @@ dependencies = [
|
|||||||
"tauri-plugin-os",
|
"tauri-plugin-os",
|
||||||
"tauri-plugin-process",
|
"tauri-plugin-process",
|
||||||
"tauri-plugin-shell",
|
"tauri-plugin-shell",
|
||||||
"tauri-plugin-single-instance",
|
|
||||||
"tauri-plugin-sql",
|
"tauri-plugin-sql",
|
||||||
"tauri-plugin-store",
|
"tauri-plugin-store",
|
||||||
"tauri-plugin-theme",
|
"tauri-plugin-theme",
|
||||||
@@ -2944,6 +2963,17 @@ dependencies = [
|
|||||||
"memoffset 0.7.1",
|
"memoffset 0.7.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nix"
|
||||||
|
version = "0.27.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.4.1",
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nodrop"
|
name = "nodrop"
|
||||||
version = "0.1.14"
|
version = "0.1.14"
|
||||||
@@ -3181,9 +3211,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl"
|
name = "openssl"
|
||||||
version = "0.10.59"
|
version = "0.10.60"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7a257ad03cd8fb16ad4172fedf8094451e1af1c4b70097636ef2eac9a5f0cc33"
|
checksum = "79a4c6c3a2b158f7f8f2a2fc5a969fa3a068df6fc9dbb4a43845436e3af7c800"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.4.1",
|
"bitflags 2.4.1",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
@@ -3222,9 +3252,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl-sys"
|
name = "openssl-sys"
|
||||||
version = "0.9.95"
|
version = "0.9.96"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "40a4130519a360279579c2053038317e40eff64d13fd3f004f9e1b72b8a6aaf9"
|
checksum = "3812c071ba60da8b5677cc12bcb1d42989a65553772897a7e0355545a819838f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -3370,9 +3400,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
version = "2.3.0"
|
version = "2.3.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
|
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "phf"
|
name = "phf"
|
||||||
@@ -3603,16 +3633,16 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "polling"
|
name = "polling"
|
||||||
version = "3.3.0"
|
version = "3.3.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e53b6af1f60f36f8c2ac2aad5459d75a5a9b4be1e8cdd40264f315d78193e531"
|
checksum = "cf63fa624ab313c11656b4cda960bfc46c410187ad493c41f6ba2d8c1e991c9e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"concurrent-queue",
|
"concurrent-queue",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"rustix 0.38.25",
|
"rustix 0.38.26",
|
||||||
"tracing",
|
"tracing",
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3684,9 +3714,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.69"
|
version = "1.0.70"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da"
|
checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
@@ -3973,9 +4003,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rsa"
|
name = "rsa"
|
||||||
version = "0.9.4"
|
version = "0.9.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6a3211b01eea83d80687da9eef70e39d65144a3894866a5153a2723e425a157f"
|
checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"const-oid",
|
"const-oid",
|
||||||
"digest",
|
"digest",
|
||||||
@@ -4022,15 +4052,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "0.38.25"
|
version = "0.38.26"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dc99bc2d4f1fed22595588a013687477aedf3cdcfb26558c559edb67b4d9b22e"
|
checksum = "9470c4bf8246c8daf25f9598dca807fb6510347b1e1cfa55749113850c79d88a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.4.1",
|
"bitflags 2.4.1",
|
||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys 0.4.11",
|
"linux-raw-sys 0.4.12",
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4450,9 +4480,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "spki"
|
name = "spki"
|
||||||
version = "0.7.2"
|
version = "0.7.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a"
|
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64ct",
|
"base64ct",
|
||||||
"der",
|
"der",
|
||||||
@@ -4471,9 +4501,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlx"
|
name = "sqlx"
|
||||||
version = "0.7.2"
|
version = "0.7.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0e50c216e3624ec8e7ecd14c6a6a6370aad6ee5d8cfc3ab30b5162eeeef2ed33"
|
checksum = "dba03c279da73694ef99763320dea58b51095dfe87d001b1d4b5fe78ba8763cf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"sqlx-core",
|
"sqlx-core",
|
||||||
"sqlx-macros",
|
"sqlx-macros",
|
||||||
@@ -4484,9 +4514,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlx-cli"
|
name = "sqlx-cli"
|
||||||
version = "0.7.2"
|
version = "0.7.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e80bc07dfc7f258db72ae5d72d793aa87943690fc1b2afc87b4cabf87035bac0"
|
checksum = "1b941ddc37071bb01d001ec479885a493021f1ca39142d754a05a780a77fff99"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -4509,9 +4539,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlx-core"
|
name = "sqlx-core"
|
||||||
version = "0.7.2"
|
version = "0.7.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8d6753e460c998bbd4cd8c6f0ed9a64346fcca0723d6e75e52fdc351c5d2169d"
|
checksum = "d84b0a3c3739e220d94b3239fd69fb1f74bc36e16643423bd99de3b43c21bfbd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash",
|
||||||
"atoi",
|
"atoi",
|
||||||
@@ -4550,9 +4580,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlx-macros"
|
name = "sqlx-macros"
|
||||||
version = "0.7.2"
|
version = "0.7.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9a793bb3ba331ec8359c1853bd39eed32cdd7baaf22c35ccf5c92a7e8d1189ec"
|
checksum = "89961c00dc4d7dffb7aee214964b065072bff69e36ddb9e2c107541f75e4f2a5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -4563,10 +4593,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlx-macros-core"
|
name = "sqlx-macros-core"
|
||||||
version = "0.7.2"
|
version = "0.7.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0a4ee1e104e00dedb6aa5ffdd1343107b0a4702e862a84320ee7cc74782d96fc"
|
checksum = "d0bd4519486723648186a08785143599760f7cc81c52334a55d6a83ea1e20841"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"atomic-write-file",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
"either",
|
"either",
|
||||||
"heck",
|
"heck",
|
||||||
@@ -4589,9 +4620,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlx-mysql"
|
name = "sqlx-mysql"
|
||||||
version = "0.7.2"
|
version = "0.7.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "864b869fdf56263f4c95c45483191ea0af340f9f3e3e7b4d57a61c7c87a970db"
|
checksum = "e37195395df71fd068f6e2082247891bc11e3289624bbc776a0cdfa1ca7f1ea4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atoi",
|
"atoi",
|
||||||
"base64",
|
"base64",
|
||||||
@@ -4632,9 +4663,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlx-postgres"
|
name = "sqlx-postgres"
|
||||||
version = "0.7.2"
|
version = "0.7.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "eb7ae0e6a97fb3ba33b23ac2671a5ce6e3cabe003f451abd5a56e7951d975624"
|
checksum = "d6ac0ac3b7ccd10cc96c7ab29791a7dd236bd94021f31eec7ba3d46a74aa1c24"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atoi",
|
"atoi",
|
||||||
"base64",
|
"base64",
|
||||||
@@ -4672,9 +4703,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlx-sqlite"
|
name = "sqlx-sqlite"
|
||||||
version = "0.7.2"
|
version = "0.7.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d59dc83cf45d89c555a577694534fcd1b55c545a816c816ce51f20bbe56a4f3f"
|
checksum = "210976b7d948c7ba9fced8ca835b11cbb2d677c59c79de41ac0d397e14547490"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atoi",
|
"atoi",
|
||||||
"flume",
|
"flume",
|
||||||
@@ -4691,6 +4722,7 @@ dependencies = [
|
|||||||
"time",
|
"time",
|
||||||
"tracing",
|
"tracing",
|
||||||
"url",
|
"url",
|
||||||
|
"urlencoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5034,7 +5066,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-autostart"
|
name = "tauri-plugin-autostart"
|
||||||
version = "2.0.0-alpha.4"
|
version = "2.0.0-alpha.4"
|
||||||
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#445c1e6cb6971ef644a08d1224fc689923be301b"
|
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#e7c72c9816d14a218e702dd233a6cfec957c2ee6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"auto-launch",
|
"auto-launch",
|
||||||
"log",
|
"log",
|
||||||
@@ -5047,7 +5079,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-cli"
|
name = "tauri-plugin-cli"
|
||||||
version = "2.0.0-alpha.4"
|
version = "2.0.0-alpha.4"
|
||||||
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#445c1e6cb6971ef644a08d1224fc689923be301b"
|
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#e7c72c9816d14a218e702dd233a6cfec957c2ee6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"log",
|
"log",
|
||||||
@@ -5060,7 +5092,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-clipboard-manager"
|
name = "tauri-plugin-clipboard-manager"
|
||||||
version = "2.0.0-alpha.4"
|
version = "2.0.0-alpha.4"
|
||||||
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#445c1e6cb6971ef644a08d1224fc689923be301b"
|
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#e7c72c9816d14a218e702dd233a6cfec957c2ee6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arboard",
|
"arboard",
|
||||||
"log",
|
"log",
|
||||||
@@ -5074,7 +5106,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-dialog"
|
name = "tauri-plugin-dialog"
|
||||||
version = "2.0.0-alpha.4"
|
version = "2.0.0-alpha.4"
|
||||||
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#445c1e6cb6971ef644a08d1224fc689923be301b"
|
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#e7c72c9816d14a218e702dd233a6cfec957c2ee6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"glib 0.16.9",
|
"glib 0.16.9",
|
||||||
"log",
|
"log",
|
||||||
@@ -5091,7 +5123,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-fs"
|
name = "tauri-plugin-fs"
|
||||||
version = "2.0.0-alpha.4"
|
version = "2.0.0-alpha.4"
|
||||||
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#445c1e6cb6971ef644a08d1224fc689923be301b"
|
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#e7c72c9816d14a218e702dd233a6cfec957c2ee6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"glob",
|
"glob",
|
||||||
@@ -5104,7 +5136,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-http"
|
name = "tauri-plugin-http"
|
||||||
version = "2.0.0-alpha.5"
|
version = "2.0.0-alpha.5"
|
||||||
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#445c1e6cb6971ef644a08d1224fc689923be301b"
|
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#e7c72c9816d14a218e702dd233a6cfec957c2ee6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"data-url",
|
"data-url",
|
||||||
"glob",
|
"glob",
|
||||||
@@ -5121,7 +5153,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-notification"
|
name = "tauri-plugin-notification"
|
||||||
version = "2.0.0-alpha.5"
|
version = "2.0.0-alpha.5"
|
||||||
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#445c1e6cb6971ef644a08d1224fc689923be301b"
|
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#e7c72c9816d14a218e702dd233a6cfec957c2ee6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"notify-rust",
|
"notify-rust",
|
||||||
@@ -5139,7 +5171,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-os"
|
name = "tauri-plugin-os"
|
||||||
version = "2.0.0-alpha.4"
|
version = "2.0.0-alpha.4"
|
||||||
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#445c1e6cb6971ef644a08d1224fc689923be301b"
|
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#e7c72c9816d14a218e702dd233a6cfec957c2ee6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gethostname 0.4.3",
|
"gethostname 0.4.3",
|
||||||
"log",
|
"log",
|
||||||
@@ -5155,7 +5187,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-process"
|
name = "tauri-plugin-process"
|
||||||
version = "2.0.0-alpha.4"
|
version = "2.0.0-alpha.4"
|
||||||
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#445c1e6cb6971ef644a08d1224fc689923be301b"
|
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#e7c72c9816d14a218e702dd233a6cfec957c2ee6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"tauri",
|
"tauri",
|
||||||
]
|
]
|
||||||
@@ -5163,7 +5195,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-shell"
|
name = "tauri-plugin-shell"
|
||||||
version = "2.0.0-alpha.4"
|
version = "2.0.0-alpha.4"
|
||||||
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#445c1e6cb6971ef644a08d1224fc689923be301b"
|
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#e7c72c9816d14a218e702dd233a6cfec957c2ee6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"encoding_rs",
|
"encoding_rs",
|
||||||
"log",
|
"log",
|
||||||
@@ -5177,24 +5209,10 @@ dependencies = [
|
|||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tauri-plugin-single-instance"
|
|
||||||
version = "2.0.0-alpha.4"
|
|
||||||
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#445c1e6cb6971ef644a08d1224fc689923be301b"
|
|
||||||
dependencies = [
|
|
||||||
"log",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"tauri",
|
|
||||||
"thiserror",
|
|
||||||
"windows-sys 0.48.0",
|
|
||||||
"zbus",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-sql"
|
name = "tauri-plugin-sql"
|
||||||
version = "2.0.0-alpha.4"
|
version = "2.0.0-alpha.4"
|
||||||
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#445c1e6cb6971ef644a08d1224fc689923be301b"
|
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#e7c72c9816d14a218e702dd233a6cfec957c2ee6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"log",
|
"log",
|
||||||
@@ -5210,7 +5228,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-store"
|
name = "tauri-plugin-store"
|
||||||
version = "2.0.0-alpha.4"
|
version = "2.0.0-alpha.4"
|
||||||
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#445c1e6cb6971ef644a08d1224fc689923be301b"
|
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#e7c72c9816d14a218e702dd233a6cfec957c2ee6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -5238,7 +5256,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-updater"
|
name = "tauri-plugin-updater"
|
||||||
version = "2.0.0-alpha.4"
|
version = "2.0.0-alpha.4"
|
||||||
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#445c1e6cb6971ef644a08d1224fc689923be301b"
|
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#e7c72c9816d14a218e702dd233a6cfec957c2ee6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"dirs-next",
|
"dirs-next",
|
||||||
@@ -5264,7 +5282,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-upload"
|
name = "tauri-plugin-upload"
|
||||||
version = "2.0.0-alpha.4"
|
version = "2.0.0-alpha.4"
|
||||||
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#445c1e6cb6971ef644a08d1224fc689923be301b"
|
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#e7c72c9816d14a218e702dd233a6cfec957c2ee6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"log",
|
"log",
|
||||||
@@ -5281,7 +5299,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-window-state"
|
name = "tauri-plugin-window-state"
|
||||||
version = "2.0.0-alpha.4"
|
version = "2.0.0-alpha.4"
|
||||||
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#445c1e6cb6971ef644a08d1224fc689923be301b"
|
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#e7c72c9816d14a218e702dd233a6cfec957c2ee6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bincode",
|
"bincode",
|
||||||
"bitflags 2.4.1",
|
"bitflags 2.4.1",
|
||||||
@@ -5389,7 +5407,7 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
"fastrand 2.0.1",
|
"fastrand 2.0.1",
|
||||||
"redox_syscall 0.4.1",
|
"redox_syscall 0.4.1",
|
||||||
"rustix 0.38.25",
|
"rustix 0.38.26",
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -5798,9 +5816,9 @@ checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "url"
|
name = "url"
|
||||||
version = "2.4.1"
|
version = "2.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5"
|
checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"form_urlencoded",
|
"form_urlencoded",
|
||||||
"idna",
|
"idna",
|
||||||
@@ -5808,6 +5826,12 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "urlencoding"
|
||||||
|
version = "2.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf-8"
|
name = "utf-8"
|
||||||
version = "0.7.6"
|
version = "0.7.6"
|
||||||
@@ -5912,9 +5936,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen"
|
name = "wasm-bindgen"
|
||||||
version = "0.2.88"
|
version = "0.2.89"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce"
|
checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"wasm-bindgen-macro",
|
"wasm-bindgen-macro",
|
||||||
@@ -5922,9 +5946,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-backend"
|
name = "wasm-bindgen-backend"
|
||||||
version = "0.2.88"
|
version = "0.2.89"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217"
|
checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bumpalo",
|
"bumpalo",
|
||||||
"log",
|
"log",
|
||||||
@@ -5937,9 +5961,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-futures"
|
name = "wasm-bindgen-futures"
|
||||||
version = "0.4.38"
|
version = "0.4.39"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9afec9963e3d0994cac82455b2b3502b81a7f40f9a0d32181f7528d9f4b43e02"
|
checksum = "ac36a15a220124ac510204aec1c3e5db8a22ab06fd6706d881dc6149f8ed9a12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
@@ -5949,9 +5973,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro"
|
name = "wasm-bindgen-macro"
|
||||||
version = "0.2.88"
|
version = "0.2.89"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2"
|
checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"wasm-bindgen-macro-support",
|
"wasm-bindgen-macro-support",
|
||||||
@@ -5959,9 +5983,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro-support"
|
name = "wasm-bindgen-macro-support"
|
||||||
version = "0.2.88"
|
version = "0.2.89"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907"
|
checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -5972,9 +5996,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-shared"
|
name = "wasm-bindgen-shared"
|
||||||
version = "0.2.88"
|
version = "0.2.89"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b"
|
checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-streams"
|
name = "wasm-streams"
|
||||||
@@ -5991,9 +6015,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "web-sys"
|
name = "web-sys"
|
||||||
version = "0.3.65"
|
version = "0.3.66"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5db499c5f66323272151db0e666cd34f78617522fb0c1604d31a27c50c206a85"
|
checksum = "50c24a44ec86bb68fbecd1b3efed7e85ea5621b39b35ef2766b66cd984f8010f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
@@ -6631,18 +6655,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy"
|
name = "zerocopy"
|
||||||
version = "0.7.26"
|
version = "0.7.28"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e97e415490559a91254a2979b4829267a57d2fcd741a98eee8b722fb57289aa0"
|
checksum = "7d6f15f7ade05d2a4935e34a457b936c23dc70a05cc1d97133dc99e7a3fe0f0e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zerocopy-derive",
|
"zerocopy-derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy-derive"
|
name = "zerocopy-derive"
|
||||||
version = "0.7.26"
|
version = "0.7.28"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dd7e48ccf166952882ca8bd778a43502c64f33bf94c12ebe2a7f08e5a0f6689f"
|
checksum = "dbbad221e3f78500350ecbd7dfa4e63ef945c05f4c61cb7f4d3f84cd0bba649b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lume"
|
name = "lume"
|
||||||
version = "2.1.2"
|
version = "2.2.0"
|
||||||
description = "the communication app"
|
description = "the communication app"
|
||||||
authors = ["Ren Amamiya"]
|
authors = ["Ren Amamiya"]
|
||||||
license = "GPL-3.0"
|
license = "GPL-3.0"
|
||||||
@@ -28,13 +28,12 @@ tauri-plugin-os = { git = "https://github.com/tauri-apps/plugins-workspace", bra
|
|||||||
tauri-plugin-process = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
tauri-plugin-process = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||||
tauri-plugin-shell = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
tauri-plugin-shell = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||||
tauri-plugin-updater = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
tauri-plugin-updater = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||||
tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
|
||||||
tauri-plugin-autostart = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
tauri-plugin-autostart = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||||
tauri-plugin-store = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
tauri-plugin-store = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||||
tauri-plugin-upload = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
tauri-plugin-upload = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||||
tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||||
tauri-plugin-theme = { git = "https://github.com/wyhaya/tauri-plugin-theme" }
|
tauri-plugin-theme = { git = "https://github.com/wyhaya/tauri-plugin-theme" }
|
||||||
tauri-plugin-sql = { git = "hhttps://github.com/tauri-apps/plugins-workspace", branch = "v2", features = [
|
tauri-plugin-sql = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2", features = [
|
||||||
"sqlite",
|
"sqlite",
|
||||||
] }
|
] }
|
||||||
sqlx-cli = { version = "0.7.0", default-features = false, features = [
|
sqlx-cli = { version = "0.7.0", default-features = false, features = [
|
||||||
|
|||||||
5
src-tauri/migrations/20231130105202_clean_up_table.sql
Normal file
5
src-tauri/migrations/20231130105202_clean_up_table.sql
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
ALTER TABLE accounts DROP COLUMN follows;
|
||||||
|
ALTER TABLE accounts DROP COLUMN circles;
|
||||||
|
ALTER TABLE accounts DROP COLUMN last_login_at;
|
||||||
|
DROP TABLE IF EXISTS events;
|
||||||
|
DROP TABLE IF EXISTS relays;
|
||||||
@@ -115,7 +115,6 @@ fn main() {
|
|||||||
.plugin(tauri_plugin_updater::Builder::new().build())?;
|
.plugin(tauri_plugin_updater::Builder::new().build())?;
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.plugin(ThemePlugin::init(ctx.config_mut()))
|
|
||||||
.plugin(
|
.plugin(
|
||||||
tauri_plugin_sql::Builder::default()
|
tauri_plugin_sql::Builder::default()
|
||||||
.add_migrations(
|
.add_migrations(
|
||||||
@@ -133,10 +132,17 @@ fn main() {
|
|||||||
sql: include_str!("../migrations/20231028083224_add_ndk_cache_table.sql"),
|
sql: include_str!("../migrations/20231028083224_add_ndk_cache_table.sql"),
|
||||||
kind: MigrationKind::Up,
|
kind: MigrationKind::Up,
|
||||||
},
|
},
|
||||||
|
Migration {
|
||||||
|
version: 20231130105202,
|
||||||
|
description: "clean up table",
|
||||||
|
sql: include_str!("../migrations/20231130105202_clean_up_table.sql"),
|
||||||
|
kind: MigrationKind::Up,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
.build(),
|
.build(),
|
||||||
)
|
)
|
||||||
|
.plugin(ThemePlugin::init(ctx.config_mut()))
|
||||||
.plugin(tauri_plugin_clipboard_manager::init())
|
.plugin(tauri_plugin_clipboard_manager::init())
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.plugin(tauri_plugin_fs::init())
|
.plugin(tauri_plugin_fs::init())
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
},
|
},
|
||||||
"package": {
|
"package": {
|
||||||
"productName": "Lume",
|
"productName": "Lume",
|
||||||
"version": "2.1.2"
|
"version": "2.2.3"
|
||||||
},
|
},
|
||||||
"plugins": {
|
"plugins": {
|
||||||
"fs": {
|
"fs": {
|
||||||
|
|||||||
28
src/app.css
28
src/app.css
@@ -1,4 +1,8 @@
|
|||||||
@import 'reactflow/dist/style.css';
|
/* @import 'reactflow/dist/style.css'; */
|
||||||
|
|
||||||
|
/* Vidstack */
|
||||||
|
@import '@vidstack/react/player/styles/default/theme.css';
|
||||||
|
@import '@vidstack/react/player/styles/default/layouts/video.css';
|
||||||
|
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@@ -41,28 +45,8 @@ input::-ms-clear {
|
|||||||
background-clip: padding-box;
|
background-clip: padding-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player {
|
|
||||||
--brand-color: #f5f5f5;
|
|
||||||
--focus-color: #4e9cf6;
|
|
||||||
--audio-brand: var(--brand-color);
|
|
||||||
--audio-focus-ring-color: var(--focus-color);
|
|
||||||
--audio-border-radius: 2px;
|
|
||||||
--video-brand: var(--brand-color);
|
|
||||||
--video-focus-ring-color: var(--focus-color);
|
|
||||||
--video-border-radius: 8px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player[data-view-type='video'] {
|
|
||||||
aspect-ratio: 16 /9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror p.is-empty::before {
|
.ProseMirror p.is-empty::before {
|
||||||
@apply text-neutral-600 dark:text-neutral-400;
|
@apply text-neutral-600 dark:text-neutral-400 float-left h-0 pointer-events-none content-[attr(data-placeholder)];
|
||||||
content: attr(data-placeholder);
|
|
||||||
float: left;
|
|
||||||
height: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ProseMirror img.ProseMirror-selectednode {
|
.ProseMirror img.ProseMirror-selectednode {
|
||||||
|
|||||||
80
src/app.tsx
80
src/app.tsx
@@ -3,7 +3,6 @@ import { fetch } from '@tauri-apps/plugin-http';
|
|||||||
import { RouterProvider, createBrowserRouter, defer, redirect } from 'react-router-dom';
|
import { RouterProvider, createBrowserRouter, defer, redirect } from 'react-router-dom';
|
||||||
import { ReactFlowProvider } from 'reactflow';
|
import { ReactFlowProvider } from 'reactflow';
|
||||||
|
|
||||||
import { OnboardingScreen } from '@app/auth/onboarding';
|
|
||||||
import { ChatsScreen } from '@app/chats';
|
import { ChatsScreen } from '@app/chats';
|
||||||
import { ErrorScreen } from '@app/error';
|
import { ErrorScreen } from '@app/error';
|
||||||
import { ExploreScreen } from '@app/explore';
|
import { ExploreScreen } from '@app/explore';
|
||||||
@@ -197,37 +196,52 @@ export default function App() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'onboarding',
|
path: 'onboarding',
|
||||||
element: <OnboardingScreen />,
|
async lazy() {
|
||||||
errorElement: <ErrorScreen />,
|
const { OnboardingScreen } = await import('@app/auth/onboarding');
|
||||||
children: [
|
return { Component: OnboardingScreen };
|
||||||
{
|
},
|
||||||
path: '',
|
},
|
||||||
async lazy() {
|
{
|
||||||
const { OnboardingListScreen } = await import(
|
path: 'follow',
|
||||||
'@app/auth/onboarding/list'
|
async lazy() {
|
||||||
);
|
const { FollowScreen } = await import('@app/auth/follow');
|
||||||
return { Component: OnboardingListScreen };
|
return { Component: FollowScreen };
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'enrich',
|
path: 'finish',
|
||||||
async lazy() {
|
async lazy() {
|
||||||
const { OnboardEnrichScreen } = await import(
|
const { FinishScreen } = await import('@app/auth/finish');
|
||||||
'@app/auth/onboarding/enrich'
|
return { Component: FinishScreen };
|
||||||
);
|
},
|
||||||
return { Component: OnboardEnrichScreen };
|
},
|
||||||
},
|
{
|
||||||
},
|
path: 'tutorials/note',
|
||||||
{
|
async lazy() {
|
||||||
path: 'hashtag',
|
const { TutorialNoteScreen } = await import('@app/auth/tutorials/note');
|
||||||
async lazy() {
|
return { Component: TutorialNoteScreen };
|
||||||
const { OnboardHashtagScreen } = await import(
|
},
|
||||||
'@app/auth/onboarding/hashtag'
|
},
|
||||||
);
|
{
|
||||||
return { Component: OnboardHashtagScreen };
|
path: 'tutorials/widget',
|
||||||
},
|
async lazy() {
|
||||||
},
|
const { TutorialWidgetScreen } = await import('@app/auth/tutorials/widget');
|
||||||
],
|
return { Component: TutorialWidgetScreen };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'tutorials/posting',
|
||||||
|
async lazy() {
|
||||||
|
const { TutorialPostingScreen } = await import('@app/auth/tutorials/posting');
|
||||||
|
return { Component: TutorialPostingScreen };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'tutorials/finish',
|
||||||
|
async lazy() {
|
||||||
|
const { TutorialFinishScreen } = await import('@app/auth/tutorials/finish');
|
||||||
|
return { Component: TutorialFinishScreen };
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -294,7 +308,7 @@ export default function App() {
|
|||||||
router={router}
|
router={router}
|
||||||
fallbackElement={
|
fallbackElement={
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
<LoaderIcon className="h-6 w-6 animate-spin text-white" />
|
<LoaderIcon className="h-6 w-6 animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
future={{ v7_startTransition: true }}
|
future={{ v7_startTransition: true }}
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
import { isPermissionGranted, requestPermission } from '@tauri-apps/plugin-notification';
|
|
||||||
|
|
||||||
import { CheckCircleIcon } from '@shared/icons';
|
|
||||||
|
|
||||||
import { useOnboarding } from '@stores/onboarding';
|
|
||||||
|
|
||||||
export function AllowNotification() {
|
|
||||||
const [notification, setNotification] = useOnboarding((state) => [
|
|
||||||
state.notification,
|
|
||||||
state.toggleNotification,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const allow = async () => {
|
|
||||||
let permissionGranted = await isPermissionGranted();
|
|
||||||
if (!permissionGranted) {
|
|
||||||
const permission = await requestPermission();
|
|
||||||
permissionGranted = permission === 'granted';
|
|
||||||
}
|
|
||||||
if (permissionGranted) {
|
|
||||||
setNotification();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200">
|
|
||||||
<div className="flex items-start justify-between gap-2">
|
|
||||||
<div>
|
|
||||||
<h5 className="font-semibold">Allow notification</h5>
|
|
||||||
<p className="text-sm">
|
|
||||||
By allowing Lume to send notifications in your OS settings, you will receive
|
|
||||||
notification messages when someone interacts with you or your content.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{notification ? (
|
|
||||||
<div className="mt-1 inline-flex h-9 w-24 shrink-0 items-center justify-center rounded-lg bg-teal-500 text-white">
|
|
||||||
<CheckCircleIcon className="h-4 w-4" />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={allow}
|
|
||||||
className="mt-1 inline-flex h-9 w-24 shrink-0 items-center justify-center rounded-lg bg-neutral-200 font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
|
||||||
>
|
|
||||||
Allow
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
|
|
||||||
import { LRUCache } from 'lru-cache';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
|
|
||||||
import { useNDK } from '@libs/ndk/provider';
|
|
||||||
import { useStorage } from '@libs/storage/provider';
|
|
||||||
|
|
||||||
import { CheckCircleIcon, LoaderIcon } from '@shared/icons';
|
|
||||||
|
|
||||||
import { useOnboarding } from '@stores/onboarding';
|
|
||||||
|
|
||||||
export function Circle() {
|
|
||||||
const { db } = useStorage();
|
|
||||||
const { ndk } = useNDK();
|
|
||||||
|
|
||||||
const [circle, setCircle] = useOnboarding((state) => [
|
|
||||||
state.circle,
|
|
||||||
state.toggleCircle,
|
|
||||||
]);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const enableLinks = async () => {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
const users = ndk.getUser({ pubkey: db.account.pubkey });
|
|
||||||
const follows = await users.follows();
|
|
||||||
|
|
||||||
if (follows.size === 0) {
|
|
||||||
setLoading(false);
|
|
||||||
return toast('You need to follow at least 1 account');
|
|
||||||
}
|
|
||||||
|
|
||||||
const lru = new LRUCache<string, string, void>({ max: 300 });
|
|
||||||
const followsAsArr = [];
|
|
||||||
|
|
||||||
// add user's follows to lru
|
|
||||||
follows.forEach((user) => {
|
|
||||||
lru.set(user.pubkey, user.pubkey);
|
|
||||||
followsAsArr.push(user.pubkey);
|
|
||||||
});
|
|
||||||
|
|
||||||
// get follows from follows
|
|
||||||
const events = await ndk.fetchEvents({
|
|
||||||
kinds: [NDKKind.Contacts],
|
|
||||||
authors: followsAsArr,
|
|
||||||
limit: 300,
|
|
||||||
});
|
|
||||||
|
|
||||||
events.forEach((event: NDKEvent) => {
|
|
||||||
event.tags.forEach((tag) => {
|
|
||||||
if (tag[0] === 'p') lru.set(tag[1], tag[1]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// get lru values
|
|
||||||
const circleList = [...lru.values()] as string[];
|
|
||||||
|
|
||||||
// update db
|
|
||||||
await db.updateAccount('follows', JSON.stringify(followsAsArr));
|
|
||||||
await db.updateAccount('circles', JSON.stringify(circleList));
|
|
||||||
|
|
||||||
db.account.follows = followsAsArr;
|
|
||||||
db.account.circles = circleList;
|
|
||||||
|
|
||||||
// clear lru
|
|
||||||
lru.clear();
|
|
||||||
|
|
||||||
// done
|
|
||||||
await db.createSetting('circles', '1');
|
|
||||||
setCircle();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200">
|
|
||||||
<div className="flex items-start justify-between gap-2">
|
|
||||||
<div>
|
|
||||||
<h5 className="font-semibold">Enable Circle</h5>
|
|
||||||
<p className="text-sm">
|
|
||||||
Beside newsfeed from your follows, you will see more content from all people
|
|
||||||
that followed by your follows.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{circle ? (
|
|
||||||
<div className="mt-1 inline-flex h-9 w-24 shrink-0 items-center justify-center rounded-lg bg-teal-500 text-white">
|
|
||||||
<CheckCircleIcon className="h-4 w-4" />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={enableLinks}
|
|
||||||
className="mt-1 inline-flex h-9 w-24 shrink-0 items-center justify-center rounded-lg bg-neutral-200 font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
|
||||||
>
|
|
||||||
{loading ? <LoaderIcon className="h-4 w-4 animate-spin" /> : 'Enable'}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import { useStorage } from '@libs/storage/provider';
|
|
||||||
|
|
||||||
import { CheckCircleIcon } from '@shared/icons';
|
|
||||||
|
|
||||||
import { useOnboarding } from '@stores/onboarding';
|
|
||||||
|
|
||||||
export function OutboxModel() {
|
|
||||||
const { db } = useStorage();
|
|
||||||
|
|
||||||
const [outbox, setOutbox] = useOnboarding((state) => [
|
|
||||||
state.outbox,
|
|
||||||
state.toggleOutbox,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const enableOutbox = async () => {
|
|
||||||
await db.createSetting('outbox', '1');
|
|
||||||
setOutbox();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200">
|
|
||||||
<div className="flex items-start justify-between gap-2">
|
|
||||||
<div>
|
|
||||||
<h5 className="font-semibold">Enable Outbox (experiment)</h5>
|
|
||||||
<p className="text-sm">
|
|
||||||
When you request information about a user, Lume will automatically query the
|
|
||||||
user's outbox relays and subsequent queries will favour using those
|
|
||||||
relays for queries with that user's pubkey.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{outbox ? (
|
|
||||||
<div className="mt-1 inline-flex h-9 w-24 shrink-0 items-center justify-center rounded-lg bg-teal-500 text-white">
|
|
||||||
<CheckCircleIcon className="h-4 w-4" />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={enableOutbox}
|
|
||||||
className="mt-1 inline-flex h-9 w-24 shrink-0 items-center justify-center rounded-lg bg-neutral-200 font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
|
||||||
>
|
|
||||||
Enable
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { CheckCircleIcon } from '@shared/icons';
|
|
||||||
|
|
||||||
import { useOnboarding } from '@stores/onboarding';
|
|
||||||
|
|
||||||
export function FavoriteHashtag() {
|
|
||||||
const hashtag = useOnboarding((state) => state.hashtag);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200">
|
|
||||||
<div className="flex items-start justify-between">
|
|
||||||
<div>
|
|
||||||
<h5 className="font-semibold">Favorite topic</h5>
|
|
||||||
<p className="text-sm">
|
|
||||||
By adding favorite topic, Lume will display all contents related to this topic
|
|
||||||
for you
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{hashtag ? (
|
|
||||||
<div className="mt-1 inline-flex h-9 w-24 shrink-0 items-center justify-center rounded-lg bg-teal-500 text-white">
|
|
||||||
<CheckCircleIcon className="h-4 w-4" />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Link
|
|
||||||
to="/auth/onboarding/hashtag"
|
|
||||||
className="mt-1 inline-flex h-9 w-24 shrink-0 items-center justify-center rounded-lg bg-neutral-200 font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
|
||||||
>
|
|
||||||
Add
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
|
|
||||||
import { useNDK } from '@libs/ndk/provider';
|
|
||||||
import { useStorage } from '@libs/storage/provider';
|
|
||||||
|
|
||||||
import { LoaderIcon } from '@shared/icons';
|
|
||||||
import { User } from '@shared/user';
|
|
||||||
|
|
||||||
export function FollowList() {
|
|
||||||
const { db } = useStorage();
|
|
||||||
const { ndk } = useNDK();
|
|
||||||
const { status, data } = useQuery({
|
|
||||||
queryKey: ['follows'],
|
|
||||||
queryFn: async () => {
|
|
||||||
const user = ndk.getUser({ pubkey: db.account.pubkey });
|
|
||||||
const follows = await user.follows();
|
|
||||||
const followsAsArr = [];
|
|
||||||
|
|
||||||
follows.forEach((user) => {
|
|
||||||
followsAsArr.push(user.pubkey);
|
|
||||||
});
|
|
||||||
|
|
||||||
// update db
|
|
||||||
await db.updateAccount('follows', JSON.stringify(followsAsArr));
|
|
||||||
await db.updateAccount('circles', JSON.stringify(followsAsArr));
|
|
||||||
|
|
||||||
db.account.follows = followsAsArr;
|
|
||||||
db.account.circles = followsAsArr;
|
|
||||||
|
|
||||||
return followsAsArr;
|
|
||||||
},
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200">
|
|
||||||
<h5 className="font-semibold">Your follows</h5>
|
|
||||||
<div className="mt-2 flex w-full items-center justify-center">
|
|
||||||
{status === 'pending' ? (
|
|
||||||
<LoaderIcon className="h-4 w-4 animate-spin text-neutral-900 dark:text-neutral-100" />
|
|
||||||
) : (
|
|
||||||
<div className="isolate flex -space-x-2">
|
|
||||||
{data.slice(0, 16).map((item) => (
|
|
||||||
<User key={item} pubkey={item} variant="stacked" />
|
|
||||||
))}
|
|
||||||
{data.length > 16 ? (
|
|
||||||
<div className="inline-flex h-8 w-8 items-center justify-center rounded-full bg-neutral-200 text-neutral-900 ring-1 ring-neutral-200 dark:bg-neutral-800 dark:text-neutral-100 dark:ring-neutral-800">
|
|
||||||
<span className="text-xs font-medium">+{data.length}</span>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { CheckCircleIcon } from '@shared/icons';
|
|
||||||
|
|
||||||
import { useOnboarding } from '@stores/onboarding';
|
|
||||||
|
|
||||||
export function SuggestFollow() {
|
|
||||||
const enrich = useOnboarding((state) => state.enrich);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200">
|
|
||||||
<div className="flex items-start justify-between">
|
|
||||||
<div>
|
|
||||||
<h5 className="font-semibold">Enrich your network</h5>
|
|
||||||
<p className="text-sm">
|
|
||||||
Follow more people to stay up to date with everything happening around the
|
|
||||||
world.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{enrich ? (
|
|
||||||
<div className="mt-1 inline-flex h-9 w-24 shrink-0 items-center justify-center rounded-lg bg-teal-500 text-white">
|
|
||||||
<CheckCircleIcon className="h-4 w-4" />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Link
|
|
||||||
to="/auth/onboarding/enrich"
|
|
||||||
className="mt-1 inline-flex h-9 w-24 shrink-0 items-center justify-center rounded-lg bg-neutral-200 font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
|
||||||
>
|
|
||||||
Check
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { NDKEvent, NDKKind, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
|
import { NDKEvent, NDKKind, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
|
||||||
import { downloadDir } from '@tauri-apps/api/path';
|
import { downloadDir } from '@tauri-apps/api/path';
|
||||||
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
|
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
|
||||||
import { message, save } from '@tauri-apps/plugin-dialog';
|
import { save } from '@tauri-apps/plugin-dialog';
|
||||||
import { writeTextFile } from '@tauri-apps/plugin-fs';
|
import { writeTextFile } from '@tauri-apps/plugin-fs';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { minidenticon } from 'minidenticons';
|
import { minidenticon } from 'minidenticons';
|
||||||
@@ -15,7 +15,7 @@ import { useNDK } from '@libs/ndk/provider';
|
|||||||
import { useStorage } from '@libs/storage/provider';
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
import { AvatarUploader } from '@shared/avatarUploader';
|
import { AvatarUploader } from '@shared/avatarUploader';
|
||||||
import { ArrowLeftIcon, LoaderIcon } from '@shared/icons';
|
import { ArrowLeftIcon, InfoIcon, LoaderIcon } from '@shared/icons';
|
||||||
import { User } from '@shared/user';
|
import { User } from '@shared/user';
|
||||||
|
|
||||||
export function CreateAccountScreen() {
|
export function CreateAccountScreen() {
|
||||||
@@ -45,8 +45,6 @@ export function CreateAccountScreen() {
|
|||||||
|
|
||||||
const onSubmit = async (data: { name: string; about: string }) => {
|
const onSubmit = async (data: { name: string; about: string }) => {
|
||||||
try {
|
try {
|
||||||
if (!ndk.signer) return navigate('/new/privkey');
|
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const profile = {
|
const profile = {
|
||||||
@@ -69,7 +67,6 @@ export function CreateAccountScreen() {
|
|||||||
const event = new NDKEvent(ndk);
|
const event = new NDKEvent(ndk);
|
||||||
event.content = JSON.stringify(profile);
|
event.content = JSON.stringify(profile);
|
||||||
event.kind = NDKKind.Metadata;
|
event.kind = NDKKind.Metadata;
|
||||||
event.created_at = Math.floor(Date.now() / 1000);
|
|
||||||
event.pubkey = userPubkey;
|
event.pubkey = userPubkey;
|
||||||
event.tags = [];
|
event.tags = [];
|
||||||
|
|
||||||
@@ -78,6 +75,16 @@ export function CreateAccountScreen() {
|
|||||||
if (publish) {
|
if (publish) {
|
||||||
await db.createAccount(userNpub, userPubkey);
|
await db.createAccount(userNpub, userPubkey);
|
||||||
await db.secureSave(userPubkey, userPrivkey);
|
await db.secureSave(userPubkey, userPrivkey);
|
||||||
|
|
||||||
|
const relayListEvent = new NDKEvent(ndk);
|
||||||
|
relayListEvent.kind = NDKKind.RelayList;
|
||||||
|
relayListEvent.tags = [...ndk.pool.relays.values()].map((item) => [
|
||||||
|
'r',
|
||||||
|
item.url,
|
||||||
|
]);
|
||||||
|
|
||||||
|
await relayListEvent.publish();
|
||||||
|
|
||||||
setKeys({
|
setKeys({
|
||||||
npub: userNpub,
|
npub: userNpub,
|
||||||
nsec: userNsec,
|
nsec: userNsec,
|
||||||
@@ -90,7 +97,7 @@ export function CreateAccountScreen() {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return toast(e);
|
return toast.error(e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -115,7 +122,7 @@ export function CreateAccountScreen() {
|
|||||||
setDownloaded(true);
|
setDownloaded(true);
|
||||||
} // else { user cancel action }
|
} // else { user cancel action }
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await message(e, { title: 'Cannot download account keys', type: 'error' });
|
return toast.error(e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -125,33 +132,33 @@ export function CreateAccountScreen() {
|
|||||||
{!keys ? (
|
{!keys ? (
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate(-1)}
|
onClick={() => navigate(-1)}
|
||||||
className="inline-flex items-center gap-2 text-sm font-medium"
|
className="group inline-flex items-center gap-2 text-sm font-medium"
|
||||||
>
|
>
|
||||||
<div className="inline-flex h-8 w-8 items-center justify-center rounded-lg bg-neutral-200 text-neutral-800 dark:bg-neutral-800 dark:text-neutral-200">
|
<div className="inline-flex h-8 w-8 items-center justify-center rounded-lg bg-neutral-200 text-neutral-800 group-hover:bg-neutral-300 dark:bg-neutral-800 dark:text-neutral-200 dark:group-hover:bg-neutral-700">
|
||||||
<ArrowLeftIcon className="h-5 w-5" />
|
<ArrowLeftIcon className="h-4 w-4" />
|
||||||
</div>
|
</div>
|
||||||
Back
|
Back
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-auto flex w-full max-w-md flex-col gap-10">
|
<div className="mx-auto flex w-full max-w-md flex-col gap-10">
|
||||||
<h1 className="text-center text-2xl font-semibold text-neutral-900 dark:text-neutral-100">
|
<h1 className="text-center text-2xl font-semibold">
|
||||||
Let's set up your account.
|
Let's set up your account.
|
||||||
</h1>
|
</h1>
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
{!keys ? (
|
{!keys ? (
|
||||||
<div className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200">
|
<div className="rounded-xl bg-neutral-50 p-3 dark:bg-neutral-950">
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="mb-0 flex flex-col">
|
<form onSubmit={handleSubmit(onSubmit)} className="mb-0 flex flex-col">
|
||||||
<input type={'hidden'} {...register('picture')} value={picture} />
|
<input type={'hidden'} {...register('picture')} value={picture} />
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="font-semibold">Avatar</span>
|
<span className="font-semibold">Avatar</span>
|
||||||
<div className="relative flex h-36 w-full items-center justify-center rounded-lg bg-neutral-200 dark:bg-neutral-800">
|
<div className="flex h-36 w-full flex-col items-center justify-center gap-3 rounded-lg bg-neutral-100 dark:bg-neutral-900">
|
||||||
{picture.length > 0 ? (
|
{picture.length > 0 ? (
|
||||||
<img
|
<img
|
||||||
src={picture}
|
src={picture}
|
||||||
alt="user's avatar"
|
alt="user's avatar"
|
||||||
className="h-14 w-14 rounded-xl"
|
className="h-14 w-14 rounded-xl object-cover"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<img
|
<img
|
||||||
@@ -160,9 +167,7 @@ export function CreateAccountScreen() {
|
|||||||
className="h-14 w-14 rounded-xl bg-black dark:bg-white"
|
className="h-14 w-14 rounded-xl bg-black dark:bg-white"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="absolute bottom-2 right-2">
|
<AvatarUploader setPicture={setPicture} />
|
||||||
<AvatarUploader setPicture={setPicture} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
@@ -176,7 +181,7 @@ export function CreateAccountScreen() {
|
|||||||
minLength: 1,
|
minLength: 1,
|
||||||
})}
|
})}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
className="h-11 rounded-lg bg-neutral-200 px-3 placeholder:text-neutral-500 dark:bg-neutral-800 dark:placeholder:text-neutral-400"
|
className="h-11 rounded-lg border-transparent bg-neutral-100 px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
@@ -186,20 +191,29 @@ export function CreateAccountScreen() {
|
|||||||
<textarea
|
<textarea
|
||||||
{...register('about')}
|
{...register('about')}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
className="relative h-20 w-full resize-none rounded-lg bg-neutral-200 px-3 py-2 !outline-none placeholder:text-neutral-500 dark:bg-neutral-800 dark:placeholder:text-neutral-400"
|
className="relative h-24 w-full resize-none rounded-lg border-transparent bg-neutral-100 px-3 py-2 !outline-none placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<div className="flex flex-col gap-3">
|
||||||
type="submit"
|
<div className="flex items-center gap-2 rounded-lg bg-blue-100 p-3 text-sm text-blue-800 dark:bg-blue-900 dark:text-blue-200">
|
||||||
disabled={!isDirty || !isValid}
|
<InfoIcon className="h-8 w-8" />
|
||||||
className="inline-flex h-9 w-full shrink-0 items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600 disabled:opacity-50"
|
<p>
|
||||||
>
|
There are many more settings you can configure from the
|
||||||
{loading ? (
|
"Settings" screen. Be sure to visit it later.
|
||||||
<LoaderIcon className="h-5 w-4 animate-spin" />
|
</p>
|
||||||
) : (
|
</div>
|
||||||
'Create and Continue'
|
<button
|
||||||
)}
|
type="submit"
|
||||||
</button>
|
disabled={!isDirty || !isValid}
|
||||||
|
className="inline-flex h-11 w-full shrink-0 items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<LoaderIcon className="h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
'Create and Continue'
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -211,7 +225,7 @@ export function CreateAccountScreen() {
|
|||||||
opacity: 1,
|
opacity: 1,
|
||||||
y: 0,
|
y: 0,
|
||||||
}}
|
}}
|
||||||
className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200"
|
className="rounded-xl bg-neutral-50 p-3 dark:bg-neutral-950"
|
||||||
>
|
>
|
||||||
<User pubkey={keys.pubkey} variant="simple" />
|
<User pubkey={keys.pubkey} variant="simple" />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -221,7 +235,7 @@ export function CreateAccountScreen() {
|
|||||||
opacity: 1,
|
opacity: 1,
|
||||||
y: 0,
|
y: 0,
|
||||||
}}
|
}}
|
||||||
className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200"
|
className="rounded-xl bg-neutral-50 p-3 dark:bg-neutral-950"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<h5 className="font-semibold">Backup account</h5>
|
<h5 className="font-semibold">Backup account</h5>
|
||||||
@@ -229,7 +243,7 @@ export function CreateAccountScreen() {
|
|||||||
<p className="mb-2 select-text text-sm text-neutral-800 dark:text-neutral-200">
|
<p className="mb-2 select-text text-sm text-neutral-800 dark:text-neutral-200">
|
||||||
Your private key is your password. If you lose this key, you will
|
Your private key is your password. If you lose this key, you will
|
||||||
lose access to your account! Copy it and keep it in a safe place.{' '}
|
lose access to your account! Copy it and keep it in a safe place.{' '}
|
||||||
<span className="text-red-600">
|
<span className="text-red-500">
|
||||||
There is no way to reset your private key.
|
There is no way to reset your private key.
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
@@ -249,13 +263,13 @@ export function CreateAccountScreen() {
|
|||||||
value={
|
value={
|
||||||
keys.nsec.substring(0, 10) + '**************************'
|
keys.nsec.substring(0, 10) + '**************************'
|
||||||
}
|
}
|
||||||
className="h-11 w-full rounded-lg bg-neutral-200 px-3 placeholder:text-neutral-500 dark:bg-neutral-800 dark:placeholder:text-neutral-400"
|
className="h-11 w-full rounded-lg border-transparent bg-neutral-100 px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
||||||
/>
|
/>
|
||||||
<div className="absolute right-0 top-0 inline-flex h-11 items-center justify-center px-2">
|
<div className="absolute right-0 top-0 inline-flex h-11 items-center justify-center px-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={copyNsec}
|
onClick={copyNsec}
|
||||||
className="rounded-md bg-neutral-300 px-2 py-1 text-sm font-medium hover:bg-neutral-400 dark:bg-neutral-700 dark:hover:bg-neutral-600"
|
className="rounded-md bg-neutral-200 px-2 py-1 text-sm font-medium hover:bg-neutral-400 dark:bg-neutral-700 dark:hover:bg-neutral-600"
|
||||||
>
|
>
|
||||||
Copy
|
Copy
|
||||||
</button>
|
</button>
|
||||||
@@ -269,7 +283,7 @@ export function CreateAccountScreen() {
|
|||||||
<input
|
<input
|
||||||
readOnly
|
readOnly
|
||||||
value={keys.npub}
|
value={keys.npub}
|
||||||
className="h-11 rounded-lg bg-neutral-200 px-3 placeholder:text-neutral-500 dark:bg-neutral-800 dark:placeholder:text-neutral-400"
|
className="h-11 w-full rounded-lg border-transparent bg-neutral-100 px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -277,7 +291,7 @@ export function CreateAccountScreen() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => download()}
|
onClick={() => download()}
|
||||||
className="inline-flex h-9 w-full shrink-0 items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600"
|
className="mt-1 inline-flex h-11 w-full shrink-0 items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600"
|
||||||
>
|
>
|
||||||
Download account keys
|
Download account keys
|
||||||
</button>
|
</button>
|
||||||
@@ -293,9 +307,9 @@ export function CreateAccountScreen() {
|
|||||||
opacity: 1,
|
opacity: 1,
|
||||||
y: 0,
|
y: 0,
|
||||||
}}
|
}}
|
||||||
className="inline-flex h-9 w-full shrink-0 items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600"
|
className="inline-flex h-11 w-full shrink-0 items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => navigate('/auth/onboarding', { state: { newuser: true } })}
|
onClick={() => navigate('/auth/onboarding')}
|
||||||
>
|
>
|
||||||
Finish
|
Finish
|
||||||
</motion.button>
|
</motion.button>
|
||||||
|
|||||||
34
src/app/auth/finish.tsx
Normal file
34
src/app/auth/finish.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
export function FinishScreen() {
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<div className="mx-auto flex w-full max-w-md flex-col gap-10">
|
||||||
|
<div className="text-center">
|
||||||
|
<img src="/icon.png" alt="Lume's logo" className="mx-auto mb-1 h-auto w-16" />
|
||||||
|
<h1 className="text-2xl font-light">
|
||||||
|
Yo, you're ready to use <span className="font-bold">Lume</span>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Link
|
||||||
|
to="/auth/tutorials/note"
|
||||||
|
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-blue-500 font-medium text-white hover:bg-blue-600"
|
||||||
|
>
|
||||||
|
Start tutorial
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/"
|
||||||
|
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-neutral-100 font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
|
||||||
|
>
|
||||||
|
Skip
|
||||||
|
</Link>
|
||||||
|
<p className="text-center text-sm font-medium text-neutral-500 dark:text-neutral-600">
|
||||||
|
You need to restart app to make changes in previous step take effect or you
|
||||||
|
can continue with Lume default settings
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
276
src/app/auth/follow.tsx
Normal file
276
src/app/auth/follow.tsx
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
|
||||||
|
import * as Accordion from '@radix-ui/react-accordion';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { nip19 } from 'nostr-tools';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
import { useNDK } from '@libs/ndk/provider';
|
||||||
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ArrowLeftIcon,
|
||||||
|
ArrowRightIcon,
|
||||||
|
CancelIcon,
|
||||||
|
ChevronDownIcon,
|
||||||
|
LoaderIcon,
|
||||||
|
PlusIcon,
|
||||||
|
} from '@shared/icons';
|
||||||
|
import { User } from '@shared/user';
|
||||||
|
|
||||||
|
const POPULAR_USERS = [
|
||||||
|
'npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6',
|
||||||
|
'npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m',
|
||||||
|
'npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s',
|
||||||
|
'npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z',
|
||||||
|
'npub1az9xj85cmxv8e9j9y80lvqp97crsqdu2fpu3srwthd99qfu9qsgstam8y8',
|
||||||
|
'npub1a2cww4kn9wqte4ry70vyfwqyqvpswksna27rtxd8vty6c74era8sdcw83a',
|
||||||
|
'npub168ghgug469n4r2tuyw05dmqhqv5jcwm7nxytn67afmz8qkc4a4zqsu2dlc',
|
||||||
|
'npub133vj8ycevdle0cq8mtgddq0xtn34kxkwxvak983dx0u5vhqnycyqj6tcza',
|
||||||
|
'npub18ams6ewn5aj2n3wt2qawzglx9mr4nzksxhvrdc4gzrecw7n5tvjqctp424',
|
||||||
|
'npub1r0rs5q2gk0e3dk3nlc7gnu378ec6cnlenqp8a3cjhyzu6f8k5sgs4sq9ac',
|
||||||
|
'npub1prya33fnqerq0fljwjtp77ehtu7jlsjt5ydhwveuwmqdsdm6k8esk42xcv',
|
||||||
|
'npub19mduaf5569jx9xz555jcx3v06mvktvtpu0zgk47n4lcpjsz43zzqhj6vzk',
|
||||||
|
];
|
||||||
|
const LUME_USERS = ['npub1zfss807aer0j26mwp2la0ume0jqde3823rmu97ra6sgyyg956e0s6xw445'];
|
||||||
|
|
||||||
|
export function FollowScreen() {
|
||||||
|
const { ndk } = useNDK();
|
||||||
|
const { db } = useStorage();
|
||||||
|
const { status, data } = useQuery({
|
||||||
|
queryKey: ['trending-profiles-widget'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const res = await fetch('https://api.nostr.band/v0/trending/profiles');
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error('Error');
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [follows, setFollows] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
// toggle follow state
|
||||||
|
const toggleFollow = (pubkey: string) => {
|
||||||
|
const arr = follows.includes(pubkey)
|
||||||
|
? follows.filter((i) => i !== pubkey)
|
||||||
|
: [...follows, pubkey];
|
||||||
|
setFollows(arr);
|
||||||
|
};
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
if (!follows.length) return navigate('/auth/finish');
|
||||||
|
|
||||||
|
const event = new NDKEvent(ndk);
|
||||||
|
event.kind = NDKKind.Contacts;
|
||||||
|
event.tags = follows.map((item) => {
|
||||||
|
if (item.startsWith('npub')) return ['p', nip19.decode(item).data as string];
|
||||||
|
return ['p', item];
|
||||||
|
});
|
||||||
|
|
||||||
|
const publish = await event.publish();
|
||||||
|
if (publish) {
|
||||||
|
db.account.contacts = follows.map((item) => {
|
||||||
|
if (item.startsWith('npub')) return nip19.decode(item).data as string;
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
|
||||||
|
return navigate('/auth/finish');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setLoading(false);
|
||||||
|
toast.error(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative flex h-full w-full items-center justify-center">
|
||||||
|
<div className="mx-auto flex w-full max-w-md flex-col gap-10">
|
||||||
|
<div className="text-center">
|
||||||
|
<h1 className="text-2xl font-semibold">Dive into the nostrverse</h1>
|
||||||
|
<h2 className="text-neutral-700 dark:text-neutral-300">
|
||||||
|
Try following some users that interest you
|
||||||
|
<br />
|
||||||
|
to build up your timeline.
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<Accordion.Root type="single" defaultValue="recommended" collapsible>
|
||||||
|
<Accordion.Item value="recommended" className="mb-3 overflow-hidden rounded-xl">
|
||||||
|
<Accordion.Trigger className="flex h-12 w-full items-center justify-between rounded-t-xl bg-neutral-100 px-3 font-medium dark:bg-neutral-900">
|
||||||
|
Popular users
|
||||||
|
<ChevronDownIcon className="h-4 w-4" />
|
||||||
|
</Accordion.Trigger>
|
||||||
|
<Accordion.Content>
|
||||||
|
<div className="flex h-[420px] w-full flex-col gap-3 overflow-y-auto rounded-b-xl bg-neutral-50 p-3 dark:bg-neutral-950">
|
||||||
|
{POPULAR_USERS.map((pubkey) => (
|
||||||
|
<div
|
||||||
|
key={pubkey}
|
||||||
|
className="flex h-max w-full shrink-0 flex-col overflow-hidden rounded-lg border border-neutral-100 bg-white dark:border-neutral-900 dark:bg-black"
|
||||||
|
>
|
||||||
|
<div className="p-3">
|
||||||
|
<User pubkey={pubkey} variant="large" />
|
||||||
|
</div>
|
||||||
|
<div className="border-t border-neutral-100 px-3 py-4 dark:border-neutral-900">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => toggleFollow(pubkey)}
|
||||||
|
className={twMerge(
|
||||||
|
'inline-flex h-9 w-full items-center justify-center gap-1 rounded-lg font-medium text-white',
|
||||||
|
follows.includes(pubkey)
|
||||||
|
? 'bg-red-500 hover:bg-red-600'
|
||||||
|
: 'bg-blue-500 hover:bg-blue-600'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{follows.includes(pubkey) ? (
|
||||||
|
<>
|
||||||
|
<CancelIcon className="h-4 w-4" />
|
||||||
|
Unfollow
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<PlusIcon className="h-4 w-4" />
|
||||||
|
Follow
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Accordion.Content>
|
||||||
|
</Accordion.Item>
|
||||||
|
<Accordion.Item value="trending" className="mb-3 overflow-hidden rounded-xl">
|
||||||
|
<Accordion.Trigger className="flex h-12 w-full items-center justify-between rounded-t-xl bg-neutral-100 px-3 font-medium dark:bg-neutral-900">
|
||||||
|
Trending users
|
||||||
|
<ChevronDownIcon className="h-4 w-4" />
|
||||||
|
</Accordion.Trigger>
|
||||||
|
<Accordion.Content>
|
||||||
|
<div className="flex h-[420px] w-full flex-col gap-3 overflow-y-auto rounded-b-xl bg-neutral-50 p-3 dark:bg-neutral-950">
|
||||||
|
{status === 'pending' ? (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<LoaderIcon className="h-4 w-4 animate-spin" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
data?.profiles.map(
|
||||||
|
(item: { pubkey: string; profile: { content: string } }) => (
|
||||||
|
<div
|
||||||
|
key={item.pubkey}
|
||||||
|
className="flex h-max w-full shrink-0 flex-col overflow-hidden rounded-lg border border-neutral-100 bg-white dark:border-neutral-900 dark:bg-black"
|
||||||
|
>
|
||||||
|
<div className="p-3">
|
||||||
|
<User
|
||||||
|
pubkey={item.pubkey}
|
||||||
|
variant="large"
|
||||||
|
embedProfile={item.profile?.content}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="border-t border-neutral-100 px-3 py-4 dark:border-neutral-900">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => toggleFollow(item.pubkey)}
|
||||||
|
className={twMerge(
|
||||||
|
'inline-flex h-9 w-full items-center justify-center gap-1 rounded-lg font-medium text-white',
|
||||||
|
follows.includes(item.pubkey)
|
||||||
|
? 'bg-red-500 hover:bg-red-600'
|
||||||
|
: 'bg-blue-500 hover:bg-blue-600'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{follows.includes(item.pubkey) ? (
|
||||||
|
<>
|
||||||
|
<CancelIcon className="h-4 w-4" />
|
||||||
|
Unfollow
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<PlusIcon className="h-4 w-4" />
|
||||||
|
Follow
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Accordion.Content>
|
||||||
|
</Accordion.Item>
|
||||||
|
<Accordion.Item value="lume" className="mb-3 overflow-hidden rounded-xl">
|
||||||
|
<Accordion.Trigger className="flex h-12 w-full items-center justify-between rounded-t-xl bg-neutral-100 px-3 font-medium dark:bg-neutral-900">
|
||||||
|
Lume team
|
||||||
|
<ChevronDownIcon className="h-4 w-4" />
|
||||||
|
</Accordion.Trigger>
|
||||||
|
<Accordion.Content>
|
||||||
|
<div className="flex h-[420px] w-full flex-col gap-3 overflow-y-auto rounded-b-xl bg-neutral-50 p-3 dark:bg-neutral-950">
|
||||||
|
{LUME_USERS.map((pubkey) => (
|
||||||
|
<div
|
||||||
|
key={pubkey}
|
||||||
|
className="flex h-max w-full shrink-0 flex-col overflow-hidden rounded-lg border border-neutral-100 bg-white dark:border-neutral-900 dark:bg-black"
|
||||||
|
>
|
||||||
|
<div className="p-3">
|
||||||
|
<User pubkey={pubkey} variant="large" />
|
||||||
|
</div>
|
||||||
|
<div className="border-t border-neutral-100 px-3 py-4 dark:border-neutral-900">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => toggleFollow(pubkey)}
|
||||||
|
className={twMerge(
|
||||||
|
'inline-flex h-9 w-full items-center justify-center gap-1 rounded-lg font-medium text-white',
|
||||||
|
follows.includes(pubkey)
|
||||||
|
? 'bg-red-500 hover:bg-red-600'
|
||||||
|
: 'bg-blue-500 hover:bg-blue-600'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{follows.includes(pubkey) ? (
|
||||||
|
<>
|
||||||
|
<CancelIcon className="h-4 w-4" />
|
||||||
|
Unfollow
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<PlusIcon className="h-4 w-4" />
|
||||||
|
Follow
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Accordion.Content>
|
||||||
|
</Accordion.Item>
|
||||||
|
</Accordion.Root>
|
||||||
|
</div>
|
||||||
|
<div className="absolute bottom-3 right-3 flex w-full items-center justify-end gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => navigate(-1)}
|
||||||
|
className="inline-flex h-11 w-max items-center justify-center gap-2 rounded-lg bg-neutral-100 px-3 font-semibold hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-blue-800"
|
||||||
|
>
|
||||||
|
<ArrowLeftIcon className="h-4 w-4" />
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={submit}
|
||||||
|
disabled={loading}
|
||||||
|
className="inline-flex h-11 w-max items-center justify-center gap-2 rounded-lg bg-blue-500 px-3 font-semibold text-white hover:bg-blue-600"
|
||||||
|
>
|
||||||
|
Continue
|
||||||
|
{loading ? (
|
||||||
|
<LoaderIcon className="h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<ArrowRightIcon className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { NDKNip46Signer, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
|
import NDK, { NDKNip46Signer, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
|
||||||
import { readText } from '@tauri-apps/plugin-clipboard-manager';
|
import { readText } from '@tauri-apps/plugin-clipboard-manager';
|
||||||
|
import { open } from '@tauri-apps/plugin-shell';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { nip19 } from 'nostr-tools';
|
import { nip19 } from 'nostr-tools';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
@@ -10,21 +11,22 @@ import { twMerge } from 'tailwind-merge';
|
|||||||
import { useNDK } from '@libs/ndk/provider';
|
import { useNDK } from '@libs/ndk/provider';
|
||||||
import { useStorage } from '@libs/storage/provider';
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
import { ArrowLeftIcon } from '@shared/icons';
|
import { ArrowLeftIcon, LoaderIcon } from '@shared/icons';
|
||||||
import { User } from '@shared/user';
|
import { User } from '@shared/user';
|
||||||
|
|
||||||
export function ImportAccountScreen() {
|
export function ImportAccountScreen() {
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const { db } = useStorage();
|
const { db } = useStorage();
|
||||||
const { ndk } = useNDK();
|
const { ndk } = useNDK();
|
||||||
|
|
||||||
const [npub, setNpub] = useState<string>('');
|
const [npub, setNpub] = useState<string>('');
|
||||||
const [nsec, setNsec] = useState<string>('');
|
const [nsec, setNsec] = useState<string>('');
|
||||||
const [pubkey, setPubkey] = useState<undefined | string>(undefined);
|
const [pubkey, setPubkey] = useState<undefined | string>(undefined);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
const [created, setCreated] = useState({ ok: false, remote: false });
|
const [created, setCreated] = useState({ ok: false, remote: false });
|
||||||
const [savedPrivkey, setSavedPrivkey] = useState(false);
|
const [savedPrivkey, setSavedPrivkey] = useState(false);
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const submitNpub = async () => {
|
const submitNpub = async () => {
|
||||||
if (npub.length < 6) return toast.error('You must enter valid npub');
|
if (npub.length < 6) return toast.error('You must enter valid npub');
|
||||||
if (!npub.startsWith('npub1')) return toast.error('npub must be starts with npub1');
|
if (!npub.startsWith('npub1')) return toast.error('npub must be starts with npub1');
|
||||||
@@ -44,9 +46,19 @@ export function ImportAccountScreen() {
|
|||||||
try {
|
try {
|
||||||
const pubkey = nip19.decode(npub.split('#')[0]).data as string;
|
const pubkey = nip19.decode(npub.split('#')[0]).data as string;
|
||||||
const localSigner = NDKPrivateKeySigner.generate();
|
const localSigner = NDKPrivateKeySigner.generate();
|
||||||
await db.secureSave(pubkey + '-bunker', localSigner.privateKey);
|
|
||||||
|
|
||||||
const remoteSigner = new NDKNip46Signer(ndk, npub, localSigner);
|
await db.createSetting('nsecbunker', '1');
|
||||||
|
await db.secureSave(`${npub}-nsecbunker`, localSigner.privateKey);
|
||||||
|
|
||||||
|
// open nsecbunker web app in default browser
|
||||||
|
await open('https://app.nsecbunker.com/keys');
|
||||||
|
|
||||||
|
const bunker = new NDK({
|
||||||
|
explicitRelayUrls: ['wss://relay.nsecbunker.com', 'wss://nostr.vulpem.com'],
|
||||||
|
});
|
||||||
|
await bunker.connect();
|
||||||
|
|
||||||
|
const remoteSigner = new NDKNip46Signer(bunker, npub, localSigner);
|
||||||
await remoteSigner.blockUntilReady();
|
await remoteSigner.blockUntilReady();
|
||||||
|
|
||||||
ndk.signer = remoteSigner;
|
ndk.signer = remoteSigner;
|
||||||
@@ -65,12 +77,25 @@ export function ImportAccountScreen() {
|
|||||||
|
|
||||||
const createAccount = async () => {
|
const createAccount = async () => {
|
||||||
try {
|
try {
|
||||||
await db.createAccount(npub, pubkey);
|
setLoading(true);
|
||||||
setCreated((prev) => ({ ...prev, ok: true }));
|
|
||||||
|
|
||||||
if (created.remote) navigate('/auth/onboarding', { state: { newuser: false } });
|
// add account to db
|
||||||
|
await db.createAccount(npub, pubkey);
|
||||||
|
|
||||||
|
// get account metadata
|
||||||
|
const user = ndk.getUser({ pubkey });
|
||||||
|
if (user) {
|
||||||
|
db.account.contacts = [...(await user.follows())].map((user) => user.pubkey);
|
||||||
|
db.account.relayList = await user.relayList();
|
||||||
|
}
|
||||||
|
|
||||||
|
setCreated((prev) => ({ ...prev, ok: true }));
|
||||||
|
setLoading(false);
|
||||||
|
|
||||||
|
if (created.remote) navigate('/auth/onboarding');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return toast(`Create account failed: ${e}`);
|
setLoading(false);
|
||||||
|
return toast.error(e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -111,17 +136,16 @@ export function ImportAccountScreen() {
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-auto flex w-full max-w-md flex-col gap-10">
|
<div className="mx-auto flex w-full max-w-md flex-col gap-10">
|
||||||
<h1 className="text-center text-2xl font-semibold text-neutral-900 dark:text-neutral-100">
|
<h1 className="text-center text-2xl font-semibold">Import your account.</h1>
|
||||||
Import your account.
|
|
||||||
</h1>
|
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<div className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200">
|
<div className="rounded-xl bg-neutral-50 p-3 dark:bg-neutral-950">
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<label htmlFor="npub" className="font-semibold">
|
<label htmlFor="npub" className="font-semibold">
|
||||||
Enter your public key:
|
Enter your public key:
|
||||||
</label>
|
</label>
|
||||||
<div className="flex w-full flex-col gap-2">
|
<div className="flex w-full flex-col gap-2">
|
||||||
<input
|
<input
|
||||||
|
readOnly={!!pubkey}
|
||||||
name="npub"
|
name="npub"
|
||||||
type="text"
|
type="text"
|
||||||
value={npub}
|
value={npub}
|
||||||
@@ -131,26 +155,32 @@ export function ImportAccountScreen() {
|
|||||||
autoCorrect="off"
|
autoCorrect="off"
|
||||||
autoCapitalize="off"
|
autoCapitalize="off"
|
||||||
placeholder="npub1"
|
placeholder="npub1"
|
||||||
className="h-11 w-full rounded-lg bg-neutral-200 px-3 placeholder:text-neutral-500 dark:bg-neutral-800 dark:placeholder:text-neutral-400"
|
className="h-11 rounded-lg border-transparent bg-neutral-100 px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
||||||
/>
|
/>
|
||||||
{!pubkey ? (
|
{!pubkey ? (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={submitNpub}
|
onClick={submitNpub}
|
||||||
className="h-9 w-full shrink-0 rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600"
|
className="h-11 w-full shrink-0 rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600"
|
||||||
>
|
>
|
||||||
Continue
|
Continue
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={connectNsecBunker}
|
onClick={connectNsecBunker}
|
||||||
className="h-9 w-full shrink-0 rounded-lg bg-neutral-200 font-semibold text-neutral-900 hover:bg-neutral-300 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-neutral-700"
|
className="h-11 w-full shrink-0 rounded-lg bg-neutral-200 font-medium hover:bg-neutral-300 dark:bg-neutral-800 dark:hover:bg-neutral-700"
|
||||||
>
|
>
|
||||||
Continue with nsecBunker
|
Continue with nsecBunker
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
{npub.indexOf('#') > -1 ? (
|
||||||
|
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
||||||
|
You're using nsecbunker token, keep in mind it only can redeem
|
||||||
|
one-time, you need to login again in the next launch
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -161,16 +191,16 @@ export function ImportAccountScreen() {
|
|||||||
opacity: 1,
|
opacity: 1,
|
||||||
y: 0,
|
y: 0,
|
||||||
}}
|
}}
|
||||||
className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200"
|
className="rounded-xl bg-neutral-50 p-3 dark:bg-neutral-950"
|
||||||
>
|
>
|
||||||
<h5 className="mb-1.5 font-semibold">Account found</h5>
|
<h5 className="mb-1.5 font-semibold">Account found</h5>
|
||||||
<div className="flex w-full flex-col gap-2">
|
<div className="flex w-full flex-col gap-2">
|
||||||
<div className="flex h-full w-full items-center justify-between rounded-lg bg-neutral-200 p-2 dark:bg-neutral-800">
|
<div className="flex h-full w-full items-center justify-between rounded-lg bg-neutral-100 px-4 py-3 dark:bg-neutral-900">
|
||||||
<User pubkey={pubkey} variant="simple" />
|
<User pubkey={pubkey} variant="simple" />
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={changeAccount}
|
onClick={changeAccount}
|
||||||
className="h-8 w-20 shrink-0 rounded-lg bg-neutral-300 text-sm font-medium text-neutral-800 hover:bg-neutral-400 dark:bg-neutral-700 dark:text-neutral-200 dark:hover:bg-neutral-600"
|
className="h-8 w-max shrink-0 rounded-lg bg-neutral-200 px-2.5 text-sm font-medium hover:bg-neutral-300 dark:bg-neutral-800 dark:hover:bg-neutral-700"
|
||||||
>
|
>
|
||||||
Change
|
Change
|
||||||
</button>
|
</button>
|
||||||
@@ -179,9 +209,13 @@ export function ImportAccountScreen() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={createAccount}
|
onClick={createAccount}
|
||||||
className="h-9 w-full shrink-0 rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600"
|
className="inline-flex h-11 w-full shrink-0 items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600"
|
||||||
>
|
>
|
||||||
Continue
|
{loading ? (
|
||||||
|
<LoaderIcon className="h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
'Continue'
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
@@ -214,7 +248,7 @@ export function ImportAccountScreen() {
|
|||||||
autoCorrect="off"
|
autoCorrect="off"
|
||||||
autoCapitalize="off"
|
autoCapitalize="off"
|
||||||
placeholder="nsec1"
|
placeholder="nsec1"
|
||||||
className="h-11 w-full rounded-lg bg-neutral-200 px-3 placeholder:text-neutral-500 dark:bg-neutral-800 dark:placeholder:text-neutral-400"
|
className="h-11 w-full rounded-lg border-transparent bg-neutral-200 px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-800 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
||||||
/>
|
/>
|
||||||
{nsec.length < 5 ? (
|
{nsec.length < 5 ? (
|
||||||
<div className="absolute right-0 top-0 inline-flex h-11 items-center justify-center px-2">
|
<div className="absolute right-0 top-0 inline-flex h-11 items-center justify-center px-2">
|
||||||
@@ -259,8 +293,8 @@ export function ImportAccountScreen() {
|
|||||||
{db.platform === 'macos'
|
{db.platform === 'macos'
|
||||||
? 'Apple Keychain (macOS)'
|
? 'Apple Keychain (macOS)'
|
||||||
: db.platform === 'windows'
|
: db.platform === 'windows'
|
||||||
? 'Credential Manager (Windows)'
|
? 'Credential Manager (Windows)'
|
||||||
: 'Secret Service (Linux)'}
|
: 'Secret Service (Linux)'}
|
||||||
</b>
|
</b>
|
||||||
, it will be secured by your OS
|
, it will be secured by your OS
|
||||||
</p>
|
</p>
|
||||||
@@ -281,11 +315,9 @@ export function ImportAccountScreen() {
|
|||||||
opacity: 1,
|
opacity: 1,
|
||||||
y: 0,
|
y: 0,
|
||||||
}}
|
}}
|
||||||
className="inline-flex h-9 w-full shrink-0 items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600"
|
className="inline-flex h-11 w-full shrink-0 items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() =>
|
onClick={() => navigate('/auth/onboarding')}
|
||||||
navigate('/auth/onboarding', { state: { newuser: false } })
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
Continue
|
Continue
|
||||||
</motion.button>
|
</motion.button>
|
||||||
|
|||||||
148
src/app/auth/onboarding.tsx
Normal file
148
src/app/auth/onboarding.tsx
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import * as Switch from '@radix-ui/react-switch';
|
||||||
|
import { isPermissionGranted, requestPermission } from '@tauri-apps/plugin-notification';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
|
import { InfoIcon } from '@shared/icons';
|
||||||
|
|
||||||
|
export function OnboardingScreen() {
|
||||||
|
const { db } = useStorage();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const [settings, setSettings] = useState({
|
||||||
|
autoupdate: false,
|
||||||
|
outbox: false,
|
||||||
|
notification: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const next = () => {
|
||||||
|
if (!db.account.contacts.length) return navigate('/auth/follow');
|
||||||
|
return navigate('/auth/finish');
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleOutbox = async () => {
|
||||||
|
await db.createSetting('outbox', String(+!settings.outbox));
|
||||||
|
// update state
|
||||||
|
setSettings((prev) => ({ ...prev, outbox: !settings.outbox }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleAutoupdate = async () => {
|
||||||
|
await db.createSetting('autoupdate', String(+!settings.autoupdate));
|
||||||
|
db.settings.autoupdate = !settings.autoupdate;
|
||||||
|
// update state
|
||||||
|
setSettings((prev) => ({ ...prev, autoupdate: !settings.autoupdate }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleNofitication = async () => {
|
||||||
|
await requestPermission();
|
||||||
|
// update state
|
||||||
|
setSettings((prev) => ({ ...prev, notification: !settings.notification }));
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function loadSettings() {
|
||||||
|
const permissionGranted = await isPermissionGranted();
|
||||||
|
setSettings((prev) => ({ ...prev, notification: permissionGranted }));
|
||||||
|
|
||||||
|
const data = await db.getAllSettings();
|
||||||
|
if (!data) return;
|
||||||
|
|
||||||
|
data.forEach((item) => {
|
||||||
|
if (item.key === 'autoupdate')
|
||||||
|
setSettings((prev) => ({
|
||||||
|
...prev,
|
||||||
|
autoupdate: !!parseInt(item.value),
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (item.key === 'outbox')
|
||||||
|
setSettings((prev) => ({
|
||||||
|
...prev,
|
||||||
|
outbox: !!parseInt(item.value),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadSettings();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative flex h-full w-full items-center justify-center">
|
||||||
|
<div className="mx-auto flex w-full max-w-md flex-col gap-10">
|
||||||
|
<div className="text-center">
|
||||||
|
<h1 className="text-2xl font-light text-neutral-900 dark:text-neutral-100">
|
||||||
|
You're almost ready to use Lume.
|
||||||
|
</h1>
|
||||||
|
<h2 className="text-xl font-semibold text-neutral-900 dark:text-neutral-100">
|
||||||
|
Let's start personalizing your experience.
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<div className="flex w-full items-start justify-between gap-4 rounded-lg bg-neutral-100 p-3 dark:bg-neutral-900">
|
||||||
|
<Switch.Root
|
||||||
|
checked={settings.autoupdate}
|
||||||
|
onClick={() => toggleAutoupdate()}
|
||||||
|
className="relative mt-1 h-7 w-12 shrink-0 cursor-default rounded-full bg-neutral-200 outline-none data-[state=checked]:bg-blue-500 dark:bg-neutral-800"
|
||||||
|
>
|
||||||
|
<Switch.Thumb className="block h-6 w-6 translate-x-0.5 rounded-full bg-white transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
|
||||||
|
</Switch.Root>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold">Auto check for update on Login</h3>
|
||||||
|
<p className="text-sm">
|
||||||
|
Keep Lume up to date with latest version, always have new features and bug
|
||||||
|
free.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full items-start justify-between gap-4 rounded-lg bg-neutral-100 p-3 dark:bg-neutral-900">
|
||||||
|
<Switch.Root
|
||||||
|
checked={settings.notification}
|
||||||
|
onClick={() => toggleNofitication()}
|
||||||
|
className="relative mt-1 h-7 w-12 shrink-0 cursor-default rounded-full bg-neutral-200 outline-none data-[state=checked]:bg-blue-500 dark:bg-neutral-800"
|
||||||
|
>
|
||||||
|
<Switch.Thumb className="block h-6 w-6 translate-x-0.5 rounded-full bg-white transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
|
||||||
|
</Switch.Root>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold">Push notification</h3>
|
||||||
|
<p className="text-sm">
|
||||||
|
Enabling push notifications will allow you to receive notifications from
|
||||||
|
Lume directly on your device.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full items-start justify-between gap-4 rounded-lg bg-neutral-100 p-3 dark:bg-neutral-900">
|
||||||
|
<Switch.Root
|
||||||
|
checked={settings.outbox}
|
||||||
|
onClick={() => toggleOutbox()}
|
||||||
|
className="relative mt-1 h-7 w-12 shrink-0 cursor-default rounded-full bg-neutral-200 outline-none data-[state=checked]:bg-blue-500 dark:bg-neutral-800"
|
||||||
|
>
|
||||||
|
<Switch.Thumb className="block h-6 w-6 translate-x-0.5 rounded-full bg-white transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
|
||||||
|
</Switch.Root>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold">Use Gossip model (recommended)</h3>
|
||||||
|
<p className="text-sm">
|
||||||
|
Automatically discover relays to connect based on the preferences of each
|
||||||
|
author.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 rounded-lg bg-blue-100 p-3 text-sm text-blue-800 dark:bg-blue-900 dark:text-blue-200">
|
||||||
|
<InfoIcon className="h-8 w-8" />
|
||||||
|
<p>
|
||||||
|
There are many more settings you can configure from the "Settings"
|
||||||
|
screen. Be sure to visit it later.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={next}
|
||||||
|
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600"
|
||||||
|
>
|
||||||
|
Continue
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
|
|
||||||
import { useNDK } from '@libs/ndk/provider';
|
|
||||||
import { useStorage } from '@libs/storage/provider';
|
|
||||||
|
|
||||||
import { ArrowLeftIcon, CheckCircleIcon, LoaderIcon } from '@shared/icons';
|
|
||||||
import { User } from '@shared/user';
|
|
||||||
|
|
||||||
import { useOnboarding } from '@stores/onboarding';
|
|
||||||
|
|
||||||
import { arrayToNIP02 } from '@utils/transform';
|
|
||||||
|
|
||||||
export function OnboardEnrichScreen() {
|
|
||||||
const { ndk } = useNDK();
|
|
||||||
const { db } = useStorage();
|
|
||||||
const { status, data } = useQuery({
|
|
||||||
queryKey: ['trending-profiles-widget'],
|
|
||||||
queryFn: async () => {
|
|
||||||
const res = await fetch('https://api.nostr.band/v0/trending/profiles');
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error('Error');
|
|
||||||
}
|
|
||||||
return res.json();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [follows, setFollows] = useState([]);
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const setEnrich = useOnboarding((state) => state.toggleEnrich);
|
|
||||||
|
|
||||||
// toggle follow state
|
|
||||||
const toggleFollow = (pubkey: string) => {
|
|
||||||
const arr = follows.includes(pubkey)
|
|
||||||
? follows.filter((i) => i !== pubkey)
|
|
||||||
: [...follows, pubkey];
|
|
||||||
setFollows(arr);
|
|
||||||
};
|
|
||||||
|
|
||||||
const submit = async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
const tags = arrayToNIP02(follows);
|
|
||||||
const event = new NDKEvent(ndk);
|
|
||||||
event.content = '';
|
|
||||||
event.kind = NDKKind.Contacts;
|
|
||||||
event.created_at = Math.floor(Date.now() / 1000);
|
|
||||||
event.tags = tags;
|
|
||||||
|
|
||||||
const publish = await event.publish();
|
|
||||||
|
|
||||||
// redirect to next step
|
|
||||||
if (publish) {
|
|
||||||
db.account.follows = follows;
|
|
||||||
|
|
||||||
await db.updateAccount('follows', JSON.stringify(follows));
|
|
||||||
await db.updateAccount('circles', JSON.stringify(follows));
|
|
||||||
|
|
||||||
setEnrich();
|
|
||||||
navigate(-1);
|
|
||||||
} else {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
setLoading(false);
|
|
||||||
toast(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative flex h-full w-full flex-col justify-center">
|
|
||||||
<div className="absolute left-[8px] top-4">
|
|
||||||
<button
|
|
||||||
onClick={() => navigate(-1)}
|
|
||||||
className="inline-flex items-center gap-2 text-sm font-medium"
|
|
||||||
>
|
|
||||||
<div className="inline-flex h-8 w-8 items-center justify-center rounded-lg bg-neutral-200 text-neutral-800 dark:bg-neutral-800 dark:text-neutral-200">
|
|
||||||
<ArrowLeftIcon className="h-5 w-5" />
|
|
||||||
</div>
|
|
||||||
Back
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="mx-auto mb-8 w-full max-w-md px-3">
|
|
||||||
<h1 className="text-center text-2xl font-semibold text-neutral-900 dark:text-neutral-100">
|
|
||||||
Enrich your network
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
<div className="flex w-full flex-nowrap items-center gap-4 overflow-x-auto px-4 scrollbar-none">
|
|
||||||
{status === 'pending' ? (
|
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
|
||||||
<LoaderIcon className="h-4 w-4 animate-spin text-neutral-900 dark:text-neutral-100" />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
data?.profiles.map((item: { pubkey: string; profile: { content: string } }) => (
|
|
||||||
<button
|
|
||||||
key={item.pubkey}
|
|
||||||
type="button"
|
|
||||||
onClick={() => toggleFollow(item.pubkey)}
|
|
||||||
className="relative h-[300px] shrink-0 grow-0 basis-[250px] overflow-hidden rounded-lg bg-neutral-200 px-4 py-4 hover:bg-neutral-300 dark:bg-neutral-800 dark:hover:bg-neutral-700"
|
|
||||||
>
|
|
||||||
<User
|
|
||||||
pubkey={item.pubkey}
|
|
||||||
variant="large"
|
|
||||||
embedProfile={item.profile?.content}
|
|
||||||
/>
|
|
||||||
{follows.includes(item.pubkey) && (
|
|
||||||
<div className="absolute right-2 top-2">
|
|
||||||
<CheckCircleIcon className="h-5 w-5 text-teal-400" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="mx-auto mt-8 w-full max-w-md px-3">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={submit}
|
|
||||||
disabled={loading || follows.length === 0}
|
|
||||||
className="inline-flex h-9 w-full items-center justify-center gap-2 rounded-lg bg-blue-500 font-medium text-white hover:bg-blue-600 focus:outline-none disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<>
|
|
||||||
<LoaderIcon className="h-4 w-4 animate-spin" />
|
|
||||||
<span>It might take a bit, please patient...</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<span>Follow {follows.length} accounts & Continue</span>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
import { message } from '@tauri-apps/plugin-dialog';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { ArrowLeftIcon, CheckCircleIcon, LoaderIcon } from '@shared/icons';
|
|
||||||
|
|
||||||
import { TOPICS, WIDGET_KIND } from '@stores/constants';
|
|
||||||
import { useOnboarding } from '@stores/onboarding';
|
|
||||||
|
|
||||||
import { useWidget } from '@utils/hooks/useWidget';
|
|
||||||
|
|
||||||
export function OnboardHashtagScreen() {
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [topic, setTopic] = useState(null);
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const setHashtag = useOnboarding((state) => state.toggleHashtag);
|
|
||||||
|
|
||||||
const { addWidget } = useWidget();
|
|
||||||
|
|
||||||
const submit = async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
setHashtag();
|
|
||||||
|
|
||||||
addWidget.mutate({
|
|
||||||
kind: WIDGET_KIND.topic,
|
|
||||||
title: topic.title,
|
|
||||||
content: JSON.stringify(topic.content),
|
|
||||||
});
|
|
||||||
|
|
||||||
navigate(-1);
|
|
||||||
} catch (e) {
|
|
||||||
setLoading(false);
|
|
||||||
await message(e, { title: 'Lume', type: 'error' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative flex h-full w-full flex-col justify-center">
|
|
||||||
<div className="absolute left-[8px] top-4">
|
|
||||||
<button
|
|
||||||
onClick={() => navigate(-1)}
|
|
||||||
className="inline-flex items-center gap-2 text-sm font-medium"
|
|
||||||
>
|
|
||||||
<div className="inline-flex h-8 w-8 items-center justify-center rounded-lg bg-neutral-200 text-neutral-800 dark:bg-neutral-800 dark:text-neutral-200">
|
|
||||||
<ArrowLeftIcon className="h-5 w-5" />
|
|
||||||
</div>
|
|
||||||
Back
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="mx-auto flex w-full max-w-md flex-col gap-10 px-3">
|
|
||||||
<h1 className="text-center text-2xl font-semibold text-neutral-900 dark:text-neutral-100">
|
|
||||||
Choose your favorite topic
|
|
||||||
</h1>
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<div className="flex w-full flex-col gap-3">
|
|
||||||
{TOPICS.map((item) => (
|
|
||||||
<button
|
|
||||||
key={item.title}
|
|
||||||
type="button"
|
|
||||||
onClick={() => setTopic(item)}
|
|
||||||
className="inline-flex h-14 items-center justify-between rounded-xl bg-neutral-100 px-4 hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
|
|
||||||
>
|
|
||||||
<p className="font-medium">{item.title}</p>
|
|
||||||
{topic && topic.title === item.title && (
|
|
||||||
<div>
|
|
||||||
<CheckCircleIcon className="h-5 w-5 text-teal-500" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={submit}
|
|
||||||
disabled={loading || !topic}
|
|
||||||
className="inline-flex h-9 w-full items-center justify-center gap-2 rounded-lg bg-blue-500 font-medium text-white hover:bg-blue-600 focus:outline-none disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<>
|
|
||||||
<LoaderIcon className="h-4 w-4 animate-spin" />
|
|
||||||
<span>Adding...</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<span>Add & Continue</span>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { Outlet } from 'react-router-dom';
|
|
||||||
|
|
||||||
export function OnboardingScreen() {
|
|
||||||
return <Outlet />;
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { AllowNotification } from '@app/auth/components/features/allowNotification';
|
|
||||||
import { Circle } from '@app/auth/components/features/enableCircle';
|
|
||||||
import { OutboxModel } from '@app/auth/components/features/enableOutbox';
|
|
||||||
import { FavoriteHashtag } from '@app/auth/components/features/favoriteHashtag';
|
|
||||||
import { FollowList } from '@app/auth/components/features/followList';
|
|
||||||
import { SuggestFollow } from '@app/auth/components/features/suggestFollow';
|
|
||||||
|
|
||||||
import { LoaderIcon } from '@shared/icons';
|
|
||||||
|
|
||||||
export function OnboardingListScreen() {
|
|
||||||
const { state } = useLocation();
|
|
||||||
const { newuser }: { newuser: boolean } = state;
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const completed = () => {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
const timeout = setTimeout(() => setLoading(false), 200);
|
|
||||||
clearTimeout(timeout);
|
|
||||||
|
|
||||||
navigate('/');
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative flex h-full w-full items-center justify-center">
|
|
||||||
<div className="mx-auto flex w-full max-w-md flex-col gap-10">
|
|
||||||
<div className="text-center">
|
|
||||||
<h1 className="text-2xl font-light text-neutral-900 dark:text-neutral-100">
|
|
||||||
You're almost ready to use Lume.
|
|
||||||
</h1>
|
|
||||||
<h2 className="text-xl font-semibold text-neutral-900 dark:text-neutral-100">
|
|
||||||
Let's start personalizing your experience.
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
{newuser ? <SuggestFollow /> : <FollowList />}
|
|
||||||
<FavoriteHashtag />
|
|
||||||
<Circle />
|
|
||||||
<OutboxModel />
|
|
||||||
<AllowNotification />
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={completed}
|
|
||||||
className="inline-flex h-9 w-full items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600"
|
|
||||||
>
|
|
||||||
{loading ? <LoaderIcon className="h-4 w-4 animate-spin" /> : ' Continue'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
|
|
||||||
import { useNDK } from '@libs/ndk/provider';
|
|
||||||
import { useStorage } from '@libs/storage/provider';
|
|
||||||
|
|
||||||
import { ArrowLeftIcon, CheckCircleIcon, LoaderIcon } from '@shared/icons';
|
|
||||||
import { User } from '@shared/user';
|
|
||||||
|
|
||||||
import { useOnboarding } from '@stores/onboarding';
|
|
||||||
|
|
||||||
import { useNostr } from '@utils/hooks/useNostr';
|
|
||||||
|
|
||||||
export function OnboardRelaysScreen() {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const toggleRelays = useOnboarding((state) => state.toggleRelays);
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [relays, setRelays] = useState(new Set<string>());
|
|
||||||
|
|
||||||
const { db } = useStorage();
|
|
||||||
const { ndk } = useNDK();
|
|
||||||
const { getAllRelaysByUsers } = useNostr();
|
|
||||||
const { status, data } = useQuery({
|
|
||||||
queryKey: ['relays'],
|
|
||||||
queryFn: async () => {
|
|
||||||
return await getAllRelaysByUsers();
|
|
||||||
},
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const toggleRelay = (relay: string) => {
|
|
||||||
if (relays.has(relay)) {
|
|
||||||
setRelays((prev) => {
|
|
||||||
prev.delete(relay);
|
|
||||||
return new Set(prev);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setRelays((prev) => new Set(prev.add(relay)));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const submit = async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
for (const relay of relays) {
|
|
||||||
await db.createRelay(relay);
|
|
||||||
}
|
|
||||||
|
|
||||||
const tags = Array.from(relays).map((relay) => ['r', relay.replace(/\/+$/, '')]);
|
|
||||||
const event = new NDKEvent(ndk);
|
|
||||||
event.content = '';
|
|
||||||
event.kind = 10002;
|
|
||||||
event.created_at = Math.floor(Date.now() / 1000);
|
|
||||||
event.tags = tags;
|
|
||||||
|
|
||||||
await event.publish();
|
|
||||||
|
|
||||||
toggleRelays();
|
|
||||||
navigate(-1);
|
|
||||||
} catch (e) {
|
|
||||||
setLoading(false);
|
|
||||||
toast.error(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative flex h-full w-full flex-col justify-center">
|
|
||||||
<div className="absolute left-[8px] top-4">
|
|
||||||
<button
|
|
||||||
onClick={() => navigate(-1)}
|
|
||||||
className="inline-flex items-center gap-2 text-sm font-medium"
|
|
||||||
>
|
|
||||||
<div className="inline-flex h-8 w-8 items-center justify-center rounded-lg bg-neutral-200 text-neutral-800 dark:bg-neutral-800 dark:text-neutral-200">
|
|
||||||
<ArrowLeftIcon className="h-5 w-5" />
|
|
||||||
</div>
|
|
||||||
Back
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="mx-auto flex w-full max-w-md flex-col gap-10 px-3">
|
|
||||||
<h1 className="text-center text-2xl font-semibold text-neutral-900 dark:text-neutral-100">
|
|
||||||
Relay discovery
|
|
||||||
</h1>
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<div className="flex h-[420px] w-full flex-col overflow-y-auto rounded-xl bg-neutral-100 dark:bg-neutral-900">
|
|
||||||
{status === 'pending' ? (
|
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
|
||||||
<LoaderIcon className="h-4 w-4 animate-spin text-neutral-900 dark:text-neutral-100" />
|
|
||||||
</div>
|
|
||||||
) : data.size === 0 ? (
|
|
||||||
<div className="flex h-full w-full items-center justify-center px-6">
|
|
||||||
<p className="text-center text-neutral-300 dark:text-neutral-600">
|
|
||||||
Lume couldn't find any relays from your follows.
|
|
||||||
<br />
|
|
||||||
You can skip this step and use default relays instead.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
[...data].map(([key, value]) => (
|
|
||||||
<button
|
|
||||||
key={key}
|
|
||||||
type="button"
|
|
||||||
onClick={() => toggleRelay(key)}
|
|
||||||
className="inline-flex transform items-start justify-between px-4 py-2 hover:bg-neutral-300 dark:hover:bg-neutral-700"
|
|
||||||
>
|
|
||||||
<div className="flex w-full items-center justify-between">
|
|
||||||
<div className="inline-flex items-center gap-2">
|
|
||||||
<div className="pt-1.5">
|
|
||||||
{relays.has(key) ? (
|
|
||||||
<CheckCircleIcon className="h-4 w-4 text-teal-500" />
|
|
||||||
) : (
|
|
||||||
<CheckCircleIcon className="h-4 w-4 text-neutral-300 dark:text-neutral-700" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<p className="max-w-[15rem] truncate">{key.replace(/\/+$/, '')}</p>
|
|
||||||
</div>
|
|
||||||
<div className="inline-flex items-center gap-2">
|
|
||||||
<span className="text-sm font-medium text-neutral-500 dark:text-neutral-400">
|
|
||||||
Used by
|
|
||||||
</span>
|
|
||||||
<div className="isolate flex -space-x-2">
|
|
||||||
{value.slice(0, 3).map((item) => (
|
|
||||||
<User key={item} pubkey={item} variant="stacked" />
|
|
||||||
))}
|
|
||||||
{value.length > 3 ? (
|
|
||||||
<div className="inline-flex h-8 w-8 items-center justify-center rounded-full bg-neutral-200 text-neutral-900 ring-1 ring-neutral-300 dark:bg-neutral-800 dark:text-neutral-100 dark:ring-neutral-700">
|
|
||||||
<span className="text-xs font-medium">+{value.length}</span>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => submit()}
|
|
||||||
disabled={loading}
|
|
||||||
className="inline-flex h-9 w-full items-center justify-center gap-2 rounded-lg bg-blue-500 font-medium text-white hover:bg-blue-600 focus:outline-none disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<>
|
|
||||||
<LoaderIcon className="h-4 w-4 animate-spin" />
|
|
||||||
<span>Adding...</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<span>Add {relays.size} relays & Continue</span>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
41
src/app/auth/tutorials/finish.tsx
Normal file
41
src/app/auth/tutorials/finish.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
export function TutorialFinishScreen() {
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<div className="mx-auto flex w-full max-w-md flex-col gap-10">
|
||||||
|
<div className="text-center">
|
||||||
|
<img src="/icon.png" alt="Lume's logo" className="mx-auto mb-1 h-auto w-16" />
|
||||||
|
<h1 className="text-2xl font-light">
|
||||||
|
Yo, you've understood basic features 🎉
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Link
|
||||||
|
to="/"
|
||||||
|
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-blue-500 font-medium text-white hover:bg-blue-600"
|
||||||
|
>
|
||||||
|
Start using
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="https://nostr.how/"
|
||||||
|
target="_blank"
|
||||||
|
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-neutral-100 font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
|
||||||
|
>
|
||||||
|
Learn more about Nostr
|
||||||
|
</Link>
|
||||||
|
<p className="text-center text-sm font-medium text-neutral-500 dark:text-neutral-600">
|
||||||
|
If you've trouble when user Lume, you can report the issue{' '}
|
||||||
|
<a
|
||||||
|
href="github.com/luminous-devs/lume"
|
||||||
|
target="_blank"
|
||||||
|
className="text-blue-500 !underline"
|
||||||
|
>
|
||||||
|
here
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
91
src/app/auth/tutorials/note.tsx
Normal file
91
src/app/auth/tutorials/note.tsx
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { useNDK } from '@libs/ndk/provider';
|
||||||
|
|
||||||
|
import { EditIcon, ReactionIcon, ReplyIcon, RepostIcon, ZapIcon } from '@shared/icons';
|
||||||
|
import { TextNote } from '@shared/notes';
|
||||||
|
|
||||||
|
export function TutorialNoteScreen() {
|
||||||
|
const { ndk } = useNDK();
|
||||||
|
const exampleEvent = new NDKEvent(ndk, {
|
||||||
|
id: 'a3527670dd9b178bf7c2a9ea673b63bc8bfe774942b196691145343623c45821',
|
||||||
|
pubkey: '04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9',
|
||||||
|
created_at: 1701355223,
|
||||||
|
kind: 1,
|
||||||
|
tags: [],
|
||||||
|
content: 'good morning nostr, stay humble and stack sats 🫡',
|
||||||
|
sig: '9e0bd67ec25598744f20bff0fe360fdf190c4240edb9eea260e50f77e07f94ea767ececcc6270819b7f64e5e7ca1fe20b4971f46dc120e6db43114557f3a6dae',
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full select-text items-center justify-center">
|
||||||
|
<div className="mx-auto flex w-full max-w-md flex-col gap-10">
|
||||||
|
<div className="flex flex-col items-center gap-3">
|
||||||
|
<div className="inline-flex h-11 w-11 items-center justify-center rounded-lg bg-neutral-100 dark:bg-neutral-900">
|
||||||
|
<EditIcon className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<h1 className="text-2xl font-light">
|
||||||
|
What is a <span className="font-bold">Note?</span>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<p className="px-3">
|
||||||
|
Posts on Nostr based Social Network client are usually called
|
||||||
|
'Notes.' Notes are arranged chronologically on the timeline and are
|
||||||
|
updated in real-time.
|
||||||
|
</p>
|
||||||
|
<p className="px-3 font-semibold">Here is one example:</p>
|
||||||
|
<TextNote event={exampleEvent} className="pointer-events-none my-2" />
|
||||||
|
<p className="px-3 font-semibold">Here are how you can interact with a note:</p>
|
||||||
|
<div className="flex flex-col gap-2 px-3">
|
||||||
|
<div className="inline-flex gap-3">
|
||||||
|
<div className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-neutral-100 dark:bg-neutral-900">
|
||||||
|
<ReplyIcon className="h-5 w-5 text-blue-500" />
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
Reply - Click on this button to reply to a note. It's also possible
|
||||||
|
to reply to replies, continuing the conversation like a thread.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="inline-flex gap-3">
|
||||||
|
<div className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-neutral-100 dark:bg-neutral-900">
|
||||||
|
<ReactionIcon className="h-5 w-5 text-red-500" />
|
||||||
|
</div>
|
||||||
|
<p>Reaction - You can add reactions to the Note to express your concern.</p>
|
||||||
|
</div>
|
||||||
|
<div className="inline-flex gap-3">
|
||||||
|
<div className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-neutral-100 dark:bg-neutral-900">
|
||||||
|
<RepostIcon className="h-5 w-5 text-teal-500" />
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
Repost - You can share that note to your own timeline. You can also quote
|
||||||
|
them with your comments.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="inline-flex gap-3">
|
||||||
|
<div className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-neutral-100 dark:bg-neutral-900">
|
||||||
|
<ZapIcon className="h-5 w-5 text-orange-500" />
|
||||||
|
</div>
|
||||||
|
<p>Zap - You can send tip in Bitcoin to that note owner with zero-fees</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-5 flex gap-2 px-3">
|
||||||
|
<Link
|
||||||
|
to="/auth/finish"
|
||||||
|
className="inline-flex h-9 w-full items-center justify-center rounded-lg bg-neutral-100 font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/auth/tutorials/widget"
|
||||||
|
className="inline-flex h-9 w-full items-center justify-center rounded-lg bg-blue-500 font-medium text-white hover:bg-blue-600"
|
||||||
|
>
|
||||||
|
Continue
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
3
src/app/auth/tutorials/posting.tsx
Normal file
3
src/app/auth/tutorials/posting.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export function TutorialPostingScreen() {
|
||||||
|
return <div></div>;
|
||||||
|
}
|
||||||
64
src/app/auth/tutorials/widget.tsx
Normal file
64
src/app/auth/tutorials/widget.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { BellIcon, HomeIcon, PlusIcon } from '@shared/icons';
|
||||||
|
|
||||||
|
export function TutorialWidgetScreen() {
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full select-text items-center justify-center">
|
||||||
|
<div className="mx-auto flex w-full max-w-md flex-col gap-10">
|
||||||
|
<div className="flex flex-col items-center gap-3">
|
||||||
|
<div className="inline-flex h-11 w-11 items-center justify-center rounded-lg bg-neutral-100 dark:bg-neutral-900">
|
||||||
|
<HomeIcon className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<h1 className="text-2xl font-light">
|
||||||
|
The concept of <span className="font-bold">Widgets</span>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2 px-3">
|
||||||
|
<p>
|
||||||
|
Lume provides multiple widgets based on usage. You always can control what you
|
||||||
|
need to show on your Home.
|
||||||
|
</p>
|
||||||
|
<p className="font-semibold">Default widgets:</p>
|
||||||
|
<div className="inline-flex gap-3">
|
||||||
|
<div className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-neutral-100 dark:bg-neutral-900">
|
||||||
|
<HomeIcon className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<p>Newsfeed - You can view notes from accounts you follow.</p>
|
||||||
|
</div>
|
||||||
|
<div className="inline-flex gap-3">
|
||||||
|
<div className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-neutral-100 dark:bg-neutral-900">
|
||||||
|
<BellIcon className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<p>Notification - You can view all notifications related to your account.</p>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
If you want to add more widget, you can click to this button on Home Screen.
|
||||||
|
</p>
|
||||||
|
<div className="flex h-24 w-full items-center justify-center rounded-lg bg-neutral-100 dark:bg-neutral-900">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex h-14 w-14 items-center justify-center rounded-full bg-neutral-200 text-neutral-900 hover:bg-neutral-300 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-neutral-700"
|
||||||
|
>
|
||||||
|
<PlusIcon className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="mt-5 flex gap-2">
|
||||||
|
<Link
|
||||||
|
to="/auth/tutorials/note"
|
||||||
|
className="inline-flex h-9 w-full items-center justify-center rounded-lg bg-neutral-100 font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/auth/tutorials/finish"
|
||||||
|
className="inline-flex h-9 w-full items-center justify-center rounded-lg bg-blue-500 font-medium text-white hover:bg-blue-600"
|
||||||
|
>
|
||||||
|
Continue
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,19 +7,19 @@ export function WelcomeScreen() {
|
|||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<img src="/icon.png" alt="Lume's logo" className="mx-auto mb-1 h-auto w-16" />
|
<img src="/icon.png" alt="Lume's logo" className="mx-auto mb-1 h-auto w-16" />
|
||||||
<h1 className="text-2xl">
|
<h1 className="text-2xl">
|
||||||
Welcome to <span className="font-semibold">Lume</span>
|
Welcome to <span className="font-bold">Lume</span>
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2 px-8">
|
<div className="flex flex-col gap-2 px-8">
|
||||||
<Link
|
<Link
|
||||||
to="/auth/create"
|
to="/auth/create"
|
||||||
className="inline-flex h-10 w-full items-center justify-center rounded-lg bg-blue-500 font-medium text-white hover:bg-blue-600"
|
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-blue-500 font-medium text-white hover:bg-blue-600"
|
||||||
>
|
>
|
||||||
Create new account
|
Create new account
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
to="/auth/import"
|
to="/auth/import"
|
||||||
className="inline-flex h-10 w-full items-center justify-center rounded-lg font-medium text-neutral-900 hover:bg-neutral-100 dark:text-neutral-100 dark:hover:bg-neutral-900"
|
className="inline-flex h-11 w-full items-center justify-center rounded-lg font-medium text-neutral-900 hover:bg-neutral-100 dark:text-neutral-100 dark:hover:bg-neutral-900"
|
||||||
>
|
>
|
||||||
Log in
|
Log in
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export function ChatForm({ receiverPubkey }: { receiverPubkey: string }) {
|
|||||||
autoCorrect="off"
|
autoCorrect="off"
|
||||||
autoCapitalize="off"
|
autoCapitalize="off"
|
||||||
placeholder="Message..."
|
placeholder="Message..."
|
||||||
className="h-10 flex-1 resize-none bg-transparent px-3 text-neutral-900 placeholder:text-neutral-600 focus:outline-none dark:text-neutral-100 dark:placeholder:text-neutral-300"
|
className="h-10 flex-1 resize-none border-none bg-transparent px-3 text-neutral-900 placeholder:text-neutral-600 focus:border-none focus:shadow-none focus:outline-none focus:ring-0 dark:text-neutral-100 dark:placeholder:text-neutral-300"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -12,14 +12,14 @@ import { useProfile } from '@utils/hooks/useProfile';
|
|||||||
import { displayNpub } from '@utils/shortenKey';
|
import { displayNpub } from '@utils/shortenKey';
|
||||||
|
|
||||||
export const ChatListItem = memo(function ChatListItem({ event }: { event: NDKEvent }) {
|
export const ChatListItem = memo(function ChatListItem({ event }: { event: NDKEvent }) {
|
||||||
const { status, user } = useProfile(event.pubkey);
|
const { isLoading, user } = useProfile(event.pubkey);
|
||||||
const decryptedContent = useDecryptMessage(event);
|
const decryptedContent = useDecryptMessage(event);
|
||||||
|
|
||||||
const createdAt = formatCreatedAt(event.created_at, true);
|
const createdAt = formatCreatedAt(event.created_at, true);
|
||||||
const svgURI =
|
const svgURI =
|
||||||
'data:image/svg+xml;utf8,' + encodeURIComponent(minidenticon(event.pubkey, 90, 50));
|
'data:image/svg+xml;utf8,' + encodeURIComponent(minidenticon(event.pubkey, 90, 50));
|
||||||
|
|
||||||
if (status === 'pending') {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2.5 rounded-md px-3">
|
<div className="flex items-center gap-2.5 rounded-md px-3">
|
||||||
<div className="h-10 w-10 shrink-0 animate-pulse rounded-lg bg-neutral-400 dark:bg-neutral-600" />
|
<div className="h-10 w-10 shrink-0 animate-pulse rounded-lg bg-neutral-400 dark:bg-neutral-600" />
|
||||||
|
|||||||
@@ -116,17 +116,32 @@ export function ErrorScreen() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="select-text text-lg font-medium text-blue-300">
|
<div className="select-text text-lg font-medium text-blue-300">
|
||||||
<p>
|
<p>
|
||||||
While waiting for Lume's Devs to release the bug fixes, you always can use
|
While waiting for Lume's Devs to release the bug fixes, you always
|
||||||
other Nostr clients with your account:
|
can use other Nostr clients with your account:
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-2 flex flex-col gap-1 text-white">
|
<div className="mt-2 flex flex-col gap-1 text-white">
|
||||||
<a href="https://snort.social" className="hover:!underline">
|
<a
|
||||||
|
className="hover:!underline"
|
||||||
|
href="https://snort.social"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
snort.social
|
snort.social
|
||||||
</a>
|
</a>
|
||||||
<a href="https://primal.net" className="hover:!underline">
|
<a
|
||||||
|
className="hover:!underline"
|
||||||
|
href="https://primal.net"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
primal.net
|
primal.net
|
||||||
</a>
|
</a>
|
||||||
<a href="https://nostrudel.ninja" className="hover:!underline">
|
<a
|
||||||
|
className="hover:!underline"
|
||||||
|
href="https://nostrudel.ninja"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
nostrudel.ninja
|
nostrudel.ninja
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { memo } from 'react';
|
|||||||
import { useProfile } from '@utils/hooks/useProfile';
|
import { useProfile } from '@utils/hooks/useProfile';
|
||||||
|
|
||||||
export const GroupTitle = memo(function GroupTitle({ pubkey }: { pubkey: string }) {
|
export const GroupTitle = memo(function GroupTitle({ pubkey }: { pubkey: string }) {
|
||||||
const { status, user } = useProfile(pubkey);
|
const { isLoading, user } = useProfile(pubkey);
|
||||||
|
|
||||||
if (status === 'pending') {
|
if (isLoading) {
|
||||||
return <div className="h-3 w-24 animate-pulse rounded bg-white/10" />;
|
return <div className="h-3 w-24 animate-pulse rounded bg-white/10" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export const UserWithDrawer = memo(function UserWithDrawer({
|
|||||||
}) {
|
}) {
|
||||||
const { db } = useStorage();
|
const { db } = useStorage();
|
||||||
const { ndk } = useNDK();
|
const { ndk } = useNDK();
|
||||||
const { status, user } = useProfile(pubkey);
|
const { isLoading, user } = useProfile(pubkey);
|
||||||
|
|
||||||
const [followed, setFollowed] = useState(false);
|
const [followed, setFollowed] = useState(false);
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ export const UserWithDrawer = memo(function UserWithDrawer({
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (db.account.follows.includes(pubkey)) {
|
if (db.account.contacts.includes(pubkey)) {
|
||||||
setFollowed(true);
|
setFollowed(true);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
@@ -82,7 +82,7 @@ export const UserWithDrawer = memo(function UserWithDrawer({
|
|||||||
<Dialog.Portal>
|
<Dialog.Portal>
|
||||||
<Dialog.Content className="fixed right-0 top-0 z-50 flex h-full w-[400px] animate-slideRightAndFade items-center justify-center px-4 pb-4 pt-16 transition-all">
|
<Dialog.Content className="fixed right-0 top-0 z-50 flex h-full w-[400px] animate-slideRightAndFade items-center justify-center px-4 pb-4 pt-16 transition-all">
|
||||||
<div className="h-full w-full overflow-y-auto rounded-lg border border-neutral-300 bg-neutral-200 py-3 dark:border-neutral-700 dark:bg-neutral-800">
|
<div className="h-full w-full overflow-y-auto rounded-lg border border-neutral-300 bg-neutral-200 py-3 dark:border-neutral-700 dark:bg-neutral-800">
|
||||||
{status === 'pending' ? (
|
{isLoading ? (
|
||||||
<div>
|
<div>
|
||||||
<p>Loading...</p>
|
<p>Loading...</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export function ExploreScreen() {
|
|||||||
const { getContactsByPubkey } = useNostr();
|
const { getContactsByPubkey } = useNostr();
|
||||||
const { project } = useReactFlow();
|
const { project } = useReactFlow();
|
||||||
|
|
||||||
const defaultContacts = useMemo(() => getMultipleRandom(db.account.follows, 10), []);
|
const defaultContacts = useMemo(() => getMultipleRandom(db.account.contacts, 10), []);
|
||||||
const reactFlowWrapper = useRef(null);
|
const reactFlowWrapper = useRef(null);
|
||||||
const connectingNodeId = useRef(null);
|
const connectingNodeId = useRef(null);
|
||||||
|
|
||||||
|
|||||||
@@ -21,8 +21,7 @@ import {
|
|||||||
WidgetList,
|
WidgetList,
|
||||||
} from '@shared/widgets';
|
} from '@shared/widgets';
|
||||||
|
|
||||||
import { WIDGET_KIND } from '@stores/constants';
|
import { WIDGET_KIND } from '@utils/constants';
|
||||||
|
|
||||||
import { Widget } from '@utils/types';
|
import { Widget } from '@utils/types';
|
||||||
|
|
||||||
export function HomeScreen() {
|
export function HomeScreen() {
|
||||||
@@ -35,18 +34,18 @@ export function HomeScreen() {
|
|||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const dbWidgets = await db.getWidgets();
|
const dbWidgets = await db.getWidgets();
|
||||||
const defaultWidgets = [
|
const defaultWidgets = [
|
||||||
{
|
|
||||||
id: '9998',
|
|
||||||
title: 'Notification',
|
|
||||||
content: '',
|
|
||||||
kind: WIDGET_KIND.notification,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: '9999',
|
id: '9999',
|
||||||
title: 'Newsfeed',
|
title: 'Newsfeed',
|
||||||
content: '',
|
content: '',
|
||||||
kind: WIDGET_KIND.newsfeed,
|
kind: WIDGET_KIND.newsfeed,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: '9998',
|
||||||
|
title: 'Notification',
|
||||||
|
content: '',
|
||||||
|
kind: WIDGET_KIND.notification,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return [...defaultWidgets, ...dbWidgets];
|
return [...defaultWidgets, ...dbWidgets];
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import Image from '@tiptap/extension-image';
|
|||||||
import Placeholder from '@tiptap/extension-placeholder';
|
import Placeholder from '@tiptap/extension-placeholder';
|
||||||
import { EditorContent, FloatingMenu, useEditor } from '@tiptap/react';
|
import { EditorContent, FloatingMenu, useEditor } from '@tiptap/react';
|
||||||
import StarterKit from '@tiptap/starter-kit';
|
import StarterKit from '@tiptap/starter-kit';
|
||||||
import { useMemo, useState } from 'react';
|
import { useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
@@ -27,12 +27,14 @@ import {
|
|||||||
export function NewArticleScreen() {
|
export function NewArticleScreen() {
|
||||||
const { ndk } = useNDK();
|
const { ndk } = useNDK();
|
||||||
|
|
||||||
|
const [height, setHeight] = useState(0);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [title, setTitle] = useState('');
|
const [title, setTitle] = useState('');
|
||||||
const [summary, setSummary] = useState({ open: false, content: '' });
|
const [summary, setSummary] = useState({ open: false, content: '' });
|
||||||
const [cover, setCover] = useState('');
|
const [cover, setCover] = useState('');
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const containerRef = useRef(null);
|
||||||
const ident = useMemo(() => String(Date.now()), []);
|
const ident = useMemo(() => String(Date.now()), []);
|
||||||
const editor = useEditor({
|
const editor = useEditor({
|
||||||
extensions: [
|
extensions: [
|
||||||
@@ -113,123 +115,133 @@ export function NewArticleScreen() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
setHeight(containerRef.current.clientHeight);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col justify-between">
|
<div className="flex flex-1 flex-col justify-between">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex-1 overflow-y-auto">
|
||||||
{cover ? (
|
<div
|
||||||
<img
|
className="flex flex-col gap-4"
|
||||||
src={cover}
|
ref={containerRef}
|
||||||
alt="post cover"
|
style={{ height: `${height}px` }}
|
||||||
className="h-72 w-full rounded-lg object-cover"
|
>
|
||||||
/>
|
{cover ? (
|
||||||
) : null}
|
<img
|
||||||
<div className="group flex justify-between gap-2">
|
src={cover}
|
||||||
<input
|
alt="post cover"
|
||||||
name="title"
|
className="h-72 w-full rounded-lg object-cover"
|
||||||
className="h-9 flex-1 border-none bg-transparent text-2xl font-semibold text-neutral-900 shadow-none outline-none placeholder:text-neutral-400 dark:text-neutral-100 dark:placeholder:text-neutral-600"
|
/>
|
||||||
placeholder="Untitled"
|
) : null}
|
||||||
value={title}
|
<div className="group flex justify-between gap-2">
|
||||||
onChange={(e) => setTitle(e.target.value)}
|
<input
|
||||||
/>
|
name="title"
|
||||||
<div
|
className="h-9 flex-1 border-none bg-transparent px-0 text-2xl font-semibold text-neutral-900 shadow-none outline-none placeholder:text-neutral-400 focus:border-none focus:outline-none focus:ring-0 dark:text-neutral-100 dark:placeholder:text-neutral-600"
|
||||||
className={twMerge(
|
placeholder="Untitled"
|
||||||
'inline-flex shrink-0 gap-2 group-hover:inline-flex',
|
value={title}
|
||||||
title.length > 0 ? '' : 'hidden'
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
)}
|
/>
|
||||||
>
|
<div
|
||||||
<ArticleCoverUploader setCover={setCover} />
|
className={twMerge(
|
||||||
<button
|
'inline-flex shrink-0 gap-2 group-hover:inline-flex',
|
||||||
type="button"
|
title.length > 0 ? '' : 'hidden'
|
||||||
onClick={() => setSummary((prev) => ({ ...prev, open: !prev.open }))}
|
)}
|
||||||
className="inline-flex h-9 w-max items-center gap-2 rounded-lg bg-neutral-100 px-2.5 text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-800 dark:hover:bg-neutral-800"
|
|
||||||
>
|
>
|
||||||
<ThreadsIcon className="h-4 w-4" />
|
<ArticleCoverUploader setCover={setCover} />
|
||||||
Add summary
|
<button
|
||||||
</button>
|
type="button"
|
||||||
</div>
|
onClick={() => setSummary((prev) => ({ ...prev, open: !prev.open }))}
|
||||||
</div>
|
className="inline-flex h-9 w-max items-center gap-2 rounded-lg bg-neutral-100 px-2.5 text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-800 dark:hover:bg-neutral-800"
|
||||||
{summary.open ? (
|
>
|
||||||
<div className="flex gap-3">
|
<ThreadsIcon className="h-4 w-4" />
|
||||||
<div className="h-16 w-1 shrink-0 rounded-full bg-neutral-200 dark:bg-neutral-800" />
|
Add summary
|
||||||
<div className="flex-1">
|
</button>
|
||||||
<textarea
|
|
||||||
className="h-16 w-full border-none bg-transparent px-1 py-1 text-neutral-900 shadow-none outline-none placeholder:text-neutral-400 dark:text-neutral-100 dark:placeholder:text-neutral-600"
|
|
||||||
placeholder="A brief summary of your article"
|
|
||||||
value={summary.content}
|
|
||||||
onChange={(e) =>
|
|
||||||
setSummary((prev) => ({ ...prev, content: e.target.value }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
{summary.open ? (
|
||||||
<div>
|
<div className="flex gap-3">
|
||||||
{editor && (
|
<div className="h-16 w-1 shrink-0 rounded-full bg-neutral-200 dark:bg-neutral-800" />
|
||||||
<FloatingMenu
|
<div className="flex-1">
|
||||||
|
<textarea
|
||||||
|
className="h-16 w-full border-none bg-transparent px-1 py-1 text-neutral-900 shadow-none outline-none placeholder:text-neutral-400 dark:text-neutral-100 dark:placeholder:text-neutral-600"
|
||||||
|
placeholder="A brief summary of your article"
|
||||||
|
value={summary.content}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSummary((prev) => ({ ...prev, content: e.target.value }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div>
|
||||||
|
{editor && (
|
||||||
|
<FloatingMenu
|
||||||
|
editor={editor}
|
||||||
|
tippyOptions={{ duration: 100 }}
|
||||||
|
className="ml-36 inline-flex h-10 items-center gap-1 rounded-lg border border-neutral-200 bg-neutral-100 px-px dark:border-neutral-800 dark:bg-neutral-900"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
|
||||||
|
className={twMerge(
|
||||||
|
'inline-flex h-9 w-9 items-center justify-center rounded-md text-neutral-900 hover:bg-neutral-50 dark:text-neutral-100 dark:hover:bg-neutral-950',
|
||||||
|
editor.isActive('heading', { level: 1 })
|
||||||
|
? 'bg-white shadow dark:bg-black'
|
||||||
|
: ''
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Heading1Icon className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
|
||||||
|
className={twMerge(
|
||||||
|
'inline-flex h-9 w-9 items-center justify-center rounded-md text-neutral-900 hover:bg-neutral-50 dark:text-neutral-100 dark:hover:bg-neutral-950',
|
||||||
|
editor.isActive('heading', { level: 2 })
|
||||||
|
? 'bg-white shadow dark:bg-black'
|
||||||
|
: ''
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Heading2Icon className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
|
||||||
|
className={twMerge(
|
||||||
|
'inline-flex h-9 w-9 items-center justify-center rounded-md text-neutral-900 hover:bg-neutral-50 dark:text-neutral-100 dark:hover:bg-neutral-950',
|
||||||
|
editor.isActive('heading', { level: 3 })
|
||||||
|
? 'bg-white shadow dark:bg-black'
|
||||||
|
: ''
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Heading3Icon className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => editor.chain().focus().toggleBold().run()}
|
||||||
|
className={twMerge(
|
||||||
|
'inline-flex h-9 w-9 items-center justify-center rounded-md text-neutral-900 hover:bg-neutral-50 dark:text-neutral-100 dark:hover:bg-neutral-950',
|
||||||
|
editor.isActive('bold') ? 'bg-white shadow dark:bg-black' : ''
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<BoldIcon className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||||
|
className={twMerge(
|
||||||
|
'inline-flex h-9 w-9 items-center justify-center rounded-md text-neutral-900 hover:bg-neutral-50 dark:text-neutral-100 dark:hover:bg-neutral-950',
|
||||||
|
editor.isActive('italic') ? 'bg-white shadow dark:bg-black' : ''
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ItalicIcon className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</FloatingMenu>
|
||||||
|
)}
|
||||||
|
<EditorContent
|
||||||
editor={editor}
|
editor={editor}
|
||||||
tippyOptions={{ duration: 100 }}
|
spellCheck="false"
|
||||||
className="ml-36 inline-flex h-10 items-center gap-1 rounded-lg border border-neutral-200 bg-neutral-100 px-px dark:border-neutral-800 dark:bg-neutral-900"
|
autoComplete="off"
|
||||||
>
|
autoCorrect="off"
|
||||||
<button
|
autoCapitalize="off"
|
||||||
onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
|
/>
|
||||||
className={twMerge(
|
</div>
|
||||||
'inline-flex h-9 w-9 items-center justify-center rounded-md text-neutral-900 hover:bg-neutral-50 dark:text-neutral-100 dark:hover:bg-neutral-950',
|
|
||||||
editor.isActive('heading', { level: 1 })
|
|
||||||
? 'bg-white shadow dark:bg-black'
|
|
||||||
: ''
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Heading1Icon className="h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
|
|
||||||
className={twMerge(
|
|
||||||
'inline-flex h-9 w-9 items-center justify-center rounded-md text-neutral-900 hover:bg-neutral-50 dark:text-neutral-100 dark:hover:bg-neutral-950',
|
|
||||||
editor.isActive('heading', { level: 2 })
|
|
||||||
? 'bg-white shadow dark:bg-black'
|
|
||||||
: ''
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Heading2Icon className="h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
|
|
||||||
className={twMerge(
|
|
||||||
'inline-flex h-9 w-9 items-center justify-center rounded-md text-neutral-900 hover:bg-neutral-50 dark:text-neutral-100 dark:hover:bg-neutral-950',
|
|
||||||
editor.isActive('heading', { level: 3 })
|
|
||||||
? 'bg-white shadow dark:bg-black'
|
|
||||||
: ''
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Heading3Icon className="h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => editor.chain().focus().toggleBold().run()}
|
|
||||||
className={twMerge(
|
|
||||||
'inline-flex h-9 w-9 items-center justify-center rounded-md text-neutral-900 hover:bg-neutral-50 dark:text-neutral-100 dark:hover:bg-neutral-950',
|
|
||||||
editor.isActive('bold') ? 'bg-white shadow dark:bg-black' : ''
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<BoldIcon className="h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => editor.chain().focus().toggleItalic().run()}
|
|
||||||
className={twMerge(
|
|
||||||
'inline-flex h-9 w-9 items-center justify-center rounded-md text-neutral-900 hover:bg-neutral-50 dark:text-neutral-100 dark:hover:bg-neutral-950',
|
|
||||||
editor.isActive('italic') ? 'bg-white shadow dark:bg-black' : ''
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<ItalicIcon className="h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
</FloatingMenu>
|
|
||||||
)}
|
|
||||||
<EditorContent
|
|
||||||
editor={editor}
|
|
||||||
spellCheck="false"
|
|
||||||
autoComplete="off"
|
|
||||||
autoCorrect="off"
|
|
||||||
autoCapitalize="off"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ export * from './articleCoverUploader';
|
|||||||
export * from './mediaUploader';
|
export * from './mediaUploader';
|
||||||
export * from './mentionPopup';
|
export * from './mentionPopup';
|
||||||
export * from './mentionPopupItem';
|
export * from './mentionPopupItem';
|
||||||
|
export * from './mentionList';
|
||||||
|
|||||||
104
src/app/new/components/mentionList.tsx
Normal file
104
src/app/new/components/mentionList.tsx
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import * as Avatar from '@radix-ui/react-avatar';
|
||||||
|
import { minidenticon } from 'minidenticons';
|
||||||
|
import { Ref, forwardRef, useEffect, useImperativeHandle, useState } from 'react';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
import { NDKCacheUserProfile } from '@utils/types';
|
||||||
|
|
||||||
|
type MentionListRef = {
|
||||||
|
onKeyDown: (props: { event: Event }) => boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const List = (
|
||||||
|
props: {
|
||||||
|
items: NDKCacheUserProfile[];
|
||||||
|
command: (arg0: { id: string }) => void;
|
||||||
|
},
|
||||||
|
ref: Ref<unknown>
|
||||||
|
) => {
|
||||||
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||||
|
|
||||||
|
const selectItem = (index) => {
|
||||||
|
const item = props.items[index];
|
||||||
|
if (item) {
|
||||||
|
props.command({ id: item.pubkey });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const upHandler = () => {
|
||||||
|
setSelectedIndex((selectedIndex + props.items.length - 1) % props.items.length);
|
||||||
|
};
|
||||||
|
|
||||||
|
const downHandler = () => {
|
||||||
|
setSelectedIndex((selectedIndex + 1) % props.items.length);
|
||||||
|
};
|
||||||
|
|
||||||
|
const enterHandler = () => {
|
||||||
|
selectItem(selectedIndex);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => setSelectedIndex(0), [props.items]);
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
onKeyDown: ({ event }) => {
|
||||||
|
if (event.key === 'ArrowUp') {
|
||||||
|
upHandler();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'ArrowDown') {
|
||||||
|
downHandler();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
enterHandler();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex w-[200px] flex-col overflow-y-auto rounded-lg border border-neutral-200 bg-neutral-50 p-2 shadow-lg shadow-neutral-500/20 dark:border-neutral-800 dark:bg-neutral-950 dark:shadow-neutral-300/50">
|
||||||
|
{props.items.length ? (
|
||||||
|
props.items.map((item, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
onClick={() => selectItem(index)}
|
||||||
|
className={twMerge(
|
||||||
|
'inline-flex h-11 items-center gap-2 rounded-md px-2',
|
||||||
|
index === selectedIndex ? 'bg-neutral-100 dark:bg-neutral-900' : ''
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Avatar.Root className="h-8 w-8 shrink-0">
|
||||||
|
<Avatar.Image
|
||||||
|
src={item.image}
|
||||||
|
alt={item.name}
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
className="h-8 w-8 rounded-md"
|
||||||
|
/>
|
||||||
|
<Avatar.Fallback delayMs={150}>
|
||||||
|
<img
|
||||||
|
src={
|
||||||
|
'data:image/svg+xml;utf8,' +
|
||||||
|
encodeURIComponent(minidenticon(item.name, 90, 50))
|
||||||
|
}
|
||||||
|
alt={item.name}
|
||||||
|
className="h-8 w-8 rounded-md bg-black dark:bg-white"
|
||||||
|
/>
|
||||||
|
</Avatar.Fallback>
|
||||||
|
</Avatar.Root>
|
||||||
|
<h5 className="max-w-[150px] truncate text-sm font-medium">{item.name}</h5>
|
||||||
|
</button>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="text-center text-sm font-medium">No result</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MentionList = forwardRef<MentionListRef>(List);
|
||||||
@@ -32,8 +32,8 @@ export function MentionPopup({ editor }: { editor: Editor }) {
|
|||||||
className="h-full max-h-[200px] w-[250px] overflow-hidden overflow-y-auto rounded-lg border border-neutral-200 bg-neutral-100 focus:outline-none dark:border-neutral-800 dark:bg-neutral-900"
|
className="h-full max-h-[200px] w-[250px] overflow-hidden overflow-y-auto rounded-lg border border-neutral-200 bg-neutral-100 focus:outline-none dark:border-neutral-800 dark:bg-neutral-900"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-1 py-1">
|
<div className="flex flex-col gap-1 py-1">
|
||||||
{db.account.follows.length > 0 ? (
|
{db.account.contacts.length > 0 ? (
|
||||||
db.account.follows.map((item) => (
|
db.account.contacts.map((item) => (
|
||||||
<button key={item} type="button" onClick={() => insertMention(item)}>
|
<button key={item} type="button" onClick={() => insertMention(item)}>
|
||||||
<MentionPopupItem pubkey={item} />
|
<MentionPopupItem pubkey={item} />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
|
import * as Avatar from '@radix-ui/react-avatar';
|
||||||
|
import { minidenticon } from 'minidenticons';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { useProfile } from '@utils/hooks/useProfile';
|
import { useProfile } from '@utils/hooks/useProfile';
|
||||||
import { displayNpub } from '@utils/shortenKey';
|
import { displayNpub } from '@utils/shortenKey';
|
||||||
|
|
||||||
export function MentionPopupItem({ pubkey, embed }: { pubkey: string; embed?: string }) {
|
export function MentionPopupItem({ pubkey, embed }: { pubkey: string; embed?: string }) {
|
||||||
const { status, user } = useProfile(pubkey, embed);
|
const { isLoading, user } = useProfile(pubkey, embed);
|
||||||
|
const svgURI = useMemo(
|
||||||
|
() => 'data:image/svg+xml;utf8,' + encodeURIComponent(minidenticon(pubkey, 90, 50)),
|
||||||
|
[pubkey]
|
||||||
|
);
|
||||||
|
|
||||||
if (status === 'pending') {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2.5 px-2">
|
<div className="flex items-center gap-2.5 px-2">
|
||||||
<div className="relative h-8 w-8 shrink-0 animate-pulse rounded bg-neutral-400 dark:bg-neutral-600" />
|
<div className="relative h-8 w-8 shrink-0 animate-pulse rounded bg-neutral-400 dark:bg-neutral-600" />
|
||||||
@@ -18,14 +26,25 @@ export function MentionPopupItem({ pubkey, embed }: { pubkey: string; embed?: st
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-11 items-center justify-start gap-2.5 px-2 hover:bg-neutral-200 dark:bg-neutral-800">
|
<div className="flex h-11 items-center justify-start gap-2.5 px-2 hover:bg-neutral-200 dark:bg-neutral-800">
|
||||||
<img
|
<Avatar.Root className="shirnk-0 h-8 w-8">
|
||||||
src={user.picture || user.image}
|
<Avatar.Image
|
||||||
alt={pubkey}
|
src={user?.picture || user?.image}
|
||||||
className="shirnk-0 h-8 w-8 rounded-md object-cover"
|
alt={pubkey}
|
||||||
/>
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
className="h-8 w-8 rounded-md object-cover"
|
||||||
|
/>
|
||||||
|
<Avatar.Fallback delayMs={300}>
|
||||||
|
<img
|
||||||
|
src={svgURI}
|
||||||
|
alt={pubkey}
|
||||||
|
className="h-8 w-8 rounded-md bg-black dark:bg-white"
|
||||||
|
/>
|
||||||
|
</Avatar.Fallback>
|
||||||
|
</Avatar.Root>
|
||||||
<div className="flex flex-col items-start gap-px">
|
<div className="flex flex-col items-start gap-px">
|
||||||
<h5 className="max-w-[10rem] truncate text-sm font-medium leading-none text-neutral-900 dark:text-neutral-100">
|
<h5 className="max-w-[10rem] truncate text-sm font-medium leading-none text-neutral-900 dark:text-neutral-100">
|
||||||
{user.display_name || user.displayName || user.name}
|
{user?.display_name || user?.displayName || user?.name}
|
||||||
</h5>
|
</h5>
|
||||||
<span className="text-sm leading-none text-neutral-600 dark:text-neutral-400">
|
<span className="text-sm leading-none text-neutral-600 dark:text-neutral-400">
|
||||||
{displayNpub(pubkey, 16)}
|
{displayNpub(pubkey, 16)}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
|
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
|
||||||
import CharacterCount from '@tiptap/extension-character-count';
|
import CharacterCount from '@tiptap/extension-character-count';
|
||||||
import Image from '@tiptap/extension-image';
|
import Image from '@tiptap/extension-image';
|
||||||
|
import Mention from '@tiptap/extension-mention';
|
||||||
import Placeholder from '@tiptap/extension-placeholder';
|
import Placeholder from '@tiptap/extension-placeholder';
|
||||||
import { EditorContent, useEditor } from '@tiptap/react';
|
import { EditorContent, useEditor } from '@tiptap/react';
|
||||||
import StarterKit from '@tiptap/starter-kit';
|
import StarterKit from '@tiptap/starter-kit';
|
||||||
import { convert } from 'html-to-text';
|
import { convert } from 'html-to-text';
|
||||||
import { useEffect, useState } from 'react';
|
import { nip19 } from 'nostr-tools';
|
||||||
|
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
@@ -16,18 +18,21 @@ import { useNDK } from '@libs/ndk/provider';
|
|||||||
import { CancelIcon, LoaderIcon } from '@shared/icons';
|
import { CancelIcon, LoaderIcon } from '@shared/icons';
|
||||||
import { MentionNote } from '@shared/notes';
|
import { MentionNote } from '@shared/notes';
|
||||||
|
|
||||||
import { WIDGET_KIND } from '@stores/constants';
|
import { WIDGET_KIND } from '@utils/constants';
|
||||||
|
import { useSuggestion } from '@utils/hooks/useSuggestion';
|
||||||
import { useWidget } from '@utils/hooks/useWidget';
|
import { useWidget } from '@utils/hooks/useWidget';
|
||||||
|
|
||||||
export function NewPostScreen() {
|
export function NewPostScreen() {
|
||||||
const { ndk } = useNDK();
|
const { ndk } = useNDK();
|
||||||
const { addWidget } = useWidget();
|
const { addWidget } = useWidget();
|
||||||
|
const { suggestion } = useSuggestion();
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [height, setHeight] = useState(0);
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const containerRef = useRef(null);
|
||||||
const editor = useEditor({
|
const editor = useEditor({
|
||||||
extensions: [
|
extensions: [
|
||||||
StarterKit.configure(),
|
StarterKit.configure(),
|
||||||
@@ -39,6 +44,14 @@ export function NewPostScreen() {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
CharacterCount.configure(),
|
CharacterCount.configure(),
|
||||||
|
Mention.configure({
|
||||||
|
suggestion,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
renderLabel({ options, node }) {
|
||||||
|
const npub = nip19.npubEncode(node.attrs.id);
|
||||||
|
return `nostr:${npub}`;
|
||||||
|
},
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
content: JSON.parse(localStorage.getItem('editor-post') || '{}'),
|
content: JSON.parse(localStorage.getItem('editor-post') || '{}'),
|
||||||
editorProps: {
|
editorProps: {
|
||||||
@@ -115,34 +128,40 @@ export function NewPostScreen() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
setHeight(containerRef.current.clientHeight);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editor) editor.commands.focus('end');
|
if (editor) editor.commands.focus('end');
|
||||||
}, [editor]);
|
}, [editor]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col justify-between">
|
<div className="flex flex-1 flex-col gap-4">
|
||||||
<div>
|
<div className="flex-1 overflow-y-auto">
|
||||||
<EditorContent
|
<div ref={containerRef} style={{ height: `${height}px` }}>
|
||||||
editor={editor}
|
<EditorContent
|
||||||
spellCheck="false"
|
editor={editor}
|
||||||
autoComplete="off"
|
spellCheck="false"
|
||||||
autoCorrect="off"
|
autoComplete="off"
|
||||||
autoCapitalize="off"
|
autoCorrect="off"
|
||||||
/>
|
autoCapitalize="off"
|
||||||
{searchParams.get('replyTo') && (
|
/>
|
||||||
<div className="relative max-w-lg">
|
{searchParams.get('replyTo') && (
|
||||||
<MentionNote id={searchParams.get('replyTo')} editing />
|
<div className="relative max-w-lg">
|
||||||
<button
|
<MentionNote id={searchParams.get('replyTo')} editing />
|
||||||
type="button"
|
<button
|
||||||
onClick={() => setSearchParams({})}
|
type="button"
|
||||||
className="absolute right-3 top-3 inline-flex h-6 w-6 items-center justify-center rounded bg-neutral-200 px-2 dark:bg-neutral-800"
|
onClick={() => setSearchParams({})}
|
||||||
>
|
className="absolute right-3 top-3 inline-flex h-6 w-6 items-center justify-center rounded bg-neutral-200 px-2 dark:bg-neutral-800"
|
||||||
<CancelIcon className="h-5 w-5" />
|
>
|
||||||
</button>
|
<CancelIcon className="h-5 w-5" />
|
||||||
</div>
|
</button>
|
||||||
)}
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex h-16 w-full items-center justify-between border-t border-neutral-100 dark:border-neutral-900">
|
<div className="inline-flex h-16 w-full items-center justify-between border-t border-neutral-100 bg-neutral-50 dark:border-neutral-900 dark:bg-neutral-950">
|
||||||
<span className="text-sm font-medium tabular-nums text-neutral-600 dark:text-neutral-400">
|
<span className="text-sm font-medium tabular-nums text-neutral-600 dark:text-neutral-400">
|
||||||
{editor?.storage?.characterCount.characters()} characters
|
{editor?.storage?.characterCount.characters()} characters
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export function NewPrivkeyScreen() {
|
|||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
autoCorrect="off"
|
autoCorrect="off"
|
||||||
autoCapitalize="off"
|
autoCapitalize="off"
|
||||||
className="h-11 w-full rounded-lg bg-neutral-100 px-3 py-2 placeholder:text-neutral-500 dark:bg-neutral-900 dark:placeholder:text-neutral-400"
|
className="h-11 rounded-lg border-transparent bg-neutral-100 px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
||||||
/>
|
/>
|
||||||
<div className="mt-2 flex flex-col gap-2">
|
<div className="mt-2 flex flex-col gap-2">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { nip19 } from 'nostr-tools';
|
|||||||
import { EventPointer } from 'nostr-tools/lib/types/nip19';
|
import { EventPointer } from 'nostr-tools/lib/types/nip19';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
import { ArrowLeftIcon, CheckCircleIcon, ShareIcon } from '@shared/icons';
|
import { ArrowLeftIcon, CheckCircleIcon, ShareIcon } from '@shared/icons';
|
||||||
import { NoteReplyForm } from '@shared/notes';
|
import { NoteReplyForm } from '@shared/notes';
|
||||||
@@ -40,14 +41,18 @@ export function ArticleNoteScreen() {
|
|||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
const share = async () => {
|
const share = async () => {
|
||||||
await writeText(
|
try {
|
||||||
'https://njump.me/' +
|
await writeText(
|
||||||
nip19.neventEncode({ id: data.id, author: data.pubkey } as EventPointer)
|
'https://njump.me/' +
|
||||||
);
|
nip19.neventEncode({ id: data?.id, author: data?.pubkey } as EventPointer)
|
||||||
// update state
|
);
|
||||||
setIsCopy(true);
|
// update state
|
||||||
// reset state after 2 sec
|
setIsCopy(true);
|
||||||
setTimeout(() => setIsCopy(false), 2000);
|
// reset state after 2 sec
|
||||||
|
setTimeout(() => setIsCopy(false), 2000);
|
||||||
|
} catch (e) {
|
||||||
|
toast.error(e);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { nip19 } from 'nostr-tools';
|
|||||||
import { EventPointer } from 'nostr-tools/lib/types/nip19';
|
import { EventPointer } from 'nostr-tools/lib/types/nip19';
|
||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
import { ArrowLeftIcon, CheckCircleIcon, ReplyIcon, ShareIcon } from '@shared/icons';
|
import { ArrowLeftIcon, CheckCircleIcon, ReplyIcon, ShareIcon } from '@shared/icons';
|
||||||
import {
|
import {
|
||||||
@@ -30,14 +31,18 @@ export function TextNoteScreen() {
|
|||||||
const [isCopy, setIsCopy] = useState(false);
|
const [isCopy, setIsCopy] = useState(false);
|
||||||
|
|
||||||
const share = async () => {
|
const share = async () => {
|
||||||
await writeText(
|
try {
|
||||||
'https://njump.me/' +
|
await writeText(
|
||||||
nip19.neventEncode({ id: data.id, author: data.pubkey } as EventPointer)
|
'https://njump.me/' +
|
||||||
);
|
nip19.neventEncode({ id: data?.id, author: data?.pubkey } as EventPointer)
|
||||||
// update state
|
);
|
||||||
setIsCopy(true);
|
// update state
|
||||||
// reset state after 2 sec
|
setIsCopy(true);
|
||||||
setTimeout(() => setIsCopy(false), 2000);
|
// reset state after 2 sec
|
||||||
|
setTimeout(() => setIsCopy(false), 2000);
|
||||||
|
} catch (e) {
|
||||||
|
toast.error(e);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const scrollToReply = () => {
|
const scrollToReply = () => {
|
||||||
|
|||||||
@@ -43,21 +43,19 @@ export function NWCForm({ setWalletConnectURL }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-3 rounded-xl bg-neutral-100 p-3 dark:bg-neutral-900">
|
<div className="flex flex-col gap-3 rounded-xl bg-neutral-100 p-3 dark:bg-neutral-900">
|
||||||
<div className="flex flex-col gap-1.5">
|
<textarea
|
||||||
<textarea
|
name="walletConnectURL"
|
||||||
name="walletConnectURL"
|
value={uri}
|
||||||
value={uri}
|
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
autoFocus={false}
|
||||||
autoFocus={false}
|
onChange={(e) => setUri(e.target.value)}
|
||||||
onChange={(e) => setUri(e.target.value)}
|
placeholder="nostr+walletconnect://"
|
||||||
placeholder="nostr+walletconnect://"
|
className="h-40 w-full resize-none rounded-lg border-transparent bg-neutral-200 px-3 py-3 text-neutral-900 !outline-none placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-800 dark:text-neutral-100 dark:placeholder:text-neutral-400"
|
||||||
className="h-40 w-full resize-none rounded-lg bg-neutral-200 px-3 py-3 text-neutral-900 !outline-none placeholder:text-neutral-600 dark:bg-neutral-800 dark:text-neutral-100 dark:placeholder:text-neutral-400"
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={submit}
|
onClick={submit}
|
||||||
className="inline-flex h-9 w-full items-center justify-center rounded-lg bg-blue-500 font-medium text-white hover:bg-blue-600"
|
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-blue-500 font-medium text-white hover:bg-blue-600"
|
||||||
>
|
>
|
||||||
{loading ? <LoaderIcon className="h-4 w-4 animate-spin" /> : 'Connect'}
|
{loading ? <LoaderIcon className="h-4 w-4 animate-spin" /> : 'Connect'}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export function NWCScreen() {
|
|||||||
const [walletConnectURL, setWalletConnectURL] = useState<null | string>(null);
|
const [walletConnectURL, setWalletConnectURL] = useState<null | string>(null);
|
||||||
|
|
||||||
const remove = async () => {
|
const remove = async () => {
|
||||||
await db.secureRemove('nwc');
|
await db.secureRemove(`${db.account.pubkey}-nwc`);
|
||||||
setWalletConnectURL(null);
|
setWalletConnectURL(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -41,16 +41,16 @@ export function NWCScreen() {
|
|||||||
<CheckCircleIcon className="h-4 w-4" />
|
<CheckCircleIcon className="h-4 w-4" />
|
||||||
<div>You're using nostr wallet connect</div>
|
<div>You're using nostr wallet connect</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-3">
|
||||||
<textarea
|
<textarea
|
||||||
readOnly
|
readOnly
|
||||||
value={walletConnectURL.substring(0, 120) + '****'}
|
value={walletConnectURL.substring(0, 120) + '****'}
|
||||||
className="relative h-40 w-full resize-none rounded-lg bg-neutral-200 px-3 py-1 text-neutral-900 !outline-none placeholder:text-neutral-600 dark:bg-neutral-800 dark:text-neutral-100 dark:placeholder:text-neutral-400"
|
className="h-40 w-full resize-none rounded-lg border-transparent bg-neutral-200 px-3 py-3 text-neutral-900 !outline-none placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-800 dark:text-neutral-100 dark:placeholder:text-neutral-400"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => remove()}
|
onClick={() => remove()}
|
||||||
className="inline-flex h-9 w-full items-center justify-center gap-2 rounded-lg bg-neutral-200 px-6 font-medium text-red-500 hover:bg-red-500 hover:text-white focus:outline-none dark:bg-neutral-800 dark:text-neutral-100"
|
className="inline-flex h-11 w-full items-center justify-center gap-2 rounded-lg bg-neutral-200 px-6 font-medium text-red-500 hover:bg-red-500 hover:text-white focus:outline-none dark:bg-neutral-800 dark:text-neutral-100"
|
||||||
>
|
>
|
||||||
Remove connection
|
Remove connection
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
|
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { normalizeRelayUrl } from 'nostr-fetch';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { VList } from 'virtua';
|
import { VList } from 'virtua';
|
||||||
|
|
||||||
@@ -15,7 +16,7 @@ export function RelayEventList({ relayUrl }: { relayUrl: string }) {
|
|||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const url = 'wss://' + relayUrl;
|
const url = 'wss://' + relayUrl;
|
||||||
const events = await fetcher.fetchLatestEvents(
|
const events = await fetcher.fetchLatestEvents(
|
||||||
[url],
|
[normalizeRelayUrl(url)],
|
||||||
{
|
{
|
||||||
kinds: [NDKKind.Text, NDKKind.Repost],
|
kinds: [NDKKind.Text, NDKKind.Repost],
|
||||||
},
|
},
|
||||||
@@ -24,6 +25,8 @@ export function RelayEventList({ relayUrl }: { relayUrl: string }) {
|
|||||||
return events as unknown as NDKEvent[];
|
return events as unknown as NDKEvent[];
|
||||||
},
|
},
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
|
refetchOnReconnect: false,
|
||||||
|
refetchOnMount: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const renderItem = useCallback(
|
const renderItem = useCallback(
|
||||||
@@ -41,19 +44,18 @@ export function RelayEventList({ relayUrl }: { relayUrl: string }) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full">
|
<VList className="mx-auto h-full w-full max-w-[500px] pt-10 scrollbar-none">
|
||||||
<VList className="mx-auto w-full max-w-[500px] scrollbar-none">
|
{status === 'pending' ? (
|
||||||
{status === 'pending' ? (
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
<div className="inline-flex flex-col items-center justify-center gap-2">
|
||||||
<div className="inline-flex flex-col items-center justify-center gap-2">
|
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
|
||||||
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
|
<p className="text-sm font-medium text-white/80">Loading newsfeed...</p>
|
||||||
<p className="text-sm font-medium text-white/80">Loading newsfeed...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
</div>
|
||||||
data.map((item) => renderItem(item))
|
) : (
|
||||||
)}
|
data.map((item) => renderItem(item))
|
||||||
</VList>
|
)}
|
||||||
</div>
|
<div className="h-20" />
|
||||||
|
</VList>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,69 +1,62 @@
|
|||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { NDKRelayUrl } from '@nostr-dev-kit/ndk';
|
||||||
|
import { normalizeRelayUrl } from 'nostr-fetch';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
import { useStorage } from '@libs/storage/provider';
|
|
||||||
|
|
||||||
import { PlusIcon } from '@shared/icons';
|
import { PlusIcon } from '@shared/icons';
|
||||||
|
|
||||||
|
import { useRelay } from '@utils/hooks/useRelay';
|
||||||
|
|
||||||
const domainRegex = /^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/;
|
const domainRegex = /^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/;
|
||||||
|
|
||||||
export function RelayForm() {
|
export function RelayForm() {
|
||||||
const { db } = useStorage();
|
const { connectRelay } = useRelay();
|
||||||
const queryClient = useQueryClient();
|
const [relay, setRelay] = useState<{
|
||||||
|
url: NDKRelayUrl;
|
||||||
|
purpose: 'read' | 'write' | undefined;
|
||||||
|
}>({ url: '', purpose: undefined });
|
||||||
|
|
||||||
const [url, setUrl] = useState('');
|
const create = () => {
|
||||||
const [error, setError] = useState('');
|
if (relay.url.length < 1) return toast.info('Please enter relay url');
|
||||||
|
|
||||||
const createRelay = async () => {
|
|
||||||
if (url.length < 1) return setError('Please enter relay url');
|
|
||||||
try {
|
try {
|
||||||
const relay = new URL(url.replace(/\s/g, ''));
|
const relayUrl = new URL(relay.url.replace(/\s/g, ''));
|
||||||
if (
|
if (
|
||||||
domainRegex.test(relay.host) &&
|
domainRegex.test(relayUrl.host) &&
|
||||||
(relay.protocol === 'wss:' || relay.protocol === 'ws:')
|
(relayUrl.protocol === 'wss:' || relayUrl.protocol === 'ws:')
|
||||||
) {
|
) {
|
||||||
const res = await db.createRelay(url);
|
connectRelay.mutate(normalizeRelayUrl(relay.url));
|
||||||
if (!res) return setError("You're already using this relay");
|
setRelay({ url: '', purpose: undefined });
|
||||||
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: ['user-relay'],
|
|
||||||
});
|
|
||||||
|
|
||||||
setError('');
|
|
||||||
setUrl('');
|
|
||||||
} else {
|
} else {
|
||||||
return setError(
|
return toast.error(
|
||||||
'URL is invalid, a relay must use websocket protocol (start with wss:// or ws://). Please check again'
|
'URL is invalid, a relay must use websocket protocol (start with wss:// or ws://). Please check again'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
return setError('Relay URL is not valid. Please check again');
|
return toast.error('Relay URL is not valid. Please check again');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<div className="flex h-10 items-center justify-between rounded-lg bg-neutral-200 pr-1.5 dark:bg-neutral-800">
|
<div className="flex gap-2">
|
||||||
<input
|
<input
|
||||||
className="h-full w-full bg-transparent pl-3 pr-1.5 text-neutral-900 placeholder:text-neutral-600 focus:outline-none dark:text-neutral-100 dark:placeholder:text-neutral-400"
|
className="h-11 flex-1 rounded-lg border-transparent bg-neutral-100 px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
||||||
type="url"
|
|
||||||
placeholder="wss://"
|
placeholder="wss://"
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
autoCorrect="off"
|
autoCorrect="off"
|
||||||
autoCapitalize="off"
|
autoCapitalize="off"
|
||||||
value={url}
|
value={relay.url}
|
||||||
onChange={(e) => setUrl(e.target.value)}
|
onChange={(e) => setRelay((prev) => ({ ...prev, url: e.target.value }))}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => createRelay()}
|
onClick={() => create()}
|
||||||
className="inline-flex h-6 w-6 items-center justify-center rounded bg-blue-500 text-white hover:bg-blue-600"
|
className="inline-flex h-11 w-11 shrink-0 items-center justify-center rounded-lg bg-blue-500 text-white hover:bg-blue-600"
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-4 w-4" />
|
<PlusIcon className="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm text-red-400">{error}</span>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,16 @@
|
|||||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { message } from '@tauri-apps/plugin-dialog';
|
|
||||||
import { normalizeRelayUrl } from 'nostr-fetch';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { VList } from 'virtua';
|
import { VList } from 'virtua';
|
||||||
|
|
||||||
import { useStorage } from '@libs/storage/provider';
|
|
||||||
|
|
||||||
import { LoaderIcon, PlusIcon, ShareIcon } from '@shared/icons';
|
import { LoaderIcon, PlusIcon, ShareIcon } from '@shared/icons';
|
||||||
import { User } from '@shared/user';
|
import { User } from '@shared/user';
|
||||||
|
|
||||||
import { useNostr } from '@utils/hooks/useNostr';
|
import { useNostr } from '@utils/hooks/useNostr';
|
||||||
|
import { useRelay } from '@utils/hooks/useRelay';
|
||||||
|
|
||||||
export function RelayList() {
|
export function RelayList() {
|
||||||
const navigate = useNavigate();
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
const { getAllRelaysByUsers } = useNostr();
|
const { getAllRelaysByUsers } = useNostr();
|
||||||
const { db } = useStorage();
|
const { connectRelay } = useRelay();
|
||||||
const { status, data } = useQuery({
|
const { status, data } = useQuery({
|
||||||
queryKey: ['relays'],
|
queryKey: ['relays'],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
@@ -28,21 +22,13 @@ export function RelayList() {
|
|||||||
staleTime: Infinity,
|
staleTime: Infinity,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const inspectRelay = (relayUrl: string) => {
|
const inspectRelay = (relayUrl: string) => {
|
||||||
const url = new URL(relayUrl);
|
const url = new URL(relayUrl);
|
||||||
navigate(`/relays/${url.hostname}`);
|
navigate(`/relays/${url.hostname}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const connectRelay = async (relayUrl: string) => {
|
|
||||||
const url = normalizeRelayUrl(relayUrl);
|
|
||||||
const res = await db.createRelay(url);
|
|
||||||
|
|
||||||
if (!res) await message("You're aldready connected to this relay");
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: ['user-relay'],
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="col-span-2 border-r border-neutral-100 dark:border-neutral-900">
|
<div className="col-span-2 border-r border-neutral-100 dark:border-neutral-900">
|
||||||
{status === 'pending' ? (
|
{status === 'pending' ? (
|
||||||
@@ -55,9 +41,7 @@ export function RelayList() {
|
|||||||
) : (
|
) : (
|
||||||
<VList className="h-full">
|
<VList className="h-full">
|
||||||
<div className="inline-flex h-16 w-full items-center border-b border-neutral-100 px-3 dark:border-neutral-900">
|
<div className="inline-flex h-16 w-full items-center border-b border-neutral-100 px-3 dark:border-neutral-900">
|
||||||
<h3 className="font-semibold text-neutral-950 dark:text-neutral-50">
|
<h3 className="font-semibold">Relay discovery</h3>
|
||||||
All relays
|
|
||||||
</h3>
|
|
||||||
</div>
|
</div>
|
||||||
{[...data].map(([key, value]) => (
|
{[...data].map(([key, value]) => (
|
||||||
<div
|
<div
|
||||||
@@ -76,7 +60,7 @@ export function RelayList() {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => connectRelay(key)}
|
onClick={() => connectRelay.mutate(key)}
|
||||||
className="inline-flex h-6 w-6 items-center justify-center rounded text-neutral-900 hover:bg-neutral-200 dark:text-neutral-100 dark:hover:bg-neutral-800"
|
className="inline-flex h-6 w-6 items-center justify-center rounded text-neutral-900 hover:bg-neutral-200 dark:text-neutral-100 dark:hover:bg-neutral-800"
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-3 w-3" />
|
<PlusIcon className="h-3 w-3" />
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
|
||||||
|
|
||||||
import { RelayForm } from '@app/relays/components/relayForm';
|
|
||||||
|
|
||||||
import { useNDK } from '@libs/ndk/provider';
|
|
||||||
import { useStorage } from '@libs/storage/provider';
|
|
||||||
|
|
||||||
import { CancelIcon } from '@shared/icons';
|
|
||||||
|
|
||||||
export function UserRelay() {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
const { relayUrls } = useNDK();
|
|
||||||
const { db } = useStorage();
|
|
||||||
const { status, data } = useQuery({
|
|
||||||
queryKey: ['user-relay'],
|
|
||||||
queryFn: async () => {
|
|
||||||
return await db.getExplicitRelayUrls();
|
|
||||||
},
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const removeRelay = async (relayUrl: string) => {
|
|
||||||
await db.removeRelay(relayUrl);
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: ['user-relay'],
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mt-3 px-3">
|
|
||||||
{status === 'pending' ? (
|
|
||||||
<p>Loading...</p>
|
|
||||||
) : (
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
{data.map((item) => (
|
|
||||||
<div
|
|
||||||
key={item}
|
|
||||||
className="group flex h-10 items-center justify-between rounded-lg bg-neutral-100 pl-3 pr-1.5 dark:bg-neutral-900"
|
|
||||||
>
|
|
||||||
<div className="inline-flex items-center gap-2.5">
|
|
||||||
{relayUrls.includes(item) ? (
|
|
||||||
<span className="relative flex h-2 w-2">
|
|
||||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-400 opacity-75"></span>
|
|
||||||
<span className="relative inline-flex h-2 w-2 rounded-full bg-teal-500"></span>
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="relative flex h-2 w-2">
|
|
||||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-red-400 opacity-75"></span>
|
|
||||||
<span className="relative inline-flex h-2 w-2 rounded-full bg-red-500"></span>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<p className="max-w-[20rem] truncate text-sm font-medium text-neutral-900 dark:text-neutral-100">
|
|
||||||
{item}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => removeRelay(item)}
|
|
||||||
className="hidden h-6 w-6 items-center justify-center rounded group-hover:inline-flex hover:bg-neutral-300 dark:hover:bg-neutral-700"
|
|
||||||
>
|
|
||||||
<CancelIcon className="h-4 w-4 text-neutral-900 dark:text-neutral-100" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<RelayForm />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
98
src/app/relays/components/userRelayList.tsx
Normal file
98
src/app/relays/components/userRelayList.tsx
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import { NDKKind, NDKSubscriptionCacheUsage } from '@nostr-dev-kit/ndk';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { RelayForm } from '@app/relays/components/relayForm';
|
||||||
|
|
||||||
|
import { useNDK } from '@libs/ndk/provider';
|
||||||
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
|
import { CancelIcon, RefreshIcon } from '@shared/icons';
|
||||||
|
|
||||||
|
import { useRelay } from '@utils/hooks/useRelay';
|
||||||
|
|
||||||
|
export function UserRelayList() {
|
||||||
|
const { db } = useStorage();
|
||||||
|
const { ndk } = useNDK();
|
||||||
|
const { removeRelay } = useRelay();
|
||||||
|
const { status, data, refetch } = useQuery({
|
||||||
|
queryKey: ['relays', db.account.pubkey],
|
||||||
|
queryFn: async () => {
|
||||||
|
const event = await ndk.fetchEvent(
|
||||||
|
{
|
||||||
|
kinds: [NDKKind.RelayList],
|
||||||
|
authors: [db.account.pubkey],
|
||||||
|
},
|
||||||
|
{ cacheUsage: NDKSubscriptionCacheUsage.ONLY_RELAY }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!event) return [];
|
||||||
|
return event.tags;
|
||||||
|
},
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentRelays = new Set([...ndk.pool.relays.values()].map((item) => item.url));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="col-span-1">
|
||||||
|
<div className="inline-flex h-16 w-full items-center justify-between border-b border-neutral-100 px-3 dark:border-neutral-900">
|
||||||
|
<h3 className="font-semibold">Connected relays</h3>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => refetch()}
|
||||||
|
className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-md hover:bg-neutral-100 dark:hover:bg-neutral-900"
|
||||||
|
>
|
||||||
|
<RefreshIcon className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 flex flex-col gap-2 px-3">
|
||||||
|
{status === 'pending' ? (
|
||||||
|
<p>Loading...</p>
|
||||||
|
) : !data.length ? (
|
||||||
|
<div className="flex h-20 w-full items-center justify-center rounded-xl bg-neutral-50 dark:bg-neutral-950">
|
||||||
|
<p className="text-sm font-medium">You not have personal relay list yet</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
data.map((item) => (
|
||||||
|
<div
|
||||||
|
key={item[1]}
|
||||||
|
className="group flex h-11 items-center justify-between rounded-lg bg-neutral-100 px-3 dark:bg-neutral-900"
|
||||||
|
>
|
||||||
|
<div className="inline-flex items-baseline gap-2">
|
||||||
|
{currentRelays.has(item[1]) ? (
|
||||||
|
<span className="relative flex h-2 w-2">
|
||||||
|
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-400 opacity-75"></span>
|
||||||
|
<span className="relative inline-flex h-2 w-2 rounded-full bg-teal-500"></span>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="relative flex h-2 w-2">
|
||||||
|
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-red-400 opacity-75"></span>
|
||||||
|
<span className="relative inline-flex h-2 w-2 rounded-full bg-red-500"></span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<p className="max-w-[20rem] truncate text-sm font-medium text-neutral-900 dark:text-neutral-100">
|
||||||
|
{item[1]}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="inline-flex items-center gap-2">
|
||||||
|
{item[2]?.length ? (
|
||||||
|
<div className="inline-flex h-6 w-max items-center justify-center rounded bg-neutral-200 px-2 text-xs font-medium capitalize dark:bg-neutral-800">
|
||||||
|
{item[2]}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => removeRelay.mutate(item[1])}
|
||||||
|
className="hidden h-6 w-6 items-center justify-center rounded group-hover:inline-flex hover:bg-neutral-300 dark:hover:bg-neutral-700"
|
||||||
|
>
|
||||||
|
<CancelIcon className="h-4 w-4 text-neutral-900 dark:text-neutral-100" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
<RelayForm />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,18 +1,11 @@
|
|||||||
import { RelayList } from '@app/relays/components/relayList';
|
import { RelayList } from '@app/relays/components/relayList';
|
||||||
import { UserRelay } from '@app/relays/components/userRelay';
|
import { UserRelayList } from '@app/relays/components/userRelayList';
|
||||||
|
|
||||||
export function RelaysScreen() {
|
export function RelaysScreen() {
|
||||||
return (
|
return (
|
||||||
<div className="grid h-full w-full grid-cols-3">
|
<div className="grid h-full w-full grid-cols-3">
|
||||||
<RelayList />
|
<RelayList />
|
||||||
<div className="col-span-1">
|
<UserRelayList />
|
||||||
<div className="inline-flex h-16 w-full items-center border-b border-neutral-100 px-3 dark:border-neutral-900">
|
|
||||||
<h3 className="font-semibold text-neutral-900 dark:text-neutral-100">
|
|
||||||
Connected relays
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<UserRelay />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export function BackupSettingScreen() {
|
|||||||
readOnly
|
readOnly
|
||||||
type={showPassword ? 'text' : 'password'}
|
type={showPassword ? 'text' : 'password'}
|
||||||
value={nip19.nsecEncode(privkey)}
|
value={nip19.nsecEncode(privkey)}
|
||||||
className="relative h-11 w-full resize-none rounded-lg bg-neutral-200 py-1 pl-3 pr-11 text-neutral-900 !outline-none placeholder:text-neutral-600 dark:bg-neutral-800 dark:text-neutral-100 dark:placeholder:text-neutral-400"
|
className="relative h-11 w-full resize-none rounded-lg border-none bg-neutral-200 py-1 pl-3 pr-11 text-neutral-900 !outline-none placeholder:text-neutral-600 dark:bg-neutral-800 dark:text-neutral-100 dark:placeholder:text-neutral-400"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { displayNpub } from '@utils/shortenKey';
|
|||||||
|
|
||||||
export function ProfileCard() {
|
export function ProfileCard() {
|
||||||
const { db } = useStorage();
|
const { db } = useStorage();
|
||||||
const { status, user } = useProfile(db.account.pubkey);
|
const { isLoading, user } = useProfile(db.account.pubkey);
|
||||||
|
|
||||||
const svgURI =
|
const svgURI =
|
||||||
'data:image/svg+xml;utf8,' +
|
'data:image/svg+xml;utf8,' +
|
||||||
@@ -19,7 +19,7 @@ export function ProfileCard() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-4 h-56 w-full rounded-2xl bg-neutral-100 transition-all duration-150 ease-smooth hover:scale-105 dark:bg-neutral-900">
|
<div className="mb-4 h-56 w-full rounded-2xl bg-neutral-100 transition-all duration-150 ease-smooth hover:scale-105 dark:bg-neutral-900">
|
||||||
{status === 'pending' ? (
|
{isLoading ? (
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
<LoaderIcon className="h-4 w-4 animate-spin" />
|
<LoaderIcon className="h-4 w-4 animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
@@ -57,7 +57,7 @@ export function ProfileCard() {
|
|||||||
{user?.display_name || user?.name}
|
{user?.display_name || user?.name}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-lg text-neutral-700 dark:text-neutral-300">
|
<p className="text-lg text-neutral-700 dark:text-neutral-300">
|
||||||
{user.nip05 || displayNpub(db.account.pubkey, 16)}
|
{user?.nip05 || displayNpub(db.account.pubkey, 16)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -184,12 +184,9 @@ export function EditProfileScreen() {
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type={'text'}
|
type={'text'}
|
||||||
{...register('display_name', {
|
{...register('display_name')}
|
||||||
required: true,
|
|
||||||
minLength: 4,
|
|
||||||
})}
|
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
className="relative h-11 w-full rounded-lg bg-neutral-100 px-3 py-1 text-neutral-900 !outline-none backdrop-blur-xl placeholder:text-neutral-500 dark:bg-neutral-900 dark:text-neutral-100"
|
className="relative h-11 w-full rounded-lg border-transparent bg-neutral-100 px-3 py-1 text-neutral-900 !outline-none backdrop-blur-xl placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:text-neutral-100"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
@@ -201,12 +198,9 @@ export function EditProfileScreen() {
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type={'text'}
|
type={'text'}
|
||||||
{...register('name', {
|
{...register('name')}
|
||||||
required: true,
|
|
||||||
minLength: 4,
|
|
||||||
})}
|
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
className="relative h-11 w-full rounded-lg bg-neutral-100 px-3 py-1 text-neutral-900 !outline-none backdrop-blur-xl placeholder:text-neutral-500 dark:bg-neutral-900 dark:text-neutral-100"
|
className="relative h-11 w-full rounded-lg border-transparent bg-neutral-100 px-3 py-1 text-neutral-900 !outline-none backdrop-blur-xl placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:text-neutral-100"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
@@ -218,12 +212,9 @@ export function EditProfileScreen() {
|
|||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<input
|
<input
|
||||||
{...register('nip05', {
|
{...register('nip05')}
|
||||||
required: true,
|
|
||||||
minLength: 4,
|
|
||||||
})}
|
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
className="relative h-11 w-full rounded-lg bg-neutral-100 px-3 py-1 text-neutral-900 !outline-none backdrop-blur-xl placeholder:text-neutral-500 dark:bg-neutral-900 dark:text-neutral-100"
|
className="relative h-11 w-full rounded-lg border-transparent bg-neutral-100 px-3 py-1 text-neutral-900 !outline-none backdrop-blur-xl placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:text-neutral-100"
|
||||||
/>
|
/>
|
||||||
<div className="absolute right-2 top-1/2 -translate-y-1/2 transform">
|
<div className="absolute right-2 top-1/2 -translate-y-1/2 transform">
|
||||||
{nip05.verified ? (
|
{nip05.verified ? (
|
||||||
@@ -256,7 +247,7 @@ export function EditProfileScreen() {
|
|||||||
type={'text'}
|
type={'text'}
|
||||||
{...register('website', { required: false })}
|
{...register('website', { required: false })}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
className="relative h-11 w-full rounded-lg bg-neutral-100 px-3 py-1 text-neutral-900 !outline-none backdrop-blur-xl placeholder:text-neutral-500 dark:bg-neutral-900 dark:text-neutral-100"
|
className="relative h-11 w-full rounded-lg border-transparent bg-neutral-100 px-3 py-1 text-neutral-900 !outline-none backdrop-blur-xl placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:text-neutral-100"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
@@ -270,7 +261,7 @@ export function EditProfileScreen() {
|
|||||||
type={'text'}
|
type={'text'}
|
||||||
{...register('lud16', { required: false })}
|
{...register('lud16', { required: false })}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
className="relative h-11 w-full rounded-lg bg-neutral-100 px-3 py-1 text-neutral-900 !outline-none backdrop-blur-xl placeholder:text-neutral-500 dark:bg-neutral-900 dark:text-neutral-100"
|
className="relative h-11 w-full rounded-lg border-transparent bg-neutral-100 px-3 py-1 text-neutral-900 !outline-none backdrop-blur-xl placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:text-neutral-100"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
@@ -283,14 +274,14 @@ export function EditProfileScreen() {
|
|||||||
<textarea
|
<textarea
|
||||||
{...register('about')}
|
{...register('about')}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
className="relative h-20 w-full resize-none rounded-lg bg-neutral-100 px-3 py-2 text-neutral-900 !outline-none backdrop-blur-xl placeholder:text-neutral-500 dark:bg-neutral-900 dark:text-neutral-100"
|
className="relative h-20 w-full resize-none rounded-lg border-transparent bg-neutral-100 px-3 py-2 text-neutral-900 !outline-none backdrop-blur-xl placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:text-neutral-100"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!isValid}
|
disabled={!isValid}
|
||||||
className="mx-auto inline-flex h-9 w-full transform items-center justify-center gap-1 rounded-lg bg-blue-500 font-medium text-white hover:bg-blue-600 focus:outline-none active:translate-y-1 disabled:pointer-events-none disabled:opacity-50"
|
className="mx-auto inline-flex h-11 w-full transform items-center justify-center gap-1 rounded-lg bg-blue-500 font-medium text-white hover:bg-blue-600 focus:outline-none active:translate-y-1 disabled:pointer-events-none disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-white" />
|
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-white" />
|
||||||
|
|||||||
@@ -113,12 +113,6 @@ export function GeneralSettingScreen() {
|
|||||||
...prev,
|
...prev,
|
||||||
hashtag: !!parseInt(item.value),
|
hashtag: !!parseInt(item.value),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (item.key === 'notification')
|
|
||||||
setSettings((prev) => ({
|
|
||||||
...prev,
|
|
||||||
notification: !!parseInt(item.value),
|
|
||||||
}));
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,54 +21,58 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
|
|||||||
const { user } = useProfile(pubkey);
|
const { user } = useProfile(pubkey);
|
||||||
|
|
||||||
const [followed, setFollowed] = useState(false);
|
const [followed, setFollowed] = useState(false);
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
const svgURI =
|
const svgURI =
|
||||||
'data:image/svg+xml;utf8,' + encodeURIComponent(minidenticon(pubkey, 90, 50));
|
'data:image/svg+xml;utf8,' + encodeURIComponent(minidenticon(pubkey, 90, 50));
|
||||||
|
|
||||||
const follow = async (pubkey: string) => {
|
const follow = async () => {
|
||||||
try {
|
try {
|
||||||
|
if (!ndk.signer) return navigate('/new/privkey');
|
||||||
|
setFollowed(true);
|
||||||
|
|
||||||
const user = ndk.getUser({ pubkey: db.account.pubkey });
|
const user = ndk.getUser({ pubkey: db.account.pubkey });
|
||||||
const contacts = await user.follows();
|
const contacts = await user.follows();
|
||||||
const add = await user.follow(new NDKUser({ pubkey: pubkey }), contacts);
|
const add = await user.follow(new NDKUser({ pubkey: pubkey }), contacts);
|
||||||
|
|
||||||
if (add) {
|
if (!add) {
|
||||||
setFollowed(true);
|
toast.success('You already follow this user');
|
||||||
} else {
|
setFollowed(false);
|
||||||
toast('You already follow this user');
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (e) {
|
||||||
console.log(error);
|
toast.error(e);
|
||||||
|
setFollowed(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const unfollow = async (pubkey: string) => {
|
const unfollow = async () => {
|
||||||
try {
|
try {
|
||||||
if (!ndk.signer) return navigate('/new/privkey');
|
if (!ndk.signer) return navigate('/new/privkey');
|
||||||
|
setFollowed(false);
|
||||||
|
|
||||||
const user = ndk.getUser({ pubkey: db.account.pubkey });
|
const user = ndk.getUser({ pubkey: db.account.pubkey });
|
||||||
const contacts = await user.follows();
|
const contacts = await user.follows();
|
||||||
contacts.delete(new NDKUser({ pubkey: pubkey }));
|
contacts.delete(new NDKUser({ pubkey: pubkey }));
|
||||||
|
|
||||||
let list: string[][];
|
const list = [...contacts].map((item) => [
|
||||||
contacts.forEach((el) => list.push(['p', el.pubkey, el.relayUrls?.[0] || '', '']));
|
'p',
|
||||||
|
item.pubkey,
|
||||||
|
item.relayUrls?.[0] || '',
|
||||||
|
'',
|
||||||
|
]);
|
||||||
const event = new NDKEvent(ndk);
|
const event = new NDKEvent(ndk);
|
||||||
event.content = '';
|
event.content = '';
|
||||||
event.kind = NDKKind.Contacts;
|
event.kind = NDKKind.Contacts;
|
||||||
event.tags = list;
|
event.tags = list;
|
||||||
|
|
||||||
const publishedRelays = await event.publish();
|
await event.publish();
|
||||||
if (publishedRelays) {
|
} catch (e) {
|
||||||
setFollowed(false);
|
toast.error(e);
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (db.account.follows.includes(pubkey)) {
|
if (db.account.contacts.includes(pubkey)) {
|
||||||
setFollowed(true);
|
setFollowed(true);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
@@ -78,9 +82,9 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="h-56 w-full overflow-hidden rounded-tl-lg">
|
<div className="h-56 w-full overflow-hidden rounded-tl-lg">
|
||||||
{user.banner ? (
|
{user?.banner ? (
|
||||||
<img
|
<img
|
||||||
src={user.banner}
|
src={user?.banner}
|
||||||
alt="user banner"
|
alt="user banner"
|
||||||
className="h-full w-full rounded-tl-lg object-cover"
|
className="h-full w-full rounded-tl-lg object-cover"
|
||||||
/>
|
/>
|
||||||
@@ -96,7 +100,7 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
|
|||||||
loading="lazy"
|
loading="lazy"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
style={{ contentVisibility: 'auto' }}
|
style={{ contentVisibility: 'auto' }}
|
||||||
className="h-14 w-14 rounded-lg bg-white ring-2 ring-neutral-100 dark:ring-neutral-900"
|
className="h-14 w-14 rounded-lg bg-white object-cover ring-2 ring-neutral-100 dark:ring-neutral-900"
|
||||||
/>
|
/>
|
||||||
<Avatar.Fallback delayMs={300}>
|
<Avatar.Fallback delayMs={300}>
|
||||||
<img
|
<img
|
||||||
@@ -110,12 +114,12 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
|
|||||||
<div className="flex flex-col items-center gap-1">
|
<div className="flex flex-col items-center gap-1">
|
||||||
<div className="inline-flex flex-col items-center">
|
<div className="inline-flex flex-col items-center">
|
||||||
<h5 className="text-center text-xl font-semibold text-neutral-900 dark:text-neutral-100">
|
<h5 className="text-center text-xl font-semibold text-neutral-900 dark:text-neutral-100">
|
||||||
{user.name || user.display_name || user.displayName || 'No name'}
|
{user?.name || user?.display_name || user?.displayName || 'No name'}
|
||||||
</h5>
|
</h5>
|
||||||
{user.nip05 ? (
|
{user?.nip05 ? (
|
||||||
<NIP05
|
<NIP05
|
||||||
pubkey={pubkey}
|
pubkey={pubkey}
|
||||||
nip05={user?.nip05}
|
nip05={user.nip05}
|
||||||
className="text-neutral-600 dark:text-neutral-400"
|
className="text-neutral-600 dark:text-neutral-400"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@@ -125,7 +129,7 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
{user.about || user.bio ? (
|
{user?.about || user?.bio ? (
|
||||||
<p className="mt-2 max-w-[500px] select-text break-words text-center text-neutral-900 dark:text-neutral-100">
|
<p className="mt-2 max-w-[500px] select-text break-words text-center text-neutral-900 dark:text-neutral-100">
|
||||||
{user.about || user.bio}
|
{user.about || user.bio}
|
||||||
</p>
|
</p>
|
||||||
@@ -139,23 +143,23 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
|
|||||||
{followed ? (
|
{followed ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => unfollow(pubkey)}
|
onClick={unfollow}
|
||||||
className="inline-flex h-10 w-36 items-center justify-center rounded-md bg-neutral-200 text-sm font-medium text-neutral-900 backdrop-blur-xl hover:bg-blue-600 hover:text-neutral-100 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-blue-600 dark:hover:text-neutral-100"
|
className="inline-flex h-10 w-36 items-center justify-center rounded-md bg-neutral-200 text-sm font-medium text-neutral-900 backdrop-blur-xl hover:bg-blue-500 hover:text-neutral-100 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-blue-600 dark:hover:text-neutral-100"
|
||||||
>
|
>
|
||||||
Unfollow
|
Unfollow
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => follow(pubkey)}
|
onClick={follow}
|
||||||
className="inline-flex h-10 w-36 items-center justify-center rounded-md bg-neutral-200 text-sm font-medium text-neutral-900 backdrop-blur-xl hover:bg-blue-600 hover:text-neutral-100 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-blue-600 dark:hover:text-neutral-100"
|
className="inline-flex h-10 w-36 items-center justify-center rounded-md bg-neutral-200 text-sm font-medium text-neutral-900 backdrop-blur-xl hover:bg-blue-500 hover:text-neutral-100 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-blue-600 dark:hover:text-neutral-100"
|
||||||
>
|
>
|
||||||
Follow
|
Follow
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<Link
|
<Link
|
||||||
to={`/chats/${pubkey}`}
|
to={`/chats/${pubkey}`}
|
||||||
className="inline-flex h-10 w-36 items-center justify-center rounded-md bg-neutral-200 text-sm font-medium text-neutral-900 backdrop-blur-xl hover:bg-blue-600 hover:text-neutral-100 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-blue-600 dark:hover:text-neutral-100"
|
className="inline-flex h-10 w-36 items-center justify-center rounded-md bg-neutral-200 text-sm font-medium text-neutral-900 backdrop-blur-xl hover:bg-blue-500 hover:text-neutral-100 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-blue-600 dark:hover:text-neutral-100"
|
||||||
>
|
>
|
||||||
Message
|
Message
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -1,141 +1,205 @@
|
|||||||
import NDK, { NDKNip46Signer, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
|
import NDK, {
|
||||||
|
NDKEvent,
|
||||||
|
NDKKind,
|
||||||
|
NDKNip46Signer,
|
||||||
|
NDKPrivateKeySigner,
|
||||||
|
} from '@nostr-dev-kit/ndk';
|
||||||
import { ndkAdapter } from '@nostr-fetch/adapter-ndk';
|
import { ndkAdapter } from '@nostr-fetch/adapter-ndk';
|
||||||
import { message } from '@tauri-apps/plugin-dialog';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { fetch } from '@tauri-apps/plugin-http';
|
import { ask } from '@tauri-apps/plugin-dialog';
|
||||||
import { NostrFetcher } from 'nostr-fetch';
|
import { relaunch } from '@tauri-apps/plugin-process';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { NostrFetcher, normalizeRelayUrlSet } from 'nostr-fetch';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
import NDKCacheAdapterTauri from '@libs/ndk/cache';
|
import NDKCacheAdapterTauri from '@libs/ndk/cache';
|
||||||
import { useStorage } from '@libs/storage/provider';
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
|
import { FETCH_LIMIT } from '@utils/constants';
|
||||||
|
|
||||||
export const NDKInstance = () => {
|
export const NDKInstance = () => {
|
||||||
|
const { db } = useStorage();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const [ndk, setNDK] = useState<NDK | undefined>(undefined);
|
const [ndk, setNDK] = useState<NDK | undefined>(undefined);
|
||||||
|
const [fetcher, setFetcher] = useState<NostrFetcher | undefined>(undefined);
|
||||||
const [relayUrls, setRelayUrls] = useState<string[]>([]);
|
const [relayUrls, setRelayUrls] = useState<string[]>([]);
|
||||||
|
|
||||||
const { db } = useStorage();
|
async function getSigner(nsecbunker?: boolean) {
|
||||||
const fetcher = useMemo(
|
if (!db.account) return;
|
||||||
() => (ndk ? NostrFetcher.withCustomPool(ndkAdapter(ndk)) : null),
|
|
||||||
[ndk]
|
|
||||||
);
|
|
||||||
|
|
||||||
// TODO: fully support NIP-11
|
|
||||||
async function getExplicitRelays() {
|
|
||||||
try {
|
try {
|
||||||
// get relays
|
// NIP-46 Signer
|
||||||
const relays = await db.getExplicitRelayUrls();
|
if (nsecbunker) {
|
||||||
const onlineRelays = new Set(relays);
|
const localSignerPrivkey = await db.secureLoad(`${db.account.id}-nsecbunker`);
|
||||||
|
if (!localSignerPrivkey) return null;
|
||||||
|
|
||||||
const controller = new AbortController();
|
const localSigner = new NDKPrivateKeySigner(localSignerPrivkey);
|
||||||
const timeoutId = setTimeout(() => controller.abort(), 8000);
|
const bunker = new NDK({
|
||||||
|
explicitRelayUrls: ['wss://relay.nsecbunker.com', 'wss://nostr.vulpem.com'],
|
||||||
|
});
|
||||||
|
await bunker.connect();
|
||||||
|
|
||||||
for (const relay of relays) {
|
const remoteSigner = new NDKNip46Signer(bunker, db.account.pubkey, localSigner);
|
||||||
try {
|
await remoteSigner.blockUntilReady();
|
||||||
const url = new URL(relay);
|
|
||||||
const res = await fetch(`https://${url.hostname}`, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/nostr+json',
|
|
||||||
},
|
|
||||||
signal: controller.signal,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
return remoteSigner;
|
||||||
toast.warning(`${relay} is not working, skipping...`);
|
|
||||||
onlineRelays.delete(relay);
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.success(`Connected to ${relay}`);
|
|
||||||
} catch {
|
|
||||||
toast.warning(`${relay} is not working, skipping...`);
|
|
||||||
onlineRelays.delete(relay);
|
|
||||||
} finally {
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// return all online relays
|
// Privkey Signer
|
||||||
return [...onlineRelays];
|
const userPrivkey = await db.secureLoad(db.account.pubkey);
|
||||||
} catch (e) {
|
if (!userPrivkey) return null;
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getSigner(instance: NDK) {
|
|
||||||
if (!db.account) return null;
|
|
||||||
|
|
||||||
const localSignerPrivkey = await db.secureLoad(db.account.pubkey + '-bunker');
|
|
||||||
const userPrivkey = await db.secureLoad(db.account.pubkey);
|
|
||||||
|
|
||||||
// NIP-46 Signer
|
|
||||||
if (localSignerPrivkey) {
|
|
||||||
const localSigner = new NDKPrivateKeySigner(localSignerPrivkey);
|
|
||||||
const remoteSigner = new NDKNip46Signer(instance, db.account.id, localSigner);
|
|
||||||
await remoteSigner.blockUntilReady();
|
|
||||||
|
|
||||||
return remoteSigner;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Privkey Signer
|
|
||||||
if (userPrivkey) {
|
|
||||||
return new NDKPrivateKeySigner(userPrivkey);
|
return new NDKPrivateKeySigner(userPrivkey);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
if (e === 'Token already redeemed') {
|
||||||
|
toast.info(
|
||||||
|
'nsecbunker token already redeemed. You need to re-login with another token.'
|
||||||
|
);
|
||||||
|
|
||||||
|
await db.secureRemove(`${db.account.pubkey}-nsecbunker`);
|
||||||
|
await db.accountLogout();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initNDK() {
|
async function initNDK() {
|
||||||
const outboxSetting = await db.getSettingValue('outbox');
|
const outboxSetting = await db.getSettingValue('outbox');
|
||||||
const explicitRelayUrls = await getExplicitRelays();
|
const bunkerSetting = await db.getSettingValue('nsecbunker');
|
||||||
|
|
||||||
const tauriAdapter = new NDKCacheAdapterTauri(db);
|
const bunker = !!parseInt(bunkerSetting);
|
||||||
const instance = new NDK({
|
const outbox = !!parseInt(outboxSetting);
|
||||||
explicitRelayUrls,
|
|
||||||
cacheAdapter: tauriAdapter,
|
const explicitRelayUrls = normalizeRelayUrlSet([
|
||||||
outboxRelayUrls: ['wss://purplepag.es'],
|
'wss://relay.damus.io',
|
||||||
enableOutboxModel: outboxSetting === '1',
|
'wss://relay.nostr.band',
|
||||||
});
|
'wss://nos.lol',
|
||||||
|
'wss://nostr.mutinywallet.com',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// #TODO: user should config outbox relays
|
||||||
|
const outboxRelayUrls = normalizeRelayUrlSet(['wss://purplepag.es']);
|
||||||
|
|
||||||
|
// #TODO: user should config blacklist relays
|
||||||
|
const blacklistRelayUrls = normalizeRelayUrlSet(['wss://brb.io']);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// connect
|
const tauriAdapter = new NDKCacheAdapterTauri(db);
|
||||||
await instance.connect(2000);
|
const instance = new NDK({
|
||||||
|
explicitRelayUrls,
|
||||||
|
outboxRelayUrls,
|
||||||
|
blacklistRelayUrls,
|
||||||
|
enableOutboxModel: outbox,
|
||||||
|
autoConnectUserRelays: true,
|
||||||
|
autoFetchUserMutelist: true,
|
||||||
|
cacheAdapter: tauriAdapter,
|
||||||
|
// clientName: 'Lume',
|
||||||
|
// clientNip89: '',
|
||||||
|
});
|
||||||
|
|
||||||
// add signer
|
// add signer if exist
|
||||||
const signer = await getSigner(instance);
|
const signer = await getSigner(bunker);
|
||||||
instance.signer = signer;
|
if (signer) instance.signer = signer;
|
||||||
|
|
||||||
|
// connect
|
||||||
|
await instance.connect();
|
||||||
|
const _fetcher = NostrFetcher.withCustomPool(ndkAdapter(instance));
|
||||||
|
|
||||||
// update account's metadata
|
// update account's metadata
|
||||||
if (db.account) {
|
if (db.account) {
|
||||||
const circleSetting = await db.getSettingValue('circles');
|
|
||||||
|
|
||||||
const user = instance.getUser({ pubkey: db.account.pubkey });
|
const user = instance.getUser({ pubkey: db.account.pubkey });
|
||||||
const follows = await user.follows();
|
instance.activeUser = user;
|
||||||
const relayList = await user.relayList();
|
|
||||||
|
|
||||||
const followsAsArr = [];
|
const contacts = await user.follows(undefined /* outbox */);
|
||||||
follows.forEach((user) => {
|
db.account.contacts = [...contacts].map((user) => user.pubkey);
|
||||||
followsAsArr.push(user.pubkey);
|
|
||||||
|
// prefetch newsfeed
|
||||||
|
await queryClient.prefetchInfiniteQuery({
|
||||||
|
queryKey: ['newsfeed'],
|
||||||
|
initialPageParam: 0,
|
||||||
|
queryFn: async ({
|
||||||
|
signal,
|
||||||
|
pageParam,
|
||||||
|
}: {
|
||||||
|
signal: AbortSignal;
|
||||||
|
pageParam: number;
|
||||||
|
}) => {
|
||||||
|
const rootIds = new Set();
|
||||||
|
const dedupQueue = new Set();
|
||||||
|
|
||||||
|
const events = await _fetcher.fetchLatestEvents(
|
||||||
|
explicitRelayUrls,
|
||||||
|
{
|
||||||
|
kinds: [NDKKind.Text, NDKKind.Repost],
|
||||||
|
authors: db.account.contacts,
|
||||||
|
},
|
||||||
|
FETCH_LIMIT,
|
||||||
|
{ asOf: pageParam === 0 ? undefined : pageParam, abortSignal: signal }
|
||||||
|
);
|
||||||
|
|
||||||
|
const ndkEvents = events.map((event) => {
|
||||||
|
return new NDKEvent(ndk, event);
|
||||||
|
});
|
||||||
|
|
||||||
|
ndkEvents.forEach((event) => {
|
||||||
|
const tags = event.tags.filter((el) => el[0] === 'e');
|
||||||
|
if (tags && tags.length > 0) {
|
||||||
|
const rootId = tags.filter((el) => el[3] === 'root')[1] ?? tags[0][1];
|
||||||
|
if (rootIds.has(rootId)) return dedupQueue.add(event.id);
|
||||||
|
rootIds.add(rootId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return ndkEvents
|
||||||
|
.filter((event) => !dedupQueue.has(event.id))
|
||||||
|
.sort((a, b) => b.created_at - a.created_at);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// update user's follows
|
// prefetch notification
|
||||||
await db.updateAccount('follows', JSON.stringify(followsAsArr));
|
await queryClient.prefetchInfiniteQuery({
|
||||||
if (circleSetting !== '1')
|
queryKey: ['notification'],
|
||||||
await db.updateAccount('circles', JSON.stringify(followsAsArr));
|
initialPageParam: 0,
|
||||||
|
queryFn: async ({
|
||||||
|
signal,
|
||||||
|
pageParam,
|
||||||
|
}: {
|
||||||
|
signal: AbortSignal;
|
||||||
|
pageParam: number;
|
||||||
|
}) => {
|
||||||
|
const events = await _fetcher.fetchLatestEvents(
|
||||||
|
explicitRelayUrls,
|
||||||
|
{
|
||||||
|
kinds: [NDKKind.Text, NDKKind.Repost, NDKKind.Reaction, NDKKind.Zap],
|
||||||
|
'#p': [db.account.pubkey],
|
||||||
|
},
|
||||||
|
FETCH_LIMIT,
|
||||||
|
{ asOf: pageParam === 0 ? undefined : pageParam, abortSignal: signal }
|
||||||
|
);
|
||||||
|
|
||||||
// update user's relay list
|
const ndkEvents = events.map((event) => {
|
||||||
if (relayList) {
|
return new NDKEvent(ndk, event);
|
||||||
for (const relay of relayList.relays) {
|
});
|
||||||
await db.createRelay(relay);
|
|
||||||
}
|
return ndkEvents.sort((a, b) => b.created_at - a.created_at);
|
||||||
}
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
await message(`NDK instance init failed: ${error}`, {
|
setNDK(instance);
|
||||||
|
setFetcher(_fetcher);
|
||||||
|
setRelayUrls(explicitRelayUrls);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
const yes = await ask(e, {
|
||||||
title: 'Lume',
|
title: 'Lume',
|
||||||
type: 'error',
|
type: 'error',
|
||||||
|
okLabel: 'Yes',
|
||||||
});
|
});
|
||||||
|
if (yes) relaunch();
|
||||||
}
|
}
|
||||||
|
|
||||||
setNDK(instance);
|
|
||||||
setRelayUrls(explicitRelayUrls);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -144,7 +208,7 @@ export const NDKInstance = () => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
ndk,
|
ndk,
|
||||||
relayUrls,
|
|
||||||
fetcher,
|
fetcher,
|
||||||
|
relayUrls,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { NDKInstance } from '@libs/ndk/instance';
|
|||||||
|
|
||||||
import { LoaderIcon } from '@shared/icons';
|
import { LoaderIcon } from '@shared/icons';
|
||||||
|
|
||||||
import { QUOTES } from '@stores/constants';
|
import { QUOTES } from '@utils/constants';
|
||||||
|
|
||||||
interface NDKContext {
|
interface NDKContext {
|
||||||
ndk: undefined | NDK;
|
ndk: undefined | NDK;
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ import { invoke } from '@tauri-apps/api/primitives';
|
|||||||
import { Platform } from '@tauri-apps/plugin-os';
|
import { Platform } from '@tauri-apps/plugin-os';
|
||||||
import Database from '@tauri-apps/plugin-sql';
|
import Database from '@tauri-apps/plugin-sql';
|
||||||
|
|
||||||
import { FULL_RELAYS } from '@stores/constants';
|
|
||||||
|
|
||||||
import { rawEvent } from '@utils/transform';
|
import { rawEvent } from '@utils/transform';
|
||||||
import type {
|
import type {
|
||||||
Account,
|
Account,
|
||||||
@@ -12,6 +10,7 @@ import type {
|
|||||||
NDKCacheEvent,
|
NDKCacheEvent,
|
||||||
NDKCacheEventTag,
|
NDKCacheEventTag,
|
||||||
NDKCacheUser,
|
NDKCacheUser,
|
||||||
|
NDKCacheUserProfile,
|
||||||
Relays,
|
Relays,
|
||||||
Widget,
|
Widget,
|
||||||
} from '@utils/types';
|
} from '@utils/types';
|
||||||
@@ -52,13 +51,27 @@ export class LumeStorage {
|
|||||||
return await invoke('secure_remove', { key });
|
return await invoke('secure_remove', { key });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getAllCacheUsers() {
|
||||||
|
const results: Array<NDKCacheUser> = await this.db.select(
|
||||||
|
'SELECT * FROM ndk_users ORDER BY createdAt DESC;'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!results.length) return [];
|
||||||
|
|
||||||
|
const users: NDKCacheUserProfile[] = results.map((item) => ({
|
||||||
|
pubkey: item.pubkey,
|
||||||
|
...JSON.parse(item.profile as string),
|
||||||
|
}));
|
||||||
|
return users;
|
||||||
|
}
|
||||||
|
|
||||||
public async getCacheUser(pubkey: string) {
|
public async getCacheUser(pubkey: string) {
|
||||||
const results: Array<NDKCacheUser> = await this.db.select(
|
const results: Array<NDKCacheUser> = await this.db.select(
|
||||||
'SELECT * FROM ndk_users WHERE pubkey = $1 ORDER BY pubkey DESC LIMIT 1;',
|
'SELECT * FROM ndk_users WHERE pubkey = $1 ORDER BY pubkey DESC LIMIT 1;',
|
||||||
[pubkey]
|
[pubkey]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (results.length < 1) return null;
|
if (!results.length) return null;
|
||||||
|
|
||||||
if (typeof results[0].profile === 'string')
|
if (typeof results[0].profile === 'string')
|
||||||
results[0].profile = JSON.parse(results[0].profile);
|
results[0].profile = JSON.parse(results[0].profile);
|
||||||
@@ -72,7 +85,7 @@ export class LumeStorage {
|
|||||||
[id]
|
[id]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (results.length < 1) return null;
|
if (!results.length) return null;
|
||||||
return results[0];
|
return results[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +96,7 @@ export class LumeStorage {
|
|||||||
`SELECT * FROM ndk_events WHERE id IN (${idsArr}) ORDER BY id;`
|
`SELECT * FROM ndk_events WHERE id IN (${idsArr}) ORDER BY id;`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (results.length < 1) return [];
|
if (!results.length) return [];
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +106,7 @@ export class LumeStorage {
|
|||||||
[pubkey]
|
[pubkey]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (results.length < 1) return [];
|
if (!results.length) return [];
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +116,7 @@ export class LumeStorage {
|
|||||||
[kind]
|
[kind]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (results.length < 1) return [];
|
if (!results.length) return [];
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,7 +126,7 @@ export class LumeStorage {
|
|||||||
[kind, pubkey]
|
[kind, pubkey]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (results.length < 1) return [];
|
if (!results.length) return [];
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,7 +136,7 @@ export class LumeStorage {
|
|||||||
[tagValue]
|
[tagValue]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (results.length < 1) return [];
|
if (!results.length) return [];
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +176,7 @@ export class LumeStorage {
|
|||||||
|
|
||||||
public async checkAccount() {
|
public async checkAccount() {
|
||||||
const result: Array<{ total: string }> = await this.db.select(
|
const result: Array<{ total: string }> = await this.db.select(
|
||||||
'SELECT COUNT(*) AS "total" FROM accounts;'
|
'SELECT COUNT(*) AS "total" FROM accounts WHERE is_active = "1" ORDER BY id DESC LIMIT 1;'
|
||||||
);
|
);
|
||||||
return parseInt(result[0].total);
|
return parseInt(result[0].total);
|
||||||
}
|
}
|
||||||
@@ -173,20 +186,9 @@ export class LumeStorage {
|
|||||||
'SELECT * FROM accounts WHERE is_active = "1" ORDER BY id DESC LIMIT 1;'
|
'SELECT * FROM accounts WHERE is_active = "1" ORDER BY id DESC LIMIT 1;'
|
||||||
);
|
);
|
||||||
|
|
||||||
if (results.length > 0) {
|
if (results.length) {
|
||||||
const account = results[0];
|
this.account = results[0];
|
||||||
|
this.account.contacts = [];
|
||||||
if (typeof account.follows === 'string')
|
|
||||||
account.follows = JSON.parse(account.follows) ?? [];
|
|
||||||
|
|
||||||
if (typeof account.circles === 'string')
|
|
||||||
account.circles = JSON.parse(account.circles) ?? [];
|
|
||||||
|
|
||||||
if (typeof account.last_login_at === 'string')
|
|
||||||
account.last_login_at = parseInt(account.last_login_at);
|
|
||||||
|
|
||||||
this.account = account;
|
|
||||||
return account;
|
|
||||||
} else {
|
} else {
|
||||||
console.log('no active account, please create new account');
|
console.log('no active account, please create new account');
|
||||||
return null;
|
return null;
|
||||||
@@ -199,7 +201,7 @@ export class LumeStorage {
|
|||||||
[pubkey]
|
[pubkey]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (existAccounts.length > 0) {
|
if (existAccounts.length) {
|
||||||
await this.db.execute("UPDATE accounts SET is_active = '1' WHERE pubkey = $1;", [
|
await this.db.execute("UPDATE accounts SET is_active = '1' WHERE pubkey = $1;", [
|
||||||
pubkey,
|
pubkey,
|
||||||
]);
|
]);
|
||||||
@@ -210,8 +212,7 @@ export class LumeStorage {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const account = await this.getActiveAccount();
|
return await this.getActiveAccount();
|
||||||
return account;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateAccount(column: string, value: string) {
|
public async updateAccount(column: string, value: string) {
|
||||||
@@ -226,15 +227,6 @@ export class LumeStorage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateLastLogin() {
|
|
||||||
const now = Math.floor(Date.now() / 1000);
|
|
||||||
this.account.last_login_at = now;
|
|
||||||
return await this.db.execute(
|
|
||||||
'UPDATE accounts SET last_login_at = $1 WHERE id = $2;',
|
|
||||||
[now, this.account.id]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getWidgets() {
|
public async getWidgets() {
|
||||||
const widgets: Array<Widget> = await this.db.select(
|
const widgets: Array<Widget> = await this.db.select(
|
||||||
'SELECT * FROM widgets WHERE account_id = $1 ORDER BY created_at DESC;',
|
'SELECT * FROM widgets WHERE account_id = $1 ORDER BY created_at DESC;',
|
||||||
@@ -406,24 +398,13 @@ export class LumeStorage {
|
|||||||
return results.length < 1;
|
return results.length < 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getExplicitRelayUrls() {
|
|
||||||
if (!this.account) return FULL_RELAYS;
|
|
||||||
|
|
||||||
const result: Relays[] = await this.db.select(
|
|
||||||
`SELECT * FROM relays WHERE account_id = "${this.account.id}" ORDER BY id DESC LIMIT 50;`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!result || !result.length) return FULL_RELAYS;
|
|
||||||
return result.map((el) => el.relay);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async createRelay(relay: string, purpose?: string) {
|
public async createRelay(relay: string, purpose?: string) {
|
||||||
const existRelays: Relays[] = await this.db.select(
|
const existRelays: Relays[] = await this.db.select(
|
||||||
'SELECT * FROM relays WHERE relay = $1 AND account_id = $2 ORDER BY id DESC LIMIT 1;',
|
'SELECT * FROM relays WHERE relay = $1 AND account_id = $2 ORDER BY id DESC LIMIT 1;',
|
||||||
[relay, this.account.id]
|
[relay, this.account.id]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (existRelays.length > 0) return false;
|
if (existRelays.length) return;
|
||||||
|
|
||||||
return await this.db.execute(
|
return await this.db.execute(
|
||||||
'INSERT OR IGNORE INTO relays (account_id, relay, purpose) VALUES ($1, $2, $3);',
|
'INSERT OR IGNORE INTO relays (account_id, relay, purpose) VALUES ($1, $2, $3);',
|
||||||
@@ -435,8 +416,15 @@ export class LumeStorage {
|
|||||||
return await this.db.execute(`DELETE FROM relays WHERE relay = "${relay}";`);
|
return await this.db.execute(`DELETE FROM relays WHERE relay = "${relay}";`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createSetting(key: string, value: string) {
|
public async createSetting(key: string, value: string | undefined) {
|
||||||
const currentSetting = await this.getSettingValue(key);
|
if (value) {
|
||||||
|
return await this.db.execute(
|
||||||
|
'INSERT OR IGNORE INTO settings (key, value) VALUES ($1, $2);',
|
||||||
|
[key, value]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentSetting = await this.checkSettingValue(key);
|
||||||
|
|
||||||
if (!currentSetting)
|
if (!currentSetting)
|
||||||
return await this.db.execute(
|
return await this.db.execute(
|
||||||
@@ -460,12 +448,21 @@ export class LumeStorage {
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async checkSettingValue(key: string) {
|
||||||
|
const results: { key: string; value: string }[] = await this.db.select(
|
||||||
|
'SELECT * FROM settings WHERE key = $1 ORDER BY id DESC LIMIT 1;',
|
||||||
|
[key]
|
||||||
|
);
|
||||||
|
if (!results.length) return false;
|
||||||
|
return results[0].value;
|
||||||
|
}
|
||||||
|
|
||||||
public async getSettingValue(key: string) {
|
public async getSettingValue(key: string) {
|
||||||
const results: { key: string; value: string }[] = await this.db.select(
|
const results: { key: string; value: string }[] = await this.db.select(
|
||||||
'SELECT * FROM settings WHERE key = $1 ORDER BY id DESC LIMIT 1;',
|
'SELECT * FROM settings WHERE key = $1 ORDER BY id DESC LIMIT 1;',
|
||||||
[key]
|
[key]
|
||||||
);
|
);
|
||||||
if (results.length < 1) return null;
|
if (!results.length) return '0';
|
||||||
return results[0].value;
|
return results[0].value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -480,9 +477,7 @@ export class LumeStorage {
|
|||||||
await this.db.execute("UPDATE accounts SET is_active = '0' WHERE id = $1;", [
|
await this.db.execute("UPDATE accounts SET is_active = '0' WHERE id = $1;", [
|
||||||
this.account.id,
|
this.account.id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
this.account = null;
|
this.account = null;
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async close() {
|
public async close() {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { LumeStorage } from '@libs/storage/instance';
|
|||||||
|
|
||||||
import { LoaderIcon } from '@shared/icons';
|
import { LoaderIcon } from '@shared/icons';
|
||||||
|
|
||||||
import { QUOTES } from '@stores/constants';
|
import { QUOTES } from '@utils/constants';
|
||||||
|
|
||||||
interface StorageContext {
|
interface StorageContext {
|
||||||
db: LumeStorage;
|
db: LumeStorage;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import { Toaster } from 'sonner';
|
import { Toaster } from 'sonner';
|
||||||
|
|
||||||
@@ -10,7 +11,7 @@ import App from './app';
|
|||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
queries: {
|
queries: {
|
||||||
gcTime: 1000 * 60 * 60 * 24, // 24 hours
|
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000), // 10 seconds
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -20,7 +21,8 @@ const root = createRoot(container);
|
|||||||
|
|
||||||
root.render(
|
root.render(
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<Toaster position="top-center" closeButton theme="system" />
|
<ReactQueryDevtools initialIsOpen={false} />
|
||||||
|
<Toaster position="top-center" theme="system" closeButton />
|
||||||
<StorageProvider>
|
<StorageProvider>
|
||||||
<NDKProvider>
|
<NDKProvider>
|
||||||
<App />
|
<App />
|
||||||
|
|||||||
@@ -11,18 +11,12 @@ import { useProfile } from '@utils/hooks/useProfile';
|
|||||||
|
|
||||||
export function ActiveAccount() {
|
export function ActiveAccount() {
|
||||||
const { db } = useStorage();
|
const { db } = useStorage();
|
||||||
const { status, user } = useProfile(db.account.pubkey);
|
const { user } = useProfile(db.account.pubkey);
|
||||||
|
|
||||||
const svgURI =
|
const svgURI =
|
||||||
'data:image/svg+xml;utf8,' +
|
'data:image/svg+xml;utf8,' +
|
||||||
encodeURIComponent(minidenticon(db.account.pubkey, 90, 50));
|
encodeURIComponent(minidenticon(db.account.pubkey, 90, 50));
|
||||||
|
|
||||||
if (status === 'pending') {
|
|
||||||
return (
|
|
||||||
<div className="aspect-square h-auto w-full animate-pulse rounded-lg bg-neutral-300 dark:bg-neutral-700" />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-1 rounded-lg bg-neutral-100 p-1 ring-1 ring-transparent hover:bg-neutral-200 hover:ring-blue-500 dark:bg-neutral-900 dark:hover:bg-neutral-800">
|
<div className="flex flex-col gap-1 rounded-lg bg-neutral-100 p-1 ring-1 ring-transparent hover:bg-neutral-200 hover:ring-blue-500 dark:bg-neutral-900 dark:hover:bg-neutral-800">
|
||||||
<Link to="/settings/" className="relative inline-block">
|
<Link to="/settings/" className="relative inline-block">
|
||||||
@@ -33,7 +27,7 @@ export function ActiveAccount() {
|
|||||||
loading="lazy"
|
loading="lazy"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
style={{ contentVisibility: 'auto' }}
|
style={{ contentVisibility: 'auto' }}
|
||||||
className="aspect-square h-auto w-full rounded-md"
|
className="aspect-square h-auto w-full rounded-md object-cover"
|
||||||
/>
|
/>
|
||||||
<Avatar.Fallback delayMs={150}>
|
<Avatar.Fallback delayMs={150}>
|
||||||
<img
|
<img
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import * as AlertDialog from '@radix-ui/react-alert-dialog';
|
import * as AlertDialog from '@radix-ui/react-alert-dialog';
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
import { useNDK } from '@libs/ndk/provider';
|
import { useNDK } from '@libs/ndk/provider';
|
||||||
import { useStorage } from '@libs/storage/provider';
|
import { useStorage } from '@libs/storage/provider';
|
||||||
@@ -9,17 +11,27 @@ export function Logout() {
|
|||||||
const { ndk } = useNDK();
|
const { ndk } = useNDK();
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const logout = async () => {
|
const logout = async () => {
|
||||||
ndk.signer = null;
|
try {
|
||||||
|
ndk.signer = null;
|
||||||
|
|
||||||
// remove account
|
// remove private key
|
||||||
await db.accountLogout();
|
await db.secureRemove(db.account.pubkey);
|
||||||
await db.secureRemove(db.account.pubkey);
|
await db.secureRemove(`${db.account.id}-nsecbunker`);
|
||||||
await db.secureRemove(db.account.pubkey + '-bunker');
|
|
||||||
|
|
||||||
// redirect to welcome screen
|
// logout
|
||||||
navigate('/auth/welcome');
|
await db.accountLogout();
|
||||||
|
|
||||||
|
// clear cache
|
||||||
|
queryClient.clear();
|
||||||
|
|
||||||
|
// redirect to welcome screen
|
||||||
|
navigate('/auth/welcome');
|
||||||
|
} catch (e) {
|
||||||
|
toast.error(e);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -33,7 +45,7 @@ export function Logout() {
|
|||||||
</button>
|
</button>
|
||||||
</AlertDialog.Trigger>
|
</AlertDialog.Trigger>
|
||||||
<AlertDialog.Portal>
|
<AlertDialog.Portal>
|
||||||
<AlertDialog.Overlay className="fixed inset-0 z-50 bg-black/50 backdrop-blur-2xl dark:bg-white/50" />
|
<AlertDialog.Overlay className="fixed inset-0 z-50 bg-black/20 backdrop-blur-sm dark:bg-black/20" />
|
||||||
<AlertDialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
|
<AlertDialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
|
||||||
<div className="relative h-min w-full max-w-md rounded-xl bg-neutral-100 dark:bg-neutral-900">
|
<div className="relative h-min w-full max-w-md rounded-xl bg-neutral-100 dark:bg-neutral-900">
|
||||||
<div className="flex flex-col gap-1 border-b border-white/5 px-5 py-4">
|
<div className="flex flex-col gap-1 border-b border-white/5 px-5 py-4">
|
||||||
@@ -54,13 +66,15 @@ export function Logout() {
|
|||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
</AlertDialog.Cancel>
|
</AlertDialog.Cancel>
|
||||||
<button
|
<AlertDialog.Action asChild>
|
||||||
type="button"
|
<button
|
||||||
onClick={() => logout()}
|
type="button"
|
||||||
className="inline-flex h-9 items-center justify-center rounded-lg bg-red-500 px-4 text-sm font-medium text-white outline-none hover:bg-red-600"
|
onClick={() => logout()}
|
||||||
>
|
className="inline-flex h-9 items-center justify-center rounded-lg bg-red-500 px-4 text-sm font-medium text-white outline-none hover:bg-red-600"
|
||||||
Logout
|
>
|
||||||
</button>
|
Logout
|
||||||
|
</button>
|
||||||
|
</AlertDialog.Action>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AlertDialog.Content>
|
</AlertDialog.Content>
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { Logout } from '@shared/accounts/logout';
|
||||||
import { HorizontalDotsIcon } from '@shared/icons';
|
import { HorizontalDotsIcon } from '@shared/icons';
|
||||||
import { Logout } from '@shared/logout';
|
|
||||||
|
|
||||||
export function AccountMoreActions() {
|
export function AccountMoreActions() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { message } from '@tauri-apps/plugin-dialog';
|
import { message } from '@tauri-apps/plugin-dialog';
|
||||||
import { Dispatch, SetStateAction, useState } from 'react';
|
import { Dispatch, SetStateAction, useState } from 'react';
|
||||||
|
|
||||||
import { LoaderIcon, PlusIcon } from '@shared/icons';
|
import { LoaderIcon } from '@shared/icons';
|
||||||
|
|
||||||
import { useNostr } from '@utils/hooks/useNostr';
|
import { useNostr } from '@utils/hooks/useNostr';
|
||||||
|
|
||||||
@@ -37,14 +37,9 @@ export function AvatarUploader({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => uploadAvatar()}
|
onClick={() => uploadAvatar()}
|
||||||
className="inline-flex items-center gap-1 rounded-lg border border-blue-200 bg-blue-100 px-1.5 py-1 text-sm font-medium text-blue-500 hover:border-blue-300 hover:bg-blue-200 dark:border-blue-800 dark:bg-blue-900 dark:text-blue-500 dark:hover:border-blue-800 dark:hover:bg-blue-800"
|
className="inline-flex items-center justify-center rounded-lg border border-blue-200 bg-blue-100 px-2 py-1.5 text-sm font-medium text-blue-500 hover:border-blue-300 hover:bg-blue-200 dark:border-blue-800 dark:bg-blue-900 dark:text-blue-500 dark:hover:border-blue-800 dark:hover:bg-blue-800"
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? <LoaderIcon className="h-4 w-4 animate-spin" /> : 'Change avatar'}
|
||||||
<LoaderIcon className="h-4 w-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<PlusIcon className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
Change avatar
|
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,15 +7,17 @@ export function AuthLayout() {
|
|||||||
const { db } = useStorage();
|
const { db } = useStorage();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen w-screen flex-col bg-neutral-50 dark:bg-neutral-950">
|
<div className="flex h-screen w-screen flex-col">
|
||||||
{db.platform !== 'macos' ? (
|
{db.platform !== 'macos' ? (
|
||||||
<WindowTitlebar />
|
<WindowTitlebar />
|
||||||
) : (
|
) : (
|
||||||
<div data-tauri-drag-region className="h-9" />
|
<div data-tauri-drag-region className="h-9" />
|
||||||
)}
|
)}
|
||||||
<div className="flex h-full min-h-0 w-full">
|
<div className="h-full w-full px-2.5 pb-2.5 pt-1">
|
||||||
<Outlet />
|
<div className="flex h-full min-h-0 w-full rounded-lg bg-white p-3 shadow-[rgba(50,_50,_105,_0.15)_0px_2px_5px_0px,_rgba(0,_0,_0,_0.05)_0px_1px_1px_0px] dark:bg-black dark:shadow-[inset_0_0_0.5px_1px_hsla(0,0%,100%,0.075),0_0_0_1px_hsla(0,0%,0%,0.05),0_0.3px_0.4px_hsla(0,0%,0%,0.02),0_0.9px_1.5px_hsla(0,0%,0%,0.045),0_3.5px_6px_hsla(0,0%,0%,0.09)]">
|
||||||
<ScrollRestoration />
|
<Outlet />
|
||||||
|
<ScrollRestoration />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,65 +15,62 @@ export function NewLayout() {
|
|||||||
{db.platform !== 'macos' ? (
|
{db.platform !== 'macos' ? (
|
||||||
<WindowTitlebar />
|
<WindowTitlebar />
|
||||||
) : (
|
) : (
|
||||||
<div data-tauri-drag-region className="h-9" />
|
<div data-tauri-drag-region className="h-9 shrink-0" />
|
||||||
)}
|
)}
|
||||||
<div data-tauri-drag-region className="h-6" />
|
<div data-tauri-drag-region className="h-4 shrink-0" />
|
||||||
<div className="flex h-full min-h-0 w-full">
|
<div className="container mx-auto grid flex-1 grid-cols-8 px-4">
|
||||||
<div className="container mx-auto grid grid-cols-8 px-4">
|
<div className="col-span-1">
|
||||||
<div className="col-span-1">
|
<Link
|
||||||
<Link
|
to="/"
|
||||||
to="/"
|
className="inline-flex h-10 w-10 items-center justify-center rounded-lg bg-neutral-100 hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
|
||||||
className="inline-flex h-10 w-10 items-center justify-center rounded-lg bg-neutral-100 hover:bg-neutral-200 dark:bg-neutral-900"
|
>
|
||||||
>
|
<ArrowLeftIcon className="h-5 w-5" />
|
||||||
<ArrowLeftIcon className="h-5 w-5" />
|
</Link>
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<div className="relative col-span-6 flex flex-col">
|
|
||||||
<div className="mb-8 flex h-10 shrink-0 items-center gap-3">
|
|
||||||
{location.pathname !== '/new/privkey' ? (
|
|
||||||
<div className="flex h-10 items-center gap-2 rounded-lg bg-neutral-100 px-0.5 dark:bg-neutral-800">
|
|
||||||
<NavLink
|
|
||||||
to="/new/"
|
|
||||||
className={({ isActive }) =>
|
|
||||||
twMerge(
|
|
||||||
'inline-flex h-9 w-20 items-center justify-center rounded-lg text-sm font-medium',
|
|
||||||
isActive ? 'bg-white shadow dark:bg-black' : 'bg-transparent'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Post
|
|
||||||
</NavLink>
|
|
||||||
<NavLink
|
|
||||||
to="/new/article"
|
|
||||||
className={({ isActive }) =>
|
|
||||||
twMerge(
|
|
||||||
'inline-flex h-9 w-20 items-center justify-center rounded-lg text-sm font-medium',
|
|
||||||
isActive ? 'bg-white shadow dark:bg-black' : 'bg-transparent'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Article
|
|
||||||
</NavLink>
|
|
||||||
<NavLink
|
|
||||||
to="/new/file"
|
|
||||||
className={({ isActive }) =>
|
|
||||||
twMerge(
|
|
||||||
'inline-flex h-9 w-28 items-center justify-center rounded-lg text-sm font-medium',
|
|
||||||
isActive ? 'bg-white shadow dark:bg-black' : 'bg-transparent'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
File Sharing
|
|
||||||
</NavLink>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
<div className="h-full min-h-0 w-full">
|
|
||||||
<Outlet />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="col-span-1" />
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="col-span-6 flex flex-col">
|
||||||
|
<div className="mb-8 flex h-10 shrink-0 items-center gap-3">
|
||||||
|
{location.pathname !== '/new/privkey' ? (
|
||||||
|
<div className="flex h-10 items-center gap-2 rounded-lg bg-neutral-100 px-0.5 dark:bg-neutral-800">
|
||||||
|
<NavLink
|
||||||
|
to="/new/"
|
||||||
|
end
|
||||||
|
className={({ isActive }) =>
|
||||||
|
twMerge(
|
||||||
|
'inline-flex h-9 w-20 items-center justify-center rounded-lg text-sm font-medium',
|
||||||
|
isActive ? 'bg-white shadow dark:bg-black' : 'bg-transparent'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Post
|
||||||
|
</NavLink>
|
||||||
|
<NavLink
|
||||||
|
to="/new/article"
|
||||||
|
className={({ isActive }) =>
|
||||||
|
twMerge(
|
||||||
|
'inline-flex h-9 w-20 items-center justify-center rounded-lg text-sm font-medium',
|
||||||
|
isActive ? 'bg-white shadow dark:bg-black' : 'bg-transparent'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Article
|
||||||
|
</NavLink>
|
||||||
|
<NavLink
|
||||||
|
to="/new/file"
|
||||||
|
className={({ isActive }) =>
|
||||||
|
twMerge(
|
||||||
|
'inline-flex h-9 w-28 items-center justify-center rounded-lg text-sm font-medium',
|
||||||
|
isActive ? 'bg-white shadow dark:bg-black' : 'bg-transparent'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
File Sharing
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
<div className="col-span-1" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Link, NavLink, Outlet, ScrollRestoration } from 'react-router-dom';
|
import { NavLink, Outlet, ScrollRestoration, useNavigate } from 'react-router-dom';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
import { WindowTitlebar } from 'tauri-controls';
|
import { WindowTitlebar } from 'tauri-controls';
|
||||||
|
|
||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
|
|
||||||
export function SettingsLayout() {
|
export function SettingsLayout() {
|
||||||
const { db } = useStorage();
|
const { db } = useStorage();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen w-screen flex-col bg-neutral-50 dark:bg-neutral-950">
|
<div className="flex h-screen w-screen flex-col bg-neutral-50 dark:bg-neutral-950">
|
||||||
@@ -26,16 +27,17 @@ export function SettingsLayout() {
|
|||||||
<div className="flex h-full min-h-0 w-full flex-col gap-8 overflow-y-auto pb-10">
|
<div className="flex h-full min-h-0 w-full flex-col gap-8 overflow-y-auto pb-10">
|
||||||
<div className="flex h-20 w-full items-center justify-between border-b border-neutral-200 px-2 pb-2 dark:border-neutral-900">
|
<div className="flex h-20 w-full items-center justify-between border-b border-neutral-200 px-2 pb-2 dark:border-neutral-900">
|
||||||
<div>
|
<div>
|
||||||
<Link
|
<button
|
||||||
to="/"
|
type="button"
|
||||||
|
onClick={() => navigate(-1)}
|
||||||
className="inline-flex h-12 w-12 items-center justify-center rounded-xl bg-neutral-100 dark:bg-neutral-900"
|
className="inline-flex h-12 w-12 items-center justify-center rounded-xl bg-neutral-100 dark:bg-neutral-900"
|
||||||
>
|
>
|
||||||
<ArrowLeftIcon className="h-5 w-5" />
|
<ArrowLeftIcon className="h-5 w-5" />
|
||||||
</Link>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-0.5">
|
<div className="flex items-center gap-0.5">
|
||||||
<NavLink
|
<NavLink
|
||||||
to="/settings"
|
to="/settings/"
|
||||||
end
|
end
|
||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
twMerge(
|
twMerge(
|
||||||
|
|||||||
@@ -39,9 +39,12 @@ export const NIP05 = memo(function NIP05({
|
|||||||
if (!res.ok) throw new Error(`Failed to fetch NIP-05 service: ${nip05}`);
|
if (!res.ok) throw new Error(`Failed to fetch NIP-05 service: ${nip05}`);
|
||||||
|
|
||||||
const data: NIP05 = await res.json();
|
const data: NIP05 = await res.json();
|
||||||
|
|
||||||
if (data.names) {
|
if (data.names) {
|
||||||
if (data.names[localPath] !== pubkey) return false;
|
if (
|
||||||
|
data.names[localPath.toLowerCase()] !== pubkey ||
|
||||||
|
data.names[localPath] !== pubkey
|
||||||
|
)
|
||||||
|
return false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -61,17 +64,13 @@ export const NIP05 = memo(function NIP05({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="inline-flex items-center gap-1">
|
<div className="inline-flex items-center gap-1">
|
||||||
<p className={twMerge('text-sm font-medium', className)}>{nip05}</p>
|
<p className={twMerge('text-sm font-medium', className)}>
|
||||||
|
{nip05.startsWith('_@') ? nip05.replace('_@', '') : nip05}
|
||||||
|
</p>
|
||||||
{data === true ? (
|
{data === true ? (
|
||||||
<div className="inline-flex h-5 w-max shrink-0 items-center justify-center gap-1 rounded-full bg-teal-500 pl-0.5 pr-1.5 text-xs font-medium text-white">
|
<VerifiedIcon className="h-4 w-4 text-teal-500" />
|
||||||
<VerifiedIcon className="h-4 w-4" />
|
|
||||||
Verified
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="inline-flex h-5 w-max shrink-0 items-center justify-center gap-1.5 rounded-full bg-red-500 pl-0.5 pr-1.5 text-xs font-medium text-white">
|
<UnverifiedIcon className="h-4 w-4 text-red-500" />
|
||||||
<UnverifiedIcon className="h-4 w-4" />
|
|
||||||
Unverified
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ import { NoteReaction } from '@shared/notes/actions/reaction';
|
|||||||
import { NoteRepost } from '@shared/notes/actions/repost';
|
import { NoteRepost } from '@shared/notes/actions/repost';
|
||||||
import { NoteZap } from '@shared/notes/actions/zap';
|
import { NoteZap } from '@shared/notes/actions/zap';
|
||||||
|
|
||||||
import { WIDGET_KIND } from '@stores/constants';
|
import { WIDGET_KIND } from '@utils/constants';
|
||||||
|
|
||||||
import { useWidget } from '@utils/hooks/useWidget';
|
import { useWidget } from '@utils/hooks/useWidget';
|
||||||
|
|
||||||
export function NoteActions({
|
export function NoteActions({
|
||||||
|
|||||||
@@ -10,17 +10,20 @@ import CurrencyInput from 'react-currency-input-field';
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { useNDK } from '@libs/ndk/provider';
|
import { useNDK } from '@libs/ndk/provider';
|
||||||
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
import { CancelIcon, ZapIcon } from '@shared/icons';
|
import { CancelIcon, ZapIcon } from '@shared/icons';
|
||||||
|
|
||||||
import { useProfile } from '@utils/hooks/useProfile';
|
import { useProfile } from '@utils/hooks/useProfile';
|
||||||
import { sendNativeNotification } from '@utils/notification';
|
import { sendNativeNotification } from '@utils/notification';
|
||||||
import { compactNumber } from '@utils/number';
|
import { compactNumber } from '@utils/number';
|
||||||
|
import { displayNpub } from '@utils/shortenKey';
|
||||||
|
|
||||||
export function NoteZap({ event }: { event: NDKEvent }) {
|
export function NoteZap({ event }: { event: NDKEvent }) {
|
||||||
const nwc = useRef(null);
|
const nwc = useRef(null);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const { db } = useStorage();
|
||||||
const { ndk } = useNDK();
|
const { ndk } = useNDK();
|
||||||
const { user } = useProfile(event.pubkey);
|
const { user } = useProfile(event.pubkey);
|
||||||
|
|
||||||
@@ -84,7 +87,9 @@ export function NoteZap({ event }: { event: NDKEvent }) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function getWalletConnectURL() {
|
async function getWalletConnectURL() {
|
||||||
const uri: string = await invoke('secure_load', { key: 'nwc' });
|
const uri: string = await invoke('secure_load', {
|
||||||
|
key: `${db.account.pubkey}-nwc`,
|
||||||
|
});
|
||||||
if (uri) setWalletConnectURL(uri);
|
if (uri) setWalletConnectURL(uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,7 +120,8 @@ export function NoteZap({ event }: { event: NDKEvent }) {
|
|||||||
<div className="inline-flex w-full shrink-0 items-center justify-between px-5 py-3">
|
<div className="inline-flex w-full shrink-0 items-center justify-between px-5 py-3">
|
||||||
<div className="w-6" />
|
<div className="w-6" />
|
||||||
<Dialog.Title className="text-center font-semibold">
|
<Dialog.Title className="text-center font-semibold">
|
||||||
Send tip to {user?.name || user?.display_name || user?.displayName}
|
Send tip to{' '}
|
||||||
|
{user?.name || user?.displayName || displayNpub(event.pubkey, 16)}
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<Dialog.Close className="inline-flex h-6 w-6 items-center justify-center rounded-md bg-neutral-100 dark:bg-neutral-900">
|
<Dialog.Close className="inline-flex h-6 w-6 items-center justify-center rounded-md bg-neutral-100 dark:bg-neutral-900">
|
||||||
<CancelIcon className="h-4 w-4" />
|
<CancelIcon className="h-4 w-4" />
|
||||||
@@ -135,7 +141,7 @@ export function NoteZap({ event }: { event: NDKEvent }) {
|
|||||||
max={10000} // 1M sats
|
max={10000} // 1M sats
|
||||||
maxLength={10000} // 1M sats
|
maxLength={10000} // 1M sats
|
||||||
onValueChange={(value) => setAmount(value)}
|
onValueChange={(value) => setAmount(value)}
|
||||||
className="w-full flex-1 bg-transparent text-right text-4xl font-semibold placeholder:text-neutral-600 focus:outline-none dark:text-neutral-400"
|
className="w-full flex-1 border-none bg-transparent text-right text-4xl font-semibold placeholder:text-neutral-600 focus:outline-none focus:ring-0 dark:text-neutral-400"
|
||||||
/>
|
/>
|
||||||
<span className="w-full flex-1 text-left text-4xl font-semibold text-neutral-600 dark:text-neutral-400">
|
<span className="w-full flex-1 text-left text-4xl font-semibold text-neutral-600 dark:text-neutral-400">
|
||||||
sats
|
sats
|
||||||
@@ -189,28 +195,28 @@ export function NoteZap({ event }: { event: NDKEvent }) {
|
|||||||
autoCorrect="off"
|
autoCorrect="off"
|
||||||
autoCapitalize="off"
|
autoCapitalize="off"
|
||||||
placeholder="Enter message (optional)"
|
placeholder="Enter message (optional)"
|
||||||
className="w-full resize-none rounded-lg bg-neutral-100 px-3 py-3 !outline-none placeholder:text-neutral-600 dark:bg-neutral-900 dark:text-neutral-400"
|
className="w-full resize-none rounded-lg border-transparent bg-neutral-100 px-3 py-3 !outline-none placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:text-neutral-400"
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{walletConnectURL ? (
|
{walletConnectURL ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => createZapRequest()}
|
onClick={() => createZapRequest()}
|
||||||
className="inline-flex h-9 w-full items-center justify-center rounded-lg bg-blue-500 px-4 font-medium text-white hover:bg-blue-600"
|
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-blue-500 px-4 font-medium text-white hover:bg-blue-600"
|
||||||
>
|
>
|
||||||
{isCompleted ? (
|
{isCompleted ? (
|
||||||
<p>Successfully tipped</p>
|
<p className="leading-tight">Successfully zapped</p>
|
||||||
) : isLoading ? (
|
) : isLoading ? (
|
||||||
<span className="flex flex-col">
|
<span className="flex flex-col">
|
||||||
<p>Waiting for approval</p>
|
<p className="leading-tight">Waiting for approval</p>
|
||||||
<p className="text-xs text-neutral-600 dark:text-neutral-400">
|
<p className="text-xs leading-tight text-neutral-100">
|
||||||
Go to your wallet and approve payment request
|
Go to your wallet and approve payment request
|
||||||
</p>
|
</p>
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="flex flex-col">
|
<span className="flex flex-col">
|
||||||
<p>Send tip</p>
|
<p className="leading-tight">Send zap</p>
|
||||||
<p className="text-xs text-neutral-600 dark:text-neutral-400">
|
<p className="text-xs leading-tight text-neutral-100">
|
||||||
You're using nostr wallet connect
|
You're using nostr wallet connect
|
||||||
</p>
|
</p>
|
||||||
</span>
|
</span>
|
||||||
@@ -220,7 +226,7 @@ export function NoteZap({ event }: { event: NDKEvent }) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => createZapRequest()}
|
onClick={() => createZapRequest()}
|
||||||
className="inline-flex h-9 w-full items-center justify-center rounded-lg bg-blue-500 px-4 font-medium text-white hover:bg-blue-600"
|
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-blue-500 px-4 font-medium text-white hover:bg-blue-600"
|
||||||
>
|
>
|
||||||
Create Lightning invoice
|
Create Lightning invoice
|
||||||
</button>
|
</button>
|
||||||
@@ -234,7 +240,7 @@ export function NoteZap({ event }: { event: NDKEvent }) {
|
|||||||
<QRCodeSVG value={invoice} size={256} />
|
<QRCodeSVG value={invoice} size={256} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-center gap-1">
|
<div className="flex flex-col items-center gap-1">
|
||||||
<h3 className="text-lg font-medium">Scan to pay</h3>
|
<h3 className="text-lg font-medium">Scan to zap</h3>
|
||||||
<span className="text-center text-sm text-neutral-600 dark:text-neutral-400">
|
<span className="text-center text-sm text-neutral-600 dark:text-neutral-400">
|
||||||
You must use Bitcoin wallet which support Lightning
|
You must use Bitcoin wallet which support Lightning
|
||||||
<br />
|
<br />
|
||||||
|
|||||||
@@ -4,12 +4,22 @@ import { User } from '@shared/user';
|
|||||||
import { useEvent } from '@utils/hooks/useEvent';
|
import { useEvent } from '@utils/hooks/useEvent';
|
||||||
|
|
||||||
export function ChildNote({ id, isRoot }: { id: string; isRoot?: boolean }) {
|
export function ChildNote({ id, isRoot }: { id: string; isRoot?: boolean }) {
|
||||||
const { status, data } = useEvent(id);
|
const { isFetching, isError, data } = useEvent(id);
|
||||||
|
|
||||||
if (status === 'pending' || !data) {
|
if (isFetching) {
|
||||||
return <NoteSkeleton />;
|
return <NoteSkeleton />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
return (
|
||||||
|
<div className="relative flex gap-3">
|
||||||
|
<div className="relative flex-1 rounded-md bg-neutral-200 px-2 py-2 dark:bg-neutral-800">
|
||||||
|
Failed to fetch event
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex gap-3">
|
<div className="relative flex gap-3">
|
||||||
<div className="relative flex-1 rounded-md bg-neutral-200 px-2 py-2 dark:bg-neutral-800">
|
<div className="relative flex-1 rounded-md bg-neutral-200 px-2 py-2 dark:bg-neutral-800">
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
||||||
import { downloadDir } from '@tauri-apps/api/path';
|
import { downloadDir } from '@tauri-apps/api/path';
|
||||||
import { download } from '@tauri-apps/plugin-upload';
|
import { download } from '@tauri-apps/plugin-upload';
|
||||||
|
import { MediaPlayer, MediaProvider } from '@vidstack/react';
|
||||||
import {
|
import {
|
||||||
MediaControlBar,
|
DefaultVideoLayout,
|
||||||
MediaController,
|
defaultLayoutIcons,
|
||||||
MediaFullscreenButton,
|
} from '@vidstack/react/player/layouts/default';
|
||||||
MediaMuteButton,
|
|
||||||
MediaPlayButton,
|
|
||||||
MediaTimeRange,
|
|
||||||
} from 'media-chrome/dist/react';
|
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
@@ -32,7 +29,7 @@ export function FileNote({ event }: { event: NDKEvent }) {
|
|||||||
switch (type) {
|
switch (type) {
|
||||||
case 'image':
|
case 'image':
|
||||||
return (
|
return (
|
||||||
<div key={url} className="group relative">
|
<div className="group relative">
|
||||||
<img
|
<img
|
||||||
src={url}
|
src={url}
|
||||||
alt={url}
|
alt={url}
|
||||||
@@ -52,18 +49,15 @@ export function FileNote({ event }: { event: NDKEvent }) {
|
|||||||
);
|
);
|
||||||
case 'video':
|
case 'video':
|
||||||
return (
|
return (
|
||||||
<MediaController
|
<MediaPlayer
|
||||||
key={url}
|
src={url}
|
||||||
className="aspect-video w-full overflow-hidden rounded-lg"
|
className="w-full overflow-hidden rounded-lg"
|
||||||
|
aspectRatio="16/9"
|
||||||
|
load="visible"
|
||||||
>
|
>
|
||||||
<video slot="media" src={url} preload="metadata" muted />
|
<MediaProvider />
|
||||||
<MediaControlBar>
|
<DefaultVideoLayout icons={defaultLayoutIcons} />
|
||||||
<MediaPlayButton></MediaPlayButton>
|
</MediaPlayer>
|
||||||
<MediaTimeRange></MediaTimeRange>
|
|
||||||
<MediaMuteButton></MediaMuteButton>
|
|
||||||
<MediaFullscreenButton></MediaFullscreenButton>
|
|
||||||
</MediaControlBar>
|
|
||||||
</MediaController>
|
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
import { NDKTag } from '@nostr-dev-kit/ndk';
|
import { NDKTag } from '@nostr-dev-kit/ndk';
|
||||||
import { downloadDir } from '@tauri-apps/api/path';
|
import { downloadDir } from '@tauri-apps/api/path';
|
||||||
import { download } from '@tauri-apps/plugin-upload';
|
import { download } from '@tauri-apps/plugin-upload';
|
||||||
|
import { MediaPlayer, MediaProvider } from '@vidstack/react';
|
||||||
import {
|
import {
|
||||||
MediaControlBar,
|
DefaultVideoLayout,
|
||||||
MediaController,
|
defaultLayoutIcons,
|
||||||
MediaFullscreenButton,
|
} from '@vidstack/react/player/layouts/default';
|
||||||
MediaMuteButton,
|
|
||||||
MediaPlayButton,
|
|
||||||
MediaTimeRange,
|
|
||||||
} from 'media-chrome/dist/react';
|
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
@@ -50,18 +47,15 @@ export function FileKind({ tags }: { tags: NDKTag[] }) {
|
|||||||
|
|
||||||
if (type === 'video') {
|
if (type === 'video') {
|
||||||
return (
|
return (
|
||||||
<MediaController
|
<MediaPlayer
|
||||||
key={url}
|
src={url}
|
||||||
className="aspect-video w-full overflow-hidden rounded-lg"
|
className="w-full overflow-hidden rounded-lg"
|
||||||
|
aspectRatio="16/9"
|
||||||
|
load="visible"
|
||||||
>
|
>
|
||||||
<video slot="media" src={url} preload="metadata" muted />
|
<MediaProvider />
|
||||||
<MediaControlBar>
|
<DefaultVideoLayout icons={defaultLayoutIcons} />
|
||||||
<MediaPlayButton></MediaPlayButton>
|
</MediaPlayer>
|
||||||
<MediaTimeRange></MediaTimeRange>
|
|
||||||
<MediaMuteButton></MediaMuteButton>
|
|
||||||
<MediaFullscreenButton></MediaFullscreenButton>
|
|
||||||
</MediaControlBar>
|
|
||||||
</MediaController>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,14 +7,14 @@ export function TextKind({ content, textmode }: { content: string; textmode?: bo
|
|||||||
|
|
||||||
if (textmode) {
|
if (textmode) {
|
||||||
return (
|
return (
|
||||||
<div className="break-p line-clamp-3 select-text leading-normal text-neutral-900 dark:text-neutral-100">
|
<div className="line-clamp-3 select-text leading-normal text-neutral-900 dark:text-neutral-100">
|
||||||
{parsedContent}
|
{parsedContent}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'min-w-0 px-3'}>
|
<div className="min-w-0 px-3">
|
||||||
<div className="break-p select-text leading-normal text-neutral-900 dark:text-neutral-100">
|
<div className="break-p select-text leading-normal text-neutral-900 dark:text-neutral-100">
|
||||||
{parsedContent}
|
{parsedContent}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { WIDGET_KIND } from '@stores/constants';
|
import { WIDGET_KIND } from '@utils/constants';
|
||||||
|
|
||||||
import { useWidget } from '@utils/hooks/useWidget';
|
import { useWidget } from '@utils/hooks/useWidget';
|
||||||
|
|
||||||
export function Hashtag({ tag }: { tag: string }) {
|
export function Hashtag({ tag }: { tag: string }) {
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ import {
|
|||||||
} from '@shared/notes';
|
} from '@shared/notes';
|
||||||
import { User } from '@shared/user';
|
import { User } from '@shared/user';
|
||||||
|
|
||||||
import { WIDGET_KIND } from '@stores/constants';
|
import { WIDGET_KIND } from '@utils/constants';
|
||||||
|
|
||||||
import { useEvent } from '@utils/hooks/useEvent';
|
import { useEvent } from '@utils/hooks/useEvent';
|
||||||
import { useWidget } from '@utils/hooks/useWidget';
|
import { useWidget } from '@utils/hooks/useWidget';
|
||||||
|
|
||||||
@@ -21,7 +20,7 @@ export const MentionNote = memo(function MentionNote({
|
|||||||
id: string;
|
id: string;
|
||||||
editing?: boolean;
|
editing?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { status, data } = useEvent(id);
|
const { isFetching, isError, data } = useEvent(id);
|
||||||
const { addWidget } = useWidget();
|
const { addWidget } = useWidget();
|
||||||
|
|
||||||
const renderKind = (event: NDKEvent) => {
|
const renderKind = (event: NDKEvent) => {
|
||||||
@@ -37,7 +36,7 @@ export const MentionNote = memo(function MentionNote({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (status === 'pending') {
|
if (isFetching) {
|
||||||
return (
|
return (
|
||||||
<div className="w-full cursor-default rounded-lg bg-neutral-100 p-3 dark:bg-neutral-900">
|
<div className="w-full cursor-default rounded-lg bg-neutral-100 p-3 dark:bg-neutral-900">
|
||||||
<NoteSkeleton />
|
<NoteSkeleton />
|
||||||
@@ -45,6 +44,14 @@ export const MentionNote = memo(function MentionNote({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
return (
|
||||||
|
<div className="w-full cursor-default rounded-lg bg-neutral-100 p-3 dark:bg-neutral-900">
|
||||||
|
Failed to fetch event
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="my-2 flex w-full cursor-default flex-col gap-1 rounded-lg bg-neutral-100 dark:bg-neutral-900">
|
<div className="my-2 flex w-full cursor-default flex-col gap-1 rounded-lg bg-neutral-100 dark:bg-neutral-900">
|
||||||
<div className="mt-3 px-3">
|
<div className="mt-3 px-3">
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
import { WIDGET_KIND } from '@stores/constants';
|
import { WIDGET_KIND } from '@utils/constants';
|
||||||
|
|
||||||
import { useProfile } from '@utils/hooks/useProfile';
|
import { useProfile } from '@utils/hooks/useProfile';
|
||||||
import { useWidget } from '@utils/hooks/useWidget';
|
import { useWidget } from '@utils/hooks/useWidget';
|
||||||
|
|
||||||
@@ -21,12 +20,7 @@ export const MentionUser = memo(function MentionUser({ pubkey }: { pubkey: strin
|
|||||||
}
|
}
|
||||||
className="break-words text-blue-500 hover:text-blue-600"
|
className="break-words text-blue-500 hover:text-blue-600"
|
||||||
>
|
>
|
||||||
{'@' +
|
{'@' + (user?.name || user?.displayName || user?.username || 'unknown')}
|
||||||
(user?.name ||
|
|
||||||
user?.display_name ||
|
|
||||||
user?.displayName ||
|
|
||||||
user?.username ||
|
|
||||||
'unknown')}
|
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,109 +1,157 @@
|
|||||||
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
|
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
import { ShareIcon } from '@shared/icons';
|
import { ReplyIcon, RepostIcon } from '@shared/icons';
|
||||||
import {
|
import { ChildNote, TextKind } from '@shared/notes';
|
||||||
MemoizedArticleKind,
|
|
||||||
MemoizedFileKind,
|
|
||||||
MemoizedTextKind,
|
|
||||||
NoteSkeleton,
|
|
||||||
} from '@shared/notes';
|
|
||||||
import { User } from '@shared/user';
|
import { User } from '@shared/user';
|
||||||
|
|
||||||
import { WIDGET_KIND } from '@stores/constants';
|
import { WIDGET_KIND } from '@utils/constants';
|
||||||
|
|
||||||
import { formatCreatedAt } from '@utils/createdAt';
|
import { formatCreatedAt } from '@utils/createdAt';
|
||||||
import { useEvent } from '@utils/hooks/useEvent';
|
import { useNostr } from '@utils/hooks/useNostr';
|
||||||
import { useWidget } from '@utils/hooks/useWidget';
|
import { useWidget } from '@utils/hooks/useWidget';
|
||||||
|
|
||||||
export function NotifyNote({ event }: { event: NDKEvent }) {
|
export function NotifyNote({ event }: { event: NDKEvent }) {
|
||||||
const createdAt = formatCreatedAt(event.created_at, false);
|
const { getEventThread } = useNostr();
|
||||||
const rootEventId = event.tags.find((el) => el[0] === 'e')?.[1];
|
|
||||||
|
|
||||||
const { status, data } = useEvent(rootEventId);
|
|
||||||
const { addWidget } = useWidget();
|
const { addWidget } = useWidget();
|
||||||
|
|
||||||
const renderKind = (event: NDKEvent) => {
|
const thread = getEventThread(event.tags);
|
||||||
switch (event.kind) {
|
const createdAt = formatCreatedAt(event.created_at, false);
|
||||||
case NDKKind.Text:
|
|
||||||
return <MemoizedTextKind key={event.id} content={event.content} textmode />;
|
|
||||||
case NDKKind.Article:
|
|
||||||
return <MemoizedArticleKind key={event.id} id={event.id} tags={event.tags} />;
|
|
||||||
case 1063:
|
|
||||||
return <MemoizedFileKind key={event.id} tags={event.tags} />;
|
|
||||||
default:
|
|
||||||
return (
|
|
||||||
<div className="break-p line-clamp-3 select-text leading-normal text-neutral-900 dark:text-neutral-100">
|
|
||||||
{event.content}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderText = (kind: number) => {
|
if (event.kind === NDKKind.Reaction) {
|
||||||
switch (kind) {
|
|
||||||
case NDKKind.Text:
|
|
||||||
return 'replied';
|
|
||||||
case NDKKind.Reaction: {
|
|
||||||
return `reacted your post`;
|
|
||||||
}
|
|
||||||
case NDKKind.Repost:
|
|
||||||
return 'reposted your post';
|
|
||||||
case NDKKind.Zap:
|
|
||||||
return 'zapped your post';
|
|
||||||
default:
|
|
||||||
return 'unknown';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (status === 'pending') {
|
|
||||||
return (
|
return (
|
||||||
<div className="h-min w-full px-3 pb-3">
|
<div className="mb-3 h-min w-full px-3">
|
||||||
<div className="relative overflow-hidden rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
|
<div className="flex flex-col gap-2 rounded-xl bg-neutral-50 p-3 dark:bg-neutral-950">
|
||||||
<NoteSkeleton />
|
<div className="flex h-10 items-center justify-between">
|
||||||
|
<div className="relative flex w-full items-center gap-2 px-3 pt-2">
|
||||||
|
<div className="absolute -left-0.5 -top-0.5 inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-blue-100 text-xs ring-2 ring-neutral-50 dark:bg-blue-900 dark:ring-neutral-950">
|
||||||
|
{event.content === '+' ? '👍' : event.content}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-1 items-center justify-between">
|
||||||
|
<div className="inline-flex items-center gap-1.5">
|
||||||
|
<User pubkey={event.pubkey} variant="notify" />
|
||||||
|
<p className="text-neutral-700 dark:text-neutral-300">reacted</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-neutral-500 dark:text-neutral-400">{createdAt}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="w-full px-3">
|
||||||
|
<div className="flex h-min w-full flex-col gap-3 rounded-lg bg-neutral-100 p-3 dark:bg-neutral-900">
|
||||||
|
{thread.rootEventId ? <ChildNote id={thread.rootEventId} /> : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
addWidget.mutate({
|
||||||
|
kind: WIDGET_KIND.thread,
|
||||||
|
title: 'Thread',
|
||||||
|
content: thread.rootEventId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="self-start text-blue-500 hover:text-blue-600"
|
||||||
|
>
|
||||||
|
Show original post
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
if (event.kind === NDKKind.Repost) {
|
||||||
<div className="mb-3 h-min w-full px-3">
|
return (
|
||||||
<div className="flex flex-col gap-2 rounded-xl bg-neutral-50 p-3 dark:bg-neutral-950">
|
<div className="mb-3 h-min w-full px-3">
|
||||||
<div className="flex h-10 items-center justify-between">
|
<div className="flex flex-col gap-2 rounded-xl bg-neutral-50 p-3 dark:bg-neutral-950">
|
||||||
<div className="relative flex w-full items-center gap-2 px-3 pt-2">
|
<div className="flex h-10 items-center justify-between">
|
||||||
<div className="absolute -left-0.5 -top-0.5 inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-blue-100 text-xs ring-2 ring-neutral-50 dark:bg-blue-900 dark:ring-neutral-950">
|
<div className="relative flex w-full items-center gap-2 px-3 pt-2">
|
||||||
{event.kind === 7 ? (event.content === '+' ? '👍' : event.content) : '⚡️'}
|
<div className="absolute -left-0.5 -top-0.5 inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-teal-500 text-xs ring-2 ring-neutral-50 dark:ring-neutral-950">
|
||||||
</div>
|
<RepostIcon className="h-4 w-4 text-white" />
|
||||||
<div className="flex flex-1 items-center justify-between">
|
</div>
|
||||||
<div className="inline-flex items-center gap-1.5">
|
<div className="flex flex-1 items-center justify-between">
|
||||||
<User pubkey={event.pubkey} variant="notify" />
|
<div className="inline-flex items-center gap-1.5">
|
||||||
<p className="text-neutral-900 dark:text-neutral-100">
|
<User pubkey={event.pubkey} variant="notify" />
|
||||||
{renderText(event.kind)}
|
<p className="text-neutral-700 dark:text-neutral-300">reposted</p>
|
||||||
</p>
|
</div>
|
||||||
|
<div className="text-neutral-500 dark:text-neutral-400">{createdAt}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-neutral-500 dark:text-neutral-400">{createdAt}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="flex flex-col gap-2">
|
||||||
<div className="flex gap-2">
|
<div className="w-full px-3">
|
||||||
<div className="flex-1">{data ? renderKind(data) : <p>Loading...</p>}</div>
|
<div className="flex h-min w-full flex-col gap-3 rounded-lg bg-neutral-100 p-3 dark:bg-neutral-900">
|
||||||
<button
|
{thread.rootEventId ? <ChildNote id={thread.rootEventId} /> : null}
|
||||||
type="button"
|
</div>
|
||||||
onClick={() =>
|
</div>
|
||||||
addWidget.mutate({
|
<button
|
||||||
kind: WIDGET_KIND.thread,
|
type="button"
|
||||||
title: 'Thread',
|
onClick={() =>
|
||||||
content: data.id,
|
addWidget.mutate({
|
||||||
})
|
kind: WIDGET_KIND.thread,
|
||||||
}
|
title: 'Thread',
|
||||||
className="inline-flex min-h-full w-10 shrink-0 items-center justify-center rounded-lg text-neutral-600 hover:text-blue-500 dark:text-neutral-400"
|
content: thread.rootEventId,
|
||||||
>
|
})
|
||||||
<ShareIcon className="h-5 w-5" />
|
}
|
||||||
</button>
|
className="self-start text-blue-500 hover:text-blue-600"
|
||||||
|
>
|
||||||
|
Show original post
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
}
|
||||||
|
|
||||||
|
if (event.kind === NDKKind.Text) {
|
||||||
|
return (
|
||||||
|
<div className="mb-3 h-min w-full px-3">
|
||||||
|
<div className="flex flex-col gap-2 rounded-xl bg-neutral-50 p-3 dark:bg-neutral-950">
|
||||||
|
<div className="flex h-10 items-center justify-between">
|
||||||
|
<div className="relative flex w-full items-center gap-2 px-3 pt-2">
|
||||||
|
<div className="absolute -left-0.5 -top-0.5 inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-blue-500 text-xs ring-2 ring-neutral-50 dark:ring-neutral-950">
|
||||||
|
<ReplyIcon className="h-4 w-4 text-white" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-1 items-center justify-between">
|
||||||
|
<div className="inline-flex items-center gap-1.5">
|
||||||
|
<User pubkey={event.pubkey} variant="notify" />
|
||||||
|
<p className="text-neutral-700 dark:text-neutral-300">replied</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-neutral-500 dark:text-neutral-400">{createdAt}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="w-full px-3">
|
||||||
|
<div className="flex h-min w-full flex-col gap-3 rounded-lg bg-neutral-100 p-3 dark:bg-neutral-900">
|
||||||
|
{thread?.replyEventId ? (
|
||||||
|
<ChildNote id={thread?.replyEventId} />
|
||||||
|
) : thread?.rootEventId ? (
|
||||||
|
<ChildNote id={thread?.rootEventId} isRoot />
|
||||||
|
) : null}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
addWidget.mutate({
|
||||||
|
kind: WIDGET_KIND.thread,
|
||||||
|
title: 'Thread',
|
||||||
|
content: thread.replyEventId
|
||||||
|
? thread.replyEventId
|
||||||
|
: thread.rootEventId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="self-start text-blue-500 hover:text-blue-600"
|
||||||
|
>
|
||||||
|
Show full thread
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<TextKind content={event.content} textmode />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MemoizedNotifyNote = memo(NotifyNote);
|
export const MemoizedNotifyNote = memo(NotifyNote);
|
||||||
|
|||||||
@@ -1,27 +1,19 @@
|
|||||||
|
import { MediaPlayer, MediaProvider } from '@vidstack/react';
|
||||||
import {
|
import {
|
||||||
MediaControlBar,
|
DefaultVideoLayout,
|
||||||
MediaController,
|
defaultLayoutIcons,
|
||||||
MediaFullscreenButton,
|
} from '@vidstack/react/player/layouts/default';
|
||||||
MediaLoadingIndicator,
|
|
||||||
MediaMuteButton,
|
|
||||||
MediaPlayButton,
|
|
||||||
MediaTimeRange,
|
|
||||||
} from 'media-chrome/dist/react';
|
|
||||||
|
|
||||||
export function VideoPreview({ url }: { url: string }) {
|
export function VideoPreview({ url }: { url: string }) {
|
||||||
return (
|
return (
|
||||||
<MediaController
|
<MediaPlayer
|
||||||
key={url}
|
src={url}
|
||||||
className="my-2 aspect-video w-full overflow-hidden rounded-lg"
|
className="my-2 w-full overflow-hidden rounded-lg"
|
||||||
|
aspectRatio="16/9"
|
||||||
|
load="visible"
|
||||||
>
|
>
|
||||||
<video slot="media" src={url} preload="metadata" muted />
|
<MediaProvider />
|
||||||
<MediaLoadingIndicator slot="centered-chrome"></MediaLoadingIndicator>
|
<DefaultVideoLayout icons={defaultLayoutIcons} />
|
||||||
<MediaControlBar>
|
</MediaPlayer>
|
||||||
<MediaPlayButton></MediaPlayButton>
|
|
||||||
<MediaTimeRange></MediaTimeRange>
|
|
||||||
<MediaMuteButton></MediaMuteButton>
|
|
||||||
<MediaFullscreenButton></MediaFullscreenButton>
|
|
||||||
</MediaControlBar>
|
|
||||||
</MediaController>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export function NoteReplyForm({ rootEvent }: { rootEvent: NDKEvent }) {
|
|||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => setValue(e.target.value)}
|
onChange={(e) => setValue(e.target.value)}
|
||||||
placeholder="Reply to this post..."
|
placeholder="Reply to this post..."
|
||||||
className="h-28 w-full resize-none rounded-t-xl bg-neutral-100 px-5 py-4 text-neutral-900 !outline-none placeholder:text-neutral-600 dark:bg-neutral-900 dark:text-neutral-100 dark:placeholder:text-neutral-400"
|
className="h-28 w-full resize-none rounded-t-xl border-transparent bg-neutral-100 px-5 py-4 text-neutral-900 !outline-none placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:text-neutral-100 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
/>
|
/>
|
||||||
<div className="inline-flex items-center justify-end gap-2 rounded-b-xl p-2">
|
<div className="inline-flex items-center justify-end gap-2 rounded-b-xl p-2">
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
import { ChildNote, NoteActions } from '@shared/notes';
|
import { ChildNote, NoteActions } from '@shared/notes';
|
||||||
import { User } from '@shared/user';
|
import { User } from '@shared/user';
|
||||||
|
|
||||||
import { WIDGET_KIND } from '@stores/constants';
|
import { WIDGET_KIND } from '@utils/constants';
|
||||||
|
|
||||||
import { useNostr } from '@utils/hooks/useNostr';
|
import { useNostr } from '@utils/hooks/useNostr';
|
||||||
import { useRichContent } from '@utils/hooks/useRichContent';
|
import { useRichContent } from '@utils/hooks/useRichContent';
|
||||||
import { useWidget } from '@utils/hooks/useWidget';
|
import { useWidget } from '@utils/hooks/useWidget';
|
||||||
|
|
||||||
export function TextNote({ event }: { event: NDKEvent }) {
|
export function TextNote({ event, className }: { event: NDKEvent; className?: string }) {
|
||||||
const { parsedContent } = useRichContent(event.content);
|
const { parsedContent } = useRichContent(event.content);
|
||||||
const { addWidget } = useWidget();
|
const { addWidget } = useWidget();
|
||||||
const { getEventThread } = useNostr();
|
const { getEventThread } = useNostr();
|
||||||
@@ -18,7 +18,7 @@ export function TextNote({ event }: { event: NDKEvent }) {
|
|||||||
const thread = getEventThread(event.tags);
|
const thread = getEventThread(event.tags);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-3 h-min w-full px-3">
|
<div className={twMerge('mb-3 h-min w-full px-3', className)}>
|
||||||
<div className="relative flex flex-col gap-2 overflow-hidden rounded-xl bg-neutral-50 pt-3 dark:bg-neutral-950">
|
<div className="relative flex flex-col gap-2 overflow-hidden rounded-xl bg-neutral-50 pt-3 dark:bg-neutral-950">
|
||||||
<User pubkey={event.pubkey} time={event.created_at} eventId={event.id} />
|
<User pubkey={event.pubkey} time={event.created_at} eventId={event.id} />
|
||||||
{thread ? (
|
{thread ? (
|
||||||
|
|||||||
@@ -33,13 +33,13 @@ export function TitleBar({
|
|||||||
<div className="col-span-1 flex justify-center">
|
<div className="col-span-1 flex justify-center">
|
||||||
{id === '9999' ? (
|
{id === '9999' ? (
|
||||||
<div className="isolate flex -space-x-2">
|
<div className="isolate flex -space-x-2">
|
||||||
{db.account.circles
|
{db.account.contacts
|
||||||
?.slice(0, 8)
|
?.slice(0, 8)
|
||||||
.map((item) => <User key={item} pubkey={item} variant="ministacked" />)}
|
.map((item) => <User key={item} pubkey={item} variant="ministacked" />)}
|
||||||
{db.account.circles?.length > 8 ? (
|
{db.account.contacts?.length > 8 ? (
|
||||||
<div className="inline-flex h-6 w-6 items-center justify-center rounded-full bg-neutral-300 text-neutral-900 ring-1 ring-white dark:bg-neutral-700 dark:text-neutral-100 dark:ring-black">
|
<div className="inline-flex h-6 w-6 items-center justify-center rounded-full bg-neutral-300 text-neutral-900 ring-1 ring-white dark:bg-neutral-700 dark:text-neutral-100 dark:ring-black">
|
||||||
<span className="text-[8px] font-medium">
|
<span className="text-[8px] font-medium">
|
||||||
+{db.account.circles?.length - 8}
|
+{db.account.contacts?.length - 8}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { minidenticon } from 'minidenticons';
|
|||||||
import { memo, useMemo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { RepostIcon, WorldIcon } from '@shared/icons';
|
import { RepostIcon } from '@shared/icons';
|
||||||
import { NIP05 } from '@shared/nip05';
|
import { NIP05 } from '@shared/nip05';
|
||||||
import { MoreActions } from '@shared/notes';
|
import { MoreActions } from '@shared/notes';
|
||||||
|
|
||||||
@@ -40,20 +40,33 @@ export const User = memo(function User({
|
|||||||
embedProfile?: string;
|
embedProfile?: string;
|
||||||
subtext?: string;
|
subtext?: string;
|
||||||
}) {
|
}) {
|
||||||
const { status, user } = useProfile(pubkey, embedProfile);
|
const { isLoading, user } = useProfile(pubkey, embedProfile);
|
||||||
|
|
||||||
const createdAt = useMemo(() => formatCreatedAt(time, variant === 'chat'), [pubkey]);
|
const createdAt = useMemo(() => formatCreatedAt(time, variant === 'chat'), [time]);
|
||||||
const svgURI = useMemo(
|
const fallbackName = useMemo(() => displayNpub(pubkey, 16), [pubkey]);
|
||||||
|
const fallbackAvatar = useMemo(
|
||||||
() => 'data:image/svg+xml;utf8,' + encodeURIComponent(minidenticon(pubkey, 90, 50)),
|
() => 'data:image/svg+xml;utf8,' + encodeURIComponent(minidenticon(pubkey, 90, 50)),
|
||||||
[pubkey]
|
[pubkey]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (variant === 'mention') {
|
if (variant === 'mention') {
|
||||||
if (status === 'pending') {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="h-6 w-6 shrink-0 animate-pulse rounded bg-neutral-300 dark:bg-neutral-700" />
|
<Avatar.Root className="shrink-0">
|
||||||
<div className="h-3.5 w-36 animate-pulse rounded bg-neutral-300 dark:bg-neutral-700" />
|
<Avatar.Image
|
||||||
|
src={fallbackAvatar}
|
||||||
|
alt={pubkey}
|
||||||
|
className="h-6 w-6 rounded-md bg-black dark:bg-white"
|
||||||
|
/>
|
||||||
|
</Avatar.Root>
|
||||||
|
<div className="flex flex-1 items-baseline gap-2">
|
||||||
|
<h5 className="max-w-[10rem] truncate font-semibold text-neutral-900 dark:text-neutral-100">
|
||||||
|
{fallbackName}
|
||||||
|
</h5>
|
||||||
|
<span className="text-neutral-600 dark:text-neutral-400">·</span>
|
||||||
|
<span className="text-neutral-600 dark:text-neutral-400">{createdAt}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -70,7 +83,7 @@ export const User = memo(function User({
|
|||||||
/>
|
/>
|
||||||
<Avatar.Fallback delayMs={300}>
|
<Avatar.Fallback delayMs={300}>
|
||||||
<img
|
<img
|
||||||
src={svgURI}
|
src={fallbackAvatar}
|
||||||
alt={pubkey}
|
alt={pubkey}
|
||||||
className="h-6 w-6 rounded-md bg-black dark:bg-white"
|
className="h-6 w-6 rounded-md bg-black dark:bg-white"
|
||||||
/>
|
/>
|
||||||
@@ -78,10 +91,7 @@ export const User = memo(function User({
|
|||||||
</Avatar.Root>
|
</Avatar.Root>
|
||||||
<div className="flex flex-1 items-baseline gap-2">
|
<div className="flex flex-1 items-baseline gap-2">
|
||||||
<h5 className="max-w-[10rem] truncate font-semibold text-neutral-900 dark:text-neutral-100">
|
<h5 className="max-w-[10rem] truncate font-semibold text-neutral-900 dark:text-neutral-100">
|
||||||
{user?.name ||
|
{user?.name || user?.display_name || user?.displayName || fallbackName}
|
||||||
user?.display_name ||
|
|
||||||
user?.displayName ||
|
|
||||||
displayNpub(pubkey, 16)}
|
|
||||||
</h5>
|
</h5>
|
||||||
<span className="text-neutral-600 dark:text-neutral-400">·</span>
|
<span className="text-neutral-600 dark:text-neutral-400">·</span>
|
||||||
<span className="text-neutral-600 dark:text-neutral-400">{createdAt}</span>
|
<span className="text-neutral-600 dark:text-neutral-400">{createdAt}</span>
|
||||||
@@ -91,11 +101,19 @@ export const User = memo(function User({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (variant === 'notify') {
|
if (variant === 'notify') {
|
||||||
if (status === 'pending') {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="h-8 w-8 shrink-0 animate-pulse rounded bg-neutral-300 dark:bg-neutral-700" />
|
<Avatar.Root className="h-8 w-8 shrink-0">
|
||||||
<div className="h-3.5 w-36 animate-pulse rounded bg-neutral-300 dark:bg-neutral-700" />
|
<Avatar.Image
|
||||||
|
src={fallbackAvatar}
|
||||||
|
alt={pubkey}
|
||||||
|
className="h-8 w-8 rounded-md bg-black dark:bg-white"
|
||||||
|
/>
|
||||||
|
</Avatar.Root>
|
||||||
|
<h5 className="max-w-[10rem] truncate font-semibold text-neutral-900 dark:text-neutral-100">
|
||||||
|
{fallbackName}
|
||||||
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -112,31 +130,27 @@ export const User = memo(function User({
|
|||||||
/>
|
/>
|
||||||
<Avatar.Fallback delayMs={300}>
|
<Avatar.Fallback delayMs={300}>
|
||||||
<img
|
<img
|
||||||
src={svgURI}
|
src={fallbackAvatar}
|
||||||
alt={pubkey}
|
alt={pubkey}
|
||||||
className="h-8 w-8 rounded-md bg-black dark:bg-white"
|
className="h-8 w-8 rounded-md bg-black dark:bg-white"
|
||||||
/>
|
/>
|
||||||
</Avatar.Fallback>
|
</Avatar.Fallback>
|
||||||
</Avatar.Root>
|
</Avatar.Root>
|
||||||
<h5 className="max-w-[10rem] truncate font-semibold text-neutral-900 dark:text-neutral-100">
|
<h5 className="max-w-[10rem] truncate font-semibold text-neutral-900 dark:text-neutral-100">
|
||||||
{user?.name ||
|
{user?.name || user?.display_name || user?.displayName || fallbackName}
|
||||||
user?.display_name ||
|
|
||||||
user?.displayName ||
|
|
||||||
displayNpub(pubkey, 16)}
|
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (variant === 'large') {
|
if (variant === 'large') {
|
||||||
if (status === 'pending') {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2.5">
|
<div className="flex items-center gap-2.5">
|
||||||
<div className="h-14 w-14 shrink-0 animate-pulse rounded-lg bg-neutral-300 dark:bg-neutral-700" />
|
<div className="h-14 w-14 shrink-0 animate-pulse rounded-lg bg-neutral-300 dark:bg-neutral-700" />
|
||||||
<div>
|
<div className="flex flex-col gap-1.5">
|
||||||
<div className="h-3.5 w-36 animate-pulse rounded bg-neutral-300 dark:bg-neutral-700" />
|
<div className="h-3.5 w-36 animate-pulse rounded bg-neutral-300 dark:bg-neutral-700" />
|
||||||
<div className="h-4 w-24 animate-pulse rounded bg-neutral-300 dark:bg-neutral-700" />
|
<div className="h-4 w-24 animate-pulse rounded bg-neutral-300 dark:bg-neutral-700" />
|
||||||
<div className="h-4 w-24 animate-pulse rounded bg-neutral-300 dark:bg-neutral-700" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -150,44 +164,30 @@ export const User = memo(function User({
|
|||||||
alt={pubkey}
|
alt={pubkey}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
className="h-14 w-14 rounded-lg object-cover"
|
className="h-11 w-11 rounded-lg object-cover"
|
||||||
/>
|
/>
|
||||||
<Avatar.Fallback delayMs={300}>
|
<Avatar.Fallback delayMs={300}>
|
||||||
<img
|
<img
|
||||||
src={svgURI}
|
src={fallbackAvatar}
|
||||||
alt={pubkey}
|
alt={pubkey}
|
||||||
className="h-14 w-14 rounded-lg bg-black dark:bg-white"
|
className="h-11 w-11 rounded-lg bg-black dark:bg-white"
|
||||||
/>
|
/>
|
||||||
</Avatar.Fallback>
|
</Avatar.Fallback>
|
||||||
</Avatar.Root>
|
</Avatar.Root>
|
||||||
<div className="flex h-full flex-col items-start justify-between">
|
<div className="flex flex-col items-start text-start">
|
||||||
<div className="flex flex-col items-start gap-1 text-start">
|
<p className="max-w-[15rem] truncate text-lg font-semibold">
|
||||||
<p className="max-w-[15rem] truncate text-lg font-semibold text-neutral-900 dark:text-neutral-100">
|
{user?.name || user?.display_name || user?.displayName}
|
||||||
{user?.name || user?.display_name || user?.displayName}
|
</p>
|
||||||
</p>
|
<p className="break-p prose prose-neutral max-w-none select-text whitespace-pre-line leading-normal dark:prose-invert">
|
||||||
<p className="break-p prose prose-neutral max-w-none select-text whitespace-pre-line leading-normal dark:prose-invert prose-headings:mb-1 prose-headings:mt-3 prose-p:mb-0 prose-p:mt-0 prose-p:last:mb-1 prose-a:font-normal prose-a:text-blue-500 prose-blockquote:mb-1 prose-blockquote:mt-1 prose-blockquote:border-l-[2px] prose-blockquote:border-blue-500 prose-blockquote:pl-2 prose-pre:whitespace-pre-wrap prose-pre:bg-white/10 prose-ol:m-0 prose-ol:mb-1 prose-ul:mb-1 prose-ul:mt-1 prose-img:mb-2 prose-img:mt-3 prose-hr:mx-0 prose-hr:my-2 hover:prose-a:text-blue-500">
|
{user?.about || user?.bio || 'No bio'}
|
||||||
{user?.about || user?.bio || 'No bio'}
|
</p>
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
{user?.website ? (
|
|
||||||
<Link
|
|
||||||
to={user?.website}
|
|
||||||
target="_blank"
|
|
||||||
className="inline-flex items-center gap-2 text-sm text-neutral-900 dark:text-neutral-100/70"
|
|
||||||
>
|
|
||||||
<WorldIcon className="h-4 w-4" />
|
|
||||||
<p className="max-w-[10rem] truncate">{user?.website}</p>
|
|
||||||
</Link>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (variant === 'simple') {
|
if (variant === 'simple') {
|
||||||
if (status === 'pending') {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2.5">
|
<div className="flex items-center gap-2.5">
|
||||||
<div className="h-10 w-10 shrink-0 animate-pulse rounded-lg bg-neutral-300 dark:bg-neutral-700" />
|
<div className="h-10 w-10 shrink-0 animate-pulse rounded-lg bg-neutral-300 dark:bg-neutral-700" />
|
||||||
@@ -211,7 +211,7 @@ export const User = memo(function User({
|
|||||||
/>
|
/>
|
||||||
<Avatar.Fallback delayMs={300}>
|
<Avatar.Fallback delayMs={300}>
|
||||||
<img
|
<img
|
||||||
src={svgURI}
|
src={fallbackAvatar}
|
||||||
alt={pubkey}
|
alt={pubkey}
|
||||||
className="h-10 w-10 rounded-lg bg-black dark:bg-white"
|
className="h-10 w-10 rounded-lg bg-black dark:bg-white"
|
||||||
/>
|
/>
|
||||||
@@ -222,7 +222,7 @@ export const User = memo(function User({
|
|||||||
{user?.name || user?.display_name || user?.displayName}
|
{user?.name || user?.display_name || user?.displayName}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="max-w-[10rem] truncate text-sm text-neutral-900 dark:text-neutral-100/70">
|
<p className="max-w-[10rem] truncate text-sm text-neutral-900 dark:text-neutral-100/70">
|
||||||
{user?.nip05 || user?.username || displayNpub(pubkey, 16)}
|
{user?.nip05 || user?.username || fallbackName}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -230,7 +230,7 @@ export const User = memo(function User({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (variant === 'avatar') {
|
if (variant === 'avatar') {
|
||||||
if (status === 'pending') {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="h-12 w-12 animate-pulse rounded-lg bg-neutral-300 dark:bg-neutral-700" />
|
<div className="h-12 w-12 animate-pulse rounded-lg bg-neutral-300 dark:bg-neutral-700" />
|
||||||
);
|
);
|
||||||
@@ -247,7 +247,7 @@ export const User = memo(function User({
|
|||||||
/>
|
/>
|
||||||
<Avatar.Fallback delayMs={300}>
|
<Avatar.Fallback delayMs={300}>
|
||||||
<img
|
<img
|
||||||
src={svgURI}
|
src={fallbackAvatar}
|
||||||
alt={pubkey}
|
alt={pubkey}
|
||||||
className="h-12 w-12 rounded-lg bg-black dark:bg-white"
|
className="h-12 w-12 rounded-lg bg-black dark:bg-white"
|
||||||
/>
|
/>
|
||||||
@@ -257,7 +257,7 @@ export const User = memo(function User({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (variant === 'miniavatar') {
|
if (variant === 'miniavatar') {
|
||||||
if (status === 'pending') {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="h-10 w-10 shrink-0 animate-pulse rounded-lg bg-neutral-300 dark:bg-neutral-700" />
|
<div className="h-10 w-10 shrink-0 animate-pulse rounded-lg bg-neutral-300 dark:bg-neutral-700" />
|
||||||
);
|
);
|
||||||
@@ -274,7 +274,7 @@ export const User = memo(function User({
|
|||||||
/>
|
/>
|
||||||
<Avatar.Fallback delayMs={300}>
|
<Avatar.Fallback delayMs={300}>
|
||||||
<img
|
<img
|
||||||
src={svgURI}
|
src={fallbackAvatar}
|
||||||
alt={pubkey}
|
alt={pubkey}
|
||||||
className="h-10 w-10 rounded-lg bg-black dark:bg-white"
|
className="h-10 w-10 rounded-lg bg-black dark:bg-white"
|
||||||
/>
|
/>
|
||||||
@@ -284,9 +284,23 @@ export const User = memo(function User({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (variant === 'childnote') {
|
if (variant === 'childnote') {
|
||||||
if (status === 'pending') {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="h-10 w-10 shrink-0 animate-pulse rounded-lg bg-neutral-300 dark:bg-neutral-700" />
|
<>
|
||||||
|
<Avatar.Root className="h-10 w-10 shrink-0">
|
||||||
|
<Avatar.Image
|
||||||
|
src={fallbackAvatar}
|
||||||
|
alt={pubkey}
|
||||||
|
className="h-10 w-10 rounded-lg bg-black object-cover dark:bg-white"
|
||||||
|
/>
|
||||||
|
</Avatar.Root>
|
||||||
|
<div className="absolute left-2 top-2 inline-flex items-center gap-1.5 font-semibold leading-tight">
|
||||||
|
<div className="w-full max-w-[10rem] truncate">{fallbackName} </div>
|
||||||
|
<div className="font-normal text-neutral-700 dark:text-neutral-300">
|
||||||
|
{subtext}:
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,7 +316,7 @@ export const User = memo(function User({
|
|||||||
/>
|
/>
|
||||||
<Avatar.Fallback delayMs={300}>
|
<Avatar.Fallback delayMs={300}>
|
||||||
<img
|
<img
|
||||||
src={svgURI}
|
src={fallbackAvatar}
|
||||||
alt={pubkey}
|
alt={pubkey}
|
||||||
className="h-10 w-10 rounded-lg bg-black dark:bg-white"
|
className="h-10 w-10 rounded-lg bg-black dark:bg-white"
|
||||||
/>
|
/>
|
||||||
@@ -310,10 +324,7 @@ export const User = memo(function User({
|
|||||||
</Avatar.Root>
|
</Avatar.Root>
|
||||||
<div className="absolute left-2 top-2 inline-flex items-center gap-1.5 font-semibold leading-tight">
|
<div className="absolute left-2 top-2 inline-flex items-center gap-1.5 font-semibold leading-tight">
|
||||||
<div className="w-full max-w-[10rem] truncate">
|
<div className="w-full max-w-[10rem] truncate">
|
||||||
{user?.display_name ||
|
{user?.display_name || user?.name || user?.displayName || fallbackName}{' '}
|
||||||
user?.name ||
|
|
||||||
user?.displayName ||
|
|
||||||
displayNpub(pubkey, 16)}{' '}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="font-normal text-neutral-700 dark:text-neutral-300">
|
<div className="font-normal text-neutral-700 dark:text-neutral-300">
|
||||||
{subtext}:
|
{subtext}:
|
||||||
@@ -324,7 +335,7 @@ export const User = memo(function User({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (variant === 'stacked') {
|
if (variant === 'stacked') {
|
||||||
if (status === 'pending') {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="inline-block h-8 w-8 animate-pulse rounded-full bg-neutral-300 ring-1 ring-neutral-200 dark:bg-neutral-700 dark:ring-neutral-800" />
|
<div className="inline-block h-8 w-8 animate-pulse rounded-full bg-neutral-300 ring-1 ring-neutral-200 dark:bg-neutral-700 dark:ring-neutral-800" />
|
||||||
);
|
);
|
||||||
@@ -341,7 +352,7 @@ export const User = memo(function User({
|
|||||||
/>
|
/>
|
||||||
<Avatar.Fallback delayMs={300}>
|
<Avatar.Fallback delayMs={300}>
|
||||||
<img
|
<img
|
||||||
src={svgURI}
|
src={fallbackAvatar}
|
||||||
alt={pubkey}
|
alt={pubkey}
|
||||||
className="inline-block h-8 w-8 rounded-full bg-black ring-1 ring-neutral-200 dark:bg-white dark:ring-neutral-800"
|
className="inline-block h-8 w-8 rounded-full bg-black ring-1 ring-neutral-200 dark:bg-white dark:ring-neutral-800"
|
||||||
/>
|
/>
|
||||||
@@ -351,7 +362,7 @@ export const User = memo(function User({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (variant === 'ministacked') {
|
if (variant === 'ministacked') {
|
||||||
if (status === 'pending') {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="inline-block h-6 w-6 animate-pulse rounded-full bg-neutral-300 ring-1 ring-white dark:ring-black" />
|
<div className="inline-block h-6 w-6 animate-pulse rounded-full bg-neutral-300 ring-1 ring-white dark:ring-black" />
|
||||||
);
|
);
|
||||||
@@ -368,7 +379,7 @@ export const User = memo(function User({
|
|||||||
/>
|
/>
|
||||||
<Avatar.Fallback delayMs={300}>
|
<Avatar.Fallback delayMs={300}>
|
||||||
<img
|
<img
|
||||||
src={svgURI}
|
src={fallbackAvatar}
|
||||||
alt={pubkey}
|
alt={pubkey}
|
||||||
className="inline-block h-6 w-6 rounded-full bg-black ring-1 ring-white dark:bg-white dark:ring-black"
|
className="inline-block h-6 w-6 rounded-full bg-black ring-1 ring-white dark:bg-white dark:ring-black"
|
||||||
/>
|
/>
|
||||||
@@ -378,7 +389,7 @@ export const User = memo(function User({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (variant === 'repost') {
|
if (variant === 'repost') {
|
||||||
if (status === 'pending') {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<div className="inline-flex h-10 w-10 items-center justify-center">
|
<div className="inline-flex h-10 w-10 items-center justify-center">
|
||||||
@@ -404,11 +415,11 @@ export const User = memo(function User({
|
|||||||
alt={pubkey}
|
alt={pubkey}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
className="h-6 w-6 rounded"
|
className="h-6 w-6 rounded object-cover"
|
||||||
/>
|
/>
|
||||||
<Avatar.Fallback delayMs={300}>
|
<Avatar.Fallback delayMs={300}>
|
||||||
<img
|
<img
|
||||||
src={svgURI}
|
src={fallbackAvatar}
|
||||||
alt={pubkey}
|
alt={pubkey}
|
||||||
className="h-6 w-6 rounded bg-black dark:bg-white"
|
className="h-6 w-6 rounded bg-black dark:bg-white"
|
||||||
/>
|
/>
|
||||||
@@ -416,10 +427,7 @@ export const User = memo(function User({
|
|||||||
</Avatar.Root>
|
</Avatar.Root>
|
||||||
<div className="inline-flex items-baseline gap-1">
|
<div className="inline-flex items-baseline gap-1">
|
||||||
<h5 className="max-w-[10rem] truncate font-medium text-neutral-900 dark:text-neutral-100/80">
|
<h5 className="max-w-[10rem] truncate font-medium text-neutral-900 dark:text-neutral-100/80">
|
||||||
{user?.name ||
|
{user?.name || user?.display_name || user?.displayName || fallbackName}
|
||||||
user?.display_name ||
|
|
||||||
user?.displayName ||
|
|
||||||
displayNpub(pubkey, 16)}
|
|
||||||
</h5>
|
</h5>
|
||||||
<span className="text-blue-500">reposted</span>
|
<span className="text-blue-500">reposted</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -429,13 +437,13 @@ export const User = memo(function User({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (variant === 'thread') {
|
if (variant === 'thread') {
|
||||||
if (status === 'pending') {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex h-16 items-center gap-3 px-3">
|
||||||
<div className="h-10 w-10 shrink-0 animate-pulse rounded-lg bg-neutral-300 dark:bg-neutral-700" />
|
<div className="h-10 w-10 shrink-0 animate-pulse rounded-lg bg-neutral-300 dark:bg-neutral-700" />
|
||||||
<div className="flex flex-1 flex-col">
|
<div className="flex flex-1 flex-col gap-1">
|
||||||
<div className="h-4 w-36 animate-pulse rounded bg-neutral-300 dark:bg-neutral-700" />
|
<div className="h-4 w-36 animate-pulse rounded bg-neutral-300 dark:bg-neutral-700" />
|
||||||
<div className="h-4 w-24 animate-pulse rounded bg-neutral-300 dark:bg-neutral-700" />
|
<div className="h-3 w-24 animate-pulse rounded bg-neutral-300 dark:bg-neutral-700" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -449,11 +457,11 @@ export const User = memo(function User({
|
|||||||
alt={pubkey}
|
alt={pubkey}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
className="h-10 w-10 rounded-lg ring-1 ring-neutral-200/50 dark:ring-neutral-800/50"
|
className="h-10 w-10 rounded-lg object-cover ring-1 ring-neutral-200/50 dark:ring-neutral-800/50"
|
||||||
/>
|
/>
|
||||||
<Avatar.Fallback delayMs={300}>
|
<Avatar.Fallback delayMs={300}>
|
||||||
<img
|
<img
|
||||||
src={svgURI}
|
src={fallbackAvatar}
|
||||||
alt={pubkey}
|
alt={pubkey}
|
||||||
className="h-10 w-10 rounded-lg bg-black ring-1 ring-neutral-200/50 dark:bg-white dark:ring-neutral-800/50"
|
className="h-10 w-10 rounded-lg bg-black ring-1 ring-neutral-200/50 dark:bg-white dark:ring-neutral-800/50"
|
||||||
/>
|
/>
|
||||||
@@ -466,19 +474,27 @@ export const User = memo(function User({
|
|||||||
<div className="inline-flex items-center gap-2 text-sm text-neutral-600 dark:text-neutral-400">
|
<div className="inline-flex items-center gap-2 text-sm text-neutral-600 dark:text-neutral-400">
|
||||||
<span>{createdAt}</span>
|
<span>{createdAt}</span>
|
||||||
<span>·</span>
|
<span>·</span>
|
||||||
<span>{displayNpub(pubkey, 16)}</span>
|
<span>{fallbackName}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'pending') {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-3 px-3">
|
<div className="flex items-center gap-3 px-3">
|
||||||
<div className="h-9 w-9 shrink-0 animate-pulse rounded-lg bg-neutral-300 dark:bg-neutral-700" />
|
<Avatar.Root className="h-9 w-9 shrink-0">
|
||||||
|
<Avatar.Image
|
||||||
|
src={fallbackAvatar}
|
||||||
|
alt={pubkey}
|
||||||
|
className="h-9 w-9 rounded-lg bg-black ring-1 ring-neutral-200/50 dark:bg-white dark:ring-neutral-800/50"
|
||||||
|
/>
|
||||||
|
</Avatar.Root>
|
||||||
<div className="h-6 flex-1">
|
<div className="h-6 flex-1">
|
||||||
<div className="h-4 w-36 animate-pulse rounded bg-neutral-300 dark:bg-neutral-700" />
|
<div className="max-w-[15rem] truncate font-semibold text-neutral-950 dark:text-neutral-50">
|
||||||
|
{fallbackName}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -498,7 +514,7 @@ export const User = memo(function User({
|
|||||||
/>
|
/>
|
||||||
<Avatar.Fallback delayMs={300}>
|
<Avatar.Fallback delayMs={300}>
|
||||||
<img
|
<img
|
||||||
src={svgURI}
|
src={fallbackAvatar}
|
||||||
alt={pubkey}
|
alt={pubkey}
|
||||||
className="h-9 w-9 rounded-lg bg-black ring-1 ring-neutral-200/50 dark:bg-white dark:ring-neutral-800/50"
|
className="h-9 w-9 rounded-lg bg-black ring-1 ring-neutral-200/50 dark:bg-white dark:ring-neutral-800/50"
|
||||||
/>
|
/>
|
||||||
@@ -507,10 +523,7 @@ export const User = memo(function User({
|
|||||||
</HoverCard.Trigger>
|
</HoverCard.Trigger>
|
||||||
<div className="flex h-6 flex-1 items-start gap-2">
|
<div className="flex h-6 flex-1 items-start gap-2">
|
||||||
<div className="max-w-[15rem] truncate font-semibold text-neutral-950 dark:text-neutral-50">
|
<div className="max-w-[15rem] truncate font-semibold text-neutral-950 dark:text-neutral-50">
|
||||||
{user?.name ||
|
{user?.name || user?.display_name || user?.displayName || fallbackName}
|
||||||
user?.display_name ||
|
|
||||||
user?.displayName ||
|
|
||||||
displayNpub(pubkey, 16)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-auto inline-flex items-center gap-3">
|
<div className="ml-auto inline-flex items-center gap-3">
|
||||||
<div className="text-neutral-500 dark:text-neutral-400">{createdAt}</div>
|
<div className="text-neutral-500 dark:text-neutral-400">{createdAt}</div>
|
||||||
@@ -530,11 +543,11 @@ export const User = memo(function User({
|
|||||||
alt={pubkey}
|
alt={pubkey}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
className="h-10 w-10 rounded-lg"
|
className="h-10 w-10 rounded-lg object-cover"
|
||||||
/>
|
/>
|
||||||
<Avatar.Fallback delayMs={300}>
|
<Avatar.Fallback delayMs={300}>
|
||||||
<img
|
<img
|
||||||
src={svgURI}
|
src={fallbackAvatar}
|
||||||
alt={pubkey}
|
alt={pubkey}
|
||||||
className="h-10 w-10 rounded-lg bg-black dark:bg-white"
|
className="h-10 w-10 rounded-lg bg-black dark:bg-white"
|
||||||
/>
|
/>
|
||||||
@@ -551,12 +564,12 @@ export const User = memo(function User({
|
|||||||
{user?.nip05 ? (
|
{user?.nip05 ? (
|
||||||
<NIP05
|
<NIP05
|
||||||
pubkey={pubkey}
|
pubkey={pubkey}
|
||||||
nip05={user?.nip05}
|
nip05={user.nip05}
|
||||||
className="max-w-[15rem] truncate text-sm text-neutral-500 dark:text-neutral-300"
|
className="max-w-[15rem] truncate text-sm text-neutral-500 dark:text-neutral-300"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<span className="max-w-[15rem] truncate text-sm text-neutral-500 dark:text-neutral-300">
|
<span className="max-w-[15rem] truncate text-sm text-neutral-500 dark:text-neutral-300">
|
||||||
{displayNpub(pubkey, 16)}
|
{fallbackName}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (db.account.follows.includes(pubkey)) {
|
if (db.account.contacts.includes(pubkey)) {
|
||||||
setFollowed(true);
|
setFollowed(true);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ import { MemoizedArticleNote } from '@shared/notes';
|
|||||||
import { TitleBar } from '@shared/titleBar';
|
import { TitleBar } from '@shared/titleBar';
|
||||||
import { WidgetWrapper } from '@shared/widgets';
|
import { WidgetWrapper } from '@shared/widgets';
|
||||||
|
|
||||||
import { FETCH_LIMIT } from '@stores/constants';
|
import { FETCH_LIMIT } from '@utils/constants';
|
||||||
|
|
||||||
import { Widget } from '@utils/types';
|
import { Widget } from '@utils/types';
|
||||||
|
|
||||||
export function ArticleWidget({ widget }: { widget: Widget }) {
|
export function ArticleWidget({ widget }: { widget: Widget }) {
|
||||||
@@ -40,7 +39,7 @@ export function ArticleWidget({ widget }: { widget: Widget }) {
|
|||||||
} else {
|
} else {
|
||||||
filter = {
|
filter = {
|
||||||
kinds: [NDKKind.Article],
|
kinds: [NDKKind.Article],
|
||||||
authors: db.account.circles,
|
authors: db.account.contacts,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ import { MemoizedFileNote } from '@shared/notes';
|
|||||||
import { TitleBar } from '@shared/titleBar';
|
import { TitleBar } from '@shared/titleBar';
|
||||||
import { WidgetWrapper } from '@shared/widgets';
|
import { WidgetWrapper } from '@shared/widgets';
|
||||||
|
|
||||||
import { FETCH_LIMIT } from '@stores/constants';
|
import { FETCH_LIMIT } from '@utils/constants';
|
||||||
|
|
||||||
import { Widget } from '@utils/types';
|
import { Widget } from '@utils/types';
|
||||||
|
|
||||||
export function FileWidget({ widget }: { widget: Widget }) {
|
export function FileWidget({ widget }: { widget: Widget }) {
|
||||||
@@ -40,7 +39,7 @@ export function FileWidget({ widget }: { widget: Widget }) {
|
|||||||
} else {
|
} else {
|
||||||
filter = {
|
filter = {
|
||||||
kinds: [1063],
|
kinds: [1063],
|
||||||
authors: db.account.circles,
|
authors: db.account.contacts,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ import {
|
|||||||
import { TitleBar } from '@shared/titleBar';
|
import { TitleBar } from '@shared/titleBar';
|
||||||
import { WidgetWrapper } from '@shared/widgets';
|
import { WidgetWrapper } from '@shared/widgets';
|
||||||
|
|
||||||
import { FETCH_LIMIT } from '@stores/constants';
|
import { FETCH_LIMIT } from '@utils/constants';
|
||||||
|
|
||||||
import { Widget } from '@utils/types';
|
import { Widget } from '@utils/types';
|
||||||
|
|
||||||
export function GroupWidget({ widget }: { widget: Widget }) {
|
export function GroupWidget({ widget }: { widget: Widget }) {
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ import { MemoizedRepost, MemoizedTextNote, UnknownNote } from '@shared/notes';
|
|||||||
import { TitleBar } from '@shared/titleBar';
|
import { TitleBar } from '@shared/titleBar';
|
||||||
import { WidgetWrapper } from '@shared/widgets';
|
import { WidgetWrapper } from '@shared/widgets';
|
||||||
|
|
||||||
import { FETCH_LIMIT } from '@stores/constants';
|
import { FETCH_LIMIT } from '@utils/constants';
|
||||||
|
|
||||||
import { Widget } from '@utils/types';
|
import { Widget } from '@utils/types';
|
||||||
|
|
||||||
export function HashtagWidget({ widget }: { widget: Widget }) {
|
export function HashtagWidget({ widget }: { widget: Widget }) {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
import { TitleBar } from '@shared/titleBar';
|
import { TitleBar } from '@shared/titleBar';
|
||||||
import { LiveUpdater, WidgetWrapper } from '@shared/widgets';
|
import { LiveUpdater, WidgetWrapper } from '@shared/widgets';
|
||||||
|
|
||||||
import { FETCH_LIMIT } from '@stores/constants';
|
import { FETCH_LIMIT } from '@utils/constants';
|
||||||
|
|
||||||
export function NewsfeedWidget() {
|
export function NewsfeedWidget() {
|
||||||
const { db } = useStorage();
|
const { db } = useStorage();
|
||||||
@@ -39,7 +39,7 @@ export function NewsfeedWidget() {
|
|||||||
relayUrls,
|
relayUrls,
|
||||||
{
|
{
|
||||||
kinds: [NDKKind.Text, NDKKind.Repost],
|
kinds: [NDKKind.Text, NDKKind.Repost],
|
||||||
authors: db.account.circles,
|
authors: db.account.contacts,
|
||||||
},
|
},
|
||||||
FETCH_LIMIT,
|
FETCH_LIMIT,
|
||||||
{ asOf: pageParam === 0 ? undefined : pageParam, abortSignal: signal }
|
{ asOf: pageParam === 0 ? undefined : pageParam, abortSignal: signal }
|
||||||
|
|||||||
@@ -11,8 +11,7 @@ import { MemoizedNotifyNote, NoteSkeleton } from '@shared/notes';
|
|||||||
import { TitleBar } from '@shared/titleBar';
|
import { TitleBar } from '@shared/titleBar';
|
||||||
import { WidgetWrapper } from '@shared/widgets';
|
import { WidgetWrapper } from '@shared/widgets';
|
||||||
|
|
||||||
import { FETCH_LIMIT } from '@stores/constants';
|
import { FETCH_LIMIT } from '@utils/constants';
|
||||||
|
|
||||||
import { useNostr } from '@utils/hooks/useNostr';
|
import { useNostr } from '@utils/hooks/useNostr';
|
||||||
import { sendNativeNotification } from '@utils/notification';
|
import { sendNativeNotification } from '@utils/notification';
|
||||||
|
|
||||||
@@ -54,7 +53,6 @@ export function NotificationWidget() {
|
|||||||
if (!lastEvent) return;
|
if (!lastEvent) return;
|
||||||
return lastEvent.created_at - 1;
|
return lastEvent.created_at - 1;
|
||||||
},
|
},
|
||||||
enabled: false,
|
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
refetchOnMount: false,
|
refetchOnMount: false,
|
||||||
refetchOnReconnect: false,
|
refetchOnReconnect: false,
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ import {
|
|||||||
} from '@shared/icons';
|
} from '@shared/icons';
|
||||||
import { User } from '@shared/user';
|
import { User } from '@shared/user';
|
||||||
|
|
||||||
import { WIDGET_KIND } from '@stores/constants';
|
import { WIDGET_KIND } from '@utils/constants';
|
||||||
|
|
||||||
import { useWidget } from '@utils/hooks/useWidget';
|
import { useWidget } from '@utils/hooks/useWidget';
|
||||||
|
|
||||||
export function AddGroupFeeds({ currentWidgetId }: { currentWidgetId: string }) {
|
export function AddGroupFeeds({ currentWidgetId }: { currentWidgetId: string }) {
|
||||||
@@ -96,7 +95,7 @@ export function AddGroupFeeds({ currentWidgetId }: { currentWidgetId: string })
|
|||||||
Users
|
Users
|
||||||
</span>
|
</span>
|
||||||
<div className="flex h-[420px] flex-col overflow-y-auto rounded-xl bg-neutral-100 py-2 dark:bg-neutral-900">
|
<div className="flex h-[420px] flex-col overflow-y-auto rounded-xl bg-neutral-100 py-2 dark:bg-neutral-900">
|
||||||
{db.account.circles.map((item: string) => (
|
{db.account.contacts.map((item: string) => (
|
||||||
<button
|
<button
|
||||||
key={item}
|
key={item}
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ import { Resolver, useForm } from 'react-hook-form';
|
|||||||
|
|
||||||
import { CancelIcon, GroupFeedsIcon, PlusIcon } from '@shared/icons';
|
import { CancelIcon, GroupFeedsIcon, PlusIcon } from '@shared/icons';
|
||||||
|
|
||||||
import { HASHTAGS, WIDGET_KIND } from '@stores/constants';
|
import { HASHTAGS, WIDGET_KIND } from '@utils/constants';
|
||||||
|
|
||||||
import { useWidget } from '@utils/hooks/useWidget';
|
import { useWidget } from '@utils/hooks/useWidget';
|
||||||
|
|
||||||
type FormValues = {
|
type FormValues = {
|
||||||
|
|||||||
@@ -30,12 +30,10 @@ export function LiveUpdater({ status }: { status: QueryStatus }) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let sub: NDKSubscription = undefined;
|
let sub: NDKSubscription = undefined;
|
||||||
|
|
||||||
if (status === 'success' && db.account && db.account.circles.length > 0) {
|
if (status === 'success' && db.account && db.account?.contacts?.length > 0) {
|
||||||
queryClient.fetchQuery({ queryKey: ['notification'] });
|
|
||||||
|
|
||||||
const filter: NDKFilter = {
|
const filter: NDKFilter = {
|
||||||
kinds: [NDKKind.Text, NDKKind.Repost],
|
kinds: [NDKKind.Text, NDKKind.Repost],
|
||||||
authors: db.account.circles,
|
authors: db.account.contacts,
|
||||||
since: Math.floor(Date.now() / 1000),
|
since: Math.floor(Date.now() / 1000),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { NDKEvent, NDKKind, NDKUser } from '@nostr-dev-kit/ndk';
|
import { NDKUser } from '@nostr-dev-kit/ndk';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
@@ -6,7 +6,7 @@ import { toast } from 'sonner';
|
|||||||
import { useNDK } from '@libs/ndk/provider';
|
import { useNDK } from '@libs/ndk/provider';
|
||||||
import { useStorage } from '@libs/storage/provider';
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
import { FollowIcon, UnfollowIcon } from '@shared/icons';
|
import { FollowIcon } from '@shared/icons';
|
||||||
|
|
||||||
import { shortenKey } from '@utils/shortenKey';
|
import { shortenKey } from '@utils/shortenKey';
|
||||||
|
|
||||||
@@ -16,58 +16,35 @@ export interface Profile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function NostrBandUserProfile({ data }: { data: Profile }) {
|
export function NostrBandUserProfile({ data }: { data: Profile }) {
|
||||||
const embedProfile = data.profile ? JSON.parse(data.profile.content) : null;
|
|
||||||
const profile = embedProfile;
|
|
||||||
|
|
||||||
const { db } = useStorage();
|
const { db } = useStorage();
|
||||||
const { ndk } = useNDK();
|
const { ndk } = useNDK();
|
||||||
|
|
||||||
const [followed, setFollowed] = useState(false);
|
const [followed, setFollowed] = useState(false);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const profile = data.profile ? JSON.parse(data.profile.content) : null;
|
||||||
|
|
||||||
const follow = async (pubkey: string) => {
|
const follow = async (pubkey: string) => {
|
||||||
try {
|
try {
|
||||||
|
if (!ndk.signer) return navigate('/new/privkey');
|
||||||
|
setFollowed(true);
|
||||||
|
|
||||||
const user = ndk.getUser({ pubkey: db.account.pubkey });
|
const user = ndk.getUser({ pubkey: db.account.pubkey });
|
||||||
const contacts = await user.follows();
|
const contacts = await user.follows();
|
||||||
const add = await user.follow(new NDKUser({ pubkey: pubkey }), contacts);
|
const add = await user.follow(new NDKUser({ pubkey: pubkey }), contacts);
|
||||||
|
|
||||||
if (add) {
|
if (!add) {
|
||||||
setFollowed(true);
|
toast.success('You already follow this user');
|
||||||
} else {
|
|
||||||
toast('You already follow this user');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const unfollow = async (pubkey: string) => {
|
|
||||||
try {
|
|
||||||
if (!ndk.signer) return navigate('/new/privkey');
|
|
||||||
|
|
||||||
const user = ndk.getUser({ pubkey: db.account.pubkey });
|
|
||||||
const contacts = await user.follows();
|
|
||||||
contacts.delete(new NDKUser({ pubkey: pubkey }));
|
|
||||||
|
|
||||||
let list: string[][];
|
|
||||||
contacts.forEach((el) => list.push(['p', el.pubkey, el.relayUrls?.[0] || '', '']));
|
|
||||||
|
|
||||||
const event = new NDKEvent(ndk);
|
|
||||||
event.content = '';
|
|
||||||
event.kind = NDKKind.Contacts;
|
|
||||||
event.tags = list;
|
|
||||||
|
|
||||||
const publishedRelays = await event.publish();
|
|
||||||
if (publishedRelays) {
|
|
||||||
setFollowed(false);
|
setFollowed(false);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (e) {
|
||||||
console.log(error);
|
toast.error(e);
|
||||||
|
setFollowed(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (db.account.follows.includes(data.pubkey)) {
|
if (db.account.contacts.includes(data.pubkey)) {
|
||||||
setFollowed(true);
|
setFollowed(true);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
@@ -100,15 +77,7 @@ export function NostrBandUserProfile({ data }: { data: Profile }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="inline-flex items-center gap-2">
|
<div className="inline-flex items-center gap-2">
|
||||||
{followed ? (
|
{!followed ? (
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => unfollow(data.pubkey)}
|
|
||||||
className="inline-flex h-8 w-8 items-center justify-center rounded-md bg-neutral-200 text-neutral-900 backdrop-blur-xl hover:bg-blue-600 hover:text-white dark:bg-neutral-800 dark:text-neutral-100 dark:hover:text-white"
|
|
||||||
>
|
|
||||||
<UnfollowIcon className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => follow(data.pubkey)}
|
onClick={() => follow(data.pubkey)}
|
||||||
@@ -116,7 +85,7 @@ export function NostrBandUserProfile({ data }: { data: Profile }) {
|
|||||||
>
|
>
|
||||||
<FollowIcon className="h-4 w-4" />
|
<FollowIcon className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 line-clamp-5 whitespace-pre-line break-all text-neutral-900 dark:text-neutral-100">
|
<div className="mt-2 line-clamp-5 whitespace-pre-line break-all text-neutral-900 dark:text-neutral-100">
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user