Merge pull request #130 from luminous-devs/feat/ark

Introduction Ark
This commit is contained in:
Ren Amamiya
2023-12-10 11:19:04 +07:00
committed by GitHub
101 changed files with 2743 additions and 3523 deletions

View File

@@ -33,8 +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.12.2", "@tanstack/react-query": "^5.13.4",
"@tanstack/react-query-devtools": "^5.12.2", "@tanstack/react-query-devtools": "^5.13.4",
"@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",
@@ -60,9 +60,9 @@
"@tiptap/react": "^2.1.13", "@tiptap/react": "^2.1.13",
"@tiptap/starter-kit": "^2.1.13", "@tiptap/starter-kit": "^2.1.13",
"@tiptap/suggestion": "^2.1.13", "@tiptap/suggestion": "^2.1.13",
"@vidstack/react": "^1.8.3", "@vidstack/react": "^1.9.2",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"framer-motion": "^10.16.12", "framer-motion": "^10.16.16",
"html-to-text": "^9.0.5", "html-to-text": "^9.0.5",
"light-bolt11-decoder": "^3.0.0", "light-bolt11-decoder": "^3.0.0",
"lru-cache": "^10.1.0", "lru-cache": "^10.1.0",
@@ -76,16 +76,15 @@
"react": "^18.2.0", "react": "^18.2.0",
"react-currency-input-field": "^3.6.12", "react-currency-input-field": "^3.6.12",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-hook-form": "^7.48.2", "react-hook-form": "^7.49.0",
"react-hotkeys-hook": "^4.4.1", "react-hotkeys-hook": "^4.4.1",
"react-router-dom": "^6.20.1", "react-router-dom": "^6.20.1",
"react-string-replace": "^1.1.1", "react-string-replace": "^1.1.1",
"reactflow": "^11.10.1",
"sonner": "^1.2.4", "sonner": "^1.2.4",
"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.8", "tiptap-markdown": "^0.8.8",
"virtua": "^0.17.4", "virtua": "^0.17.5",
"zustand": "^4.4.7" "zustand": "^4.4.7"
}, },
"devDependencies": { "devDependencies": {
@@ -93,11 +92,11 @@
"@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.10.3", "@types/node": "^20.10.4",
"@types/react": "^18.2.41", "@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17", "@types/react-dom": "^18.2.17",
"@typescript-eslint/eslint-plugin": "^6.13.1", "@typescript-eslint/eslint-plugin": "^6.13.2",
"@typescript-eslint/parser": "^6.13.1", "@typescript-eslint/parser": "^6.13.2",
"@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",
@@ -112,14 +111,14 @@
"lint-staged": "^15.2.0", "lint-staged": "^15.2.0",
"postcss": "^8.4.32", "postcss": "^8.4.32",
"prettier": "^3.1.0", "prettier": "^3.1.0",
"prettier-plugin-tailwindcss": "^0.5.7", "prettier-plugin-tailwindcss": "^0.5.9",
"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", "tailwind-scrollbar": "^3.0.5",
"tailwindcss": "^3.3.5", "tailwindcss": "^3.3.6",
"typescript": "^5.3.2", "typescript": "^5.3.3",
"vite": "^4.5.0", "vite": "^4.5.1",
"vite-plugin-top-level-await": "^1.3.1", "vite-plugin-top-level-await": "^1.3.1",
"vite-tsconfig-paths": "^4.2.1" "vite-tsconfig-paths": "^4.2.2"
} }
} }

1215
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

294
src-tauri/Cargo.lock generated
View File

