From 0e19c1a8f3406174dc9da5bf6a1aff8e2c0b1a07 Mon Sep 17 00:00:00 2001 From: kieran Date: Tue, 7 Jan 2025 11:23:34 +0000 Subject: [PATCH] wip notedeck --- Cargo.lock | 908 +++------------------------- Cargo.toml | 26 +- src/android.rs | 67 +- src/app.rs | 209 ++++--- src/bin/zap_stream_app.rs | 104 +--- src/lib.rs | 4 +- src/link.rs | 2 +- src/login.rs | 89 --- src/note_ref.rs | 43 ++ src/note_util.rs | 5 +- src/{note_store.rs => note_view.rs} | 4 +- src/route/home.rs | 93 ++- src/route/login.rs | 59 +- src/route/mod.rs | 290 +++++---- src/route/stream.rs | 78 ++- src/services/ffmpeg_loader.rs | 2 +- src/services/image_cache.rs | 186 ------ src/services/mod.rs | 6 +- src/services/ndb_wrapper.rs | 161 ----- src/services/query.rs | 192 ------ src/widgets/avatar.rs | 59 +- src/widgets/chat.rs | 85 ++- src/widgets/chat_message.rs | 31 +- src/widgets/header.rs | 19 +- src/widgets/mod.rs | 39 +- src/widgets/profile.rs | 35 +- src/widgets/stream_list.rs | 27 +- src/widgets/stream_tile.rs | 28 +- src/widgets/stream_title.rs | 14 +- src/widgets/text_input.rs | 14 +- src/widgets/write_chat.rs | 19 +- 31 files changed, 725 insertions(+), 2173 deletions(-) delete mode 100644 src/login.rs create mode 100644 src/note_ref.rs rename src/{note_store.rs => note_view.rs} (95%) delete mode 100644 src/services/image_cache.rs delete mode 100644 src/services/ndb_wrapper.rs delete mode 100644 src/services/query.rs diff --git a/Cargo.lock b/Cargo.lock index 23f9e38..ccb5e92 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -137,17 +137,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "aes" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", -] - [[package]] name = "ahash" version = "0.8.11" @@ -256,43 +245,19 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - -[[package]] -name = "android_log-sys" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85965b6739a430150bdd138e2374a98af0c3ee0d030b3bb7fc3bddff58d0102e" - [[package]] name = "android_log-sys" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ecc8056bf6ab9892dcd53216c83d1597487d7dacac16c8df6b877d127df9937" -[[package]] -name = "android_logger" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8619b80c242aa7bd638b5c7ddd952addeecb71f69c75e33f1d47b2804f8f883a" -dependencies = [ - "android_log-sys 0.2.0", - "env_logger", - "log", - "once_cell", -] - [[package]] name = "android_logger" version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05b07e8e73d720a1f2e4b6014766e6039fd2e96a4fa44e2a78d0e1fa2ff49826" dependencies = [ - "android_log-sys 0.3.1", + "android_log-sys", "env_filter", "log", ] @@ -514,38 +479,6 @@ dependencies = [ "syn 2.0.90", ] -[[package]] -name = "async-utility" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a349201d80b4aa18d17a34a182bdd7f8ddf845e9e57d2ea130a12e10ef1e3a47" -dependencies = [ - "futures-util", - "gloo-timers", - "tokio", - "wasm-bindgen-futures", -] - -[[package]] -name = "async-wsocket" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a107e3bdbe61e8e1e1341c57241b4b2d50501127b44bd2eff13b4635ab42d35a" -dependencies = [ - "async-utility", - "futures", - "futures-util", - "js-sys", - "thiserror 1.0.64", - "tokio", - "tokio-rustls", - "tokio-socks", - "tokio-tungstenite", - "url", - "wasm-bindgen", - "web-sys", -] - [[package]] name = "atomic" version = "0.6.0" @@ -555,15 +488,6 @@ dependencies = [ "bytemuck", ] -[[package]] -name = "atomic-destructor" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d919cb60ba95c87ba42777e9e246c4e8d658057299b437b7512531ce0a09a23" -dependencies = [ - "tracing", -] - [[package]] name = "atomic-waker" version = "1.1.2" @@ -722,7 +646,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash 1.1.0", + "rustc-hash", "shlex", "syn 2.0.90", "which", @@ -741,7 +665,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash 1.1.0", + "rustc-hash", "shlex", "syn 2.0.90", ] @@ -1090,13 +1014,7 @@ version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ - "android-tzdata", - "iana-time-zone", - "js-sys", "num-traits", - "serde", - "wasm-bindgen", - "windows-targets 0.52.6", ] [[package]] @@ -1374,41 +1292,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991" -[[package]] -name = "darling" -version = "0.20.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.20.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.90", -] - -[[package]] -name = "darling_macro" -version = "0.20.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" -dependencies = [ - "darling_core", - "quote", - "syn 2.0.90", -] - [[package]] name = "dasp_sample" version = "0.11.0" @@ -1434,7 +1317,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", - "serde", ] [[package]] @@ -1646,7 +1528,7 @@ dependencies = [ "image", "log", "mime_guess2", - "resvg 0.37.0", + "resvg", ] [[package]] @@ -1679,8 +1561,7 @@ dependencies = [ [[package]] name = "egui_nav" version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cca758d7454588a5d927fd4fcb71cbb1ad21bcda251acd7860b04d91f83bcc4c" +source = "git+https://github.com/damus-io/egui-nav?rev=ac7d663307b76634757024b438dd4b899790da99#ac7d663307b76634757024b438dd4b899790da99" dependencies = [ "egui", "egui_extras", @@ -1758,17 +1639,19 @@ checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" [[package]] name = "enostr" version = "0.1.0" -source = "git+https://git.v0l.io/nostr/notedeck.git?branch=master#18226a35ffafca3621606c5e0fd9b2aba4e00913" +source = "git+https://git.v0l.io/nostr/notedeck.git?rev=e08e30f9125b9cf7391e97a2683ba0034bff1644#e08e30f9125b9cf7391e97a2683ba0034bff1644" dependencies = [ "bech32", "ewebsock", "hex", + "mio", "nostr", - "nostrdb 0.4.0 (git+https://github.com/damus-io/nostrdb-rs?rev=46ca13dffdfe2320d4488912506c7bfa02afe284)", + "nostrdb", "serde", "serde_derive", "serde_json", - "thiserror 2.0.6", + "thiserror 2.0.9", + "tokio", "tracing", "url", ] @@ -2018,12 +1901,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "foldhash" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" - [[package]] name = "foreign-types" version = "0.5.0" @@ -2068,6 +1945,7 @@ checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", + "futures-executor", "futures-io", "futures-sink", "futures-task", @@ -2090,6 +1968,17 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-io" version = "0.3.31" @@ -2216,18 +2105,6 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" -[[package]] -name = "gloo-timers" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" -dependencies = [ - "futures-channel", - "futures-core", - "js-sys", - "wasm-bindgen", -] - [[package]] name = "glow" version = "0.13.1" @@ -2380,12 +2257,6 @@ dependencies = [ "crunchy", ] -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - [[package]] name = "hashbrown" version = "0.14.5" @@ -2401,11 +2272,6 @@ name = "hashbrown" version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash", -] [[package]] name = "hassle-rs" @@ -2428,12 +2294,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - [[package]] name = "hermit-abi" version = "0.4.0" @@ -2502,29 +2362,6 @@ dependencies = [ "itoa", ] -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" -dependencies = [ - "bytes", - "futures-util", - "http", - "http-body", - "pin-project-lite", -] - [[package]] name = "httparse" version = "1.9.5" @@ -2537,92 +2374,6 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" -[[package]] -name = "hyper" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "http", - "http-body", - "httparse", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" -dependencies = [ - "futures-util", - "http", - "hyper", - "hyper-util", - "rustls", - "rustls-native-certs", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tower-service", - "webpki-roots", -] - -[[package]] -name = "hyper-util" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "http", - "http-body", - "hyper", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.61" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "wasm-bindgen", - "windows-core 0.52.0", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - [[package]] name = "idna" version = "0.5.0" @@ -2672,12 +2423,6 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284" -[[package]] -name = "imagesize" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" - [[package]] name = "imgref" version = "1.11.0" @@ -2693,17 +2438,6 @@ dependencies = [ "arrayvec", ] -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", - "serde", -] - [[package]] name = "indexmap" version = "2.6.0" @@ -2712,7 +2446,6 @@ checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", "hashbrown 0.15.0", - "serde", ] [[package]] @@ -2748,12 +2481,6 @@ dependencies = [ "syn 2.0.90", ] -[[package]] -name = "ipnet" -version = "2.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" - [[package]] name = "is-docker" version = "0.2.0" @@ -2769,7 +2496,7 @@ version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" dependencies = [ - "hermit-abi 0.4.0", + "hermit-abi", "libc", "windows-sys 0.52.0", ] @@ -2880,16 +2607,6 @@ dependencies = [ "arrayvec", ] -[[package]] -name = "kurbo" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89234b2cc610a7dd927ebde6b41dd1a5d4214cffaef4cf1fb2195d592f92518f" -dependencies = [ - "arrayvec", - "smallvec", -] - [[package]] name = "lazy_static" version = "1.5.0" @@ -2957,18 +2674,6 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" -[[package]] -name = "lnurl-pay" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "536e7c782167a2d48346ca0b2677fad19eaef20f19a4ab868e4d5b96ca879def" -dependencies = [ - "bech32", - "reqwest", - "serde", - "serde_json", -] - [[package]] name = "lock_api" version = "0.4.12" @@ -2994,15 +2699,6 @@ dependencies = [ "imgref", ] -[[package]] -name = "lru" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" -dependencies = [ - "hashbrown 0.15.0", -] - [[package]] name = "m3u8-rs" version = "6.0.0" @@ -3122,12 +2818,12 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ - "hermit-abi 0.3.9", "libc", + "log", "wasi", "windows-sys 0.52.0", ] @@ -3144,9 +2840,9 @@ dependencies = [ "cfg_aliases 0.1.1", "codespan-reporting", "hexf-parse", - "indexmap 2.6.0", + "indexmap", "log", - "rustc-hash 1.1.0", + "rustc-hash", "spirv", "termcolor", "thiserror 1.0.64", @@ -3288,7 +2984,6 @@ version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8aad4b767bbed24ac5eb4465bfb83bc1210522eb99d67cf4e547ec2ec7e47786" dependencies = [ - "aes", "async-trait", "base64 0.22.1", "bech32", @@ -3299,109 +2994,27 @@ dependencies = [ "chacha20poly1305", "getrandom", "instant", - "js-sys", "negentropy 0.3.1", "negentropy 0.4.3", "once_cell", - "reqwest", "scrypt", "serde", "serde_json", "unicode-normalization", "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "nostr-database" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23696338d51e45cd44e061823847f4b0d1d362eca80d5033facf9c184149f72f" -dependencies = [ - "async-trait", - "lru", - "nostr", - "thiserror 1.0.64", - "tokio", - "tracing", -] - -[[package]] -name = "nostr-relay-pool" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15fcc6e3f0ca54d0fc779009bc5f2684cea9147be3b6aa68a7d301ea590f95f5" -dependencies = [ - "async-utility", - "async-wsocket", - "atomic-destructor", - "negentropy 0.3.1", - "negentropy 0.4.3", - "nostr", - "nostr-database", - "thiserror 1.0.64", - "tokio", - "tokio-stream", - "tracing", -] - -[[package]] -name = "nostr-sdk" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "491221fc89b1aa189a0de640127127d68b4e7c5c1d44371b04d9a6d10694b5af" -dependencies = [ - "async-utility", - "atomic-destructor", - "lnurl-pay", - "nostr", - "nostr-database", - "nostr-relay-pool", - "nostr-zapper", - "nwc", - "thiserror 1.0.64", - "tokio", - "tracing", -] - -[[package]] -name = "nostr-zapper" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecbc3085d25f09a5530e571e43c4bd4af6421ef0dc7819405c9a58d1a59686b2" -dependencies = [ - "async-trait", - "nostr", - "thiserror 1.0.64", ] [[package]] name = "nostrdb" -version = "0.4.0" -source = "git+https://github.com/damus-io/nostrdb-rs?rev=3deb94aef3f436469158c4424650d81be26f9315#3deb94aef3f436469158c4424650d81be26f9315" +version = "0.5.1" +source = "git+https://github.com/damus-io/nostrdb-rs?rev=2111948b078b24a1659d0bd5d8570f370269c99b#2111948b078b24a1659d0bd5d8570f370269c99b" dependencies = [ "bindgen 0.69.5", "cc", "flatbuffers", + "futures", "libc", - "thiserror 2.0.6", - "tokio", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "nostrdb" -version = "0.4.0" -source = "git+https://github.com/damus-io/nostrdb-rs?rev=46ca13dffdfe2320d4488912506c7bfa02afe284#46ca13dffdfe2320d4488912506c7bfa02afe284" -dependencies = [ - "bindgen 0.69.5", - "cc", - "flatbuffers", - "libc", - "thiserror 2.0.6", + "thiserror 2.0.9", "tokio", "tracing", "tracing-subscriber", @@ -3410,7 +3023,7 @@ dependencies = [ [[package]] name = "notedeck" version = "0.1.0" -source = "git+https://git.v0l.io/nostr/notedeck.git?branch=master#18226a35ffafca3621606c5e0fd9b2aba4e00913" +source = "git+https://git.v0l.io/nostr/notedeck.git?rev=e08e30f9125b9cf7391e97a2683ba0034bff1644#e08e30f9125b9cf7391e97a2683ba0034bff1644" dependencies = [ "base32", "dirs", @@ -3418,14 +3031,14 @@ dependencies = [ "enostr", "hex", "image", - "nostrdb 0.4.0 (git+https://github.com/damus-io/nostrdb-rs?rev=46ca13dffdfe2320d4488912506c7bfa02afe284)", + "nostrdb", "poll-promise", "security-framework", "serde", "serde_json", "strum", "strum_macros", - "thiserror 2.0.6", + "thiserror 2.0.9", "tracing", "url", "uuid", @@ -3434,16 +3047,15 @@ dependencies = [ [[package]] name = "notedeck_chrome" version = "0.2.0" -source = "git+https://git.v0l.io/nostr/notedeck.git?branch=master#18226a35ffafca3621606c5e0fd9b2aba4e00913" +source = "git+https://git.v0l.io/nostr/notedeck.git?rev=e08e30f9125b9cf7391e97a2683ba0034bff1644#e08e30f9125b9cf7391e97a2683ba0034bff1644" dependencies = [ "android-activity 0.4.3", - "android_logger 0.11.3", "eframe", "egui", "egui_extras", "enostr", "log", - "nostrdb 0.4.0 (git+https://github.com/damus-io/nostrdb-rs?rev=46ca13dffdfe2320d4488912506c7bfa02afe284)", + "nostrdb", "notedeck", "notedeck_columns", "serde_json", @@ -3451,6 +3063,7 @@ dependencies = [ "tokio", "tracing", "tracing-appender", + "tracing-logcat", "tracing-subscriber", "winit", ] @@ -3458,7 +3071,7 @@ dependencies = [ [[package]] name = "notedeck_columns" version = "0.2.0" -source = "git+https://git.v0l.io/nostr/notedeck.git?branch=master#18226a35ffafca3621606c5e0fd9b2aba4e00913" +source = "git+https://git.v0l.io/nostr/notedeck.git?rev=e08e30f9125b9cf7391e97a2683ba0034bff1644#e08e30f9125b9cf7391e97a2683ba0034bff1644" dependencies = [ "bitflags 2.6.0", "dirs", @@ -3470,11 +3083,10 @@ dependencies = [ "egui_virtual_list", "ehttp 0.2.0", "enostr", - "env_logger", "hex", "image", - "indexmap 2.6.0", - "nostrdb 0.4.0 (git+https://github.com/damus-io/nostrdb-rs?rev=46ca13dffdfe2320d4488912506c7bfa02afe284)", + "indexmap", + "nostrdb", "notedeck", "open", "poll-promise", @@ -3484,7 +3096,7 @@ dependencies = [ "serde_json", "strum", "strum_macros", - "thiserror 2.0.6", + "thiserror 2.0.9", "tokio", "tracing", "tracing-appender", @@ -3623,20 +3235,6 @@ dependencies = [ "syn 2.0.90", ] -[[package]] -name = "nwc" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00fa4a303789d6c0e374ec8e377c3ba1279c929a1e4a8a06a43666d0de1e4b53" -dependencies = [ - "async-trait", - "async-utility", - "nostr", - "nostr-relay-pool", - "nostr-zapper", - "thiserror 1.0.64", -] - [[package]] name = "objc" version = "0.2.7" @@ -3904,12 +3502,6 @@ dependencies = [ "pathdiff", ] -[[package]] -name = "openssl-probe" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" - [[package]] name = "option-ext" version = "0.2.0" @@ -4105,7 +3697,7 @@ checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511" dependencies = [ "cfg-if", "concurrent-queue", - "hermit-abi 0.4.0", + "hermit-abi", "pin-project-lite", "rustix", "tracing", @@ -4251,54 +3843,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "quinn" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c7c5fdde3cdae7203427dc4f0a68fe0ed09833edc525a03456b153b79828684" -dependencies = [ - "bytes", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash 2.0.0", - "rustls", - "socket2", - "thiserror 1.0.64", - "tokio", - "tracing", -] - -[[package]] -name = "quinn-proto" -version = "0.11.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fadfaed2cd7f389d0161bb73eeb07b7b78f8691047a6f3e73caaeae55310a4a6" -dependencies = [ - "bytes", - "rand", - "ring", - "rustc-hash 2.0.0", - "rustls", - "slab", - "thiserror 1.0.64", - "tinyvec", - "tracing", -] - -[[package]] -name = "quinn-udp" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fe68c2e9e1a1234e218683dbdf9f9dfcb094113c5ac2b938dfcb9bab4c4140b" -dependencies = [ - "libc", - "once_cell", - "socket2", - "tracing", - "windows-sys 0.59.0", -] - [[package]] name = "quote" version = "1.0.37" @@ -4504,50 +4048,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" -[[package]] -name = "reqwest" -version = "0.12.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f713147fbe92361e52392c73b8c9e48c04c6625bce969ef54dc901e58e042a7b" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-core", - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "ipnet", - "js-sys", - "log", - "mime", - "once_cell", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-native-certs", - "rustls-pemfile", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tokio-socks", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "webpki-roots", - "windows-registry", -] - [[package]] name = "resvg" version = "0.37.0" @@ -4557,23 +4057,9 @@ dependencies = [ "log", "pico-args", "rgb", - "svgtypes 0.13.0", + "svgtypes", "tiny-skia", - "usvg 0.37.0", -] - -[[package]] -name = "resvg" -version = "0.44.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a325d5e8d1cebddd070b13f44cec8071594ab67d1012797c121f27a669b7958" -dependencies = [ - "log", - "pico-args", - "rgb", - "svgtypes 0.15.2", - "tiny-skia", - "usvg 0.44.0", + "usvg", ] [[package]] @@ -4606,12 +4092,6 @@ version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f" -[[package]] -name = "roxmltree" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" - [[package]] name = "rustc-demangle" version = "0.1.24" @@ -4624,12 +4104,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" -[[package]] -name = "rustc-hash" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" - [[package]] name = "rustc_version" version = "0.4.1" @@ -4667,28 +4141,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rustls-native-certs" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcaf18a4f2be7326cd874a5fa579fae794320a0f388d365dca7e480e55f83f8a" -dependencies = [ - "openssl-probe", - "rustls-pemfile", - "rustls-pki-types", - "schannel", - "security-framework", -] - -[[package]] -name = "rustls-pemfile" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "rustls-pki-types" version = "1.10.0" @@ -4736,15 +4188,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "schannel" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1" -dependencies = [ - "windows-sys 0.59.0", -] - [[package]] name = "scoped-tls" version = "1.0.1" @@ -4858,7 +4301,7 @@ version = "1.0.128" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" dependencies = [ - "indexmap 2.6.0", + "indexmap", "itoa", "memchr", "ryu", @@ -4885,48 +4328,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_with" -version = "3.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817" -dependencies = [ - "base64 0.22.1", - "chrono", - "hex", - "indexmap 1.9.3", - "indexmap 2.6.0", - "serde", - "serde_derive", - "serde_json", - "serde_with_macros", - "time", -] - -[[package]] -name = "serde_with_macros" -version = "3.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn 2.0.90", -] - [[package]] name = "sha1" version = "0.10.6" @@ -5003,12 +4404,6 @@ version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" -[[package]] -name = "siphasher" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" - [[package]] name = "slab" version = "0.4.9" @@ -5084,16 +4479,6 @@ dependencies = [ "serde", ] -[[package]] -name = "socket2" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - [[package]] name = "spin" version = "0.9.8" @@ -5124,12 +4509,6 @@ dependencies = [ "float-cmp", ] -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - [[package]] name = "strum" version = "0.26.3" @@ -5161,18 +4540,8 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e44e288cd960318917cbd540340968b90becc8bc81f171345d706e7a89d9d70" dependencies = [ - "kurbo 0.9.5", - "siphasher 0.3.11", -] - -[[package]] -name = "svgtypes" -version = "0.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "794de53cc48eaabeed0ab6a3404a65f40b3e38c067e4435883a65d2aa4ca000e" -dependencies = [ - "kurbo 0.11.1", - "siphasher 1.0.1", + "kurbo", + "siphasher", ] [[package]] @@ -5197,15 +4566,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "sync_wrapper" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" -dependencies = [ - "futures-core", -] - [[package]] name = "system-deps" version = "6.2.2" @@ -5258,11 +4618,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.6" +version = "2.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec2a1820ebd077e2b90c4df007bebf344cd394098a13c563957d0afc83ea47" +checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc" dependencies = [ - "thiserror-impl 2.0.6", + "thiserror-impl 2.0.9", ] [[package]] @@ -5278,9 +4638,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.6" +version = "2.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d65750cab40f4ff1929fb1ba509e9914eb756131cef4210da8d5d700d26f6312" +checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4" dependencies = [ "proc-macro2", "quote", @@ -5387,13 +4747,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" dependencies = [ "backtrace", - "bytes", - "libc", - "mio", "pin-project-lite", - "socket2", "tokio-macros", - "windows-sys 0.52.0", ] [[package]] @@ -5407,56 +4762,6 @@ dependencies = [ "syn 2.0.90", ] -[[package]] -name = "tokio-rustls" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" -dependencies = [ - "rustls", - "rustls-pki-types", - "tokio", -] - -[[package]] -name = "tokio-socks" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f" -dependencies = [ - "either", - "futures-util", - "thiserror 1.0.64", - "tokio", -] - -[[package]] -name = "tokio-stream" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tokio-tungstenite" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" -dependencies = [ - "futures-util", - "log", - "rustls", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tungstenite", - "webpki-roots", -] - [[package]] name = "toml" version = "0.8.19" @@ -5484,7 +4789,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.6.0", + "indexmap", "toml_datetime", "winnow 0.5.40", ] @@ -5495,19 +4800,13 @@ version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ - "indexmap 2.6.0", + "indexmap", "serde", "serde_spanned", "toml_datetime", "winnow 0.6.20", ] -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - [[package]] name = "tracing" version = "0.1.40" @@ -5563,6 +4862,17 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-logcat" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678d561ce3d4b33dfcfb1c6bd13c422aad89be5bf0e66e20d518a5a254345b54" +dependencies = [ + "rustix", + "tracing", + "tracing-subscriber", +] + [[package]] name = "tracing-subscriber" version = "0.3.18" @@ -5581,12 +4891,6 @@ dependencies = [ "tracing-log", ] -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - [[package]] name = "ttf-parser" version = "0.25.0" @@ -5620,7 +4924,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "deb68604048ff8fa93347f02441e4487594adc20bb8a084f9e564d2b827a0a9f" dependencies = [ - "rustc-hash 1.1.0", + "rustc-hash", ] [[package]] @@ -5749,28 +5053,6 @@ dependencies = [ "xmlwriter", ] -[[package]] -name = "usvg" -version = "0.44.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7447e703d7223b067607655e625e0dbca80822880248937da65966194c4864e6" -dependencies = [ - "base64 0.22.1", - "data-url", - "flate2", - "imagesize 0.13.0", - "kurbo 0.11.1", - "log", - "pico-args", - "roxmltree 0.20.0", - "simplecss", - "siphasher 1.0.1", - "strict-num", - "svgtypes 0.15.2", - "tiny-skia-path", - "xmlwriter", -] - [[package]] name = "usvg-parser" version = "0.37.0" @@ -5779,13 +5061,13 @@ checksum = "9bd4e3c291f45d152929a31f0f6c819245e2921bfd01e7bd91201a9af39a2bdc" dependencies = [ "data-url", "flate2", - "imagesize 0.12.0", - "kurbo 0.9.5", + "imagesize", + "kurbo", "log", - "roxmltree 0.19.0", + "roxmltree", "simplecss", - "siphasher 0.3.11", - "svgtypes 0.13.0", + "siphasher", + "svgtypes", "usvg-tree", ] @@ -5797,7 +5079,7 @@ checksum = "8ee3d202ebdb97a6215604b8f5b4d6ef9024efd623cf2e373a6416ba976ec7d3" dependencies = [ "rctree", "strict-num", - "svgtypes 0.13.0", + "svgtypes", "tiny-skia-path", ] @@ -5861,15 +5143,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -6141,14 +5414,14 @@ dependencies = [ "bitflags 2.6.0", "cfg_aliases 0.1.1", "document-features", - "indexmap 2.6.0", + "indexmap", "log", "naga", "once_cell", "parking_lot", "profiling", "raw-window-handle 0.6.2", - "rustc-hash 1.1.0", + "rustc-hash", "smallvec", "thiserror 1.0.64", "wgpu-hal", @@ -6188,7 +5461,7 @@ dependencies = [ "profiling", "raw-window-handle 0.6.2", "renderdoc-sys", - "rustc-hash 1.1.0", + "rustc-hash", "smallvec", "thiserror 1.0.64", "wasm-bindgen", @@ -6341,17 +5614,6 @@ dependencies = [ "syn 2.0.90", ] -[[package]] -name = "windows-registry" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" -dependencies = [ - "windows-result 0.2.0", - "windows-strings", - "windows-targets 0.52.6", -] - [[package]] name = "windows-result" version = "0.1.2" @@ -6749,32 +6011,26 @@ version = "0.1.0" dependencies = [ "android-activity 0.6.0", "android-ndk-sys", - "android_logger 0.14.1", + "android_logger", "anyhow", - "async-trait", "bech32", - "chrono", "directories", "eframe", "egui", "egui-video", "egui_inbox", + "ehttp 0.5.0", + "enostr", "itertools 0.13.0", - "libc", "log", - "lru", - "nostr-sdk", - "nostrdb 0.4.0 (git+https://github.com/damus-io/nostrdb-rs?rev=3deb94aef3f436469158c4424650d81be26f9315)", + "nostr", + "nostrdb", "notedeck", "notedeck_chrome", + "poll-promise", "pretty_env_logger", - "reqwest", - "resvg 0.44.0", "serde", - "serde_with", - "sha2", "tokio", - "uuid", "winit", ] diff --git a/Cargo.toml b/Cargo.toml index 0744c10..25b2e09 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,34 +7,30 @@ edition = "2021" crate-type = ["lib", "cdylib"] [features] -default = [] -notedeck = ["dep:notedeck", "dep:notedeck-chrome"] +default = ["notedeck"] +notedeck = ["dep:notedeck", "dep:notedeck-chrome", "dep:enostr"] [dependencies] tokio = { version = "1.40.0", features = ["fs", "rt-multi-thread", "rt"] } egui = { version = "0.29.1", default-features = false, features = [] } -nostrdb = { git = "https://github.com/damus-io/nostrdb-rs", rev = "3deb94aef3f436469158c4424650d81be26f9315" } -nostr-sdk = { version = "0.37", features = ["all-nips"] } log = "0.4.22" pretty_env_logger = "0.5.0" egui_inbox = "0.6.0" bech32 = "0.11.0" -libc = "0.2.158" -uuid = { version = "1.11.0", features = ["v4"] } -chrono = "0.4.38" anyhow = "^1.0.91" -async-trait = "0.1.83" -sha2 = "0.10.8" -reqwest = { version = "0.12.7", default-features = false, features = ["rustls-tls-native-roots"] } itertools = "0.13.0" -lru = "0.12.5" -resvg = { version = "0.44.0", default-features = false } serde = { version = "1.0.214", features = ["derive"] } -serde_with = { version = "3.11.0", features = ["hex"] } directories = "5.0.1" egui-video = { git = "https://github.com/v0l/egui-video.git", rev = "d2ea3b4db21eb870a207db19e4cd21c7d1d24836" } -notedeck-chrome = { git = "https://git.v0l.io/nostr/notedeck.git", branch = "master", package = "notedeck_chrome", optional = true } -notedeck = { git = "https://git.v0l.io/nostr/notedeck.git", branch = "master", package = "notedeck", optional = true } + +# notedeck stuff +nostr = { version = "0.37.0", default-features = false, features = ["std", "nip49"] } +nostrdb = { git = "https://github.com/damus-io/nostrdb-rs", rev = "2111948b078b24a1659d0bd5d8570f370269c99b" } +notedeck-chrome = { git = "https://git.v0l.io/nostr/notedeck.git", rev = "e08e30f9125b9cf7391e97a2683ba0034bff1644", package = "notedeck_chrome", optional = true } +notedeck = { git = "https://git.v0l.io/nostr/notedeck.git", rev = "e08e30f9125b9cf7391e97a2683ba0034bff1644", package = "notedeck", optional = true } +enostr = { git = "https://git.v0l.io/nostr/notedeck.git", rev = "e08e30f9125b9cf7391e97a2683ba0034bff1644", package = "enostr", optional = true } +poll-promise = "0.3.0" +ehttp = "0.5.0" [target.'cfg(not(target_os = "android"))'.dependencies] eframe = { version = "0.29.1" } diff --git a/src/android.rs b/src/android.rs index cffbed6..fd52ce9 100644 --- a/src/android.rs +++ b/src/android.rs @@ -1,9 +1,6 @@ -use crate::app::{NativeLayerOps, ZapStreamApp}; +use crate::app::ZapStreamApp; use eframe::Renderer; -use egui::{Margin, ViewportBuilder}; -use serde::de::DeserializeOwned; -use serde::Serialize; -use std::ops::Div; +use egui::ViewportBuilder; use winit::platform::android::activity::AndroidApp; use winit::platform::android::EventLoopBuilderExtAndroid; @@ -34,57 +31,17 @@ pub fn start_android(app: AndroidApp) { if let Err(e) = eframe::run_native( "zap.stream", options, - Box::new(move |cc| Ok(Box::new(ZapStreamApp::new(cc, data_path, app)))), + Box::new(move |cc| { + let args: Vec = std::env::args().collect(); + let mut notedeck = + notedeck_chrome::Notedeck::new(&cc.egui_ctx, data_path.clone(), &args); + + let app = ZapStreamApp::new(cc); + notedeck.add_app(app); + + Ok(Box::new(notedeck)) + }), ) { eprintln!("{}", e); } } - -impl NativeLayerOps for AndroidApp { - fn frame_margin(&self) -> Margin { - if let Some(wd) = self.native_window() { - let (w, h) = (wd.width(), wd.height()); - let c_rect = self.content_rect(); - let dpi = self.config().density().unwrap_or(160); - let dpi_scale = dpi as f32 / 160.0; - // TODO: this calc is weird but seems to work on my phone - Margin { - bottom: (h - c_rect.bottom) as f32, - left: c_rect.left as f32, - right: (w - c_rect.right) as f32, - top: (c_rect.top - (h - c_rect.bottom)) as f32, - } - .div(dpi_scale) - } else { - Margin::ZERO - } - } - - fn show_keyboard(&self) { - self.show_soft_input(true); - } - - fn hide_keyboard(&self) { - self.hide_soft_input(true); - } - - fn get(&self, k: &str) -> Option { - None - } - - fn set(&mut self, k: &str, v: &str) -> bool { - false - } - - fn remove(&mut self, k: &str) -> bool { - false - } - - fn get_obj(&self, k: &str) -> Option { - None - } - - fn set_obj(&mut self, k: &str, v: &T) -> bool { - false - } -} diff --git a/src/app.rs b/src/app.rs index 1d2e9f1..7aadd23 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,67 +1,26 @@ -use crate::route::Router; -use eframe::epaint::FontFamily; +use crate::route::{page, RouteServices, RouteType}; +use crate::widgets::{Header, NostrWidget}; +use eframe::epaint::{FontFamily, Margin}; use eframe::CreationContext; -use egui::{Color32, FontData, FontDefinitions, Margin}; -use nostr_sdk::prelude::MemoryDatabase; -use nostr_sdk::Client; -use nostrdb::{Config, Ndb}; +use egui::{Color32, FontData, FontDefinitions, Ui}; +use enostr::ewebsock::{WsEvent, WsMessage}; +use enostr::{PoolEvent, RelayEvent, RelayMessage}; +use log::{error, info, warn}; +use nostrdb::Transaction; use notedeck::AppContext; -use std::path::PathBuf; +use std::ops::Div; +use std::sync::mpsc; -pub struct ZapStreamApp { - client: Client, - router: Router, - native_layer: T, +pub struct ZapStreamApp { + current: RouteType, + routes_rx: mpsc::Receiver, + routes_tx: mpsc::Sender, + + widget: Box, } -pub trait NativeLayerOps { - /// Get any display layout margins - fn frame_margin(&self) -> Margin; - /// Show the keyboard on the screen - fn show_keyboard(&self); - /// Hide on screen keyboard - fn hide_keyboard(&self); - fn get(&self, k: &str) -> Option; - fn set(&mut self, k: &str, v: &str) -> bool; - fn remove(&mut self, k: &str) -> bool; - fn get_obj(&self, k: &str) -> Option; - fn set_obj(&mut self, k: &str, v: &T) -> bool; -} - -impl ZapStreamApp -where - T: NativeLayerOps + Clone, -{ - pub fn new(cc: &CreationContext, data_path: PathBuf, config: T) -> Self { - let client = Client::builder() - .database(MemoryDatabase::with_opts(Default::default())) - .build(); - - let client_clone = client.clone(); - tokio::spawn(async move { - client_clone - .add_relay("wss://nos.lol") - .await - .expect("Failed to add relay"); - client_clone - .add_relay("wss://relay.damus.io") - .await - .expect("Failed to add relay"); - client_clone - .add_relay("wss://relay.snort.social") - .await - .expect("Failed to add relay"); - client_clone.connect().await; - }); - - let ndb_path = data_path.join("ndb"); - std::fs::create_dir_all(&ndb_path).expect("Failed to create ndb directory"); - - let mut ndb_config = Config::default(); - ndb_config.set_ingester_threads(4); - - let ndb = Ndb::new(ndb_path.to_str().unwrap(), &ndb_config).unwrap(); - +impl ZapStreamApp { + pub fn new(cc: &CreationContext) -> Self { let mut fd = FontDefinitions::default(); fd.font_data.insert( "Outfit".to_string(), @@ -71,63 +30,113 @@ where .insert(FontFamily::Proportional, vec!["Outfit".to_string()]); cc.egui_ctx.set_fonts(fd); - let cfg = config.clone(); + let (tx, rx) = mpsc::channel(); Self { - client: client.clone(), - router: Router::new( - data_path, - cc.egui_ctx.clone(), - client.clone(), - ndb.clone(), - cfg, - ), - native_layer: config, + current: RouteType::HomePage, + widget: Box::new(page::HomePage::new()), + routes_tx: tx, + routes_rx: rx, } } } -#[cfg(not(feature = "notedeck"))] -impl App for ZapStreamApp -where - T: NativeLayerOps, -{ - fn update(&mut self, ctx: &Context, frame: &mut Frame) { +impl notedeck::App for ZapStreamApp { + fn update(&mut self, ctx: &mut AppContext<'_>, ui: &mut Ui) { + ctx.accounts.update(ctx.ndb, ctx.pool, ui.ctx()); + while let Some(PoolEvent { event, relay }) = ctx.pool.try_recv() { + match (&event).into() { + RelayEvent::Message(msg) => match msg { + RelayMessage::OK(_) => {} + RelayMessage::Eose(_) => {} + RelayMessage::Event(_sub, ev) => { + if let Err(e) = ctx.ndb.process_event(ev) { + error!("Error processing event: {:?}", e); + } + } + RelayMessage::Notice(m) => warn!("Notice from {}: {}", relay, m), + }, + _ => {} + } + } + let mut app_frame = egui::containers::Frame::default(); - let margin = self.native_layer.frame_margin(); + let margin = self.frame_margin(); app_frame.inner_margin = margin; app_frame.stroke.color = Color32::BLACK; - //ctx.set_debug_on_hover(true); - + // handle app state changes + while let Ok(r) = self.routes_rx.try_recv() { + if let RouteType::Action(a) = r { + match a { + _ => info!("Not implemented"), + } + } else { + self.current = r; + match &self.current { + RouteType::HomePage => { + self.widget = Box::new(page::HomePage::new()); + } + RouteType::EventPage { link, .. } => { + self.widget = Box::new(page::StreamPage::new_from_link(link.clone())); + } + RouteType::LoginPage => { + self.widget = Box::new(page::LoginPage::new()); + } + RouteType::Action { .. } => panic!("Actions!"), + _ => panic!("Not implemented"), + } + } + } egui::CentralPanel::default() .frame(app_frame) - .show(ctx, |ui| { + .show(ui.ctx(), |ui| { ui.visuals_mut().override_text_color = Some(Color32::WHITE); - self.router.show(ui); + + // display app + ui.vertical(|ui| { + let mut svc = RouteServices { + router: self.routes_tx.clone(), + tx: Transaction::new(ctx.ndb).expect("transaction"), + egui: ui.ctx().clone(), + ctx, + }; + Header::new().render(ui, &mut svc); + if let Err(e) = self.widget.update(&mut svc) { + error!("{}", e); + } + self.widget.render(ui, &mut svc); + }) + .response }); } } -#[cfg(feature = "notedeck")] -impl notedeck::App for ZapStreamApp -where - T: NativeLayerOps, -{ - fn update(&mut self, ctx: &mut AppContext<'_>) { - let mut app_frame = egui::containers::Frame::default(); - let margin = self.native_layer.frame_margin(); - - app_frame.inner_margin = margin; - app_frame.stroke.color = Color32::BLACK; - - //ctx.set_debug_on_hover(true); - - egui::CentralPanel::default() - .frame(app_frame) - .show(ctx.egui, |ui| { - ui.visuals_mut().override_text_color = Some(Color32::WHITE); - self.router.show(ui); - }); +#[cfg(not(target_os = "android"))] +impl ZapStreamApp { + fn frame_margin(&self) -> Margin { + Margin::ZERO + } +} + +#[cfg(target_os = "android")] +impl ZapStreamApp { + fn frame_margin(&self) -> Margin { + if let Some(wd) = self.native_window() { + let (w, h) = (wd.width(), wd.height()); + let c_rect = self.content_rect(); + let dpi = self.config().density().unwrap_or(160); + let dpi_scale = dpi as f32 / 160.0; + // TODO: this calc is weird but seems to work on my phone + Margin { + bottom: (h - c_rect.bottom) as f32, + left: c_rect.left as f32, + right: (w - c_rect.right) as f32, + top: (c_rect.top - (h - c_rect.bottom)) as f32, + } + .div(dpi_scale) + } else { + Margin::ZERO + } } } diff --git a/src/bin/zap_stream_app.rs b/src/bin/zap_stream_app.rs index acd43b8..10bf746 100644 --- a/src/bin/zap_stream_app.rs +++ b/src/bin/zap_stream_app.rs @@ -1,17 +1,9 @@ use anyhow::Result; use directories::ProjectDirs; use eframe::Renderer; -use egui::{Margin, Vec2, ViewportBuilder}; +use egui::{Vec2, ViewportBuilder}; use log::error; -use nostr_sdk::serde_json; -use serde::de::DeserializeOwned; -use serde::Serialize; -use std::collections::HashMap; -use std::io::{Read, Write}; -use std::ops::Deref; -use std::path::PathBuf; -use std::sync::{Arc, RwLock}; -use zap_stream_app::app::{NativeLayerOps, ZapStreamApp}; +use zap_stream_app::app::ZapStreamApp; #[tokio::main] async fn main() -> Result<()> { @@ -26,8 +18,6 @@ async fn main() -> Result<()> { .config_dir() .to_path_buf(); - let config = DesktopApp::new(data_path.clone()); - #[cfg(feature = "notedeck")] if let Err(e) = eframe::run_native( "zap.stream", options, @@ -36,7 +26,7 @@ async fn main() -> Result<()> { let mut notedeck = notedeck_chrome::Notedeck::new(&cc.egui_ctx, data_path.clone(), &args); - let app = ZapStreamApp::new(cc, data_path, config); + let app = ZapStreamApp::new(cc); notedeck.add_app(app); Ok(Box::new(notedeck)) @@ -44,93 +34,5 @@ async fn main() -> Result<()> { ) { error!("{}", e); } - - #[cfg(not(feature = "notedeck"))] - if let Err(e) = eframe::run_native("zap.stream", options, Box::new(move |cc| Ok(Box::new()))) { - error!("{}", e); - } Ok(()) } - -#[derive(Clone)] -pub struct DesktopApp { - data_path: PathBuf, - data: Arc>>, -} - -impl DesktopApp { - pub fn new(data_path: PathBuf) -> Self { - let mut r = Self { - data_path, - data: Arc::new(RwLock::new(HashMap::new())), - }; - r.load(); - r - } - - fn storage_file_path(&self) -> PathBuf { - self.data_path.join("kv.json") - } - - fn load(&mut self) { - let path = self.storage_file_path(); - if path.exists() { - let mut file = std::fs::File::open(path).unwrap(); - let mut data = Vec::new(); - file.read_to_end(&mut data).unwrap(); - if let Ok(d) = serde_json::from_slice(data.as_slice()) { - self.data = Arc::new(RwLock::new(d)); - } - } - } - - fn save(&self) { - let path = self.storage_file_path(); - let mut file = std::fs::File::create(path).unwrap(); - let json = serde_json::to_string_pretty(self.data.read().unwrap().deref()).unwrap(); - file.write_all(json.as_bytes()).unwrap(); - } -} - -impl NativeLayerOps for DesktopApp { - fn frame_margin(&self) -> Margin { - Margin::ZERO - } - - fn show_keyboard(&self) { - // nothing to do - } - - fn hide_keyboard(&self) { - // nothing to do - } - fn get(&self, k: &str) -> Option { - self.data.read().unwrap().get(k).cloned() - } - - fn set(&mut self, k: &str, v: &str) -> bool { - self.data - .write() - .unwrap() - .insert(k.to_owned(), v.to_owned()) - .is_none() - } - - fn remove(&mut self, k: &str) -> bool { - self.data.write().unwrap().remove(k).is_some() - } - - fn get_obj(&self, k: &str) -> Option { - serde_json::from_str(self.get(k)?.as_str()).ok() - } - - fn set_obj(&mut self, k: &str, v: &T) -> bool { - self.set(k, serde_json::to_string(v).unwrap().as_str()) - } -} - -impl Drop for DesktopApp { - fn drop(&mut self) { - self.save(); - } -} diff --git a/src/lib.rs b/src/lib.rs index 1f317e1..7ed988d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,14 +2,14 @@ mod android; pub mod app; mod link; -mod login; -mod note_store; mod note_util; +mod note_view; mod route; mod services; mod stream_info; mod theme; mod widgets; +mod note_ref; #[cfg(target_os = "android")] use android_activity::AndroidApp; diff --git a/src/link.rs b/src/link.rs index eb0b1c4..2e0e74f 100644 --- a/src/link.rs +++ b/src/link.rs @@ -1,7 +1,7 @@ use crate::note_util::NoteUtil; use bech32::{Hrp, NoChecksum}; use egui::TextBuffer; -use nostr_sdk::util::hex; +use nostr::prelude::hex; use nostrdb::{Filter, Note}; use std::fmt::{Display, Formatter}; diff --git a/src/login.rs b/src/login.rs deleted file mode 100644 index a5bbf84..0000000 --- a/src/login.rs +++ /dev/null @@ -1,89 +0,0 @@ -use crate::app::NativeLayerOps; -use crate::link::NostrLink; -use anyhow::Error; -use nostr_sdk::secp256k1::{Keypair, XOnlyPublicKey}; -use nostr_sdk::{Event, EventBuilder, Keys, Kind, SecretKey, Tag, UnsignedEvent}; -use serde::{Deserialize, Serialize}; -use serde_with::serde_as; -use std::ops::Deref; - -#[serde_as] -#[derive(Serialize, Deserialize, Debug, Clone)] -pub enum LoginKind { - PublicKey { - #[serde_as(as = "serde_with::hex::Hex")] - key: [u8; 32], - }, - PrivateKey { - #[serde_as(as = "serde_with::hex::Hex")] - key: [u8; 32], - }, - LoggedOut, -} - -pub struct Login { - kind: LoginKind, -} - -impl Login { - pub fn new() -> Self { - Self { - kind: LoginKind::LoggedOut, - } - } - - pub fn load(&mut self, storage: &T) { - if let Some(k) = storage.get_obj("login") { - self.kind = k; - } - } - - pub fn save(&mut self, storage: &mut T) { - storage.set_obj("login", &self.kind); - } - - pub fn login(&mut self, kind: LoginKind) { - self.kind = kind; - } - - pub fn is_logged_in(&self) -> bool { - !matches!(self.kind, LoginKind::LoggedOut) - } - - pub fn public_key(&self) -> Option<[u8; 32]> { - match self.kind { - LoginKind::PublicKey { key } => Some(key), - LoginKind::PrivateKey { key } => { - // TODO: wow this is annoying - let sk = Keypair::from_seckey_slice(nostr_sdk::SECP256K1.deref(), key.as_slice()) - .unwrap(); - Some(XOnlyPublicKey::from_keypair(&sk).0.serialize()) - } - _ => None, - } - } - - fn secret_key(&self) -> Result { - if let LoginKind::PrivateKey { key } = self.kind { - Ok(Keys::new(SecretKey::from_slice(key.as_slice())?)) - } else { - anyhow::bail!("No private key"); - } - } - - pub fn sign_event(&self, ev: UnsignedEvent) -> Result { - let secret = self.secret_key()?; - ev.sign_with_keys(&secret).map_err(Error::new) - } - - pub fn write_live_chat_msg(&self, link: &NostrLink, msg: &str) -> Result { - if msg.len() == 0 { - return Err(anyhow::anyhow!("Empty message")); - } - let secret = self.secret_key()?; - EventBuilder::new(Kind::LiveEventMessage, msg) - .tag(Tag::parse(&link.to_tag())?) - .sign_with_keys(&secret) - .map_err(Error::new) - } -} diff --git a/src/note_ref.rs b/src/note_ref.rs new file mode 100644 index 0000000..a96e85d --- /dev/null +++ b/src/note_ref.rs @@ -0,0 +1,43 @@ +use nostrdb::{Note, NoteKey, QueryResult}; +use std::cmp::Ordering; + +#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)] +pub struct NoteRef { + pub key: NoteKey, + pub created_at: u64, +} + +impl NoteRef { + pub fn new(key: NoteKey, created_at: u64) -> Self { + NoteRef { key, created_at } + } + + pub fn from_note(note: &Note<'_>) -> Self { + let created_at = note.created_at(); + let key = note.key().expect("todo: implement NoteBuf"); + NoteRef::new(key, created_at) + } + + pub fn from_query_result(qr: QueryResult<'_>) -> Self { + NoteRef { + key: qr.note_key, + created_at: qr.note.created_at(), + } + } +} + +impl Ord for NoteRef { + fn cmp(&self, other: &Self) -> Ordering { + match self.created_at.cmp(&other.created_at) { + Ordering::Equal => self.key.cmp(&other.key), + Ordering::Less => Ordering::Greater, + Ordering::Greater => Ordering::Less, + } + } +} + +impl PartialOrd for NoteRef { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} diff --git a/src/note_util.rs b/src/note_util.rs index 264c670..36165c1 100644 --- a/src/note_util.rs +++ b/src/note_util.rs @@ -1,4 +1,4 @@ -use nostr_sdk::util::hex; +use nostr::prelude::hex; use nostrdb::{NdbStr, Note, Tag}; pub trait NoteUtil { @@ -64,6 +64,3 @@ impl<'a> Iterator for TagIterBorrow<'a> { } } } - -#[derive(Eq, PartialEq)] -pub struct OwnedNote(pub u64); diff --git a/src/note_store.rs b/src/note_view.rs similarity index 95% rename from src/note_store.rs rename to src/note_view.rs index 351b685..66bdf17 100644 --- a/src/note_store.rs +++ b/src/note_view.rs @@ -2,11 +2,11 @@ use crate::link::NostrLink; use nostrdb::Note; use std::collections::HashMap; -pub struct NoteStore<'a> { +pub struct NotesView<'a> { events: HashMap>, } -impl<'a> NoteStore<'a> { +impl<'a> NotesView<'a> { pub fn new() -> Self { Self { events: HashMap::new(), diff --git a/src/route/home.rs b/src/route/home.rs index 5cf74da..ac50f68 100644 --- a/src/route/home.rs +++ b/src/route/home.rs @@ -1,96 +1,93 @@ -use crate::note_store::NoteStore; -use crate::note_util::OwnedNote; +use crate::note_ref::NoteRef; +use crate::note_view::NotesView; use crate::route::RouteServices; -use crate::services::ndb_wrapper::{NDBWrapper, SubWrapper}; use crate::stream_info::{StreamInfo, StreamStatus}; use crate::widgets; -use crate::widgets::NostrWidget; -use egui::{Id, Response, RichText, ScrollArea, Ui, Widget}; -use nostrdb::{Filter, Note, NoteKey, Transaction}; +use crate::widgets::{sub_or_poll, NostrWidget}; +use egui::{Id, Response, RichText, ScrollArea, Ui}; +use nostrdb::{Filter, Note, Subscription}; +use std::collections::HashSet; pub struct HomePage { - sub: SubWrapper, - events: Vec, + events: HashSet, + sub: Option, } impl HomePage { - pub fn new(ndb: &NDBWrapper, tx: &Transaction) -> Self { - let filter = [Filter::new().kinds([30_311]).limit(100).build()]; - let (sub, events) = ndb.subscribe_with_results("home-page", &filter, tx, 1000); + pub fn new() -> Self { Self { - sub, - events: events - .iter() - .map(|e| OwnedNote(e.note_key.as_u64())) - .collect(), + events: HashSet::new(), + sub: None, } } + + fn get_filters() -> Vec { + vec![Filter::new().kinds([30_311]).limit(100).build()] + } } impl NostrWidget for HomePage { - fn render(&mut self, ui: &mut Ui, services: &mut RouteServices<'_>) -> Response { - let new_notes = services.ndb.poll(&self.sub, 100); - new_notes - .iter() - .for_each(|n| self.events.push(OwnedNote(n.as_u64()))); - - let events: Vec> = self - .events - .iter() - .map(|n| services.ndb.get_note_by_key(services.tx, NoteKey::new(n.0))) - .map_while(|f| f.ok()) - .filter(|f| f.can_play()) - .collect(); - + fn render(&mut self, ui: &mut Ui, services: &mut RouteServices<'_, '_>) -> Response { ScrollArea::vertical() .show(ui, |ui| { - let events_live = NoteStore::from_vec( - events - .iter() + let events: Vec = self + .events + .iter() + .map_while(|n| services.ctx.ndb.get_note_by_key(&services.tx, n.key).ok()) + .collect(); + + let events_live = NotesView::from_vec( + events.iter() .filter(|r| matches!(r.status(), StreamStatus::Live)) .collect(), ); if events_live.len() > 0 { widgets::StreamList::new( Id::new("live-streams"), - &events_live, - services, + events_live, Some(RichText::new("Live").size(32.0)), ) - .ui(ui); + .render(ui, services); } - let events_planned = NoteStore::from_vec( - events - .iter() + let events_planned = NotesView::from_vec( + events.iter() .filter(|r| matches!(r.status(), StreamStatus::Planned)) .collect(), ); if events_planned.len() > 0 { widgets::StreamList::new( Id::new("planned-streams"), - &events_planned, - services, + events_planned, Some(RichText::new("Planned").size(32.0)), ) - .ui(ui); + .render(ui, services); } - let events_ended = NoteStore::from_vec( - events - .iter() + let events_ended = NotesView::from_vec( + events.iter() .filter(|r| matches!(r.status(), StreamStatus::Ended)) .collect(), ); if events_ended.len() > 0 { widgets::StreamList::new( Id::new("ended-streams"), - &events_ended, - services, + events_ended, Some(RichText::new("Ended").size(32.0)), ) - .ui(ui); + .render(ui, services); } ui.response() }) .inner } + + fn update(&mut self, services: &mut RouteServices<'_, '_>) -> anyhow::Result<()> { + sub_or_poll( + services.ctx.ndb, + &services.tx, + &mut services.ctx.pool, + &mut self.events, + &mut self.sub, + Self::get_filters(), + ) + } } diff --git a/src/route/login.rs b/src/route/login.rs index 41e1564..6aa2339 100644 --- a/src/route/login.rs +++ b/src/route/login.rs @@ -1,8 +1,8 @@ -use crate::login::LoginKind; -use crate::route::{RouteServices, Routes}; +use crate::route::{RouteServices, RouteType}; use crate::widgets::{Button, NativeTextInput, NostrWidget}; use egui::{Color32, Frame, Margin, Response, RichText, Ui}; -use nostr_sdk::util::hex; +use nostr::prelude::hex; +use nostr::SecretKey; pub struct LoginPage { key: String, @@ -19,7 +19,7 @@ impl LoginPage { } impl NostrWidget for LoginPage { - fn render(&mut self, ui: &mut Ui, services: &mut RouteServices<'_>) -> Response { + fn render(&mut self, ui: &mut Ui, services: &mut RouteServices<'_, '_>) -> Response { Frame::none() .inner_margin(Margin::same(12.)) .show(ui, |ui| { @@ -27,30 +27,53 @@ impl NostrWidget for LoginPage { ui.spacing_mut().item_spacing.y = 8.; ui.label(RichText::new("Login").size(32.)); - let mut input = NativeTextInput::new(&mut self.key).with_hint_text("npub/nsec"); - input.render(ui, services); + let input = NativeTextInput::new(&mut self.key).with_hint_text("npub/nsec"); + ui.add(input); if Button::new().show(ui, |ui| ui.label("Login")).clicked() { if let Ok((hrp, key)) = bech32::decode(&self.key) { match hrp.to_lowercase().as_str() { "nsec" => { - services.login.login(LoginKind::PrivateKey { - key: key.as_slice().try_into().unwrap(), - }); - services.navigate(Routes::HomePage); + let mut ids = services.ctx.accounts.add_account( + enostr::Keypair::from_secret( + SecretKey::from_slice(key.as_slice()).unwrap(), + ), + ); + ids.process_action( + services.ctx.unknown_ids, + services.ctx.ndb, + &services.tx, + ); + services.ctx.accounts.select_account(0); + services.navigate(RouteType::HomePage); } "npub" | "nprofile" => { - services.login.login(LoginKind::PublicKey { - key: key.as_slice().try_into().unwrap(), - }); - services.navigate(Routes::HomePage); + let mut ids = + services.ctx.accounts.add_account(enostr::Keypair::new( + enostr::Pubkey::new(key.as_slice().try_into().unwrap()), + None, + )); + ids.process_action( + services.ctx.unknown_ids, + services.ctx.ndb, + &services.tx, + ); + services.ctx.accounts.select_account(0); + services.navigate(RouteType::HomePage); } _ => {} } } else if let Ok(pk) = hex::decode(&self.key) { if let Ok(pk) = pk.as_slice().try_into() { - services.login.login(LoginKind::PublicKey { key: pk }); - services.navigate(Routes::HomePage); + let mut ids = services.ctx.accounts.add_account( + enostr::Keypair::new(enostr::Pubkey::new(pk), None), + ); + ids.process_action( + services.ctx.unknown_ids, + services.ctx.ndb, + &services.tx, + ); + services.navigate(RouteType::HomePage); return; } } @@ -64,4 +87,8 @@ impl NostrWidget for LoginPage { }) .inner } + + fn update(&mut self, _services: &mut RouteServices<'_, '_>) -> anyhow::Result<()> { + Ok(()) + } } diff --git a/src/route/mod.rs b/src/route/mod.rs index 4b43c00..9e4cb73 100644 --- a/src/route/mod.rs +++ b/src/route/mod.rs @@ -1,34 +1,43 @@ -use crate::app::NativeLayerOps; use crate::link::NostrLink; -use crate::login::Login; -use crate::note_util::OwnedNote; use crate::route::home::HomePage; use crate::route::login::LoginPage; use crate::route::stream::StreamPage; -use crate::services::image_cache::ImageCache; -use crate::services::ndb_wrapper::NDBWrapper; -use crate::widgets::{Header, NostrWidget}; -use egui::{Context, Response, Ui}; -use egui_inbox::{UiInbox, UiInboxSender}; +use crate::services::ffmpeg_loader::FfmpegLoader; +use crate::widgets::{Header, NostrWidget, PlaceholderRect}; +use anyhow::{bail, Result}; +use egui::{Context, Image, Response, TextureHandle, Ui}; +use egui_inbox::{RequestRepaintTrait, UiInbox, UiInboxSender}; +use enostr::{EventClientMessage, Note}; +use itertools::Itertools; use log::{info, warn}; -use nostr_sdk::{Client, Event, JsonUtil}; -use nostrdb::{Ndb, Transaction}; -use std::path::PathBuf; +use nostr::{ClientMessage, Event, EventBuilder, JsonUtil, Kind, Tag}; +use nostrdb::{Ndb, NdbProfile, NoteKey, Transaction}; +use notedeck::{AppContext, ImageCache}; +use poll_promise::Promise; +use std::path::{Path, PathBuf}; +use std::sync::mpsc; mod home; mod login; mod stream; +pub mod page { + use crate::route::{home, login, stream}; + pub use home::HomePage; + pub use login::LoginPage; + pub use stream::StreamPage; +} + #[derive(PartialEq)] -pub enum Routes { +pub enum RouteType { HomePage, EventPage { link: NostrLink, - event: Option, + event: Option, }, ProfilePage { link: NostrLink, - profile: Option, + profile: Option, }, LoginPage, @@ -37,156 +46,129 @@ pub enum Routes { } #[derive(PartialEq)] -pub enum RouteAction { - ShowKeyboard, - HideKeyboard, +pub enum RouteAction {} + +pub struct RouteServices<'a, 'ctx> { + pub router: mpsc::Sender, + pub tx: Transaction, + pub egui: Context, + pub ctx: &'a mut AppContext<'ctx>, } -pub struct Router { - current: Routes, - current_widget: Option>, - router: UiInbox, - - ctx: Context, - ndb: NDBWrapper, - login: Login, - client: Client, - image_cache: ImageCache, - native_layer: T, -} - -impl Drop for Router { - fn drop(&mut self) { - self.login.save(&mut self.native_layer) - } -} - -impl Router { - pub fn new( - data_path: PathBuf, - ctx: Context, - client: Client, - ndb: Ndb, - native_layer: T, - ) -> Self { - let mut login = Login::new(); - login.load(&native_layer); - - Self { - current: Routes::HomePage, - current_widget: None, - router: UiInbox::new(), - ctx: ctx.clone(), - ndb: NDBWrapper::new(ctx.clone(), ndb.clone(), client.clone()), - client, - login, - image_cache: ImageCache::new(data_path, ctx.clone()), - native_layer, - } - } - - fn load_widget(&mut self, route: Routes, tx: &Transaction) { - match &route { - Routes::HomePage => { - let w = HomePage::new(&self.ndb, tx); - self.current_widget = Some(Box::new(w)); - } - Routes::EventPage { link, .. } => { - let w = StreamPage::new_from_link(&self.ndb, tx, link.clone()); - self.current_widget = Some(Box::new(w)); - } - Routes::LoginPage => { - let w = LoginPage::new(); - self.current_widget = Some(Box::new(w)); - } - _ => warn!("Not implemented"), - } - self.current = route; - } - - pub fn show(&mut self, ui: &mut Ui) -> Response { - let tx = self.ndb.start_transaction(); - - // handle app state changes - let q = self.router.read(ui); - for r in q { - if let Routes::Action(a) = r { - match a { - RouteAction::ShowKeyboard => self.native_layer.show_keyboard(), - RouteAction::HideKeyboard => self.native_layer.hide_keyboard(), - _ => info!("Not implemented"), - } - } else { - self.load_widget(r, &tx); - } - } - - // load homepage on start - if self.current_widget.is_none() { - self.load_widget(Routes::HomePage, &tx); - } - - let mut svc = RouteServices { - context: self.ctx.clone(), - router: self.router.sender(), - client: self.client.clone(), - ndb: &self.ndb, - tx: &tx, - login: &mut self.login, - img_cache: &self.image_cache, - }; - - // display app - ui.vertical(|ui| { - Header::new().render(ui, &mut svc); - if let Some(w) = self.current_widget.as_mut() { - w.render(ui, &mut svc) - } else { - ui.label("No widget") - } - }) - .response - } -} - -pub struct RouteServices<'a> { - pub context: Context, //cloned - pub router: UiInboxSender, //cloned - pub client: Client, - - pub ndb: &'a NDBWrapper, //ref - pub tx: &'a Transaction, //ref - pub login: &'a mut Login, //ref - pub img_cache: &'a ImageCache, //ref -} - -impl<'a> RouteServices<'a> { - pub fn navigate(&self, route: Routes) { - if let Err(e) = self.router.send(route) { - warn!("Failed to navigate"); - } +impl<'a, 'ctx> RouteServices<'a, 'ctx> { + pub fn navigate(&self, route: RouteType) { + self.router.send(route).expect("route send failed"); + self.egui.request_repaint(); } pub fn action(&self, route: RouteAction) { - if let Err(e) = self.router.send(Routes::Action(route)) { - warn!("Failed to navigate"); - } + self.router + .send(RouteType::Action(route)) + .expect("route send failed"); + self.egui.request_repaint(); } - pub fn broadcast_event(&self, event: Event) { - let client = self.client.clone(); - + pub fn broadcast_event(&mut self, event: Event) { let ev_json = event.as_json(); - if let Err(e) = self.ndb.submit_event(&ev_json) { + if let Err(e) = self.ctx.ndb.process_event(&ev_json) { warn!("Failed to submit event {}", e); } - tokio::spawn(async move { - match client.send_event(event).await { - Ok(e) => { - info!("Broadcast event: {:?}", e) - } - Err(e) => warn!("Failed to broadcast event: {:?}", e), + self.ctx + .pool + .send(&enostr::ClientMessage::Event(EventClientMessage { + note_json: ev_json, + })); + } + + /// Load/Fetch profiles + pub fn profile(&self, pk: &[u8; 32]) -> Option> { + // TODO + None + } + + /// Load image from URL + pub fn image<'img, 'b>(&'b mut self, url: &'b str) -> Image<'img> { + image_from_cache(self.ctx.img_cache, &self.egui, url) + } + + /// Load image from bytes + pub fn image_bytes(&self, name: &'static str, data: &'static [u8]) -> Image<'_> { + // TODO: loader + Image::from_bytes(name, data) + } + + pub fn write_live_chat_msg(&self, link: &NostrLink, msg: &str) -> Option { + if msg.len() == 0 { + return None; + } + if let Some(acc) = self.ctx.accounts.get_selected_account() { + if let Some(key) = &acc.secret_key { + let nostr_key = + nostr::Keys::new(nostr::SecretKey::from_slice(key.as_secret_bytes()).unwrap()); + return Some( + EventBuilder::new(Kind::LiveEventMessage, msg) + .tag(Tag::parse(&link.to_tag()).unwrap()) + .sign_with_keys(&nostr_key) + .ok()?, + ); } - }); + } + None } } + +pub fn image_from_cache<'a>(img_cache: &mut ImageCache, ctx: &Context, url: &str) -> Image<'a> { + let m_cached_promise = img_cache.map().get(url); + if m_cached_promise.is_none() { + let fetch = fetch_img(img_cache, ctx, url); + img_cache.map_mut().insert(url.to_string(), fetch); + } + Image::new(url.to_string()) +} + +fn fetch_img( + img_cache: &ImageCache, + ctx: &Context, + url: &str, +) -> Promise> { + let k = ImageCache::key(url); + let dst_path = img_cache.cache_dir.join(k); + if dst_path.exists() { + let ctx = ctx.clone(); + let url = url.to_owned(); + let dst_path = dst_path.clone(); + Promise::spawn_async(async move { + match FfmpegLoader::new().load_image(dst_path) { + Ok(img) => Ok(ctx.load_texture(&url, img, Default::default())), + Err(e) => Err(notedeck::Error::Generic(e.to_string())), + } + }) + } else { + fetch_img_from_net(&dst_path, ctx, url) + } +} + +fn fetch_img_from_net( + cache_path: &Path, + ctx: &Context, + url: &str, +) -> Promise> { + let (sender, promise) = Promise::new(); + let request = ehttp::Request::get(url); + let ctx = ctx.clone(); + let cloned_url = url.to_owned(); + let cache_path = cache_path.to_owned(); + ehttp::fetch(request, move |response| { + let handle = response.map_err(notedeck::Error::Generic).map(|img| { + std::fs::write(&cache_path, &img.bytes).unwrap(); + let img_loaded = FfmpegLoader::new().load_image(cache_path).unwrap(); + + ctx.load_texture(&cloned_url, img_loaded, Default::default()) + }); + + sender.send(handle); + ctx.request_repaint(); + }); + + promise +} diff --git a/src/route/stream.rs b/src/route/stream.rs index b42a2b0..5abd3bf 100644 --- a/src/route/stream.rs +++ b/src/route/stream.rs @@ -1,42 +1,51 @@ use crate::link::NostrLink; -use crate::note_util::OwnedNote; use crate::route::RouteServices; -use crate::services::ndb_wrapper::{NDBWrapper, SubWrapper}; use crate::stream_info::StreamInfo; use crate::theme::{MARGIN_DEFAULT, NEUTRAL_800, ROUNDING_DEFAULT}; -use crate::widgets::{Chat, NostrWidget, PlaceholderRect, StreamPlayer, StreamTitle, WriteChat}; +use crate::widgets::{ + sub_or_poll, Chat, NostrWidget, PlaceholderRect, StreamPlayer, StreamTitle, WriteChat, +}; use egui::{vec2, Align, Frame, Layout, Response, Stroke, Ui, Vec2, Widget}; -use nostrdb::{Filter, Note, NoteKey, Transaction}; +use nostrdb::{Filter, Note, NoteKey, Subscription}; + +use crate::note_ref::NoteRef; use std::borrow::Borrow; +use std::collections::HashSet; pub struct StreamPage { link: NostrLink, - event: Option, + event: Option, player: Option, chat: Option, - sub: SubWrapper, new_msg: WriteChat, + + events: HashSet, + sub: Option, } impl StreamPage { - pub fn new_from_link(ndb: &NDBWrapper, tx: &Transaction, link: NostrLink) -> Self { - let f: Filter = link.borrow().try_into().unwrap(); - let f = [f.limit_mut(1)]; - let (sub, events) = ndb.subscribe_with_results("streams", &f, tx, 1); + pub fn new_from_link(link: NostrLink) -> Self { Self { - link: link.clone(), - sub, - event: events.first().map(|n| OwnedNote(n.note_key.as_u64())), + new_msg: WriteChat::new(link.clone()), + link, + event: None, chat: None, player: None, - new_msg: WriteChat::new(link), + events: HashSet::new(), + sub: None, } } + + fn get_filters(&self) -> Vec { + let f: Filter = self.link.borrow().try_into().unwrap(); + vec![f.limit_mut(1)] + } + fn render_mobile( &mut self, event: &Note<'_>, ui: &mut Ui, - services: &mut RouteServices<'_>, + services: &mut RouteServices<'_, '_>, ) -> Response { let chat_h = 60.0; let w = ui.available_width(); @@ -80,7 +89,7 @@ impl StreamPage { &mut self, event: &Note<'_>, ui: &mut Ui, - services: &mut RouteServices<'_>, + services: &mut RouteServices<'_, '_>, ) -> Response { let max_h = ui.available_height(); let chat_w = 450.0; @@ -136,21 +145,10 @@ impl StreamPage { } impl NostrWidget for StreamPage { - fn render(&mut self, ui: &mut Ui, services: &mut RouteServices<'_>) -> Response { - let poll = services.ndb.poll(&self.sub, 1); - if let Some(k) = poll.first() { - self.event = Some(OwnedNote(k.as_u64())) - } + fn render(&mut self, ui: &mut Ui, services: &mut RouteServices<'_, '_>) -> Response { + let events: Vec = vec![]; - let event = if let Some(k) = &self.event { - services - .ndb - .get_note_by_key(services.tx, NoteKey::new(k.0)) - .ok() - } else { - None - }; - if let Some(event) = event { + if let Some(event) = events.first() { if let Some(stream) = event.stream() { if self.player.is_none() { let p = StreamPlayer::new(ui.ctx(), &stream.to_string()); @@ -159,8 +157,8 @@ impl NostrWidget for StreamPage { } if self.chat.is_none() { - let ok = OwnedNote(event.key().unwrap().as_u64()); - let chat = Chat::new(self.link.clone(), ok, services.ndb, services.tx); + let ok = event.key().unwrap(); + let chat = Chat::new(self.link.clone(), ok); self.chat = Some(chat); } @@ -173,4 +171,20 @@ impl NostrWidget for StreamPage { ui.label("Loading..") } } + + fn update(&mut self, services: &mut RouteServices<'_, '_>) -> anyhow::Result<()> { + let filt = self.get_filters(); + sub_or_poll( + services.ctx.ndb, + &services.tx, + &mut services.ctx.pool, + &mut self.events, + &mut self.sub, + filt, + )?; + if let Some(c) = self.chat.as_mut() { + c.update(services)?; + } + Ok(()) + } } diff --git a/src/services/ffmpeg_loader.rs b/src/services/ffmpeg_loader.rs index 0e6911d..d2f2533 100644 --- a/src/services/ffmpeg_loader.rs +++ b/src/services/ffmpeg_loader.rs @@ -17,7 +17,7 @@ impl FfmpegLoader { Self::load_image_from_demuxer(demux) } - pub fn load_image_bytes<'a>( + pub fn load_image_bytes( &self, key: &str, data: &'static [u8], diff --git a/src/services/image_cache.rs b/src/services/image_cache.rs deleted file mode 100644 index 5d22d0c..0000000 --- a/src/services/image_cache.rs +++ /dev/null @@ -1,186 +0,0 @@ -use crate::services::ffmpeg_loader::FfmpegLoader; -use crate::theme::NEUTRAL_800; -use anyhow::{Error, Result}; -use egui::{ColorImage, Context, Image, ImageData, TextureHandle, TextureOptions}; -use itertools::Itertools; -use log::{info, warn}; -use lru::LruCache; -use nostr_sdk::util::hex; -use resvg::usvg::Transform; -use sha2::{Digest, Sha256}; -use std::collections::VecDeque; -use std::fs; -use std::num::NonZeroUsize; -use std::path::PathBuf; -use std::sync::{Arc, Mutex}; - -type ImageCacheStore = Arc>>; - -#[derive(PartialEq, Eq, Hash, Clone)] -struct LoadRequest(String); - -pub struct ImageCache { - ctx: Context, - dir: PathBuf, - placeholder: TextureHandle, - cache: ImageCacheStore, - fetch_queue: Arc>>, -} - -impl ImageCache { - pub fn new(data_path: PathBuf, ctx: Context) -> Self { - let out = data_path.join("cache/images"); - fs::create_dir_all(&out).unwrap(); - - let placeholder = ctx.load_texture( - "placeholder", - ImageData::from(ColorImage::new([1, 1], NEUTRAL_800)), - TextureOptions::default(), - ); - let cache = Arc::new(Mutex::new(LruCache::new(NonZeroUsize::new(1_000).unwrap()))); - let fetch_queue = Arc::new(Mutex::new(VecDeque::::new())); - let cc = cache.clone(); - let fq = fetch_queue.clone(); - let out_dir = out.clone(); - let ctx_clone = ctx.clone(); - let placeholder_clone = placeholder.clone(); - tokio::spawn(async move { - loop { - let next = fq.lock().unwrap().pop_front(); - if let Some(next) = next { - let path = Self::find(&out_dir, &next.0); - if path.exists() { - let th = Self::load_image_texture(&ctx_clone, path, &next.0) - .unwrap_or(placeholder_clone.clone()); - cc.lock().unwrap().put(next.0, th); - ctx_clone.request_repaint(); - } else { - match Self::download_image_to_disk(&path, &next.0).await { - Ok(()) => { - let th = Self::load_image_texture(&ctx_clone, path, &next.0) - .unwrap_or(placeholder_clone.clone()); - cc.lock().unwrap().put(next.0, th); - ctx_clone.request_repaint(); - } - Err(e) => { - warn!("Failed to download image {}: {}", next.0, e); - cc.lock().unwrap().put(next.0, placeholder_clone.clone()); - ctx_clone.request_repaint(); - } - } - } - } else { - tokio::time::sleep(std::time::Duration::from_millis(30)).await; - } - } - }); - Self { - ctx, - dir: out, - placeholder, - cache, - fetch_queue, - } - } - - pub fn find(dir: &PathBuf, url: U) -> PathBuf - where - U: Into, - { - let mut sha = Sha256::new(); - sha2::digest::Update::update(&mut sha, url.into().as_bytes()); - let hash = hex::encode(sha.finalize()); - dir.join(PathBuf::from(hash[0..2].to_string())) - .join(PathBuf::from(hash)) - } - - fn load_bytes_impl(url: &str, bytes: &'static [u8]) -> Result { - if url.ends_with(".svg") { - Self::load_svg(bytes) - } else { - let loader = FfmpegLoader::new(); - loader.load_image_bytes(url, bytes) - } - } - - pub fn load_bytes<'a, U>(&self, url: U, bytes: &'static [u8]) -> Image<'a> - where - U: Into, - { - let url = url.into(); - match Self::load_bytes_impl(&url, bytes) { - Ok(i) => { - let tex = self - .ctx - .load_texture(url, ImageData::from(i), TextureOptions::default()); - Image::from_texture(&tex) - } - Err(e) => { - panic!("Failed to load image: {}", e); - } - } - } - - pub fn load<'a, U>(&self, url: U) -> Image<'a> - where - U: Into, - { - let u = url.into(); - if let Ok(mut c) = self.cache.lock() { - if let Some(i) = c.get(&u) { - return Image::from_texture(i); - } - } - if let Ok(mut ql) = self.fetch_queue.lock() { - let lr = LoadRequest(u.clone()); - if !ql.contains(&lr) { - ql.push_back(lr); - } - } - Image::from_texture(&self.placeholder) - } - - /// Download an image to disk - async fn download_image_to_disk(dst: &PathBuf, u: &str) -> Result<()> { - info!("Fetching image: {}", &u); - tokio::fs::create_dir_all(dst.parent().unwrap()).await?; - - let data = reqwest::get(u).await?; - let img_data = data.bytes().await?; - tokio::fs::write(dst, img_data).await?; - Ok(()) - } - - /// Load an image from disk into an egui texture handle - fn load_image_texture(ctx: &Context, path: PathBuf, key: &str) -> Option { - let loader = FfmpegLoader::new(); - match loader.load_image(path) { - Ok(i) => Some(ctx.load_texture(key, ImageData::from(i), TextureOptions::default())), - Err(e) => { - println!("Failed to load image: {}", e); - None - } - } - } - - fn load_svg(svg: &[u8]) -> Result { - use resvg::tiny_skia::Pixmap; - use resvg::usvg::{Options, Tree}; - - let opt = Options::default(); - let rtree = Tree::from_data(svg, &opt) - .map_err(|err| err.to_string()) - .map_err(|e| Error::msg(e))?; - - let size = rtree.size().to_int_size(); - let (w, h) = (size.width(), size.height()); - - let mut pixmap = Pixmap::new(w, h) - .ok_or_else(|| Error::msg(format!("Failed to create SVG Pixmap of size {w}x{h}")))?; - - resvg::render(&rtree, Transform::default(), &mut pixmap.as_mut()); - let image = ColorImage::from_rgba_unmultiplied([w as _, h as _], pixmap.data()); - - Ok(image) - } -} diff --git a/src/services/mod.rs b/src/services/mod.rs index 1142a8e..178715f 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -1,5 +1 @@ -pub mod image_cache; -pub mod ndb_wrapper; -pub mod query; - -mod ffmpeg_loader; +pub mod ffmpeg_loader; diff --git a/src/services/ndb_wrapper.rs b/src/services/ndb_wrapper.rs deleted file mode 100644 index d81319a..0000000 --- a/src/services/ndb_wrapper.rs +++ /dev/null @@ -1,161 +0,0 @@ -use crate::services::query::QueryManager; -use log::warn; -use nostr_sdk::{nostr, Client, JsonUtil, Kind, PublicKey, RelayPoolNotification}; -use nostrdb::{ - Error, Filter, Ndb, NdbProfile, Note, NoteKey, ProfileRecord, QueryResult, Subscription, - Transaction, -}; -use std::collections::HashSet; -use std::sync::Mutex; - -pub struct NDBWrapper { - ctx: egui::Context, - ndb: Ndb, - client: Client, - query_manager: QueryManager, - profiles: Mutex>, -} - -/// Automatic cleanup for subscriptions -pub struct SubWrapper { - ndb: Ndb, - subscription: Subscription, -} - -impl SubWrapper { - pub fn new(ndb: Ndb, subscription: Subscription) -> Self { - Self { ndb, subscription } - } -} - -impl From<&SubWrapper> for u64 { - fn from(val: &SubWrapper) -> Self { - val.subscription.id() - } -} - -impl Drop for SubWrapper { - fn drop(&mut self) { - self.ndb.unsubscribe(self.subscription).unwrap() - } -} - -impl NDBWrapper { - pub fn new(ctx: egui::Context, ndb: Ndb, client: Client) -> Self { - let client_clone = client.clone(); - let ndb_clone = ndb.clone(); - let ctx_clone = ctx.clone(); - tokio::spawn(async move { - let mut notifications = client_clone.notifications(); - while let Ok(e) = notifications.recv().await { - match e { - RelayPoolNotification::Event { event, .. } => { - if let Err(e) = ndb_clone.process_event(event.as_json().as_str()) { - warn!("Failed to process event: {:?}", e); - } else { - ctx_clone.request_repaint(); - } - } - _ => { - // dont care - } - } - } - }); - let qm = QueryManager::new(client.clone()); - - Self { - ctx, - ndb, - client, - query_manager: qm, - profiles: Mutex::new(HashSet::new()), - } - } - - pub fn start_transaction(&self) -> Transaction { - Transaction::new(&self.ndb).unwrap() - } - - pub fn subscribe(&self, id: &str, filters: &[Filter]) -> SubWrapper { - let sub = self.ndb.subscribe(filters).unwrap(); - // very lazy conversion - let filters: Vec = filters - .iter() - .map(|f| nostr_sdk::Filter::from_json(f.json().unwrap()).unwrap()) - .collect(); - self.query_manager.queue_query(id, filters); - SubWrapper::new(self.ndb.clone(), sub) - } - - pub fn unsubscribe(&self, sub: &SubWrapper) { - self.ndb.unsubscribe(sub.subscription).unwrap() - } - - pub fn subscribe_with_results<'a>( - &self, - id: &str, - filters: &[Filter], - tx: &'a Transaction, - max_results: i32, - ) -> (SubWrapper, Vec>) { - let sub = self.subscribe(id, filters); - let q = self.query(tx, filters, max_results); - (sub, q) - } - - pub fn query<'a>( - &self, - tx: &'a Transaction, - filters: &[Filter], - max_results: i32, - ) -> Vec> { - self.ndb.query(tx, filters, max_results).unwrap() - } - - pub fn poll(&self, sub: &SubWrapper, max_results: u32) -> Vec { - self.ndb.poll_for_notes(sub.subscription, max_results) - } - - pub fn get_note_by_key<'a>( - &self, - tx: &'a Transaction, - key: NoteKey, - ) -> Result, Error> { - self.ndb.get_note_by_key(tx, key) - } - - pub fn get_profile_by_pubkey<'a>( - &self, - tx: &'a Transaction, - pubkey: &[u8; 32], - ) -> Result, Error> { - self.ndb.get_profile_by_pubkey(tx, pubkey) - } - - pub fn fetch_profile<'a>( - &self, - tx: &'a Transaction, - pubkey: &[u8; 32], - ) -> (Option>, Option) { - let p = self - .get_profile_by_pubkey(tx, pubkey) - .map_or(None, |p| p.record().profile()); - - // TODO: fix this shit - if p.is_none() && self.profiles.lock().unwrap().insert(*pubkey) { - self.query_manager.queue_query( - "profile", - &[nostr::Filter::new() - .kinds([Kind::Metadata]) - .authors([PublicKey::from_slice(pubkey).unwrap()])], - ) - } - let sub = None; - (p, sub) - } - - pub fn submit_event(&self, ev: &str) -> Result<(), Error> { - self.ndb.process_event(ev) - } -} diff --git a/src/services/query.rs b/src/services/query.rs deleted file mode 100644 index c694a84..0000000 --- a/src/services/query.rs +++ /dev/null @@ -1,192 +0,0 @@ -use anyhow::Error; -use chrono::Utc; -use log::{error, info}; -use nostr_sdk::prelude::StreamExt; -use nostr_sdk::Kind::Metadata; -use nostr_sdk::{Client, Filter, SubscribeAutoCloseOptions, SubscriptionId}; -use std::collections::{HashMap, HashSet}; -use std::sync::Arc; -use std::time::Duration; -use tokio::sync::mpsc::{unbounded_channel, UnboundedSender}; -use tokio::sync::RwLock; -use tokio::task::JoinHandle; -use uuid::Uuid; - -#[async_trait::async_trait] -pub trait QueryClient { - async fn subscribe(&self, id: &str, filters: &[QueryFilter]) -> Result<(), Error>; -} - -pub type QueryFilter = Filter; - -pub struct Query { - pub id: String, - queue: HashSet, - traces: HashSet, -} - -#[derive(Hash, Eq, PartialEq, Debug)] -pub struct QueryTrace { - /// Subscription id on the relay - pub id: Uuid, - /// Filters associated with this subscription - pub filters: Vec, - /// When the query was created - pub queued: u64, - /// When the query was sent to the relay - pub sent: Option, - /// When EOSE was received - pub eose: Option, -} - -impl Query { - pub fn new(id: &str) -> Self { - Self { - id: id.to_string(), - queue: HashSet::new(), - traces: HashSet::new(), - } - } - - /// Add filters to query - pub fn add(&mut self, filter: Vec) { - for f in filter { - self.queue.insert(f); - } - } - - /// Return next query batch - pub fn next(&mut self) -> Option { - let mut next: Vec = self.queue.drain().collect(); - if next.is_empty() { - return None; - } - let now = Utc::now(); - let id = Uuid::new_v4(); - - // remove filters already sent - next.retain(|f| { - self.traces.is_empty() || !self.traces.iter().all(|y| y.filters.iter().any(|z| z == f)) - }); - - // force profile queries into single filter - if next.iter().all(|f| { - if let Some(k) = &f.kinds { - k.len() == 1 && k.first().unwrap().as_u16() == 0 - } else { - false - } - }) { - next = vec![Filter::new().kinds([Metadata]).authors( - next.iter() - .flat_map(|f| f.authors.as_ref().unwrap().clone()), - )] - } - - if next.is_empty() { - return None; - } - Some(QueryTrace { - id, - filters: next, - queued: now.timestamp() as u64, - sent: None, - eose: None, - }) - } -} - -struct QueueDefer { - id: String, - filters: Vec, -} - -pub struct QueryManager { - client: C, - queries: Arc>>, - queue_into_queries: UnboundedSender, - sender: JoinHandle<()>, -} - -impl QueryManager -where - C: QueryClient + Clone + Send + Sync + 'static, -{ - pub(crate) fn new(client: C) -> Self { - let queries = Arc::new(RwLock::new(HashMap::new())); - let (tx, mut rx) = unbounded_channel::(); - Self { - client: client.clone(), - queries: queries.clone(), - queue_into_queries: tx, - sender: tokio::spawn(async move { - loop { - { - let mut q = queries.write().await; - while let Ok(x) = rx.try_recv() { - Self::push_filters(&mut q, &x.id, x.filters); - } - for (k, v) in q.iter_mut() { - if let Some(qt) = v.next() { - info!("Sending trace: {:?}", qt); - match client - .subscribe(&qt.id.to_string(), qt.filters.as_slice()) - .await - { - Ok(_) => {} - Err(e) => { - error!("Failed to subscribe to query filters: {}", e); - } - } - } - } - } - tokio::time::sleep(Duration::from_millis(100)).await; - } - }), - } - } - - pub async fn query(&mut self, id: &str, filters: F) - where - F: Into>, - { - let mut qq = self.queries.write().await; - Self::push_filters(&mut qq, id, filters.into()); - } - - fn push_filters(qq: &mut HashMap, id: &str, filters: Vec) { - if let Some(q) = qq.get_mut(id) { - q.add(filters); - } else { - let mut q = Query::new(id); - q.add(filters); - qq.insert(id.to_string(), q); - } - } - - pub fn queue_query(&self, id: &str, filters: F) - where - F: Into>, - { - self.queue_into_queries - .send(QueueDefer { - id: id.to_string(), - filters: filters.into(), - }) - .unwrap() - } -} - -#[async_trait::async_trait] -impl QueryClient for Client { - async fn subscribe(&self, id: &str, filters: &[QueryFilter]) -> Result<(), Error> { - self.subscribe_with_id( - SubscriptionId::new(id), - filters.into(), - Some(SubscribeAutoCloseOptions::default()), - ) - .await?; - Ok(()) - } -} diff --git a/src/widgets/avatar.rs b/src/widgets/avatar.rs index 2c6dfb9..3cf9840 100644 --- a/src/widgets/avatar.rs +++ b/src/widgets/avatar.rs @@ -1,42 +1,37 @@ -use crate::route::RouteServices; -use crate::services::ndb_wrapper::SubWrapper; +use crate::route::image_from_cache; use egui::{vec2, Color32, Pos2, Response, Rounding, Sense, Ui, Vec2, Widget}; -use nostrdb::NdbProfile; +use nostrdb::{Ndb, NdbProfile, Transaction}; +use notedeck::ImageCache; -pub struct Avatar<'a> { - image: Option<&'a str>, - sub: Option, +pub struct Avatar { + image: Option, size: Option, - services: &'a RouteServices<'a>, } -impl<'a> Avatar<'a> { - pub fn new_optional(img: Option<&'a str>, services: &'a RouteServices<'a>) -> Self { +impl Avatar { + pub fn new_optional(img: Option<&str>) -> Self { Self { - image: img, - sub: None, + image: img.map(String::from), size: None, - services, } } - pub fn from_profile(p: &'a Option>, services: &'a RouteServices<'a>) -> Self { + pub fn pubkey(pk: &[u8; 32], ndb: &Ndb, tx: &Transaction) -> Self { + let picture = ndb + .get_profile_by_pubkey(&tx, pk) + .map(|p| p.record().profile().map(|p| p.picture()).unwrap_or(None)) + .unwrap_or(None); + Self { + image: picture.map(|s| s.to_string()), + size: None, + } + } + + pub fn from_profile(p: &Option>) -> Self { let img = p.map(|f| f.picture()).unwrap_or(None); Self { - image: img, - sub: None, + image: img.map(String::from), size: None, - services, - } - } - - pub fn pubkey(pk: &[u8; 32], services: &'a RouteServices<'a>) -> Self { - let (p, sub) = services.ndb.fetch_profile(services.tx, pk); - Self { - image: p.map(|f| f.picture()).unwrap_or(None), - sub, - size: None, - services, } } @@ -54,21 +49,15 @@ impl<'a> Avatar<'a> { ); response } -} -impl<'a> Widget for Avatar<'a> { - fn ui(self, ui: &mut Ui) -> Response { + pub fn render(&self, ui: &mut Ui, img_cache: &mut ImageCache) -> Response { let size_v = self.size.unwrap_or(40.); let size = Vec2::new(size_v, size_v); if !ui.is_visible() { return Self::placeholder(ui, size_v); } - match self - .image - .as_ref() - .map(|i| self.services.img_cache.load(*i)) - { - Some(img) => img + match &self.image { + Some(img) => image_from_cache(img_cache, ui.ctx(), &img) .fit_to_exact_size(size) .rounding(Rounding::same(size_v)) .ui(ui), diff --git a/src/widgets/chat.rs b/src/widgets/chat.rs index 615b90e..837ede9 100644 --- a/src/widgets/chat.rs +++ b/src/widgets/chat.rs @@ -1,62 +1,44 @@ use crate::link::NostrLink; -use crate::note_util::OwnedNote; +use crate::note_ref::NoteRef; use crate::route::RouteServices; -use crate::services::ndb_wrapper::{NDBWrapper, SubWrapper}; use crate::widgets::chat_message::ChatMessage; -use crate::widgets::NostrWidget; +use crate::widgets::{sub_or_poll, NostrWidget}; use egui::{Frame, Margin, Response, ScrollArea, Ui}; use itertools::Itertools; -use nostrdb::{Filter, Note, NoteKey, Transaction}; +use nostrdb::{Filter, NoteKey, Subscription}; +use std::collections::HashSet; pub struct Chat { link: NostrLink, - stream: OwnedNote, - events: Vec, - sub: SubWrapper, + stream: NoteKey, + events: HashSet, + sub: Option, } impl Chat { - pub fn new(link: NostrLink, stream: OwnedNote, ndb: &NDBWrapper, tx: &Transaction) -> Self { - let filter = Filter::new() - .kinds([1_311]) - .tags([link.to_tag_value()], 'a') - .build(); - let filter = [filter]; - - let (sub, events) = ndb.subscribe_with_results("live-chat", &filter, tx, 500); - + pub fn new<'a>(link: NostrLink, stream: NoteKey) -> Self { Self { link, - sub, stream, - events: events - .iter() - .map(|n| OwnedNote(n.note_key.as_u64())) - .collect(), + events: HashSet::new(), + sub: None, } } + + pub fn get_filter(&self) -> Filter { + Filter::new() + .kinds([1_311]) + .tags([self.link.to_tag_value()], 'a') + .build() + } } impl NostrWidget for Chat { - fn render(&mut self, ui: &mut Ui, services: &mut RouteServices<'_>) -> Response { - let poll = services.ndb.poll(&self.sub, 500); - poll.iter() - .for_each(|n| self.events.push(OwnedNote(n.as_u64()))); - - let events: Vec = self - .events - .iter() - .map_while(|n| { - services - .ndb - .get_note_by_key(services.tx, NoteKey::new(n.0)) - .ok() - }) - .collect(); - + fn render(&mut self, ui: &mut Ui, services: &mut RouteServices<'_, '_>) -> Response { let stream = services + .ctx .ndb - .get_note_by_key(services.tx, NoteKey::new(self.stream.0)) + .get_note_by_key(&services.tx, self.stream) .unwrap(); ScrollArea::vertical() @@ -67,12 +49,17 @@ impl NostrWidget for Chat { .show(ui, |ui| { ui.vertical(|ui| { ui.spacing_mut().item_spacing.y = 8.0; - for ev in events - .into_iter() - .sorted_by(|a, b| a.created_at().cmp(&b.created_at())) + for ev in self + .events + .iter() + .sorted_by(|a, b| a.created_at.cmp(&b.created_at)) { - let c = ChatMessage::new(&stream, &ev, services); - ui.add(c); + if let Ok(ev) = + services.ctx.ndb.get_note_by_key(&services.tx, ev.key) + { + ChatMessage::new(&stream, &ev, &None) + .render(ui, services.ctx.img_cache); + } } }) }) @@ -80,4 +67,16 @@ impl NostrWidget for Chat { }) .inner } + + fn update(&mut self, services: &mut RouteServices<'_, '_>) -> anyhow::Result<()> { + let filters = vec![self.get_filter()]; + sub_or_poll( + services.ctx.ndb, + &services.tx, + &mut services.ctx.pool, + &mut self.events, + &mut self.sub, + filters, + ) + } } diff --git a/src/widgets/chat_message.rs b/src/widgets/chat_message.rs index b13e402..2478a26 100644 --- a/src/widgets/chat_message.rs +++ b/src/widgets/chat_message.rs @@ -1,50 +1,39 @@ -use crate::route::RouteServices; -use crate::services::ndb_wrapper::SubWrapper; use crate::stream_info::StreamInfo; use crate::theme::{NEUTRAL_500, PRIMARY}; -use crate::widgets::Avatar; +use crate::widgets::{Avatar, NostrWidget}; use eframe::epaint::text::TextWrapMode; use egui::text::LayoutJob; -use egui::{Align, Color32, Label, Response, TextFormat, Ui, Widget}; +use egui::{Align, Color32, Label, Response, TextFormat, Ui}; use nostrdb::{NdbProfile, Note}; +use notedeck::ImageCache; pub struct ChatMessage<'a> { stream: &'a Note<'a>, ev: &'a Note<'a>, - services: &'a RouteServices<'a>, - profile: (Option>, Option), + profile: &'a Option>, } impl<'a> ChatMessage<'a> { pub fn new( stream: &'a Note<'a>, ev: &'a Note<'a>, - services: &'a RouteServices<'a>, + profile: &'a Option>, ) -> ChatMessage<'a> { ChatMessage { stream, ev, - services, - profile: services.ndb.fetch_profile(services.tx, ev.pubkey()), + profile, } } -} -impl<'a> Widget for ChatMessage<'a> { - fn ui(self, ui: &mut Ui) -> Response { + pub fn render(&mut self, ui: &mut Ui, img_cache: &mut ImageCache) -> Response { ui.horizontal_wrapped(|ui| { let mut job = LayoutJob::default(); // TODO: avoid this somehow job.wrap.break_anywhere = true; let is_host = self.stream.host().eq(self.ev.pubkey()); - let profile = self - .services - .ndb - .get_profile_by_pubkey(self.services.tx, self.ev.pubkey()) - .map_or(None, |p| p.record().profile()); - - let name = profile.map_or("Nostrich", |f| f.name().map_or("Nostrich", |f| f)); + let name = self.profile.map_or("Nostrich", |f| f.name().map_or("Nostrich", |f| f)); let name_color = if is_host { PRIMARY } else { NEUTRAL_500 }; @@ -57,7 +46,9 @@ impl<'a> Widget for ChatMessage<'a> { format.color = Color32::WHITE; job.append(self.ev.content(), 5.0, format.clone()); - ui.add(Avatar::from_profile(&profile, self.services).size(24.)); + Avatar::from_profile(&self.profile) + .size(24.) + .render(ui, img_cache); ui.add(Label::new(job).wrap_mode(TextWrapMode::Wrap)); }) .response diff --git a/src/widgets/header.rs b/src/widgets/header.rs index bac240d..a2e9329 100644 --- a/src/widgets/header.rs +++ b/src/widgets/header.rs @@ -1,4 +1,4 @@ -use crate::route::{RouteServices, Routes}; +use crate::route::{RouteServices, RouteType}; use crate::widgets::avatar::Avatar; use crate::widgets::{Button, NostrWidget}; use eframe::emath::Align; @@ -14,7 +14,7 @@ impl Header { } impl NostrWidget for Header { - fn render(&mut self, ui: &mut Ui, services: &mut RouteServices<'_>) -> Response { + fn render(&mut self, ui: &mut Ui, services: &mut RouteServices<'_, '_>) -> Response { let logo_bytes = include_bytes!("../resources/logo.svg"); Frame::none() .outer_margin(Margin::symmetric(16., 8.)) @@ -25,22 +25,21 @@ impl NostrWidget for Header { |ui| { ui.style_mut().spacing.item_spacing.x = 16.; if services - .img_cache - .load_bytes("logo.svg", logo_bytes) + .image_bytes("logo.svg", logo_bytes) .max_height(24.) .sense(Sense::click()) .ui(ui) .on_hover_and_drag_cursor(CursorIcon::PointingHand) .clicked() { - services.navigate(Routes::HomePage); + services.navigate(RouteType::HomePage); } ui.with_layout(Layout::right_to_left(Align::Center), |ui| { - if let Some(pk) = services.login.public_key() { - ui.add(Avatar::pubkey(&pk, services)); + if let Some(acc) = services.ctx.accounts.get_selected_account() { + Avatar::pubkey(&acc.pubkey, services.ctx.ndb, &services.tx).render(ui, services.ctx.img_cache); } else if Button::new().show(ui, |ui| ui.label("Login")).clicked() { - services.navigate(Routes::LoginPage); + services.navigate(RouteType::LoginPage); } }); }, @@ -48,4 +47,8 @@ impl NostrWidget for Header { }) .response } + + fn update(&mut self, _services: &mut RouteServices<'_, '_>) -> anyhow::Result<()> { + Ok(()) + } } diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index 2488579..eed309a 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -13,11 +13,48 @@ mod text_input; mod username; mod write_chat; +use crate::note_ref::NoteRef; use crate::route::RouteServices; use egui::{Response, Ui}; +use enostr::RelayPool; +use nostrdb::{Filter, Ndb, Subscription, Transaction}; +use std::collections::HashSet; +/// A stateful widget which requests nostr data pub trait NostrWidget { - fn render(&mut self, ui: &mut Ui, services: &mut RouteServices<'_>) -> Response; + /// Render with widget on the UI + fn render(&mut self, ui: &mut Ui, services: &mut RouteServices<'_, '_>) -> Response; + + /// Update widget on draw + fn update(&mut self, services: &mut RouteServices<'_, '_>) -> anyhow::Result<()>; +} + +/// On widget update call this to update NDB data +pub fn sub_or_poll( + ndb: &Ndb, + tx: &Transaction, + pool: &mut RelayPool, + store: &mut HashSet, + sub: &mut Option, + filters: Vec, +) -> anyhow::Result<()> { + if let Some(sub) = sub { + ndb.poll_for_notes(*sub, 500).into_iter().for_each(|e| { + if let Ok(note) = ndb.get_note_by_key(tx, e) { + store.insert(NoteRef::from_note(¬e)); + } + }); + } else { + let s = ndb.subscribe(filters.as_slice())?; + sub.replace(s); + ndb.query(tx, filters.as_slice(), 500)? + .into_iter() + .for_each(|e| { + store.insert(NoteRef::from_query_result(e)); + }); + pool.subscribe(format!("ndb-{}", s.id()), filters); + } + Ok(()) } pub use self::avatar::Avatar; diff --git a/src/widgets/profile.rs b/src/widgets/profile.rs index 3954617..4ac62cd 100644 --- a/src/widgets/profile.rs +++ b/src/widgets/profile.rs @@ -1,46 +1,31 @@ use crate::route::RouteServices; -use crate::services::image_cache::ImageCache; -use crate::services::ndb_wrapper::SubWrapper; use crate::theme::FONT_SIZE; -use crate::widgets::{Avatar, Username}; -use egui::{Response, Ui, Widget}; -use nostrdb::NdbProfile; +use crate::widgets::{Avatar, NostrWidget, Username}; +use egui::{Response, Ui}; pub struct Profile<'a> { size: f32, pubkey: &'a [u8; 32], - profile: Option>, - sub: Option, - img_cache: &'a ImageCache, - services: &'a RouteServices<'a>, } impl<'a> Profile<'a> { - pub fn new(pubkey: &'a [u8; 32], services: &'a RouteServices<'a>) -> Self { - let (p, sub) = services.ndb.fetch_profile(services.tx, pubkey); - - Self { - pubkey, - size: 40., - profile: p, - img_cache: services.img_cache, - sub, - services, - } + pub fn new(pubkey: &'a [u8; 32]) -> Self { + Self { pubkey, size: 40. } } pub fn size(self, size: f32) -> Self { Self { size, ..self } } -} -impl<'a> Widget for Profile<'a> { - fn ui(self, ui: &mut Ui) -> Response { + pub fn render(self, ui: &mut Ui, services: &mut RouteServices<'_, '_>) -> Response { ui.horizontal(|ui| { ui.spacing_mut().item_spacing.x = 8.; - ui.add(Avatar::from_profile(&self.profile, self.services).size(self.size)); - ui.add(Username::new(&self.profile, FONT_SIZE)) + let profile = services.profile(self.pubkey); + Avatar::from_profile(&profile) + .size(self.size) + .render(ui, services.ctx.img_cache); + ui.add(Username::new(&profile, FONT_SIZE)) }) .response } diff --git a/src/widgets/stream_list.rs b/src/widgets/stream_list.rs index acc6318..add83cf 100644 --- a/src/widgets/stream_list.rs +++ b/src/widgets/stream_list.rs @@ -1,35 +1,31 @@ -use crate::note_store::NoteStore; +use crate::note_view::NotesView; use crate::route::RouteServices; use crate::stream_info::StreamInfo; use crate::widgets::stream_tile::StreamEvent; -use egui::{vec2, Frame, Grid, Margin, Response, Ui, Widget, WidgetText}; +use crate::widgets::NostrWidget; +use egui::{vec2, Frame, Grid, Margin, Response, Ui, WidgetText}; use itertools::Itertools; pub struct StreamList<'a> { id: egui::Id, - streams: &'a NoteStore<'a>, - services: &'a RouteServices<'a>, + streams: NotesView<'a>, heading: Option, } impl<'a> StreamList<'a> { pub fn new( id: egui::Id, - streams: &'a NoteStore<'a>, - services: &'a RouteServices<'a>, + streams: NotesView<'a>, heading: Option>, ) -> Self { Self { id, streams, - services, heading: heading.map(Into::into), } } -} -impl Widget for StreamList<'_> { - fn ui(self, ui: &mut Ui) -> Response { + pub fn render(&mut self, ui: &mut Ui, services: &mut RouteServices<'_, '_>) -> Response { let cols = match ui.available_width() as u16 { 720..1080 => 2, 1080..1300 => 3, @@ -46,7 +42,7 @@ impl Widget for StreamList<'_> { .show(ui, |ui| { let grid_spacing_consumed = (cols - 1) as f32 * grid_padding; let g_w = (ui.available_width() - grid_spacing_consumed) / cols as f32; - if let Some(heading) = self.heading { + if let Some(heading) = self.heading.take() { ui.label(heading); } Grid::new(self.id) @@ -58,10 +54,9 @@ impl Widget for StreamList<'_> { .cmp(&b.status()) .then(a.starts().cmp(&b.starts()).reverse()) }) { - ui.add_sized( - vec2(g_w, (g_w / 16.0) * 9.0), - StreamEvent::new(event, self.services), - ); + ui.allocate_ui(vec2(g_w, (g_w / 16.0) * 9.0), |ui| { + StreamEvent::new(event).render(ui, services) + }); ctr += 1; if ctr % cols == 0 { ui.end_row(); @@ -71,4 +66,4 @@ impl Widget for StreamList<'_> { }) .response } -} +} \ No newline at end of file diff --git a/src/widgets/stream_tile.rs b/src/widgets/stream_tile.rs index 3296d93..0428746 100644 --- a/src/widgets/stream_tile.rs +++ b/src/widgets/stream_tile.rs @@ -1,34 +1,34 @@ use crate::link::NostrLink; -use crate::route::{RouteServices, Routes}; +use crate::route::{RouteServices, RouteType}; use crate::stream_info::{StreamInfo, StreamStatus}; use crate::theme::{NEUTRAL_800, NEUTRAL_900, PRIMARY, ROUNDING_DEFAULT}; use crate::widgets::avatar::Avatar; +use crate::widgets::NostrWidget; use eframe::epaint::{Rounding, Vec2}; use egui::epaint::RectShape; use egui::load::TexturePoll; use egui::{ vec2, Color32, CursorIcon, FontId, Label, Pos2, Rect, Response, RichText, Sense, TextWrapMode, - Ui, Widget, + Ui, }; use nostrdb::Note; pub struct StreamEvent<'a> { event: &'a Note<'a>, - services: &'a RouteServices<'a>, } impl<'a> StreamEvent<'a> { - pub fn new(event: &'a Note<'a>, services: &'a RouteServices) -> Self { - Self { event, services } + pub fn new(event: &'a Note<'a>) -> Self { + Self { event } } } -impl Widget for StreamEvent<'_> { - fn ui(self, ui: &mut Ui) -> Response { +impl NostrWidget for StreamEvent<'_> { + fn render(&mut self, ui: &mut Ui, services: &mut RouteServices<'_, '_>) -> Response { ui.vertical(|ui| { ui.style_mut().spacing.item_spacing = Vec2::new(12., 16.); let host = self.event.host(); - let (host_profile, _sub) = self.services.ndb.fetch_profile(self.services.tx, host); + let host_profile = services.profile(host); let w = ui.available_width(); let h = (w / 16.0) * 9.0; @@ -36,7 +36,7 @@ impl Widget for StreamEvent<'_> { let (response, painter) = ui.allocate_painter(Vec2::new(w, h), Sense::click()); let cover = if ui.is_visible() { - self.event.image().map(|p| self.services.img_cache.load(p)) + self.event.image().map(|p| services.image(p)) } else { None }; @@ -110,13 +110,15 @@ impl Widget for StreamEvent<'_> { } let response = response.on_hover_and_drag_cursor(CursorIcon::PointingHand); if response.clicked() { - self.services.navigate(Routes::EventPage { + services.navigate(RouteType::EventPage { link: NostrLink::from_note(self.event), event: None, }); } ui.horizontal(|ui| { - ui.add(Avatar::from_profile(&host_profile, self.services).size(40.)); + Avatar::from_profile(&host_profile) + .size(40.) + .render(ui, services.ctx.img_cache); let title = RichText::new(self.event.title().unwrap_or("Untitled")) .size(16.) .color(Color32::WHITE); @@ -125,4 +127,8 @@ impl Widget for StreamEvent<'_> { }) .response } + + fn update(&mut self, _services: &mut RouteServices<'_, '_>) -> anyhow::Result<()> { + Ok(()) + } } diff --git a/src/widgets/stream_title.rs b/src/widgets/stream_title.rs index b48876f..6f79984 100644 --- a/src/widgets/stream_title.rs +++ b/src/widgets/stream_title.rs @@ -10,13 +10,13 @@ pub struct StreamTitle<'a> { } impl<'a> StreamTitle<'a> { - pub fn new(event: &'a Note<'a>) -> StreamTitle { + pub fn new(event: &'a Note<'a>) -> StreamTitle<'a> { StreamTitle { event } } } -impl<'a> NostrWidget for StreamTitle<'a> { - fn render(&mut self, ui: &mut Ui, services: &mut RouteServices<'_>) -> Response { +impl NostrWidget for StreamTitle<'_> { + fn render(&mut self, ui: &mut Ui, services: &mut RouteServices<'_, '_>) -> Response { Frame::none() .outer_margin(Margin::symmetric(12., 8.)) .show(ui, |ui| { @@ -26,7 +26,9 @@ impl<'a> NostrWidget for StreamTitle<'a> { .color(Color32::WHITE); ui.add(Label::new(title.strong()).wrap_mode(TextWrapMode::Truncate)); - ui.add(Profile::new(self.event.host(), services).size(32.)); + Profile::new(self.event.host()) + .size(32.) + .render(ui, services); if let Some(summary) = self .event @@ -41,4 +43,8 @@ impl<'a> NostrWidget for StreamTitle<'a> { }) .response } + + fn update(&mut self, _services: &mut RouteServices<'_, '_>) -> anyhow::Result<()> { + Ok(()) + } } diff --git a/src/widgets/text_input.rs b/src/widgets/text_input.rs index 40ceddd..4548fdd 100644 --- a/src/widgets/text_input.rs +++ b/src/widgets/text_input.rs @@ -1,7 +1,5 @@ -use crate::route::{RouteAction, RouteServices}; use crate::theme::{MARGIN_DEFAULT, NEUTRAL_500, NEUTRAL_900, ROUNDING_DEFAULT}; -use crate::widgets::NostrWidget; -use egui::{Frame, Response, TextEdit, Ui}; +use egui::{Frame, Response, TextEdit, Ui, Widget}; /// Wrap the [TextEdit] widget to handle native keyboard pub struct NativeTextInput<'a> { @@ -30,8 +28,8 @@ impl<'a> NativeTextInput<'a> { } } -impl<'a> NostrWidget for NativeTextInput<'a> { - fn render(&mut self, ui: &mut Ui, services: &mut RouteServices<'_>) -> Response { +impl Widget for NativeTextInput<'_> { + fn ui(self, ui: &mut Ui) -> Response { let mut editor = TextEdit::multiline(self.text) .frame(false) .desired_rows(1) @@ -49,12 +47,6 @@ impl<'a> NostrWidget for NativeTextInput<'a> { } else { ui.add(editor) }; - if response.lost_focus() { - services.action(RouteAction::HideKeyboard); - } - if response.gained_focus() { - services.action(RouteAction::ShowKeyboard); - } response } } diff --git a/src/widgets/write_chat.rs b/src/widgets/write_chat.rs index 64afa47..139b2e6 100644 --- a/src/widgets/write_chat.rs +++ b/src/widgets/write_chat.rs @@ -5,6 +5,8 @@ use crate::widgets::{NativeTextInput, NostrWidget}; use eframe::emath::Align; use egui::{Frame, Layout, Response, Sense, Ui, Widget}; use log::info; +use nostrdb::Filter; +use notedeck::AppContext; pub struct WriteChat { link: NostrLink, @@ -21,7 +23,7 @@ impl WriteChat { } impl NostrWidget for WriteChat { - fn render(&mut self, ui: &mut Ui, services: &mut RouteServices<'_>) -> Response { + fn render(&mut self, ui: &mut Ui, services: &mut RouteServices<'_, '_>) -> Response { let logo_bytes = include_bytes!("../resources/send-03.svg"); Frame::none() .inner_margin(MARGIN_DEFAULT) @@ -31,16 +33,13 @@ impl NostrWidget for WriteChat { .show(ui, |ui| { ui.with_layout(Layout::right_to_left(Align::Center), |ui| { if services - .img_cache - .load_bytes("send-03.svg", logo_bytes) + .image_bytes("send-03.svg", logo_bytes) .sense(Sense::click()) .ui(ui) .clicked() || self.msg.ends_with('\n') { - if let Ok(ev) = services - .login - .write_live_chat_msg(&self.link, &self.msg.trim()) + if let Some(ev) = services.write_live_chat_msg(&self.link, &self.msg.trim()) { info!("Sending: {:?}", ev); services.broadcast_event(ev); @@ -48,11 +47,13 @@ impl NostrWidget for WriteChat { self.msg.clear(); } - let mut editor = - NativeTextInput::new(&mut self.msg).with_hint_text("Message.."); - editor.render(ui, services) + ui.add(NativeTextInput::new(&mut self.msg).with_hint_text("Message..")); }); }) .response } + + fn update(&mut self, _services: &mut RouteServices<'_, '_>) -> anyhow::Result<()> { + Ok(()) + } }