31
package.json
31
package.json
@@ -33,8 +33,8 @@
|
||||
"@radix-ui/react-switch": "^1.0.3",
|
||||
"@radix-ui/react-toolbar": "^1.0.4",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@tanstack/react-query": "^5.12.2",
|
||||
"@tanstack/react-query-devtools": "^5.12.2",
|
||||
"@tanstack/react-query": "^5.13.4",
|
||||
"@tanstack/react-query-devtools": "^5.13.4",
|
||||
"@tauri-apps/api": "2.0.0-alpha.11",
|
||||
"@tauri-apps/cli": "2.0.0-alpha.17",
|
||||
"@tauri-apps/plugin-autostart": "2.0.0-alpha.3",
|
||||
@@ -60,9 +60,9 @@
|
||||
"@tiptap/react": "^2.1.13",
|
||||
"@tiptap/starter-kit": "^2.1.13",
|
||||
"@tiptap/suggestion": "^2.1.13",
|
||||
"@vidstack/react": "^1.8.3",
|
||||
"@vidstack/react": "^1.9.2",
|
||||
"dayjs": "^1.11.10",
|
||||
"framer-motion": "^10.16.12",
|
||||
"framer-motion": "^10.16.16",
|
||||
"html-to-text": "^9.0.5",
|
||||
"light-bolt11-decoder": "^3.0.0",
|
||||
"lru-cache": "^10.1.0",
|
||||
@@ -76,16 +76,15 @@
|
||||
"react": "^18.2.0",
|
||||
"react-currency-input-field": "^3.6.12",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.48.2",
|
||||
"react-hook-form": "^7.49.0",
|
||||
"react-hotkeys-hook": "^4.4.1",
|
||||
"react-router-dom": "^6.20.1",
|
||||
"react-string-replace": "^1.1.1",
|
||||
"reactflow": "^11.10.1",
|
||||
"sonner": "^1.2.4",
|
||||
"tauri-controls": "github:reyamir/tauri-controls",
|
||||
"tippy.js": "^6.3.7",
|
||||
"tiptap-markdown": "^0.8.8",
|
||||
"virtua": "^0.17.4",
|
||||
"virtua": "^0.17.5",
|
||||
"zustand": "^4.4.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -93,11 +92,11 @@
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||
"@types/html-to-text": "^9.0.4",
|
||||
"@types/node": "^20.10.3",
|
||||
"@types/react": "^18.2.41",
|
||||
"@types/node": "^20.10.4",
|
||||
"@types/react": "^18.2.43",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@typescript-eslint/eslint-plugin": "^6.13.1",
|
||||
"@typescript-eslint/parser": "^6.13.1",
|
||||
"@typescript-eslint/eslint-plugin": "^6.13.2",
|
||||
"@typescript-eslint/parser": "^6.13.2",
|
||||
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"clsx": "^2.0.0",
|
||||
@@ -112,14 +111,14 @@
|
||||
"lint-staged": "^15.2.0",
|
||||
"postcss": "^8.4.32",
|
||||
"prettier": "^3.1.0",
|
||||
"prettier-plugin-tailwindcss": "^0.5.7",
|
||||
"prettier-plugin-tailwindcss": "^0.5.9",
|
||||
"prop-types": "^15.8.1",
|
||||
"tailwind-merge": "^1.14.0",
|
||||
"tailwind-scrollbar": "^3.0.5",
|
||||
"tailwindcss": "^3.3.5",
|
||||
"typescript": "^5.3.2",
|
||||
"vite": "^4.5.0",
|
||||
"tailwindcss": "^3.3.6",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^4.5.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
1215
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
294
src-tauri/Cargo.lock
generated
294
src-tauri/Cargo.lock
generated
@@ -100,9 +100,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.4"
|
||||
version = "0.6.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44"
|
||||
checksum = "d664a92ecae85fd0a7392615844904654d1d5f5514837f471ddef4a057aba1b6"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
@@ -120,30 +120,30 @@ checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.2"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140"
|
||||
checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.0.0"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
|
||||
checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648"
|
||||
dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.1"
|
||||
version = "3.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628"
|
||||
checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -200,11 +200,11 @@ version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17ae5ebefcc48e7452b4987947920dac9450be1110cadf34d1b8c116bdbaf97c"
|
||||
dependencies = [
|
||||
"async-lock 3.1.2",
|
||||
"async-lock 3.2.0",
|
||||
"async-task",
|
||||
"concurrent-queue",
|
||||
"fastrand 2.0.1",
|
||||
"futures-lite 2.0.1",
|
||||
"futures-lite 2.1.0",
|
||||
"slab",
|
||||
]
|
||||
|
||||
@@ -246,14 +246,14 @@ version = "2.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6d3b15875ba253d1110c740755e246537483f152fa334f91abd7fe84c88b3ff"
|
||||
dependencies = [
|
||||
"async-lock 3.1.2",
|
||||
"async-lock 3.2.0",
|
||||
"cfg-if",
|
||||
"concurrent-queue",
|
||||
"futures-io",
|
||||
"futures-lite 2.0.1",
|
||||
"futures-lite 2.1.0",
|
||||
"parking",
|
||||
"polling 3.3.1",
|
||||
"rustix 0.38.26",
|
||||
"rustix 0.38.28",
|
||||
"slab",
|
||||
"tracing",
|
||||
"windows-sys 0.52.0",
|
||||
@@ -270,9 +270,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-lock"
|
||||
version = "3.1.2"
|
||||
version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dea8b3453dd7cc96711834b75400d671b73e3656975fa68d9f277163b7f7e316"
|
||||
checksum = "7125e42787d53db9dd54261812ef17e937c95a51e4d291373b670342fa44310c"
|
||||
dependencies = [
|
||||
"event-listener 4.0.0",
|
||||
"event-listener-strategy",
|
||||
@@ -292,7 +292,7 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"event-listener 3.1.0",
|
||||
"futures-lite 1.13.0",
|
||||
"rustix 0.38.26",
|
||||
"rustix 0.38.28",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
@@ -319,7 +319,7 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"rustix 0.38.26",
|
||||
"rustix 0.38.28",
|
||||
"signal-hook-registry",
|
||||
"slab",
|
||||
"windows-sys 0.48.0",
|
||||
@@ -349,7 +349,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4af014b17dd80e8af9fa689b2d4a211ddba6eb583c1622f35d0cb543f6b17e4"
|
||||
dependencies = [
|
||||
"atk-sys",
|
||||
"glib 0.18.3",
|
||||
"glib 0.18.4",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@@ -510,11 +510,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-lock 3.1.2",
|
||||
"async-lock 3.2.0",
|
||||
"async-task",
|
||||
"fastrand 2.0.1",
|
||||
"futures-io",
|
||||
"futures-lite 2.0.1",
|
||||
"futures-lite 2.1.0",
|
||||
"piper",
|
||||
"tracing",
|
||||
]
|
||||
@@ -596,7 +596,7 @@ checksum = "f33613627f0dea6a731b0605101fad59ba4f193a52c96c4687728d822605a8a1"
|
||||
dependencies = [
|
||||
"bitflags 2.4.1",
|
||||
"cairo-sys-rs",
|
||||
"glib 0.18.3",
|
||||
"glib 0.18.4",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"thiserror",
|
||||
@@ -651,7 +651,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d1ece59890e746567b467253aea0adbe8a21784d0b025d8a306f66c391c2957"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"toml 0.8.8",
|
||||
"toml 0.8.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -731,9 +731,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.4.10"
|
||||
version = "4.4.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41fffed7514f420abec6d183b1d3acfd9099c79c3a10a06ade4f8203f1411272"
|
||||
checksum = "bfaff671f6b22ca62406885ece523383b9b64022e341e53e009a62ebc47a45f2"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -741,9 +741,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.4.9"
|
||||
version = "4.4.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63361bae7eef3771745f02d8d892bec2fee5f6e34af316ba556e7f97a7069ff1"
|
||||
checksum = "a216b506622bb1d316cd51328dce24e07bdff4a6128a47c7e7fad11878d5adbb"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -850,9 +850,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "2.3.0"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f057a694a54f12365049b0958a1685bb52d567f5593b355fbf685838e873d400"
|
||||
checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
@@ -1067,9 +1067,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
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"
|
||||
checksum = "b4a0d18d88360e374b16b2273c832b5e57258ffc1d4aa4f96b108e0738d5752f"
|
||||
checksum = "3c0333d8849afe78a4c8102a429a446bfdd055832af071945520e835ae2d841e"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
@@ -1134,9 +1134,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.3.9"
|
||||
version = "0.3.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3"
|
||||
checksum = "8eb30d70a07a3b04884d2677f06bec33509dc67ca60d92949e5535352d3191dc"
|
||||
dependencies = [
|
||||
"powerfmt",
|
||||
"serde",
|
||||
@@ -1269,7 +1269,7 @@ checksum = "f54cc3e827ee1c3812239a9a41dede7b4d7d5d5464faa32d71bd7cba28ce2cb2"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"rustc_version",
|
||||
"toml 0.8.8",
|
||||
"toml 0.8.2",
|
||||
"vswhom",
|
||||
"winreg 0.51.0",
|
||||
]
|
||||
@@ -1419,7 +1419,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef033ed5e9bad94e55838ca0ca906db0e043f517adda0c8b79c7a8c66c93c1b5"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"rustix 0.38.26",
|
||||
"rustix 0.38.28",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
@@ -1444,14 +1444,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.22"
|
||||
version = "0.2.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4029edd3e734da6fe05b6cd7bd2960760a616bd2ddd0d59a0124746d6272af0"
|
||||
checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall 0.3.5",
|
||||
"windows-sys 0.48.0",
|
||||
"redox_syscall",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1624,14 +1624,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-lite"
|
||||
version = "2.0.1"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3831c2651acb5177cbd83943f3d9c8912c5ad03c76afcc0e9511ba568ec5ebb"
|
||||
checksum = "aeee267a1883f7ebef3700f262d2d54de95dfaf38189015a74fdc4e0c7ad8143"
|
||||
dependencies = [
|
||||
"fastrand 2.0.1",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"memchr",
|
||||
"parking",
|
||||
"pin-project-lite",
|
||||
]
|
||||
@@ -1696,7 +1695,7 @@ dependencies = [
|
||||
"gdk-pixbuf",
|
||||
"gdk-sys",
|
||||
"gio",
|
||||
"glib 0.18.3",
|
||||
"glib 0.18.4",
|
||||
"libc",
|
||||
"pango",
|
||||
]
|
||||
@@ -1709,7 +1708,7 @@ checksum = "446f32b74d22c33b7b258d4af4ffde53c2bf96ca2e29abdf1a785fe59bd6c82c"
|
||||
dependencies = [
|
||||
"gdk-pixbuf-sys",
|
||||
"gio",
|
||||
"glib 0.18.3",
|
||||
"glib 0.18.4",
|
||||
"libc",
|
||||
"once_cell",
|
||||
]
|
||||
@@ -1844,16 +1843,16 @@ checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
|
||||
|
||||
[[package]]
|
||||
name = "gio"
|
||||
version = "0.18.3"
|
||||
version = "0.18.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47d809baf02bdf1b5ef4ad3bf60dd9d4977149db4612b7bbb58e56aef168193b"
|
||||
checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-util",
|
||||
"gio-sys 0.18.1",
|
||||
"glib 0.18.3",
|
||||
"glib 0.18.4",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"pin-project-lite",
|
||||
@@ -1911,9 +1910,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "glib"
|
||||
version = "0.18.3"
|
||||
version = "0.18.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "58cf801b6f7829fa76db37449ab67c9c98a2b1bf21076d9113225621e61a0fa6"
|
||||
checksum = "951bbd7fdc5c044ede9f05170f05a3ae9479239c3afdfe2d22d537a3add15c4e"
|
||||
dependencies = [
|
||||
"bitflags 2.4.1",
|
||||
"futures-channel",
|
||||
@@ -1954,7 +1953,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72793962ceece3863c2965d7f10c8786323b17c7adea75a515809fa20ab799a5"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro-crate 2.0.0",
|
||||
"proc-macro-crate 2.0.1",
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2022,7 +2021,7 @@ dependencies = [
|
||||
"gdk",
|
||||
"gdk-pixbuf",
|
||||
"gio",
|
||||
"glib 0.18.3",
|
||||
"glib 0.18.4",
|
||||
"gtk-sys",
|
||||
"gtk3-macros",
|
||||
"libc",
|
||||
@@ -2175,14 +2174,14 @@ checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
"itoa 1.0.9",
|
||||
"itoa 1.0.10",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body"
|
||||
version = "0.4.5"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
|
||||
checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http",
|
||||
@@ -2216,7 +2215,7 @@ dependencies = [
|
||||
"http-body",
|
||||
"httparse",
|
||||
"httpdate",
|
||||
"itoa 1.0.9",
|
||||
"itoa 1.0.10",
|
||||
"pin-project-lite",
|
||||
"socket2 0.4.10",
|
||||
"tokio",
|
||||
@@ -2389,9 +2388,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.11.0"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
|
||||
checksum = "25db6b064527c5d482d0423354fcd07a89a2dfe07b67892e62411946db7f07b0"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
@@ -2404,9 +2403,9 @@ checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.9"
|
||||
version = "1.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
|
||||
checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
|
||||
|
||||
[[package]]
|
||||
name = "javascriptcore-rs"
|
||||
@@ -2415,7 +2414,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"glib 0.18.3",
|
||||
"glib 0.18.4",
|
||||
"javascriptcore-rs-sys",
|
||||
]
|
||||
|
||||
@@ -2502,9 +2501,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "keyring"
|
||||
version = "2.0.5"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9549a129bd08149e0a71b2d1ce2729780d47127991bfd0a78cc1df697ec72492"
|
||||
checksum = "ec6488afbd1d8202dbd6e2dd38c0753d8c0adba9ac9985fc6f732a0d551f75e1"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"lazy_static",
|
||||
@@ -2542,7 +2541,7 @@ version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a"
|
||||
dependencies = [
|
||||
"glib 0.18.3",
|
||||
"glib 0.18.4",
|
||||
"gtk",
|
||||
"gtk-sys",
|
||||
"libappindicator-sys",
|
||||
@@ -2590,7 +2589,7 @@ checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8"
|
||||
dependencies = [
|
||||
"bitflags 2.4.1",
|
||||
"libc",
|
||||
"redox_syscall 0.4.1",
|
||||
"redox_syscall",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2849,9 +2848,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.8.9"
|
||||
version = "0.8.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0"
|
||||
checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
@@ -3188,9 +3187,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.18.0"
|
||||
version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
|
||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||
|
||||
[[package]]
|
||||
name = "opaque-debug"
|
||||
@@ -3211,9 +3210,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.60"
|
||||
version = "0.10.61"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79a4c6c3a2b158f7f8f2a2fc5a969fa3a068df6fc9dbb4a43845436e3af7c800"
|
||||
checksum = "6b8419dc8cc6d866deb801274bba2e6f8f6108c1bb7fcc10ee5ab864931dbb45"
|
||||
dependencies = [
|
||||
"bitflags 2.4.1",
|
||||
"cfg-if",
|
||||
@@ -3243,18 +3242,18 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
||||
|
||||
[[package]]
|
||||
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"
|
||||
checksum = "439fac53e092cd7442a3660c85dde4643ab3b5bd39040912388dcdabf6b88085"
|
||||
checksum = "b1ebed1d188c4cd64c2bcd73d6c1fe1092f3d98c111831923cc1b706c3859fca"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.96"
|
||||
version = "0.9.97"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3812c071ba60da8b5677cc12bcb1d42989a65553772897a7e0355545a819838f"
|
||||
checksum = "c3eaad34cdd97d81de97964fc7f29e2d104f483840d906ef56daa1912338460b"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
@@ -3307,7 +3306,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4"
|
||||
dependencies = [
|
||||
"gio",
|
||||
"glib 0.18.3",
|
||||
"glib 0.18.4",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"pango-sys",
|
||||
@@ -3349,7 +3348,7 @@ checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall 0.4.1",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
@@ -3640,7 +3639,7 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"concurrent-queue",
|
||||
"pin-project-lite",
|
||||
"rustix 0.38.26",
|
||||
"rustix 0.38.28",
|
||||
"tracing",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
@@ -3675,11 +3674,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "2.0.0"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8"
|
||||
checksum = "97dc5fea232fc28d2f597b37c4876b348a40e33f3b02cc975c8d006d78d94b1a"
|
||||
dependencies = [
|
||||
"toml_edit 0.20.7",
|
||||
"toml_datetime",
|
||||
"toml_edit 0.20.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3865,15 +3865,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "redox_syscall"
|
||||
version = "0.4.1"
|
||||
@@ -3980,9 +3971,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rfd"
|
||||
version = "0.12.1"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c9e7b57df6e8472152674607f6cc68aa14a748a3157a857a94f516e11aeacc2"
|
||||
checksum = "241a0deb168c88050d872294f7b3106c1dfa8740942bcc97bc91b98e97b5c501"
|
||||
dependencies = [
|
||||
"block",
|
||||
"dispatch",
|
||||
@@ -4052,9 +4043,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.26"
|
||||
version = "0.38.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9470c4bf8246c8daf25f9598dca807fb6510347b1e1cfa55749113850c79d88a"
|
||||
checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316"
|
||||
dependencies = [
|
||||
"bitflags 2.4.1",
|
||||
"errno",
|
||||
@@ -4095,9 +4086,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.15"
|
||||
version = "1.0.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
|
||||
checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c"
|
||||
|
||||
[[package]]
|
||||
name = "safemem"
|
||||
@@ -4232,7 +4223,7 @@ version = "1.0.108"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b"
|
||||
dependencies = [
|
||||
"itoa 1.0.9",
|
||||
"itoa 1.0.10",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
@@ -4264,7 +4255,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"itoa 1.0.9",
|
||||
"itoa 1.0.10",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
@@ -4445,7 +4436,7 @@ checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"gio",
|
||||
"glib 0.18.3",
|
||||
"glib 0.18.4",
|
||||
"libc",
|
||||
"soup3-sys",
|
||||
]
|
||||
@@ -4490,9 +4481,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlformat"
|
||||
version = "0.2.2"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b7b278788e7be4d0d29c0f39497a0eef3fba6bbc8e70d8bf7fde46edeaa9e85"
|
||||
checksum = "ce81b7bd7c4493975347ef60d8c7e8b742d4694f4c49f93e0a12ea263938176c"
|
||||
dependencies = [
|
||||
"itertools",
|
||||
"nom",
|
||||
@@ -4641,7 +4632,7 @@ dependencies = [
|
||||
"hex",
|
||||
"hkdf",
|
||||
"hmac",
|
||||
"itoa 1.0.9",
|
||||
"itoa 1.0.10",
|
||||
"log",
|
||||
"md-5",
|
||||
"memchr",
|
||||
@@ -4682,7 +4673,7 @@ dependencies = [
|
||||
"hkdf",
|
||||
"hmac",
|
||||
"home",
|
||||
"itoa 1.0.9",
|
||||
"itoa 1.0.10",
|
||||
"log",
|
||||
"md-5",
|
||||
"memchr",
|
||||
@@ -4873,7 +4864,7 @@ dependencies = [
|
||||
"cfg-expr",
|
||||
"heck",
|
||||
"pkg-config",
|
||||
"toml 0.8.8",
|
||||
"toml 0.8.2",
|
||||
"version-compare",
|
||||
]
|
||||
|
||||
@@ -4897,7 +4888,7 @@ dependencies = [
|
||||
"gdkwayland-sys",
|
||||
"gdkx11-sys",
|
||||
"gio",
|
||||
"glib 0.18.3",
|
||||
"glib 0.18.4",
|
||||
"glib-sys 0.18.1",
|
||||
"gtk",
|
||||
"image",
|
||||
@@ -5066,7 +5057,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tauri-plugin-autostart"
|
||||
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 = [
|
||||
"auto-launch",
|
||||
"log",
|
||||
@@ -5079,7 +5070,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tauri-plugin-cli"
|
||||
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 = [
|
||||
"clap",
|
||||
"log",
|
||||
@@ -5092,7 +5083,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tauri-plugin-clipboard-manager"
|
||||
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 = [
|
||||
"arboard",
|
||||
"log",
|
||||
@@ -5106,7 +5097,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tauri-plugin-dialog"
|
||||
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 = [
|
||||
"glib 0.16.9",
|
||||
"log",
|
||||
@@ -5123,7 +5114,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tauri-plugin-fs"
|
||||
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 = [
|
||||
"anyhow",
|
||||
"glob",
|
||||
@@ -5136,7 +5127,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tauri-plugin-http"
|
||||
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 = [
|
||||
"data-url",
|
||||
"glob",
|
||||
@@ -5153,7 +5144,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tauri-plugin-notification"
|
||||
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 = [
|
||||
"log",
|
||||
"notify-rust",
|
||||
@@ -5171,7 +5162,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tauri-plugin-os"
|
||||
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 = [
|
||||
"gethostname 0.4.3",
|
||||
"log",
|
||||
@@ -5187,7 +5178,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tauri-plugin-process"
|
||||
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 = [
|
||||
"tauri",
|
||||
]
|
||||
@@ -5195,7 +5186,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tauri-plugin-shell"
|
||||
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 = [
|
||||
"encoding_rs",
|
||||
"log",
|
||||
@@ -5212,7 +5203,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tauri-plugin-sql"
|
||||
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 = [
|
||||
"futures-core",
|
||||
"log",
|
||||
@@ -5228,7 +5219,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tauri-plugin-store"
|
||||
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 = [
|
||||
"log",
|
||||
"serde",
|
||||
@@ -5244,7 +5235,7 @@ source = "git+https://github.com/wyhaya/tauri-plugin-theme#cccc9b3fbc308a475ef87
|
||||
dependencies = [
|
||||
"cocoa 0.25.0",
|
||||
"dirs-next",
|
||||
"futures-lite 2.0.1",
|
||||
"futures-lite 2.1.0",
|
||||
"gtk",
|
||||
"once_cell",
|
||||
"serde",
|
||||
@@ -5256,7 +5247,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tauri-plugin-updater"
|
||||
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 = [
|
||||
"base64",
|
||||
"dirs-next",
|
||||
@@ -5282,7 +5273,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tauri-plugin-upload"
|
||||
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 = [
|
||||
"futures-util",
|
||||
"log",
|
||||
@@ -5299,7 +5290,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tauri-plugin-window-state"
|
||||
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 = [
|
||||
"bincode",
|
||||
"bitflags 2.4.1",
|
||||
@@ -5406,8 +5397,8 @@ checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"fastrand 2.0.1",
|
||||
"redox_syscall 0.4.1",
|
||||
"rustix 0.38.26",
|
||||
"redox_syscall",
|
||||
"rustix 0.38.28",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
@@ -5476,7 +5467,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"itoa 1.0.9",
|
||||
"itoa 1.0.10",
|
||||
"powerfmt",
|
||||
"serde",
|
||||
"time-core",
|
||||
@@ -5525,9 +5516,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.34.0"
|
||||
version = "1.35.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9"
|
||||
checksum = "841d45b238a16291a4e1584e61820b8ae57d696cc5015c459c229ccc6990cc1c"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
@@ -5600,21 +5591,21 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.8"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35"
|
||||
checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_edit 0.21.0",
|
||||
"toml_edit 0.20.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.5"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1"
|
||||
checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@@ -5634,20 +5625,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.20.7"
|
||||
version = "0.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81"
|
||||
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"
|
||||
checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338"
|
||||
dependencies = [
|
||||
"indexmap 2.1.0",
|
||||
"serde",
|
||||
@@ -5755,9 +5735,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "try-lock"
|
||||
version = "0.2.4"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
|
||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
@@ -5777,9 +5757,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.13"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460"
|
||||
checksum = "6f2528f27a9eb2b21e69c95319b30bd0efd85d09c379741b0f78ea1d86be2416"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
@@ -6035,7 +6015,7 @@ dependencies = [
|
||||
"gdk-sys",
|
||||
"gio",
|
||||
"gio-sys 0.18.1",
|
||||
"glib 0.18.3",
|
||||
"glib 0.18.4",
|
||||
"glib-sys 0.18.1",
|
||||
"gobject-sys 0.18.0",
|
||||
"gtk",
|
||||
@@ -6442,9 +6422,9 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.5.19"
|
||||
version = "0.5.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "829846f3e3db426d4cee4510841b71a8e58aa2a76b1132579487ae430ccd9c7b"
|
||||
checksum = "b67b5f0a4e7a27a64c651977932b9dc5667ca7fc31ac44b03ed37a0cf42fdfff"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
@@ -6559,9 +6539,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "xattr"
|
||||
version = "1.0.1"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f4686009f71ff3e5c4dbcf1a282d0a44db3f021ba69350cd42086b3e5f1c6985"
|
||||
checksum = "fbc6ab6ec1907d1a901cdbcd2bd4cb9e7d64ce5c9739cbb97d3c391acd8c7fae"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
@@ -6655,18 +6635,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.28"
|
||||
version = "0.7.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d6f15f7ade05d2a4935e34a457b936c23dc70a05cc1d97133dc99e7a3fe0f0e"
|
||||
checksum = "306dca4455518f1f31635ec308b6b3e4eb1b11758cefafc782827d0aa7acb5c7"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.7.28"
|
||||
version = "0.7.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dbbad221e3f78500350ecbd7dfa4e63ef945c05f4c61cb7f4d3f84cd0bba649b"
|
||||
checksum = "be912bf68235a88fbefd1b73415cb218405958d1655b2ece9035a19920bdf6ba"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
/* @import 'reactflow/dist/style.css'; */
|
||||
|
||||
/* Vidstack */
|
||||
@import '@vidstack/react/player/styles/default/theme.css';
|
||||
@import '@vidstack/react/player/styles/default/layouts/video.css';
|
||||
|
||||
17
src/app.tsx
17
src/app.tsx
@@ -1,13 +1,11 @@
|
||||
import { message } from '@tauri-apps/plugin-dialog';
|
||||
import { fetch } from '@tauri-apps/plugin-http';
|
||||
import { RouterProvider, createBrowserRouter, defer, redirect } from 'react-router-dom';
|
||||
import { ReactFlowProvider } from 'reactflow';
|
||||
|
||||
import { ChatsScreen } from '@app/chats';
|
||||
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 { AppLayout } from '@shared/layouts/app';
|
||||
@@ -19,12 +17,12 @@ import { SettingsLayout } from '@shared/layouts/settings';
|
||||
import './app.css';
|
||||
|
||||
export default function App() {
|
||||
const { db } = useStorage();
|
||||
const { ark } = useArk();
|
||||
|
||||
const accountLoader = async () => {
|
||||
try {
|
||||
// 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');
|
||||
|
||||
return null;
|
||||
@@ -87,15 +85,6 @@ export default function App() {
|
||||
return { Component: RelayScreen };
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'explore',
|
||||
element: (
|
||||
<ReactFlowProvider>
|
||||
<ExploreScreen />
|
||||
</ReactFlowProvider>
|
||||
),
|
||||
errorElement: <ErrorScreen />,
|
||||
},
|
||||
{
|
||||
path: 'chats',
|
||||
element: <ChatsScreen />,
|
||||
|
||||
@@ -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 { writeText } from '@tauri-apps/plugin-clipboard-manager';
|
||||
import { save } from '@tauri-apps/plugin-dialog';
|
||||
@@ -11,8 +11,7 @@ import { useForm } from 'react-hook-form';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { AvatarUploader } from '@shared/avatarUploader';
|
||||
import { ArrowLeftIcon, InfoIcon, LoaderIcon } from '@shared/icons';
|
||||
@@ -29,13 +28,12 @@ export function CreateAccountScreen() {
|
||||
privkey: string;
|
||||
}>(null);
|
||||
|
||||
const { ark } = useArk();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { isDirty, isValid },
|
||||
} = useForm();
|
||||
const { db } = useStorage();
|
||||
const { ndk } = useNDK();
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -62,28 +60,27 @@ export function CreateAccountScreen() {
|
||||
const userNsec = nip19.nsecEncode(userPrivkey);
|
||||
|
||||
const signer = new NDKPrivateKeySigner(userPrivkey);
|
||||
ndk.signer = signer;
|
||||
ark.updateNostrSigner({ signer });
|
||||
|
||||
const event = new NDKEvent(ndk);
|
||||
event.content = JSON.stringify(profile);
|
||||
event.kind = NDKKind.Metadata;
|
||||
event.pubkey = userPubkey;
|
||||
event.tags = [];
|
||||
|
||||
const publish = await event.publish();
|
||||
const publish = await ark.createEvent({
|
||||
content: JSON.stringify(profile),
|
||||
kind: NDKKind.Metadata,
|
||||
tags: [],
|
||||
publish: true,
|
||||
});
|
||||
|
||||
if (publish) {
|
||||
await db.createAccount(userNpub, userPubkey);
|
||||
await db.secureSave(userPubkey, userPrivkey);
|
||||
await ark.createAccount({
|
||||
id: userNpub,
|
||||
pubkey: userPubkey,
|
||||
privkey: userPrivkey,
|
||||
});
|
||||
|
||||
const relayListEvent = new NDKEvent(ndk);
|
||||
relayListEvent.kind = NDKKind.RelayList;
|
||||
relayListEvent.tags = [...ndk.pool.relays.values()].map((item) => [
|
||||
'r',
|
||||
item.url,
|
||||
]);
|
||||
|
||||
await relayListEvent.publish();
|
||||
await ark.createEvent({
|
||||
kind: NDKKind.RelayList,
|
||||
tags: [ark.relays],
|
||||
publish: true,
|
||||
});
|
||||
|
||||
setKeys({
|
||||
npub: userNpub,
|
||||
@@ -93,7 +90,7 @@ export function CreateAccountScreen() {
|
||||
});
|
||||
setLoading(false);
|
||||
} else {
|
||||
toast('Create account failed');
|
||||
toast('Cannot publish user profile, please try again later.');
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
@@ -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() {
|
||||
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 (
|
||||
<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">
|
||||
@@ -17,12 +92,13 @@ export function FinishScreen() {
|
||||
>
|
||||
Start tutorial
|
||||
</Link>
|
||||
<Link
|
||||
to="/"
|
||||
<button
|
||||
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"
|
||||
>
|
||||
Skip
|
||||
</Link>
|
||||
{loading ? <LoaderIcon className="h-4 w-4 animate-spin" /> : 'Skip'}
|
||||
</button>
|
||||
<p className="text-center text-sm font-medium text-neutral-500 dark:text-neutral-600">
|
||||
You need to restart app to make changes in previous step take effect or you
|
||||
can continue with Lume default settings
|
||||
|
||||
@@ -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 { useQuery } from '@tanstack/react-query';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
@@ -7,8 +7,7 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { toast } from 'sonner';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
@@ -37,8 +36,7 @@ const POPULAR_USERS = [
|
||||
const LUME_USERS = ['npub1zfss807aer0j26mwp2la0ume0jqde3823rmu97ra6sgyyg956e0s6xw445'];
|
||||
|
||||
export function FollowScreen() {
|
||||
const { ndk } = useNDK();
|
||||
const { db } = useStorage();
|
||||
const { ark } = useArk();
|
||||
const { status, data } = useQuery({
|
||||
queryKey: ['trending-profiles-widget'],
|
||||
queryFn: async () => {
|
||||
@@ -68,20 +66,22 @@ export function FollowScreen() {
|
||||
setLoading(true);
|
||||
if (!follows.length) return navigate('/auth/finish');
|
||||
|
||||
const event = new NDKEvent(ndk);
|
||||
event.kind = NDKKind.Contacts;
|
||||
event.tags = follows.map((item) => {
|
||||
if (item.startsWith('npub')) return ['p', nip19.decode(item).data as string];
|
||||
return ['p', item];
|
||||
const publish = await ark.createEvent({
|
||||
kind: NDKKind.Contacts,
|
||||
tags: follows.map((item) => {
|
||||
if (item.startsWith('npub1')) return ['p', nip19.decode(item).data as string];
|
||||
return ['p', item];
|
||||
}),
|
||||
publish: true,
|
||||
});
|
||||
|
||||
const publish = await event.publish();
|
||||
if (publish) {
|
||||
db.account.contacts = follows.map((item) => {
|
||||
if (item.startsWith('npub')) return nip19.decode(item).data as string;
|
||||
ark.account.contacts = follows.map((item) => {
|
||||
if (item.startsWith('npub1')) return nip19.decode(item).data as string;
|
||||
return item;
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
return navigate('/auth/finish');
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
@@ -8,16 +8,12 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { toast } from 'sonner';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { ArrowLeftIcon, LoaderIcon } from '@shared/icons';
|
||||
import { User } from '@shared/user';
|
||||
|
||||
export function ImportAccountScreen() {
|
||||
const { db } = useStorage();
|
||||
const { ndk } = useNDK();
|
||||
|
||||
const [npub, setNpub] = useState<string>('');
|
||||
const [nsec, setNsec] = useState<string>('');
|
||||
const [pubkey, setPubkey] = useState<undefined | string>(undefined);
|
||||
@@ -25,6 +21,7 @@ export function ImportAccountScreen() {
|
||||
const [created, setCreated] = useState({ ok: false, remote: false });
|
||||
const [savedPrivkey, setSavedPrivkey] = useState(false);
|
||||
|
||||
const { ark } = useArk();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const submitNpub = async () => {
|
||||
@@ -47,8 +44,8 @@ export function ImportAccountScreen() {
|
||||
const pubkey = nip19.decode(npub.split('#')[0]).data as string;
|
||||
const localSigner = NDKPrivateKeySigner.generate();
|
||||
|
||||
await db.createSetting('nsecbunker', '1');
|
||||
await db.secureSave(`${npub}-nsecbunker`, localSigner.privateKey);
|
||||
await ark.createSetting('nsecbunker', '1');
|
||||
await ark.createPrivkey(`${npub}-nsecbunker`, localSigner.privateKey);
|
||||
|
||||
// open nsecbunker web app in default browser
|
||||
await open('https://app.nsecbunker.com/keys');
|
||||
@@ -60,8 +57,7 @@ export function ImportAccountScreen() {
|
||||
|
||||
const remoteSigner = new NDKNip46Signer(bunker, npub, localSigner);
|
||||
await remoteSigner.blockUntilReady();
|
||||
|
||||
ndk.signer = remoteSigner;
|
||||
ark.updateNostrSigner({ signer: remoteSigner });
|
||||
|
||||
setPubkey(pubkey);
|
||||
setCreated({ ok: false, remote: true });
|
||||
@@ -80,14 +76,10 @@ export function ImportAccountScreen() {
|
||||
setLoading(true);
|
||||
|
||||
// add account to db
|
||||
await db.createAccount(npub, pubkey);
|
||||
await ark.createAccount({ id: npub, pubkey });
|
||||
|
||||
// get account metadata
|
||||
const user = ndk.getUser({ pubkey });
|
||||
if (user) {
|
||||
db.account.contacts = [...(await user.follows())].map((user) => user.pubkey);
|
||||
db.account.relayList = await user.relayList();
|
||||
}
|
||||
// get account contacts
|
||||
await ark.getUserContacts({ pubkey });
|
||||
|
||||
setCreated((prev) => ({ ...prev, ok: true }));
|
||||
setLoading(false);
|
||||
@@ -109,9 +101,8 @@ export function ImportAccountScreen() {
|
||||
if (nsec.length > 50 && nsec.startsWith('nsec1')) {
|
||||
try {
|
||||
const privkey = nip19.decode(nsec).data as string;
|
||||
await db.secureSave(pubkey, privkey);
|
||||
|
||||
ndk.signer = new NDKPrivateKeySigner(privkey);
|
||||
await ark.createPrivkey(pubkey, privkey);
|
||||
ark.updateNostrSigner({ signer: new NDKPrivateKeySigner(privkey) });
|
||||
|
||||
setSavedPrivkey(true);
|
||||
} catch (e) {
|
||||
@@ -290,9 +281,9 @@ export function ImportAccountScreen() {
|
||||
<p className="text-sm">
|
||||
Lume will put your private key to{' '}
|
||||
<b>
|
||||
{db.platform === 'macos'
|
||||
{ark.platform === 'macos'
|
||||
? 'Apple Keychain (macOS)'
|
||||
: db.platform === 'windows'
|
||||
: ark.platform === 'windows'
|
||||
? 'Credential Manager (Windows)'
|
||||
: 'Secret Service (Linux)'}
|
||||
</b>
|
||||
|
||||
@@ -3,12 +3,12 @@ import { isPermissionGranted, requestPermission } from '@tauri-apps/plugin-notif
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { InfoIcon } from '@shared/icons';
|
||||
|
||||
export function OnboardingScreen() {
|
||||
const { db } = useStorage();
|
||||
const { ark } = useArk();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [settings, setSettings] = useState({
|
||||
@@ -18,19 +18,19 @@ export function OnboardingScreen() {
|
||||
});
|
||||
|
||||
const next = () => {
|
||||
if (!db.account.contacts.length) return navigate('/auth/follow');
|
||||
if (!ark.account.contacts.length) return navigate('/auth/follow');
|
||||
return navigate('/auth/finish');
|
||||
};
|
||||
|
||||
const toggleOutbox = async () => {
|
||||
await db.createSetting('outbox', String(+!settings.outbox));
|
||||
await ark.createSetting('outbox', String(+!settings.outbox));
|
||||
// update state
|
||||
setSettings((prev) => ({ ...prev, outbox: !settings.outbox }));
|
||||
};
|
||||
|
||||
const toggleAutoupdate = async () => {
|
||||
await db.createSetting('autoupdate', String(+!settings.autoupdate));
|
||||
db.settings.autoupdate = !settings.autoupdate;
|
||||
await ark.createSetting('autoupdate', String(+!settings.autoupdate));
|
||||
ark.settings.autoupdate = !settings.autoupdate;
|
||||
// update state
|
||||
setSettings((prev) => ({ ...prev, autoupdate: !settings.autoupdate }));
|
||||
};
|
||||
@@ -46,7 +46,7 @@ export function OnboardingScreen() {
|
||||
const permissionGranted = await isPermissionGranted();
|
||||
setSettings((prev) => ({ ...prev, notification: permissionGranted }));
|
||||
|
||||
const data = await db.getAllSettings();
|
||||
const data = await ark.getAllSettings();
|
||||
if (!data) return;
|
||||
|
||||
data.forEach((item) => {
|
||||
|
||||
@@ -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() {
|
||||
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 (
|
||||
<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">
|
||||
@@ -11,12 +86,17 @@ export function TutorialFinishScreen() {
|
||||
</h1>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Link
|
||||
to="/"
|
||||
<button
|
||||
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"
|
||||
>
|
||||
Start using
|
||||
</Link>
|
||||
{loading ? (
|
||||
<LoaderIcon className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
'Start using Lume'
|
||||
)}
|
||||
</button>
|
||||
<Link
|
||||
to="https://nostr.how/"
|
||||
target="_blank"
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
||||
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 { TextNote } from '@shared/notes';
|
||||
|
||||
export function TutorialNoteScreen() {
|
||||
const { ndk } = useNDK();
|
||||
const exampleEvent = new NDKEvent(ndk, {
|
||||
id: 'a3527670dd9b178bf7c2a9ea673b63bc8bfe774942b196691145343623c45821',
|
||||
pubkey: '04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9',
|
||||
created_at: 1701355223,
|
||||
kind: 1,
|
||||
tags: [],
|
||||
content: 'good morning nostr, stay humble and stack sats 🫡',
|
||||
sig: '9e0bd67ec25598744f20bff0fe360fdf190c4240edb9eea260e50f77e07f94ea767ececcc6270819b7f64e5e7ca1fe20b4971f46dc120e6db43114557f3a6dae',
|
||||
const { ark } = useArk();
|
||||
|
||||
const exampleEvent = ark.createNDKEvent({
|
||||
event: {
|
||||
id: 'a3527670dd9b178bf7c2a9ea673b63bc8bfe774942b196691145343623c45821',
|
||||
pubkey: '04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9',
|
||||
created_at: 1701355223,
|
||||
kind: 1,
|
||||
tags: [],
|
||||
content: 'good morning nostr, stay humble and stack sats 🫡',
|
||||
sig: '9e0bd67ec25598744f20bff0fe360fdf190c4240edb9eea260e50f77e07f94ea767ececcc6270819b7f64e5e7ca1fe20b4971f46dc120e6db43114557f3a6dae',
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -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 { useCallback, useEffect, useRef } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
@@ -7,23 +7,18 @@ import { VList, VListHandle } from 'virtua';
|
||||
import { ChatForm } from '@app/chats/components/chatForm';
|
||||
import { ChatMessage } from '@app/chats/components/message';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { LoaderIcon } from '@shared/icons';
|
||||
import { User } from '@shared/user';
|
||||
|
||||
import { useNostr } from '@utils/hooks/useNostr';
|
||||
|
||||
export function ChatScreen() {
|
||||
const { db } = useStorage();
|
||||
const { ndk } = useNDK();
|
||||
const { ark } = useArk();
|
||||
const { pubkey } = useParams();
|
||||
const { fetchNIP04Messages } = useNostr();
|
||||
const { status, data } = useQuery({
|
||||
queryKey: ['nip04-dm', pubkey],
|
||||
queryFn: async () => {
|
||||
return await fetchNIP04Messages(pubkey);
|
||||
return await ark.getAllMessagesByPubkey({ pubkey });
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
@@ -59,7 +54,7 @@ export function ChatScreen() {
|
||||
<ChatMessage
|
||||
key={message.id}
|
||||
message={message}
|
||||
isSelf={message.pubkey === db.account.pubkey}
|
||||
isSelf={message.pubkey === ark.account.pubkey}
|
||||
/>
|
||||
);
|
||||
},
|
||||
@@ -71,20 +66,15 @@ export function ChatScreen() {
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
const sub: NDKSubscription = ndk.subscribe(
|
||||
{
|
||||
const sub = ark.subscribe({
|
||||
filter: {
|
||||
kinds: [4],
|
||||
authors: [db.account.pubkey],
|
||||
authors: [ark.account.pubkey],
|
||||
'#p': [pubkey],
|
||||
since: Math.floor(Date.now() / 1000),
|
||||
},
|
||||
{
|
||||
closeOnEose: false,
|
||||
}
|
||||
);
|
||||
|
||||
sub.addListener('event', (event) => {
|
||||
newMessage.mutate(event);
|
||||
closeOnEose: false,
|
||||
cb: (event) => newMessage.mutate(event),
|
||||
});
|
||||
|
||||
return () => {
|
||||
|
||||
@@ -1,29 +1,19 @@
|
||||
import { NDKEvent, NDKKind, NDKUser } from '@nostr-dev-kit/ndk';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { MediaUploader } from '@app/chats/components/mediaUploader';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { EnterIcon } from '@shared/icons';
|
||||
|
||||
export function ChatForm({ receiverPubkey }: { receiverPubkey: string }) {
|
||||
const { ndk } = useNDK();
|
||||
const { ark } = useArk();
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
const submit = async () => {
|
||||
try {
|
||||
const recipient = new NDKUser({ 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();
|
||||
|
||||
const publish = await ark.nip04Encrypt({ content: value, pubkey: receiverPubkey });
|
||||
if (publish) setValue('');
|
||||
} catch (e) {
|
||||
toast.error(e);
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
import * as Tooltip from '@radix-ui/react-tooltip';
|
||||
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({
|
||||
setState,
|
||||
}: {
|
||||
setState: Dispatch<SetStateAction<string>>;
|
||||
}) {
|
||||
const { upload } = useNostr();
|
||||
const { ark } = useArk();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const uploadMedia = async () => {
|
||||
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) {
|
||||
setState((prev: string) => `${prev}\n${image}`);
|
||||
|
||||
@@ -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 { useNDK } from '@libs/ndk/provider';
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
export function useDecryptMessage(message: NDKEvent) {
|
||||
const { db } = useStorage();
|
||||
const { ndk } = useNDK();
|
||||
|
||||
const [content, setContent] = useState(message.content);
|
||||
export function useDecryptMessage(event: NDKEvent) {
|
||||
const { ark } = useArk();
|
||||
const [content, setContent] = useState(event.content);
|
||||
|
||||
useEffect(() => {
|
||||
async function decryptContent() {
|
||||
try {
|
||||
const sender = new NDKUser({
|
||||
pubkey:
|
||||
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);
|
||||
const message = await ark.nip04Decrypt({ event });
|
||||
setContent(message);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
@@ -1,25 +1,20 @@
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { Outlet, useNavigate } from 'react-router-dom';
|
||||
import { useCallback } from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
import { ChatListItem } from '@app/chats/components/chatListItem';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { LoaderIcon } from '@shared/icons';
|
||||
|
||||
import { useNostr } from '@utils/hooks/useNostr';
|
||||
|
||||
export function ChatsScreen() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { ndk } = useNDK();
|
||||
const { getAllNIP04Chats } = useNostr();
|
||||
const { ark } = useArk();
|
||||
const { status, data } = useQuery({
|
||||
queryKey: ['nip04-chats'],
|
||||
queryFn: async () => {
|
||||
return await getAllNIP04Chats();
|
||||
return await ark.getAllChats();
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
@@ -34,10 +29,6 @@ export function ChatsScreen() {
|
||||
[data]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ndk.signer) navigate('/new/privkey');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<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">
|
||||
|
||||
@@ -4,7 +4,7 @@ import { writeTextFile } from '@tauri-apps/plugin-fs';
|
||||
import { relaunch } from '@tauri-apps/plugin-process';
|
||||
import { useRouteError } from 'react-router-dom';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
interface RouteError {
|
||||
statusText: string;
|
||||
@@ -12,7 +12,7 @@ interface RouteError {
|
||||
}
|
||||
|
||||
export function ErrorScreen() {
|
||||
const { db } = useStorage();
|
||||
const { ark } = useArk();
|
||||
const error = useRouteError() as RouteError;
|
||||
|
||||
const restart = async () => {
|
||||
@@ -26,18 +26,18 @@ export function ErrorScreen() {
|
||||
const filePath = await save({
|
||||
defaultPath: downloadPath + '/' + fileName,
|
||||
});
|
||||
const nsec = await db.secureLoad(db.account.pubkey);
|
||||
const nsec = await ark.loadPrivkey(ark.account.pubkey);
|
||||
|
||||
if (filePath) {
|
||||
if (nsec) {
|
||||
await writeTextFile(
|
||||
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 {
|
||||
await writeTextFile(
|
||||
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 }
|
||||
|
||||
@@ -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' }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { useQuery } from '@tanstack/react-query';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { VList, VListHandle } from 'virtua';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { LoaderIcon } from '@shared/icons';
|
||||
import {
|
||||
@@ -28,11 +28,11 @@ export function HomeScreen() {
|
||||
const ref = useRef<VListHandle>(null);
|
||||
const [selectedIndex, setSelectedIndex] = useState(-1);
|
||||
|
||||
const { db } = useStorage();
|
||||
const { ark } = useArk();
|
||||
const { status, data } = useQuery({
|
||||
queryKey: ['widgets'],
|
||||
queryFn: async () => {
|
||||
const dbWidgets = await db.getWidgets();
|
||||
const dbWidgets = await ark.getWidgets();
|
||||
const defaultWidgets = [
|
||||
{
|
||||
id: '9999',
|
||||
|
||||
@@ -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 Image from '@tiptap/extension-image';
|
||||
import Placeholder from '@tiptap/extension-placeholder';
|
||||
@@ -12,7 +12,7 @@ import { Markdown } from 'tiptap-markdown';
|
||||
|
||||
import { ArticleCoverUploader, MediaUploader, MentionPopup } from '@app/new/components';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import {
|
||||
BoldIcon,
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
} from '@shared/icons';
|
||||
|
||||
export function NewArticleScreen() {
|
||||
const { ndk } = useNDK();
|
||||
const { ark } = useArk();
|
||||
|
||||
const [height, setHeight] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -69,7 +69,7 @@ export function NewArticleScreen() {
|
||||
|
||||
const submit = async () => {
|
||||
try {
|
||||
if (!ndk.signer) return navigate('/new/privkey');
|
||||
if (!ark.readyToSign) return navigate('/new/privkey');
|
||||
|
||||
setLoading(true);
|
||||
|
||||
@@ -91,16 +91,16 @@ export function NewArticleScreen() {
|
||||
tags.push(['t', tag.replace('#', '')]);
|
||||
});
|
||||
|
||||
const event = new NDKEvent(ndk);
|
||||
event.content = content;
|
||||
event.kind = NDKKind.Article;
|
||||
event.tags = tags;
|
||||
|
||||
// publish
|
||||
const publishedRelays = await event.publish();
|
||||
const publish = await ark.createEvent({
|
||||
content,
|
||||
tags,
|
||||
kind: NDKKind.Article,
|
||||
publish: true,
|
||||
});
|
||||
|
||||
if (publishedRelays) {
|
||||
toast.success(`Broadcasted to ${publishedRelays.size} relays successfully.`);
|
||||
if (publish) {
|
||||
toast.success(`Broadcasted to ${publish} relays successfully.`);
|
||||
|
||||
// update state
|
||||
setLoading(false);
|
||||
|
||||
@@ -2,12 +2,12 @@ import { message } from '@tauri-apps/plugin-dialog';
|
||||
import { Editor } from '@tiptap/react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { MediaIcon } from '@shared/icons';
|
||||
|
||||
import { useNostr } from '@utils/hooks/useNostr';
|
||||
|
||||
export function MediaUploader({ editor }: { editor: Editor }) {
|
||||
const { upload } = useNostr();
|
||||
const { ark } = useArk();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const uploadToNostrBuild = async () => {
|
||||
@@ -15,7 +15,9 @@ export function MediaUploader({ editor }: { editor: Editor }) {
|
||||
// start loading
|
||||
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) {
|
||||
editor.commands.setImage({ src: image });
|
||||
|
||||
@@ -4,12 +4,12 @@ import { nip19 } from 'nostr-tools';
|
||||
|
||||
import { MentionPopupItem } from '@app/new/components';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { MentionIcon } from '@shared/icons';
|
||||
|
||||
export function MentionPopup({ editor }: { editor: Editor }) {
|
||||
const { db } = useStorage();
|
||||
const { ark } = useArk();
|
||||
|
||||
const insertMention = (pubkey: string) => {
|
||||
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"
|
||||
>
|
||||
<div className="flex flex-col gap-1 py-1">
|
||||
{db.account.contacts.length > 0 ? (
|
||||
db.account.contacts.map((item) => (
|
||||
{ark.account.contacts.length ? (
|
||||
ark.account.contacts.map((item) => (
|
||||
<button key={item} type="button" onClick={() => insertMention(item)}>
|
||||
<MentionPopupItem pubkey={item} />
|
||||
</button>
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
||||
import { message, open } from '@tauri-apps/plugin-dialog';
|
||||
import { readBinaryFile } from '@tauri-apps/plugin-fs';
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { LoaderIcon } from '@shared/icons';
|
||||
|
||||
export function NewFileScreen() {
|
||||
const { ndk } = useNDK();
|
||||
const { ark } = useArk();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -86,20 +85,21 @@ export function NewFileScreen() {
|
||||
|
||||
const submit = async () => {
|
||||
try {
|
||||
if (!ndk.signer) return navigate('/new/privkey');
|
||||
if (!ark.readyToSign) return navigate('/new/privkey');
|
||||
|
||||
setIsPublish(true);
|
||||
|
||||
const event = new NDKEvent(ndk);
|
||||
event.content = caption;
|
||||
event.kind = 1063;
|
||||
event.tags = metadata;
|
||||
const publish = await ark.createEvent({
|
||||
kind: 1063,
|
||||
tags: metadata,
|
||||
content: caption,
|
||||
publish: true,
|
||||
});
|
||||
|
||||
const publishedRelays = await event.publish();
|
||||
if (publishedRelays) {
|
||||
if (publish) {
|
||||
toast.success(`Broadcasted to ${publish} relays successfully.`);
|
||||
setMetadata(null);
|
||||
setIsPublish(false);
|
||||
toast.success(`Broadcasted to ${publishedRelays.size} relays successfully.`);
|
||||
}
|
||||
} catch (e) {
|
||||
setIsPublish(false);
|
||||
|
||||
@@ -7,13 +7,13 @@ import { EditorContent, useEditor } from '@tiptap/react';
|
||||
import StarterKit from '@tiptap/starter-kit';
|
||||
import { convert } from 'html-to-text';
|
||||
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 { toast } from 'sonner';
|
||||
|
||||
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 { MentionNote } from '@shared/notes';
|
||||
@@ -23,7 +23,7 @@ import { useSuggestion } from '@utils/hooks/useSuggestion';
|
||||
import { useWidget } from '@utils/hooks/useWidget';
|
||||
|
||||
export function NewPostScreen() {
|
||||
const { ndk } = useNDK();
|
||||
const { ark } = useArk();
|
||||
const { addWidget } = useWidget();
|
||||
const { suggestion } = useSuggestion();
|
||||
|
||||
@@ -68,7 +68,7 @@ export function NewPostScreen() {
|
||||
|
||||
const submit = async () => {
|
||||
try {
|
||||
if (!ndk.signer) return navigate('/new/privkey');
|
||||
if (!ark.readyToSign) return navigate('/new/privkey');
|
||||
|
||||
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
|
||||
const replyTo = searchParams.get('replyTo');
|
||||
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
|
||||
const publishedRelays = await event.publish();
|
||||
const event = (await ark.createEvent({
|
||||
kind: NDKKind.Text,
|
||||
tags: [],
|
||||
content: serializedContent,
|
||||
replyTo,
|
||||
rootReplyTo,
|
||||
})) as NDKEvent;
|
||||
|
||||
if (publishedRelays) {
|
||||
toast.success(`Broadcasted to ${publishedRelays.size} relays successfully.`);
|
||||
const publish = await event.publish();
|
||||
|
||||
if (publish) {
|
||||
toast.success(`Broadcasted to ${publish.size} relays successfully.`);
|
||||
|
||||
// update state
|
||||
setLoading(false);
|
||||
@@ -132,10 +126,6 @@ export function NewPostScreen() {
|
||||
setHeight(containerRef.current.clientHeight);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (editor) editor.commands.focus('end');
|
||||
}, [editor]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 flex-col gap-4">
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
|
||||
@@ -4,19 +4,13 @@ import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
export function NewPrivkeyScreen() {
|
||||
const { db } = useStorage();
|
||||
const { ndk } = useNDK();
|
||||
|
||||
const [nsec, setNsec] = useState('');
|
||||
const { ark } = useArk();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const save = async (content: string) => {
|
||||
return await db.secureSave(db.account.pubkey, content);
|
||||
};
|
||||
const [nsec, setNsec] = useState('');
|
||||
|
||||
const submit = async (isSave?: boolean) => {
|
||||
try {
|
||||
@@ -30,15 +24,15 @@ export function NewPrivkeyScreen() {
|
||||
const privkey = decoded.data;
|
||||
const pubkey = getPublicKey(privkey);
|
||||
|
||||
if (pubkey !== db.account.pubkey)
|
||||
if (pubkey !== ark.account.pubkey)
|
||||
return toast.info(
|
||||
'Your nsec is not match your current public key, please make sure you enter right nsec'
|
||||
);
|
||||
|
||||
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);
|
||||
} catch (e) {
|
||||
@@ -68,14 +62,14 @@ export function NewPrivkeyScreen() {
|
||||
<button
|
||||
type="button"
|
||||
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
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
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
|
||||
</button>
|
||||
|
||||
@@ -6,6 +6,8 @@ import { useRef, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { ArrowLeftIcon, CheckCircleIcon, ReplyIcon, ShareIcon } from '@shared/icons';
|
||||
import {
|
||||
ChildNote,
|
||||
@@ -18,15 +20,14 @@ import { ReplyList } from '@shared/notes/replies/list';
|
||||
import { User } from '@shared/user';
|
||||
|
||||
import { useEvent } from '@utils/hooks/useEvent';
|
||||
import { useNostr } from '@utils/hooks/useNostr';
|
||||
|
||||
export function TextNoteScreen() {
|
||||
const navigate = useNavigate();
|
||||
const replyRef = useRef(null);
|
||||
|
||||
const { id } = useParams();
|
||||
const { ark } = useArk();
|
||||
const { status, data } = useEvent(id);
|
||||
const { getEventThread } = useNostr();
|
||||
|
||||
const [isCopy, setIsCopy] = useState(false);
|
||||
|
||||
@@ -50,7 +51,7 @@ export function TextNoteScreen() {
|
||||
};
|
||||
|
||||
const renderKind = (event: NDKEvent) => {
|
||||
const thread = getEventThread(event.tags);
|
||||
const thread = ark.getEventThread({ tags: event.tags });
|
||||
switch (event.kind) {
|
||||
case NDKKind.Text:
|
||||
return (
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { LoaderIcon } from '@shared/icons';
|
||||
|
||||
export function NWCForm({ setWalletConnectURL }) {
|
||||
const { db } = useStorage();
|
||||
const { ark } = useArk();
|
||||
|
||||
const [uri, setUri] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -27,7 +27,7 @@ export function NWCForm({ setWalletConnectURL }) {
|
||||
const params = new URLSearchParams(uriObj.search);
|
||||
|
||||
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);
|
||||
setLoading(false);
|
||||
} else {
|
||||
|
||||
@@ -2,22 +2,22 @@ import { useEffect, useState } from 'react';
|
||||
|
||||
import { NWCForm } from '@app/nwc/components/form';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { CheckCircleIcon } from '@shared/icons';
|
||||
|
||||
export function NWCScreen() {
|
||||
const { db } = useStorage();
|
||||
const { ark } = useArk();
|
||||
const [walletConnectURL, setWalletConnectURL] = useState<null | string>(null);
|
||||
|
||||
const remove = async () => {
|
||||
await db.secureRemove(`${db.account.pubkey}-nwc`);
|
||||
await ark.removePrivkey(`${ark.account.pubkey}-nwc`);
|
||||
setWalletConnectURL(null);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
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);
|
||||
}
|
||||
getNWC();
|
||||
|
||||
@@ -1,33 +1,58 @@
|
||||
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { normalizeRelayUrl } from 'nostr-fetch';
|
||||
import { useCallback } from 'react';
|
||||
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { VList } from 'virtua';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { LoaderIcon } from '@shared/icons';
|
||||
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 RelayEventList({ relayUrl }: { relayUrl: string }) {
|
||||
const { fetcher } = useNDK();
|
||||
const { status, data } = useQuery({
|
||||
queryKey: ['relay-events', relayUrl],
|
||||
queryFn: async () => {
|
||||
const url = 'wss://' + relayUrl;
|
||||
const events = await fetcher.fetchLatestEvents(
|
||||
[normalizeRelayUrl(url)],
|
||||
{
|
||||
kinds: [NDKKind.Text, NDKKind.Repost],
|
||||
},
|
||||
20
|
||||
);
|
||||
return events as unknown as NDKEvent[];
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
});
|
||||
const { ark } = useArk();
|
||||
const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
|
||||
useInfiniteQuery({
|
||||
queryKey: ['relay-events', relayUrl],
|
||||
initialPageParam: 0,
|
||||
queryFn: async ({
|
||||
signal,
|
||||
pageParam,
|
||||
}: {
|
||||
signal: AbortSignal;
|
||||
pageParam: number;
|
||||
}) => {
|
||||
const url = 'wss://' + relayUrl;
|
||||
const events = await ark.getRelayEvents({
|
||||
relayUrl: url,
|
||||
filter: {
|
||||
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(
|
||||
(event: NDKEvent) => {
|
||||
@@ -46,16 +71,33 @@ export function RelayEventList({ relayUrl }: { relayUrl: string }) {
|
||||
return (
|
||||
<VList className="mx-auto h-full w-full max-w-[500px] pt-10 scrollbar-none">
|
||||
{status === 'pending' ? (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<div className="inline-flex flex-col items-center justify-center gap-2">
|
||||
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
|
||||
<p className="text-sm font-medium text-white/80">Loading newsfeed...</p>
|
||||
<div className="px-3 py-1.5">
|
||||
<div className="rounded-xl bg-neutral-100 px-3 py-3 dark:bg-neutral-900">
|
||||
<NoteSkeleton />
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,19 +2,20 @@ import { useQuery } from '@tanstack/react-query';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { VList } from 'virtua';
|
||||
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { LoaderIcon, PlusIcon, ShareIcon } from '@shared/icons';
|
||||
import { User } from '@shared/user';
|
||||
|
||||
import { useNostr } from '@utils/hooks/useNostr';
|
||||
import { useRelay } from '@utils/hooks/useRelay';
|
||||
|
||||
export function RelayList() {
|
||||
const { getAllRelaysByUsers } = useNostr();
|
||||
const { ark } = useArk();
|
||||
const { connectRelay } = useRelay();
|
||||
const { status, data } = useQuery({
|
||||
queryKey: ['relays'],
|
||||
queryFn: async () => {
|
||||
return await getAllRelaysByUsers();
|
||||
return await ark.getAllRelaysFromContacts();
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
|
||||
@@ -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 { RelayForm } from '@app/relays/components/relayForm';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { CancelIcon, RefreshIcon } from '@shared/icons';
|
||||
|
||||
import { useRelay } from '@utils/hooks/useRelay';
|
||||
|
||||
export function UserRelayList() {
|
||||
const { db } = useStorage();
|
||||
const { ndk } = useNDK();
|
||||
const { ark } = useArk();
|
||||
const { removeRelay } = useRelay();
|
||||
const { status, data, refetch } = useQuery({
|
||||
queryKey: ['relays', db.account.pubkey],
|
||||
queryKey: ['relays', ark.account.pubkey],
|
||||
queryFn: async () => {
|
||||
const event = await ndk.fetchEvent(
|
||||
{
|
||||
const event = await ark.getEventByFilter({
|
||||
filter: {
|
||||
kinds: [NDKKind.RelayList],
|
||||
authors: [db.account.pubkey],
|
||||
authors: [ark.account.pubkey],
|
||||
},
|
||||
{ cacheUsage: NDKSubscriptionCacheUsage.ONLY_RELAY }
|
||||
);
|
||||
});
|
||||
|
||||
if (!event) return [];
|
||||
return event.tags;
|
||||
@@ -31,7 +28,7 @@ export function UserRelayList() {
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
const currentRelays = new Set([...ndk.pool.relays.values()].map((item) => item.url));
|
||||
const currentRelays = new Set([...ark.relays]);
|
||||
|
||||
return (
|
||||
<div className="col-span-1">
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
export function AdvancedSettingScreen() {
|
||||
const { db } = useStorage();
|
||||
const { ark } = useArk();
|
||||
|
||||
const clearCache = async () => {
|
||||
await db.clearCache();
|
||||
await ark.clearCache();
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import { nip19 } from 'nostr-tools';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { EyeOffIcon } from '@shared/icons';
|
||||
|
||||
export function BackupSettingScreen() {
|
||||
const { db } = useStorage();
|
||||
const { ark } = useArk();
|
||||
|
||||
const [privkey, setPrivkey] = useState(null);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
const removePrivkey = async () => {
|
||||
await db.secureRemove(db.account.pubkey);
|
||||
await ark.removePrivkey(ark.account.pubkey);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
async function loadPrivkey() {
|
||||
const key = await db.secureLoad(db.account.pubkey);
|
||||
const key = await ark.loadPrivkey(ark.account.pubkey);
|
||||
if (key) setPrivkey(key);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { EditIcon, LoaderIcon } from '@shared/icons';
|
||||
|
||||
import { compactNumber } from '@utils/number';
|
||||
|
||||
export function ContactCard() {
|
||||
const { db } = useStorage();
|
||||
const { ndk } = useNDK();
|
||||
const { ark } = useArk();
|
||||
const { status, data } = useQuery({
|
||||
queryKey: ['contacts'],
|
||||
queryFn: async () => {
|
||||
const user = ndk.getUser({ pubkey: db.account.pubkey });
|
||||
const follows = await user.follows();
|
||||
return [...follows];
|
||||
const contacts = await ark.getUserContacts({});
|
||||
return contacts;
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
@@ -2,19 +2,19 @@ import { useQuery } from '@tanstack/react-query';
|
||||
import { fetch } from '@tauri-apps/plugin-http';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { LoaderIcon } from '@shared/icons';
|
||||
|
||||
import { compactNumber } from '@utils/number';
|
||||
|
||||
export function PostCard() {
|
||||
const { db } = useStorage();
|
||||
const { ark } = useArk();
|
||||
const { status, data } = useQuery({
|
||||
queryKey: ['user-stats', db.account.pubkey],
|
||||
queryKey: ['user-stats', ark.account.pubkey],
|
||||
queryFn: async ({ signal }: { signal: AbortSignal }) => {
|
||||
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,
|
||||
}
|
||||
@@ -41,14 +41,14 @@ export function PostCard() {
|
||||
) : (
|
||||
<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">
|
||||
{compactNumber.format(data.stats[db.account.pubkey].pub_note_count)}
|
||||
{compactNumber.format(data.stats[ark.account.pubkey].pub_note_count)}
|
||||
</h3>
|
||||
<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">
|
||||
Posts
|
||||
</p>
|
||||
<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"
|
||||
>
|
||||
View
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as Avatar from '@radix-ui/react-avatar';
|
||||
import { minidenticon } from 'minidenticons';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { EditIcon, LoaderIcon } from '@shared/icons';
|
||||
|
||||
@@ -10,12 +10,12 @@ import { useProfile } from '@utils/hooks/useProfile';
|
||||
import { displayNpub } from '@utils/shortenKey';
|
||||
|
||||
export function ProfileCard() {
|
||||
const { db } = useStorage();
|
||||
const { isLoading, user } = useProfile(db.account.pubkey);
|
||||
const { ark } = useArk();
|
||||
const { isLoading, user } = useProfile(ark.account.pubkey);
|
||||
|
||||
const svgURI =
|
||||
'data:image/svg+xml;utf8,' +
|
||||
encodeURIComponent(minidenticon(db.account.pubkey, 90, 50));
|
||||
encodeURIComponent(minidenticon(ark.account.pubkey, 90, 50));
|
||||
|
||||
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">
|
||||
@@ -38,7 +38,7 @@ export function ProfileCard() {
|
||||
<Avatar.Root className="shrink-0">
|
||||
<Avatar.Image
|
||||
src={user?.picture || user?.image}
|
||||
alt={db.account.pubkey}
|
||||
alt={ark.account.pubkey}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
style={{ contentVisibility: 'auto' }}
|
||||
@@ -47,7 +47,7 @@ export function ProfileCard() {
|
||||
<Avatar.Fallback delayMs={300}>
|
||||
<img
|
||||
src={svgURI}
|
||||
alt={db.account.pubkey}
|
||||
alt={ark.account.pubkey}
|
||||
className="h-16 w-16 rounded-xl bg-black dark:bg-white"
|
||||
/>
|
||||
</Avatar.Fallback>
|
||||
@@ -57,7 +57,7 @@ export function ProfileCard() {
|
||||
{user?.display_name || user?.name}
|
||||
</h3>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { EditIcon, LoaderIcon } from '@shared/icons';
|
||||
|
||||
import { compactNumber } from '@utils/number';
|
||||
|
||||
export function RelayCard() {
|
||||
const { db } = useStorage();
|
||||
const { ndk } = useNDK();
|
||||
const { ark } = useArk();
|
||||
const { status, data } = useQuery({
|
||||
queryKey: ['relays'],
|
||||
queryKey: ['relays', ark.account.pubkey],
|
||||
queryFn: async () => {
|
||||
const user = ndk.getUser({ pubkey: db.account.pubkey });
|
||||
const relays = await user.relayList();
|
||||
|
||||
if (!relays) return Promise.reject(new Error("user's relay set not found"));
|
||||
const relays = await ark.getUserRelays({});
|
||||
return relays;
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { fetch } from '@tauri-apps/plugin-http';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { LoaderIcon } from '@shared/icons';
|
||||
|
||||
import { compactNumber } from '@utils/number';
|
||||
|
||||
export function ZapCard() {
|
||||
const { db } = useStorage();
|
||||
const { ark } = useArk();
|
||||
const { status, data } = useQuery({
|
||||
queryKey: ['user-stats', db.account.pubkey],
|
||||
queryKey: ['user-stats', ark.account.pubkey],
|
||||
queryFn: async ({ signal }: { signal: AbortSignal }) => {
|
||||
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,
|
||||
}
|
||||
@@ -41,7 +41,7 @@ export function ZapCard() {
|
||||
<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">
|
||||
{compactNumber.format(
|
||||
data?.stats[db.account.pubkey]?.zaps_received?.msats / 1000 || 0
|
||||
data?.stats[ark.account.pubkey]?.zaps_received?.msats / 1000 || 0
|
||||
)}
|
||||
</h3>
|
||||
<div className="mt-auto flex h-6 items-center text-xl font-medium leading-none text-neutral-600 dark:text-neutral-400">
|
||||
|
||||
@@ -1,21 +1,16 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { LoaderIcon } from '@shared/icons';
|
||||
import { User } from '@shared/user';
|
||||
|
||||
export function EditContactScreen() {
|
||||
const { db } = useStorage();
|
||||
const { ndk } = useNDK();
|
||||
const { ark } = useArk();
|
||||
const { status, data } = useQuery({
|
||||
queryKey: ['contacts'],
|
||||
queryFn: async () => {
|
||||
const user = ndk.getUser({ pubkey: db.account.pubkey });
|
||||
|
||||
const follows = await user.follows();
|
||||
return [...follows];
|
||||
return await ark.getUserContacts({});
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
@@ -29,10 +24,10 @@ export function EditContactScreen() {
|
||||
) : (
|
||||
data.map((item) => (
|
||||
<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"
|
||||
>
|
||||
<User pubkey={item.pubkey} variant="simple" />
|
||||
<User pubkey={item} variant="simple" />
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
|
||||
@@ -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 { message } from '@tauri-apps/plugin-dialog';
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { CheckCircleIcon, LoaderIcon, PlusIcon, UnverifiedIcon } from '@shared/icons';
|
||||
|
||||
import { useNostr } from '@utils/hooks/useNostr';
|
||||
|
||||
export function EditProfileScreen() {
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [picture, setPicture] = useState('');
|
||||
const [banner, setBanner] = useState('');
|
||||
const [nip05, setNIP05] = useState({ verified: true, text: '' });
|
||||
|
||||
const { db } = useStorage();
|
||||
const { ndk } = useNDK();
|
||||
const { upload } = useNostr();
|
||||
const { ark } = useArk();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
@@ -32,7 +24,7 @@ export function EditProfileScreen() {
|
||||
formState: { isValid, errors },
|
||||
} = useForm({
|
||||
defaultValues: async () => {
|
||||
const res: NDKUserProfile = queryClient.getQueryData(['user', db.account.pubkey]);
|
||||
const res: NDKUserProfile = queryClient.getQueryData(['user', ark.account.pubkey]);
|
||||
if (res.image) {
|
||||
setPicture(res.image);
|
||||
}
|
||||
@@ -46,13 +38,16 @@ export function EditProfileScreen() {
|
||||
},
|
||||
});
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const uploadAvatar = async () => {
|
||||
try {
|
||||
if (!ndk.signer) return navigate('/new/privkey');
|
||||
if (!ark.readyToSign) return navigate('/new/privkey');
|
||||
|
||||
setLoading(true);
|
||||
|
||||
const image = await upload();
|
||||
const image = await ark.upload({});
|
||||
if (image) {
|
||||
setPicture(image);
|
||||
setLoading(false);
|
||||
@@ -67,7 +62,7 @@ export function EditProfileScreen() {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const image = await upload();
|
||||
const image = await ark.upload({});
|
||||
|
||||
if (image) {
|
||||
setBanner(image);
|
||||
@@ -83,7 +78,7 @@ export function EditProfileScreen() {
|
||||
// start loading
|
||||
setLoading(true);
|
||||
|
||||
const content = {
|
||||
let content = {
|
||||
...data,
|
||||
username: data.name,
|
||||
display_name: data.name,
|
||||
@@ -91,15 +86,10 @@ export function EditProfileScreen() {
|
||||
image: data.picture,
|
||||
};
|
||||
|
||||
const event = new NDKEvent(ndk);
|
||||
event.kind = NDKKind.Metadata;
|
||||
event.tags = [];
|
||||
|
||||
if (data.nip05) {
|
||||
const user = ndk.getUser({ pubkey: db.account.pubkey });
|
||||
const verify = await user.validateNip05(data.nip05);
|
||||
const verify = ark.validateNIP05({ pubkey: ark.account.pubkey, nip05: data.nip05 });
|
||||
if (verify) {
|
||||
event.content = JSON.stringify({ ...content, nip05: data.nip05 });
|
||||
content = { ...content, nip05: data.nip05 };
|
||||
} else {
|
||||
setNIP05((prev) => ({ ...prev, verified: false }));
|
||||
setError('nip05', {
|
||||
@@ -107,16 +97,19 @@ export function EditProfileScreen() {
|
||||
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
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['user', db.account.pubkey],
|
||||
queryKey: ['user', ark.account.pubkey],
|
||||
});
|
||||
// reset form
|
||||
reset();
|
||||
|
||||
@@ -6,12 +6,12 @@ import { isPermissionGranted, requestPermission } from '@tauri-apps/plugin-notif
|
||||
import { useEffect, useState } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { DarkIcon, LightIcon, SystemModeIcon } from '@shared/icons';
|
||||
|
||||
export function GeneralSettingScreen() {
|
||||
const { db } = useStorage();
|
||||
const { ark } = useArk();
|
||||
const [settings, setSettings] = useState({
|
||||
autoupdate: false,
|
||||
autolaunch: false,
|
||||
@@ -41,28 +41,28 @@ export function GeneralSettingScreen() {
|
||||
};
|
||||
|
||||
const toggleOutbox = async () => {
|
||||
await db.createSetting('outbox', String(+!settings.outbox));
|
||||
await ark.createSetting('outbox', String(+!settings.outbox));
|
||||
// update state
|
||||
setSettings((prev) => ({ ...prev, outbox: !settings.outbox }));
|
||||
};
|
||||
|
||||
const toggleMedia = async () => {
|
||||
await db.createSetting('media', String(+!settings.media));
|
||||
db.settings.media = !settings.media;
|
||||
await ark.createSetting('media', String(+!settings.media));
|
||||
ark.settings.media = !settings.media;
|
||||
// update state
|
||||
setSettings((prev) => ({ ...prev, media: !settings.media }));
|
||||
};
|
||||
|
||||
const toggleHashtag = async () => {
|
||||
await db.createSetting('hashtag', String(+!settings.hashtag));
|
||||
db.settings.hashtag = !settings.hashtag;
|
||||
await ark.createSetting('hashtag', String(+!settings.hashtag));
|
||||
ark.settings.hashtag = !settings.hashtag;
|
||||
// update state
|
||||
setSettings((prev) => ({ ...prev, hashtag: !settings.hashtag }));
|
||||
};
|
||||
|
||||
const toggleAutoupdate = async () => {
|
||||
await db.createSetting('autoupdate', String(+!settings.autoupdate));
|
||||
db.settings.autoupdate = !settings.autoupdate;
|
||||
await ark.createSetting('autoupdate', String(+!settings.autoupdate));
|
||||
ark.settings.autoupdate = !settings.autoupdate;
|
||||
// update state
|
||||
setSettings((prev) => ({ ...prev, autoupdate: !settings.autoupdate }));
|
||||
};
|
||||
@@ -86,7 +86,7 @@ export function GeneralSettingScreen() {
|
||||
const permissionGranted = await isPermissionGranted();
|
||||
setSettings((prev) => ({ ...prev, notification: permissionGranted }));
|
||||
|
||||
const data = await db.getAllSettings();
|
||||
const data = await ark.getAllSettings();
|
||||
if (!data) return;
|
||||
|
||||
data.forEach((item) => {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { NDKEvent, NDKKind, NDKUser } from '@nostr-dev-kit/ndk';
|
||||
import * as Avatar from '@radix-ui/react-avatar';
|
||||
import { minidenticon } from 'minidenticons';
|
||||
import { useEffect, useState } from 'react';
|
||||
@@ -7,8 +6,7 @@ import { toast } from 'sonner';
|
||||
|
||||
import { UserStats } from '@app/users/components/stats';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { NIP05 } from '@shared/nip05';
|
||||
|
||||
@@ -16,8 +14,7 @@ import { useProfile } from '@utils/hooks/useProfile';
|
||||
import { displayNpub } from '@utils/shortenKey';
|
||||
|
||||
export function UserProfile({ pubkey }: { pubkey: string }) {
|
||||
const { db } = useStorage();
|
||||
const { ndk } = useNDK();
|
||||
const { ark } = useArk();
|
||||
const { user } = useProfile(pubkey);
|
||||
|
||||
const [followed, setFollowed] = useState(false);
|
||||
@@ -28,12 +25,10 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
|
||||
|
||||
const follow = async () => {
|
||||
try {
|
||||
if (!ndk.signer) return navigate('/new/privkey');
|
||||
if (!ark.readyToSign) return navigate('/new/privkey');
|
||||
setFollowed(true);
|
||||
|
||||
const user = ndk.getUser({ pubkey: db.account.pubkey });
|
||||
const contacts = await user.follows();
|
||||
const add = await user.follow(new NDKUser({ pubkey: pubkey }), contacts);
|
||||
const add = await ark.createContact({ pubkey });
|
||||
|
||||
if (!add) {
|
||||
toast.success('You already follow this user');
|
||||
@@ -47,32 +42,17 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
|
||||
|
||||
const unfollow = async () => {
|
||||
try {
|
||||
if (!ndk.signer) return navigate('/new/privkey');
|
||||
if (!ark.readyToSign) return navigate('/new/privkey');
|
||||
setFollowed(false);
|
||||
|
||||
const user = ndk.getUser({ pubkey: db.account.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();
|
||||
await ark.deleteContact({ pubkey });
|
||||
} catch (e) {
|
||||
toast.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (db.account.contacts.includes(pubkey)) {
|
||||
if (ark.account.contacts.includes(pubkey)) {
|
||||
setFollowed(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -1,30 +1,60 @@
|
||||
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useCallback } from 'react';
|
||||
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
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() {
|
||||
const { pubkey } = useParams();
|
||||
const { ndk } = useNDK();
|
||||
const { status, data } = useQuery({
|
||||
queryKey: ['user-feed', pubkey],
|
||||
queryFn: async () => {
|
||||
const events = await ndk.fetchEvents({
|
||||
kinds: [NDKKind.Text, NDKKind.Repost],
|
||||
authors: [pubkey],
|
||||
limit: 20,
|
||||
});
|
||||
const sorted = [...events].sort((a, b) => b.created_at - a.created_at);
|
||||
return sorted;
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
const { ark } = useArk();
|
||||
const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
|
||||
useInfiniteQuery({
|
||||
queryKey: ['user-posts', pubkey],
|
||||
initialPageParam: 0,
|
||||
queryFn: async ({
|
||||
signal,
|
||||
pageParam,
|
||||
}: {
|
||||
signal: AbortSignal;
|
||||
pageParam: number;
|
||||
}) => {
|
||||
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
|
||||
const renderItem = useCallback(
|
||||
@@ -50,20 +80,33 @@ export function UserScreen() {
|
||||
</h3>
|
||||
<div className="mx-auto flex h-full max-w-[500px] flex-col justify-between gap-1.5 pb-4 pt-1.5">
|
||||
{status === 'pending' ? (
|
||||
<div>Loading...</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 font-medium text-neutral-900 dark:text-neutral-100">
|
||||
User doesn't have any posts in the last 48 hours.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-xl bg-neutral-100 px-3 py-3 dark:bg-neutral-900">
|
||||
<NoteSkeleton />
|
||||
</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>
|
||||
|
||||
915
src/libs/ark/ark.ts
Normal file
915
src/libs/ark/ark.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,30 @@
|
||||
// inspired by: https://github.com/nostr-dev-kit/ndk/tree/master/ndk-cache-dexie
|
||||
import { NDKEvent, NDKRelay, profileFromEvent } from '@nostr-dev-kit/ndk';
|
||||
import type {
|
||||
// inspired by NDK Cache Dexie
|
||||
// source: https://github.com/nostr-dev-kit/ndk/tree/master/ndk-cache-dexie
|
||||
import {
|
||||
Hexpubkey,
|
||||
NDKCacheAdapter,
|
||||
NDKEvent,
|
||||
NDKFilter,
|
||||
NDKRelay,
|
||||
NDKSubscription,
|
||||
NDKUserProfile,
|
||||
NostrEvent,
|
||||
profileFromEvent,
|
||||
} from '@nostr-dev-kit/ndk';
|
||||
import Database from '@tauri-apps/plugin-sql';
|
||||
import { LRUCache } from 'lru-cache';
|
||||
import { NostrEvent } from 'nostr-fetch';
|
||||
import { matchFilter } from 'nostr-tools';
|
||||
|
||||
import { LumeStorage } from '@libs/storage/instance';
|
||||
import { NDKCacheEvent, NDKCacheEventTag, NDKCacheUser } from '@utils/types';
|
||||
|
||||
export default class NDKCacheAdapterTauri implements NDKCacheAdapter {
|
||||
public db: LumeStorage;
|
||||
public profiles?: LRUCache<Hexpubkey, NDKUserProfile>;
|
||||
export class NDKCacheAdapterTauri implements NDKCacheAdapter {
|
||||
#db: Database;
|
||||
private dirtyProfiles: Set<Hexpubkey> = new Set();
|
||||
public profiles?: LRUCache<Hexpubkey, NDKUserProfile>;
|
||||
readonly locking: boolean;
|
||||
|
||||
constructor(db: LumeStorage) {
|
||||
this.db = db;
|
||||
constructor(db: Database) {
|
||||
this.#db = db;
|
||||
this.locking = true;
|
||||
|
||||
this.profiles = new LRUCache({
|
||||
@@ -32,6 +36,115 @@ export default class NDKCacheAdapterTauri implements NDKCacheAdapter {
|
||||
}, 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> {
|
||||
Promise.allSettled(
|
||||
subscription.filters.map((filter) => this.processFilter(filter, subscription))
|
||||
@@ -44,7 +157,7 @@ export default class NDKCacheAdapterTauri implements NDKCacheAdapter {
|
||||
let profile = this.profiles.get(pubkey);
|
||||
|
||||
if (!profile) {
|
||||
const user = await this.db.getCacheUser(pubkey);
|
||||
const user = await this.#getCacheUser(pubkey);
|
||||
if (user) {
|
||||
profile = user.profile as NDKUserProfile;
|
||||
this.profiles.set(pubkey, profile);
|
||||
@@ -97,7 +210,7 @@ export default class NDKCacheAdapterTauri implements NDKCacheAdapter {
|
||||
|
||||
if (event.isParamReplaceable()) {
|
||||
const replaceableId = `${event.kind}:${event.pubkey}:${event.tagId()}`;
|
||||
const existingEvent = await this.db.getCacheEvent(replaceableId);
|
||||
const existingEvent = await this.#getCacheEvent(replaceableId);
|
||||
if (
|
||||
existingEvent &&
|
||||
event.created_at &&
|
||||
@@ -108,7 +221,7 @@ export default class NDKCacheAdapterTauri implements NDKCacheAdapter {
|
||||
}
|
||||
|
||||
if (addEvent) {
|
||||
this.db.setCacheEvent({
|
||||
this.#setCacheEvent({
|
||||
id: event.tagId(),
|
||||
pubkey: event.pubkey,
|
||||
content: event.content,
|
||||
@@ -124,7 +237,7 @@ export default class NDKCacheAdapterTauri implements NDKCacheAdapter {
|
||||
event.tags.forEach((tag) => {
|
||||
if (tag[0].length !== 1) return;
|
||||
|
||||
this.db.setCacheEventTag({
|
||||
this.#setCacheEventTag({
|
||||
id: `${event.id}:${tag[0]}:${tag[1]}`,
|
||||
eventId: event.id,
|
||||
tag: tag[0],
|
||||
@@ -153,7 +266,7 @@ export default class NDKCacheAdapterTauri implements NDKCacheAdapter {
|
||||
|
||||
if (hasAllKeys && 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) {
|
||||
let rawEvent: NostrEvent;
|
||||
try {
|
||||
@@ -189,7 +302,7 @@ export default class NDKCacheAdapterTauri implements NDKCacheAdapter {
|
||||
|
||||
if (hasAllKeys && 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) {
|
||||
let rawEvent: NostrEvent;
|
||||
try {
|
||||
@@ -223,7 +336,7 @@ export default class NDKCacheAdapterTauri implements NDKCacheAdapter {
|
||||
|
||||
if (hasAllKeys && filter.ids) {
|
||||
for (const id of filter.ids) {
|
||||
const event = await this.db.getCacheEvent(id);
|
||||
const event = await this.#getCacheEvent(id);
|
||||
if (!event) continue;
|
||||
|
||||
let rawEvent: NostrEvent;
|
||||
@@ -266,7 +379,7 @@ export default class NDKCacheAdapterTauri implements NDKCacheAdapter {
|
||||
for (const author of filter.authors) {
|
||||
for (const dTag of filter['#d']) {
|
||||
const replaceableId = `${kind}:${author}:${dTag}`;
|
||||
const event = await this.db.getCacheEvent(replaceableId);
|
||||
const event = await this.#getCacheEvent(replaceableId);
|
||||
if (!event) continue;
|
||||
|
||||
let rawEvent: NostrEvent;
|
||||
@@ -306,7 +419,7 @@ export default class NDKCacheAdapterTauri implements NDKCacheAdapter {
|
||||
if (filter.kinds && filter.authors) {
|
||||
for (const kind of filter.kinds) {
|
||||
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) {
|
||||
let rawEvent: NostrEvent;
|
||||
@@ -371,12 +484,12 @@ export default class NDKCacheAdapterTauri implements NDKCacheAdapter {
|
||||
}
|
||||
|
||||
for (const value of values) {
|
||||
const eventTags = await this.db.getCacheEventTagsByTagValue(tag + value);
|
||||
const eventTags = await this.#getCacheEventTagsByTagValue(tag + value);
|
||||
if (!eventTags.length) continue;
|
||||
|
||||
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) {
|
||||
let rawEvent;
|
||||
try {
|
||||
@@ -418,7 +531,7 @@ export default class NDKCacheAdapterTauri implements NDKCacheAdapter {
|
||||
}
|
||||
|
||||
if (profiles.length) {
|
||||
await this.db.setCacheProfiles(profiles);
|
||||
await this.#setCacheProfiles(profiles);
|
||||
}
|
||||
|
||||
this.dirtyProfiles.clear();
|
||||
3
src/libs/ark/index.ts
Normal file
3
src/libs/ark/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './ark';
|
||||
export * from './cache';
|
||||
export * from './provider';
|
||||
179
src/libs/ark/provider.tsx
Normal file
179
src/libs/ark/provider.tsx
Normal 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 };
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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 };
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
11
src/main.jsx
11
src/main.jsx
@@ -3,8 +3,7 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { Toaster } from 'sonner';
|
||||
|
||||
import { NDKProvider } from '@libs/ndk/provider';
|
||||
import { StorageProvider } from '@libs/storage/provider';
|
||||
import { ArkProvider } from '@libs/ark/provider';
|
||||
|
||||
import App from './app';
|
||||
|
||||
@@ -23,10 +22,8 @@ root.render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
<Toaster position="top-center" theme="system" closeButton />
|
||||
<StorageProvider>
|
||||
<NDKProvider>
|
||||
<App />
|
||||
</NDKProvider>
|
||||
</StorageProvider>
|
||||
<ArkProvider>
|
||||
<App />
|
||||
</ArkProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as Avatar from '@radix-ui/react-avatar';
|
||||
import { minidenticon } from 'minidenticons';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { AccountMoreActions } from '@shared/accounts/more';
|
||||
import { NetworkStatusIndicator } from '@shared/networkStatusIndicator';
|
||||
@@ -10,12 +10,12 @@ import { NetworkStatusIndicator } from '@shared/networkStatusIndicator';
|
||||
import { useProfile } from '@utils/hooks/useProfile';
|
||||
|
||||
export function ActiveAccount() {
|
||||
const { db } = useStorage();
|
||||
const { user } = useProfile(db.account.pubkey);
|
||||
const { ark } = useArk();
|
||||
const { user } = useProfile(ark.account.pubkey);
|
||||
|
||||
const svgURI =
|
||||
'data:image/svg+xml;utf8,' +
|
||||
encodeURIComponent(minidenticon(db.account.pubkey, 90, 50));
|
||||
encodeURIComponent(minidenticon(ark.account.pubkey, 90, 50));
|
||||
|
||||
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">
|
||||
@@ -23,7 +23,7 @@ export function ActiveAccount() {
|
||||
<Avatar.Root>
|
||||
<Avatar.Image
|
||||
src={user?.picture || user?.image}
|
||||
alt={db.account.pubkey}
|
||||
alt={ark.account.pubkey}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
style={{ contentVisibility: 'auto' }}
|
||||
@@ -32,7 +32,7 @@ export function ActiveAccount() {
|
||||
<Avatar.Fallback delayMs={150}>
|
||||
<img
|
||||
src={svgURI}
|
||||
alt={db.account.pubkey}
|
||||
alt={ark.account.pubkey}
|
||||
className="aspect-square h-auto w-full rounded-md bg-black dark:bg-white"
|
||||
/>
|
||||
</Avatar.Fallback>
|
||||
|
||||
@@ -3,26 +3,18 @@ import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
export function Logout() {
|
||||
const { db } = useStorage();
|
||||
const { ndk } = useNDK();
|
||||
const { ark } = useArk();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const logout = async () => {
|
||||
try {
|
||||
ndk.signer = null;
|
||||
|
||||
// remove private key
|
||||
await db.secureRemove(db.account.pubkey);
|
||||
await db.secureRemove(`${db.account.id}-nsecbunker`);
|
||||
|
||||
// logout
|
||||
await db.accountLogout();
|
||||
await ark.logout();
|
||||
|
||||
// clear cache
|
||||
queryClient.clear();
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { message } from '@tauri-apps/plugin-dialog';
|
||||
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({
|
||||
setPicture,
|
||||
}: {
|
||||
setPicture: Dispatch<SetStateAction<string>>;
|
||||
}) {
|
||||
const { upload } = useNostr();
|
||||
const { ark } = useArk();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const uploadAvatar = async () => {
|
||||
@@ -18,7 +18,7 @@ export function AvatarUploader({
|
||||
// start loading
|
||||
setLoading(true);
|
||||
|
||||
const image = await upload();
|
||||
const image = await ark.upload({});
|
||||
|
||||
if (image) {
|
||||
setPicture(image);
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { message } from '@tauri-apps/plugin-dialog';
|
||||
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({
|
||||
setBanner,
|
||||
}: {
|
||||
setBanner: Dispatch<SetStateAction<string>>;
|
||||
}) {
|
||||
const { upload } = useNostr();
|
||||
const { ark } = useArk();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const uploadBanner = async () => {
|
||||
@@ -18,7 +18,7 @@ export function BannerUploader({
|
||||
// start loading
|
||||
setLoading(true);
|
||||
|
||||
const image = await upload();
|
||||
const image = await ark.upload({});
|
||||
|
||||
if (image) {
|
||||
setBanner(image);
|
||||
|
||||
@@ -2,21 +2,21 @@ import { Outlet, ScrollRestoration } from 'react-router-dom';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import { WindowTitlebar } from 'tauri-controls';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { Navigation } from '@shared/navigation';
|
||||
|
||||
export function AppLayout() {
|
||||
const { db } = useStorage();
|
||||
const { ark } = useArk();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
'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 />
|
||||
) : (
|
||||
<div data-tauri-drag-region className="h-9" />
|
||||
@@ -26,7 +26,7 @@ export function AppLayout() {
|
||||
data-tauri-drag-region
|
||||
className={twMerge(
|
||||
'h-full w-[64px] shrink-0',
|
||||
db.platform !== 'macos' ? 'pt-2' : 'pt-0'
|
||||
ark.platform !== 'macos' ? 'pt-2' : 'pt-0'
|
||||
)}
|
||||
>
|
||||
<Navigation />
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { Outlet, ScrollRestoration } from 'react-router-dom';
|
||||
import { WindowTitlebar } from 'tauri-controls';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
export function AuthLayout() {
|
||||
const { db } = useStorage();
|
||||
const { ark } = useArk();
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-screen flex-col">
|
||||
{db.platform !== 'macos' ? (
|
||||
{ark.platform !== 'macos' ? (
|
||||
<WindowTitlebar />
|
||||
) : (
|
||||
<div data-tauri-drag-region className="h-9" />
|
||||
|
||||
@@ -2,17 +2,17 @@ import { Link, NavLink, Outlet, useLocation } from 'react-router-dom';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import { WindowTitlebar } from 'tauri-controls';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { ArrowLeftIcon } from '@shared/icons';
|
||||
|
||||
export function NewLayout() {
|
||||
const { db } = useStorage();
|
||||
const { ark } = useArk();
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-screen flex-col bg-neutral-50 dark:bg-neutral-950">
|
||||
{db.platform !== 'macos' ? (
|
||||
{ark.platform !== 'macos' ? (
|
||||
<WindowTitlebar />
|
||||
) : (
|
||||
<div data-tauri-drag-region className="h-9 shrink-0" />
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { Outlet, ScrollRestoration } from 'react-router-dom';
|
||||
import { WindowTitlebar } from 'tauri-controls';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
export function NoteLayout() {
|
||||
const { db } = useStorage();
|
||||
const { ark } = useArk();
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-screen flex-col bg-neutral-50 dark:bg-neutral-950">
|
||||
{db.platform !== 'macos' ? (
|
||||
{ark.platform !== 'macos' ? (
|
||||
<WindowTitlebar />
|
||||
) : (
|
||||
<div data-tauri-drag-region className="h-9" />
|
||||
|
||||
@@ -2,7 +2,7 @@ import { NavLink, Outlet, ScrollRestoration, useNavigate } from 'react-router-do
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import { WindowTitlebar } from 'tauri-controls';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import {
|
||||
AdvancedSettingsIcon,
|
||||
@@ -14,12 +14,12 @@ import {
|
||||
} from '@shared/icons';
|
||||
|
||||
export function SettingsLayout() {
|
||||
const { db } = useStorage();
|
||||
const { ark } = useArk();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-screen flex-col bg-neutral-50 dark:bg-neutral-950">
|
||||
{db.platform !== 'macos' ? (
|
||||
{ark.platform !== 'macos' ? (
|
||||
<WindowTitlebar />
|
||||
) : (
|
||||
<div data-tauri-drag-region className="h-9" />
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { ReactionIcon } from '@shared/icons';
|
||||
|
||||
@@ -35,7 +35,7 @@ export function NoteReaction({ event }: { event: NDKEvent }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [reaction, setReaction] = useState<string | null>(null);
|
||||
|
||||
const { ndk } = useNDK();
|
||||
const { ark } = useArk();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const getReactionImage = (content: string) => {
|
||||
@@ -45,7 +45,7 @@ export function NoteReaction({ event }: { event: NDKEvent }) {
|
||||
|
||||
const react = async (content: string) => {
|
||||
try {
|
||||
if (!ndk.signer) return navigate('/new/privkey');
|
||||
if (!ark.readyToSign) return navigate('/new/privkey');
|
||||
|
||||
setReaction(content);
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { toast } from 'sonner';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { LoaderIcon, RepostIcon } from '@shared/icons';
|
||||
|
||||
@@ -15,12 +15,12 @@ export function NoteRepost({ event }: { event: NDKEvent }) {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isRepost, setIsRepost] = useState(false);
|
||||
|
||||
const { ndk } = useNDK();
|
||||
const { ark } = useArk();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const submit = async () => {
|
||||
try {
|
||||
if (!ndk.signer) return navigate('/new/privkey');
|
||||
if (!ark.readyToSign) return navigate('/new/privkey');
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
|
||||
@@ -9,8 +9,7 @@ import { useEffect, useRef, useState } from 'react';
|
||||
import CurrencyInput from 'react-currency-input-field';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { CancelIcon, ZapIcon } from '@shared/icons';
|
||||
|
||||
@@ -20,11 +19,7 @@ import { compactNumber } from '@utils/number';
|
||||
import { displayNpub } from '@utils/shortenKey';
|
||||
|
||||
export function NoteZap({ event }: { event: NDKEvent }) {
|
||||
const nwc = useRef(null);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { db } = useStorage();
|
||||
const { ndk } = useNDK();
|
||||
const { ark } = useArk();
|
||||
const { user } = useProfile(event.pubkey);
|
||||
|
||||
const [walletConnectURL, setWalletConnectURL] = useState<string>(null);
|
||||
@@ -35,9 +30,12 @@ export function NoteZap({ event }: { event: NDKEvent }) {
|
||||
const [isCompleted, setIsCompleted] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const nwc = useRef(null);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const createZapRequest = async () => {
|
||||
try {
|
||||
if (!ndk.signer) return navigate('/new/privkey');
|
||||
if (!ark.readyToSign) return navigate('/new/privkey');
|
||||
|
||||
const zapAmount = parseInt(amount) * 1000;
|
||||
const res = await event.zap(zapAmount, zapMessage);
|
||||
@@ -88,7 +86,7 @@ export function NoteZap({ event }: { event: NDKEvent }) {
|
||||
useEffect(() => {
|
||||
async function getWalletConnectURL() {
|
||||
const uri: string = await invoke('secure_load', {
|
||||
key: `${db.account.pubkey}-nwc`,
|
||||
key: `${ark.account.pubkey}-nwc`,
|
||||
});
|
||||
if (uri) setWalletConnectURL(uri);
|
||||
}
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { ReplyIcon, RepostIcon } from '@shared/icons';
|
||||
import { ChildNote, TextKind } from '@shared/notes';
|
||||
import { User } from '@shared/user';
|
||||
|
||||
import { WIDGET_KIND } from '@utils/constants';
|
||||
import { formatCreatedAt } from '@utils/createdAt';
|
||||
import { useNostr } from '@utils/hooks/useNostr';
|
||||
import { useWidget } from '@utils/hooks/useWidget';
|
||||
|
||||
export function NotifyNote({ event }: { event: NDKEvent }) {
|
||||
const { getEventThread } = useNostr();
|
||||
const { ark } = useArk();
|
||||
const { addWidget } = useWidget();
|
||||
|
||||
const thread = getEventThread(event.tags);
|
||||
const thread = ark.getEventThread({ tags: event.tags });
|
||||
const createdAt = formatCreatedAt(event.created_at, false);
|
||||
|
||||
if (event.kind === NDKKind.Reaction) {
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { LoaderIcon } from '@shared/icons';
|
||||
import { ReplyMediaUploader } from '@shared/notes';
|
||||
|
||||
export function NoteReplyForm({ rootEvent }: { rootEvent: NDKEvent }) {
|
||||
const { ndk } = useNDK();
|
||||
const { ark } = useArk();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [value, setValue] = useState('');
|
||||
@@ -17,22 +17,14 @@ export function NoteReplyForm({ rootEvent }: { rootEvent: NDKEvent }) {
|
||||
|
||||
const submit = async () => {
|
||||
try {
|
||||
if (!ndk.signer) return navigate('/new/privkey');
|
||||
|
||||
if (!ark.readyToSign) return navigate('/new/privkey');
|
||||
setLoading(true);
|
||||
|
||||
const event = new NDKEvent(ndk);
|
||||
event.content = value;
|
||||
event.kind = NDKKind.Text;
|
||||
|
||||
// tag root event
|
||||
event.tag(rootEvent, 'reply');
|
||||
|
||||
// publish event
|
||||
const publishedRelays = await event.publish();
|
||||
const publish = await ark.replyTo({ content: value, event: rootEvent });
|
||||
|
||||
if (publishedRelays) {
|
||||
toast.success(`Broadcasted to ${publishedRelays.size} relays successfully.`);
|
||||
if (publish) {
|
||||
toast.success(`Broadcasted to ${publish.size} relays successfully.`);
|
||||
|
||||
// reset state
|
||||
setValue('');
|
||||
|
||||
@@ -8,7 +8,13 @@ import { User } from '@shared/user';
|
||||
|
||||
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);
|
||||
|
||||
return (
|
||||
@@ -30,7 +36,7 @@ export function Reply({ event }: { event: NDKEventWithReplies }) {
|
||||
</div>
|
||||
</Collapsible.Trigger>
|
||||
) : null}
|
||||
<NoteActions event={event} canOpenEvent={false} />
|
||||
<NoteActions event={event} rootEventId={rootEvent} canOpenEvent={false} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={twMerge('px-3', open ? 'pb-3' : '')}>
|
||||
|
||||
@@ -1,38 +1,42 @@
|
||||
import { NDKSubscription } from '@nostr-dev-kit/ndk';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { LoaderIcon } from '@shared/icons';
|
||||
import { Reply } from '@shared/notes';
|
||||
|
||||
import { useNostr } from '@utils/hooks/useNostr';
|
||||
import { NDKEventWithReplies } from '@utils/types';
|
||||
|
||||
export function ReplyList({ eventId }: { eventId: string }) {
|
||||
const { fetchAllReplies, sub } = useNostr();
|
||||
const { ark } = useArk();
|
||||
const [data, setData] = useState<null | NDKEventWithReplies[]>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let sub: NDKSubscription;
|
||||
let isCancelled = false;
|
||||
|
||||
async function fetchRepliesAndSub() {
|
||||
const events = await fetchAllReplies(eventId);
|
||||
const events = await ark.getThreads({ id: eventId });
|
||||
if (!isCancelled) {
|
||||
setData(events);
|
||||
}
|
||||
// subscribe for new replies
|
||||
sub(
|
||||
{
|
||||
sub = ark.subscribe({
|
||||
filter: {
|
||||
'#e': [eventId],
|
||||
since: Math.floor(Date.now() / 1000),
|
||||
},
|
||||
(event: NDKEventWithReplies) => setData((prev) => [event, ...prev]),
|
||||
false
|
||||
);
|
||||
closeOnEose: false,
|
||||
cb: (event: NDKEventWithReplies) => setData((prev) => [event, ...prev]),
|
||||
});
|
||||
}
|
||||
|
||||
fetchRepliesAndSub();
|
||||
|
||||
return () => {
|
||||
isCancelled = true;
|
||||
if (sub) sub.stop();
|
||||
};
|
||||
}, [eventId]);
|
||||
|
||||
@@ -59,7 +63,7 @@ export function ReplyList({ eventId }: { eventId: string }) {
|
||||
</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>
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { NDKEvent, NDKKind, NostrEvent } from '@nostr-dev-kit/ndk';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import {
|
||||
MemoizedArticleKind,
|
||||
@@ -14,24 +14,22 @@ import {
|
||||
import { User } from '@shared/user';
|
||||
|
||||
export function Repost({ event }: { event: NDKEvent }) {
|
||||
const { ndk } = useNDK();
|
||||
const { status, data: repostEvent } = useQuery({
|
||||
const { ark } = useArk();
|
||||
const {
|
||||
isLoading,
|
||||
isError,
|
||||
data: repostEvent,
|
||||
} = useQuery({
|
||||
queryKey: ['repost', event.id],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
if (event.content.length > 50) {
|
||||
const embed = JSON.parse(event.content) as NostrEvent;
|
||||
const embedEvent = new NDKEvent(ndk, embed);
|
||||
return embedEvent;
|
||||
return ark.createNDKEvent({ event: embed });
|
||||
}
|
||||
|
||||
const id = event.tags.find((el) => el[0] === 'e')[1];
|
||||
if (!id) throw new Error('Failed to get repost event id');
|
||||
|
||||
const ndkEvent = await ndk.fetchEvent(id);
|
||||
if (!ndkEvent) return Promise.reject(new Error('Failed to get repost event'));
|
||||
|
||||
return ndkEvent;
|
||||
return await ark.getEventById({ id });
|
||||
} catch {
|
||||
throw new Error('Failed to get repost event');
|
||||
}
|
||||
@@ -53,7 +51,7 @@ export function Repost({ event }: { event: NDKEvent }) {
|
||||
}
|
||||
};
|
||||
|
||||
if (status === 'pending') {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="w-full px-3 pb-3">
|
||||
<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 (
|
||||
<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">
|
||||
|
||||
@@ -2,20 +2,21 @@ import { NDKEvent } from '@nostr-dev-kit/ndk';
|
||||
import { memo } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { ChildNote, NoteActions } from '@shared/notes';
|
||||
import { User } from '@shared/user';
|
||||
|
||||
import { WIDGET_KIND } from '@utils/constants';
|
||||
import { useNostr } from '@utils/hooks/useNostr';
|
||||
import { useRichContent } from '@utils/hooks/useRichContent';
|
||||
import { useWidget } from '@utils/hooks/useWidget';
|
||||
|
||||
export function TextNote({ event, className }: { event: NDKEvent; className?: string }) {
|
||||
const { parsedContent } = useRichContent(event.content);
|
||||
const { addWidget } = useWidget();
|
||||
const { getEventThread } = useNostr();
|
||||
const { ark } = useArk();
|
||||
|
||||
const thread = getEventThread(event.tags);
|
||||
const thread = ark.getEventThread({ tags: event.tags });
|
||||
|
||||
return (
|
||||
<div className={twMerge('mb-3 h-min w-full px-3', className)}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { CancelIcon } from '@shared/icons';
|
||||
import { User } from '@shared/user';
|
||||
@@ -14,7 +14,7 @@ export function TitleBar({
|
||||
title?: string;
|
||||
isLive?: boolean;
|
||||
}) {
|
||||
const { db } = useStorage();
|
||||
const { ark } = useArk();
|
||||
const { removeWidget } = useWidget();
|
||||
|
||||
return (
|
||||
@@ -33,13 +33,13 @@ export function TitleBar({
|
||||
<div className="col-span-1 flex justify-center">
|
||||
{id === '9999' ? (
|
||||
<div className="isolate flex -space-x-2">
|
||||
{db.account.contacts
|
||||
{ark.account.contacts
|
||||
?.slice(0, 8)
|
||||
.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">
|
||||
<span className="text-[8px] font-medium">
|
||||
+{db.account.contacts?.length - 8}
|
||||
+{ark.account.contacts?.length - 8}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { NDKEvent, NDKKind, NDKUser } from '@nostr-dev-kit/ndk';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { NIP05 } from '@shared/nip05';
|
||||
|
||||
@@ -12,18 +10,14 @@ import { useProfile } from '@utils/hooks/useProfile';
|
||||
import { displayNpub } from '@utils/shortenKey';
|
||||
|
||||
export function UserProfile({ pubkey }: { pubkey: string }) {
|
||||
const { db } = useStorage();
|
||||
const { ndk } = useNDK();
|
||||
const { ark } = useArk();
|
||||
const { user } = useProfile(pubkey);
|
||||
|
||||
const [followed, setFollowed] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
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);
|
||||
const add = await ark.createContact({ pubkey });
|
||||
|
||||
if (add) {
|
||||
setFollowed(true);
|
||||
@@ -37,22 +31,9 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
|
||||
|
||||
const unfollow = async (pubkey: string) => {
|
||||
try {
|
||||
if (!ndk.signer) return navigate('/new/privkey');
|
||||
const remove = await ark.deleteContact({ pubkey });
|
||||
|
||||
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) {
|
||||
if (remove) {
|
||||
setFollowed(false);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -61,7 +42,7 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (db.account.contacts.includes(pubkey)) {
|
||||
if (ark.account.contacts.includes(pubkey)) {
|
||||
setFollowed(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -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 { FetchFilter } from 'nostr-fetch';
|
||||
import { useMemo } from 'react';
|
||||
import { VList } from 'virtua';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
|
||||
import { MemoizedArticleNote } from '@shared/notes';
|
||||
@@ -16,8 +15,7 @@ import { FETCH_LIMIT } from '@utils/constants';
|
||||
import { Widget } from '@utils/types';
|
||||
|
||||
export function ArticleWidget({ widget }: { widget: Widget }) {
|
||||
const { db } = useStorage();
|
||||
const { ndk, relayUrls, fetcher } = useNDK();
|
||||
const { ark } = useArk();
|
||||
const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
|
||||
useInfiniteQuery({
|
||||
queryKey: ['article', widget.id],
|
||||
@@ -39,20 +37,19 @@ export function ArticleWidget({ widget }: { widget: Widget }) {
|
||||
} else {
|
||||
filter = {
|
||||
kinds: [NDKKind.Article],
|
||||
authors: db.account.contacts,
|
||||
authors: ark.account.contacts,
|
||||
};
|
||||
}
|
||||
|
||||
const events = await fetcher.fetchLatestEvents(relayUrls, filter, FETCH_LIMIT, {
|
||||
asOf: pageParam === 0 ? undefined : pageParam,
|
||||
abortSignal: signal,
|
||||
const events = await ark.getInfiniteEvents({
|
||||
filter,
|
||||
limit: FETCH_LIMIT,
|
||||
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) => {
|
||||
const lastEvent = lastPage.at(-1);
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
||||
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||
import { FetchFilter } from 'nostr-fetch';
|
||||
import { useMemo } from 'react';
|
||||
import { VList } from 'virtua';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
|
||||
import { MemoizedFileNote } from '@shared/notes';
|
||||
@@ -16,8 +14,7 @@ import { FETCH_LIMIT } from '@utils/constants';
|
||||
import { Widget } from '@utils/types';
|
||||
|
||||
export function FileWidget({ widget }: { widget: Widget }) {
|
||||
const { db } = useStorage();
|
||||
const { ndk, relayUrls, fetcher } = useNDK();
|
||||
const { ark } = useArk();
|
||||
const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
|
||||
useInfiniteQuery({
|
||||
queryKey: ['media', widget.id],
|
||||
@@ -39,20 +36,19 @@ export function FileWidget({ widget }: { widget: Widget }) {
|
||||
} else {
|
||||
filter = {
|
||||
kinds: [1063],
|
||||
authors: db.account.contacts,
|
||||
authors: ark.account.contacts,
|
||||
};
|
||||
}
|
||||
|
||||
const events = await fetcher.fetchLatestEvents(relayUrls, filter, FETCH_LIMIT, {
|
||||
asOf: pageParam === 0 ? undefined : pageParam,
|
||||
abortSignal: signal,
|
||||
const events = await ark.getInfiniteEvents({
|
||||
filter,
|
||||
limit: FETCH_LIMIT,
|
||||
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) => {
|
||||
const lastEvent = lastPage.at(-1);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useInfiniteQuery } from '@tanstack/react-query';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { VList } from 'virtua';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
|
||||
import {
|
||||
@@ -19,7 +19,7 @@ import { FETCH_LIMIT } from '@utils/constants';
|
||||
import { Widget } from '@utils/types';
|
||||
|
||||
export function GroupWidget({ widget }: { widget: Widget }) {
|
||||
const { relayUrls, ndk, fetcher } = useNDK();
|
||||
const { ark } = useArk();
|
||||
const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
|
||||
useInfiniteQuery({
|
||||
queryKey: ['groupfeeds', widget.id],
|
||||
@@ -32,21 +32,18 @@ export function GroupWidget({ widget }: { widget: Widget }) {
|
||||
pageParam: number;
|
||||
}) => {
|
||||
const authors = JSON.parse(widget.content);
|
||||
const events = await fetcher.fetchLatestEvents(
|
||||
relayUrls,
|
||||
{
|
||||
const events = await ark.getInfiniteEvents({
|
||||
filter: {
|
||||
kinds: [NDKKind.Text, NDKKind.Repost],
|
||||
authors: authors,
|
||||
},
|
||||
FETCH_LIMIT,
|
||||
{ asOf: pageParam === 0 ? undefined : pageParam, abortSignal: signal }
|
||||
);
|
||||
|
||||
const ndkEvents = events.map((event) => {
|
||||
return new NDKEvent(ndk, event);
|
||||
limit: FETCH_LIMIT,
|
||||
pageParam,
|
||||
signal,
|
||||
dedup: false,
|
||||
});
|
||||
|
||||
return ndkEvents.sort((a, b) => b.created_at - a.created_at);
|
||||
return events;
|
||||
},
|
||||
getNextPageParam: (lastPage) => {
|
||||
const lastEvent = lastPage.at(-1);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useInfiniteQuery } from '@tanstack/react-query';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { VList } from 'virtua';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
|
||||
import { MemoizedRepost, MemoizedTextNote, UnknownNote } from '@shared/notes';
|
||||
@@ -14,7 +14,7 @@ import { FETCH_LIMIT } from '@utils/constants';
|
||||
import { Widget } from '@utils/types';
|
||||
|
||||
export function HashtagWidget({ widget }: { widget: Widget }) {
|
||||
const { ndk, relayUrls, fetcher } = useNDK();
|
||||
const { ark } = useArk();
|
||||
const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
|
||||
useInfiniteQuery({
|
||||
queryKey: ['hashtag', widget.id],
|
||||
@@ -26,21 +26,18 @@ export function HashtagWidget({ widget }: { widget: Widget }) {
|
||||
signal: AbortSignal;
|
||||
pageParam: number;
|
||||
}) => {
|
||||
const events = await fetcher.fetchLatestEvents(
|
||||
relayUrls,
|
||||
{
|
||||
const events = await ark.getInfiniteEvents({
|
||||
filter: {
|
||||
kinds: [NDKKind.Text, NDKKind.Repost],
|
||||
'#t': [widget.content],
|
||||
},
|
||||
FETCH_LIMIT,
|
||||
{ asOf: pageParam === 0 ? undefined : pageParam, abortSignal: signal }
|
||||
);
|
||||
|
||||
const ndkEvents = events.map((event) => {
|
||||
return new NDKEvent(ndk, event);
|
||||
limit: FETCH_LIMIT,
|
||||
pageParam,
|
||||
signal,
|
||||
dedup: false,
|
||||
});
|
||||
|
||||
return ndkEvents.sort((a, b) => b.created_at - a.created_at);
|
||||
return events;
|
||||
},
|
||||
getNextPageParam: (lastPage) => {
|
||||
const lastEvent = lastPage.at(-1);
|
||||
|
||||
@@ -3,8 +3,7 @@ import { useInfiniteQuery } from '@tanstack/react-query';
|
||||
import { useCallback, useMemo, useRef } from 'react';
|
||||
import { VList, VListHandle } from 'virtua';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
|
||||
import {
|
||||
@@ -19,8 +18,7 @@ import { LiveUpdater, WidgetWrapper } from '@shared/widgets';
|
||||
import { FETCH_LIMIT } from '@utils/constants';
|
||||
|
||||
export function NewsfeedWidget() {
|
||||
const { db } = useStorage();
|
||||
const { relayUrls, ndk, fetcher } = useNDK();
|
||||
const { ark } = useArk();
|
||||
const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
|
||||
useInfiniteQuery({
|
||||
queryKey: ['newsfeed'],
|
||||
@@ -32,35 +30,19 @@ export function NewsfeedWidget() {
|
||||
signal: AbortSignal;
|
||||
pageParam: number;
|
||||
}) => {
|
||||
const rootIds = new Set();
|
||||
const dedupQueue = new Set();
|
||||
|
||||
const events = await fetcher.fetchLatestEvents(
|
||||
relayUrls,
|
||||
{
|
||||
const events = await ark.getInfiniteEvents({
|
||||
filter: {
|
||||
kinds: [NDKKind.Text, NDKKind.Repost],
|
||||
authors: db.account.contacts,
|
||||
authors: !ark.account.contacts.length
|
||||
? [ark.account.pubkey]
|
||||
: ark.account.contacts,
|
||||
},
|
||||
FETCH_LIMIT,
|
||||
{ asOf: pageParam === 0 ? undefined : pageParam, abortSignal: signal }
|
||||
);
|
||||
|
||||
const ndkEvents = events.map((event) => {
|
||||
return new NDKEvent(ndk, event);
|
||||
limit: FETCH_LIMIT,
|
||||
pageParam,
|
||||
signal,
|
||||
});
|
||||
|
||||
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 events;
|
||||
},
|
||||
getNextPageParam: (lastPage) => {
|
||||
const lastEvent = lastPage.at(-1);
|
||||
|
||||
@@ -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 { useCallback, useEffect, useMemo } from 'react';
|
||||
import { VList } from 'virtua';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
|
||||
import { MemoizedNotifyNote, NoteSkeleton } from '@shared/notes';
|
||||
@@ -12,15 +11,12 @@ import { TitleBar } from '@shared/titleBar';
|
||||
import { WidgetWrapper } from '@shared/widgets';
|
||||
|
||||
import { FETCH_LIMIT } from '@utils/constants';
|
||||
import { useNostr } from '@utils/hooks/useNostr';
|
||||
import { sendNativeNotification } from '@utils/notification';
|
||||
|
||||
export function NotificationWidget() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { db } = useStorage();
|
||||
const { sub } = useNostr();
|
||||
const { ndk, relayUrls, fetcher } = useNDK();
|
||||
const { ark } = useArk();
|
||||
const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
|
||||
useInfiniteQuery({
|
||||
queryKey: ['notification'],
|
||||
@@ -32,21 +28,17 @@ export function NotificationWidget() {
|
||||
signal: AbortSignal;
|
||||
pageParam: number;
|
||||
}) => {
|
||||
const events = await fetcher.fetchLatestEvents(
|
||||
relayUrls,
|
||||
{
|
||||
const events = await ark.getInfiniteEvents({
|
||||
filter: {
|
||||
kinds: [NDKKind.Text, NDKKind.Repost, NDKKind.Reaction, NDKKind.Zap],
|
||||
'#p': [db.account.pubkey],
|
||||
'#p': [ark.account.pubkey],
|
||||
},
|
||||
FETCH_LIMIT,
|
||||
{ asOf: pageParam === 0 ? undefined : pageParam, abortSignal: signal }
|
||||
);
|
||||
|
||||
const ndkEvents = events.map((event) => {
|
||||
return new NDKEvent(ndk, event);
|
||||
limit: FETCH_LIMIT,
|
||||
pageParam,
|
||||
signal,
|
||||
});
|
||||
|
||||
return ndkEvents.sort((a, b) => b.created_at - a.created_at);
|
||||
return events;
|
||||
},
|
||||
getNextPageParam: (lastPage) => {
|
||||
const lastEvent = lastPage.at(-1);
|
||||
@@ -65,21 +57,24 @@ export function NotificationWidget() {
|
||||
);
|
||||
|
||||
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} />;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (status === 'success' && db.account) {
|
||||
let sub: NDKSubscription = undefined;
|
||||
|
||||
if (status === 'success' && ark.account) {
|
||||
const filter = {
|
||||
kinds: [NDKKind.Text, NDKKind.Repost, NDKKind.Reaction, NDKKind.Zap],
|
||||
'#p': [db.account.pubkey],
|
||||
'#p': [ark.account.pubkey],
|
||||
since: Math.floor(Date.now() / 1000),
|
||||
};
|
||||
|
||||
sub(
|
||||
sub = ark.subscribe({
|
||||
filter,
|
||||
async (event) => {
|
||||
closeOnEose: false,
|
||||
cb: async (event) => {
|
||||
queryClient.setQueryData(
|
||||
['notification'],
|
||||
(prev: { pageParams: number; pages: Array<NDKEvent[]> }) => ({
|
||||
@@ -88,21 +83,18 @@ export function NotificationWidget() {
|
||||
})
|
||||
);
|
||||
|
||||
const user = ndk.getUser({ pubkey: event.pubkey });
|
||||
await user.fetchProfile();
|
||||
const profile = await ark.getUserProfile({ pubkey: event.pubkey });
|
||||
|
||||
switch (event.kind) {
|
||||
case NDKKind.Text:
|
||||
return await sendNativeNotification(
|
||||
`${
|
||||
user.profile.displayName || user.profile.name
|
||||
} has replied to your note`
|
||||
`${profile.displayName || profile.name} has replied to your note`
|
||||
);
|
||||
case NDKKind.EncryptedDirectMessage: {
|
||||
if (location.pathname !== '/chats') {
|
||||
return await sendNativeNotification(
|
||||
`${
|
||||
user.profile.displayName || user.profile.name
|
||||
profile.displayName || profile.name
|
||||
} has send you a encrypted message`
|
||||
);
|
||||
} else {
|
||||
@@ -111,28 +103,28 @@ export function NotificationWidget() {
|
||||
}
|
||||
case NDKKind.Repost:
|
||||
return await sendNativeNotification(
|
||||
`${
|
||||
user.profile.displayName || user.profile.name
|
||||
} has reposted to your note`
|
||||
`${profile.displayName || profile.name} has reposted to your note`
|
||||
);
|
||||
case NDKKind.Reaction:
|
||||
return await sendNativeNotification(
|
||||
`${user.profile.displayName || user.profile.name} has reacted ${
|
||||
`${profile.displayName || profile.name} has reacted ${
|
||||
event.content
|
||||
} to your note`
|
||||
);
|
||||
case NDKKind.Zap:
|
||||
return await sendNativeNotification(
|
||||
`${user.profile.displayName || user.profile.name} has zapped to your note`
|
||||
`${profile.displayName || profile.name} has zapped to your note`
|
||||
);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
false,
|
||||
'notification'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (sub) sub.stop();
|
||||
};
|
||||
}, [status]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as Dialog from '@radix-ui/react-dialog';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import {
|
||||
ArrowRightCircleIcon,
|
||||
@@ -16,7 +16,7 @@ import { WIDGET_KIND } from '@utils/constants';
|
||||
import { useWidget } from '@utils/hooks/useWidget';
|
||||
|
||||
export function AddGroupFeeds({ currentWidgetId }: { currentWidgetId: string }) {
|
||||
const { db } = useStorage();
|
||||
const { ark } = useArk();
|
||||
const { replaceWidget } = useWidget();
|
||||
|
||||
const [title, setTitle] = useState<string>('');
|
||||
@@ -95,7 +95,7 @@ export function AddGroupFeeds({ currentWidgetId }: { currentWidgetId: string })
|
||||
Users
|
||||
</span>
|
||||
<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
|
||||
key={item}
|
||||
type="button"
|
||||
|
||||
@@ -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 { useEffect, useState } from 'react';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { ChevronUpIcon } from '@shared/icons';
|
||||
|
||||
export function LiveUpdater({ status }: { status: QueryStatus }) {
|
||||
const { db } = useStorage();
|
||||
const { ndk } = useNDK();
|
||||
|
||||
const { ark } = useArk();
|
||||
const [events, setEvents] = useState<NDKEvent[]>([]);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -30,17 +27,16 @@ export function LiveUpdater({ status }: { status: QueryStatus }) {
|
||||
useEffect(() => {
|
||||
let sub: NDKSubscription = undefined;
|
||||
|
||||
if (status === 'success' && db.account && db.account?.contacts?.length > 0) {
|
||||
const filter: NDKFilter = {
|
||||
kinds: [NDKKind.Text, NDKKind.Repost],
|
||||
authors: db.account.contacts,
|
||||
since: Math.floor(Date.now() / 1000),
|
||||
};
|
||||
|
||||
sub = ndk.subscribe(filter, { closeOnEose: false, groupable: false });
|
||||
sub.addListener('event', (event: NDKEvent) =>
|
||||
setEvents((prev) => [...prev, event])
|
||||
);
|
||||
if (status === 'success' && ark.account && ark.account?.contacts?.length > 0) {
|
||||
sub = ark.subscribe({
|
||||
filter: {
|
||||
kinds: [NDKKind.Text, NDKKind.Repost],
|
||||
authors: ark.account.contacts,
|
||||
since: Math.floor(Date.now() / 1000),
|
||||
},
|
||||
closeOnEose: false,
|
||||
cb: (event: NDKEvent) => setEvents((prev) => [...prev, event]),
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { NDKUser } from '@nostr-dev-kit/ndk';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { FollowIcon } from '@shared/icons';
|
||||
|
||||
@@ -16,9 +14,7 @@ export interface Profile {
|
||||
}
|
||||
|
||||
export function NostrBandUserProfile({ data }: { data: Profile }) {
|
||||
const { db } = useStorage();
|
||||
const { ndk } = useNDK();
|
||||
|
||||
const { ark } = useArk();
|
||||
const [followed, setFollowed] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -26,12 +22,10 @@ export function NostrBandUserProfile({ data }: { data: Profile }) {
|
||||
|
||||
const follow = async (pubkey: string) => {
|
||||
try {
|
||||
if (!ndk.signer) return navigate('/new/privkey');
|
||||
if (!ark.readyToSign) return navigate('/new/privkey');
|
||||
setFollowed(true);
|
||||
|
||||
const user = ndk.getUser({ pubkey: db.account.pubkey });
|
||||
const contacts = await user.follows();
|
||||
const add = await user.follow(new NDKUser({ pubkey: pubkey }), contacts);
|
||||
const add = ark.createContact({ pubkey });
|
||||
|
||||
if (!add) {
|
||||
toast.success('You already follow this user');
|
||||
@@ -44,7 +38,7 @@ export function NostrBandUserProfile({ data }: { data: Profile }) {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (db.account.contacts.includes(data.pubkey)) {
|
||||
if (ark.account.contacts.includes(data.pubkey)) {
|
||||
setFollowed(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -2,6 +2,8 @@ import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
|
||||
import { useCallback } from 'react';
|
||||
import { WVList } from 'virtua';
|
||||
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { LoaderIcon } from '@shared/icons';
|
||||
import {
|
||||
ChildNote,
|
||||
@@ -17,16 +19,15 @@ import { User } from '@shared/user';
|
||||
import { WidgetWrapper } from '@shared/widgets';
|
||||
|
||||
import { useEvent } from '@utils/hooks/useEvent';
|
||||
import { useNostr } from '@utils/hooks/useNostr';
|
||||
import { Widget } from '@utils/types';
|
||||
|
||||
export function ThreadWidget({ widget }: { widget: Widget }) {
|
||||
const { isFetching, isError, data } = useEvent(widget.content);
|
||||
const { getEventThread } = useNostr();
|
||||
const { ark } = useArk();
|
||||
|
||||
const renderKind = useCallback(
|
||||
(event: NDKEvent) => {
|
||||
const thread = getEventThread(event.tags);
|
||||
const thread = ark.getEventThread({ tags: event.tags });
|
||||
switch (event.kind) {
|
||||
case NDKKind.Text:
|
||||
return (
|
||||
|
||||
@@ -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 { FetchFilter } from 'nostr-fetch';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { VList } from 'virtua';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
|
||||
import {
|
||||
@@ -20,7 +19,7 @@ import { FETCH_LIMIT } from '@utils/constants';
|
||||
import { Widget } from '@utils/types';
|
||||
|
||||
export function TopicWidget({ widget }: { widget: Widget }) {
|
||||
const { relayUrls, ndk, fetcher } = useNDK();
|
||||
const { ark } = useArk();
|
||||
const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
|
||||
useInfiniteQuery({
|
||||
queryKey: ['topic', widget.id],
|
||||
@@ -33,35 +32,19 @@ export function TopicWidget({ widget }: { widget: Widget }) {
|
||||
pageParam: number;
|
||||
}) => {
|
||||
const hashtags: string[] = JSON.parse(widget.content as string);
|
||||
const filter: FetchFilter = {
|
||||
const filter: NDKFilter = {
|
||||
kinds: [NDKKind.Text, NDKKind.Repost],
|
||||
'#t': hashtags.map((tag) => tag.replace('#', '')),
|
||||
};
|
||||
|
||||
const rootIds = new Set();
|
||||
const dedupQueue = new Set();
|
||||
|
||||
const events = await fetcher.fetchLatestEvents(relayUrls, filter, FETCH_LIMIT, {
|
||||
asOf: pageParam === 0 ? undefined : pageParam,
|
||||
abortSignal: signal,
|
||||
const events = await ark.getInfiniteEvents({
|
||||
filter,
|
||||
limit: FETCH_LIMIT,
|
||||
pageParam,
|
||||
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);
|
||||
return events;
|
||||
},
|
||||
getNextPageParam: (lastPage) => {
|
||||
const lastEvent = lastPage.at(-1);
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useCallback } from 'react';
|
||||
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { WVList } from 'virtua';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
|
||||
import {
|
||||
MemoizedRepost,
|
||||
MemoizedTextNote,
|
||||
@@ -15,43 +16,46 @@ import { TitleBar } from '@shared/titleBar';
|
||||
import { UserProfile } from '@shared/userProfile';
|
||||
import { WidgetWrapper } from '@shared/widgets';
|
||||
|
||||
import { nHoursAgo } from '@utils/date';
|
||||
import { FETCH_LIMIT } from '@utils/constants';
|
||||
import { Widget } from '@utils/types';
|
||||
|
||||
export function UserWidget({ widget }: { widget: Widget }) {
|
||||
const { ndk } = useNDK();
|
||||
const { status, data } = useQuery({
|
||||
queryKey: ['user-posts', widget.id],
|
||||
queryFn: async () => {
|
||||
const rootIds = new Set();
|
||||
const dedupQueue = new Set();
|
||||
const { ark } = useArk();
|
||||
const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
|
||||
useInfiniteQuery({
|
||||
queryKey: ['user-posts', widget.content],
|
||||
initialPageParam: 0,
|
||||
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({
|
||||
kinds: [NDKKind.Text, NDKKind.Repost],
|
||||
authors: [widget.content],
|
||||
since: nHoursAgo(24),
|
||||
});
|
||||
return events;
|
||||
},
|
||||
getNextPageParam: (lastPage) => {
|
||||
const lastEvent = lastPage.at(-1);
|
||||
if (!lastEvent) return;
|
||||
return lastEvent.created_at - 1;
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
const ndkEvents = [...events];
|
||||
|
||||
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);
|
||||
},
|
||||
staleTime: Infinity,
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
const allEvents = useMemo(
|
||||
() => (data ? data.pages.flatMap((page) => page) : []),
|
||||
[data]
|
||||
);
|
||||
|
||||
// render event match event kind
|
||||
const renderItem = useCallback(
|
||||
@@ -86,19 +90,28 @@ export function UserWidget({ widget }: { widget: Widget }) {
|
||||
<NoteSkeleton />
|
||||
</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>
|
||||
</WVList>
|
||||
|
||||
@@ -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 { nip19 } from 'nostr-tools';
|
||||
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) {
|
||||
const { ndk } = useNDK();
|
||||
const { ark } = useArk();
|
||||
const { status, isFetching, isError, data } = useQuery({
|
||||
queryKey: ['event', id],
|
||||
queryFn: async () => {
|
||||
let event: NDKEvent = undefined;
|
||||
|
||||
const naddr = id.startsWith('naddr')
|
||||
? (nip19.decode(id).data as AddressPointer)
|
||||
: null;
|
||||
|
||||
// return event refer from naddr
|
||||
if (naddr) {
|
||||
const rEvents = await ndk.fetchEvents({
|
||||
kinds: [naddr.kind],
|
||||
'#d': [naddr.identifier],
|
||||
authors: [naddr.pubkey],
|
||||
const events = await ark.getAllEvents({
|
||||
filter: {
|
||||
kinds: [naddr.kind],
|
||||
'#d': [naddr.identifier],
|
||||
authors: [naddr.pubkey],
|
||||
},
|
||||
});
|
||||
|
||||
const rEvent = [...rEvents].slice(-1)[0];
|
||||
|
||||
if (!rEvent) throw new Error('event not found');
|
||||
return rEvent;
|
||||
event = events.slice(-1)[0];
|
||||
}
|
||||
|
||||
// return embed event (nostr.band api)
|
||||
if (embed) {
|
||||
const embedEvent: NostrEvent = JSON.parse(embed);
|
||||
const ndkEvent = new NDKEvent(ndk, embedEvent);
|
||||
|
||||
return ndkEvent;
|
||||
event = ark.createNDKEvent({ event: embedEvent });
|
||||
}
|
||||
|
||||
// get event from relay
|
||||
const event = await ndk.fetchEvent(id, {
|
||||
cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST,
|
||||
});
|
||||
event = await ark.getEventById({ id });
|
||||
|
||||
if (!event)
|
||||
throw new Error(`Cannot get event with ${id}, will be retry after 10 seconds`);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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 { nip19 } from 'nostr-tools';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
export function useProfile(pubkey: string, embed?: string) {
|
||||
const { ndk } = useNDK();
|
||||
const { ark } = useArk();
|
||||
const {
|
||||
isLoading,
|
||||
isError,
|
||||
@@ -29,10 +29,7 @@ export function useProfile(pubkey: string, embed?: string) {
|
||||
if (decoded.type === 'npub') hexstring = decoded.data;
|
||||
}
|
||||
|
||||
const user = ndk.getUser({ pubkey: hexstring });
|
||||
const profile = await user.fetchProfile({
|
||||
cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST,
|
||||
});
|
||||
const profile = await ark.getUserProfile({ pubkey: hexstring });
|
||||
|
||||
if (!profile)
|
||||
throw new Error(
|
||||
|
||||
@@ -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 { useNDK } from '@libs/ndk/provider';
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
export function useRelay() {
|
||||
const { db } = useStorage();
|
||||
const { ndk } = useNDK();
|
||||
|
||||
const { ark } = useArk();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const connectRelay = useMutation({
|
||||
mutationFn: async (relay: NDKRelayUrl, purpose?: 'read' | 'write' | undefined) => {
|
||||
// Cancel any outgoing refetches
|
||||
await queryClient.cancelQueries({ queryKey: ['relays', db.account.pubkey] });
|
||||
await queryClient.cancelQueries({ queryKey: ['relays', ark.account.pubkey] });
|
||||
|
||||
// Snapshot the previous value
|
||||
const prevRelays: NDKTag[] = queryClient.getQueryData([
|
||||
'relays',
|
||||
db.account.pubkey,
|
||||
ark.account.pubkey,
|
||||
]);
|
||||
|
||||
// create new relay list if not exist
|
||||
if (!prevRelays) {
|
||||
const newListEvent = new NDKEvent(ndk);
|
||||
newListEvent.kind = NDKKind.RelayList;
|
||||
newListEvent.tags = [['r', relay, purpose ?? '']];
|
||||
await newListEvent.publish();
|
||||
await ark.createEvent({
|
||||
kind: NDKKind.RelayList,
|
||||
tags: [['r', relay, purpose ?? '']],
|
||||
publish: true,
|
||||
});
|
||||
}
|
||||
|
||||
// add relay to exist list
|
||||
const index = prevRelays.findIndex((el) => el[1] === relay);
|
||||
if (index > -1) return;
|
||||
|
||||
const event = new NDKEvent(ndk);
|
||||
event.kind = NDKKind.RelayList;
|
||||
event.tags = [...prevRelays, ['r', relay, purpose ?? '']];
|
||||
|
||||
await event.publish();
|
||||
await ark.createEvent({
|
||||
kind: NDKKind.RelayList,
|
||||
tags: [...prevRelays, ['r', relay, purpose ?? '']],
|
||||
publish: true,
|
||||
});
|
||||
|
||||
// Optimistically update to the new value
|
||||
queryClient.setQueryData(['relays', db.account.pubkey], (prev: NDKTag[]) => [
|
||||
queryClient.setQueryData(['relays', ark.account.pubkey], (prev: NDKTag[]) => [
|
||||
...prev,
|
||||
['r', relay, purpose ?? ''],
|
||||
]);
|
||||
@@ -49,19 +47,19 @@ export function useRelay() {
|
||||
return { prevRelays };
|
||||
},
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['relays', db.account.pubkey] });
|
||||
queryClient.invalidateQueries({ queryKey: ['relays', ark.account.pubkey] });
|
||||
},
|
||||
});
|
||||
|
||||
const removeRelay = useMutation({
|
||||
mutationFn: async (relay: NDKRelayUrl) => {
|
||||
// Cancel any outgoing refetches
|
||||
await queryClient.cancelQueries({ queryKey: ['relays', db.account.pubkey] });
|
||||
await queryClient.cancelQueries({ queryKey: ['relays', ark.account.pubkey] });
|
||||
|
||||
// Snapshot the previous value
|
||||
const prevRelays: NDKTag[] = queryClient.getQueryData([
|
||||
'relays',
|
||||
db.account.pubkey,
|
||||
ark.account.pubkey,
|
||||
]);
|
||||
|
||||
if (!prevRelays) return;
|
||||
@@ -69,19 +67,20 @@ export function useRelay() {
|
||||
const index = prevRelays.findIndex((el) => el[1] === relay);
|
||||
if (index > -1) prevRelays.splice(index, 1);
|
||||
|
||||
const event = new NDKEvent(ndk);
|
||||
event.kind = NDKKind.RelayList;
|
||||
event.tags = prevRelays;
|
||||
await event.publish();
|
||||
await ark.createEvent({
|
||||
kind: NDKKind.RelayList,
|
||||
tags: prevRelays,
|
||||
publish: true,
|
||||
});
|
||||
|
||||
// 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 { prevRelays };
|
||||
},
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['relays', db.account.pubkey] });
|
||||
queryClient.invalidateQueries({ queryKey: ['relays', ark.account.pubkey] });
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ReactNode } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import reactStringReplace from 'react-string-replace';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import {
|
||||
Hashtag,
|
||||
@@ -23,9 +23,19 @@ const NOSTR_MENTIONS = [
|
||||
'npub1',
|
||||
'nprofile1',
|
||||
'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'];
|
||||
|
||||
@@ -46,7 +56,7 @@ const VIDEOS = [
|
||||
];
|
||||
|
||||
export function useRichContent(content: string, textmode: boolean = false) {
|
||||
const { db } = useStorage();
|
||||
const { ark } = useArk();
|
||||
|
||||
let parsedContent: string | ReactNode[] = content.replace(/\n+/g, '\n');
|
||||
let linkPreview: string;
|
||||
@@ -58,7 +68,7 @@ export function useRichContent(content: string, textmode: boolean = false) {
|
||||
const words = text.split(/( |\n)/);
|
||||
|
||||
if (!textmode) {
|
||||
if (db.settings.media) {
|
||||
if (ark.settings.media) {
|
||||
images = words.filter((word) => IMAGES.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) {
|
||||
hashtags.forEach((hashtag) => {
|
||||
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;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,14 +4,14 @@ import tippy from 'tippy.js';
|
||||
|
||||
import { MentionList } from '@app/new/components';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
export function useSuggestion() {
|
||||
const { db } = useStorage();
|
||||
const { ark } = useArk();
|
||||
|
||||
const suggestion: MentionOptions['suggestion'] = {
|
||||
items: async ({ query }) => {
|
||||
const users = await db.getAllCacheUsers();
|
||||
const users = await ark.getAllCacheUsers();
|
||||
return users
|
||||
.filter((item) => {
|
||||
if (item.name) return item.name.toLowerCase().startsWith(query.toLowerCase());
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { Widget } from '@utils/types';
|
||||
|
||||
export function useWidget() {
|
||||
const { db } = useStorage();
|
||||
const { ark } = useArk();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const addWidget = useMutation({
|
||||
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) => {
|
||||
queryClient.setQueryData(['widgets'], (old: Widget[]) => [...old, data]);
|
||||
@@ -26,8 +26,8 @@ export function useWidget() {
|
||||
const prevWidgets = queryClient.getQueryData(['widgets']);
|
||||
|
||||
// create new widget
|
||||
await db.removeWidget(currentId);
|
||||
const newWidget = await db.createWidget(widget.kind, widget.title, widget.content);
|
||||
await ark.removeWidget(currentId);
|
||||
const newWidget = await ark.createWidget(widget.kind, widget.title, widget.content);
|
||||
|
||||
// Optimistically update to the new value
|
||||
queryClient.setQueryData(['widgets'], (prev: Widget[]) => [
|
||||
@@ -57,7 +57,7 @@ export function useWidget() {
|
||||
);
|
||||
|
||||
// Update in database
|
||||
await db.removeWidget(id);
|
||||
await ark.removeWidget(id);
|
||||
|
||||
// Return a context object with the snapshotted value
|
||||
return { prevWidgets };
|
||||
|
||||
@@ -6,7 +6,7 @@ export function shortenKey(pubkey: 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;
|
||||
|
||||
separator = separator || ' ... ';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user