@@ -100,9 +100,9 @@ dependencies = [
[[package]] [[package]]
name = "anstream" name = "anstream"
version = "0.6.4" version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" checksum = "d664a92ecae85fd0a7392615844904654d1d5f5514837f471ddef4a057aba1b6"
dependencies = [ dependencies = [
"anstyle", "anstyle",
"anstyle-parse", "anstyle-parse",
@@ -120,30 +120,30 @@ checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87"
[[package]] [[package]]
name = "anstyle-parse" name = "anstyle-parse"
version = "0.2.2" version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c"
dependencies = [ dependencies = [
"utf8parse", "utf8parse",
] ]
[[package]] [[package]]
name = "anstyle-query" name = "anstyle-query"
version = "1.0.0" version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648"
dependencies = [ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
name = "anstyle-wincon" name = "anstyle-wincon"
version = "3.0.1" version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7"
dependencies = [ dependencies = [
"anstyle", "anstyle",
"windows-sys 0.48.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -200,11 +200,11 @@ 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 = "17ae5ebefcc48e7452b4987947920dac9450be1110cadf34d1b8c116bdbaf97c" checksum = "17ae5ebefcc48e7452b4987947920dac9450be1110cadf34d1b8c116bdbaf97c"
dependencies = [ dependencies = [
"async-lock 3.1.2", "async-lock 3.2.0",
"async-task", "async-task",
"concurrent-queue", "concurrent-queue",
"fastrand 2.0.1", "fastrand 2.0.1",
"futures-lite 2.0.1", "futures-lite 2.1.0",
"slab", "slab",
] ]
@@ -246,14 +246,14 @@ 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 = "d6d3b15875ba253d1110c740755e246537483f152fa334f91abd7fe84c88b3ff" checksum = "d6d3b15875ba253d1110c740755e246537483f152fa334f91abd7fe84c88b3ff"
dependencies = [ dependencies = [
"async-lock 3.1.2", "async-lock 3.2.0",
"cfg-if", "cfg-if",
"concurrent-queue", "concurrent-queue",
"futures-io", "futures-io",
"futures-lite 2.0.1", "futures-lite 2.1.0",
"parking", "parking",
"polling 3.3.1", "polling 3.3.1",
"rustix 0.38.26", "rustix 0.38.28",
"slab", "slab",
"tracing", "tracing",
"windows-sys 0.52.0", "windows-sys 0.52.0",
@@ -270,9 +270,9 @@ dependencies = [
[[package]] [[package]]
name = "async-lock" name = "async-lock"
version = "3.1.2" version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dea8b3453dd7cc96711834b75400d671b73e3656975fa68d9f277163b7f7e316" checksum = "7125e42787d53db9dd54261812ef17e937c95a51e4d291373b670342fa44310c"
dependencies = [ dependencies = [
"event-listener 4.0.0", "event-listener 4.0.0",
"event-listener-strategy", "event-listener-strategy",
@@ -292,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.26", "rustix 0.38.28",
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
@@ -319,7 +319,7 @@ dependencies = [
"cfg-if", "cfg-if",
"futures-core", "futures-core",
"futures-io", "futures-io",
"rustix 0.38.26", "rustix 0.38.28",
"signal-hook-registry", "signal-hook-registry",
"slab", "slab",
"windows-sys 0.48.0", "windows-sys 0.48.0",
@@ -349,7 +349,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4af014b17dd80e8af9fa689b2d4a211ddba6eb583c1622f35d0cb543f6b17e4" checksum = "b4af014b17dd80e8af9fa689b2d4a211ddba6eb583c1622f35d0cb543f6b17e4"
dependencies = [ dependencies = [
"atk-sys", "atk-sys",
"glib 0.18.3", "glib 0.18.4",
"libc", "libc",
] ]
@@ -510,11 +510,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118" checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118"
dependencies = [ dependencies = [
"async-channel", "async-channel",
"async-lock 3.1.2", "async-lock 3.2.0",
"async-task", "async-task",
"fastrand 2.0.1", "fastrand 2.0.1",
"futures-io", "futures-io",
"futures-lite 2.0.1", "futures-lite 2.1.0",
"piper", "piper",
"tracing", "tracing",
] ]
@@ -596,7 +596,7 @@ checksum = "f33613627f0dea6a731b0605101fad59ba4f193a52c96c4687728d822605a8a1"
dependencies = [ dependencies = [
"bitflags 2.4.1", "bitflags 2.4.1",
"cairo-sys-rs", "cairo-sys-rs",
"glib 0.18.3", "glib 0.18.4",
"libc", "libc",
"once_cell", "once_cell",
"thiserror", "thiserror",
@@ -651,7 +651,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d1ece59890e746567b467253aea0adbe8a21784d0b025d8a306f66c391c2957" checksum = "4d1ece59890e746567b467253aea0adbe8a21784d0b025d8a306f66c391c2957"
dependencies = [ dependencies = [
"serde", "serde",
"toml 0.8.8", "toml 0.8.2",
] ]
[[package]] [[package]]
@@ -731,9 +731,9 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.4.10" version = "4.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fffed7514f420abec6d183b1d3acfd9099c79c3a10a06ade4f8203f1411272" checksum = "bfaff671f6b22ca62406885ece523383b9b64022e341e53e009a62ebc47a45f2"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@@ -741,9 +741,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.4.9" version = "4.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63361bae7eef3771745f02d8d892bec2fee5f6e34af316ba556e7f97a7069ff1" checksum = "a216b506622bb1d316cd51328dce24e07bdff4a6128a47c7e7fad11878d5adbb"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
@@ -850,9 +850,9 @@ dependencies = [
[[package]] [[package]]
name = "concurrent-queue" name = "concurrent-queue"
version = "2.3.0" version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f057a694a54f12365049b0958a1685bb52d567f5593b355fbf685838e873d400" checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363"
dependencies = [ dependencies = [
"crossbeam-utils", "crossbeam-utils",
] ]
@@ -1067,9 +1067,9 @@ dependencies = [
[[package]] [[package]]
name = "curl-sys" name = "curl-sys"
version = "0.4.68+curl-8.4.0" version = "0.4.70+curl-8.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4a0d18d88360e374b16b2273c832b5e57258ffc1d4aa4f96b108e0738d5752f" checksum = "3c0333d8849afe78a4c8102a429a446bfdd055832af071945520e835ae2d841e"
dependencies = [ dependencies = [
"cc", "cc",
"libc", "libc",
@@ -1134,9 +1134,9 @@ dependencies = [
[[package]] [[package]]
name = "deranged" name = "deranged"
version = "0.3.9" version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3" checksum = "8eb30d70a07a3b04884d2677f06bec33509dc67ca60d92949e5535352d3191dc"
dependencies = [ dependencies = [
"powerfmt", "powerfmt",
"serde", "serde",
@@ -1269,7 +1269,7 @@ checksum = "f54cc3e827ee1c3812239a9a41dede7b4d7d5d5464faa32d71bd7cba28ce2cb2"
dependencies = [ dependencies = [
"cc", "cc",
"rustc_version", "rustc_version",
"toml 0.8.8", "toml 0.8.2",
"vswhom", "vswhom",
"winreg 0.51.0", "winreg 0.51.0",
] ]
@@ -1419,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.26", "rustix 0.38.28",
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
@@ -1444,14 +1444,14 @@ dependencies = [
[[package]] [[package]]
name = "filetime" name = "filetime"
version = "0.2.22" version = "0.2.23"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4029edd3e734da6fe05b6cd7bd2960760a616bd2ddd0d59a0124746d6272af0" checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
"redox_syscall 0.3.5", "redox_syscall",
"windows-sys 0.48.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -1624,14 +1624,13 @@ dependencies = [
[[package]] [[package]]
name = "futures-lite" name = "futures-lite"
version = "2.0.1" version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3831c2651acb5177cbd83943f3d9c8912c5ad03c76afcc0e9511ba568ec5ebb" checksum = "aeee267a1883f7ebef3700f262d2d54de95dfaf38189015a74fdc4e0c7ad8143"
dependencies = [ dependencies = [
"fastrand 2.0.1", "fastrand 2.0.1",
"futures-core", "futures-core",
"futures-io", "futures-io",
"memchr",
"parking", "parking",
"pin-project-lite", "pin-project-lite",
] ]
@@ -1696,7 +1695,7 @@ dependencies = [
"gdk-pixbuf", "gdk-pixbuf",
"gdk-sys", "gdk-sys",
"gio", "gio",
"glib 0.18.3", "glib 0.18.4",
"libc", "libc",
"pango", "pango",
] ]
@@ -1709,7 +1708,7 @@ checksum = "446f32b74d22c33b7b258d4af4ffde53c2bf96ca2e29abdf1a785fe59bd6c82c"
dependencies = [ dependencies = [
"gdk-pixbuf-sys", "gdk-pixbuf-sys",
"gio", "gio",
"glib 0.18.3", "glib 0.18.4",
"libc", "libc",
"once_cell", "once_cell",
] ]
@@ -1844,16 +1843,16 @@ checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
[[package]] [[package]]
name = "gio" name = "gio"
version = "0.18.3" version = "0.18.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47d809baf02bdf1b5ef4ad3bf60dd9d4977149db4612b7bbb58e56aef168193b" checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73"
dependencies = [ dependencies = [
"futures-channel", "futures-channel",
"futures-core", "futures-core",
"futures-io", "futures-io",
"futures-util", "futures-util",
"gio-sys 0.18.1", "gio-sys 0.18.1",
"glib 0.18.3", "glib 0.18.4",
"libc", "libc",
"once_cell", "once_cell",
"pin-project-lite", "pin-project-lite",
@@ -1911,9 +1910,9 @@ dependencies = [
[[package]] [[package]]
name = "glib" name = "glib"
version = "0.18.3" version = "0.18.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58cf801b6f7829fa76db37449ab67c9c98a2b1bf21076d9113225621e61a0fa6" checksum = "951bbd7fdc5c044ede9f05170f05a3ae9479239c3afdfe2d22d537a3add15c4e"
dependencies = [ dependencies = [
"bitflags 2.4.1", "bitflags 2.4.1",
"futures-channel", "futures-channel",
@@ -1954,7 +1953,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72793962ceece3863c2965d7f10c8786323b17c7adea75a515809fa20ab799a5" checksum = "72793962ceece3863c2965d7f10c8786323b17c7adea75a515809fa20ab799a5"
dependencies = [ dependencies = [
"heck", "heck",
"proc-macro-crate 2.0.0", "proc-macro-crate 2.0.1",
"proc-macro-error", "proc-macro-error",
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -2022,7 +2021,7 @@ dependencies = [
"gdk", "gdk",
"gdk-pixbuf", "gdk-pixbuf",
"gio", "gio",
"glib 0.18.3", "glib 0.18.4",
"gtk-sys", "gtk-sys",
"gtk3-macros", "gtk3-macros",
"libc", "libc",
@@ -2175,14 +2174,14 @@ checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb"
dependencies = [ dependencies = [
"bytes", "bytes",
"fnv", "fnv",
"itoa 1.0.9", "itoa 1.0.10",
] ]
[[package]] [[package]]
name = "http-body" name = "http-body"
version = "0.4.5" version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
dependencies = [ dependencies = [
"bytes", "bytes",
"http", "http",
@@ -2216,7 +2215,7 @@ dependencies = [
"http-body", "http-body",
"httparse", "httparse",
"httpdate", "httpdate",
"itoa 1.0.9", "itoa 1.0.10",
"pin-project-lite", "pin-project-lite",
"socket2 0.4.10", "socket2 0.4.10",
"tokio", "tokio",
@@ -2389,9 +2388,9 @@ dependencies = [
[[package]] [[package]]
name = "itertools" name = "itertools"
version = "0.11.0" version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" checksum = "25db6b064527c5d482d0423354fcd07a89a2dfe07b67892e62411946db7f07b0"
dependencies = [ dependencies = [
"either", "either",
] ]
@@ -2404,9 +2403,9 @@ checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.9" version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
[[package]] [[package]]
name = "javascriptcore-rs" name = "javascriptcore-rs"
@@ -2415,7 +2414,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc"
dependencies = [ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"glib 0.18.3", "glib 0.18.4",
"javascriptcore-rs-sys", "javascriptcore-rs-sys",
] ]
@@ -2502,9 +2501,9 @@ dependencies = [
[[package]] [[package]]
name = "keyring" name = "keyring"
version = "2.0.5" version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9549a129bd08149e0a71b2d1ce2729780d47127991bfd0a78cc1df697ec72492" checksum = "ec6488afbd1d8202dbd6e2dd38c0753d8c0adba9ac9985fc6f732a0d551f75e1"
dependencies = [ dependencies = [
"byteorder", "byteorder",
"lazy_static", "lazy_static",
@@ -2542,7 +2541,7 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a"
dependencies = [ dependencies = [
"glib 0.18.3", "glib 0.18.4",
"gtk", "gtk",
"gtk-sys", "gtk-sys",
"libappindicator-sys", "libappindicator-sys",
@@ -2590,7 +2589,7 @@ checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8"
dependencies = [ dependencies = [
"bitflags 2.4.1", "bitflags 2.4.1",
"libc", "libc",
"redox_syscall 0.4.1", "redox_syscall",
] ]
[[package]] [[package]]
@@ -2849,9 +2848,9 @@ dependencies = [
[[package]] [[package]]
name = "mio" name = "mio"
version = "0.8.9" version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09"
dependencies = [ dependencies = [
"libc", "libc",
"wasi 0.11.0+wasi-snapshot-preview1", "wasi 0.11.0+wasi-snapshot-preview1",
@@ -3188,9 +3187,9 @@ dependencies = [
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.18.0" version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]] [[package]]
name = "opaque-debug" name = "opaque-debug"
@@ -3211,9 +3210,9 @@ dependencies = [
[[package]] [[package]]
name = "openssl" name = "openssl"
version = "0.10.60" version = "0.10.61"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79a4c6c3a2b158f7f8f2a2fc5a969fa3a068df6fc9dbb4a43845436e3af7c800" checksum = "6b8419dc8cc6d866deb801274bba2e6f8f6108c1bb7fcc10ee5ab864931dbb45"
dependencies = [ dependencies = [
"bitflags 2.4.1", "bitflags 2.4.1",
"cfg-if", "cfg-if",
@@ -3243,18 +3242,18 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]] [[package]]
name = "openssl-src" name = "openssl-src"
version = "300.1.6+3.1.4" version = "300.2.0+3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439fac53e092cd7442a3660c85dde4643ab3b5bd39040912388dcdabf6b88085" checksum = "b1ebed1d188c4cd64c2bcd73d6c1fe1092f3d98c111831923cc1b706c3859fca"
dependencies = [ dependencies = [
"cc", "cc",
] ]
[[package]] [[package]]
name = "openssl-sys" name = "openssl-sys"
version = "0.9.96" version = "0.9.97"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3812c071ba60da8b5677cc12bcb1d42989a65553772897a7e0355545a819838f" checksum = "c3eaad34cdd97d81de97964fc7f29e2d104f483840d906ef56daa1912338460b"
dependencies = [ dependencies = [
"cc", "cc",
"libc", "libc",
@@ -3307,7 +3306,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4"
dependencies = [ dependencies = [
"gio", "gio",
"glib 0.18.3", "glib 0.18.4",
"libc", "libc",
"once_cell", "once_cell",
"pango-sys", "pango-sys",
@@ -3349,7 +3348,7 @@ checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
"redox_syscall 0.4.1", "redox_syscall",
"smallvec", "smallvec",
"windows-targets 0.48.5", "windows-targets 0.48.5",
] ]
@@ -3640,7 +3639,7 @@ dependencies = [
"cfg-if", "cfg-if",
"concurrent-queue", "concurrent-queue",
"pin-project-lite", "pin-project-lite",
"rustix 0.38.26", "rustix 0.38.28",
"tracing", "tracing",
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
@@ -3675,11 +3674,12 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro-crate" name = "proc-macro-crate"
version = "2.0.0" version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" checksum = "97dc5fea232fc28d2f597b37c4876b348a40e33f3b02cc975c8d006d78d94b1a"
dependencies = [ dependencies = [
"toml_edit 0.20.7", "toml_datetime",
"toml_edit 0.20.2",
] ]
[[package]] [[package]]
@@ -3865,15 +3865,6 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
] ]
[[package]]
name = "redox_syscall"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29"
dependencies = [
"bitflags 1.3.2",
]
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.4.1" version = "0.4.1"
@@ -3980,9 +3971,9 @@ dependencies = [
[[package]] [[package]]
name = "rfd" name = "rfd"
version = "0.12.1" version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c9e7b57df6e8472152674607f6cc68aa14a748a3157a857a94f516e11aeacc2" checksum = "241a0deb168c88050d872294f7b3106c1dfa8740942bcc97bc91b98e97b5c501"
dependencies = [ dependencies = [
"block", "block",
"dispatch", "dispatch",
@@ -4052,9 +4043,9 @@ dependencies = [
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "0.38.26" version = "0.38.28"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9470c4bf8246c8daf25f9598dca807fb6510347b1e1cfa55749113850c79d88a" checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316"
dependencies = [ dependencies = [
"bitflags 2.4.1", "bitflags 2.4.1",
"errno", "errno",
@@ -4095,9 +4086,9 @@ dependencies = [
[[package]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.15" version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c"
[[package]] [[package]]
name = "safemem" name = "safemem"
@@ -4232,7 +4223,7 @@ version = "1.0.108"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b"
dependencies = [ dependencies = [
"itoa 1.0.9", "itoa 1.0.10",
"ryu", "ryu",
"serde", "serde",
] ]
@@ -4264,7 +4255,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [ dependencies = [
"form_urlencoded", "form_urlencoded",
"itoa 1.0.9", "itoa 1.0.10",
"ryu", "ryu",
"serde", "serde",
] ]
@@ -4445,7 +4436,7 @@ checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f"
dependencies = [ dependencies = [
"futures-channel", "futures-channel",
"gio", "gio",
"glib 0.18.3", "glib 0.18.4",
"libc", "libc",
"soup3-sys", "soup3-sys",
] ]
@@ -4490,9 +4481,9 @@ dependencies = [
[[package]] [[package]]
name = "sqlformat" name = "sqlformat"
version = "0.2.2" version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b7b278788e7be4d0d29c0f39497a0eef3fba6bbc8e70d8bf7fde46edeaa9e85" checksum = "ce81b7bd7c4493975347ef60d8c7e8b742d4694f4c49f93e0a12ea263938176c"
dependencies = [ dependencies = [
"itertools", "itertools",
"nom", "nom",
@@ -4641,7 +4632,7 @@ dependencies = [
"hex", "hex",
"hkdf", "hkdf",
"hmac", "hmac",
"itoa 1.0.9", "itoa 1.0.10",
"log", "log",
"md-5", "md-5",
"memchr", "memchr",
@@ -4682,7 +4673,7 @@ dependencies = [
"hkdf", "hkdf",
"hmac", "hmac",
"home", "home",
"itoa 1.0.9", "itoa 1.0.10",
"log", "log",
"md-5", "md-5",
"memchr", "memchr",
@@ -4873,7 +4864,7 @@ dependencies = [
"cfg-expr", "cfg-expr",
"heck", "heck",
"pkg-config", "pkg-config",
"toml 0.8.8", "toml 0.8.2",
"version-compare", "version-compare",
] ]
@@ -4897,7 +4888,7 @@ dependencies = [
"gdkwayland-sys", "gdkwayland-sys",
"gdkx11-sys", "gdkx11-sys",
"gio", "gio",
"glib 0.18.3", "glib 0.18.4",
"glib-sys 0.18.1", "glib-sys 0.18.1",
"gtk", "gtk",
"image", "image",
@@ -5066,7 +5057,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#e7c72c9816d14a218e702dd233a6cfec957c2ee6" source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#a848e1870a71b7460a0c56ba44cc1af61d818483"
dependencies = [ dependencies = [
"auto-launch", "auto-launch",
"log", "log",
@@ -5079,7 +5070,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#e7c72c9816d14a218e702dd233a6cfec957c2ee6" source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#a848e1870a71b7460a0c56ba44cc1af61d818483"
dependencies = [ dependencies = [
"clap", "clap",
"log", "log",
@@ -5092,7 +5083,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#e7c72c9816d14a218e702dd233a6cfec957c2ee6" source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#a848e1870a71b7460a0c56ba44cc1af61d818483"
dependencies = [ dependencies = [
"arboard", "arboard",
"log", "log",
@@ -5106,7 +5097,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#e7c72c9816d14a218e702dd233a6cfec957c2ee6" source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#a848e1870a71b7460a0c56ba44cc1af61d818483"
dependencies = [ dependencies = [
"glib 0.16.9", "glib 0.16.9",
"log", "log",
@@ -5123,7 +5114,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#e7c72c9816d14a218e702dd233a6cfec957c2ee6" source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#a848e1870a71b7460a0c56ba44cc1af61d818483"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"glob", "glob",
@@ -5136,7 +5127,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#e7c72c9816d14a218e702dd233a6cfec957c2ee6" source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#a848e1870a71b7460a0c56ba44cc1af61d818483"
dependencies = [ dependencies = [
"data-url", "data-url",
"glob", "glob",
@@ -5153,7 +5144,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#e7c72c9816d14a218e702dd233a6cfec957c2ee6" source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#a848e1870a71b7460a0c56ba44cc1af61d818483"
dependencies = [ dependencies = [
"log", "log",
"notify-rust", "notify-rust",
@@ -5171,7 +5162,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#e7c72c9816d14a218e702dd233a6cfec957c2ee6" source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#a848e1870a71b7460a0c56ba44cc1af61d818483"
dependencies = [ dependencies = [
"gethostname 0.4.3", "gethostname 0.4.3",
"log", "log",
@@ -5187,7 +5178,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#e7c72c9816d14a218e702dd233a6cfec957c2ee6" source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#a848e1870a71b7460a0c56ba44cc1af61d818483"
dependencies = [ dependencies = [
"tauri", "tauri",
] ]
@@ -5195,7 +5186,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#e7c72c9816d14a218e702dd233a6cfec957c2ee6" source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#a848e1870a71b7460a0c56ba44cc1af61d818483"
dependencies = [ dependencies = [
"encoding_rs", "encoding_rs",
"log", "log",
@@ -5212,7 +5203,7 @@ dependencies = [
[[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#e7c72c9816d14a218e702dd233a6cfec957c2ee6" source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#a848e1870a71b7460a0c56ba44cc1af61d818483"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"log", "log",
@@ -5228,7 +5219,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#e7c72c9816d14a218e702dd233a6cfec957c2ee6" source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#a848e1870a71b7460a0c56ba44cc1af61d818483"
dependencies = [ dependencies = [
"log", "log",
"serde", "serde",
@@ -5244,7 +5235,7 @@ source = "git+https://github.com/wyhaya/tauri-plugin-theme#cccc9b3fbc308a475ef87
dependencies = [ dependencies = [
"cocoa 0.25.0", "cocoa 0.25.0",
"dirs-next", "dirs-next",
"futures-lite 2.0.1", "futures-lite 2.1.0",
"gtk", "gtk",
"once_cell", "once_cell",
"serde", "serde",
@@ -5256,7 +5247,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#e7c72c9816d14a218e702dd233a6cfec957c2ee6" source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#a848e1870a71b7460a0c56ba44cc1af61d818483"
dependencies = [ dependencies = [
"base64", "base64",
"dirs-next", "dirs-next",
@@ -5282,7 +5273,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#e7c72c9816d14a218e702dd233a6cfec957c2ee6" source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#a848e1870a71b7460a0c56ba44cc1af61d818483"
dependencies = [ dependencies = [
"futures-util", "futures-util",
"log", "log",
@@ -5299,7 +5290,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#e7c72c9816d14a218e702dd233a6cfec957c2ee6" source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#a848e1870a71b7460a0c56ba44cc1af61d818483"
dependencies = [ dependencies = [
"bincode", "bincode",
"bitflags 2.4.1", "bitflags 2.4.1",
@@ -5406,8 +5397,8 @@ checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"fastrand 2.0.1", "fastrand 2.0.1",
"redox_syscall 0.4.1", "redox_syscall",
"rustix 0.38.26", "rustix 0.38.28",
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
@@ -5476,7 +5467,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5"
dependencies = [ dependencies = [
"deranged", "deranged",
"itoa 1.0.9", "itoa 1.0.10",
"powerfmt", "powerfmt",
"serde", "serde",
"time-core", "time-core",
@@ -5525,9 +5516,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.34.0" version = "1.35.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9" checksum = "841d45b238a16291a4e1584e61820b8ae57d696cc5015c459c229ccc6990cc1c"
dependencies = [ dependencies = [
"backtrace", "backtrace",
"bytes", "bytes",
@@ -5600,21 +5591,21 @@ dependencies = [
[[package]] [[package]]
name = "toml" name = "toml"
version = "0.8.8" version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d"
dependencies = [ dependencies = [
"serde", "serde",
"serde_spanned", "serde_spanned",
"toml_datetime", "toml_datetime",
"toml_edit 0.21.0", "toml_edit 0.20.2",
] ]
[[package]] [[package]]
name = "toml_datetime" name = "toml_datetime"
version = "0.6.5" version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
dependencies = [ dependencies = [
"serde", "serde",
] ]
@@ -5634,20 +5625,9 @@ dependencies = [
[[package]] [[package]]
name = "toml_edit" name = "toml_edit"
version = "0.20.7" version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338"
dependencies = [
"indexmap 2.1.0",
"toml_datetime",
"winnow",
]
[[package]]
name = "toml_edit"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03"
dependencies = [ dependencies = [
"indexmap 2.1.0", "indexmap 2.1.0",
"serde", "serde",
@@ -5755,9 +5735,9 @@ dependencies = [
[[package]] [[package]]
name = "try-lock" name = "try-lock"
version = "0.2.4" 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 = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]] [[package]]
name = "typenum" name = "typenum"
@@ -5777,9 +5757,9 @@ dependencies = [
[[package]] [[package]]
name = "unicode-bidi" name = "unicode-bidi"
version = "0.3.13" version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" checksum = "6f2528f27a9eb2b21e69c95319b30bd0efd85d09c379741b0f78ea1d86be2416"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
@@ -6035,7 +6015,7 @@ dependencies = [
"gdk-sys", "gdk-sys",
"gio", "gio",
"gio-sys 0.18.1", "gio-sys 0.18.1",
"glib 0.18.3", "glib 0.18.4",
"glib-sys 0.18.1", "glib-sys 0.18.1",
"gobject-sys 0.18.0", "gobject-sys 0.18.0",
"gtk", "gtk",
@@ -6442,9 +6422,9 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.5.19" version = "0.5.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "829846f3e3db426d4cee4510841b71a8e58aa2a76b1132579487ae430ccd9c7b" checksum = "b67b5f0a4e7a27a64c651977932b9dc5667ca7fc31ac44b03ed37a0cf42fdfff"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
@@ -6559,9 +6539,9 @@ dependencies = [
[[package]] [[package]]
name = "xattr" name = "xattr"
version = "1.0.1" version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4686009f71ff3e5c4dbcf1a282d0a44db3f021ba69350cd42086b3e5f1c6985" checksum = "fbc6ab6ec1907d1a901cdbcd2bd4cb9e7d64ce5c9739cbb97d3c391acd8c7fae"
dependencies = [ dependencies = [
"libc", "libc",
] ]
@@ -6655,18 +6635,18 @@ dependencies = [
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.7.28" version = "0.7.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d6f15f7ade05d2a4935e34a457b936c23dc70a05cc1d97133dc99e7a3fe0f0e" checksum = "306dca4455518f1f31635ec308b6b3e4eb1b11758cefafc782827d0aa7acb5c7"
dependencies = [ dependencies = [
"zerocopy-derive", "zerocopy-derive",
] ]
[[package]] [[package]]
name = "zerocopy-derive" name = "zerocopy-derive"
version = "0.7.28" version = "0.7.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbbad221e3f78500350ecbd7dfa4e63ef945c05f4c61cb7f4d3f84cd0bba649b" checksum = "be912bf68235a88fbefd1b73415cb218405958d1655b2ece9035a19920bdf6ba"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View File

@@ -1,5 +1,3 @@
/* @import 'reactflow/dist/style.css'; */
/* Vidstack */ /* Vidstack */
@import '@vidstack/react/player/styles/default/theme.css'; @import '@vidstack/react/player/styles/default/theme.css';
@import '@vidstack/react/player/styles/default/layouts/video.css'; @import '@vidstack/react/player/styles/default/layouts/video.css';

View File

@@ -1,13 +1,11 @@
import { message } from '@tauri-apps/plugin-dialog'; import { message } from '@tauri-apps/plugin-dialog';
import { fetch } from '@tauri-apps/plugin-http'; 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 { 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 { useStorage } from '@libs/storage/provider'; import { useArk } from '@libs/ark';
import { LoaderIcon } from '@shared/icons'; import { LoaderIcon } from '@shared/icons';
import { AppLayout } from '@shared/layouts/app'; import { AppLayout } from '@shared/layouts/app';
@@ -19,12 +17,12 @@ import { SettingsLayout } from '@shared/layouts/settings';
import './app.css'; import './app.css';
export default function App() { export default function App() {
const { db } = useStorage(); const { ark } = useArk();
const accountLoader = async () => { const accountLoader = async () => {
try { try {
// redirect to welcome screen if none user exist // redirect to welcome screen if none user exist
const totalAccount = await db.checkAccount(); const totalAccount = await ark.checkAccount();
if (totalAccount === 0) return redirect('/auth/welcome'); if (totalAccount === 0) return redirect('/auth/welcome');
return null; return null;
@@ -87,15 +85,6 @@ export default function App() {
return { Component: RelayScreen }; return { Component: RelayScreen };
}, },
}, },
{
path: 'explore',
element: (
<ReactFlowProvider>
<ExploreScreen />
</ReactFlowProvider>
),
errorElement: <ErrorScreen />,
},
{ {
path: 'chats', path: 'chats',
element: <ChatsScreen />, element: <ChatsScreen />,

View File

@@ -1,4 +1,4 @@
import { NDKEvent, NDKKind, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk'; import { 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 { save } from '@tauri-apps/plugin-dialog'; import { save } from '@tauri-apps/plugin-dialog';
@@ -11,8 +11,7 @@ import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useNDK } from '@libs/ndk/provider'; import { useArk } from '@libs/ark';
import { useStorage } from '@libs/storage/provider';
import { AvatarUploader } from '@shared/avatarUploader'; import { AvatarUploader } from '@shared/avatarUploader';
import { ArrowLeftIcon, InfoIcon, LoaderIcon } from '@shared/icons'; import { ArrowLeftIcon, InfoIcon, LoaderIcon } from '@shared/icons';
@@ -29,13 +28,12 @@ export function CreateAccountScreen() {
privkey: string; privkey: string;
}>(null); }>(null);
const { ark } = useArk();
const { const {
register, register,
handleSubmit, handleSubmit,
formState: { isDirty, isValid }, formState: { isDirty, isValid },
} = useForm(); } = useForm();
const { db } = useStorage();
const { ndk } = useNDK();
const navigate = useNavigate(); const navigate = useNavigate();
@@ -62,28 +60,27 @@ export function CreateAccountScreen() {
const userNsec = nip19.nsecEncode(userPrivkey); const userNsec = nip19.nsecEncode(userPrivkey);
const signer = new NDKPrivateKeySigner(userPrivkey); const signer = new NDKPrivateKeySigner(userPrivkey);
ndk.signer = signer; ark.updateNostrSigner({ signer });
const event = new NDKEvent(ndk); const publish = await ark.createEvent({
event.content = JSON.stringify(profile); content: JSON.stringify(profile),
event.kind = NDKKind.Metadata; kind: NDKKind.Metadata,
event.pubkey = userPubkey; tags: [],
event.tags = []; publish: true,
});
const publish = await event.publish();
if (publish) { if (publish) {
await db.createAccount(userNpub, userPubkey); await ark.createAccount({
await db.secureSave(userPubkey, userPrivkey); id: userNpub,
pubkey: userPubkey,
privkey: userPrivkey,
});
const relayListEvent = new NDKEvent(ndk); await ark.createEvent({
relayListEvent.kind = NDKKind.RelayList; kind: NDKKind.RelayList,
relayListEvent.tags = [...ndk.pool.relays.values()].map((item) => [ tags: [ark.relays],
'r', publish: true,
item.url, });
]);
await relayListEvent.publish();
setKeys({ setKeys({
npub: userNpub, npub: userNpub,
@@ -93,7 +90,7 @@ export function CreateAccountScreen() {
}); });
setLoading(false); setLoading(false);
} else { } else {
toast('Create account failed'); toast('Cannot publish user profile, please try again later.');
setLoading(false); setLoading(false);
} }
} catch (e) { } catch (e) {

View File

@@ -1,6 +1,81 @@
import { Link } from 'react-router-dom'; import { NDKKind } from '@nostr-dev-kit/ndk';
import { useQueryClient } from '@tanstack/react-query';
import { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { useArk } from '@libs/ark';
import { LoaderIcon } from '@shared/icons';
import { FETCH_LIMIT } from '@utils/constants';
export function FinishScreen() { export function FinishScreen() {
const { ark } = useArk();
const [loading, setLoading] = useState(false);
const queryClient = useQueryClient();
const navigate = useNavigate();
const prefetch = async () => {
if (!ark.account.contacts.length) return navigate('/');
try {
setLoading(true);
// prefetch newsfeed
await queryClient.prefetchInfiniteQuery({
queryKey: ['newsfeed'],
initialPageParam: 0,
queryFn: async ({
signal,
pageParam,
}: {
signal: AbortSignal;
pageParam: number;
}) => {
return await ark.getInfiniteEvents({
filter: {
kinds: [NDKKind.Text, NDKKind.Repost],
authors: !ark.account.contacts.length
? [ark.account.pubkey]
: ark.account.contacts,
},
limit: FETCH_LIMIT,
pageParam,
signal,
});
},
});
// prefetch notification
await queryClient.prefetchInfiniteQuery({
queryKey: ['notification'],
initialPageParam: 0,
queryFn: async ({
signal,
pageParam,
}: {
signal: AbortSignal;
pageParam: number;
}) => {
return await ark.getInfiniteEvents({
filter: {
kinds: [NDKKind.Text, NDKKind.Repost, NDKKind.Reaction, NDKKind.Zap],
'#p': [ark.account.pubkey],
},
limit: FETCH_LIMIT,
pageParam,
signal,
});
},
});
navigate('/');
} catch (e) {
console.error(e);
}
};
return ( return (
<div className="flex h-full w-full items-center justify-center"> <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="mx-auto flex w-full max-w-md flex-col gap-10">
@@ -17,12 +92,13 @@ export function FinishScreen() {
> >
Start tutorial Start tutorial
</Link> </Link>
<Link <button
to="/" type="button"
onClick={prefetch}
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" 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 {loading ? <LoaderIcon className="h-4 w-4 animate-spin" /> : 'Skip'}
</Link> </button>
<p className="text-center text-sm font-medium text-neutral-500 dark:text-neutral-600"> <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 You need to restart app to make changes in previous step take effect or you
can continue with Lume default settings can continue with Lume default settings

View File

@@ -1,4 +1,4 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk'; import { NDKKind } from '@nostr-dev-kit/ndk';
import * as Accordion from '@radix-ui/react-accordion'; import * as Accordion from '@radix-ui/react-accordion';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
@@ -7,8 +7,7 @@ import { useNavigate } from 'react-router-dom';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
import { useNDK } from '@libs/ndk/provider'; import { useArk } from '@libs/ark';
import { useStorage } from '@libs/storage/provider';
import { import {
ArrowLeftIcon, ArrowLeftIcon,
@@ -37,8 +36,7 @@ const POPULAR_USERS = [
const LUME_USERS = ['npub1zfss807aer0j26mwp2la0ume0jqde3823rmu97ra6sgyyg956e0s6xw445']; const LUME_USERS = ['npub1zfss807aer0j26mwp2la0ume0jqde3823rmu97ra6sgyyg956e0s6xw445'];
export function FollowScreen() { export function FollowScreen() {
const { ndk } = useNDK(); const { ark } = useArk();
const { db } = useStorage();
const { status, data } = useQuery({ const { status, data } = useQuery({
queryKey: ['trending-profiles-widget'], queryKey: ['trending-profiles-widget'],
queryFn: async () => { queryFn: async () => {
@@ -68,20 +66,22 @@ export function FollowScreen() {
setLoading(true); setLoading(true);
if (!follows.length) return navigate('/auth/finish'); if (!follows.length) return navigate('/auth/finish');
const event = new NDKEvent(ndk); const publish = await ark.createEvent({
event.kind = NDKKind.Contacts; kind: NDKKind.Contacts,
event.tags = follows.map((item) => { tags: follows.map((item) => {
if (item.startsWith('npub')) return ['p', nip19.decode(item).data as string]; if (item.startsWith('npub1')) return ['p', nip19.decode(item).data as string];
return ['p', item]; return ['p', item];
}),
publish: true,
}); });
const publish = await event.publish();
if (publish) { if (publish) {
db.account.contacts = follows.map((item) => { ark.account.contacts = follows.map((item) => {
if (item.startsWith('npub')) return nip19.decode(item).data as string; if (item.startsWith('npub1')) return nip19.decode(item).data as string;
return item; return item;
}); });
setLoading(false);
return navigate('/auth/finish'); return navigate('/auth/finish');
} }
} catch (e) { } catch (e) {

View File

@@ -8,16 +8,12 @@ import { useNavigate } from 'react-router-dom';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
import { useNDK } from '@libs/ndk/provider'; import { useArk } from '@libs/ark';
import { useStorage } from '@libs/storage/provider';
import { ArrowLeftIcon, LoaderIcon } 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 { db } = useStorage();
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);
@@ -25,6 +21,7 @@ export function ImportAccountScreen() {
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 { ark } = useArk();
const navigate = useNavigate(); const navigate = useNavigate();
const submitNpub = async () => { const submitNpub = async () => {
@@ -47,8 +44,8 @@ export function ImportAccountScreen() {
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.createSetting('nsecbunker', '1'); await ark.createSetting('nsecbunker', '1');
await db.secureSave(`${npub}-nsecbunker`, localSigner.privateKey); await ark.createPrivkey(`${npub}-nsecbunker`, localSigner.privateKey);
// open nsecbunker web app in default browser // open nsecbunker web app in default browser
await open('https://app.nsecbunker.com/keys'); await open('https://app.nsecbunker.com/keys');
@@ -60,8 +57,7 @@ export function ImportAccountScreen() {
const remoteSigner = new NDKNip46Signer(bunker, npub, localSigner); const remoteSigner = new NDKNip46Signer(bunker, npub, localSigner);
await remoteSigner.blockUntilReady(); await remoteSigner.blockUntilReady();
ark.updateNostrSigner({ signer: remoteSigner });
ndk.signer = remoteSigner;
setPubkey(pubkey); setPubkey(pubkey);
setCreated({ ok: false, remote: true }); setCreated({ ok: false, remote: true });
@@ -80,14 +76,10 @@ export function ImportAccountScreen() {
setLoading(true); setLoading(true);
// add account to db // add account to db
await db.createAccount(npub, pubkey); await ark.createAccount({ id: npub, pubkey });
// get account metadata // get account contacts
const user = ndk.getUser({ pubkey }); await ark.getUserContacts({ pubkey });
if (user) {
db.account.contacts = [...(await user.follows())].map((user) => user.pubkey);
db.account.relayList = await user.relayList();
}
setCreated((prev) => ({ ...prev, ok: true })); setCreated((prev) => ({ ...prev, ok: true }));
setLoading(false); setLoading(false);
@@ -109,9 +101,8 @@ export function ImportAccountScreen() {
if (nsec.length > 50 && nsec.startsWith('nsec1')) { if (nsec.length > 50 && nsec.startsWith('nsec1')) {
try { try {
const privkey = nip19.decode(nsec).data as string; const privkey = nip19.decode(nsec).data as string;
await db.secureSave(pubkey, privkey); await ark.createPrivkey(pubkey, privkey);
ark.updateNostrSigner({ signer: new NDKPrivateKeySigner(privkey) });
ndk.signer = new NDKPrivateKeySigner(privkey);
setSavedPrivkey(true); setSavedPrivkey(true);
} catch (e) { } catch (e) {
@@ -290,9 +281,9 @@ export function ImportAccountScreen() {
<p className="text-sm"> <p className="text-sm">
Lume will put your private key to{' '} Lume will put your private key to{' '}
<b> <b>
{db.platform === 'macos' {ark.platform === 'macos'
? 'Apple Keychain (macOS)' ? 'Apple Keychain (macOS)'
: db.platform === 'windows' : ark.platform === 'windows'
? 'Credential Manager (Windows)' ? 'Credential Manager (Windows)'
: 'Secret Service (Linux)'} : 'Secret Service (Linux)'}
</b> </b>

View File

@@ -3,12 +3,12 @@ import { isPermissionGranted, requestPermission } from '@tauri-apps/plugin-notif
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useStorage } from '@libs/storage/provider'; import { useArk } from '@libs/ark';
import { InfoIcon } from '@shared/icons'; import { InfoIcon } from '@shared/icons';
export function OnboardingScreen() { export function OnboardingScreen() {
const { db } = useStorage(); const { ark } = useArk();
const navigate = useNavigate(); const navigate = useNavigate();
const [settings, setSettings] = useState({ const [settings, setSettings] = useState({
@@ -18,19 +18,19 @@ export function OnboardingScreen() {
}); });
const next = () => { const next = () => {
if (!db.account.contacts.length) return navigate('/auth/follow'); if (!ark.account.contacts.length) return navigate('/auth/follow');
return navigate('/auth/finish'); return navigate('/auth/finish');
}; };
const toggleOutbox = async () => { const toggleOutbox = async () => {
await db.createSetting('outbox', String(+!settings.outbox)); await ark.createSetting('outbox', String(+!settings.outbox));
// update state // update state
setSettings((prev) => ({ ...prev, outbox: !settings.outbox })); setSettings((prev) => ({ ...prev, outbox: !settings.outbox }));
}; };
const toggleAutoupdate = async () => { const toggleAutoupdate = async () => {
await db.createSetting('autoupdate', String(+!settings.autoupdate)); await ark.createSetting('autoupdate', String(+!settings.autoupdate));
db.settings.autoupdate = !settings.autoupdate; ark.settings.autoupdate = !settings.autoupdate;
// update state // update state
setSettings((prev) => ({ ...prev, autoupdate: !settings.autoupdate })); setSettings((prev) => ({ ...prev, autoupdate: !settings.autoupdate }));
}; };
@@ -46,7 +46,7 @@ export function OnboardingScreen() {
const permissionGranted = await isPermissionGranted(); const permissionGranted = await isPermissionGranted();
setSettings((prev) => ({ ...prev, notification: permissionGranted })); setSettings((prev) => ({ ...prev, notification: permissionGranted }));
const data = await db.getAllSettings(); const data = await ark.getAllSettings();
if (!data) return; if (!data) return;
data.forEach((item) => { data.forEach((item) => {

View File

@@ -1,6 +1,81 @@
import { Link } from 'react-router-dom'; import { NDKKind } from '@nostr-dev-kit/ndk';
import { useQueryClient } from '@tanstack/react-query';
import { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { useArk } from '@libs/ark';
import { LoaderIcon } from '@shared/icons';
import { FETCH_LIMIT } from '@utils/constants';
export function TutorialFinishScreen() { export function TutorialFinishScreen() {
const { ark } = useArk();
const [loading, setLoading] = useState(false);
const queryClient = useQueryClient();
const navigate = useNavigate();
const prefetch = async () => {
if (!ark.account.contacts.length) return navigate('/');
try {
setLoading(true);
// prefetch newsfeed
await queryClient.prefetchInfiniteQuery({
queryKey: ['newsfeed'],
initialPageParam: 0,
queryFn: async ({
signal,
pageParam,
}: {
signal: AbortSignal;
pageParam: number;
}) => {
return await ark.getInfiniteEvents({
filter: {
kinds: [NDKKind.Text, NDKKind.Repost],
authors: !ark.account.contacts.length
? [ark.account.pubkey]
: ark.account.contacts,
},
limit: FETCH_LIMIT,
pageParam,
signal,
});
},
});
// prefetch notification
await queryClient.prefetchInfiniteQuery({
queryKey: ['notification'],
initialPageParam: 0,
queryFn: async ({
signal,
pageParam,
}: {
signal: AbortSignal;
pageParam: number;
}) => {
return await ark.getInfiniteEvents({
filter: {
kinds: [NDKKind.Text, NDKKind.Repost, NDKKind.Reaction, NDKKind.Zap],
'#p': [ark.account.pubkey],
},
limit: FETCH_LIMIT,
pageParam,
signal,
});
},
});
navigate('/');
} catch (e) {
console.error(e);
}
};
return ( return (
<div className="flex h-full w-full items-center justify-center"> <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="mx-auto flex w-full max-w-md flex-col gap-10">
@@ -11,12 +86,17 @@ export function TutorialFinishScreen() {
</h1> </h1>
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Link <button
to="/" type="button"
onClick={prefetch}
className="inline-flex h-11 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"
> >
Start using {loading ? (
</Link> <LoaderIcon className="h-4 w-4 animate-spin" />
) : (
'Start using Lume'
)}
</button>
<Link <Link
to="https://nostr.how/" to="https://nostr.how/"
target="_blank" target="_blank"

View File

@@ -1,21 +1,23 @@
import { NDKEvent } from '@nostr-dev-kit/ndk';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useNDK } from '@libs/ndk/provider'; import { useArk } from '@libs/ark';
import { EditIcon, ReactionIcon, ReplyIcon, RepostIcon, ZapIcon } from '@shared/icons'; import { EditIcon, ReactionIcon, ReplyIcon, RepostIcon, ZapIcon } from '@shared/icons';
import { TextNote } from '@shared/notes'; import { TextNote } from '@shared/notes';
export function TutorialNoteScreen() { export function TutorialNoteScreen() {
const { ndk } = useNDK(); const { ark } = useArk();
const exampleEvent = new NDKEvent(ndk, {
id: 'a3527670dd9b178bf7c2a9ea673b63bc8bfe774942b196691145343623c45821', const exampleEvent = ark.createNDKEvent({
pubkey: '04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9', event: {
created_at: 1701355223, id: 'a3527670dd9b178bf7c2a9ea673b63bc8bfe774942b196691145343623c45821',
kind: 1, pubkey: '04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9',
tags: [], created_at: 1701355223,
content: 'good morning nostr, stay humble and stack sats 🫡', kind: 1,
sig: '9e0bd67ec25598744f20bff0fe360fdf190c4240edb9eea260e50f77e07f94ea767ececcc6270819b7f64e5e7ca1fe20b4971f46dc120e6db43114557f3a6dae', tags: [],
content: 'good morning nostr, stay humble and stack sats 🫡',
sig: '9e0bd67ec25598744f20bff0fe360fdf190c4240edb9eea260e50f77e07f94ea767ececcc6270819b7f64e5e7ca1fe20b4971f46dc120e6db43114557f3a6dae',
},
}); });
return ( return (

View File

@@ -1,4 +1,4 @@
import { NDKEvent, NDKSubscription } from '@nostr-dev-kit/ndk'; import { NDKEvent } from '@nostr-dev-kit/ndk';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useCallback, useEffect, useRef } from 'react'; import { useCallback, useEffect, useRef } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
@@ -7,23 +7,18 @@ import { VList, VListHandle } from 'virtua';
import { ChatForm } from '@app/chats/components/chatForm'; import { ChatForm } from '@app/chats/components/chatForm';
import { ChatMessage } from '@app/chats/components/message'; import { ChatMessage } from '@app/chats/components/message';
import { useNDK } from '@libs/ndk/provider'; import { useArk } from '@libs/ark';
import { useStorage } from '@libs/storage/provider';
import { LoaderIcon } from '@shared/icons'; import { LoaderIcon } from '@shared/icons';
import { User } from '@shared/user'; import { User } from '@shared/user';
import { useNostr } from '@utils/hooks/useNostr';
export function ChatScreen() { export function ChatScreen() {
const { db } = useStorage(); const { ark } = useArk();
const { ndk } = useNDK();
const { pubkey } = useParams(); const { pubkey } = useParams();
const { fetchNIP04Messages } = useNostr();
const { status, data } = useQuery({ const { status, data } = useQuery({
queryKey: ['nip04-dm', pubkey], queryKey: ['nip04-dm', pubkey],
queryFn: async () => { queryFn: async () => {
return await fetchNIP04Messages(pubkey); return await ark.getAllMessagesByPubkey({ pubkey });
}, },
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
}); });
@@ -59,7 +54,7 @@ export function ChatScreen() {
<ChatMessage <ChatMessage
key={message.id} key={message.id}
message={message} message={message}
isSelf={message.pubkey === db.account.pubkey} isSelf={message.pubkey === ark.account.pubkey}
/> />
); );
}, },
@@ -71,20 +66,15 @@ export function ChatScreen() {
}, [data]); }, [data]);
useEffect(() => { useEffect(() => {
const sub: NDKSubscription = ndk.subscribe( const sub = ark.subscribe({
{ filter: {
kinds: [4], kinds: [4],
authors: [db.account.pubkey], authors: [ark.account.pubkey],
'#p': [pubkey], '#p': [pubkey],
since: Math.floor(Date.now() / 1000), since: Math.floor(Date.now() / 1000),
}, },
{ closeOnEose: false,
closeOnEose: false, cb: (event) => newMessage.mutate(event),
}
);
sub.addListener('event', (event) => {
newMessage.mutate(event);
}); });
return () => { return () => {

View File

@@ -1,29 +1,19 @@
import { NDKEvent, NDKKind, NDKUser } from '@nostr-dev-kit/ndk';
import { useState } from 'react'; import { useState } from 'react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { MediaUploader } from '@app/chats/components/mediaUploader'; import { MediaUploader } from '@app/chats/components/mediaUploader';
import { useNDK } from '@libs/ndk/provider'; import { useArk } from '@libs/ark';
import { EnterIcon } from '@shared/icons'; import { EnterIcon } from '@shared/icons';
export function ChatForm({ receiverPubkey }: { receiverPubkey: string }) { export function ChatForm({ receiverPubkey }: { receiverPubkey: string }) {
const { ndk } = useNDK(); const { ark } = useArk();
const [value, setValue] = useState(''); const [value, setValue] = useState('');
const submit = async () => { const submit = async () => {
try { try {
const recipient = new NDKUser({ pubkey: receiverPubkey }); const publish = await ark.nip04Encrypt({ content: value, pubkey: receiverPubkey });
const message = await ndk.signer.encrypt(recipient, value);
const event = new NDKEvent(ndk);
event.content = message;
event.kind = NDKKind.EncryptedDirectMessage;
event.tag(recipient);
const publish = await event.publish();
if (publish) setValue(''); if (publish) setValue('');
} catch (e) { } catch (e) {
toast.error(e); toast.error(e);

View File

@@ -1,22 +1,24 @@
import * as Tooltip from '@radix-ui/react-tooltip'; import * as Tooltip from '@radix-ui/react-tooltip';
import { Dispatch, SetStateAction, useState } from 'react'; import { Dispatch, SetStateAction, useState } from 'react';
import { LoaderIcon, MediaIcon } from '@shared/icons'; import { useArk } from '@libs/ark';
import { useNostr } from '@utils/hooks/useNostr'; import { LoaderIcon, MediaIcon } from '@shared/icons';
export function MediaUploader({ export function MediaUploader({
setState, setState,
}: { }: {
setState: Dispatch<SetStateAction<string>>; setState: Dispatch<SetStateAction<string>>;
}) { }) {
const { upload } = useNostr(); const { ark } = useArk();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const uploadMedia = async () => { const uploadMedia = async () => {
setLoading(true); setLoading(true);
const image = await upload(['mp4', 'mp3', 'webm', 'mkv', 'avi', 'mov']); const image = await ark.upload({
fileExts: ['mp4', 'mp3', 'webm', 'mkv', 'avi', 'mov'],
});
if (image) { if (image) {
setState((prev: string) => `${prev}\n${image}`); setState((prev: string) => `${prev}\n${image}`);

View File

@@ -1,26 +1,17 @@
import { NDKEvent, NDKUser } from '@nostr-dev-kit/ndk'; import { NDKEvent } from '@nostr-dev-kit/ndk';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useNDK } from '@libs/ndk/provider'; import { useArk } from '@libs/ark';
import { useStorage } from '@libs/storage/provider';
export function useDecryptMessage(message: NDKEvent) { export function useDecryptMessage(event: NDKEvent) {
const { db } = useStorage(); const { ark } = useArk();
const { ndk } = useNDK(); const [content, setContent] = useState(event.content);
const [content, setContent] = useState(message.content);
useEffect(() => { useEffect(() => {
async function decryptContent() { async function decryptContent() {
try { try {
const sender = new NDKUser({ const message = await ark.nip04Decrypt({ event });
pubkey: setContent(message);
db.account.pubkey === message.pubkey
? message.tags.find((el) => el[0] === 'p')[1]
: message.pubkey,
});
const result = await ndk.signer.decrypt(sender, message.content);
setContent(result);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }

View File

@@ -1,25 +1,20 @@
import { NDKEvent } from '@nostr-dev-kit/ndk'; import { NDKEvent } from '@nostr-dev-kit/ndk';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useCallback, useEffect } from 'react'; import { useCallback } from 'react';
import { Outlet, useNavigate } from 'react-router-dom'; import { Outlet } from 'react-router-dom';
import { ChatListItem } from '@app/chats/components/chatListItem'; import { ChatListItem } from '@app/chats/components/chatListItem';
import { useNDK } from '@libs/ndk/provider'; import { useArk } from '@libs/ark';
import { LoaderIcon } from '@shared/icons'; import { LoaderIcon } from '@shared/icons';
import { useNostr } from '@utils/hooks/useNostr';
export function ChatsScreen() { export function ChatsScreen() {
const navigate = useNavigate(); const { ark } = useArk();
const { ndk } = useNDK();
const { getAllNIP04Chats } = useNostr();
const { status, data } = useQuery({ const { status, data } = useQuery({
queryKey: ['nip04-chats'], queryKey: ['nip04-chats'],
queryFn: async () => { queryFn: async () => {
return await getAllNIP04Chats(); return await ark.getAllChats();
}, },
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
refetchOnMount: false, refetchOnMount: false,
@@ -34,10 +29,6 @@ export function ChatsScreen() {
[data] [data]
); );
useEffect(() => {
if (!ndk.signer) navigate('/new/privkey');
}, []);
return ( return (
<div className="grid h-full w-full grid-cols-3"> <div className="grid h-full w-full grid-cols-3">
<div className="col-span-1 h-full overflow-y-auto border-r border-neutral-200 scrollbar-none dark:border-neutral-800"> <div className="col-span-1 h-full overflow-y-auto border-r border-neutral-200 scrollbar-none dark:border-neutral-800">

View File

@@ -4,7 +4,7 @@ import { writeTextFile } from '@tauri-apps/plugin-fs';
import { relaunch } from '@tauri-apps/plugin-process'; import { relaunch } from '@tauri-apps/plugin-process';
import { useRouteError } from 'react-router-dom'; import { useRouteError } from 'react-router-dom';
import { useStorage } from '@libs/storage/provider'; import { useArk } from '@libs/ark';
interface RouteError { interface RouteError {
statusText: string; statusText: string;
@@ -12,7 +12,7 @@ interface RouteError {
} }
export function ErrorScreen() { export function ErrorScreen() {
const { db } = useStorage(); const { ark } = useArk();
const error = useRouteError() as RouteError; const error = useRouteError() as RouteError;
const restart = async () => { const restart = async () => {
@@ -26,18 +26,18 @@ export function ErrorScreen() {
const filePath = await save({ const filePath = await save({
defaultPath: downloadPath + '/' + fileName, defaultPath: downloadPath + '/' + fileName,
}); });
const nsec = await db.secureLoad(db.account.pubkey); const nsec = await ark.loadPrivkey(ark.account.pubkey);
if (filePath) { if (filePath) {
if (nsec) { if (nsec) {
await writeTextFile( await writeTextFile(
filePath, filePath,
`Nostr account, generated by Lume (lume.nu)\nPublic key: ${db.account.id}\nPrivate key: ${nsec}` `Nostr account, generated by Lume (lume.nu)\nPublic key: ${ark.account.id}\nPrivate key: ${nsec}`
); );
} else { } else {
await writeTextFile( await writeTextFile(
filePath, filePath,
`Nostr account, generated by Lume (lume.nu)\nPublic key: ${db.account.id}` `Nostr account, generated by Lume (lume.nu)\nPublic key: ${ark.account.id}`
); );
} }
} // else { user cancel action } } // else { user cancel action }

View File

@@ -1,29 +0,0 @@
import { BaseEdge, EdgeProps, getBezierPath } from 'reactflow';
export function Edge({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
style = {},
markerEnd,
}: EdgeProps) {
const [edgePath] = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
});
return (
<BaseEdge
path={edgePath}
markerEnd={markerEnd}
style={{ ...style, stroke: '#71717a' }}
/>
);
}

View File

@@ -1,17 +0,0 @@
import { memo } from 'react';
import { useProfile } from '@utils/hooks/useProfile';
export const GroupTitle = memo(function GroupTitle({ pubkey }: { pubkey: string }) {
const { isLoading, user } = useProfile(pubkey);
if (isLoading) {
return <div className="h-3 w-24 animate-pulse rounded bg-white/10" />;
}
return (
<h3 className="text-sm font-semibold text-blue-500">{`${
user.name || user.display_name
}'s network`}</h3>
);
});

View File

@@ -1,14 +0,0 @@
export function Line({ fromX, fromY, toX, toY }) {
return (
<g>
<path
fill="none"
stroke="#f5d0fe"
strokeWidth={1.5}
className="animated"
d={`M${fromX},${fromY} C ${fromX} ${toY} ${fromX} ${toY} ${toX},${toY}`}
/>
<circle cx={toX} cy={toY} fill="#fff" r={3} stroke="#f5d0fe" strokeWidth={1.5} />
</g>
);
}

View File

@@ -1,34 +0,0 @@
import { Handle, Position } from 'reactflow';
import { UserWithDrawer } from '@app/explore/components/userWithDrawer';
import { GroupTitle } from './groupTitle';
export function UserGroupNode({ data }) {
return (
<>
<Handle
type="target"
position={Position.Top}
className="h-2 w-5 rounded-full border-none !bg-blue-400"
/>
<div className="relative mx-3 my-3 flex flex-col gap-1">
{data.title ? (
<h3 className="text-sm font-semibold text-blue-500">{data.title}</h3>
) : (
<GroupTitle pubkey={data.pubkey} />
)}
<div className="grid grid-cols-5 gap-6 rounded-lg border border-blue-500/50 bg-blue-500/10 p-4">
{data.list.map((user: string) => (
<UserWithDrawer key={user} pubkey={user} />
))}
</div>
</div>
<Handle
type="source"
position={Position.Bottom}
className="h-2 w-5 rounded-full border-none !bg-blue-400"
/>
</>
);
}

View File

@@ -1,59 +0,0 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { useQuery } from '@tanstack/react-query';
import { useCallback } from 'react';
import { LoaderIcon } from '@shared/icons';
import { MemoizedRepost, MemoizedTextNote, UnknownNote } from '@shared/notes';
import { useNostr } from '@utils/hooks/useNostr';
export function UserLatestPosts({ pubkey }: { pubkey: string }) {
const { getEventsByPubkey } = useNostr();
const { status, data } = useQuery({
queryKey: ['user-posts', pubkey],
queryFn: async () => {
return await getEventsByPubkey(pubkey);
},
refetchOnWindowFocus: false,
});
const renderItem = useCallback(
(event: NDKEvent) => {
switch (event.kind) {
case NDKKind.Text:
return <MemoizedTextNote key={event.id} event={event} />;
case NDKKind.Repost:
return <MemoizedRepost key={event.id} event={event} />;
default:
return <UnknownNote key={event.id} event={event} />;
}
},
[data]
);
return (
<div className="mt-4 border-t border-neutral-300 pt-3 dark:border-neutral-700">
<h3 className="mb-4 px-3 text-lg font-semibold text-neutral-900 dark:text-neutral-100">
Latest post
</h3>
<div>
{status === 'pending' ? (
<div className="px-3">
<div className="inline-flex h-16 w-full items-center justify-center gap-1.5 rounded-lg bg-neutral-300 text-sm font-medium dark:bg-neutral-700">
<LoaderIcon className="h-4 w-4 animate-spin" />
Loading latest posts...
</div>
</div>
) : data.length < 1 ? (
<div className="px-3">
<div className="inline-flex h-16 w-full items-center justify-center rounded-lg bg-neutral-300 text-sm font-medium dark:bg-neutral-700">
No posts from 24 hours ago
</div>
</div>
) : (
data.map((event) => renderItem(event))
)}
</div>
</div>
);
}

View File

@@ -1,21 +0,0 @@
import { Handle, Position } from 'reactflow';
import { User } from '@shared/user';
export function UserNode({ data }) {
return (
<>
<div className="relative mx-3 my-3 inline-flex h-12 w-12 shrink-0 items-center justify-center">
<span className="absolute inline-flex h-8 w-8 animate-ping rounded-lg bg-green-400 opacity-75"></span>
<div className="relative z-10">
<User pubkey={data.pubkey} variant="avatar" />
</div>
</div>
<Handle
type="source"
position={Position.Bottom}
className="h-2 w-2 rounded-full border-none !bg-white/20"
/>
</>
);
}

View File

@@ -1,155 +0,0 @@
import { NDKEvent, NDKKind, NDKUser } from '@nostr-dev-kit/ndk';
import * as Dialog from '@radix-ui/react-dialog';
import { memo, useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { toast } from 'sonner';
import { useNDK } from '@libs/ndk/provider';
import { useStorage } from '@libs/storage/provider';
import { NIP05 } from '@shared/nip05';
import { TextNote } from '@shared/notes';
import { User } from '@shared/user';
import { useProfile } from '@utils/hooks/useProfile';
import { displayNpub } from '@utils/shortenKey';
import { UserLatestPosts } from './userLatestPosts';
export const UserWithDrawer = memo(function UserWithDrawer({
pubkey,
}: {
pubkey: string;
}) {
const { db } = useStorage();
const { ndk } = useNDK();
const { isLoading, user } = useProfile(pubkey);
const [followed, setFollowed] = useState(false);
const follow = async (pubkey: string) => {
try {
const user = ndk.getUser({ pubkey: db.account.pubkey });
const contacts = await user.follows();
const add = await user.follow(new NDKUser({ pubkey: pubkey }), contacts);
if (add) {
setFollowed(true);
} else {
toast('You already follow this user');
}
} catch (error) {
console.log(error);
}
};
const unfollow = async (pubkey: string) => {
try {
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);
}
} catch (error) {
console.log(error);
}
};
useEffect(() => {
if (db.account.contacts.includes(pubkey)) {
setFollowed(true);
}
}, []);
return (
<Dialog.Root>
<Dialog.Trigger asChild>
<button type="button">
<User pubkey={pubkey} variant="avatar" />
</button>
</Dialog.Trigger>
<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">
<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">
{isLoading ? (
<div>
<p>Loading...</p>
</div>
) : (
<>
<div className="flex flex-col gap-3 px-3">
<img
src={user?.picture || user?.image}
alt={pubkey}
loading="lazy"
decoding="async"
style={{ contentVisibility: 'auto' }}
className="h-12 w-12 rounded-lg"
/>
<div className="flex flex-1 flex-col gap-2">
<div className="flex flex-col gap-1.5">
<div>
<h5 className="text-lg font-semibold">
{user?.name || user?.display_name || user?.displayName}
</h5>
{user?.nip05 ? (
<NIP05
pubkey={pubkey}
nip05={user?.nip05}
className="max-w-[15rem] truncate text-sm text-neutral-600 dark:text-neutral-400"
/>
) : (
<span className="max-w-[15rem] truncate text-sm text-neutral-600 dark:text-neutral-400">
{displayNpub(pubkey, 16)}
</span>
)}
</div>
{user?.about ? <TextNote content={user?.about} /> : null}
</div>
<div className="inline-flex items-center gap-2">
{followed ? (
<button
type="button"
onClick={() => unfollow(pubkey)}
className="inline-flex h-9 w-36 items-center justify-center rounded-lg bg-neutral-300 text-sm font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-700"
>
Unfollow
</button>
) : (
<button
type="button"
onClick={() => follow(pubkey)}
className="inline-flex h-9 w-36 items-center justify-center rounded-lg bg-neutral-300 text-sm font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-700"
>
Follow
</button>
)}
<Link
to={`/chats/${pubkey}`}
className="inline-flex h-9 w-36 items-center justify-center rounded-lg bg-neutral-300 text-sm font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-700"
>
Message
</Link>
</div>
</div>
</div>
<UserLatestPosts pubkey={pubkey} />
</>
)}
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
});

View File

@@ -1,116 +0,0 @@
import { useCallback, useMemo, useRef } from 'react';
import ReactFlow, {
Background,
ConnectionMode,
addEdge,
useEdgesState,
useNodesState,
useReactFlow,
} from 'reactflow';
import { Edge } from '@app/explore/components/edge';
import { Line } from '@app/explore/components/line';
import { UserGroupNode } from '@app/explore/components/userGroupNode';
import { UserNode } from '@app/explore/components/userNode';
import { useStorage } from '@libs/storage/provider';
import { useNostr } from '@utils/hooks/useNostr';
import { getMultipleRandom } from '@utils/transform';
let id = 2;
const getId = () => `${id++}`;
const nodeTypes = { user: UserNode, userGroup: UserGroupNode };
const edgeTypes = { buttonedge: Edge };
export function ExploreScreen() {
const { db } = useStorage();
const { getContactsByPubkey } = useNostr();
const { project } = useReactFlow();
const defaultContacts = useMemo(() => getMultipleRandom(db.account.contacts, 10), []);
const reactFlowWrapper = useRef(null);
const connectingNodeId = useRef(null);
const initialNodes = [
{
id: '0',
type: 'user',
position: { x: 141, y: 0 },
data: { list: [], title: '', pubkey: db.account.pubkey },
},
{
id: '1',
type: 'userGroup',
position: { x: 0, y: 200 },
data: { list: defaultContacts, title: 'Starting Point', pubkey: '' },
},
];
const initialEdges = [{ id: 'e0-1', type: 'buttonedge', source: '0', target: '1' }];
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback((params) => setEdges((eds) => addEdge(params, eds)), []);
const onConnectStart = useCallback((_, { nodeId }) => {
connectingNodeId.current = nodeId;
}, []);
const onConnectEnd = useCallback(
async (event) => {
const targetIsPane = event.target.classList.contains('react-flow__pane');
if (targetIsPane) {
const { top, left } = reactFlowWrapper.current.getBoundingClientRect();
const id = getId();
const prevData = nodes.slice(-1)[0];
const randomPubkey = getMultipleRandom(prevData.data.list, 1)[0];
const newContactList = await getContactsByPubkey(randomPubkey);
const newNode = {
id,
type: 'userGroup',
position: project({ x: event.clientX - left, y: event.clientY - top }),
data: { list: newContactList, title: null, pubkey: randomPubkey },
};
setNodes((nds) => nds.concat(newNode));
setEdges((eds) =>
eds.concat({
id,
type: 'buttonedge',
source: connectingNodeId.current,
target: id,
})
);
}
},
[project]
);
return (
<div className="h-full w-full" ref={reactFlowWrapper}>
<ReactFlow
proOptions={{ hideAttribution: true }}
nodes={nodes}
edges={edges}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
connectionLineComponent={Line}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onConnectStart={onConnectStart}
onConnectEnd={onConnectEnd}
connectionMode={ConnectionMode.Loose}
minZoom={0.8}
maxZoom={1.2}
fitView
>
<Background color="#3f3f46" />
</ReactFlow>
</div>
);
}

View File

@@ -2,7 +2,7 @@ import { useQuery } from '@tanstack/react-query';
import { useCallback, useRef, useState } from 'react'; import { useCallback, useRef, useState } from 'react';
import { VList, VListHandle } from 'virtua'; import { VList, VListHandle } from 'virtua';
import { useStorage } from '@libs/storage/provider'; import { useArk } from '@libs/ark';
import { LoaderIcon } from '@shared/icons'; import { LoaderIcon } from '@shared/icons';
import { import {
@@ -28,11 +28,11 @@ export function HomeScreen() {
const ref = useRef<VListHandle>(null); const ref = useRef<VListHandle>(null);
const [selectedIndex, setSelectedIndex] = useState(-1); const [selectedIndex, setSelectedIndex] = useState(-1);
const { db } = useStorage(); const { ark } = useArk();
const { status, data } = useQuery({ const { status, data } = useQuery({
queryKey: ['widgets'], queryKey: ['widgets'],
queryFn: async () => { queryFn: async () => {
const dbWidgets = await db.getWidgets(); const dbWidgets = await ark.getWidgets();
const defaultWidgets = [ const defaultWidgets = [
{ {
id: '9999', id: '9999',

View File

@@ -1,4 +1,4 @@
import { NDKEvent, NDKKind, NDKTag } from '@nostr-dev-kit/ndk'; import { NDKKind, NDKTag } 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 Placeholder from '@tiptap/extension-placeholder'; import Placeholder from '@tiptap/extension-placeholder';
@@ -12,7 +12,7 @@ import { Markdown } from 'tiptap-markdown';
import { ArticleCoverUploader, MediaUploader, MentionPopup } from '@app/new/components'; import { ArticleCoverUploader, MediaUploader, MentionPopup } from '@app/new/components';
import { useNDK } from '@libs/ndk/provider'; import { useArk } from '@libs/ark';
import { import {
BoldIcon, BoldIcon,
@@ -25,7 +25,7 @@ import {
} from '@shared/icons'; } from '@shared/icons';
export function NewArticleScreen() { export function NewArticleScreen() {
const { ndk } = useNDK(); const { ark } = useArk();
const [height, setHeight] = useState(0); const [height, setHeight] = useState(0);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -69,7 +69,7 @@ export function NewArticleScreen() {
const submit = async () => { const submit = async () => {
try { try {
if (!ndk.signer) return navigate('/new/privkey'); if (!ark.readyToSign) return navigate('/new/privkey');
setLoading(true); setLoading(true);
@@ -91,16 +91,16 @@ export function NewArticleScreen() {
tags.push(['t', tag.replace('#', '')]); tags.push(['t', tag.replace('#', '')]);
}); });
const event = new NDKEvent(ndk);
event.content = content;
event.kind = NDKKind.Article;
event.tags = tags;
// publish // publish
const publishedRelays = await event.publish(); const publish = await ark.createEvent({
content,
tags,
kind: NDKKind.Article,
publish: true,
});
if (publishedRelays) { if (publish) {
toast.success(`Broadcasted to ${publishedRelays.size} relays successfully.`); toast.success(`Broadcasted to ${publish} relays successfully.`);
// update state // update state
setLoading(false); setLoading(false);

View File

@@ -2,12 +2,12 @@ import { message } from '@tauri-apps/plugin-dialog';
import { Editor } from '@tiptap/react'; import { Editor } from '@tiptap/react';
import { useState } from 'react'; import { useState } from 'react';
import { useArk } from '@libs/ark';
import { MediaIcon } from '@shared/icons'; import { MediaIcon } from '@shared/icons';
import { useNostr } from '@utils/hooks/useNostr';
export function MediaUploader({ editor }: { editor: Editor }) { export function MediaUploader({ editor }: { editor: Editor }) {
const { upload } = useNostr(); const { ark } = useArk();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const uploadToNostrBuild = async () => { const uploadToNostrBuild = async () => {
@@ -15,7 +15,9 @@ export function MediaUploader({ editor }: { editor: Editor }) {
// start loading // start loading
setLoading(true); setLoading(true);
const image = await upload(['mp4', 'mp3', 'webm', 'mkv', 'avi', 'mov']); const image = await ark.upload({
fileExts: ['mp4', 'mp3', 'webm', 'mkv', 'avi', 'mov'],
});
if (image) { if (image) {
editor.commands.setImage({ src: image }); editor.commands.setImage({ src: image });

View File

@@ -4,12 +4,12 @@ import { nip19 } from 'nostr-tools';
import { MentionPopupItem } from '@app/new/components'; import { MentionPopupItem } from '@app/new/components';
import { useStorage } from '@libs/storage/provider'; import { useArk } from '@libs/ark';
import { MentionIcon } from '@shared/icons'; import { MentionIcon } from '@shared/icons';
export function MentionPopup({ editor }: { editor: Editor }) { export function MentionPopup({ editor }: { editor: Editor }) {
const { db } = useStorage(); const { ark } = useArk();
const insertMention = (pubkey: string) => { const insertMention = (pubkey: string) => {
editor.commands.insertContent(`nostr:${nip19.npubEncode(pubkey)}`); editor.commands.insertContent(`nostr:${nip19.npubEncode(pubkey)}`);
@@ -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.contacts.length > 0 ? ( {ark.account.contacts.length ? (
db.account.contacts.map((item) => ( ark.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,16 +1,15 @@
import { NDKEvent } from '@nostr-dev-kit/ndk';
import { message, open } from '@tauri-apps/plugin-dialog'; import { message, open } from '@tauri-apps/plugin-dialog';
import { readBinaryFile } from '@tauri-apps/plugin-fs'; import { readBinaryFile } from '@tauri-apps/plugin-fs';
import { useState } from 'react'; import { useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useNDK } from '@libs/ndk/provider'; import { useArk } from '@libs/ark';
import { LoaderIcon } from '@shared/icons'; import { LoaderIcon } from '@shared/icons';
export function NewFileScreen() { export function NewFileScreen() {
const { ndk } = useNDK(); const { ark } = useArk();
const navigate = useNavigate(); const navigate = useNavigate();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -86,20 +85,21 @@ export function NewFileScreen() {
const submit = async () => { const submit = async () => {
try { try {
if (!ndk.signer) return navigate('/new/privkey'); if (!ark.readyToSign) return navigate('/new/privkey');
setIsPublish(true); setIsPublish(true);
const event = new NDKEvent(ndk); const publish = await ark.createEvent({
event.content = caption; kind: 1063,
event.kind = 1063; tags: metadata,
event.tags = metadata; content: caption,
publish: true,
});
const publishedRelays = await event.publish(); if (publish) {
if (publishedRelays) { toast.success(`Broadcasted to ${publish} relays successfully.`);
setMetadata(null); setMetadata(null);
setIsPublish(false); setIsPublish(false);
toast.success(`Broadcasted to ${publishedRelays.size} relays successfully.`);
} }
} catch (e) { } catch (e) {
setIsPublish(false); setIsPublish(false);

View File

@@ -7,13 +7,13 @@ 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 { nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
import { useEffect, useLayoutEffect, useRef, useState } from 'react'; import { 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';
import { MediaUploader, MentionPopup } from '@app/new/components'; import { MediaUploader, MentionPopup } from '@app/new/components';
import { useNDK } from '@libs/ndk/provider'; import { useArk } from '@libs/ark';
import { CancelIcon, LoaderIcon } from '@shared/icons'; import { CancelIcon, LoaderIcon } from '@shared/icons';
import { MentionNote } from '@shared/notes'; import { MentionNote } from '@shared/notes';
@@ -23,7 +23,7 @@ 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 { ark } = useArk();
const { addWidget } = useWidget(); const { addWidget } = useWidget();
const { suggestion } = useSuggestion(); const { suggestion } = useSuggestion();
@@ -68,7 +68,7 @@ export function NewPostScreen() {
const submit = async () => { const submit = async () => {
try { try {
if (!ndk.signer) return navigate('/new/privkey'); if (!ark.readyToSign) return navigate('/new/privkey');
setLoading(true); setLoading(true);
@@ -81,29 +81,23 @@ export function NewPostScreen() {
], ],
}); });
const event = new NDKEvent(ndk);
event.content = serializedContent;
event.kind = NDKKind.Text;
// add reply to tags if present // add reply to tags if present
const replyTo = searchParams.get('replyTo'); const replyTo = searchParams.get('replyTo');
const rootReplyTo = searchParams.get('rootReplyTo'); const rootReplyTo = searchParams.get('rootReplyTo');
if (rootReplyTo) {
const rootEvent = await ndk.fetchEvent(rootReplyTo);
event.tag(rootEvent, 'root');
}
if (replyTo) {
const replyEvent = await ndk.fetchEvent(replyTo);
event.tag(replyEvent, 'reply');
}
// publish event // publish event
const publishedRelays = await event.publish(); const event = (await ark.createEvent({
kind: NDKKind.Text,
tags: [],
content: serializedContent,
replyTo,
rootReplyTo,
})) as NDKEvent;
if (publishedRelays) { const publish = await event.publish();
toast.success(`Broadcasted to ${publishedRelays.size} relays successfully.`);
if (publish) {
toast.success(`Broadcasted to ${publish.size} relays successfully.`);
// update state // update state
setLoading(false); setLoading(false);
@@ -132,10 +126,6 @@ export function NewPostScreen() {
setHeight(containerRef.current.clientHeight); setHeight(containerRef.current.clientHeight);
}, []); }, []);
useEffect(() => {
if (editor) editor.commands.focus('end');
}, [editor]);
return ( return (
<div className="flex flex-1 flex-col gap-4"> <div className="flex flex-1 flex-col gap-4">
<div className="flex-1 overflow-y-auto"> <div className="flex-1 overflow-y-auto">

View File

@@ -4,19 +4,13 @@ import { useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useNDK } from '@libs/ndk/provider'; import { useArk } from '@libs/ark';
import { useStorage } from '@libs/storage/provider';
export function NewPrivkeyScreen() { export function NewPrivkeyScreen() {
const { db } = useStorage(); const { ark } = useArk();
const { ndk } = useNDK();
const [nsec, setNsec] = useState('');
const navigate = useNavigate(); const navigate = useNavigate();
const save = async (content: string) => { const [nsec, setNsec] = useState('');
return await db.secureSave(db.account.pubkey, content);
};
const submit = async (isSave?: boolean) => { const submit = async (isSave?: boolean) => {
try { try {
@@ -30,15 +24,15 @@ export function NewPrivkeyScreen() {
const privkey = decoded.data; const privkey = decoded.data;
const pubkey = getPublicKey(privkey); const pubkey = getPublicKey(privkey);
if (pubkey !== db.account.pubkey) if (pubkey !== ark.account.pubkey)
return toast.info( return toast.info(
'Your nsec is not match your current public key, please make sure you enter right nsec' 'Your nsec is not match your current public key, please make sure you enter right nsec'
); );
const signer = new NDKPrivateKeySigner(privkey); const signer = new NDKPrivateKeySigner(privkey);
ndk.signer = signer; ark.updateNostrSigner({ signer });
if (isSave) await save(privkey); if (isSave) await ark.createPrivkey(ark.account.pubkey, privkey);
navigate(-1); navigate(-1);
} catch (e) { } catch (e) {
@@ -68,14 +62,14 @@ export function NewPrivkeyScreen() {
<button <button
type="button" type="button"
onClick={() => submit()} onClick={() => submit()}
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"
> >
Submit Submit
</button> </button>
<button <button
type="button" type="button"
onClick={() => submit(true)} onClick={() => submit(true)}
className="inline-flex h-9 w-full shrink-0 items-center justify-center rounded-lg bg-neutral-100 font-medium text-neutral-900 hover:bg-neutral-200 dark:bg-neutral-900 dark:text-neutral-100 dark:hover:bg-neutral-800" className="inline-flex h-11 w-full shrink-0 items-center justify-center rounded-lg bg-neutral-100 font-medium text-neutral-900 hover:bg-neutral-200 dark:bg-neutral-900 dark:text-neutral-100 dark:hover:bg-neutral-800"
> >
Submit and Save Submit and Save
</button> </button>

View File

@@ -6,6 +6,8 @@ import { useRef, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useArk } from '@libs/ark';
import { ArrowLeftIcon, CheckCircleIcon, ReplyIcon, ShareIcon } from '@shared/icons'; import { ArrowLeftIcon, CheckCircleIcon, ReplyIcon, ShareIcon } from '@shared/icons';
import { import {
ChildNote, ChildNote,
@@ -18,15 +20,14 @@ import { ReplyList } from '@shared/notes/replies/list';
import { User } from '@shared/user'; import { User } from '@shared/user';
import { useEvent } from '@utils/hooks/useEvent'; import { useEvent } from '@utils/hooks/useEvent';
import { useNostr } from '@utils/hooks/useNostr';
export function TextNoteScreen() { export function TextNoteScreen() {
const navigate = useNavigate(); const navigate = useNavigate();
const replyRef = useRef(null); const replyRef = useRef(null);
const { id } = useParams(); const { id } = useParams();
const { ark } = useArk();
const { status, data } = useEvent(id); const { status, data } = useEvent(id);
const { getEventThread } = useNostr();
const [isCopy, setIsCopy] = useState(false); const [isCopy, setIsCopy] = useState(false);
@@ -50,7 +51,7 @@ export function TextNoteScreen() {
}; };
const renderKind = (event: NDKEvent) => { const renderKind = (event: NDKEvent) => {
const thread = getEventThread(event.tags); const thread = ark.getEventThread({ tags: event.tags });
switch (event.kind) { switch (event.kind) {
case NDKKind.Text: case NDKKind.Text:
return ( return (

View File

@@ -1,12 +1,12 @@
import { useState } from 'react'; import { useState } from 'react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useStorage } from '@libs/storage/provider'; import { useArk } from '@libs/ark';
import { LoaderIcon } from '@shared/icons'; import { LoaderIcon } from '@shared/icons';
export function NWCForm({ setWalletConnectURL }) { export function NWCForm({ setWalletConnectURL }) {
const { db } = useStorage(); const { ark } = useArk();
const [uri, setUri] = useState(''); const [uri, setUri] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -27,7 +27,7 @@ export function NWCForm({ setWalletConnectURL }) {
const params = new URLSearchParams(uriObj.search); const params = new URLSearchParams(uriObj.search);
if (params.has('relay') && params.has('secret')) { if (params.has('relay') && params.has('secret')) {
await db.secureSave(`${db.account.pubkey}-nwc`, uri); await ark.createPrivkey(`${ark.account.pubkey}-nwc`, uri);
setWalletConnectURL(uri); setWalletConnectURL(uri);
setLoading(false); setLoading(false);
} else { } else {

View File

@@ -2,22 +2,22 @@ import { useEffect, useState } from 'react';
import { NWCForm } from '@app/nwc/components/form'; import { NWCForm } from '@app/nwc/components/form';
import { useStorage } from '@libs/storage/provider'; import { useArk } from '@libs/ark';
import { CheckCircleIcon } from '@shared/icons'; import { CheckCircleIcon } from '@shared/icons';
export function NWCScreen() { export function NWCScreen() {
const { db } = useStorage(); const { ark } = useArk();
const [walletConnectURL, setWalletConnectURL] = useState<null | string>(null); const [walletConnectURL, setWalletConnectURL] = useState<null | string>(null);
const remove = async () => { const remove = async () => {
await db.secureRemove(`${db.account.pubkey}-nwc`); await ark.removePrivkey(`${ark.account.pubkey}-nwc`);
setWalletConnectURL(null); setWalletConnectURL(null);
}; };
useEffect(() => { useEffect(() => {
async function getNWC() { async function getNWC() {
const nwc = await db.secureLoad(`${db.account.pubkey}-nwc`); const nwc = await ark.loadPrivkey(`${ark.account.pubkey}-nwc`);
if (nwc) setWalletConnectURL(nwc); if (nwc) setWalletConnectURL(nwc);
} }
getNWC(); getNWC();

View File

@@ -1,33 +1,58 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk'; import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { useQuery } from '@tanstack/react-query'; import { useInfiniteQuery } from '@tanstack/react-query';
import { normalizeRelayUrl } from 'nostr-fetch'; import { useCallback, useMemo } from 'react';
import { useCallback } from 'react';
import { VList } from 'virtua'; import { VList } from 'virtua';
import { useNDK } from '@libs/ndk/provider'; import { useArk } from '@libs/ark';
import { LoaderIcon } from '@shared/icons'; import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
import { MemoizedRepost, MemoizedTextNote, UnknownNote } from '@shared/notes'; import {
MemoizedRepost,
MemoizedTextNote,
NoteSkeleton,
UnknownNote,
} from '@shared/notes';
import { FETCH_LIMIT } from '@utils/constants';
export function RelayEventList({ relayUrl }: { relayUrl: string }) { export function RelayEventList({ relayUrl }: { relayUrl: string }) {
const { fetcher } = useNDK(); const { ark } = useArk();
const { status, data } = useQuery({ const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
queryKey: ['relay-events', relayUrl], useInfiniteQuery({
queryFn: async () => { queryKey: ['relay-events', relayUrl],
const url = 'wss://' + relayUrl; initialPageParam: 0,
const events = await fetcher.fetchLatestEvents( queryFn: async ({
[normalizeRelayUrl(url)], signal,
{ pageParam,
kinds: [NDKKind.Text, NDKKind.Repost], }: {
}, signal: AbortSignal;
20 pageParam: number;
); }) => {
return events as unknown as NDKEvent[]; const url = 'wss://' + relayUrl;
}, const events = await ark.getRelayEvents({
refetchOnWindowFocus: false, relayUrl: url,
refetchOnReconnect: false, filter: {
refetchOnMount: false, kinds: [NDKKind.Text, NDKKind.Repost],
}); },
limit: FETCH_LIMIT,
pageParam,
signal,
});
return events;
},
getNextPageParam: (lastPage) => {
const lastEvent = lastPage.at(-1);
if (!lastEvent) return;
return lastEvent.created_at - 1;
},
refetchOnWindowFocus: false,
});
const allEvents = useMemo(
() => (data ? data.pages.flatMap((page) => page) : []),
[data]
);
const renderItem = useCallback( const renderItem = useCallback(
(event: NDKEvent) => { (event: NDKEvent) => {
@@ -46,16 +71,33 @@ export function RelayEventList({ relayUrl }: { relayUrl: string }) {
return ( return (
<VList className="mx-auto h-full w-full max-w-[500px] pt-10 scrollbar-none"> <VList className="mx-auto h-full w-full max-w-[500px] pt-10 scrollbar-none">
{status === 'pending' ? ( {status === 'pending' ? (
<div className="flex h-full w-full items-center justify-center"> <div className="px-3 py-1.5">
<div className="inline-flex flex-col items-center justify-center gap-2"> <div className="rounded-xl bg-neutral-100 px-3 py-3 dark:bg-neutral-900">
<LoaderIcon className="h-5 w-5 animate-spin text-white" /> <NoteSkeleton />
<p className="text-sm font-medium text-white/80">Loading newsfeed...</p>
</div> </div>
</div> </div>
) : ( ) : (
data.map((item) => renderItem(item)) allEvents.map((item) => renderItem(item))
)} )}
<div className="h-20" /> <div className="flex h-16 items-center justify-center px-3 pb-3">
{hasNextPage ? (
<button
type="button"
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
className="inline-flex h-10 w-max items-center justify-center gap-2 rounded-full bg-blue-500 px-6 font-medium text-white hover:bg-blue-600 focus:outline-none"
>
{isFetchingNextPage ? (
<LoaderIcon className="h-4 w-4 animate-spin" />
) : (
<>
<ArrowRightCircleIcon className="h-5 w-5" />
Load more
</>
)}
</button>
) : null}
</div>
</VList> </VList>
); );
} }

View File

@@ -2,19 +2,20 @@ import { useQuery } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { VList } from 'virtua'; import { VList } from 'virtua';
import { useArk } from '@libs/ark';
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 { useRelay } from '@utils/hooks/useRelay'; import { useRelay } from '@utils/hooks/useRelay';
export function RelayList() { export function RelayList() {
const { getAllRelaysByUsers } = useNostr(); const { ark } = useArk();
const { connectRelay } = useRelay(); const { connectRelay } = useRelay();
const { status, data } = useQuery({ const { status, data } = useQuery({
queryKey: ['relays'], queryKey: ['relays'],
queryFn: async () => { queryFn: async () => {
return await getAllRelaysByUsers(); return await ark.getAllRelaysFromContacts();
}, },
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
refetchOnMount: false, refetchOnMount: false,

View File

@@ -1,29 +1,26 @@
import { NDKKind, NDKSubscriptionCacheUsage } from '@nostr-dev-kit/ndk'; import { NDKKind } from '@nostr-dev-kit/ndk';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { RelayForm } from '@app/relays/components/relayForm'; import { RelayForm } from '@app/relays/components/relayForm';
import { useNDK } from '@libs/ndk/provider'; import { useArk } from '@libs/ark';
import { useStorage } from '@libs/storage/provider';
import { CancelIcon, RefreshIcon } from '@shared/icons'; import { CancelIcon, RefreshIcon } from '@shared/icons';
import { useRelay } from '@utils/hooks/useRelay'; import { useRelay } from '@utils/hooks/useRelay';
export function UserRelayList() { export function UserRelayList() {
const { db } = useStorage(); const { ark } = useArk();
const { ndk } = useNDK();
const { removeRelay } = useRelay(); const { removeRelay } = useRelay();
const { status, data, refetch } = useQuery({ const { status, data, refetch } = useQuery({
queryKey: ['relays', db.account.pubkey], queryKey: ['relays', ark.account.pubkey],
queryFn: async () => { queryFn: async () => {
const event = await ndk.fetchEvent( const event = await ark.getEventByFilter({
{ filter: {
kinds: [NDKKind.RelayList], kinds: [NDKKind.RelayList],
authors: [db.account.pubkey], authors: [ark.account.pubkey],
}, },
{ cacheUsage: NDKSubscriptionCacheUsage.ONLY_RELAY } });
);
if (!event) return []; if (!event) return [];
return event.tags; return event.tags;
@@ -31,7 +28,7 @@ export function UserRelayList() {
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
}); });
const currentRelays = new Set([...ndk.pool.relays.values()].map((item) => item.url)); const currentRelays = new Set([...ark.relays]);
return ( return (
<div className="col-span-1"> <div className="col-span-1">

View File

@@ -1,10 +1,10 @@
import { useStorage } from '@libs/storage/provider'; import { useArk } from '@libs/ark';
export function AdvancedSettingScreen() { export function AdvancedSettingScreen() {
const { db } = useStorage(); const { ark } = useArk();
const clearCache = async () => { const clearCache = async () => {
await db.clearCache(); await ark.clearCache();
}; };
return ( return (

View File

@@ -1,23 +1,23 @@
import { nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useStorage } from '@libs/storage/provider'; import { useArk } from '@libs/ark';
import { EyeOffIcon } from '@shared/icons'; import { EyeOffIcon } from '@shared/icons';
export function BackupSettingScreen() { export function BackupSettingScreen() {
const { db } = useStorage(); const { ark } = useArk();
const [privkey, setPrivkey] = useState(null); const [privkey, setPrivkey] = useState(null);
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
const removePrivkey = async () => { const removePrivkey = async () => {
await db.secureRemove(db.account.pubkey); await ark.removePrivkey(ark.account.pubkey);
}; };
useEffect(() => { useEffect(() => {
async function loadPrivkey() { async function loadPrivkey() {
const key = await db.secureLoad(db.account.pubkey); const key = await ark.loadPrivkey(ark.account.pubkey);
if (key) setPrivkey(key); if (key) setPrivkey(key);
} }

View File

@@ -1,22 +1,19 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useNDK } from '@libs/ndk/provider'; import { useArk } from '@libs/ark';
import { useStorage } from '@libs/storage/provider';
import { EditIcon, LoaderIcon } from '@shared/icons'; import { EditIcon, LoaderIcon } from '@shared/icons';
import { compactNumber } from '@utils/number'; import { compactNumber } from '@utils/number';
export function ContactCard() { export function ContactCard() {
const { db } = useStorage(); const { ark } = useArk();
const { ndk } = useNDK();
const { status, data } = useQuery({ const { status, data } = useQuery({
queryKey: ['contacts'], queryKey: ['contacts'],
queryFn: async () => { queryFn: async () => {
const user = ndk.getUser({ pubkey: db.account.pubkey }); const contacts = await ark.getUserContacts({});
const follows = await user.follows(); return contacts;
return [...follows];
}, },
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
}); });

View File

@@ -2,19 +2,19 @@ import { useQuery } from '@tanstack/react-query';
import { fetch } from '@tauri-apps/plugin-http'; import { fetch } from '@tauri-apps/plugin-http';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useStorage } from '@libs/storage/provider'; import { useArk } from '@libs/ark';
import { LoaderIcon } from '@shared/icons'; import { LoaderIcon } from '@shared/icons';
import { compactNumber } from '@utils/number'; import { compactNumber } from '@utils/number';
export function PostCard() { export function PostCard() {
const { db } = useStorage(); const { ark } = useArk();
const { status, data } = useQuery({ const { status, data } = useQuery({
queryKey: ['user-stats', db.account.pubkey], queryKey: ['user-stats', ark.account.pubkey],
queryFn: async ({ signal }: { signal: AbortSignal }) => { queryFn: async ({ signal }: { signal: AbortSignal }) => {
const res = await fetch( const res = await fetch(
`https://api.nostr.band/v0/stats/profile/${db.account.pubkey}`, `https://api.nostr.band/v0/stats/profile/${ark.account.pubkey}`,
{ {
signal, signal,
} }
@@ -41,14 +41,14 @@ export function PostCard() {
) : ( ) : (
<div className="flex h-full w-full flex-col justify-between p-4"> <div className="flex h-full w-full flex-col justify-between p-4">
<h3 className="pt-1 text-5xl font-semibold tabular-nums text-neutral-900 dark:text-neutral-100"> <h3 className="pt-1 text-5xl font-semibold tabular-nums text-neutral-900 dark:text-neutral-100">
{compactNumber.format(data.stats[db.account.pubkey].pub_note_count)} {compactNumber.format(data.stats[ark.account.pubkey].pub_note_count)}
</h3> </h3>
<div className="mt-auto flex h-6 w-full items-center justify-between"> <div className="mt-auto flex h-6 w-full items-center justify-between">
<p className="text-xl font-medium leading-none text-neutral-600 dark:text-neutral-400"> <p className="text-xl font-medium leading-none text-neutral-600 dark:text-neutral-400">
Posts Posts
</p> </p>
<Link <Link
to={`/users/${db.account.pubkey}`} to={`/users/${ark.account.pubkey}`}
className="inline-flex h-6 w-max items-center gap-1 rounded-full bg-neutral-200 px-2.5 text-sm font-medium hover:bg-neutral-300 dark:bg-neutral-800 dark:hover:bg-neutral-700" className="inline-flex h-6 w-max items-center gap-1 rounded-full bg-neutral-200 px-2.5 text-sm font-medium hover:bg-neutral-300 dark:bg-neutral-800 dark:hover:bg-neutral-700"
> >
View View

View File

@@ -2,7 +2,7 @@ import * as Avatar from '@radix-ui/react-avatar';
import { minidenticon } from 'minidenticons'; import { minidenticon } from 'minidenticons';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useStorage } from '@libs/storage/provider'; import { useArk } from '@libs/ark';
import { EditIcon, LoaderIcon } from '@shared/icons'; import { EditIcon, LoaderIcon } from '@shared/icons';
@@ -10,12 +10,12 @@ import { useProfile } from '@utils/hooks/useProfile';
import { displayNpub } from '@utils/shortenKey'; import { displayNpub } from '@utils/shortenKey';
export function ProfileCard() { export function ProfileCard() {
const { db } = useStorage(); const { ark } = useArk();
const { isLoading, user } = useProfile(db.account.pubkey); const { isLoading, user } = useProfile(ark.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(ark.account.pubkey, 90, 50));
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">
@@ -38,7 +38,7 @@ export function ProfileCard() {
<Avatar.Root className="shrink-0"> <Avatar.Root className="shrink-0">
<Avatar.Image <Avatar.Image
src={user?.picture || user?.image} src={user?.picture || user?.image}
alt={db.account.pubkey} alt={ark.account.pubkey}
loading="lazy" loading="lazy"
decoding="async" decoding="async"
style={{ contentVisibility: 'auto' }} style={{ contentVisibility: 'auto' }}
@@ -47,7 +47,7 @@ export function ProfileCard() {
<Avatar.Fallback delayMs={300}> <Avatar.Fallback delayMs={300}>
<img <img
src={svgURI} src={svgURI}
alt={db.account.pubkey} alt={ark.account.pubkey}
className="h-16 w-16 rounded-xl bg-black dark:bg-white" className="h-16 w-16 rounded-xl bg-black dark:bg-white"
/> />
</Avatar.Fallback> </Avatar.Fallback>
@@ -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(ark.account.pubkey, 16)}
</p> </p>
</div> </div>
</div> </div>

View File

@@ -1,23 +1,18 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useNDK } from '@libs/ndk/provider'; import { useArk } from '@libs/ark';
import { useStorage } from '@libs/storage/provider';
import { EditIcon, LoaderIcon } from '@shared/icons'; import { EditIcon, LoaderIcon } from '@shared/icons';
import { compactNumber } from '@utils/number'; import { compactNumber } from '@utils/number';
export function RelayCard() { export function RelayCard() {
const { db } = useStorage(); const { ark } = useArk();
const { ndk } = useNDK();
const { status, data } = useQuery({ const { status, data } = useQuery({
queryKey: ['relays'], queryKey: ['relays', ark.account.pubkey],
queryFn: async () => { queryFn: async () => {
const user = ndk.getUser({ pubkey: db.account.pubkey }); const relays = await ark.getUserRelays({});
const relays = await user.relayList();
if (!relays) return Promise.reject(new Error("user's relay set not found"));
return relays; return relays;
}, },
refetchOnWindowFocus: false, refetchOnWindowFocus: false,

View File

@@ -1,19 +1,19 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { fetch } from '@tauri-apps/plugin-http'; import { fetch } from '@tauri-apps/plugin-http';
import { useStorage } from '@libs/storage/provider'; import { useArk } from '@libs/ark';
import { LoaderIcon } from '@shared/icons'; import { LoaderIcon } from '@shared/icons';
import { compactNumber } from '@utils/number'; import { compactNumber } from '@utils/number';
export function ZapCard() { export function ZapCard() {
const { db } = useStorage(); const { ark } = useArk();
const { status, data } = useQuery({ const { status, data } = useQuery({
queryKey: ['user-stats', db.account.pubkey], queryKey: ['user-stats', ark.account.pubkey],
queryFn: async ({ signal }: { signal: AbortSignal }) => { queryFn: async ({ signal }: { signal: AbortSignal }) => {
const res = await fetch( const res = await fetch(
`https://api.nostr.band/v0/stats/profile/${db.account.pubkey}`, `https://api.nostr.band/v0/stats/profile/${ark.account.pubkey}`,
{ {
signal, signal,
} }
@@ -41,7 +41,7 @@ export function ZapCard() {
<div className="flex h-full w-full flex-col justify-between p-4"> <div className="flex h-full w-full flex-col justify-between p-4">
<h3 className="pt-1 text-5xl font-semibold tabular-nums text-neutral-900 dark:text-neutral-100"> <h3 className="pt-1 text-5xl font-semibold tabular-nums text-neutral-900 dark:text-neutral-100">
{compactNumber.format( {compactNumber.format(
data?.stats[db.account.pubkey]?.zaps_received?.msats / 1000 || 0 data?.stats[ark.account.pubkey]?.zaps_received?.msats / 1000 || 0
)} )}
</h3> </h3>
<div className="mt-auto flex h-6 items-center text-xl font-medium leading-none text-neutral-600 dark:text-neutral-400"> <div className="mt-auto flex h-6 items-center text-xl font-medium leading-none text-neutral-600 dark:text-neutral-400">

View File

@@ -1,21 +1,16 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useNDK } from '@libs/ndk/provider'; import { useArk } from '@libs/ark';
import { useStorage } from '@libs/storage/provider';
import { LoaderIcon } from '@shared/icons'; import { LoaderIcon } from '@shared/icons';
import { User } from '@shared/user'; import { User } from '@shared/user';
export function EditContactScreen() { export function EditContactScreen() {
const { db } = useStorage(); const { ark } = useArk();
const { ndk } = useNDK();
const { status, data } = useQuery({ const { status, data } = useQuery({
queryKey: ['contacts'], queryKey: ['contacts'],
queryFn: async () => { queryFn: async () => {
const user = ndk.getUser({ pubkey: db.account.pubkey }); return await ark.getUserContacts({});
const follows = await user.follows();
return [...follows];
}, },
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
}); });
@@ -29,10 +24,10 @@ export function EditContactScreen() {
) : ( ) : (
data.map((item) => ( data.map((item) => (
<div <div
key={item.pubkey} key={item}
className="flex h-16 w-full items-center justify-between rounded-xl bg-neutral-100 px-2.5 dark:bg-neutral-900" className="flex h-16 w-full items-center justify-between rounded-xl bg-neutral-100 px-2.5 dark:bg-neutral-900"
> >
<User pubkey={item.pubkey} variant="simple" /> <User pubkey={item} variant="simple" />
</div> </div>
)) ))
)} )}

View File

@@ -1,29 +1,21 @@
import { NDKEvent, NDKKind, NDKUserProfile } from '@nostr-dev-kit/ndk'; import { NDKKind, NDKUserProfile } from '@nostr-dev-kit/ndk';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { message } from '@tauri-apps/plugin-dialog'; import { message } from '@tauri-apps/plugin-dialog';
import { useState } from 'react'; import { useState } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useNDK } from '@libs/ndk/provider'; import { useArk } from '@libs/ark';
import { useStorage } from '@libs/storage/provider';
import { CheckCircleIcon, LoaderIcon, PlusIcon, UnverifiedIcon } from '@shared/icons'; import { CheckCircleIcon, LoaderIcon, PlusIcon, UnverifiedIcon } from '@shared/icons';
import { useNostr } from '@utils/hooks/useNostr';
export function EditProfileScreen() { export function EditProfileScreen() {
const queryClient = useQueryClient();
const navigate = useNavigate();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [picture, setPicture] = useState(''); const [picture, setPicture] = useState('');
const [banner, setBanner] = useState(''); const [banner, setBanner] = useState('');
const [nip05, setNIP05] = useState({ verified: true, text: '' }); const [nip05, setNIP05] = useState({ verified: true, text: '' });
const { db } = useStorage(); const { ark } = useArk();
const { ndk } = useNDK();
const { upload } = useNostr();
const { const {
register, register,
handleSubmit, handleSubmit,
@@ -32,7 +24,7 @@ export function EditProfileScreen() {
formState: { isValid, errors }, formState: { isValid, errors },
} = useForm({ } = useForm({
defaultValues: async () => { defaultValues: async () => {
const res: NDKUserProfile = queryClient.getQueryData(['user', db.account.pubkey]); const res: NDKUserProfile = queryClient.getQueryData(['user', ark.account.pubkey]);
if (res.image) { if (res.image) {
setPicture(res.image); setPicture(res.image);
} }
@@ -46,13 +38,16 @@ export function EditProfileScreen() {
}, },
}); });
const queryClient = useQueryClient();
const navigate = useNavigate();
const uploadAvatar = async () => { const uploadAvatar = async () => {
try { try {
if (!ndk.signer) return navigate('/new/privkey'); if (!ark.readyToSign) return navigate('/new/privkey');
setLoading(true); setLoading(true);
const image = await upload(); const image = await ark.upload({});
if (image) { if (image) {
setPicture(image); setPicture(image);
setLoading(false); setLoading(false);
@@ -67,7 +62,7 @@ export function EditProfileScreen() {
try { try {
setLoading(true); setLoading(true);
const image = await upload(); const image = await ark.upload({});
if (image) { if (image) {
setBanner(image); setBanner(image);
@@ -83,7 +78,7 @@ export function EditProfileScreen() {
// start loading // start loading
setLoading(true); setLoading(true);
const content = { let content = {
...data, ...data,
username: data.name, username: data.name,
display_name: data.name, display_name: data.name,
@@ -91,15 +86,10 @@ export function EditProfileScreen() {
image: data.picture, image: data.picture,
}; };
const event = new NDKEvent(ndk);
event.kind = NDKKind.Metadata;
event.tags = [];
if (data.nip05) { if (data.nip05) {
const user = ndk.getUser({ pubkey: db.account.pubkey }); const verify = ark.validateNIP05({ pubkey: ark.account.pubkey, nip05: data.nip05 });
const verify = await user.validateNip05(data.nip05);
if (verify) { if (verify) {
event.content = JSON.stringify({ ...content, nip05: data.nip05 }); content = { ...content, nip05: data.nip05 };
} else { } else {
setNIP05((prev) => ({ ...prev, verified: false })); setNIP05((prev) => ({ ...prev, verified: false }));
setError('nip05', { setError('nip05', {
@@ -107,16 +97,19 @@ export function EditProfileScreen() {
message: "Can't verify your Lume ID / NIP-05, please check again", message: "Can't verify your Lume ID / NIP-05, please check again",
}); });
} }
} else {
event.content = JSON.stringify(content);
} }
const publishedRelays = await event.publish(); const publish = await ark.createEvent({
kind: NDKKind.Metadata,
tags: [],
content: JSON.stringify(content),
publish: true,
});
if (publishedRelays) { if (publish) {
// invalid cache // invalid cache
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: ['user', db.account.pubkey], queryKey: ['user', ark.account.pubkey],
}); });
// reset form // reset form
reset(); reset();

View File

@@ -6,12 +6,12 @@ import { isPermissionGranted, requestPermission } from '@tauri-apps/plugin-notif
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
import { useStorage } from '@libs/storage/provider'; import { useArk } from '@libs/ark';
import { DarkIcon, LightIcon, SystemModeIcon } from '@shared/icons'; import { DarkIcon, LightIcon, SystemModeIcon } from '@shared/icons';
export function GeneralSettingScreen() { export function GeneralSettingScreen() {
const { db } = useStorage(); const { ark } = useArk();
const [settings, setSettings] = useState({ const [settings, setSettings] = useState({
autoupdate: false, autoupdate: false,
autolaunch: false, autolaunch: false,
@@ -41,28 +41,28 @@ export function GeneralSettingScreen() {
}; };
const toggleOutbox = async () => { const toggleOutbox = async () => {
await db.createSetting('outbox', String(+!settings.outbox)); await ark.createSetting('outbox', String(+!settings.outbox));
// update state // update state
setSettings((prev) => ({ ...prev, outbox: !settings.outbox })); setSettings((prev) => ({ ...prev, outbox: !settings.outbox }));
}; };
const toggleMedia = async () => { const toggleMedia = async () => {
await db.createSetting('media', String(+!settings.media)); await ark.createSetting('media', String(+!settings.media));
db.settings.media = !settings.media; ark.settings.media = !settings.media;
// update state // update state
setSettings((prev) => ({ ...prev, media: !settings.media })); setSettings((prev) => ({ ...prev, media: !settings.media }));
}; };
const toggleHashtag = async () => { const toggleHashtag = async () => {
await db.createSetting('hashtag', String(+!settings.hashtag)); await ark.createSetting('hashtag', String(+!settings.hashtag));
db.settings.hashtag = !settings.hashtag; ark.settings.hashtag = !settings.hashtag;
// update state // update state
setSettings((prev) => ({ ...prev, hashtag: !settings.hashtag })); setSettings((prev) => ({ ...prev, hashtag: !settings.hashtag }));
}; };
const toggleAutoupdate = async () => { const toggleAutoupdate = async () => {
await db.createSetting('autoupdate', String(+!settings.autoupdate)); await ark.createSetting('autoupdate', String(+!settings.autoupdate));
db.settings.autoupdate = !settings.autoupdate; ark.settings.autoupdate = !settings.autoupdate;
// update state // update state
setSettings((prev) => ({ ...prev, autoupdate: !settings.autoupdate })); setSettings((prev) => ({ ...prev, autoupdate: !settings.autoupdate }));
}; };
@@ -86,7 +86,7 @@ export function GeneralSettingScreen() {
const permissionGranted = await isPermissionGranted(); const permissionGranted = await isPermissionGranted();
setSettings((prev) => ({ ...prev, notification: permissionGranted })); setSettings((prev) => ({ ...prev, notification: permissionGranted }));
const data = await db.getAllSettings(); const data = await ark.getAllSettings();
if (!data) return; if (!data) return;
data.forEach((item) => { data.forEach((item) => {

View File

@@ -1,4 +1,3 @@
import { NDKEvent, NDKKind, NDKUser } from '@nostr-dev-kit/ndk';
import * as Avatar from '@radix-ui/react-avatar'; import * as Avatar from '@radix-ui/react-avatar';
import { minidenticon } from 'minidenticons'; import { minidenticon } from 'minidenticons';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -7,8 +6,7 @@ import { toast } from 'sonner';
import { UserStats } from '@app/users/components/stats'; import { UserStats } from '@app/users/components/stats';
import { useNDK } from '@libs/ndk/provider'; import { useArk } from '@libs/ark';
import { useStorage } from '@libs/storage/provider';
import { NIP05 } from '@shared/nip05'; import { NIP05 } from '@shared/nip05';
@@ -16,8 +14,7 @@ import { useProfile } from '@utils/hooks/useProfile';
import { displayNpub } from '@utils/shortenKey'; import { displayNpub } from '@utils/shortenKey';
export function UserProfile({ pubkey }: { pubkey: string }) { export function UserProfile({ pubkey }: { pubkey: string }) {
const { db } = useStorage(); const { ark } = useArk();
const { ndk } = useNDK();
const { user } = useProfile(pubkey); const { user } = useProfile(pubkey);
const [followed, setFollowed] = useState(false); const [followed, setFollowed] = useState(false);
@@ -28,12 +25,10 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
const follow = async () => { const follow = async () => {
try { try {
if (!ndk.signer) return navigate('/new/privkey'); if (!ark.readyToSign) return navigate('/new/privkey');
setFollowed(true); setFollowed(true);
const user = ndk.getUser({ pubkey: db.account.pubkey }); const add = await ark.createContact({ pubkey });
const contacts = await user.follows();
const add = await user.follow(new NDKUser({ pubkey: pubkey }), contacts);
if (!add) { if (!add) {
toast.success('You already follow this user'); toast.success('You already follow this user');
@@ -47,32 +42,17 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
const unfollow = async () => { const unfollow = async () => {
try { try {
if (!ndk.signer) return navigate('/new/privkey'); if (!ark.readyToSign) return navigate('/new/privkey');
setFollowed(false); setFollowed(false);
const user = ndk.getUser({ pubkey: db.account.pubkey }); await ark.deleteContact({ pubkey });
const contacts = await user.follows();
contacts.delete(new NDKUser({ pubkey: pubkey }));
const list = [...contacts].map((item) => [
'p',
item.pubkey,
item.relayUrls?.[0] || '',
'',
]);
const event = new NDKEvent(ndk);
event.content = '';
event.kind = NDKKind.Contacts;
event.tags = list;
await event.publish();
} catch (e) { } catch (e) {
toast.error(e); toast.error(e);
} }
}; };
useEffect(() => { useEffect(() => {
if (db.account.contacts.includes(pubkey)) { if (ark.account.contacts.includes(pubkey)) {
setFollowed(true); setFollowed(true);
} }
}, []); }, []);

View File

@@ -1,30 +1,60 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk'; import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { useQuery } from '@tanstack/react-query'; import { useInfiniteQuery } from '@tanstack/react-query';
import { useCallback } from 'react'; import { useCallback, useMemo } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { UserProfile } from '@app/users/components/profile'; import { UserProfile } from '@app/users/components/profile';
import { useNDK } from '@libs/ndk/provider'; import { useArk } from '@libs/ark';
import { MemoizedRepost, MemoizedTextNote, UnknownNote } from '@shared/notes'; import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
import {
MemoizedRepost,
MemoizedTextNote,
NoteSkeleton,
UnknownNote,
} from '@shared/notes';
import { FETCH_LIMIT } from '@utils/constants';
export function UserScreen() { export function UserScreen() {
const { pubkey } = useParams(); const { pubkey } = useParams();
const { ndk } = useNDK(); const { ark } = useArk();
const { status, data } = useQuery({ const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
queryKey: ['user-feed', pubkey], useInfiniteQuery({
queryFn: async () => { queryKey: ['user-posts', pubkey],
const events = await ndk.fetchEvents({ initialPageParam: 0,
kinds: [NDKKind.Text, NDKKind.Repost], queryFn: async ({
authors: [pubkey], signal,
limit: 20, pageParam,
}); }: {
const sorted = [...events].sort((a, b) => b.created_at - a.created_at); signal: AbortSignal;
return sorted; pageParam: number;
}, }) => {
refetchOnWindowFocus: false, const events = await ark.getInfiniteEvents({
}); filter: {
kinds: [NDKKind.Text, NDKKind.Repost],
authors: [pubkey],
},
limit: FETCH_LIMIT,
pageParam,
signal,
});
return events;
},
getNextPageParam: (lastPage) => {
const lastEvent = lastPage.at(-1);
if (!lastEvent) return;
return lastEvent.created_at - 1;
},
refetchOnWindowFocus: false,
});
const allEvents = useMemo(
() => (data ? data.pages.flatMap((page) => page) : []),
[data]
);
// render event match event kind // render event match event kind
const renderItem = useCallback( const renderItem = useCallback(
@@ -50,20 +80,33 @@ export function UserScreen() {
</h3> </h3>
<div className="mx-auto flex h-full max-w-[500px] flex-col justify-between gap-1.5 pb-4 pt-1.5"> <div className="mx-auto flex h-full max-w-[500px] flex-col justify-between gap-1.5 pb-4 pt-1.5">
{status === 'pending' ? ( {status === 'pending' ? (
<div>Loading...</div>
) : data.length === 0 ? (
<div className="px-3 py-1.5"> <div className="px-3 py-1.5">
<div className="rounded-xl bg-neutral-100 px-3 py-6 dark:bg-neutral-900"> <div className="rounded-xl bg-neutral-100 px-3 py-3 dark:bg-neutral-900">
<div className="flex flex-col items-center gap-4"> <NoteSkeleton />
<p className="text-center text-sm font-medium text-neutral-900 dark:text-neutral-100">
User doesn&apos;t have any posts in the last 48 hours.
</p>
</div>
</div> </div>
</div> </div>
) : ( ) : (
data.map((item) => renderItem(item)) allEvents.map((item) => renderItem(item))
)} )}
<div className="flex h-16 items-center justify-center px-3 pb-3">
{hasNextPage ? (
<button
type="button"
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
className="inline-flex h-10 w-max items-center justify-center gap-2 rounded-full bg-blue-500 px-6 font-medium text-white hover:bg-blue-600 focus:outline-none"
>
{isFetchingNextPage ? (
<LoaderIcon className="h-4 w-4 animate-spin" />
) : (
<>
<ArrowRightCircleIcon className="h-5 w-5" />
Load more
</>
)}
</button>
) : null}
</div>
</div> </div>
</div> </div>
</div> </div>

915
src/libs/ark/ark.ts Normal file
View File

@@ -0,0 +1,915 @@
import NDK, {
NDKEvent,
NDKFilter,
NDKKind,
NDKNip46Signer,
NDKPrivateKeySigner,
NDKSubscriptionCacheUsage,
NDKTag,
NDKUser,
NostrEvent,
} from '@nostr-dev-kit/ndk';
import { ndkAdapter } from '@nostr-fetch/adapter-ndk';
import { invoke } from '@tauri-apps/api/primitives';
import { open } from '@tauri-apps/plugin-dialog';
import { readBinaryFile } from '@tauri-apps/plugin-fs';
import { fetch } from '@tauri-apps/plugin-http';
import { Platform } from '@tauri-apps/plugin-os';
import Database from '@tauri-apps/plugin-sql';
import {
NostrEventExt,
NostrFetcher,
normalizeRelayUrl,
normalizeRelayUrlSet,
} from 'nostr-fetch';
import { toast } from 'sonner';
import { NDKCacheAdapterTauri } from '@libs/ark';
import {
Account,
NDKCacheUser,
NDKCacheUserProfile,
NDKEventWithReplies,
NIP05,
Widget,
} from '@utils/types';
export class Ark {
#ndk: NDK;
#fetcher: NostrFetcher;
#storage: Database;
public account: Account | null;
public relays: string[] | null;
public readyToSign: boolean;
readonly platform: Platform | null;
readonly settings: {
autoupdate: boolean;
outbox: boolean;
media: boolean;
hashtag: boolean;
};
constructor({ storage, platform }: { storage: Database; platform: Platform }) {
this.#storage = storage;
this.platform = platform;
this.settings = {
autoupdate: false,
outbox: false,
media: true,
hashtag: true,
};
}
async #keyring_save(key: string, value: string) {
return await invoke('secure_save', { key, value });
}
async #keyring_load(key: string) {
try {
const value: string = await invoke('secure_load', { key });
if (!value) return null;
return value;
} catch {
return null;
}
}
async #keyring_remove(key: string) {
return await invoke('secure_remove', { key });
}
async #initNostrSigner({ nsecbunker }: { nsecbunker?: boolean }) {
const account = await this.getActiveAccount();
if (!account) return null;
// update active account
this.account = account;
try {
// NIP-46 Signer
if (nsecbunker) {
const localSignerPrivkey = await this.#keyring_load(
`${this.account.id}-nsecbunker`
);
if (!localSignerPrivkey) {
this.readyToSign = false;
return null;
}
const localSigner = new NDKPrivateKeySigner(localSignerPrivkey);
const bunker = new NDK({
explicitRelayUrls: ['wss://relay.nsecbunker.com', 'wss://nostr.vulpem.com'],
});
await bunker.connect();
const remoteSigner = new NDKNip46Signer(bunker, this.account.pubkey, localSigner);
await remoteSigner.blockUntilReady();
this.readyToSign = true;
return remoteSigner;
}
// Privkey Signer
const userPrivkey = await this.#keyring_load(this.account.pubkey);
if (!userPrivkey) {
this.readyToSign = false;
return null;
}
this.readyToSign = true;
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 this.logout();
}
this.readyToSign = false;
return null;
}
}
public async init() {
const outboxSetting = await this.getSettingValue('outbox');
const bunkerSetting = await this.getSettingValue('nsecbunker');
const bunker = !!parseInt(bunkerSetting);
const enableOutboxModel = !!parseInt(outboxSetting);
const explicitRelayUrls = normalizeRelayUrlSet([
'wss://relay.damus.io',
'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']);
const cacheAdapter = new NDKCacheAdapterTauri(this.#storage);
const ndk = new NDK({
cacheAdapter,
explicitRelayUrls,
outboxRelayUrls,
blacklistRelayUrls,
enableOutboxModel,
autoConnectUserRelays: true,
autoFetchUserMutelist: true,
// clientName: 'Lume',
// clientNip89: '',
});
// add signer if exist
const signer = await this.#initNostrSigner({ nsecbunker: bunker });
if (signer) ndk.signer = signer;
// connect
await ndk.connect();
const fetcher = NostrFetcher.withCustomPool(ndkAdapter(ndk));
// update account's metadata
if (this.account) {
const user = ndk.getUser({ pubkey: this.account.pubkey });
ndk.activeUser = user;
const contacts = await user.follows(undefined /* outbox */);
this.account.contacts = [...contacts].map((user) => user.pubkey);
}
this.relays = [...ndk.pool.relays.values()].map((relay) => relay.url);
this.#ndk = ndk;
this.#fetcher = fetcher;
}
public updateNostrSigner({ signer }: { signer: NDKNip46Signer | NDKPrivateKeySigner }) {
this.#ndk.signer = signer;
this.readyToSign = true;
return this.#ndk.signer;
}
public async getAllCacheUsers() {
const results: Array<NDKCacheUser> = await this.#storage.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 checkAccount() {
const result: Array<{ total: string }> = await this.#storage.select(
'SELECT COUNT(*) AS "total" FROM accounts WHERE is_active = "1" ORDER BY id DESC LIMIT 1;'
);
return parseInt(result[0].total);
}
public async getActiveAccount() {
const results: Array<Account> = await this.#storage.select(
'SELECT * FROM accounts WHERE is_active = "1" ORDER BY id DESC LIMIT 1;'
);
if (results.length) {
return results[0];
} else {
return null;
}
}
public async createAccount({
id,
pubkey,
privkey,
}: {
id: string;
pubkey: string;
privkey?: string;
}) {
const existAccounts: Array<Account> = await this.#storage.select(
'SELECT * FROM accounts WHERE pubkey = $1 ORDER BY id DESC LIMIT 1;',
[pubkey]
);
if (existAccounts.length) {
await this.#storage.execute(
"UPDATE accounts SET is_active = '1' WHERE pubkey = $1;",
[pubkey]
);
} else {
await this.#storage.execute(
'INSERT OR IGNORE INTO accounts (id, pubkey, is_active) VALUES ($1, $2, $3);',
[id, pubkey, 1]
);
if (privkey) await this.#keyring_save(pubkey, privkey);
}
const account = await this.getActiveAccount();
this.account = account;
this.account.contacts = [];
return account;
}
/**
* Save private key to OS secure storage
* @deprecated this method will be remove in the next update
*/
public async createPrivkey(name: string, privkey: string) {
return await this.#keyring_save(name, privkey);
}
/**
* Load private key from OS secure storage
* @deprecated this method will be remove in the next update
*/
public async loadPrivkey(name: string) {
return await this.#keyring_load(name);
}
/**
* Remove private key from OS secure storage
* @deprecated this method will be remove in the next update
*/
public async removePrivkey(name: string) {
return await this.#keyring_remove(name);
}
public async updateAccount(column: string, value: string) {
const insert = await this.#storage.execute(
`UPDATE accounts SET ${column} = $1 WHERE id = $2;`,
[value, this.account.id]
);
if (insert) {
const account = await this.getActiveAccount();
return account;
}
}
public async getWidgets() {
const widgets: Array<Widget> = await this.#storage.select(
'SELECT * FROM widgets WHERE account_id = $1 ORDER BY created_at DESC;',
[this.account.id]
);
return widgets;
}
public async createWidget(kind: number, title: string, content: string | string[]) {
const insert = await this.#storage.execute(
'INSERT INTO widgets (account_id, kind, title, content) VALUES ($1, $2, $3, $4);',
[this.account.id, kind, title, content]
);
if (insert) {
const widgets: Array<Widget> = await this.#storage.select(
'SELECT * FROM widgets ORDER BY id DESC LIMIT 1;'
);
if (widgets.length < 1) console.error('get created widget failed');
return widgets[0];
} else {
console.error('create widget failed');
}
}
public async removeWidget(id: string) {
const res = await this.#storage.execute('DELETE FROM widgets WHERE id = $1;', [id]);
if (res) return id;
}
public async createSetting(key: string, value: string | undefined) {
if (value) {
return await this.#storage.execute(
'INSERT OR IGNORE INTO settings (key, value) VALUES ($1, $2);',
[key, value]
);
}
const currentSetting = await this.checkSettingValue(key);
if (!currentSetting)
return await this.#storage.execute(
'INSERT OR IGNORE INTO settings (key, value) VALUES ($1, $2);',
[key, value]
);
const currentValue = !!parseInt(currentSetting);
return await this.#storage.execute('UPDATE settings SET value = $1 WHERE key = $2;', [
+!currentValue,
key,
]);
}
public async getAllSettings() {
const results: { key: string; value: string }[] = await this.#storage.select(
'SELECT * FROM settings ORDER BY id DESC;'
);
if (results.length < 1) return null;
return results;
}
public async checkSettingValue(key: string) {
const results: { key: string; value: string }[] = await this.#storage.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) {
const results: { key: string; value: string }[] = await this.#storage.select(
'SELECT * FROM settings WHERE key = $1 ORDER BY id DESC LIMIT 1;',
[key]
);
if (!results.length) return '0';
return results[0].value;
}
public async clearCache() {
await this.#storage.execute('DELETE FROM ndk_events;');
await this.#storage.execute('DELETE FROM ndk_eventtags;');
await this.#storage.execute('DELETE FROM ndk_users;');
}
public async logout() {
await this.#storage.execute("UPDATE accounts SET is_active = '0' WHERE id = $1;", [
this.account.id,
]);
await this.#keyring_remove(this.account.pubkey);
await this.#keyring_remove(`${this.account.pubkey}-nsecbunker`);
this.account = null;
this.#ndk.signer = null;
}
public subscribe({
filter,
closeOnEose = false,
cb,
}: {
filter: NDKFilter;
closeOnEose: boolean;
cb: (event: NDKEvent) => void;
}) {
const sub = this.#ndk.subscribe(filter, { closeOnEose });
sub.addListener('event', (event: NDKEvent) => cb(event));
return sub;
}
public createNDKEvent({ event }: { event: NostrEvent | NostrEventExt }) {
return new NDKEvent(this.#ndk, event);
}
public async createEvent({
kind,
tags,
content,
rootReplyTo = undefined,
replyTo = undefined,
publish,
}: {
kind: NDKKind | number;
tags: NDKTag[];
content?: string;
rootReplyTo?: string;
replyTo?: string;
publish?: boolean;
}) {
try {
const event = new NDKEvent(this.#ndk);
if (content) event.content = content;
event.kind = kind;
event.tags = tags;
if (rootReplyTo) {
const rootEvent = await this.#ndk.fetchEvent(rootReplyTo);
if (rootEvent) event.tag(rootEvent, 'root');
}
if (replyTo) {
const replyEvent = await this.#ndk.fetchEvent(replyTo);
if (replyEvent) event.tag(replyEvent, 'reply');
}
if (publish) {
const publishedEvent = await event.publish();
if (!publishedEvent) throw new Error('Failed to publish event');
return publishedEvent.size;
}
return event;
} catch (e) {
throw new Error(e);
}
}
public async getUserProfile({ pubkey }: { pubkey: string }) {
try {
const user = this.#ndk.getUser({ pubkey });
const profile = await user.fetchProfile({
cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST,
});
if (!profile) return null;
return profile;
} catch (e) {
console.error(e);
return null;
}
}
public async getUserContacts({
pubkey = undefined,
outbox = undefined,
}: {
pubkey?: string;
outbox?: boolean;
}) {
try {
const user = this.#ndk.getUser({ pubkey: pubkey ? pubkey : this.account.pubkey });
const contacts = [...(await user.follows(undefined, outbox))].map(
(user) => user.pubkey
);
if (pubkey === this.account.pubkey) this.account.contacts = contacts;
return contacts;
} catch (e) {
console.error(e);
return [];
}
}
public async getUserRelays({ pubkey }: { pubkey?: string }) {
try {
const user = this.#ndk.getUser({ pubkey: pubkey ? pubkey : this.account.pubkey });
return await user.relayList();
} catch (e) {
console.error(e);
return null;
}
}
public async createContact({ pubkey }: { pubkey: string }) {
const user = this.#ndk.getUser({ pubkey: this.account.pubkey });
const contacts = await user.follows();
return await user.follow(new NDKUser({ pubkey: pubkey }), contacts);
}
public async deleteContact({ pubkey }: { pubkey: string }) {
const user = this.#ndk.getUser({ pubkey: this.account.pubkey });
const contacts = await user.follows();
contacts.delete(new NDKUser({ pubkey: pubkey }));
const event = new NDKEvent(this.#ndk);
event.content = '';
event.kind = NDKKind.Contacts;
event.tags = [...contacts].map((item) => [
'p',
item.pubkey,
item.relayUrls?.[0] || '',
'',
]);
return await event.publish();
}
public async getAllEvents({ filter }: { filter: NDKFilter }) {
const events = await this.#ndk.fetchEvents(filter);
if (!events) return [];
return [...events];
}
public async getEventById({ id }: { id: string }) {
const event = await this.#ndk.fetchEvent(id, {
cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST,
});
if (!event) return null;
return event;
}
public async getEventByFilter({ filter }: { filter: NDKFilter }) {
const event = await this.#ndk.fetchEvent(filter, {
cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST,
});
if (!event) return null;
return event;
}
public getEventThread({ tags }: { tags: NDKTag[] }) {
let rootEventId: string = null;
let replyEventId: string = null;
const events = tags.filter((el) => el[0] === 'e');
if (!events.length) return null;
if (events.length === 1)
return {
rootEventId: events[0][1],
replyEventId: null,
};
if (events.length > 1) {
rootEventId = events.find((el) => el[3] === 'root')?.[1];
replyEventId = events.find((el) => el[3] === 'reply')?.[1];
if (!rootEventId && !replyEventId) {
rootEventId = events[0][1];
replyEventId = events[1][1];
}
}
return {
rootEventId,
replyEventId,
};
}
public async getThreads({ id, data }: { id: string; data?: NDKEventWithReplies[] }) {
let events = data || null;
if (!data) {
const relayUrls = [...this.#ndk.pool.relays.values()].map((item) => item.url);
const rawEvents = (await this.#fetcher.fetchAllEvents(
relayUrls,
{
kinds: [NDKKind.Text],
'#e': [id],
},
{ since: 0 },
{ sort: true }
)) as unknown as NostrEvent[];
events = rawEvents.map(
(event) => new NDKEvent(this.#ndk, event)
) as NDKEvent[] as NDKEventWithReplies[];
}
if (events.length > 0) {
const replies = new Set();
events.forEach((event) => {
const tags = event.tags.filter((el) => el[0] === 'e' && el[1] !== id);
if (tags.length > 0) {
tags.forEach((tag) => {
const rootIndex = events.findIndex((el) => el.id === tag[1]);
if (rootIndex !== -1) {
const rootEvent = events[rootIndex];
if (rootEvent && rootEvent.replies) {
rootEvent.replies.push(event);
} else {
rootEvent.replies = [event];
}
replies.add(event.id);
}
});
}
});
const cleanEvents = events.filter((ev) => !replies.has(ev.id));
return cleanEvents;
}
return events;
}
public async getAllRelaysFromContacts() {
const LIMIT = 1;
const relayMap = new Map<string, string[]>();
const relayEvents = this.#fetcher.fetchLatestEventsPerAuthor(
{
authors: this.account.contacts,
relayUrls: this.relays,
},
{ kinds: [NDKKind.RelayList] },
LIMIT
);
for await (const { author, events } of relayEvents) {
if (events[0]) {
events[0].tags.forEach((tag) => {
const users = relayMap.get(tag[1]);
if (!users) return relayMap.set(tag[1], [author]);
return users.push(author);
});
}
}
return relayMap;
}
public async getInfiniteEvents({
filter,
limit,
pageParam = 0,
signal = undefined,
dedup = true,
}: {
filter: NDKFilter;
limit: number;
pageParam?: number;
signal?: AbortSignal;
dedup?: boolean;
}) {
const rootIds = new Set();
const dedupQueue = new Set();
const events = await this.#fetcher.fetchLatestEvents(this.relays, filter, limit, {
asOf: pageParam === 0 ? undefined : pageParam,
abortSignal: signal,
});
const ndkEvents = events.map((event) => {
return new NDKEvent(this.#ndk, event);
});
if (dedup) {
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);
}
return ndkEvents.sort((a, b) => b.created_at - a.created_at);
}
public async getRelayEvents({
relayUrl,
filter,
limit,
pageParam = 0,
signal = undefined,
}: {
relayUrl: string;
filter: NDKFilter;
limit: number;
pageParam?: number;
signal?: AbortSignal;
dedup?: boolean;
}) {
const events = await this.#fetcher.fetchLatestEvents(
[normalizeRelayUrl(relayUrl)],
filter,
limit,
{
asOf: pageParam === 0 ? undefined : pageParam,
abortSignal: signal,
}
);
const ndkEvents = events.map((event) => {
return new NDKEvent(this.#ndk, event);
});
return ndkEvents.sort((a, b) => b.created_at - a.created_at);
}
/**
* Upload media file to nostr.build
* @todo support multiple backends
*/
public async upload({ fileExts }: { fileExts?: string[] }) {
const defaultExts = ['png', 'jpeg', 'jpg', 'gif'].concat(fileExts);
const selected = await open({
multiple: false,
filters: [
{
name: 'Image',
extensions: defaultExts,
},
],
});
if (!selected) return null;
const file = await readBinaryFile(selected.path);
const blob = new Blob([file]);
const data = new FormData();
data.append('fileToUpload', blob);
data.append('submit', 'Upload Image');
const res = await fetch('https://nostr.build/api/v2/upload/files', {
method: 'POST',
body: data,
});
if (!res.ok) return null;
const json = await res.json();
const content = json.data[0];
return content.url as string;
}
public async validateNIP05({
pubkey,
nip05,
signal,
}: {
pubkey: string;
nip05: string;
signal?: AbortSignal;
}) {
const localPath = nip05.split('@')[0];
const service = nip05.split('@')[1];
const verifyURL = `https://${service}/.well-known/nostr.json?name=${localPath}`;
const res = await fetch(verifyURL, {
method: 'GET',
headers: {
'Content-Type': 'application/json; charset=utf-8',
},
signal,
});
if (!res.ok) throw new Error(`Failed to fetch NIP-05 service: ${nip05}`);
const data: NIP05 = await res.json();
if (!data.names) return false;
if (data.names[localPath.toLowerCase()] === pubkey) return true;
if (data.names[localPath] === pubkey) return true;
return false;
}
/**
* Return all NIP-04 messages
* @deprecated NIP-04 will be replace by NIP-44 in the next update
*/
public async getAllChats() {
const events = await this.#fetcher.fetchAllEvents(
this.relays,
{
kinds: [NDKKind.EncryptedDirectMessage],
'#p': [this.account.pubkey],
},
{ since: 0 }
);
const dedup: NDKEvent[] = Object.values(
events.reduce((ev, { id, content, pubkey, created_at, tags }) => {
if (ev[pubkey]) {
if (ev[pubkey].created_at < created_at) {
ev[pubkey] = { id, content, pubkey, created_at, tags };
}
} else {
ev[pubkey] = { id, content, pubkey, created_at, tags };
}
return ev;
}, {})
);
return dedup;
}
/**
* Return all NIP-04 messages by pubkey
* @deprecated NIP-04 will be replace by NIP-44 in the next update
*/
public async getAllMessagesByPubkey({ pubkey }: { pubkey: string }) {
let senderMessages: NostrEventExt<false>[] = [];
if (pubkey !== this.account.pubkey) {
senderMessages = await this.#fetcher.fetchAllEvents(
this.relays,
{
kinds: [NDKKind.EncryptedDirectMessage],
authors: [pubkey],
'#p': [this.account.pubkey],
},
{ since: 0 }
);
}
const userMessages = await this.#fetcher.fetchAllEvents(
this.relays,
{
kinds: [NDKKind.EncryptedDirectMessage],
authors: [this.account.pubkey],
'#p': [pubkey],
},
{ since: 0 }
);
const all = [...senderMessages, ...userMessages].sort(
(a, b) => a.created_at - b.created_at
);
return all as unknown as NDKEvent[];
}
public async nip04Decrypt({ event }: { event: NDKEvent }) {
try {
const sender = new NDKUser({
pubkey:
this.account.pubkey === event.pubkey
? event.tags.find((el) => el[0] === 'p')[1]
: event.pubkey,
});
const content = await this.#ndk.signer.decrypt(sender, event.content);
return content;
} catch (e) {
console.error(e);
}
}
public async nip04Encrypt({ content, pubkey }: { content: string; pubkey: string }) {
try {
const recipient = new NDKUser({ pubkey });
const message = await this.#ndk.signer.encrypt(recipient, content);
const event = new NDKEvent(this.#ndk);
event.content = message;
event.kind = NDKKind.EncryptedDirectMessage;
event.tag(recipient);
const publish = await event.publish();
if (!publish) throw new Error('Failed to send NIP-04 encrypted message');
return publish;
} catch (e) {
console.error(e);
}
}
public async replyTo({ content, event }: { content: string; event: NDKEvent }) {
try {
const replyEvent = new NDKEvent(this.#ndk);
replyEvent.content = content;
replyEvent.kind = NDKKind.Text;
replyEvent.tag(event, 'reply');
return await replyEvent.publish();
} catch (e) {
console.error(e);
}
}
}

View File

@@ -1,26 +1,30 @@
// inspired by: https://github.com/nostr-dev-kit/ndk/tree/master/ndk-cache-dexie // inspired by NDK Cache Dexie
import { NDKEvent, NDKRelay, profileFromEvent } from '@nostr-dev-kit/ndk'; // source: https://github.com/nostr-dev-kit/ndk/tree/master/ndk-cache-dexie
import type { import {
Hexpubkey, Hexpubkey,
NDKCacheAdapter, NDKCacheAdapter,
NDKEvent,
NDKFilter, NDKFilter,
NDKRelay,
NDKSubscription, NDKSubscription,
NDKUserProfile, NDKUserProfile,
NostrEvent, profileFromEvent,
} from '@nostr-dev-kit/ndk'; } from '@nostr-dev-kit/ndk';
import Database from '@tauri-apps/plugin-sql';
import { LRUCache } from 'lru-cache'; import { LRUCache } from 'lru-cache';
import { NostrEvent } from 'nostr-fetch';
import { matchFilter } from 'nostr-tools'; import { matchFilter } from 'nostr-tools';
import { LumeStorage } from '@libs/storage/instance'; import { NDKCacheEvent, NDKCacheEventTag, NDKCacheUser } from '@utils/types';
export default class NDKCacheAdapterTauri implements NDKCacheAdapter { export class NDKCacheAdapterTauri implements NDKCacheAdapter {
public db: LumeStorage; #db: Database;
public profiles?: LRUCache<Hexpubkey, NDKUserProfile>;
private dirtyProfiles: Set<Hexpubkey> = new Set(); private dirtyProfiles: Set<Hexpubkey> = new Set();
public profiles?: LRUCache<Hexpubkey, NDKUserProfile>;
readonly locking: boolean; readonly locking: boolean;
constructor(db: LumeStorage) { constructor(db: Database) {
this.db = db; this.#db = db;
this.locking = true; this.locking = true;
this.profiles = new LRUCache({ this.profiles = new LRUCache({
@@ -32,6 +36,115 @@ export default class NDKCacheAdapterTauri implements NDKCacheAdapter {
}, 1000 * 10); }, 1000 * 10);
} }
async #getCacheUser(pubkey: string) {
const results: Array<NDKCacheUser> = await this.#db.select(
'SELECT * FROM ndk_users WHERE pubkey = $1 ORDER BY pubkey DESC LIMIT 1;',
[pubkey]
);
if (!results.length) return null;
if (typeof results[0].profile === 'string')
results[0].profile = JSON.parse(results[0].profile);
return results[0];
}
async #getCacheEvent(id: string) {
const results: Array<NDKCacheEvent> = await this.#db.select(
'SELECT * FROM ndk_events WHERE id = $1 ORDER BY id DESC LIMIT 1;',
[id]
);
if (!results.length) return null;
return results[0];
}
async #getCacheEvents(ids: string[]) {
const idsArr = `'${ids.join("','")}'`;
const results: Array<NDKCacheEvent> = await this.#db.select(
`SELECT * FROM ndk_events WHERE id IN (${idsArr}) ORDER BY id;`
);
if (!results.length) return [];
return results;
}
async #getCacheEventsByPubkey(pubkey: string) {
const results: Array<NDKCacheEvent> = await this.#db.select(
'SELECT * FROM ndk_events WHERE pubkey = $1 ORDER BY id;',
[pubkey]
);
if (!results.length) return [];
return results;
}
async #getCacheEventsByKind(kind: number) {
const results: Array<NDKCacheEvent> = await this.#db.select(
'SELECT * FROM ndk_events WHERE kind = $1 ORDER BY id;',
[kind]
);
if (!results.length) return [];
return results;
}
async #getCacheEventsByKindAndAuthor(kind: number, pubkey: string) {
const results: Array<NDKCacheEvent> = await this.#db.select(
'SELECT * FROM ndk_events WHERE kind = $1 AND pubkey = $2 ORDER BY id;',
[kind, pubkey]
);
if (!results.length) return [];
return results;
}
async #getCacheEventTagsByTagValue(tagValue: string) {
const results: Array<NDKCacheEventTag> = await this.#db.select(
'SELECT * FROM ndk_eventtags WHERE tagValue = $1 ORDER BY id;',
[tagValue]
);
if (!results.length) return [];
return results;
}
async #setCacheEvent({
id,
pubkey,
content,
kind,
createdAt,
relay,
event,
}: NDKCacheEvent) {
return await this.#db.execute(
'INSERT OR IGNORE INTO ndk_events (id, pubkey, content, kind, createdAt, relay, event) VALUES ($1, $2, $3, $4, $5, $6, $7);',
[id, pubkey, content, kind, createdAt, relay, event]
);
}
async #setCacheEventTag({ id, eventId, tag, value, tagValue }: NDKCacheEventTag) {
return await this.#db.execute(
'INSERT OR IGNORE INTO ndk_eventtags (id, eventId, tag, value, tagValue) VALUES ($1, $2, $3, $4, $5);',
[id, eventId, tag, value, tagValue]
);
}
async #setCacheProfiles(profiles: Array<NDKCacheUser>) {
return await Promise.all(
profiles.map(
async (profile) =>
await this.#db.execute(
'INSERT OR IGNORE INTO ndk_users (pubkey, profile, createdAt) VALUES ($1, $2, $3);',
[profile.pubkey, profile.profile, profile.createdAt]
)
)
);
}
public async query(subscription: NDKSubscription): Promise<void> { public async query(subscription: NDKSubscription): Promise<void> {
Promise.allSettled( Promise.allSettled(
subscription.filters.map((filter) => this.processFilter(filter, subscription)) subscription.filters.map((filter) => this.processFilter(filter, subscription))
@@ -44,7 +157,7 @@ export default class NDKCacheAdapterTauri implements NDKCacheAdapter {
let profile = this.profiles.get(pubkey); let profile = this.profiles.get(pubkey);
if (!profile) { if (!profile) {
const user = await this.db.getCacheUser(pubkey); const user = await this.#getCacheUser(pubkey);
if (user) { if (user) {
profile = user.profile as NDKUserProfile; profile = user.profile as NDKUserProfile;
this.profiles.set(pubkey, profile); this.profiles.set(pubkey, profile);
@@ -97,7 +210,7 @@ export default class NDKCacheAdapterTauri implements NDKCacheAdapter {
if (event.isParamReplaceable()) { if (event.isParamReplaceable()) {
const replaceableId = `${event.kind}:${event.pubkey}:${event.tagId()}`; const replaceableId = `${event.kind}:${event.pubkey}:${event.tagId()}`;
const existingEvent = await this.db.getCacheEvent(replaceableId); const existingEvent = await this.#getCacheEvent(replaceableId);
if ( if (
existingEvent && existingEvent &&
event.created_at && event.created_at &&
@@ -108,7 +221,7 @@ export default class NDKCacheAdapterTauri implements NDKCacheAdapter {
} }
if (addEvent) { if (addEvent) {
this.db.setCacheEvent({ this.#setCacheEvent({
id: event.tagId(), id: event.tagId(),
pubkey: event.pubkey, pubkey: event.pubkey,
content: event.content, content: event.content,
@@ -124,7 +237,7 @@ export default class NDKCacheAdapterTauri implements NDKCacheAdapter {
event.tags.forEach((tag) => { event.tags.forEach((tag) => {
if (tag[0].length !== 1) return; if (tag[0].length !== 1) return;
this.db.setCacheEventTag({ this.#setCacheEventTag({
id: `${event.id}:${tag[0]}:${tag[1]}`, id: `${event.id}:${tag[0]}:${tag[1]}`,
eventId: event.id, eventId: event.id,
tag: tag[0], tag: tag[0],
@@ -153,7 +266,7 @@ export default class NDKCacheAdapterTauri implements NDKCacheAdapter {
if (hasAllKeys && filter.authors) { if (hasAllKeys && filter.authors) {
for (const pubkey of filter.authors) { for (const pubkey of filter.authors) {
const events = await this.db.getCacheEventsByPubkey(pubkey); const events = await this.#getCacheEventsByPubkey(pubkey);
for (const event of events) { for (const event of events) {
let rawEvent: NostrEvent; let rawEvent: NostrEvent;
try { try {
@@ -189,7 +302,7 @@ export default class NDKCacheAdapterTauri implements NDKCacheAdapter {
if (hasAllKeys && filter.kinds) { if (hasAllKeys && filter.kinds) {
for (const kind of filter.kinds) { for (const kind of filter.kinds) {
const events = await this.db.getCacheEventsByKind(kind); const events = await this.#getCacheEventsByKind(kind);
for (const event of events) { for (const event of events) {
let rawEvent: NostrEvent; let rawEvent: NostrEvent;
try { try {
@@ -223,7 +336,7 @@ export default class NDKCacheAdapterTauri implements NDKCacheAdapter {
if (hasAllKeys && filter.ids) { if (hasAllKeys && filter.ids) {
for (const id of filter.ids) { for (const id of filter.ids) {
const event = await this.db.getCacheEvent(id); const event = await this.#getCacheEvent(id);
if (!event) continue; if (!event) continue;
let rawEvent: NostrEvent; let rawEvent: NostrEvent;
@@ -266,7 +379,7 @@ export default class NDKCacheAdapterTauri implements NDKCacheAdapter {
for (const author of filter.authors) { for (const author of filter.authors) {
for (const dTag of filter['#d']) { for (const dTag of filter['#d']) {
const replaceableId = `${kind}:${author}:${dTag}`; const replaceableId = `${kind}:${author}:${dTag}`;
const event = await this.db.getCacheEvent(replaceableId); const event = await this.#getCacheEvent(replaceableId);
if (!event) continue; if (!event) continue;
let rawEvent: NostrEvent; let rawEvent: NostrEvent;
@@ -306,7 +419,7 @@ export default class NDKCacheAdapterTauri implements NDKCacheAdapter {
if (filter.kinds && filter.authors) { if (filter.kinds && filter.authors) {
for (const kind of filter.kinds) { for (const kind of filter.kinds) {
for (const author of filter.authors) { for (const author of filter.authors) {
const events = await this.db.getCacheEventsByKindAndAuthor(kind, author); const events = await this.#getCacheEventsByKindAndAuthor(kind, author);
for (const event of events) { for (const event of events) {
let rawEvent: NostrEvent; let rawEvent: NostrEvent;
@@ -371,12 +484,12 @@ export default class NDKCacheAdapterTauri implements NDKCacheAdapter {
} }
for (const value of values) { for (const value of values) {
const eventTags = await this.db.getCacheEventTagsByTagValue(tag + value); const eventTags = await this.#getCacheEventTagsByTagValue(tag + value);
if (!eventTags.length) continue; if (!eventTags.length) continue;
const eventIds = eventTags.map((t) => t.eventId); const eventIds = eventTags.map((t) => t.eventId);
const events = await this.db.getCacheEvents(eventIds); const events = await this.#getCacheEvents(eventIds);
for (const event of events) { for (const event of events) {
let rawEvent; let rawEvent;
try { try {
@@ -418,7 +531,7 @@ export default class NDKCacheAdapterTauri implements NDKCacheAdapter {
} }
if (profiles.length) { if (profiles.length) {
await this.db.setCacheProfiles(profiles); await this.#setCacheProfiles(profiles);
} }
this.dirtyProfiles.clear(); this.dirtyProfiles.clear();

3
src/libs/ark/index.ts Normal file
View File

@@ -0,0 +1,3 @@
export * from './ark';
export * from './cache';
export * from './provider';

179
src/libs/ark/provider.tsx Normal file
View File

@@ -0,0 +1,179 @@
import { NDKKind } from '@nostr-dev-kit/ndk';
import { useQueryClient } from '@tanstack/react-query';
import { ask } from '@tauri-apps/plugin-dialog';
import { platform } from '@tauri-apps/plugin-os';
import { relaunch } from '@tauri-apps/plugin-process';
import Database from '@tauri-apps/plugin-sql';
import { check } from '@tauri-apps/plugin-updater';
import Markdown from 'markdown-to-jsx';
import { PropsWithChildren, createContext, useContext, useEffect, useState } from 'react';
import { Ark } from '@libs/ark';
import { LoaderIcon } from '@shared/icons';
import { FETCH_LIMIT, QUOTES } from '@utils/constants';
interface ArkContext {
ark: Ark;
}
const ArkContext = createContext<ArkContext>({
ark: undefined,
});
const ArkProvider = ({ children }: PropsWithChildren<object>) => {
const [ark, setArk] = useState<Ark>(undefined);
const [isNewVersion, setIsNewVersion] = useState(false);
const queryClient = useQueryClient();
async function initArk() {
try {
const sqlite = await Database.load('sqlite:lume_v2.db');
const platformName = await platform();
const _ark = new Ark({ storage: sqlite, platform: platformName });
await _ark.init();
const settings = await _ark.getAllSettings();
let autoUpdater = false;
if (settings) {
settings.forEach((item) => {
if (item.key === 'outbox') _ark.settings.outbox = !!parseInt(item.value);
if (item.key === 'media') _ark.settings.media = !!parseInt(item.value);
if (item.key === 'hashtag') _ark.settings.hashtag = !!parseInt(item.value);
if (item.key === 'autoupdate') {
if (parseInt(item.value)) autoUpdater = true;
}
});
}
if (autoUpdater) {
// check update
const update = await check();
// install new version
if (update) {
setIsNewVersion(true);
await update.downloadAndInstall();
await relaunch();
}
}
if (_ark.account) {
// prefetch newsfeed
await queryClient.prefetchInfiniteQuery({
queryKey: ['newsfeed'],
initialPageParam: 0,
queryFn: async ({
signal,
pageParam,
}: {
signal: AbortSignal;
pageParam: number;
}) => {
return await ark.getInfiniteEvents({
filter: {
kinds: [NDKKind.Text, NDKKind.Repost],
authors: !ark.account.contacts.length
? [ark.account.pubkey]
: ark.account.contacts,
},
limit: FETCH_LIMIT,
pageParam,
signal,
});
},
});
// prefetch notification
await queryClient.prefetchInfiniteQuery({
queryKey: ['notification'],
initialPageParam: 0,
queryFn: async ({
signal,
pageParam,
}: {
signal: AbortSignal;
pageParam: number;
}) => {
return await ark.getInfiniteEvents({
filter: {
kinds: [NDKKind.Text, NDKKind.Repost, NDKKind.Reaction, NDKKind.Zap],
'#p': [ark.account.pubkey],
},
limit: FETCH_LIMIT,
pageParam,
signal,
});
},
});
}
setArk(_ark);
} catch (e) {
console.error(e);
const yes = await ask(`${e}. Click "Yes" to relaunch app`, {
title: 'Lume',
type: 'error',
okLabel: 'Yes',
});
if (yes) relaunch();
}
}
useEffect(() => {
if (!ark && !isNewVersion) initArk();
}, []);
if (!ark) {
return (
<div
data-tauri-drag-region
className="relative flex h-screen w-screen items-center justify-center bg-neutral-50 dark:bg-neutral-950"
>
<div className="flex max-w-2xl flex-col items-start gap-1">
<h5 className="font-semibold uppercase">TIP:</h5>
<Markdown
options={{
overrides: {
a: {
props: {
className: 'text-blue-500 hover:text-blue-600',
target: '_blank',
},
},
},
}}
className="text-4xl font-semibold leading-snug text-neutral-300 dark:text-neutral-700"
>
{QUOTES[Math.floor(Math.random() * QUOTES.length)]}
</Markdown>
</div>
<div className="absolute bottom-5 right-5 inline-flex items-center gap-2.5">
<LoaderIcon className="h-6 w-6 animate-spin text-blue-500" />
<p className="font-semibold">
{isNewVersion ? 'Found a new version, updating...' : 'Starting...'}
</p>
</div>
</div>
);
}
return <ArkContext.Provider value={{ ark }}>{children}</ArkContext.Provider>;
};
const useArk = () => {
const context = useContext(ArkContext);
if (context === undefined) {
throw new Error('Please import Ark Provider to use useArk() hook');
}
return context;
};
export { ArkProvider, useArk };

View File

@@ -1,214 +0,0 @@
import NDK, {
NDKEvent,
NDKKind,
NDKNip46Signer,
NDKPrivateKeySigner,
} from '@nostr-dev-kit/ndk';
import { ndkAdapter } from '@nostr-fetch/adapter-ndk';
import { useQueryClient } from '@tanstack/react-query';
import { ask } from '@tauri-apps/plugin-dialog';
import { relaunch } from '@tauri-apps/plugin-process';
import { NostrFetcher, normalizeRelayUrlSet } from 'nostr-fetch';
import { useEffect, useState } from 'react';
import { toast } from 'sonner';
import NDKCacheAdapterTauri from '@libs/ndk/cache';
import { useStorage } from '@libs/storage/provider';
import { FETCH_LIMIT } from '@utils/constants';
export const NDKInstance = () => {
const { db } = useStorage();
const queryClient = useQueryClient();
const [ndk, setNDK] = useState<NDK | undefined>(undefined);
const [fetcher, setFetcher] = useState<NostrFetcher | undefined>(undefined);
const [relayUrls, setRelayUrls] = useState<string[]>([]);
async function getSigner(nsecbunker?: boolean) {
if (!db.account) return;
try {
// NIP-46 Signer
if (nsecbunker) {
const localSignerPrivkey = await db.secureLoad(`${db.account.id}-nsecbunker`);
if (!localSignerPrivkey) return null;
const localSigner = new NDKPrivateKeySigner(localSignerPrivkey);
const bunker = new NDK({
explicitRelayUrls: ['wss://relay.nsecbunker.com', 'wss://nostr.vulpem.com'],
});
await bunker.connect();
const remoteSigner = new NDKNip46Signer(bunker, db.account.pubkey, localSigner);
await remoteSigner.blockUntilReady();
return remoteSigner;
}
// Privkey Signer
const userPrivkey = await db.secureLoad(db.account.pubkey);
if (!userPrivkey) return null;
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() {
const outboxSetting = await db.getSettingValue('outbox');
const bunkerSetting = await db.getSettingValue('nsecbunker');
const bunker = !!parseInt(bunkerSetting);
const outbox = !!parseInt(outboxSetting);
const explicitRelayUrls = normalizeRelayUrlSet([
'wss://relay.damus.io',
'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 {
const tauriAdapter = new NDKCacheAdapterTauri(db);
const instance = new NDK({
explicitRelayUrls,
outboxRelayUrls,
blacklistRelayUrls,
enableOutboxModel: outbox,
autoConnectUserRelays: true,
autoFetchUserMutelist: true,
cacheAdapter: tauriAdapter,
// clientName: 'Lume',
// clientNip89: '',
});
// add signer if exist
const signer = await getSigner(bunker);
if (signer) instance.signer = signer;
// connect
await instance.connect();
const _fetcher = NostrFetcher.withCustomPool(ndkAdapter(instance));
// update account's metadata
if (db.account) {
const user = instance.getUser({ pubkey: db.account.pubkey });
instance.activeUser = user;
const contacts = await user.follows(undefined /* outbox */);
db.account.contacts = [...contacts].map((user) => 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);
},
});
// prefetch notification
await queryClient.prefetchInfiniteQuery({
queryKey: ['notification'],
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 }
);
const ndkEvents = events.map((event) => {
return new NDKEvent(ndk, event);
});
return ndkEvents.sort((a, b) => b.created_at - a.created_at);
},
});
}
setNDK(instance);
setFetcher(_fetcher);
setRelayUrls(explicitRelayUrls);
} catch (e) {
console.error(e);
const yes = await ask(e, {
title: 'Lume',
type: 'error',
okLabel: 'Yes',
});
if (yes) relaunch();
}
}
useEffect(() => {
if (!ndk) initNDK();
}, []);
return {
ndk,
fetcher,
relayUrls,
};
};

View File

@@ -1,80 +0,0 @@
// source: https://github.com/nostr-dev-kit/ndk-react/
import NDK from '@nostr-dev-kit/ndk';
import Markdown from 'markdown-to-jsx';
import { NostrFetcher } from 'nostr-fetch';
import { PropsWithChildren, createContext, useContext } from 'react';
import { NDKInstance } from '@libs/ndk/instance';
import { LoaderIcon } from '@shared/icons';
import { QUOTES } from '@utils/constants';
interface NDKContext {
ndk: undefined | NDK;
fetcher: undefined | NostrFetcher;
relayUrls: string[];
}
const NDKContext = createContext<NDKContext>({
ndk: undefined,
fetcher: undefined,
relayUrls: [],
});
const NDKProvider = ({ children }: PropsWithChildren<object>) => {
const { ndk, relayUrls, fetcher } = NDKInstance();
if (!ndk)
return (
<div
data-tauri-drag-region
className="relative flex h-screen w-screen items-center justify-center bg-neutral-50 dark:bg-neutral-950"
>
<div className="flex max-w-2xl flex-col items-start gap-1">
<h5 className="font-semibold uppercase">TIP:</h5>
<Markdown
options={{
overrides: {
a: {
props: {
className: 'text-blue-500 hover:text-blue-600',
target: '_blank',
},
},
},
}}
className="text-4xl font-semibold leading-snug text-neutral-300 dark:text-neutral-700"
>
{QUOTES[Math.floor(Math.random() * QUOTES.length)]}
</Markdown>
</div>
<div className="absolute bottom-5 right-5 inline-flex items-center gap-2.5">
<LoaderIcon className="h-6 w-6 animate-spin text-blue-500" />
<p className="font-semibold">Connecting to relays...</p>
</div>
</div>
);
return (
<NDKContext.Provider
value={{
ndk,
relayUrls,
fetcher,
}}
>
{children}
</NDKContext.Provider>
);
};
const useNDK = () => {
const context = useContext(NDKContext);
if (context === undefined) {
throw new Error('import NDKProvider to use useNDK');
}
return context;
};
export { NDKProvider, useNDK };

View File

@@ -1,486 +0,0 @@
import { NDKEvent } from '@nostr-dev-kit/ndk';
import { invoke } from '@tauri-apps/api/primitives';
import { Platform } from '@tauri-apps/plugin-os';
import Database from '@tauri-apps/plugin-sql';
import { rawEvent } from '@utils/transform';
import type {
Account,
DBEvent,
NDKCacheEvent,
NDKCacheEventTag,
NDKCacheUser,
NDKCacheUserProfile,
Relays,
Widget,
} from '@utils/types';
export class LumeStorage {
public db: Database;
public account: Account | null;
public platform: Platform | null;
public settings: {
autoupdate: boolean;
outbox: boolean;
media: boolean;
hashtag: boolean;
};
constructor(sqlite: Database, platform: Platform) {
this.db = sqlite;
this.account = null;
this.platform = platform;
this.settings = { autoupdate: false, outbox: false, media: true, hashtag: true };
}
public async secureSave(key: string, value: string) {
return await invoke('secure_save', { key, value });
}
public async secureLoad(key: string) {
try {
const value: string = await invoke('secure_load', { key });
if (!value) return null;
return value;
} catch {
return null;
}
}
public async secureRemove(key: string) {
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) {
const results: Array<NDKCacheUser> = await this.db.select(
'SELECT * FROM ndk_users WHERE pubkey = $1 ORDER BY pubkey DESC LIMIT 1;',
[pubkey]
);
if (!results.length) return null;
if (typeof results[0].profile === 'string')
results[0].profile = JSON.parse(results[0].profile);
return results[0];
}
public async getCacheEvent(id: string) {
const results: Array<NDKCacheEvent> = await this.db.select(
'SELECT * FROM ndk_events WHERE id = $1 ORDER BY id DESC LIMIT 1;',
[id]
);
if (!results.length) return null;
return results[0];
}
public async getCacheEvents(ids: string[]) {
const idsArr = `'${ids.join("','")}'`;
const results: Array<NDKCacheEvent> = await this.db.select(
`SELECT * FROM ndk_events WHERE id IN (${idsArr}) ORDER BY id;`
);
if (!results.length) return [];
return results;
}
public async getCacheEventsByPubkey(pubkey: string) {
const results: Array<NDKCacheEvent> = await this.db.select(
'SELECT * FROM ndk_events WHERE pubkey = $1 ORDER BY id;',
[pubkey]
);
if (!results.length) return [];
return results;
}
public async getCacheEventsByKind(kind: number) {
const results: Array<NDKCacheEvent> = await this.db.select(
'SELECT * FROM ndk_events WHERE kind = $1 ORDER BY id;',
[kind]
);
if (!results.length) return [];
return results;
}
public async getCacheEventsByKindAndAuthor(kind: number, pubkey: string) {
const results: Array<NDKCacheEvent> = await this.db.select(
'SELECT * FROM ndk_events WHERE kind = $1 AND pubkey = $2 ORDER BY id;',
[kind, pubkey]
);
if (!results.length) return [];
return results;
}
public async getCacheEventTagsByTagValue(tagValue: string) {
const results: Array<NDKCacheEventTag> = await this.db.select(
'SELECT * FROM ndk_eventtags WHERE tagValue = $1 ORDER BY id;',
[tagValue]
);
if (!results.length) return [];
return results;
}
public async setCacheEvent({
id,
pubkey,
content,
kind,
createdAt,
relay,
event,
}: NDKCacheEvent) {
return await this.db.execute(
'INSERT OR IGNORE INTO ndk_events (id, pubkey, content, kind, createdAt, relay, event) VALUES ($1, $2, $3, $4, $5, $6, $7);',
[id, pubkey, content, kind, createdAt, relay, event]
);
}
public async setCacheEventTag({ id, eventId, tag, value, tagValue }: NDKCacheEventTag) {
return await this.db.execute(
'INSERT OR IGNORE INTO ndk_eventtags (id, eventId, tag, value, tagValue) VALUES ($1, $2, $3, $4, $5);',
[id, eventId, tag, value, tagValue]
);
}
public async setCacheProfiles(profiles: Array<NDKCacheUser>) {
return await Promise.all(
profiles.map(
async (profile) =>
await this.db.execute(
'INSERT OR IGNORE INTO ndk_users (pubkey, profile, createdAt) VALUES ($1, $2, $3);',
[profile.pubkey, profile.profile, profile.createdAt]
)
)
);
}
public async checkAccount() {
const result: Array<{ total: string }> = await this.db.select(
'SELECT COUNT(*) AS "total" FROM accounts WHERE is_active = "1" ORDER BY id DESC LIMIT 1;'
);
return parseInt(result[0].total);
}
public async getActiveAccount() {
const results: Array<Account> = await this.db.select(
'SELECT * FROM accounts WHERE is_active = "1" ORDER BY id DESC LIMIT 1;'
);
if (results.length) {
this.account = results[0];
this.account.contacts = [];
} else {
console.log('no active account, please create new account');
return null;
}
}
public async createAccount(npub: string, pubkey: string) {
const existAccounts: Array<Account> = await this.db.select(
'SELECT * FROM accounts WHERE pubkey = $1 ORDER BY id DESC LIMIT 1;',
[pubkey]
);
if (existAccounts.length) {
await this.db.execute("UPDATE accounts SET is_active = '1' WHERE pubkey = $1;", [
pubkey,
]);
} else {
await this.db.execute(
'INSERT OR IGNORE INTO accounts (id, pubkey, is_active) VALUES ($1, $2, $3);',
[npub, pubkey, 1]
);
}
return await this.getActiveAccount();
}
public async updateAccount(column: string, value: string) {
const insert = await this.db.execute(
`UPDATE accounts SET ${column} = $1 WHERE id = $2;`,
[value, this.account.id]
);
if (insert) {
const account = await this.getActiveAccount();
return account;
}
}
public async getWidgets() {
const widgets: Array<Widget> = await this.db.select(
'SELECT * FROM widgets WHERE account_id = $1 ORDER BY created_at DESC;',
[this.account.id]
);
return widgets;
}
public async createWidget(kind: number, title: string, content: string | string[]) {
const insert = await this.db.execute(
'INSERT INTO widgets (account_id, kind, title, content) VALUES ($1, $2, $3, $4);',
[this.account.id, kind, title, content]
);
if (insert) {
const widgets: Array<Widget> = await this.db.select(
'SELECT * FROM widgets ORDER BY id DESC LIMIT 1;'
);
if (widgets.length < 1) console.error('get created widget failed');
return widgets[0];
} else {
console.error('create widget failed');
}
}
public async removeWidget(id: string) {
const res = await this.db.execute('DELETE FROM widgets WHERE id = $1;', [id]);
if (res) return id;
}
public async createEvent(event: NDKEvent) {
const rawNostrEvent = rawEvent(event);
let root: string;
let reply: string;
if (event.tags?.[0]?.[0] === 'e' && !event.tags?.[0]?.[3]) {
root = event.tags[0][1];
} else {
root = event.tags.find((el) => el[3] === 'root')?.[1];
reply = event.tags.find((el) => el[3] === 'reply')?.[1];
}
return await this.db.execute(
'INSERT OR IGNORE INTO events (id, account_id, event, author, kind, root_id, reply_id, created_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8);',
[
event.id,
this.account.id,
JSON.stringify(rawNostrEvent),
event.pubkey,
event.kind,
root,
reply,
event.created_at,
]
);
}
public async getEventByID(id: string) {
const results: DBEvent[] = await this.db.select(
'SELECT * FROM events WHERE id = $1 LIMIT 1;',
[id]
);
if (results.length < 1) return null;
return JSON.parse(results[0].event as string) as NDKEvent;
}
public async countTotalEvents() {
const result: Array<{ total: string }> = await this.db.select(
'SELECT COUNT(*) AS "total" FROM events WHERE account_id = $1;',
[this.account.id]
);
return parseInt(result[0].total);
}
public async getAllEvents(limit: number, offset: number) {
const totalEvents = await this.countTotalEvents();
const nextCursor = offset + limit;
const events: { data: DBEvent[] | null; nextCursor: number } = {
data: null,
nextCursor: 0,
};
const query: DBEvent[] = await this.db.select(
'SELECT * FROM events WHERE account_id = $1 GROUP BY root_id ORDER BY created_at DESC LIMIT $2 OFFSET $3;',
[this.account.id, limit, offset]
);
if (query && query.length > 0) {
events['data'] = query;
events['nextCursor'] =
Math.round(totalEvents / nextCursor) > 1 ? nextCursor : undefined;
return events;
}
return {
data: [],
nextCursor: 0,
};
}
public async getAllEventsByAuthors(authors: string[], limit: number, offset: number) {
const totalEvents = await this.countTotalEvents();
const nextCursor = offset + limit;
const authorsArr = `'${authors.join("','")}'`;
const events: { data: DBEvent[] | null; nextCursor: number } = {
data: null,
nextCursor: 0,
};
const query: DBEvent[] = await this.db.select(
`SELECT * FROM events WHERE author IN (${authorsArr}) ORDER BY created_at DESC LIMIT $1 OFFSET $2;`,
[limit, offset]
);
if (query && query.length > 0) {
events['data'] = query;
events['nextCursor'] =
Math.round(totalEvents / nextCursor) > 1 ? nextCursor : undefined;
return events;
}
return {
data: [],
nextCursor: 0,
};
}
public async getAllEventsByKinds(kinds: number[], limit: number, offset: number) {
const totalEvents = await this.countTotalEvents();
const nextCursor = offset + limit;
const authorsArr = `'${kinds.join("','")}'`;
const events: { data: DBEvent[] | null; nextCursor: number } = {
data: null,
nextCursor: 0,
};
const query: DBEvent[] = await this.db.select(
`SELECT * FROM events WHERE kinds IN (${authorsArr}) AND account_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3;`,
[this.account.id, limit, offset]
);
if (query && query.length > 0) {
events['data'] = query;
events['nextCursor'] =
Math.round(totalEvents / nextCursor) > 1 ? nextCursor : undefined;
return events;
}
return {
data: [],
nextCursor: 0,
};
}
public async isEventsEmpty() {
const results: DBEvent[] = await this.db.select(
'SELECT * FROM events WHERE account_id = $1 ORDER BY id DESC LIMIT 1;',
[this.account.id]
);
return results.length < 1;
}
public async createRelay(relay: string, purpose?: string) {
const existRelays: Relays[] = await this.db.select(
'SELECT * FROM relays WHERE relay = $1 AND account_id = $2 ORDER BY id DESC LIMIT 1;',
[relay, this.account.id]
);
if (existRelays.length) return;
return await this.db.execute(
'INSERT OR IGNORE INTO relays (account_id, relay, purpose) VALUES ($1, $2, $3);',
[this.account.id, relay, purpose || '']
);
}
public async removeRelay(relay: string) {
return await this.db.execute(`DELETE FROM relays WHERE relay = "${relay}";`);
}
public async createSetting(key: string, value: string | undefined) {
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)
return await this.db.execute(
'INSERT OR IGNORE INTO settings (key, value) VALUES ($1, $2);',
[key, value]
);
const currentValue = !!parseInt(currentSetting);
return await this.db.execute('UPDATE settings SET value = $1 WHERE key = $2;', [
+!currentValue,
key,
]);
}
public async getAllSettings() {
const results: { key: string; value: string }[] = await this.db.select(
'SELECT * FROM settings ORDER BY id DESC;'
);
if (results.length < 1) return null;
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) {
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 '0';
return results[0].value;
}
public async clearCache() {
await this.db.execute('DELETE FROM ndk_events;');
await this.db.execute('DELETE FROM ndk_eventtags;');
await this.db.execute('DELETE FROM ndk_users;');
}
public async accountLogout() {
// update current account status
await this.db.execute("UPDATE accounts SET is_active = '0' WHERE id = $1;", [
this.account.id,
]);
this.account = null;
}
public async close() {
return this.db.close();
}
}

View File

@@ -1,128 +0,0 @@
import { message } from '@tauri-apps/plugin-dialog';
import { platform } from '@tauri-apps/plugin-os';
import { relaunch } from '@tauri-apps/plugin-process';
import Database from '@tauri-apps/plugin-sql';
import { check } from '@tauri-apps/plugin-updater';
import Markdown from 'markdown-to-jsx';
import { PropsWithChildren, createContext, useContext, useEffect, useState } from 'react';
import { LumeStorage } from '@libs/storage/instance';
import { LoaderIcon } from '@shared/icons';
import { QUOTES } from '@utils/constants';
interface StorageContext {
db: LumeStorage;
}
const StorageContext = createContext<StorageContext>({
db: undefined,
});
const StorageInstance = () => {
const [db, setDB] = useState<LumeStorage>(undefined);
const [isNewVersion, setIsNewVersion] = useState(false);
const initLumeStorage = async () => {
try {
const sqlite = await Database.load('sqlite:lume_v2.db');
const platformName = await platform();
const lumeStorage = new LumeStorage(sqlite, platformName);
if (!lumeStorage.account) await lumeStorage.getActiveAccount();
const settings = await lumeStorage.getAllSettings();
let autoUpdater = false;
if (settings) {
settings.forEach((item) => {
if (item.key === 'outbox') lumeStorage.settings.outbox = !!parseInt(item.value);
if (item.key === 'media') lumeStorage.settings.media = !!parseInt(item.value);
if (item.key === 'hashtag')
lumeStorage.settings.hashtag = !!parseInt(item.value);
if (item.key === 'autoupdate') {
if (parseInt(item.value)) autoUpdater = true;
}
});
}
if (autoUpdater) {
// check update
const update = await check();
// install new version
if (update) {
setIsNewVersion(true);
await update.downloadAndInstall();
await relaunch();
}
}
setDB(lumeStorage);
} catch (e) {
await message(`Cannot initialize database: ${e}`, {
title: 'Lume',
type: 'error',
});
}
};
useEffect(() => {
if (!db) initLumeStorage();
}, []);
return { db, isNewVersion };
};
const StorageProvider = ({ children }: PropsWithChildren<object>) => {
const { db, isNewVersion } = StorageInstance();
if (!db)
return (
<div
data-tauri-drag-region
className="relative flex h-screen w-screen items-center justify-center bg-neutral-50 dark:bg-neutral-950"
>
<div className="flex max-w-2xl flex-col items-start gap-1">
<h5 className="font-semibold uppercase">TIP:</h5>
<Markdown
options={{
overrides: {
a: {
props: {
className: 'text-blue-500 hover:text-blue-600',
target: '_blank',
},
},
},
}}
className="text-4xl font-semibold leading-snug text-neutral-300 dark:text-neutral-700"
>
{QUOTES[Math.floor(Math.random() * QUOTES.length)]}
</Markdown>
</div>
<div className="absolute bottom-5 right-5 inline-flex items-center gap-2.5">
<LoaderIcon className="h-6 w-6 animate-spin text-blue-500" />
<p className="font-semibold">
{isNewVersion ? 'Found a new version, updating...' : 'Starting...'}
</p>
</div>
</div>
);
return <StorageContext.Provider value={{ db }}>{children}</StorageContext.Provider>;
};
const useStorage = () => {
const context = useContext(StorageContext);
if (context === undefined) {
throw new Error('Storage not found');
}
return context;
};
export { StorageProvider, useStorage };

View File

@@ -3,8 +3,7 @@ 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';
import { NDKProvider } from '@libs/ndk/provider'; import { ArkProvider } from '@libs/ark/provider';
import { StorageProvider } from '@libs/storage/provider';
import App from './app'; import App from './app';
@@ -23,10 +22,8 @@ root.render(
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<ReactQueryDevtools initialIsOpen={false} /> <ReactQueryDevtools initialIsOpen={false} />
<Toaster position="top-center" theme="system" closeButton /> <Toaster position="top-center" theme="system" closeButton />
<StorageProvider> <ArkProvider>
<NDKProvider> <App />
<App /> </ArkProvider>
</NDKProvider>
</StorageProvider>
</QueryClientProvider> </QueryClientProvider>
); );

View File

@@ -2,7 +2,7 @@ import * as Avatar from '@radix-ui/react-avatar';
import { minidenticon } from 'minidenticons'; import { minidenticon } from 'minidenticons';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useStorage } from '@libs/storage/provider'; import { useArk } from '@libs/ark';
import { AccountMoreActions } from '@shared/accounts/more'; import { AccountMoreActions } from '@shared/accounts/more';
import { NetworkStatusIndicator } from '@shared/networkStatusIndicator'; import { NetworkStatusIndicator } from '@shared/networkStatusIndicator';
@@ -10,12 +10,12 @@ import { NetworkStatusIndicator } from '@shared/networkStatusIndicator';
import { useProfile } from '@utils/hooks/useProfile'; import { useProfile } from '@utils/hooks/useProfile';
export function ActiveAccount() { export function ActiveAccount() {
const { db } = useStorage(); const { ark } = useArk();
const { user } = useProfile(db.account.pubkey); const { user } = useProfile(ark.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(ark.account.pubkey, 90, 50));
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">
@@ -23,7 +23,7 @@ export function ActiveAccount() {
<Avatar.Root> <Avatar.Root>
<Avatar.Image <Avatar.Image
src={user?.picture || user?.image} src={user?.picture || user?.image}
alt={db.account.pubkey} alt={ark.account.pubkey}
loading="lazy" loading="lazy"
decoding="async" decoding="async"
style={{ contentVisibility: 'auto' }} style={{ contentVisibility: 'auto' }}
@@ -32,7 +32,7 @@ export function ActiveAccount() {
<Avatar.Fallback delayMs={150}> <Avatar.Fallback delayMs={150}>
<img <img
src={svgURI} src={svgURI}
alt={db.account.pubkey} alt={ark.account.pubkey}
className="aspect-square h-auto w-full rounded-md bg-black dark:bg-white" className="aspect-square h-auto w-full rounded-md bg-black dark:bg-white"
/> />
</Avatar.Fallback> </Avatar.Fallback>

View File

@@ -3,26 +3,18 @@ import { useQueryClient } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useNDK } from '@libs/ndk/provider'; import { useArk } from '@libs/ark';
import { useStorage } from '@libs/storage/provider';
export function Logout() { export function Logout() {
const { db } = useStorage(); const { ark } = useArk();
const { ndk } = useNDK();
const navigate = useNavigate(); const navigate = useNavigate();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const logout = async () => { const logout = async () => {
try { try {
ndk.signer = null;
// remove private key
await db.secureRemove(db.account.pubkey);
await db.secureRemove(`${db.account.id}-nsecbunker`);
// logout // logout
await db.accountLogout(); await ark.logout();
// clear cache // clear cache
queryClient.clear(); queryClient.clear();

View File

@@ -1,16 +1,16 @@
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 } from '@shared/icons'; import { useArk } from '@libs/ark';
import { useNostr } from '@utils/hooks/useNostr'; import { LoaderIcon } from '@shared/icons';
export function AvatarUploader({ export function AvatarUploader({
setPicture, setPicture,
}: { }: {
setPicture: Dispatch<SetStateAction<string>>; setPicture: Dispatch<SetStateAction<string>>;
}) { }) {
const { upload } = useNostr(); const { ark } = useArk();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const uploadAvatar = async () => { const uploadAvatar = async () => {
@@ -18,7 +18,7 @@ export function AvatarUploader({
// start loading // start loading
setLoading(true); setLoading(true);
const image = await upload(); const image = await ark.upload({});
if (image) { if (image) {
setPicture(image); setPicture(image);

View File

@@ -1,16 +1,16 @@
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 { useArk } from '@libs/ark';
import { useNostr } from '@utils/hooks/useNostr'; import { LoaderIcon, PlusIcon } from '@shared/icons';
export function BannerUploader({ export function BannerUploader({
setBanner, setBanner,
}: { }: {
setBanner: Dispatch<SetStateAction<string>>; setBanner: Dispatch<SetStateAction<string>>;
}) { }) {
const { upload } = useNostr(); const { ark } = useArk();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const uploadBanner = async () => { const uploadBanner = async () => {
@@ -18,7 +18,7 @@ export function BannerUploader({
// start loading // start loading
setLoading(true); setLoading(true);
const image = await upload(); const image = await ark.upload({});
if (image) { if (image) {
setBanner(image); setBanner(image);

View File

@@ -2,21 +2,21 @@ import { Outlet, ScrollRestoration } from 'react-router-dom';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
import { WindowTitlebar } from 'tauri-controls'; import { WindowTitlebar } from 'tauri-controls';
import { useStorage } from '@libs/storage/provider'; import { useArk } from '@libs/ark';
import { Navigation } from '@shared/navigation'; import { Navigation } from '@shared/navigation';
export function AppLayout() { export function AppLayout() {
const { db } = useStorage(); const { ark } = useArk();
return ( return (
<div <div
className={twMerge( className={twMerge(
'flex h-screen w-screen flex-col', 'flex h-screen w-screen flex-col',
db.platform !== 'macos' ? 'bg-neutral-50 dark:bg-neutral-950' : '' ark.platform !== 'macos' ? 'bg-neutral-50 dark:bg-neutral-950' : ''
)} )}
> >
{db.platform !== 'macos' ? ( {ark.platform !== 'macos' ? (
<WindowTitlebar /> <WindowTitlebar />
) : ( ) : (
<div data-tauri-drag-region className="h-9" /> <div data-tauri-drag-region className="h-9" />
@@ -26,7 +26,7 @@ export function AppLayout() {
data-tauri-drag-region data-tauri-drag-region
className={twMerge( className={twMerge(
'h-full w-[64px] shrink-0', 'h-full w-[64px] shrink-0',
db.platform !== 'macos' ? 'pt-2' : 'pt-0' ark.platform !== 'macos' ? 'pt-2' : 'pt-0'
)} )}
> >
<Navigation /> <Navigation />

View File

@@ -1,14 +1,14 @@
import { Outlet, ScrollRestoration } from 'react-router-dom'; import { Outlet, ScrollRestoration } from 'react-router-dom';
import { WindowTitlebar } from 'tauri-controls'; import { WindowTitlebar } from 'tauri-controls';
import { useStorage } from '@libs/storage/provider'; import { useArk } from '@libs/ark';
export function AuthLayout() { export function AuthLayout() {
const { db } = useStorage(); const { ark } = useArk();
return ( return (
<div className="flex h-screen w-screen flex-col"> <div className="flex h-screen w-screen flex-col">
{db.platform !== 'macos' ? ( {ark.platform !== 'macos' ? (
<WindowTitlebar /> <WindowTitlebar />
) : ( ) : (
<div data-tauri-drag-region className="h-9" /> <div data-tauri-drag-region className="h-9" />

View File

@@ -2,17 +2,17 @@ import { Link, NavLink, Outlet, useLocation } from 'react-router-dom';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
import { WindowTitlebar } from 'tauri-controls'; import { WindowTitlebar } from 'tauri-controls';
import { useStorage } from '@libs/storage/provider'; import { useArk } from '@libs/ark';
import { ArrowLeftIcon } from '@shared/icons'; import { ArrowLeftIcon } from '@shared/icons';
export function NewLayout() { export function NewLayout() {
const { db } = useStorage(); const { ark } = useArk();
const location = useLocation(); const location = useLocation();
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">
{db.platform !== 'macos' ? ( {ark.platform !== 'macos' ? (
<WindowTitlebar /> <WindowTitlebar />
) : ( ) : (
<div data-tauri-drag-region className="h-9 shrink-0" /> <div data-tauri-drag-region className="h-9 shrink-0" />

View File

@@ -1,14 +1,14 @@
import { Outlet, ScrollRestoration } from 'react-router-dom'; import { Outlet, ScrollRestoration } from 'react-router-dom';
import { WindowTitlebar } from 'tauri-controls'; import { WindowTitlebar } from 'tauri-controls';
import { useStorage } from '@libs/storage/provider'; import { useArk } from '@libs/ark';
export function NoteLayout() { export function NoteLayout() {
const { db } = useStorage(); const { ark } = useArk();
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">
{db.platform !== 'macos' ? ( {ark.platform !== 'macos' ? (
<WindowTitlebar /> <WindowTitlebar />
) : ( ) : (
<div data-tauri-drag-region className="h-9" /> <div data-tauri-drag-region className="h-9" />

View File

@@ -2,7 +2,7 @@ import { NavLink, Outlet, ScrollRestoration, useNavigate } from 'react-router-do
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
import { WindowTitlebar } from 'tauri-controls'; import { WindowTitlebar } from 'tauri-controls';
import { useStorage } from '@libs/storage/provider'; import { useArk } from '@libs/ark';
import { import {
AdvancedSettingsIcon, AdvancedSettingsIcon,
@@ -14,12 +14,12 @@ import {
} from '@shared/icons'; } from '@shared/icons';
export function SettingsLayout() { export function SettingsLayout() {
const { db } = useStorage(); const { ark } = useArk();
const navigate = useNavigate(); 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">
{db.platform !== 'macos' ? ( {ark.platform !== 'macos' ? (
<WindowTitlebar /> <WindowTitlebar />
) : ( ) : (
<div data-tauri-drag-region className="h-9" /> <div data-tauri-drag-region className="h-9" />

View File

@@ -4,7 +4,7 @@ import { useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useNDK } from '@libs/ndk/provider'; import { useArk } from '@libs/ark';
import { ReactionIcon } from '@shared/icons'; import { ReactionIcon } from '@shared/icons';
@@ -35,7 +35,7 @@ export function NoteReaction({ event }: { event: NDKEvent }) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [reaction, setReaction] = useState<string | null>(null); const [reaction, setReaction] = useState<string | null>(null);
const { ndk } = useNDK(); const { ark } = useArk();
const navigate = useNavigate(); const navigate = useNavigate();
const getReactionImage = (content: string) => { const getReactionImage = (content: string) => {
@@ -45,7 +45,7 @@ export function NoteReaction({ event }: { event: NDKEvent }) {
const react = async (content: string) => { const react = async (content: string) => {
try { try {
if (!ndk.signer) return navigate('/new/privkey'); if (!ark.readyToSign) return navigate('/new/privkey');
setReaction(content); setReaction(content);

View File

@@ -6,7 +6,7 @@ import { useNavigate } from 'react-router-dom';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
import { useNDK } from '@libs/ndk/provider'; import { useArk } from '@libs/ark';
import { LoaderIcon, RepostIcon } from '@shared/icons'; import { LoaderIcon, RepostIcon } from '@shared/icons';
@@ -15,12 +15,12 @@ export function NoteRepost({ event }: { event: NDKEvent }) {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isRepost, setIsRepost] = useState(false); const [isRepost, setIsRepost] = useState(false);
const { ndk } = useNDK(); const { ark } = useArk();
const navigate = useNavigate(); const navigate = useNavigate();
const submit = async () => { const submit = async () => {
try { try {
if (!ndk.signer) return navigate('/new/privkey'); if (!ark.readyToSign) return navigate('/new/privkey');
setIsLoading(true); setIsLoading(true);

View File

@@ -9,8 +9,7 @@ import { useEffect, useRef, useState } from 'react';
import CurrencyInput from 'react-currency-input-field'; 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 { useArk } from '@libs/ark';
import { useStorage } from '@libs/storage/provider';
import { CancelIcon, ZapIcon } from '@shared/icons'; import { CancelIcon, ZapIcon } from '@shared/icons';
@@ -20,11 +19,7 @@ import { compactNumber } from '@utils/number';
import { displayNpub } from '@utils/shortenKey'; import { displayNpub } from '@utils/shortenKey';
export function NoteZap({ event }: { event: NDKEvent }) { export function NoteZap({ event }: { event: NDKEvent }) {
const nwc = useRef(null); const { ark } = useArk();
const navigate = useNavigate();
const { db } = useStorage();
const { ndk } = useNDK();
const { user } = useProfile(event.pubkey); const { user } = useProfile(event.pubkey);
const [walletConnectURL, setWalletConnectURL] = useState<string>(null); const [walletConnectURL, setWalletConnectURL] = useState<string>(null);
@@ -35,9 +30,12 @@ export function NoteZap({ event }: { event: NDKEvent }) {
const [isCompleted, setIsCompleted] = useState(false); const [isCompleted, setIsCompleted] = useState(false);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const nwc = useRef(null);
const navigate = useNavigate();
const createZapRequest = async () => { const createZapRequest = async () => {
try { try {
if (!ndk.signer) return navigate('/new/privkey'); if (!ark.readyToSign) return navigate('/new/privkey');
const zapAmount = parseInt(amount) * 1000; const zapAmount = parseInt(amount) * 1000;
const res = await event.zap(zapAmount, zapMessage); const res = await event.zap(zapAmount, zapMessage);
@@ -88,7 +86,7 @@ export function NoteZap({ event }: { event: NDKEvent }) {
useEffect(() => { useEffect(() => {
async function getWalletConnectURL() { async function getWalletConnectURL() {
const uri: string = await invoke('secure_load', { const uri: string = await invoke('secure_load', {
key: `${db.account.pubkey}-nwc`, key: `${ark.account.pubkey}-nwc`,
}); });
if (uri) setWalletConnectURL(uri); if (uri) setWalletConnectURL(uri);
} }

View File

@@ -1,20 +1,21 @@
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 { useArk } from '@libs/ark';
import { ReplyIcon, RepostIcon } from '@shared/icons'; import { ReplyIcon, RepostIcon } from '@shared/icons';
import { ChildNote, TextKind } from '@shared/notes'; import { ChildNote, TextKind } from '@shared/notes';
import { User } from '@shared/user'; import { User } from '@shared/user';
import { WIDGET_KIND } from '@utils/constants'; import { WIDGET_KIND } from '@utils/constants';
import { formatCreatedAt } from '@utils/createdAt'; import { formatCreatedAt } from '@utils/createdAt';
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 { getEventThread } = useNostr(); const { ark } = useArk();
const { addWidget } = useWidget(); const { addWidget } = useWidget();
const thread = getEventThread(event.tags); const thread = ark.getEventThread({ tags: event.tags });
const createdAt = formatCreatedAt(event.created_at, false); const createdAt = formatCreatedAt(event.created_at, false);
if (event.kind === NDKKind.Reaction) { if (event.kind === NDKKind.Reaction) {

View File

@@ -1,15 +1,15 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk'; import { NDKEvent } from '@nostr-dev-kit/ndk';
import { useState } from 'react'; import { useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useNDK } from '@libs/ndk/provider'; import { useArk } from '@libs/ark';
import { LoaderIcon } from '@shared/icons'; import { LoaderIcon } from '@shared/icons';
import { ReplyMediaUploader } from '@shared/notes'; import { ReplyMediaUploader } from '@shared/notes';
export function NoteReplyForm({ rootEvent }: { rootEvent: NDKEvent }) { export function NoteReplyForm({ rootEvent }: { rootEvent: NDKEvent }) {
const { ndk } = useNDK(); const { ark } = useArk();
const navigate = useNavigate(); const navigate = useNavigate();
const [value, setValue] = useState(''); const [value, setValue] = useState('');
@@ -17,22 +17,14 @@ export function NoteReplyForm({ rootEvent }: { rootEvent: NDKEvent }) {
const submit = async () => { const submit = async () => {
try { try {
if (!ndk.signer) return navigate('/new/privkey'); if (!ark.readyToSign) return navigate('/new/privkey');
setLoading(true); setLoading(true);
const event = new NDKEvent(ndk);
event.content = value;
event.kind = NDKKind.Text;
// tag root event
event.tag(rootEvent, 'reply');
// publish event // publish event
const publishedRelays = await event.publish(); const publish = await ark.replyTo({ content: value, event: rootEvent });
if (publishedRelays) { if (publish) {
toast.success(`Broadcasted to ${publishedRelays.size} relays successfully.`); toast.success(`Broadcasted to ${publish.size} relays successfully.`);
// reset state // reset state
setValue(''); setValue('');

View File

@@ -8,7 +8,13 @@ import { User } from '@shared/user';
import { NDKEventWithReplies } from '@utils/types'; import { NDKEventWithReplies } from '@utils/types';
export function Reply({ event }: { event: NDKEventWithReplies }) { export function Reply({
event,
rootEvent,
}: {
event: NDKEventWithReplies;
rootEvent: string;
}) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
return ( return (
@@ -30,7 +36,7 @@ export function Reply({ event }: { event: NDKEventWithReplies }) {
</div> </div>
</Collapsible.Trigger> </Collapsible.Trigger>
) : null} ) : null}
<NoteActions event={event} canOpenEvent={false} /> <NoteActions event={event} rootEventId={rootEvent} canOpenEvent={false} />
</div> </div>
</div> </div>
<div className={twMerge('px-3', open ? 'pb-3' : '')}> <div className={twMerge('px-3', open ? 'pb-3' : '')}>

View File

@@ -1,38 +1,42 @@
import { NDKSubscription } from '@nostr-dev-kit/ndk';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useArk } from '@libs/ark';
import { LoaderIcon } from '@shared/icons'; import { LoaderIcon } from '@shared/icons';
import { Reply } from '@shared/notes'; import { Reply } from '@shared/notes';
import { useNostr } from '@utils/hooks/useNostr';
import { NDKEventWithReplies } from '@utils/types'; import { NDKEventWithReplies } from '@utils/types';
export function ReplyList({ eventId }: { eventId: string }) { export function ReplyList({ eventId }: { eventId: string }) {
const { fetchAllReplies, sub } = useNostr(); const { ark } = useArk();
const [data, setData] = useState<null | NDKEventWithReplies[]>(null); const [data, setData] = useState<null | NDKEventWithReplies[]>(null);
useEffect(() => { useEffect(() => {
let sub: NDKSubscription;
let isCancelled = false; let isCancelled = false;
async function fetchRepliesAndSub() { async function fetchRepliesAndSub() {
const events = await fetchAllReplies(eventId); const events = await ark.getThreads({ id: eventId });
if (!isCancelled) { if (!isCancelled) {
setData(events); setData(events);
} }
// subscribe for new replies // subscribe for new replies
sub( sub = ark.subscribe({
{ filter: {
'#e': [eventId], '#e': [eventId],
since: Math.floor(Date.now() / 1000), since: Math.floor(Date.now() / 1000),
}, },
(event: NDKEventWithReplies) => setData((prev) => [event, ...prev]), closeOnEose: false,
false cb: (event: NDKEventWithReplies) => setData((prev) => [event, ...prev]),
); });
} }
fetchRepliesAndSub(); fetchRepliesAndSub();
return () => { return () => {
isCancelled = true; isCancelled = true;
if (sub) sub.stop();
}; };
}, [eventId]); }, [eventId]);
@@ -59,7 +63,7 @@ export function ReplyList({ eventId }: { eventId: string }) {
</div> </div>
</div> </div>
) : ( ) : (
data.map((event) => <Reply key={event.id} event={event} root={eventId} />) data.map((event) => <Reply key={event.id} event={event} rootEvent={eventId} />)
)} )}
</div> </div>
); );

View File

@@ -2,7 +2,7 @@ import { NDKEvent, NDKKind, NostrEvent } from '@nostr-dev-kit/ndk';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { memo } from 'react'; import { memo } from 'react';
import { useNDK } from '@libs/ndk/provider'; import { useArk } from '@libs/ark';
import { import {
MemoizedArticleKind, MemoizedArticleKind,
@@ -14,24 +14,22 @@ import {
import { User } from '@shared/user'; import { User } from '@shared/user';
export function Repost({ event }: { event: NDKEvent }) { export function Repost({ event }: { event: NDKEvent }) {
const { ndk } = useNDK(); const { ark } = useArk();
const { status, data: repostEvent } = useQuery({ const {
isLoading,
isError,
data: repostEvent,
} = useQuery({
queryKey: ['repost', event.id], queryKey: ['repost', event.id],
queryFn: async () => { queryFn: async () => {
try { try {
if (event.content.length > 50) { if (event.content.length > 50) {
const embed = JSON.parse(event.content) as NostrEvent; const embed = JSON.parse(event.content) as NostrEvent;
const embedEvent = new NDKEvent(ndk, embed); return ark.createNDKEvent({ event: embed });
return embedEvent;
} }
const id = event.tags.find((el) => el[0] === 'e')[1]; const id = event.tags.find((el) => el[0] === 'e')[1];
if (!id) throw new Error('Failed to get repost event id'); return await ark.getEventById({ id });
const ndkEvent = await ndk.fetchEvent(id);
if (!ndkEvent) return Promise.reject(new Error('Failed to get repost event'));
return ndkEvent;
} catch { } catch {
throw new Error('Failed to get repost event'); throw new Error('Failed to get repost event');
} }
@@ -53,7 +51,7 @@ export function Repost({ event }: { event: NDKEvent }) {
} }
}; };
if (status === 'pending') { if (isLoading) {
return ( return (
<div className="w-full px-3 pb-3"> <div className="w-full px-3 pb-3">
<NoteSkeleton /> <NoteSkeleton />
@@ -61,6 +59,21 @@ export function Repost({ event }: { event: NDKEvent }) {
); );
} }
if (isError) {
return (
<div className="mb-3 h-min w-full px-3">
<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} variant="repost" />
<div className="relative flex flex-col gap-2">
<div className="px-3">
<p>Failed to load event</p>
</div>
</div>
</div>
</div>
);
}
return ( return (
<div className="mb-3 h-min w-full px-3"> <div className="mb-3 h-min w-full px-3">
<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">

View File

@@ -2,20 +2,21 @@ import { NDKEvent } from '@nostr-dev-kit/ndk';
import { memo } from 'react'; import { memo } from 'react';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
import { useArk } from '@libs/ark';
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 '@utils/constants'; import { WIDGET_KIND } from '@utils/constants';
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, className }: { event: NDKEvent; className?: string }) { 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 { ark } = useArk();
const thread = getEventThread(event.tags); const thread = ark.getEventThread({ tags: event.tags });
return ( return (
<div className={twMerge('mb-3 h-min w-full px-3', className)}> <div className={twMerge('mb-3 h-min w-full px-3', className)}>

View File

@@ -1,4 +1,4 @@
import { useStorage } from '@libs/storage/provider'; import { useArk } from '@libs/ark';
import { CancelIcon } from '@shared/icons'; import { CancelIcon } from '@shared/icons';
import { User } from '@shared/user'; import { User } from '@shared/user';
@@ -14,7 +14,7 @@ export function TitleBar({
title?: string; title?: string;
isLive?: boolean; isLive?: boolean;
}) { }) {
const { db } = useStorage(); const { ark } = useArk();
const { removeWidget } = useWidget(); const { removeWidget } = useWidget();
return ( return (
@@ -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.contacts {ark.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.contacts?.length > 8 ? ( {ark.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.contacts?.length - 8} +{ark.account.contacts?.length - 8}
</span> </span>
</div> </div>
) : null} ) : null}

View File

@@ -1,10 +1,8 @@
import { NDKEvent, NDKKind, NDKUser } from '@nostr-dev-kit/ndk';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useNDK } from '@libs/ndk/provider'; import { useArk } from '@libs/ark';
import { useStorage } from '@libs/storage/provider';
import { NIP05 } from '@shared/nip05'; import { NIP05 } from '@shared/nip05';
@@ -12,18 +10,14 @@ import { useProfile } from '@utils/hooks/useProfile';
import { displayNpub } from '@utils/shortenKey'; import { displayNpub } from '@utils/shortenKey';
export function UserProfile({ pubkey }: { pubkey: string }) { export function UserProfile({ pubkey }: { pubkey: string }) {
const { db } = useStorage(); const { ark } = useArk();
const { ndk } = useNDK();
const { user } = useProfile(pubkey); const { user } = useProfile(pubkey);
const [followed, setFollowed] = useState(false); const [followed, setFollowed] = useState(false);
const navigate = useNavigate();
const follow = async (pubkey: string) => { const follow = async (pubkey: string) => {
try { try {
const user = ndk.getUser({ pubkey: db.account.pubkey }); const add = await ark.createContact({ pubkey });
const contacts = await user.follows();
const add = await user.follow(new NDKUser({ pubkey: pubkey }), contacts);
if (add) { if (add) {
setFollowed(true); setFollowed(true);
@@ -37,22 +31,9 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
const unfollow = async (pubkey: string) => { const unfollow = async (pubkey: string) => {
try { try {
if (!ndk.signer) return navigate('/new/privkey'); const remove = await ark.deleteContact({ pubkey });
const user = ndk.getUser({ pubkey: db.account.pubkey }); if (remove) {
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 (error) {
@@ -61,7 +42,7 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
}; };
useEffect(() => { useEffect(() => {
if (db.account.contacts.includes(pubkey)) { if (ark.account.contacts.includes(pubkey)) {
setFollowed(true); setFollowed(true);
} }
}, []); }, []);

View File

@@ -1,11 +1,10 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk'; import { NDKKind } from '@nostr-dev-kit/ndk';
import { useInfiniteQuery } from '@tanstack/react-query'; import { useInfiniteQuery } from '@tanstack/react-query';
import { FetchFilter } from 'nostr-fetch'; import { FetchFilter } from 'nostr-fetch';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { VList } from 'virtua'; import { VList } from 'virtua';
import { useNDK } from '@libs/ndk/provider'; import { useArk } from '@libs/ark';
import { useStorage } from '@libs/storage/provider';
import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons'; import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
import { MemoizedArticleNote } from '@shared/notes'; import { MemoizedArticleNote } from '@shared/notes';
@@ -16,8 +15,7 @@ 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 }) {
const { db } = useStorage(); const { ark } = useArk();
const { ndk, relayUrls, fetcher } = useNDK();
const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } = const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({ useInfiniteQuery({
queryKey: ['article', widget.id], queryKey: ['article', widget.id],
@@ -39,20 +37,19 @@ export function ArticleWidget({ widget }: { widget: Widget }) {
} else { } else {
filter = { filter = {
kinds: [NDKKind.Article], kinds: [NDKKind.Article],
authors: db.account.contacts, authors: ark.account.contacts,
}; };
} }
const events = await fetcher.fetchLatestEvents(relayUrls, filter, FETCH_LIMIT, { const events = await ark.getInfiniteEvents({
asOf: pageParam === 0 ? undefined : pageParam, filter,
abortSignal: signal, limit: FETCH_LIMIT,
pageParam,
signal,
dedup: false,
}); });
const ndkEvents = events.map((event) => { return events;
return new NDKEvent(ndk, event);
});
return ndkEvents.sort((a, b) => b.created_at - a.created_at);
}, },
getNextPageParam: (lastPage) => { getNextPageParam: (lastPage) => {
const lastEvent = lastPage.at(-1); const lastEvent = lastPage.at(-1);

View File

@@ -1,11 +1,9 @@
import { NDKEvent } from '@nostr-dev-kit/ndk';
import { useInfiniteQuery } from '@tanstack/react-query'; import { useInfiniteQuery } from '@tanstack/react-query';
import { FetchFilter } from 'nostr-fetch'; import { FetchFilter } from 'nostr-fetch';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { VList } from 'virtua'; import { VList } from 'virtua';
import { useNDK } from '@libs/ndk/provider'; import { useArk } from '@libs/ark';
import { useStorage } from '@libs/storage/provider';
import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons'; import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
import { MemoizedFileNote } from '@shared/notes'; import { MemoizedFileNote } from '@shared/notes';
@@ -16,8 +14,7 @@ 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 }) {
const { db } = useStorage(); const { ark } = useArk();
const { ndk, relayUrls, fetcher } = useNDK();
const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } = const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({ useInfiniteQuery({
queryKey: ['media', widget.id], queryKey: ['media', widget.id],
@@ -39,20 +36,19 @@ export function FileWidget({ widget }: { widget: Widget }) {
} else { } else {
filter = { filter = {
kinds: [1063], kinds: [1063],
authors: db.account.contacts, authors: ark.account.contacts,
}; };
} }
const events = await fetcher.fetchLatestEvents(relayUrls, filter, FETCH_LIMIT, { const events = await ark.getInfiniteEvents({
asOf: pageParam === 0 ? undefined : pageParam, filter,
abortSignal: signal, limit: FETCH_LIMIT,
pageParam,
signal,
dedup: false,
}); });
const ndkEvents = events.map((event) => { return events;
return new NDKEvent(ndk, event);
});
return ndkEvents.sort((a, b) => b.created_at - a.created_at);
}, },
getNextPageParam: (lastPage) => { getNextPageParam: (lastPage) => {
const lastEvent = lastPage.at(-1); const lastEvent = lastPage.at(-1);

View File

@@ -3,7 +3,7 @@ import { useInfiniteQuery } from '@tanstack/react-query';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { VList } from 'virtua'; import { VList } from 'virtua';
import { useNDK } from '@libs/ndk/provider'; import { useArk } from '@libs/ark';
import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons'; import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
import { import {
@@ -19,7 +19,7 @@ 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 }) {
const { relayUrls, ndk, fetcher } = useNDK(); const { ark } = useArk();
const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } = const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({ useInfiniteQuery({
queryKey: ['groupfeeds', widget.id], queryKey: ['groupfeeds', widget.id],
@@ -32,21 +32,18 @@ export function GroupWidget({ widget }: { widget: Widget }) {
pageParam: number; pageParam: number;
}) => { }) => {
const authors = JSON.parse(widget.content); const authors = JSON.parse(widget.content);
const events = await fetcher.fetchLatestEvents( const events = await ark.getInfiniteEvents({
relayUrls, filter: {
{
kinds: [NDKKind.Text, NDKKind.Repost], kinds: [NDKKind.Text, NDKKind.Repost],
authors: authors, authors: authors,
}, },
FETCH_LIMIT, limit: FETCH_LIMIT,
{ asOf: pageParam === 0 ? undefined : pageParam, abortSignal: signal } pageParam,
); signal,
dedup: false,
const ndkEvents = events.map((event) => {
return new NDKEvent(ndk, event);
}); });
return ndkEvents.sort((a, b) => b.created_at - a.created_at); return events;
}, },
getNextPageParam: (lastPage) => { getNextPageParam: (lastPage) => {
const lastEvent = lastPage.at(-1); const lastEvent = lastPage.at(-1);

View File

@@ -3,7 +3,7 @@ import { useInfiniteQuery } from '@tanstack/react-query';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { VList } from 'virtua'; import { VList } from 'virtua';
import { useNDK } from '@libs/ndk/provider'; import { useArk } from '@libs/ark';
import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons'; import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
import { MemoizedRepost, MemoizedTextNote, UnknownNote } from '@shared/notes'; import { MemoizedRepost, MemoizedTextNote, UnknownNote } from '@shared/notes';
@@ -14,7 +14,7 @@ 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 }) {
const { ndk, relayUrls, fetcher } = useNDK(); const { ark } = useArk();
const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } = const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({ useInfiniteQuery({
queryKey: ['hashtag', widget.id], queryKey: ['hashtag', widget.id],
@@ -26,21 +26,18 @@ export function HashtagWidget({ widget }: { widget: Widget }) {
signal: AbortSignal; signal: AbortSignal;
pageParam: number; pageParam: number;
}) => { }) => {
const events = await fetcher.fetchLatestEvents( const events = await ark.getInfiniteEvents({
relayUrls, filter: {
{
kinds: [NDKKind.Text, NDKKind.Repost], kinds: [NDKKind.Text, NDKKind.Repost],
'#t': [widget.content], '#t': [widget.content],
}, },
FETCH_LIMIT, limit: FETCH_LIMIT,
{ asOf: pageParam === 0 ? undefined : pageParam, abortSignal: signal } pageParam,
); signal,
dedup: false,
const ndkEvents = events.map((event) => {
return new NDKEvent(ndk, event);
}); });
return ndkEvents.sort((a, b) => b.created_at - a.created_at); return events;
}, },
getNextPageParam: (lastPage) => { getNextPageParam: (lastPage) => {
const lastEvent = lastPage.at(-1); const lastEvent = lastPage.at(-1);

View File

@@ -3,8 +3,7 @@ import { useInfiniteQuery } from '@tanstack/react-query';
import { useCallback, useMemo, useRef } from 'react'; import { useCallback, useMemo, useRef } from 'react';
import { VList, VListHandle } from 'virtua'; import { VList, VListHandle } from 'virtua';
import { useNDK } from '@libs/ndk/provider'; import { useArk } from '@libs/ark';
import { useStorage } from '@libs/storage/provider';
import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons'; import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
import { import {
@@ -19,8 +18,7 @@ import { LiveUpdater, WidgetWrapper } from '@shared/widgets';
import { FETCH_LIMIT } from '@utils/constants'; import { FETCH_LIMIT } from '@utils/constants';
export function NewsfeedWidget() { export function NewsfeedWidget() {
const { db } = useStorage(); const { ark } = useArk();
const { relayUrls, ndk, fetcher } = useNDK();
const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } = const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({ useInfiniteQuery({
queryKey: ['newsfeed'], queryKey: ['newsfeed'],
@@ -32,35 +30,19 @@ export function NewsfeedWidget() {
signal: AbortSignal; signal: AbortSignal;
pageParam: number; pageParam: number;
}) => { }) => {
const rootIds = new Set(); const events = await ark.getInfiniteEvents({
const dedupQueue = new Set(); filter: {
const events = await fetcher.fetchLatestEvents(
relayUrls,
{
kinds: [NDKKind.Text, NDKKind.Repost], kinds: [NDKKind.Text, NDKKind.Repost],
authors: db.account.contacts, authors: !ark.account.contacts.length
? [ark.account.pubkey]
: ark.account.contacts,
}, },
FETCH_LIMIT, limit: FETCH_LIMIT,
{ asOf: pageParam === 0 ? undefined : pageParam, abortSignal: signal } pageParam,
); signal,
const ndkEvents = events.map((event) => {
return new NDKEvent(ndk, event);
}); });
ndkEvents.forEach((event) => { return events;
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);
}, },
getNextPageParam: (lastPage) => { getNextPageParam: (lastPage) => {
const lastEvent = lastPage.at(-1); const lastEvent = lastPage.at(-1);

View File

@@ -1,10 +1,9 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk'; import { NDKEvent, NDKKind, NDKSubscription } from '@nostr-dev-kit/ndk';
import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query'; import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query';
import { useCallback, useEffect, useMemo } from 'react'; import { useCallback, useEffect, useMemo } from 'react';
import { VList } from 'virtua'; import { VList } from 'virtua';
import { useNDK } from '@libs/ndk/provider'; import { useArk } from '@libs/ark';
import { useStorage } from '@libs/storage/provider';
import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons'; import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
import { MemoizedNotifyNote, NoteSkeleton } from '@shared/notes'; import { MemoizedNotifyNote, NoteSkeleton } from '@shared/notes';
@@ -12,15 +11,12 @@ import { TitleBar } from '@shared/titleBar';
import { WidgetWrapper } from '@shared/widgets'; import { WidgetWrapper } from '@shared/widgets';
import { FETCH_LIMIT } from '@utils/constants'; import { FETCH_LIMIT } from '@utils/constants';
import { useNostr } from '@utils/hooks/useNostr';
import { sendNativeNotification } from '@utils/notification'; import { sendNativeNotification } from '@utils/notification';
export function NotificationWidget() { export function NotificationWidget() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { db } = useStorage(); const { ark } = useArk();
const { sub } = useNostr();
const { ndk, relayUrls, fetcher } = useNDK();
const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } = const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({ useInfiniteQuery({
queryKey: ['notification'], queryKey: ['notification'],
@@ -32,21 +28,17 @@ export function NotificationWidget() {
signal: AbortSignal; signal: AbortSignal;
pageParam: number; pageParam: number;
}) => { }) => {
const events = await fetcher.fetchLatestEvents( const events = await ark.getInfiniteEvents({
relayUrls, filter: {
{
kinds: [NDKKind.Text, NDKKind.Repost, NDKKind.Reaction, NDKKind.Zap], kinds: [NDKKind.Text, NDKKind.Repost, NDKKind.Reaction, NDKKind.Zap],
'#p': [db.account.pubkey], '#p': [ark.account.pubkey],
}, },
FETCH_LIMIT, limit: FETCH_LIMIT,
{ asOf: pageParam === 0 ? undefined : pageParam, abortSignal: signal } pageParam,
); signal,
const ndkEvents = events.map((event) => {
return new NDKEvent(ndk, event);
}); });
return ndkEvents.sort((a, b) => b.created_at - a.created_at); return events;
}, },
getNextPageParam: (lastPage) => { getNextPageParam: (lastPage) => {
const lastEvent = lastPage.at(-1); const lastEvent = lastPage.at(-1);
@@ -65,21 +57,24 @@ export function NotificationWidget() {
); );
const renderEvent = useCallback((event: NDKEvent) => { const renderEvent = useCallback((event: NDKEvent) => {
if (event.pubkey === db.account.pubkey) return null; if (event.pubkey === ark.account.pubkey) return null;
return <MemoizedNotifyNote key={event.id} event={event} />; return <MemoizedNotifyNote key={event.id} event={event} />;
}, []); }, []);
useEffect(() => { useEffect(() => {
if (status === 'success' && db.account) { let sub: NDKSubscription = undefined;
if (status === 'success' && ark.account) {
const filter = { const filter = {
kinds: [NDKKind.Text, NDKKind.Repost, NDKKind.Reaction, NDKKind.Zap], kinds: [NDKKind.Text, NDKKind.Repost, NDKKind.Reaction, NDKKind.Zap],
'#p': [db.account.pubkey], '#p': [ark.account.pubkey],
since: Math.floor(Date.now() / 1000), since: Math.floor(Date.now() / 1000),
}; };
sub( sub = ark.subscribe({
filter, filter,
async (event) => { closeOnEose: false,
cb: async (event) => {
queryClient.setQueryData( queryClient.setQueryData(
['notification'], ['notification'],
(prev: { pageParams: number; pages: Array<NDKEvent[]> }) => ({ (prev: { pageParams: number; pages: Array<NDKEvent[]> }) => ({
@@ -88,21 +83,18 @@ export function NotificationWidget() {
}) })
); );
const user = ndk.getUser({ pubkey: event.pubkey }); const profile = await ark.getUserProfile({ pubkey: event.pubkey });
await user.fetchProfile();
switch (event.kind) { switch (event.kind) {
case NDKKind.Text: case NDKKind.Text:
return await sendNativeNotification( return await sendNativeNotification(
`${ `${profile.displayName || profile.name} has replied to your note`
user.profile.displayName || user.profile.name
} has replied to your note`
); );
case NDKKind.EncryptedDirectMessage: { case NDKKind.EncryptedDirectMessage: {
if (location.pathname !== '/chats') { if (location.pathname !== '/chats') {
return await sendNativeNotification( return await sendNativeNotification(
`${ `${
user.profile.displayName || user.profile.name profile.displayName || profile.name
} has send you a encrypted message` } has send you a encrypted message`
); );
} else { } else {
@@ -111,28 +103,28 @@ export function NotificationWidget() {
} }
case NDKKind.Repost: case NDKKind.Repost:
return await sendNativeNotification( return await sendNativeNotification(
`${ `${profile.displayName || profile.name} has reposted to your note`
user.profile.displayName || user.profile.name
} has reposted to your note`
); );
case NDKKind.Reaction: case NDKKind.Reaction:
return await sendNativeNotification( return await sendNativeNotification(
`${user.profile.displayName || user.profile.name} has reacted ${ `${profile.displayName || profile.name} has reacted ${
event.content event.content
} to your note` } to your note`
); );
case NDKKind.Zap: case NDKKind.Zap:
return await sendNativeNotification( return await sendNativeNotification(
`${user.profile.displayName || user.profile.name} has zapped to your note` `${profile.displayName || profile.name} has zapped to your note`
); );
default: default:
break; break;
} }
}, },
false, });
'notification'
);
} }
return () => {
if (sub) sub.stop();
};
}, [status]); }, [status]);
return ( return (

View File

@@ -1,7 +1,7 @@
import * as Dialog from '@radix-ui/react-dialog'; import * as Dialog from '@radix-ui/react-dialog';
import { useState } from 'react'; import { useState } from 'react';
import { useStorage } from '@libs/storage/provider'; import { useArk } from '@libs/ark';
import { import {
ArrowRightCircleIcon, ArrowRightCircleIcon,
@@ -16,7 +16,7 @@ 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 }) {
const { db } = useStorage(); const { ark } = useArk();
const { replaceWidget } = useWidget(); const { replaceWidget } = useWidget();
const [title, setTitle] = useState<string>(''); const [title, setTitle] = useState<string>('');
@@ -95,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.contacts.map((item: string) => ( {ark.account?.contacts?.map((item: string) => (
<button <button
key={item} key={item}
type="button" type="button"

View File

@@ -1,16 +1,13 @@
import { NDKEvent, NDKFilter, NDKKind, NDKSubscription } from '@nostr-dev-kit/ndk'; import { NDKEvent, NDKKind, NDKSubscription } from '@nostr-dev-kit/ndk';
import { QueryStatus, useQueryClient } from '@tanstack/react-query'; import { QueryStatus, useQueryClient } from '@tanstack/react-query';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useNDK } from '@libs/ndk/provider'; import { useArk } from '@libs/ark';
import { useStorage } from '@libs/storage/provider';
import { ChevronUpIcon } from '@shared/icons'; import { ChevronUpIcon } from '@shared/icons';
export function LiveUpdater({ status }: { status: QueryStatus }) { export function LiveUpdater({ status }: { status: QueryStatus }) {
const { db } = useStorage(); const { ark } = useArk();
const { ndk } = useNDK();
const [events, setEvents] = useState<NDKEvent[]>([]); const [events, setEvents] = useState<NDKEvent[]>([]);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -30,17 +27,16 @@ export function LiveUpdater({ status }: { status: QueryStatus }) {
useEffect(() => { useEffect(() => {
let sub: NDKSubscription = undefined; let sub: NDKSubscription = undefined;
if (status === 'success' && db.account && db.account?.contacts?.length > 0) { if (status === 'success' && ark.account && ark.account?.contacts?.length > 0) {
const filter: NDKFilter = { sub = ark.subscribe({
kinds: [NDKKind.Text, NDKKind.Repost], filter: {
authors: db.account.contacts, kinds: [NDKKind.Text, NDKKind.Repost],
since: Math.floor(Date.now() / 1000), authors: ark.account.contacts,
}; since: Math.floor(Date.now() / 1000),
},
sub = ndk.subscribe(filter, { closeOnEose: false, groupable: false }); closeOnEose: false,
sub.addListener('event', (event: NDKEvent) => cb: (event: NDKEvent) => setEvents((prev) => [...prev, event]),
setEvents((prev) => [...prev, event]) });
);
} }
return () => { return () => {

View File

@@ -1,10 +1,8 @@
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';
import { useNDK } from '@libs/ndk/provider'; import { useArk } from '@libs/ark';
import { useStorage } from '@libs/storage/provider';
import { FollowIcon } from '@shared/icons'; import { FollowIcon } from '@shared/icons';
@@ -16,9 +14,7 @@ export interface Profile {
} }
export function NostrBandUserProfile({ data }: { data: Profile }) { export function NostrBandUserProfile({ data }: { data: Profile }) {
const { db } = useStorage(); const { ark } = useArk();
const { ndk } = useNDK();
const [followed, setFollowed] = useState(false); const [followed, setFollowed] = useState(false);
const navigate = useNavigate(); const navigate = useNavigate();
@@ -26,12 +22,10 @@ export function NostrBandUserProfile({ data }: { data: Profile }) {
const follow = async (pubkey: string) => { const follow = async (pubkey: string) => {
try { try {
if (!ndk.signer) return navigate('/new/privkey'); if (!ark.readyToSign) return navigate('/new/privkey');
setFollowed(true); setFollowed(true);
const user = ndk.getUser({ pubkey: db.account.pubkey }); const add = ark.createContact({ pubkey });
const contacts = await user.follows();
const add = await user.follow(new NDKUser({ pubkey: pubkey }), contacts);
if (!add) { if (!add) {
toast.success('You already follow this user'); toast.success('You already follow this user');
@@ -44,7 +38,7 @@ export function NostrBandUserProfile({ data }: { data: Profile }) {
}; };
useEffect(() => { useEffect(() => {
if (db.account.contacts.includes(data.pubkey)) { if (ark.account.contacts.includes(data.pubkey)) {
setFollowed(true); setFollowed(true);
} }
}, []); }, []);

View File

@@ -2,6 +2,8 @@ import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { WVList } from 'virtua'; import { WVList } from 'virtua';
import { useArk } from '@libs/ark';
import { LoaderIcon } from '@shared/icons'; import { LoaderIcon } from '@shared/icons';
import { import {
ChildNote, ChildNote,
@@ -17,16 +19,15 @@ import { User } from '@shared/user';
import { WidgetWrapper } from '@shared/widgets'; import { WidgetWrapper } from '@shared/widgets';
import { useEvent } from '@utils/hooks/useEvent'; import { useEvent } from '@utils/hooks/useEvent';
import { useNostr } from '@utils/hooks/useNostr';
import { Widget } from '@utils/types'; import { Widget } from '@utils/types';
export function ThreadWidget({ widget }: { widget: Widget }) { export function ThreadWidget({ widget }: { widget: Widget }) {
const { isFetching, isError, data } = useEvent(widget.content); const { isFetching, isError, data } = useEvent(widget.content);
const { getEventThread } = useNostr(); const { ark } = useArk();
const renderKind = useCallback( const renderKind = useCallback(
(event: NDKEvent) => { (event: NDKEvent) => {
const thread = getEventThread(event.tags); const thread = ark.getEventThread({ tags: event.tags });
switch (event.kind) { switch (event.kind) {
case NDKKind.Text: case NDKKind.Text:
return ( return (

View File

@@ -1,10 +1,9 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk'; import { NDKEvent, NDKFilter, NDKKind } from '@nostr-dev-kit/ndk';
import { useInfiniteQuery } from '@tanstack/react-query'; import { useInfiniteQuery } from '@tanstack/react-query';
import { FetchFilter } from 'nostr-fetch';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { VList } from 'virtua'; import { VList } from 'virtua';
import { useNDK } from '@libs/ndk/provider'; import { useArk } from '@libs/ark';
import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons'; import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
import { import {
@@ -20,7 +19,7 @@ import { FETCH_LIMIT } from '@utils/constants';
import { Widget } from '@utils/types'; import { Widget } from '@utils/types';
export function TopicWidget({ widget }: { widget: Widget }) { export function TopicWidget({ widget }: { widget: Widget }) {
const { relayUrls, ndk, fetcher } = useNDK(); const { ark } = useArk();
const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } = const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({ useInfiniteQuery({
queryKey: ['topic', widget.id], queryKey: ['topic', widget.id],
@@ -33,35 +32,19 @@ export function TopicWidget({ widget }: { widget: Widget }) {
pageParam: number; pageParam: number;
}) => { }) => {
const hashtags: string[] = JSON.parse(widget.content as string); const hashtags: string[] = JSON.parse(widget.content as string);
const filter: FetchFilter = { const filter: NDKFilter = {
kinds: [NDKKind.Text, NDKKind.Repost], kinds: [NDKKind.Text, NDKKind.Repost],
'#t': hashtags.map((tag) => tag.replace('#', '')), '#t': hashtags.map((tag) => tag.replace('#', '')),
}; };
const rootIds = new Set(); const events = await ark.getInfiniteEvents({
const dedupQueue = new Set(); filter,
limit: FETCH_LIMIT,
const events = await fetcher.fetchLatestEvents(relayUrls, filter, FETCH_LIMIT, { pageParam,
asOf: pageParam === 0 ? undefined : pageParam, signal,
abortSignal: signal,
}); });
const ndkEvents = events.map((event) => { return events;
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);
}, },
getNextPageParam: (lastPage) => { getNextPageParam: (lastPage) => {
const lastEvent = lastPage.at(-1); const lastEvent = lastPage.at(-1);

View File

@@ -1,10 +1,11 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk'; import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { useQuery } from '@tanstack/react-query'; import { useInfiniteQuery } from '@tanstack/react-query';
import { useCallback } from 'react'; import { useCallback, useMemo } from 'react';
import { WVList } from 'virtua'; import { WVList } from 'virtua';
import { useNDK } from '@libs/ndk/provider'; import { useArk } from '@libs/ark';
import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
import { import {
MemoizedRepost, MemoizedRepost,
MemoizedTextNote, MemoizedTextNote,
@@ -15,43 +16,46 @@ import { TitleBar } from '@shared/titleBar';
import { UserProfile } from '@shared/userProfile'; import { UserProfile } from '@shared/userProfile';
import { WidgetWrapper } from '@shared/widgets'; import { WidgetWrapper } from '@shared/widgets';
import { nHoursAgo } from '@utils/date'; import { FETCH_LIMIT } from '@utils/constants';
import { Widget } from '@utils/types'; import { Widget } from '@utils/types';
export function UserWidget({ widget }: { widget: Widget }) { export function UserWidget({ widget }: { widget: Widget }) {
const { ndk } = useNDK(); const { ark } = useArk();
const { status, data } = useQuery({ const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
queryKey: ['user-posts', widget.id], useInfiniteQuery({
queryFn: async () => { queryKey: ['user-posts', widget.content],
const rootIds = new Set(); initialPageParam: 0,
const dedupQueue = new Set(); queryFn: async ({
signal,
pageParam,
}: {
signal: AbortSignal;
pageParam: number;
}) => {
const events = await ark.getInfiniteEvents({
filter: {
kinds: [NDKKind.Text, NDKKind.Repost],
authors: [widget.content],
},
limit: FETCH_LIMIT,
pageParam,
signal,
});
const events = await ndk.fetchEvents({ return events;
kinds: [NDKKind.Text, NDKKind.Repost], },
authors: [widget.content], getNextPageParam: (lastPage) => {
since: nHoursAgo(24), const lastEvent = lastPage.at(-1);
}); if (!lastEvent) return;
return lastEvent.created_at - 1;
},
refetchOnWindowFocus: false,
});
const ndkEvents = [...events]; const allEvents = useMemo(
() => (data ? data.pages.flatMap((page) => page) : []),
ndkEvents.forEach((event) => { [data]
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);
},
staleTime: Infinity,
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
});
// render event match event kind // render event match event kind
const renderItem = useCallback( const renderItem = useCallback(
@@ -86,19 +90,28 @@ export function UserWidget({ widget }: { widget: Widget }) {
<NoteSkeleton /> <NoteSkeleton />
</div> </div>
</div> </div>
) : data.length === 0 ? (
<div className="px-3 py-1.5">
<div className="rounded-xl bg-neutral-100 px-3 py-6 dark:bg-neutral-900">
<div className="flex flex-col items-center gap-4">
<p className="text-center text-sm text-neutral-900 dark:text-neutral-100">
No new post from 24 hours ago
</p>
</div>
</div>
</div>
) : ( ) : (
data.map((item) => renderItem(item)) allEvents.map((item) => renderItem(item))
)} )}
<div className="flex h-16 items-center justify-center px-3 pb-3">
{hasNextPage ? (
<button
type="button"
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
className="inline-flex h-10 w-max items-center justify-center gap-2 rounded-full bg-blue-500 px-6 font-medium text-white hover:bg-blue-600 focus:outline-none"
>
{isFetchingNextPage ? (
<LoaderIcon className="h-4 w-4 animate-spin" />
) : (
<>
<ArrowRightCircleIcon className="h-5 w-5" />
Load more
</>
)}
</button>
) : null}
</div>
</div> </div>
</div> </div>
</WVList> </WVList>

View File

@@ -1,45 +1,41 @@
import { NDKEvent, NDKSubscriptionCacheUsage, NostrEvent } from '@nostr-dev-kit/ndk'; import { NDKEvent, NostrEvent } from '@nostr-dev-kit/ndk';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
import { AddressPointer } from 'nostr-tools/lib/types/nip19'; import { AddressPointer } from 'nostr-tools/lib/types/nip19';
import { useNDK } from '@libs/ndk/provider'; import { useArk } from '@libs/ark';
export function useEvent(id: undefined | string, embed?: undefined | string) { export function useEvent(id: undefined | string, embed?: undefined | string) {
const { ndk } = useNDK(); const { ark } = useArk();
const { status, isFetching, isError, data } = useQuery({ const { status, isFetching, isError, data } = useQuery({
queryKey: ['event', id], queryKey: ['event', id],
queryFn: async () => { queryFn: async () => {
let event: NDKEvent = undefined;
const naddr = id.startsWith('naddr') const naddr = id.startsWith('naddr')
? (nip19.decode(id).data as AddressPointer) ? (nip19.decode(id).data as AddressPointer)
: null; : null;
// return event refer from naddr // return event refer from naddr
if (naddr) { if (naddr) {
const rEvents = await ndk.fetchEvents({ const events = await ark.getAllEvents({
kinds: [naddr.kind], filter: {
'#d': [naddr.identifier], kinds: [naddr.kind],
authors: [naddr.pubkey], '#d': [naddr.identifier],
authors: [naddr.pubkey],
},
}); });
event = events.slice(-1)[0];
const rEvent = [...rEvents].slice(-1)[0];
if (!rEvent) throw new Error('event not found');
return rEvent;
} }
// return embed event (nostr.band api) // return embed event (nostr.band api)
if (embed) { if (embed) {
const embedEvent: NostrEvent = JSON.parse(embed); const embedEvent: NostrEvent = JSON.parse(embed);
const ndkEvent = new NDKEvent(ndk, embedEvent); event = ark.createNDKEvent({ event: embedEvent });
return ndkEvent;
} }
// get event from relay // get event from relay
const event = await ndk.fetchEvent(id, { event = await ark.getEventById({ id });
cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST,
});
if (!event) if (!event)
throw new Error(`Cannot get event with ${id}, will be retry after 10 seconds`); throw new Error(`Cannot get event with ${id}, will be retry after 10 seconds`);

View File

@@ -1,299 +0,0 @@
import {
NDKEvent,
NDKFilter,
NDKKind,
NDKSubscription,
NDKTag,
} from '@nostr-dev-kit/ndk';
import { open } from '@tauri-apps/plugin-dialog';
import { readBinaryFile } from '@tauri-apps/plugin-fs';
import { fetch } from '@tauri-apps/plugin-http';
import { LRUCache } from 'lru-cache';
import { NostrEventExt } from 'nostr-fetch';
import { useMemo } from 'react';
import { useNDK } from '@libs/ndk/provider';
import { useStorage } from '@libs/storage/provider';
import { nHoursAgo } from '@utils/date';
import { getMultipleRandom } from '@utils/transform';
import { NDKEventWithReplies } from '@utils/types';
export function useNostr() {
const { db } = useStorage();
const { ndk, relayUrls, fetcher } = useNDK();
const subManager = useMemo(
() =>
new LRUCache<string, NDKSubscription, void>({
max: 4,
dispose: (sub) => sub.stop(),
}),
[]
);
const sub = async (
filter: NDKFilter,
callback: (event: NDKEvent) => void,
groupable?: boolean,
subKey?: string
) => {
if (!ndk) throw new Error('NDK instance not found');
const key = subKey ?? JSON.stringify(filter);
if (!subManager.get(key)) {
const subEvent = ndk.subscribe(filter, {
closeOnEose: false,
groupable: groupable ?? true,
});
subEvent.addListener('event', (event: NDKEvent) => {
callback(event);
});
subManager.set(JSON.stringify(filter), subEvent);
console.log('sub: ', key);
}
};
const getEventThread = (tags: NDKTag[]) => {
let rootEventId: string = null;
let replyEventId: string = null;
const events = tags.filter((el) => el[0] === 'e');
if (!events.length) return null;
if (events.length === 1)
return {
rootEventId: events[0][1],
replyEventId: null,
};
if (events.length > 1) {
rootEventId = events.find((el) => el[3] === 'root')?.[1];
replyEventId = events.find((el) => el[3] === 'reply')?.[1];
if (!rootEventId && !replyEventId) {
rootEventId = events[0][1];
replyEventId = events[1][1];
}
}
return {
rootEventId,
replyEventId,
};
};
const getAllActivities = async (limit?: number) => {
try {
const events = await ndk.fetchEvents({
kinds: [NDKKind.Text, NDKKind.Repost, NDKKind.Reaction, NDKKind.Zap],
'#p': [db.account.pubkey],
limit: limit ?? 50,
});
return [...events];
} catch (e) {
console.error('Error fetching activities', e);
}
};
const fetchNIP04Messages = async (sender: string) => {
let senderMessages: NostrEventExt<false>[] = [];
if (sender !== db.account.pubkey) {
senderMessages = await fetcher.fetchAllEvents(
relayUrls,
{
kinds: [NDKKind.EncryptedDirectMessage],
authors: [sender],
'#p': [db.account.pubkey],
},
{ since: 0 }
);
}
const userMessages = await fetcher.fetchAllEvents(
relayUrls,
{
kinds: [NDKKind.EncryptedDirectMessage],
authors: [db.account.pubkey],
'#p': [sender],
},
{ since: 0 }
);
const all = [...senderMessages, ...userMessages].sort(
(a, b) => a.created_at - b.created_at
);
return all as unknown as NDKEvent[];
};
const fetchAllReplies = async (id: string, data?: NDKEventWithReplies[]) => {
let events = data || null;
if (!data) {
events = (await fetcher.fetchAllEvents(
relayUrls,
{
kinds: [NDKKind.Text],
'#e': [id],
},
{ since: 0 },
{ sort: true }
)) as unknown as NDKEventWithReplies[];
}
if (events.length > 0) {
const replies = new Set();
events.forEach((event) => {
const tags = event.tags.filter((el) => el[0] === 'e' && el[1] !== id);
if (tags.length > 0) {
tags.forEach((tag) => {
const rootIndex = events.findIndex((el) => el.id === tag[1]);
if (rootIndex !== -1) {
const rootEvent = events[rootIndex];
if (rootEvent && rootEvent.replies) {
rootEvent.replies.push(event);
} else {
rootEvent.replies = [event];
}
replies.add(event.id);
}
});
}
});
const cleanEvents = events.filter((ev) => !replies.has(ev.id));
return cleanEvents;
}
return events;
};
const getAllNIP04Chats = async () => {
const events = await fetcher.fetchAllEvents(
relayUrls,
{
kinds: [NDKKind.EncryptedDirectMessage],
'#p': [db.account.pubkey],
},
{ since: 0 }
);
const dedup: NDKEvent[] = Object.values(
events.reduce((ev, { id, content, pubkey, created_at, tags }) => {
if (ev[pubkey]) {
if (ev[pubkey].created_at < created_at) {
ev[pubkey] = { id, content, pubkey, created_at, tags };
}
} else {
ev[pubkey] = { id, content, pubkey, created_at, tags };
}
return ev;
}, {})
);
return dedup;
};
const getContactsByPubkey = async (pubkey: string) => {
const user = ndk.getUser({ pubkey: pubkey });
const follows = [...(await user.follows())].map((user) => user.hexpubkey);
return getMultipleRandom([...follows], 10);
};
const getEventsByPubkey = async (pubkey: string) => {
const events = await fetcher.fetchAllEvents(
relayUrls,
{ authors: [pubkey], kinds: [NDKKind.Text, NDKKind.Repost, NDKKind.Article] },
{ since: nHoursAgo(24) },
{ sort: true }
);
return events as unknown as NDKEvent[];
};
const getAllRelaysByUsers = async () => {
const relayMap = new Map<string, string[]>();
const relayEvents = fetcher.fetchLatestEventsPerAuthor(
{
authors: db.account.contacts,
relayUrls: relayUrls,
},
{ kinds: [NDKKind.RelayList] },
5
);
for await (const { author, events } of relayEvents) {
if (events[0]) {
events[0].tags.forEach((tag) => {
const users = relayMap.get(tag[1]);
if (!users) return relayMap.set(tag[1], [author]);
return users.push(author);
});
}
}
return relayMap;
};
const createZap = async (event: NDKEvent, amount: number, message?: string) => {
// @ts-expect-error, NostrEvent to NDKEvent
const ndkEvent = new NDKEvent(ndk, event);
const res = await ndkEvent.zap(amount, message ?? 'zap from lume');
return res;
};
const upload = async (ext: string[] = []) => {
const defaultExts = ['png', 'jpeg', 'jpg', 'gif'].concat(ext);
const selected = await open({
multiple: false,
filters: [
{
name: 'Image',
extensions: defaultExts,
},
],
});
if (!selected) return null;
const file = await readBinaryFile(selected.path);
const blob = new Blob([file]);
const data = new FormData();
data.append('fileToUpload', blob);
data.append('submit', 'Upload Image');
const res = await fetch('https://nostr.build/api/v2/upload/files', {
method: 'POST',
body: data,
});
if (!res.ok) return null;
const json = await res.json();
const content = json.data[0];
return content.url as string;
};
return {
sub,
getEventThread,
getAllNIP04Chats,
getContactsByPubkey,
getEventsByPubkey,
getAllRelaysByUsers,
getAllActivities,
fetchNIP04Messages,
fetchAllReplies,
createZap,
upload,
};
}

View File

@@ -1,11 +1,11 @@
import { NDKSubscriptionCacheUsage, NDKUserProfile } from '@nostr-dev-kit/ndk'; import { NDKUserProfile } from '@nostr-dev-kit/ndk';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
import { useNDK } from '@libs/ndk/provider'; import { useArk } from '@libs/ark';
export function useProfile(pubkey: string, embed?: string) { export function useProfile(pubkey: string, embed?: string) {
const { ndk } = useNDK(); const { ark } = useArk();
const { const {
isLoading, isLoading,
isError, isError,
@@ -29,10 +29,7 @@ export function useProfile(pubkey: string, embed?: string) {
if (decoded.type === 'npub') hexstring = decoded.data; if (decoded.type === 'npub') hexstring = decoded.data;
} }
const user = ndk.getUser({ pubkey: hexstring }); const profile = await ark.getUserProfile({ pubkey: hexstring });
const profile = await user.fetchProfile({
cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST,
});
if (!profile) if (!profile)
throw new Error( throw new Error(

View File

@@ -1,46 +1,44 @@
import { NDKEvent, NDKKind, NDKRelayUrl, NDKTag } from '@nostr-dev-kit/ndk'; import { NDKKind, NDKRelayUrl, NDKTag } from '@nostr-dev-kit/ndk';
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useNDK } from '@libs/ndk/provider'; import { useArk } from '@libs/ark';
import { useStorage } from '@libs/storage/provider';
export function useRelay() { export function useRelay() {
const { db } = useStorage(); const { ark } = useArk();
const { ndk } = useNDK();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const connectRelay = useMutation({ const connectRelay = useMutation({
mutationFn: async (relay: NDKRelayUrl, purpose?: 'read' | 'write' | undefined) => { mutationFn: async (relay: NDKRelayUrl, purpose?: 'read' | 'write' | undefined) => {
// Cancel any outgoing refetches // Cancel any outgoing refetches
await queryClient.cancelQueries({ queryKey: ['relays', db.account.pubkey] }); await queryClient.cancelQueries({ queryKey: ['relays', ark.account.pubkey] });
// Snapshot the previous value // Snapshot the previous value
const prevRelays: NDKTag[] = queryClient.getQueryData([ const prevRelays: NDKTag[] = queryClient.getQueryData([
'relays', 'relays',
db.account.pubkey, ark.account.pubkey,
]); ]);
// create new relay list if not exist // create new relay list if not exist
if (!prevRelays) { if (!prevRelays) {
const newListEvent = new NDKEvent(ndk); await ark.createEvent({
newListEvent.kind = NDKKind.RelayList; kind: NDKKind.RelayList,
newListEvent.tags = [['r', relay, purpose ?? '']]; tags: [['r', relay, purpose ?? '']],
await newListEvent.publish(); publish: true,
});
} }
// add relay to exist list // add relay to exist list
const index = prevRelays.findIndex((el) => el[1] === relay); const index = prevRelays.findIndex((el) => el[1] === relay);
if (index > -1) return; if (index > -1) return;
const event = new NDKEvent(ndk); await ark.createEvent({
event.kind = NDKKind.RelayList; kind: NDKKind.RelayList,
event.tags = [...prevRelays, ['r', relay, purpose ?? '']]; tags: [...prevRelays, ['r', relay, purpose ?? '']],
publish: true,
await event.publish(); });
// Optimistically update to the new value // Optimistically update to the new value
queryClient.setQueryData(['relays', db.account.pubkey], (prev: NDKTag[]) => [ queryClient.setQueryData(['relays', ark.account.pubkey], (prev: NDKTag[]) => [
...prev, ...prev,
['r', relay, purpose ?? ''], ['r', relay, purpose ?? ''],
]); ]);
@@ -49,19 +47,19 @@ export function useRelay() {
return { prevRelays }; return { prevRelays };
}, },
onSettled: () => { onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['relays', db.account.pubkey] }); queryClient.invalidateQueries({ queryKey: ['relays', ark.account.pubkey] });
}, },
}); });
const removeRelay = useMutation({ const removeRelay = useMutation({
mutationFn: async (relay: NDKRelayUrl) => { mutationFn: async (relay: NDKRelayUrl) => {
// Cancel any outgoing refetches // Cancel any outgoing refetches
await queryClient.cancelQueries({ queryKey: ['relays', db.account.pubkey] }); await queryClient.cancelQueries({ queryKey: ['relays', ark.account.pubkey] });
// Snapshot the previous value // Snapshot the previous value
const prevRelays: NDKTag[] = queryClient.getQueryData([ const prevRelays: NDKTag[] = queryClient.getQueryData([
'relays', 'relays',
db.account.pubkey, ark.account.pubkey,
]); ]);
if (!prevRelays) return; if (!prevRelays) return;
@@ -69,19 +67,20 @@ export function useRelay() {
const index = prevRelays.findIndex((el) => el[1] === relay); const index = prevRelays.findIndex((el) => el[1] === relay);
if (index > -1) prevRelays.splice(index, 1); if (index > -1) prevRelays.splice(index, 1);
const event = new NDKEvent(ndk); await ark.createEvent({
event.kind = NDKKind.RelayList; kind: NDKKind.RelayList,
event.tags = prevRelays; tags: prevRelays,
await event.publish(); publish: true,
});
// Optimistically update to the new value // Optimistically update to the new value
queryClient.setQueryData(['relays', db.account.pubkey], prevRelays); queryClient.setQueryData(['relays', ark.account.pubkey], prevRelays);
// Return a context object with the snapshotted value // Return a context object with the snapshotted value
return { prevRelays }; return { prevRelays };
}, },
onSettled: () => { onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['relays', db.account.pubkey] }); queryClient.invalidateQueries({ queryKey: ['relays', ark.account.pubkey] });
}, },
}); });

View File

@@ -4,7 +4,7 @@ import { ReactNode } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import reactStringReplace from 'react-string-replace'; import reactStringReplace from 'react-string-replace';
import { useStorage } from '@libs/storage/provider'; import { useArk } from '@libs/ark';
import { import {
Hashtag, Hashtag,
@@ -23,9 +23,19 @@ const NOSTR_MENTIONS = [
'npub1', 'npub1',
'nprofile1', 'nprofile1',
'naddr1', 'naddr1',
'Nostr:npub1',
'Nostr:nprofile1',
'Nostr:naddre1',
]; ];
const NOSTR_EVENTS = ['nostr:note1', 'note1', 'nostr:nevent1', 'nevent1']; const NOSTR_EVENTS = [
'nostr:note1',
'note1',
'nostr:nevent1',
'nevent1',
'Nostr:note1',
'Nostr:nevent1',
];
// const BITCOINS = ['lnbc', 'bc1p', 'bc1q']; // const BITCOINS = ['lnbc', 'bc1p', 'bc1q'];
@@ -46,7 +56,7 @@ const VIDEOS = [
]; ];
export function useRichContent(content: string, textmode: boolean = false) { export function useRichContent(content: string, textmode: boolean = false) {
const { db } = useStorage(); const { ark } = useArk();
let parsedContent: string | ReactNode[] = content.replace(/\n+/g, '\n'); let parsedContent: string | ReactNode[] = content.replace(/\n+/g, '\n');
let linkPreview: string; let linkPreview: string;
@@ -58,7 +68,7 @@ export function useRichContent(content: string, textmode: boolean = false) {
const words = text.split(/( |\n)/); const words = text.split(/( |\n)/);
if (!textmode) { if (!textmode) {
if (db.settings.media) { if (ark.settings.media) {
images = words.filter((word) => IMAGES.some((el) => word.endsWith(el))); images = words.filter((word) => IMAGES.some((el) => word.endsWith(el)));
videos = words.filter((word) => VIDEOS.some((el) => word.endsWith(el))); videos = words.filter((word) => VIDEOS.some((el) => word.endsWith(el)));
} }
@@ -90,7 +100,7 @@ export function useRichContent(content: string, textmode: boolean = false) {
if (hashtags.length) { if (hashtags.length) {
hashtags.forEach((hashtag) => { hashtags.forEach((hashtag) => {
parsedContent = reactStringReplace(parsedContent, hashtag, (match, i) => { parsedContent = reactStringReplace(parsedContent, hashtag, (match, i) => {
if (db.settings.hashtag) return <Hashtag key={match + i} tag={hashtag} />; if (ark.settings.hashtag) return <Hashtag key={match + i} tag={hashtag} />;
return null; return null;
}); });
}); });

View File

@@ -4,14 +4,14 @@ import tippy from 'tippy.js';
import { MentionList } from '@app/new/components'; import { MentionList } from '@app/new/components';
import { useStorage } from '@libs/storage/provider'; import { useArk } from '@libs/ark';
export function useSuggestion() { export function useSuggestion() {
const { db } = useStorage(); const { ark } = useArk();
const suggestion: MentionOptions['suggestion'] = { const suggestion: MentionOptions['suggestion'] = {
items: async ({ query }) => { items: async ({ query }) => {
const users = await db.getAllCacheUsers(); const users = await ark.getAllCacheUsers();
return users return users
.filter((item) => { .filter((item) => {
if (item.name) return item.name.toLowerCase().startsWith(query.toLowerCase()); if (item.name) return item.name.toLowerCase().startsWith(query.toLowerCase());

View File

@@ -1,16 +1,16 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useStorage } from '@libs/storage/provider'; import { useArk } from '@libs/ark';
import { Widget } from '@utils/types'; import { Widget } from '@utils/types';
export function useWidget() { export function useWidget() {
const { db } = useStorage(); const { ark } = useArk();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const addWidget = useMutation({ const addWidget = useMutation({
mutationFn: async (widget: Widget) => { mutationFn: async (widget: Widget) => {
return await db.createWidget(widget.kind, widget.title, widget.content); return await ark.createWidget(widget.kind, widget.title, widget.content);
}, },
onSuccess: (data) => { onSuccess: (data) => {
queryClient.setQueryData(['widgets'], (old: Widget[]) => [...old, data]); queryClient.setQueryData(['widgets'], (old: Widget[]) => [...old, data]);
@@ -26,8 +26,8 @@ export function useWidget() {
const prevWidgets = queryClient.getQueryData(['widgets']); const prevWidgets = queryClient.getQueryData(['widgets']);
// create new widget // create new widget
await db.removeWidget(currentId); await ark.removeWidget(currentId);
const newWidget = await db.createWidget(widget.kind, widget.title, widget.content); const newWidget = await ark.createWidget(widget.kind, widget.title, widget.content);
// Optimistically update to the new value // Optimistically update to the new value
queryClient.setQueryData(['widgets'], (prev: Widget[]) => [ queryClient.setQueryData(['widgets'], (prev: Widget[]) => [
@@ -57,7 +57,7 @@ export function useWidget() {
); );
// Update in database // Update in database
await db.removeWidget(id); await ark.removeWidget(id);
// Return a context object with the snapshotted value // Return a context object with the snapshotted value
return { prevWidgets }; return { prevWidgets };

View File

@@ -6,7 +6,7 @@ export function shortenKey(pubkey: string) {
} }
export function displayNpub(pubkey: string, len: number, separator?: string) { export function displayNpub(pubkey: string, len: number, separator?: string) {
const npub = nip19.npubEncode(pubkey) as string; const npub = pubkey.startsWith('npub1') ? pubkey : (nip19.npubEncode(pubkey) as string);
if (npub.length <= len) return npub; if (npub.length <= len) return npub;
separator = separator || ' ... '; separator = separator || ' ... ';

Some files were not shown because too many files have changed in this diff Show More