Compare commits

...

54 Commits

Author SHA1 Message Date
f2b1458bd2 bump version & fix using nsecbunker with token 2023-12-07 18:49:55 +07:00
0d43c13928 bump version 2023-12-07 11:55:49 +07:00
a42a2788ea fix nip-05 2023-12-06 10:15:41 +07:00
e30274dab3 fix typo 2023-12-06 09:07:32 +07:00
Ren Amamiya
740b7588bc Merge pull request #127 from luminous-devs/hotfix/2.2.1
v2.2.1
2023-12-06 08:09:44 +07:00
24c2ed4eb2 update 2023-12-06 08:08:11 +07:00
4006c0010e polish 2023-12-05 15:31:45 +07:00
7decf264d7 polish nsecbunker 2023-12-05 14:25:44 +07:00
482b218f74 improve error handling for useevent hook 2023-12-05 09:42:08 +07:00
e06b760e41 bump version 2023-12-04 14:14:50 +07:00
7efc35f622 replace media chrome with vidstack 2023-12-04 14:02:07 +07:00
8795923443 update 2023-12-04 13:36:16 +07:00
4093821fd0 clean up 2023-12-04 12:47:29 +07:00
b19637bdb7 remove misconfigure in react query 2023-12-04 12:17:16 +07:00
21e758ec13 update user component 2023-12-04 11:49:52 +07:00
48ab123850 improve relay screen 2023-12-04 09:26:19 +07:00
a401070031 update dependencies 2023-12-04 08:46:42 +07:00
e5e4109e79 bump version 2023-12-03 08:38:32 +07:00
Ren Amamiya
d62c814f33 Merge pull request #125 from luminous-devs/next
Lume v2.2.0
2023-12-03 08:37:37 +07:00
2a92b7c202 polish 2023-12-03 08:34:44 +07:00
255dcb43fe improve relay form 2023-12-02 17:53:45 +07:00
a528b646e3 add finish step to tutorial 2023-12-02 08:33:23 +07:00
fc35745c0d wip: tutorial 2023-12-01 15:45:43 +07:00
9ddf3471ce fix nsecbunker 2023-12-01 08:23:46 +07:00
8355ad6863 auto connect user relays 2023-11-30 20:15:54 +07:00
217ac490b1 fix outbox 2023-11-30 19:22:00 +07:00
092cf49227 improve relay connection 2023-11-30 18:19:24 +07:00
5318f6c4cb clean up 2023-11-30 17:24:07 +07:00
80f675cb54 improve notification and performance 2023-11-30 16:02:28 +07:00
6f68c2762b add prefetch data 2023-11-30 10:35:08 +07:00
f4390b29e2 revamp onboarding and launching process 2023-11-30 09:38:58 +07:00
00e4f9d357 clean up dependencies 2023-11-28 15:36:57 +07:00
d28d183620 Merge branch 'main' into next 2023-11-28 09:07:14 +07:00
3c6c9c86d1 2.1.7 2023-11-28 09:00:46 +07:00
bcd079c88e update dependencies 2023-11-28 08:34:21 +07:00
Ren Amamiya
d989d6ffad Merge pull request #123 from luminous-devs/feat/v2.1.6
Feat/v2.1.6
2023-11-27 12:01:25 +07:00
5229458746 bump version 2023-11-27 09:56:43 +07:00
2bfa1db816 update 2023-11-27 09:48:51 +07:00
8439428ce1 fix crash on settings screen 2023-11-26 15:01:13 +07:00
34dceef4a3 fix mention popup 2023-11-26 07:48:28 +07:00
Ren Amamiya
619bfb8dff Merge pull request #122 from luminous-devs/v2.1.4
v2.1.4
2023-11-26 07:22:13 +07:00
7759851541 clean up 2023-11-26 07:21:24 +07:00
9112c1c24a improve connection 2023-11-25 17:56:45 +07:00
24b21a9451 update 2023-11-25 16:03:05 +07:00
31a53b9c48 add @ suggestion popup 2023-11-25 15:41:18 +07:00
dc229f40cb fix new article layout 2023-11-25 11:07:31 +07:00
54ad1e6e1d fix new post layout 2023-11-25 09:22:15 +07:00
Ren Amamiya
065ccbbea4 Merge pull request #121 from luminous-devs/fix/nsecbunker
Fix stuck issue for connect with nsecbunker
2023-11-24 13:53:26 +07:00
74738c36cd disable blockUntilReady 2023-11-23 15:12:46 +07:00
Ren Amamiya
2fdf437789 Merge pull request #120 from luminous-devs/fix/logout
Fix logout function and other issues
2023-11-23 08:54:24 +07:00
731c72535c bump version 2023-11-23 08:52:47 +07:00
628102087e fix total account count function 2023-11-23 08:52:04 +07:00
536ea30ed2 fix logout function 2023-11-23 08:49:05 +07:00
8ee38cdb42 temp disable single-instance plugin 2023-11-22 17:27:09 +07:00
114 changed files with 3542 additions and 2990 deletions

View File

@@ -11,7 +11,6 @@
"^@app/(.*)$", "^@app/(.*)$",
"^@libs/(.*)$", "^@libs/(.*)$",
"^@shared/(.*)$", "^@shared/(.*)$",
"^@stores/(.*)$",
"^@utils/(.*)$", "^@utils/(.*)$",
"^[./]" "^[./]"
], ],

View File

@@ -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>

View File

@@ -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

File diff suppressed because it is too large Load Diff

312
src-tauri/Cargo.lock generated
View File

@@ -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",

View File

@@ -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 = [

View 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;

View File

@@ -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())

View File

@@ -9,7 +9,7 @@
}, },
"package": { "package": {
"productName": "Lume", "productName": "Lume",
"version": "2.1.2" "version": "2.2.3"
}, },
"plugins": { "plugins": {
"fs": { "fs": {

View File

@@ -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 {

View File

@@ -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 }}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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&apos;s outbox relays and subsequent queries will favour using those
relays for queries with that user&apos;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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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&apos;s set up your account. Let&apos;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 ? ( &quot;Settings&quot; 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
View 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&apos;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
View 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>
);
}

View File

@@ -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&apos;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
View 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&apos;re almost ready to use Lume.
</h1>
<h2 className="text-xl font-semibold text-neutral-900 dark:text-neutral-100">
Let&apos;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 &quot;Settings&quot;
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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -1,5 +0,0 @@
import { Outlet } from 'react-router-dom';
export function OnboardingScreen() {
return <Outlet />;
}

View File

@@ -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&apos;re almost ready to use Lume.
</h1>
<h2 className="text-xl font-semibold text-neutral-900 dark:text-neutral-100">
Let&apos;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>
);
}

View File

@@ -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&apos;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>
);
}

View 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&apos;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&apos;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>
);
}

View 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
&apos;Notes.&apos; 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&apos;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>
);
}

View File

@@ -0,0 +1,3 @@
export function TutorialPostingScreen() {
return <div></div>;
}

View 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>
);
}

View File

@@ -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>

View File

@@ -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"

View File

@@ -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" />

View File

@@ -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&apos;s Devs to release the bug fixes, you always can use While waiting for Lume&apos;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>

View File

@@ -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" />;
} }

View File

@@ -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>

View File

@@ -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);

View File

@@ -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];

View File

@@ -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>

View File

@@ -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';

View 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);

View File

@@ -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>

View File

@@ -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)}

View File

@@ -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>

View File

@@ -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

View File

@@ -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 (

View File

@@ -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 = () => {

View File

@@ -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>

View File

@@ -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&apos;re using nostr wallet connect</div> <div>You&apos;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>

View File

@@ -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>
); );
} }

View File

@@ -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>
); );
} }

View File

@@ -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" />

View File

@@ -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>
);
}

View 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>
);
}

View File

@@ -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>
); );
} }

View File

@@ -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"

View File

@@ -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>

View File

@@ -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" />

View File

@@ -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),
}));
}); });
} }

View File

@@ -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>

View File

@@ -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,
}; };
}; };

View File

@@ -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;

View File

@@ -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() {

View File

@@ -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;

View File

@@ -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 />

View File

@@ -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

View File

@@ -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>

View File

@@ -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 (

View File

@@ -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>
); );
} }

View File

@@ -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>
); );

View File

@@ -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>
); );

View File

@@ -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(

View File

@@ -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>
); );

View File

@@ -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({

View File

@@ -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&apos;re using nostr wallet connect You&apos;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 />

View File

@@ -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">

View File

@@ -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 (

View File

@@ -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>
); );
} }

View File

@@ -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>

View File

@@ -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 }) {

View File

@@ -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">

View File

@@ -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>
); );
}); });

View File

@@ -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);

View File

@@ -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>
); );
} }

View File

@@ -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">

View File

@@ -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 ? (

View File

@@ -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}

View File

@@ -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>

View File

@@ -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);
} }
}, []); }, []);

View File

@@ -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,
}; };
} }

View File

@@ -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,
}; };
} }

View File

@@ -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 }) {

View File

@@ -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 }) {

View File

@@ -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 }

View File

@@ -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,

View File

@@ -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"

View File

@@ -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 = {

View File

@@ -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),
}; };

View File

@@ -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