From 68a8ec7a69af9a0c47efc42be780d905f4e56a99 Mon Sep 17 00:00:00 2001 From: reya <123083837+reyamir@users.noreply.github.com> Date: Fri, 10 Oct 2025 17:36:38 +0700 Subject: [PATCH] feat: custom gossip implementation (#181) * . * rename global to app_state * refactor event tracker * gossip * . * . --- Cargo.lock | 186 +++--- crates/{global => app_state}/Cargo.toml | 2 +- crates/{global => app_state}/src/constants.rs | 0 crates/app_state/src/lib.rs | 44 ++ crates/{global => app_state}/src/paths.rs | 0 crates/app_state/src/state/gossip.rs | 217 +++++++ crates/app_state/src/state/mod.rs | 516 +++++++++++++++++ crates/auto_update/Cargo.toml | 2 +- crates/auto_update/src/lib.rs | 2 +- crates/client_keys/Cargo.toml | 2 +- crates/client_keys/src/lib.rs | 4 +- crates/common/Cargo.toml | 2 +- crates/common/src/display.rs | 2 +- crates/coop/Cargo.toml | 2 +- crates/coop/src/chatspace.rs | 528 ++---------------- crates/coop/src/main.rs | 4 +- crates/coop/src/views/account.rs | 5 +- crates/coop/src/views/chat/mod.rs | 8 +- crates/coop/src/views/compose.rs | 4 +- crates/coop/src/views/edit_profile.rs | 2 +- crates/coop/src/views/login.rs | 4 +- crates/coop/src/views/new_account.rs | 4 +- crates/coop/src/views/onboarding.rs | 6 +- crates/coop/src/views/screening.rs | 4 +- crates/coop/src/views/setup_relay.rs | 4 +- crates/coop/src/views/sidebar/mod.rs | 5 +- crates/coop/src/views/user_profile.rs | 2 +- crates/global/src/lib.rs | 261 --------- crates/registry/Cargo.toml | 2 +- crates/registry/src/lib.rs | 3 +- crates/registry/src/room.rs | 98 ++-- crates/settings/Cargo.toml | 2 +- crates/settings/src/lib.rs | 4 +- crates/signer_proxy/Cargo.toml | 2 +- 34 files changed, 1020 insertions(+), 913 deletions(-) rename crates/{global => app_state}/Cargo.toml (94%) rename crates/{global => app_state}/src/constants.rs (100%) create mode 100644 crates/app_state/src/lib.rs rename crates/{global => app_state}/src/paths.rs (100%) create mode 100644 crates/app_state/src/state/gossip.rs create mode 100644 crates/app_state/src/state/mod.rs delete mode 100644 crates/global/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 6acf3a8..0baf66f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -91,6 +91,21 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +[[package]] +name = "app_state" +version = "0.2.11" +dependencies = [ + "anyhow", + "dirs 5.0.1", + "flume", + "log", + "nostr-lmdb", + "nostr-sdk", + "rustls", + "smol", + "whoami", +] + [[package]] name = "arbitrary" version = "1.4.2" @@ -167,6 +182,9 @@ dependencies = [ "serde", "serde_repr", "url", + "wayland-backend", + "wayland-client", + "wayland-protocols 0.32.9", "zbus", ] @@ -498,9 +516,9 @@ name = "auto_update" version = "0.2.11" dependencies = [ "anyhow", + "app_state", "cargo-packager-updater", "common", - "global", "gpui", "log", "nostr-sdk", @@ -609,7 +627,7 @@ dependencies = [ "bitflags 2.9.4", "cexpr", "clang-sys", - "itertools 0.11.0", + "itertools 0.13.0", "log", "prettyplease", "proc-macro2", @@ -629,7 +647,7 @@ dependencies = [ "bitflags 2.9.4", "cexpr", "clang-sys", - "itertools 0.11.0", + "itertools 0.13.0", "log", "prettyplease", "proc-macro2", @@ -1101,7 +1119,7 @@ name = "client_keys" version = "0.2.11" dependencies = [ "anyhow", - "global", + "app_state", "gpui", "log", "nostr-sdk", @@ -1218,9 +1236,9 @@ name = "common" version = "0.2.11" dependencies = [ "anyhow", + "app_state", "chrono", "futures", - "global", "gpui", "itertools 0.13.0", "log", @@ -1292,6 +1310,7 @@ name = "coop" version = "0.2.11" dependencies = [ "anyhow", + "app_state", "assets", "auto_update", "client_keys", @@ -1299,7 +1318,6 @@ dependencies = [ "dirs 5.0.1", "flume", "futures", - "global", "gpui", "gpui_tokio", "i18n", @@ -1785,7 +1803,7 @@ dependencies = [ "cc", "memchr", "rustc_version", - "toml 0.9.7", + "toml 0.9.8", "vswhom", "winreg", ] @@ -2434,21 +2452,6 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" -[[package]] -name = "global" -version = "0.2.11" -dependencies = [ - "anyhow", - "dirs 5.0.1", - "flume", - "log", - "nostr-lmdb", - "nostr-sdk", - "rustls", - "smol", - "whoami", -] - [[package]] name = "globset" version = "0.4.16" @@ -2529,8 +2532,8 @@ dependencies = [ [[package]] name = "gpui" -version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#963204c99ddb08bd1dacbb5168194b0a39fa5ad2" +version = "0.2.0" +source = "git+https://github.com/zed-industries/zed#a4ec693e3471f87aea89bc4c8132330b0fde3a9f" dependencies = [ "anyhow", "as-raw-xcb-connection", @@ -2598,7 +2601,7 @@ dependencies = [ "wayland-backend", "wayland-client", "wayland-cursor", - "wayland-protocols", + "wayland-protocols 0.31.2", "wayland-protocols-plasma", "windows 0.61.3", "windows-core 0.61.2", @@ -2624,7 +2627,7 @@ dependencies = [ [[package]] name = "gpui-macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#963204c99ddb08bd1dacbb5168194b0a39fa5ad2" +source = "git+https://github.com/zed-industries/zed#a4ec693e3471f87aea89bc4c8132330b0fde3a9f" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -2636,7 +2639,7 @@ dependencies = [ [[package]] name = "gpui_tokio" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#963204c99ddb08bd1dacbb5168194b0a39fa5ad2" +source = "git+https://github.com/zed-industries/zed#a4ec693e3471f87aea89bc4c8132330b0fde3a9f" dependencies = [ "anyhow", "gpui", @@ -2672,13 +2675,14 @@ dependencies = [ [[package]] name = "half" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +checksum = "e54c115d4f30f52c67202f079c5f9d8b49db4691f460fdb0b4c2e838261b2ba5" dependencies = [ "cfg-if", "crunchy", "num-traits", + "zerocopy", ] [[package]] @@ -2705,6 +2709,12 @@ dependencies = [ "foldhash", ] +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + [[package]] name = "heck" version = "0.4.1" @@ -2859,7 +2869,7 @@ dependencies = [ [[package]] name = "http_client_tls" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#963204c99ddb08bd1dacbb5168194b0a39fa5ad2" +source = "git+https://github.com/zed-industries/zed#a4ec693e3471f87aea89bc4c8132330b0fde3a9f" dependencies = [ "rustls", "rustls-platform-verifier", @@ -3167,7 +3177,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" dependencies = [ "equivalent", - "hashbrown 0.15.5", + "hashbrown 0.16.0", "serde", "serde_core", ] @@ -3427,9 +3437,9 @@ checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" [[package]] name = "libc" -version = "0.2.176" +version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "libfuzzer-sys" @@ -3448,7 +3458,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.53.5", ] [[package]] @@ -3904,7 +3914,7 @@ dependencies = [ [[package]] name = "nostr" version = "0.43.0" -source = "git+https://github.com/rust-nostr/nostr#ca8eda4903f34ca3d948493257bb96489dcd4b3d" +source = "git+https://github.com/rust-nostr/nostr#293f5d6747c57681e3c1e37670a59ca0f6802382" dependencies = [ "aes", "base64", @@ -3928,7 +3938,7 @@ dependencies = [ [[package]] name = "nostr-connect" version = "0.43.0" -source = "git+https://github.com/rust-nostr/nostr#ca8eda4903f34ca3d948493257bb96489dcd4b3d" +source = "git+https://github.com/rust-nostr/nostr#293f5d6747c57681e3c1e37670a59ca0f6802382" dependencies = [ "async-utility", "nostr", @@ -3940,7 +3950,7 @@ dependencies = [ [[package]] name = "nostr-database" version = "0.43.0" -source = "git+https://github.com/rust-nostr/nostr#ca8eda4903f34ca3d948493257bb96489dcd4b3d" +source = "git+https://github.com/rust-nostr/nostr#293f5d6747c57681e3c1e37670a59ca0f6802382" dependencies = [ "flatbuffers", "lru", @@ -3951,7 +3961,7 @@ dependencies = [ [[package]] name = "nostr-lmdb" version = "0.43.0" -source = "git+https://github.com/rust-nostr/nostr#ca8eda4903f34ca3d948493257bb96489dcd4b3d" +source = "git+https://github.com/rust-nostr/nostr#293f5d6747c57681e3c1e37670a59ca0f6802382" dependencies = [ "async-utility", "flume", @@ -3965,7 +3975,7 @@ dependencies = [ [[package]] name = "nostr-relay-pool" version = "0.43.0" -source = "git+https://github.com/rust-nostr/nostr#ca8eda4903f34ca3d948493257bb96489dcd4b3d" +source = "git+https://github.com/rust-nostr/nostr#293f5d6747c57681e3c1e37670a59ca0f6802382" dependencies = [ "async-utility", "async-wsocket", @@ -3982,7 +3992,7 @@ dependencies = [ [[package]] name = "nostr-sdk" version = "0.43.0" -source = "git+https://github.com/rust-nostr/nostr#ca8eda4903f34ca3d948493257bb96489dcd4b3d" +source = "git+https://github.com/rust-nostr/nostr#293f5d6747c57681e3c1e37670a59ca0f6802382" dependencies = [ "async-utility", "nostr", @@ -4706,7 +4716,7 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit 0.23.6", + "toml_edit 0.23.7", ] [[package]] @@ -4770,9 +4780,9 @@ dependencies = [ [[package]] name = "pxfm" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83f9b339b02259ada5c0f4a389b7fb472f933aa17ce176fd2ad98f28bb401fde" +checksum = "a3cbdf373972bf78df4d3b518d07003938e2c7d1fb5891e55f9cb6df57009d84" dependencies = [ "num-traits", ] @@ -5135,9 +5145,9 @@ name = "registry" version = "0.2.11" dependencies = [ "anyhow", + "app_state", "common", "fuzzy-matcher", - "global", "gpui", "itertools 0.13.0", "log", @@ -5200,7 +5210,7 @@ dependencies = [ [[package]] name = "reqwest_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#963204c99ddb08bd1dacbb5168194b0a39fa5ad2" +source = "git+https://github.com/zed-industries/zed#a4ec693e3471f87aea89bc4c8132330b0fde3a9f" dependencies = [ "anyhow", "bytes", @@ -5255,7 +5265,7 @@ dependencies = [ [[package]] name = "rope" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#963204c99ddb08bd1dacbb5168194b0a39fa5ad2" +source = "git+https://github.com/zed-industries/zed#a4ec693e3471f87aea89bc4c8132330b0fde3a9f" dependencies = [ "arrayvec", "log", @@ -5831,9 +5841,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5417783452c2be558477e104686f7de5dae53dba813c28435e0e70f82d9b04ee" +checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" dependencies = [ "serde_core", ] @@ -5868,7 +5878,7 @@ name = "settings" version = "0.2.11" dependencies = [ "anyhow", - "global", + "app_state", "gpui", "log", "nostr-sdk", @@ -5935,10 +5945,10 @@ name = "signer_proxy" version = "0.2.11" dependencies = [ "anyhow", + "app_state", "atomic-destructor", "bytes", "futures", - "global", "http-body-util", "hyper", "hyper-util", @@ -6066,9 +6076,9 @@ dependencies = [ [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "stacker" @@ -6747,14 +6757,14 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.7" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00e5e5d9bf2475ac9d4f0d9edab68cc573dc2fd644b0dba36b0c30a92dd9eaa0" +checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" dependencies = [ "indexmap", "serde_core", - "serde_spanned 1.0.2", - "toml_datetime 0.7.2", + "serde_spanned 1.0.3", + "toml_datetime 0.7.3", "toml_parser", "toml_writer", "winnow", @@ -6771,9 +6781,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" dependencies = [ "serde_core", ] @@ -6794,21 +6804,21 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.23.6" +version = "0.23.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3effe7c0e86fdff4f69cdd2ccc1b96f933e24811c5441d44904e8683e27184b" +checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" dependencies = [ "indexmap", - "toml_datetime 0.7.2", + "toml_datetime 0.7.3", "toml_parser", "winnow", ] [[package]] name = "toml_parser" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" dependencies = [ "winnow", ] @@ -6821,9 +6831,9 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "toml_writer" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d163a63c116ce562a22cda521fcc4d79152e7aba014456fb5eb442f6d6a10109" +checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" [[package]] name = "tower" @@ -7490,6 +7500,18 @@ dependencies = [ "wayland-scanner", ] +[[package]] +name = "wayland-protocols" +version = "0.32.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" +dependencies = [ + "bitflags 2.9.4", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + [[package]] name = "wayland-protocols-plasma" version = "0.2.0" @@ -7499,7 +7521,7 @@ dependencies = [ "bitflags 2.9.4", "wayland-backend", "wayland-client", - "wayland-protocols", + "wayland-protocols 0.31.2", "wayland-scanner", ] @@ -7568,14 +7590,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e" dependencies = [ - "webpki-root-certs 1.0.2", + "webpki-root-certs 1.0.3", ] [[package]] name = "webpki-root-certs" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4ffd8df1c57e87c325000a3d6ef93db75279dc3a231125aac571650f22b12a" +checksum = "05d651ec480de84b762e7be71e6efa7461699c19d9e2c272c8d93455f567786e" dependencies = [ "rustls-pki-types", ] @@ -7586,14 +7608,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.2", + "webpki-roots 1.0.3", ] [[package]] name = "webpki-roots" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" +checksum = "32b130c0d2d49f8b6889abc456e795e82525204f27c42cf767cf0d7734e089b8" dependencies = [ "rustls-pki-types", ] @@ -8473,7 +8495,7 @@ dependencies = [ [[package]] name = "zed-collections" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#963204c99ddb08bd1dacbb5168194b0a39fa5ad2" +source = "git+https://github.com/zed-industries/zed#a4ec693e3471f87aea89bc4c8132330b0fde3a9f" dependencies = [ "indexmap", "rustc-hash 2.1.1", @@ -8483,7 +8505,7 @@ dependencies = [ [[package]] name = "zed-derive-refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#963204c99ddb08bd1dacbb5168194b0a39fa5ad2" +source = "git+https://github.com/zed-industries/zed#a4ec693e3471f87aea89bc4c8132330b0fde3a9f" dependencies = [ "proc-macro2", "quote", @@ -8518,7 +8540,7 @@ dependencies = [ [[package]] name = "zed-http-client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#963204c99ddb08bd1dacbb5168194b0a39fa5ad2" +source = "git+https://github.com/zed-industries/zed#a4ec693e3471f87aea89bc4c8132330b0fde3a9f" dependencies = [ "anyhow", "async-compression", @@ -8544,7 +8566,7 @@ dependencies = [ [[package]] name = "zed-media" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#963204c99ddb08bd1dacbb5168194b0a39fa5ad2" +source = "git+https://github.com/zed-industries/zed#a4ec693e3471f87aea89bc4c8132330b0fde3a9f" dependencies = [ "anyhow", "bindgen 0.71.1", @@ -8560,7 +8582,7 @@ dependencies = [ [[package]] name = "zed-perf" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#963204c99ddb08bd1dacbb5168194b0a39fa5ad2" +source = "git+https://github.com/zed-industries/zed#a4ec693e3471f87aea89bc4c8132330b0fde3a9f" dependencies = [ "serde", "serde_json", @@ -8571,7 +8593,7 @@ dependencies = [ [[package]] name = "zed-refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#963204c99ddb08bd1dacbb5168194b0a39fa5ad2" +source = "git+https://github.com/zed-industries/zed#a4ec693e3471f87aea89bc4c8132330b0fde3a9f" dependencies = [ "workspace-hack", "zed-derive-refineable", @@ -8650,7 +8672,7 @@ dependencies = [ [[package]] name = "zed-semantic-version" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#963204c99ddb08bd1dacbb5168194b0a39fa5ad2" +source = "git+https://github.com/zed-industries/zed#a4ec693e3471f87aea89bc4c8132330b0fde3a9f" dependencies = [ "anyhow", "serde", @@ -8660,7 +8682,7 @@ dependencies = [ [[package]] name = "zed-sum-tree" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#963204c99ddb08bd1dacbb5168194b0a39fa5ad2" +source = "git+https://github.com/zed-industries/zed#a4ec693e3471f87aea89bc4c8132330b0fde3a9f" dependencies = [ "arrayvec", "log", @@ -8671,7 +8693,7 @@ dependencies = [ [[package]] name = "zed-util" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#963204c99ddb08bd1dacbb5168194b0a39fa5ad2" +source = "git+https://github.com/zed-industries/zed#a4ec693e3471f87aea89bc4c8132330b0fde3a9f" dependencies = [ "anyhow", "async-fs", @@ -8707,7 +8729,7 @@ dependencies = [ [[package]] name = "zed-util-macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#963204c99ddb08bd1dacbb5168194b0a39fa5ad2" +source = "git+https://github.com/zed-industries/zed#a4ec693e3471f87aea89bc4c8132330b0fde3a9f" dependencies = [ "quote", "syn 2.0.106", diff --git a/crates/global/Cargo.toml b/crates/app_state/Cargo.toml similarity index 94% rename from crates/global/Cargo.toml rename to crates/app_state/Cargo.toml index 83c144e..77a88b7 100644 --- a/crates/global/Cargo.toml +++ b/crates/app_state/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "global" +name = "app_state" version.workspace = true edition.workspace = true publish.workspace = true diff --git a/crates/global/src/constants.rs b/crates/app_state/src/constants.rs similarity index 100% rename from crates/global/src/constants.rs rename to crates/app_state/src/constants.rs diff --git a/crates/app_state/src/lib.rs b/crates/app_state/src/lib.rs new file mode 100644 index 0000000..3c24878 --- /dev/null +++ b/crates/app_state/src/lib.rs @@ -0,0 +1,44 @@ +use std::sync::OnceLock; +use std::time::Duration; + +use nostr_lmdb::NostrLMDB; +use nostr_sdk::prelude::*; +use paths::nostr_file; + +use crate::state::AppState; + +pub mod constants; +pub mod paths; +pub mod state; + +static APP_STATE: OnceLock = OnceLock::new(); +static NOSTR_CLIENT: OnceLock = OnceLock::new(); + +/// Initialize the application state. +pub fn app_state() -> &'static AppState { + APP_STATE.get_or_init(AppState::new) +} + +/// Initialize the nostr client. +pub fn nostr_client() -> &'static Client { + NOSTR_CLIENT.get_or_init(|| { + // rustls uses the `aws_lc_rs` provider by default + // This only errors if the default provider has already + // been installed. We can ignore this `Result`. + rustls::crypto::aws_lc_rs::default_provider() + .install_default() + .ok(); + + let lmdb = NostrLMDB::open(nostr_file()).expect("Database is NOT initialized"); + + let opts = ClientOptions::new() + .gossip(false) + .automatic_authentication(false) + .verify_subscriptions(false) + .sleep_when_idle(SleepWhenIdle::Enabled { + timeout: Duration::from_secs(600), + }); + + ClientBuilder::default().database(lmdb).opts(opts).build() + }) +} diff --git a/crates/global/src/paths.rs b/crates/app_state/src/paths.rs similarity index 100% rename from crates/global/src/paths.rs rename to crates/app_state/src/paths.rs diff --git a/crates/app_state/src/state/gossip.rs b/crates/app_state/src/state/gossip.rs new file mode 100644 index 0000000..e531eeb --- /dev/null +++ b/crates/app_state/src/state/gossip.rs @@ -0,0 +1,217 @@ +use std::collections::{HashMap, HashSet}; +use std::time::Duration; + +use anyhow::{anyhow, Error}; +use nostr_sdk::prelude::*; + +use crate::constants::BOOTSTRAP_RELAYS; +use crate::state::SignalKind; +use crate::{app_state, nostr_client}; + +#[derive(Debug, Clone, Default)] +pub struct Gossip { + pub nip17: HashMap>, + pub nip65: HashMap)>>, +} + +impl Gossip { + pub fn insert(&mut self, event: &Event) { + match event.kind { + Kind::InboxRelays => { + let urls: Vec = nip17::extract_relay_list(event).cloned().collect(); + + if !urls.is_empty() { + self.nip17.entry(event.pubkey).or_default().extend(urls); + } + } + Kind::RelayList => { + let urls: Vec<(RelayUrl, Option)> = nip65::extract_relay_list(event) + .map(|(url, metadata)| (url.to_owned(), metadata.to_owned())) + .collect(); + + if !urls.is_empty() { + self.nip65.entry(event.pubkey).or_default().extend(urls); + } + } + _ => {} + } + } + + pub fn write_relays(&self, public_key: &PublicKey) -> Vec<&RelayUrl> { + self.nip65 + .get(public_key) + .map(|relays| { + relays + .iter() + .filter(|(_, metadata)| metadata.as_ref() != Some(&RelayMetadata::Write)) + .map(|(url, _)| url) + .take(3) + .collect() + }) + .unwrap_or_default() + } + + pub fn read_relays(&self, public_key: &PublicKey) -> Vec<&RelayUrl> { + self.nip65 + .get(public_key) + .map(|relays| { + relays + .iter() + .filter(|(_, metadata)| metadata.as_ref() != Some(&RelayMetadata::Read)) + .map(|(url, _)| url) + .take(3) + .collect() + }) + .unwrap_or_default() + } + + pub fn messaging_relays(&self, public_key: &PublicKey) -> Vec<&RelayUrl> { + self.nip17 + .get(public_key) + .map(|relays| relays.iter().collect()) + .unwrap_or_default() + } + + pub async fn get_nip65(&mut self, public_key: PublicKey) -> Result<(), Error> { + let client = nostr_client(); + let timeout = Duration::from_secs(5); + let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); + + let filter = Filter::new() + .kind(Kind::RelayList) + .author(public_key) + .limit(1); + + // Subscribe to events from the bootstrapping relays + client + .subscribe_to(BOOTSTRAP_RELAYS, filter.clone(), Some(opts)) + .await?; + + // Verify the received data after a timeout + smol::spawn(async move { + smol::Timer::after(timeout).await; + + if client.database().count(filter).await.unwrap_or(0) < 1 { + app_state() + .signal + .send(SignalKind::GossipRelaysNotFound) + .await; + } + }) + .detach(); + + Ok(()) + } + + pub async fn get_nip17(&mut self, public_key: PublicKey) -> Result<(), Error> { + let client = nostr_client(); + let timeout = Duration::from_secs(5); + let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); + + let filter = Filter::new() + .kind(Kind::InboxRelays) + .author(public_key) + .limit(1); + + let urls = self.write_relays(&public_key); + + // Ensure user's have at least one write relay + if urls.is_empty() { + return Err(anyhow!("NIP-17 relays are empty")); + } + + // Ensure connection to relays + for url in urls.iter().cloned() { + client.add_relay(url).await?; + client.connect_relay(url).await?; + } + + // Subscribe to events from the bootstrapping relays + client + .subscribe_to(urls, filter.clone(), Some(opts)) + .await?; + + // Verify the received data after a timeout + smol::spawn(async move { + smol::Timer::after(timeout).await; + + if client.database().count(filter).await.unwrap_or(0) < 1 { + app_state() + .signal + .send(SignalKind::MessagingRelaysNotFound) + .await; + } + }) + .detach(); + + Ok(()) + } + + pub async fn subscribe(&mut self, public_key: PublicKey, kind: Kind) -> Result<(), Error> { + let client = nostr_client(); + let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); + + let filter = Filter::new().author(public_key).kind(kind).limit(1); + let urls = self.write_relays(&public_key); + + // Ensure user's have at least one write relay + if urls.is_empty() { + return Err(anyhow!("NIP-65 relays are empty")); + } + + // Ensure connection to relays + for url in urls.iter().cloned() { + client.add_relay(url).await?; + client.connect_relay(url).await?; + } + + // Subscribe to filters to user's write relays + client.subscribe_to(urls, filter, Some(opts)).await?; + + Ok(()) + } + + pub async fn bulk_subscribe(&mut self, public_keys: HashSet) -> Result<(), Error> { + if public_keys.is_empty() { + return Err(anyhow!("You need at least one public key")); + } + + let client = nostr_client(); + let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); + + let kinds = vec![Kind::Metadata, Kind::ContactList, Kind::RelayList]; + let limit = public_keys.len() * kinds.len() + 20; + + let filter = Filter::new().authors(public_keys).kinds(kinds).limit(limit); + let urls = BOOTSTRAP_RELAYS; + + // Subscribe to filters to the bootstrap relays + client.subscribe_to(urls, filter, Some(opts)).await?; + + Ok(()) + } + + /// Monitor all gift wrap events in the messaging relays for a given public key + pub async fn monitor_inbox(&mut self, public_key: PublicKey) -> Result<(), Error> { + let client = nostr_client(); + let id = SubscriptionId::new("inbox"); + let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key); + let urls = self.messaging_relays(&public_key); + + // Ensure user's have at least one messaging relay + if urls.is_empty() { + return Err(anyhow!("Messaging relays are empty")); + } + + // Ensure connection to relays + for url in urls.iter().cloned() { + client.add_relay(url).await?; + client.connect_relay(url).await?; + } + + // Subscribe to filters to user's messaging relays + client.subscribe_with_id_to(urls, id, filter, None).await?; + + Ok(()) + } +} diff --git a/crates/app_state/src/state/mod.rs b/crates/app_state/src/state/mod.rs new file mode 100644 index 0000000..e3c5f1d --- /dev/null +++ b/crates/app_state/src/state/mod.rs @@ -0,0 +1,516 @@ +use std::borrow::Cow; +use std::collections::{HashMap, HashSet}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::time::Duration; + +use anyhow::{anyhow, Error}; +use flume::{Receiver, Sender}; +use nostr_sdk::prelude::*; +use smol::lock::RwLock; + +use crate::constants::{ + BOOTSTRAP_RELAYS, METADATA_BATCH_LIMIT, METADATA_BATCH_TIMEOUT, SEARCH_RELAYS, +}; +use crate::nostr_client; +use crate::paths::support_dir; +use crate::state::gossip::Gossip; + +pub mod gossip; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct AuthRequest { + pub url: RelayUrl, + pub challenge: String, + pub sending: bool, +} + +impl AuthRequest { + pub fn new(challenge: impl Into, url: RelayUrl) -> Self { + Self { + challenge: challenge.into(), + sending: false, + url, + } + } +} + +#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord)] +pub enum UnwrappingStatus { + #[default] + Initialized, + Processing, + Complete, +} + +/// Signals sent through the global event channel to notify UI +#[derive(Debug)] +pub enum SignalKind { + /// A signal to notify UI that the client's signer has been set + SignerSet(PublicKey), + + /// A signal to notify UI that the client's signer has been unset + SignerUnset, + + /// A signal to notify UI that the relay requires authentication + Auth(AuthRequest), + + /// A signal to notify UI that the browser proxy service is down + ProxyDown, + + /// A signal to notify UI that a new profile has been received + NewProfile(Profile), + + /// A signal to notify UI that a new gift wrap event has been received + NewMessage((EventId, Event)), + + /// A signal to notify UI that no messaging relays for current user was found + MessagingRelaysNotFound, + + /// A signal to notify UI that no gossip relays for current user was found + GossipRelaysNotFound, + + /// A signal to notify UI that gift wrap status has changed + GiftWrapStatus(UnwrappingStatus), +} + +#[derive(Debug)] +pub struct Signal { + rx: Receiver, + tx: Sender, +} + +impl Default for Signal { + fn default() -> Self { + Self::new() + } +} + +impl Signal { + pub fn new() -> Self { + let (tx, rx) = flume::bounded::(2048); + Self { rx, tx } + } + + pub fn receiver(&self) -> &Receiver { + &self.rx + } + + pub async fn send(&self, kind: SignalKind) { + if let Err(e) = self.tx.send_async(kind).await { + log::error!("Failed to send signal: {e}"); + } + } +} + +#[derive(Debug)] +pub struct Ingester { + rx: Receiver, + tx: Sender, +} + +impl Default for Ingester { + fn default() -> Self { + Self::new() + } +} + +impl Ingester { + pub fn new() -> Self { + let (tx, rx) = flume::bounded::(1024); + Self { rx, tx } + } + + pub fn receiver(&self) -> &Receiver { + &self.rx + } + + pub async fn send(&self, public_key: PublicKey) { + if let Err(e) = self.tx.send_async(public_key).await { + log::error!("Failed to send public key: {e}"); + } + } +} + +#[derive(Debug, Clone, Default)] +pub struct EventTracker { + /// Tracking events that have been resent by Coop in the current session + pub resent_ids: Vec>, + + /// Temporarily store events that need to be resent later + pub resend_queue: HashMap, + + /// Tracking events sent by Coop in the current session + pub sent_ids: HashSet, + + /// Tracking events seen on which relays in the current session + pub seen_on_relays: HashMap>, +} + +impl EventTracker { + pub fn resent_ids(&self) -> &Vec> { + &self.resent_ids + } + + pub fn resend_queue(&self) -> &HashMap { + &self.resend_queue + } + + pub fn sent_ids(&self) -> &HashSet { + &self.sent_ids + } + + pub fn seen_on_relays(&self) -> &HashMap> { + &self.seen_on_relays + } +} + +/// A simple storage to store all states that using across the application. +#[derive(Debug)] +pub struct AppState { + /// The timestamp when the application was initialized. + pub initialized_at: Timestamp, + + /// Whether this is the first run of the application. + pub is_first_run: AtomicBool, + + /// Whether gift wrap processing is in progress. + pub gift_wrap_processing: AtomicBool, + + /// Subscription ID for listening to gift wrap events from relays. + pub gift_wrap_sub_id: SubscriptionId, + + /// Auto-close options for relay subscriptions + pub auto_close_opts: Option, + + /// NIP-65: https://github.com/nostr-protocol/nips/blob/master/65.md + pub gossip: RwLock, + + /// Tracks activity related to Nostr events + pub event_tracker: RwLock, + + /// Signal channel for communication between Nostr and GPUI + pub signal: Signal, + + /// Ingester channel for processing public keys + pub ingester: Ingester, +} + +impl Default for AppState { + fn default() -> Self { + Self::new() + } +} + +impl AppState { + pub fn new() -> Self { + let first_run = Self::first_run(); + let initialized_at = Timestamp::now(); + let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); + + let signal = Signal::default(); + let ingester = Ingester::default(); + + Self { + initialized_at, + signal, + ingester, + is_first_run: AtomicBool::new(first_run), + gift_wrap_sub_id: SubscriptionId::new("inbox"), + gift_wrap_processing: AtomicBool::new(false), + auto_close_opts: Some(opts), + gossip: RwLock::new(Gossip::default()), + event_tracker: RwLock::new(EventTracker::default()), + } + } + + pub async fn handle_notifications(&self) -> Result<(), Error> { + let client = nostr_client(); + + // Get all bootstrapping relays + let mut urls = vec![]; + urls.extend(BOOTSTRAP_RELAYS); + urls.extend(SEARCH_RELAYS); + + // Add relay to the relay pool + for url in urls.into_iter() { + client.add_relay(url).await?; + } + + // Establish connection to relays + client.connect().await; + + let mut processed_events: HashSet = HashSet::new(); + let mut challenges: HashSet> = HashSet::new(); + let mut notifications = client.notifications(); + + while let Ok(notification) = notifications.recv().await { + let RelayPoolNotification::Message { message, relay_url } = notification else { + continue; + }; + + match message { + RelayMessage::Event { event, .. } => { + // Keep track of which relays have seen this event + { + let mut event_tracker = self.event_tracker.write().await; + event_tracker + .seen_on_relays + .entry(event.id) + .or_default() + .insert(relay_url); + } + + // Skip events that have already been processed + if !processed_events.insert(event.id) { + continue; + } + + match event.kind { + Kind::RelayList => { + let mut gossip = self.gossip.write().await; + let is_self_authored = Self::is_self_authored(&event).await; + + // Update NIP-65 relays for event's public key + gossip.insert(&event); + + // Get events if relay list belongs to current user + if is_self_authored { + // Fetch user's metadata event + gossip.subscribe(event.pubkey, Kind::Metadata).await.ok(); + + // Fetch user's contact list event + gossip.subscribe(event.pubkey, Kind::ContactList).await.ok(); + + // Fetch user's messaging relays event + gossip.get_nip17(event.pubkey).await.ok(); + } + } + Kind::InboxRelays => { + let mut gossip = self.gossip.write().await; + let is_self_authored = Self::is_self_authored(&event).await; + + // Update NIP-17 relays for event's public key + gossip.insert(&event); + + // Subscribe to gift wrap events if messaging relays belong to the current user + if is_self_authored { + if let Err(e) = gossip.monitor_inbox(event.pubkey).await { + log::error!("Error: {e}"); + self.signal.send(SignalKind::MessagingRelaysNotFound).await; + } + } + } + Kind::ContactList => { + let is_self_authored = Self::is_self_authored(&event).await; + + if is_self_authored { + let mut gossip = self.gossip.write().await; + let public_keys: HashSet = + event.tags.public_keys().copied().collect(); + + gossip.bulk_subscribe(public_keys).await.ok(); + } + } + Kind::Metadata => { + let metadata = Metadata::from_json(&event.content).unwrap_or_default(); + let profile = Profile::new(event.pubkey, metadata); + + self.signal.send(SignalKind::NewProfile(profile)).await; + } + Kind::GiftWrap => { + self.extract_rumor(&event).await; + } + _ => {} + } + } + RelayMessage::EndOfStoredEvents(subscription_id) => { + if *subscription_id == self.gift_wrap_sub_id { + self.signal + .send(SignalKind::GiftWrapStatus(UnwrappingStatus::Processing)) + .await; + } + } + RelayMessage::Auth { challenge } => { + if challenges.insert(challenge.clone()) { + // Send a signal to the ingester to handle the auth request + self.signal + .send(SignalKind::Auth(AuthRequest::new(challenge, relay_url))) + .await; + } + } + RelayMessage::Ok { + event_id, message, .. + } => { + let msg = MachineReadablePrefix::parse(&message); + let mut event_tracker = self.event_tracker.write().await; + + // Keep track of events sent by Coop + event_tracker.sent_ids.insert(event_id); + + // Keep track of events that need to be resend after auth + if let Some(MachineReadablePrefix::AuthRequired) = msg { + event_tracker.resend_queue.insert(event_id, relay_url); + } + } + _ => {} + } + } + + Ok(()) + } + + pub async fn handle_metadata_batching(&self) { + let timeout = Duration::from_millis(METADATA_BATCH_TIMEOUT); + let mut processed_pubkeys: HashSet = HashSet::new(); + let mut batch: HashSet = HashSet::new(); + + /// Internal events for the metadata batching system + enum BatchEvent { + PublicKey(PublicKey), + Timeout, + Closed, + } + + loop { + let futs = smol::future::or( + async move { + if let Ok(public_key) = self.ingester.receiver().recv_async().await { + BatchEvent::PublicKey(public_key) + } else { + BatchEvent::Closed + } + }, + async move { + smol::Timer::after(timeout).await; + BatchEvent::Timeout + }, + ); + + match futs.await { + BatchEvent::PublicKey(public_key) => { + // Prevent duplicate keys from being processed + if processed_pubkeys.insert(public_key) { + batch.insert(public_key); + } + + // Process the batch if it's full + if batch.len() >= METADATA_BATCH_LIMIT { + let mut gossip = self.gossip.write().await; + gossip.bulk_subscribe(std::mem::take(&mut batch)).await.ok(); + } + } + BatchEvent::Timeout => { + let mut gossip = self.gossip.write().await; + gossip.bulk_subscribe(std::mem::take(&mut batch)).await.ok(); + } + BatchEvent::Closed => { + let mut gossip = self.gossip.write().await; + gossip.bulk_subscribe(std::mem::take(&mut batch)).await.ok(); + + // Exit the current loop + break; + } + } + } + } + + async fn is_self_authored(event: &Event) -> bool { + let client = nostr_client(); + + let Ok(signer) = client.signer().await else { + return false; + }; + + let Ok(public_key) = signer.get_public_key().await else { + return false; + }; + + public_key == event.pubkey + } + + /// Stores an unwrapped event in local database with reference to original + async fn set_rumor(&self, id: EventId, rumor: &Event) -> Result<(), Error> { + let client = nostr_client(); + + // Save unwrapped event + client.database().save_event(rumor).await?; + + // Create a reference event pointing to the unwrapped event + let event = EventBuilder::new(Kind::ApplicationSpecificData, "") + .tags(vec![Tag::identifier(id), Tag::event(rumor.id)]) + .sign(&Keys::generate()) + .await?; + + // Save reference event + client.database().save_event(&event).await?; + + Ok(()) + } + + /// Retrieves a previously unwrapped event from local database + async fn get_rumor(&self, id: EventId) -> Result { + let client = nostr_client(); + let filter = Filter::new() + .kind(Kind::ApplicationSpecificData) + .identifier(id) + .limit(1); + + if let Some(event) = client.database().query(filter).await?.first_owned() { + let target_id = event.tags.event_ids().collect::>()[0]; + + if let Some(event) = client.database().event_by_id(target_id).await? { + Ok(event) + } else { + Err(anyhow!("Event not found.")) + } + } else { + Err(anyhow!("Event is not cached yet.")) + } + } + + // Unwraps a gift-wrapped event and processes its contents. + async fn extract_rumor(&self, gift_wrap: &Event) { + let client = nostr_client(); + + let mut rumor: Option = None; + + if let Ok(event) = self.get_rumor(gift_wrap.id).await { + rumor = Some(event); + } else if let Ok(unwrapped) = client.unwrap_gift_wrap(gift_wrap).await { + // Sign the unwrapped event with a RANDOM KEYS + if let Ok(event) = unwrapped.rumor.sign_with_keys(&Keys::generate()) { + // Save this event to the database for future use. + if let Err(e) = self.set_rumor(gift_wrap.id, &event).await { + log::warn!("Failed to cache unwrapped event: {e}") + } + + rumor = Some(event); + } + } + + if let Some(event) = rumor { + // Send all pubkeys to the metadata batch to sync data + for public_key in event.tags.public_keys().copied() { + self.ingester.send(public_key).await; + } + + match event.created_at >= self.initialized_at { + // New message: send a signal to notify the UI + true => { + self.signal + .send(SignalKind::NewMessage((gift_wrap.id, event))) + .await; + } + // Old message: Coop is probably processing the user's messages during initial load + false => { + self.gift_wrap_processing.store(true, Ordering::Release); + } + } + } + } + + fn first_run() -> bool { + let flag = support_dir().join(".first_run"); + !flag.exists() && std::fs::write(&flag, "").is_ok() + } +} diff --git a/crates/auto_update/Cargo.toml b/crates/auto_update/Cargo.toml index 8bfad2d..dee274f 100644 --- a/crates/auto_update/Cargo.toml +++ b/crates/auto_update/Cargo.toml @@ -6,7 +6,7 @@ publish.workspace = true [dependencies] common = { path = "../common" } -global = { path = "../global" } +app_state = { path = "../app_state" } gpui.workspace = true nostr-sdk.workspace = true diff --git a/crates/auto_update/src/lib.rs b/crates/auto_update/src/lib.rs index 92b3375..2151c41 100644 --- a/crates/auto_update/src/lib.rs +++ b/crates/auto_update/src/lib.rs @@ -1,7 +1,7 @@ use anyhow::Error; +use app_state::constants::{APP_PUBKEY, APP_UPDATER_ENDPOINT}; use cargo_packager_updater::semver::Version; use cargo_packager_updater::{check_update, Config, Update}; -use global::constants::{APP_PUBKEY, APP_UPDATER_ENDPOINT}; use gpui::http_client::Url; use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task, Window}; use smallvec::{smallvec, SmallVec}; diff --git a/crates/client_keys/Cargo.toml b/crates/client_keys/Cargo.toml index c5d5258..8dfb3d6 100644 --- a/crates/client_keys/Cargo.toml +++ b/crates/client_keys/Cargo.toml @@ -5,7 +5,7 @@ edition.workspace = true publish.workspace = true [dependencies] -global = { path = "../global" } +app_state = { path = "../app_state" } nostr-sdk.workspace = true gpui.workspace = true diff --git a/crates/client_keys/src/lib.rs b/crates/client_keys/src/lib.rs index ece82df..3f12f88 100644 --- a/crates/client_keys/src/lib.rs +++ b/crates/client_keys/src/lib.rs @@ -1,7 +1,7 @@ use std::sync::atomic::Ordering; -use global::app_state; -use global::constants::KEYRING_URL; +use app_state::app_state; +use app_state::constants::KEYRING_URL; use gpui::{App, AppContext, Context, Entity, Global, Subscription, Window}; use nostr_sdk::prelude::*; use smallvec::{smallvec, SmallVec}; diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index ffbb804..422f860 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -5,7 +5,7 @@ edition.workspace = true publish.workspace = true [dependencies] -global = { path = "../global" } +app_state = { path = "../app_state" } gpui.workspace = true nostr-connect.workspace = true diff --git a/crates/common/src/display.rs b/crates/common/src/display.rs index 765fafb..a129c56 100644 --- a/crates/common/src/display.rs +++ b/crates/common/src/display.rs @@ -1,8 +1,8 @@ use std::sync::Arc; use anyhow::{anyhow, Error}; +use app_state::constants::IMAGE_RESIZE_SERVICE; use chrono::{Local, TimeZone}; -use global::constants::IMAGE_RESIZE_SERVICE; use gpui::{Image, ImageFormat, SharedString, SharedUri}; use nostr_sdk::prelude::*; use qrcode::render::svg; diff --git a/crates/coop/Cargo.toml b/crates/coop/Cargo.toml index 32d2d3b..f7d2d89 100644 --- a/crates/coop/Cargo.toml +++ b/crates/coop/Cargo.toml @@ -32,7 +32,7 @@ ui = { path = "../ui" } title_bar = { path = "../title_bar" } theme = { path = "../theme" } common = { path = "../common" } -global = { path = "../global" } +app_state = { path = "../app_state" } registry = { path = "../registry" } settings = { path = "../settings" } client_keys = { path = "../client_keys" } diff --git a/crates/coop/src/chatspace.rs b/crates/coop/src/chatspace.rs index 183c03b..5875be9 100644 --- a/crates/coop/src/chatspace.rs +++ b/crates/coop/src/chatspace.rs @@ -5,15 +5,13 @@ use std::sync::Arc; use std::time::Duration; use anyhow::{anyhow, Error}; +use app_state::constants::{ACCOUNT_IDENTIFIER, BOOTSTRAP_RELAYS, DEFAULT_SIDEBAR_WIDTH}; +use app_state::state::{AuthRequest, SignalKind, UnwrappingStatus}; +use app_state::{app_state, nostr_client}; use auto_update::AutoUpdater; use client_keys::ClientKeys; use common::display::RenderedProfile; use common::event::EventUtils; -use global::constants::{ - ACCOUNT_IDENTIFIER, BOOTSTRAP_RELAYS, DEFAULT_SIDEBAR_WIDTH, METADATA_BATCH_LIMIT, - METADATA_BATCH_TIMEOUT, SEARCH_RELAYS, -}; -use global::{app_state, nostr_client, AuthRequest, Notice, SignalKind, UnwrappingStatus}; use gpui::prelude::FluentBuilder; use gpui::{ deferred, div, px, rems, App, AppContext, AsyncWindowContext, Axis, ClipboardItem, Context, @@ -139,23 +137,43 @@ impl ChatSpace { ); subscriptions.push( - // Subscribe to open chat room requests - cx.subscribe_in(®istry, window, move |this, _, event, window, cx| { - this.process_registry_event(event, window, cx); + // Handle registry events + cx.subscribe_in(®istry, window, move |this, _, ev, window, cx| { + match ev { + RegistryEvent::Open(room) => { + if let Some(room) = room.upgrade() { + this.dock.update(cx, |this, cx| { + let panel = chat::init(room, window, cx); + this.add_panel(Arc::new(panel), DockPlacement::Center, window, cx); + }); + } + } + RegistryEvent::Close(..) => { + this.dock.update(cx, |this, cx| { + this.focus_tab_panel(window, cx); + + cx.defer_in(window, |_, window, cx| { + window.dispatch_action(Box::new(ClosePanel), cx); + window.close_all_modals(cx); + }); + }); + } + _ => {} + }; }), ); tasks.push( - // Connect to the bootstrap relays - // Then handle nostr events in the background + // Handle nostr events in the background cx.background_spawn(async move { - Self::connect() - .await - .expect("Failed connect the bootstrap relays. Please restart the application."); + app_state().handle_notifications().await.ok(); + }), + ); - Self::process_nostr_events() - .await - .expect("Failed to handle nostr events. Please restart the application."); + tasks.push( + // Listen all metadata requests then batch them into single subscription + cx.background_spawn(async move { + app_state().handle_metadata_batching().await; }), ); @@ -174,17 +192,10 @@ impl ChatSpace { }), ); - tasks.push( - // Listen all metadata requests then batch them into single subscription - cx.background_spawn(async move { - Self::process_batching_metadata().await; - }), - ); - tasks.push( // Continuously handle signals from the Nostr channel cx.spawn_in(window, async move |this, cx| { - Self::process_nostr_signals(this, cx).await + Self::handle_signals(this, cx).await }), ); @@ -198,100 +209,26 @@ impl ChatSpace { } } - async fn connect() -> Result<(), Error> { - let client = nostr_client(); - - for relay in BOOTSTRAP_RELAYS.into_iter() { - client.add_relay(relay).await?; - } - - log::info!("Connected to bootstrap relays"); - - for relay in SEARCH_RELAYS.into_iter() { - client.add_relay(relay).await?; - } - - log::info!("Connected to search relays"); - - // Establish connection to relays - client.connect().await; - - Ok(()) - } - async fn observe_signer() { let client = nostr_client(); let app_state = app_state(); - let stream_timeout = Duration::from_secs(5); - let loop_duration = Duration::from_secs(1); + let loop_duration = Duration::from_millis(800); loop { - let Ok(signer) = client.signer().await else { - smol::Timer::after(loop_duration).await; - continue; - }; + if let Ok(signer) = client.signer().await { + if let Ok(pk) = signer.get_public_key().await { + // Notify the app that the signer has been set + app_state.signal.send(SignalKind::SignerSet(pk)).await; - let Ok(public_key) = signer.get_public_key().await else { - smol::Timer::after(loop_duration).await; - continue; - }; + // Get user's gossip relays + app_state.gossip.write().await.get_nip65(pk).await.ok(); - // Notify the app that the signer has been set. - app_state - .signal - .send(SignalKind::SignerSet(public_key)) - .await; - - // Subscribe to the NIP-65 relays for the public key. - let filter = Filter::new() - .kind(Kind::RelayList) - .author(public_key) - .limit(1); - - let mut nip65_found = false; - - match client - .stream_events_from(BOOTSTRAP_RELAYS, filter, stream_timeout) - .await - { - Ok(mut stream) => { - if stream.next().await.is_some() { - nip65_found = true; - } else { - // Timeout - app_state.signal.send(SignalKind::RelaysNotFound).await; - } + // Exit the current loop + break; } - Err(e) => { - log::error!("Error fetching NIP-65 Relay: {e:?}"); - app_state.signal.send(SignalKind::RelaysNotFound).await; - } - }; - - if nip65_found { - // Subscribe to the NIP-17 relays for the public key. - let filter = Filter::new() - .kind(Kind::InboxRelays) - .author(public_key) - .limit(1); - - match client.stream_events(filter, stream_timeout).await { - Ok(mut stream) => { - if stream.next().await.is_some() { - break; - } else { - // Timeout - app_state.signal.send(SignalKind::RelaysNotFound).await; - } - } - Err(e) => { - log::error!("Error fetching NIP-17 Relay: {e:?}"); - app_state.signal.send(SignalKind::RelaysNotFound).await; - } - }; } - break; + smol::Timer::after(loop_duration).await; } } @@ -337,189 +274,7 @@ impl ChatSpace { } } - async fn process_batching_metadata() { - let app_state = app_state(); - let timeout = Duration::from_millis(METADATA_BATCH_TIMEOUT); - let mut processed_pubkeys: HashSet = HashSet::new(); - let mut batch: HashSet = HashSet::new(); - - /// Internal events for the metadata batching system - enum BatchEvent { - PublicKey(PublicKey), - Timeout, - Closed, - } - - loop { - let futs = smol::future::or( - async move { - if let Ok(public_key) = app_state.ingester.receiver().recv_async().await { - BatchEvent::PublicKey(public_key) - } else { - BatchEvent::Closed - } - }, - async move { - smol::Timer::after(timeout).await; - BatchEvent::Timeout - }, - ); - - match futs.await { - BatchEvent::PublicKey(public_key) => { - // Prevent duplicate keys from being processed - if processed_pubkeys.insert(public_key) { - batch.insert(public_key); - } - - // Process the batch if it's full - if batch.len() >= METADATA_BATCH_LIMIT { - Self::fetch_metadata_for_pubkeys(std::mem::take(&mut batch)).await; - } - } - BatchEvent::Timeout => { - Self::fetch_metadata_for_pubkeys(std::mem::take(&mut batch)).await; - } - BatchEvent::Closed => { - Self::fetch_metadata_for_pubkeys(std::mem::take(&mut batch)).await; - break; - } - } - } - } - - async fn process_nostr_events() -> Result<(), Error> { - let client = nostr_client(); - let app_state = app_state(); - - let mut processed_events: HashSet = HashSet::new(); - let mut challenges: HashSet> = HashSet::new(); - let mut notifications = client.notifications(); - - while let Ok(notification) = notifications.recv().await { - let RelayPoolNotification::Message { message, relay_url } = notification else { - continue; - }; - - match message { - RelayMessage::Event { event, .. } => { - // Keep track of which relays have seen this event - app_state - .seen_on_relays - .write() - .await - .entry(event.id) - .or_insert_with(HashSet::new) - .insert(relay_url); - - // Skip events that have already been processed - if !processed_events.insert(event.id) { - continue; - } - - match event.kind { - Kind::RelayList => { - if let Ok(true) = Self::is_self_event(&event).await { - // Fetch user's metadata event - Self::fetch_single_event(Kind::Metadata, event.pubkey).await; - - // Fetch user's contact list event - Self::fetch_single_event(Kind::ContactList, event.pubkey).await; - } - } - Kind::InboxRelays => { - if let Ok(true) = Self::is_self_event(&event).await { - let relays = nip17::extract_relay_list(&event).collect_vec(); - - if !relays.is_empty() { - for relay in relays.clone().into_iter() { - if client.add_relay(relay).await.is_err() { - let notice = Notice::RelayFailed(relay.clone()); - app_state.signal.send(SignalKind::Notice(notice)).await; - } - if client.connect_relay(relay).await.is_err() { - let notice = Notice::RelayFailed(relay.clone()); - app_state.signal.send(SignalKind::Notice(notice)).await; - } - } - - // Subscribe to gift wrap events only in the current user's NIP-17 relays - Self::fetch_gift_wrap(relays, event.pubkey).await; - } else { - app_state.signal.send(SignalKind::RelaysNotFound).await; - } - } - } - Kind::ContactList => { - if let Ok(true) = Self::is_self_event(&event).await { - let public_keys = event.tags.public_keys().copied().collect_vec(); - let kinds = vec![Kind::Metadata, Kind::ContactList]; - let limit = public_keys.len() * kinds.len(); - let filter = - Filter::new().limit(limit).authors(public_keys).kinds(kinds); - - client - .subscribe_to( - BOOTSTRAP_RELAYS, - filter, - app_state.auto_close_opts, - ) - .await - .ok(); - } - } - Kind::Metadata => { - let metadata = Metadata::from_json(&event.content).unwrap_or_default(); - let profile = Profile::new(event.pubkey, metadata); - - app_state.signal.send(SignalKind::NewProfile(profile)).await; - } - Kind::GiftWrap => { - Self::unwrap_gift_wrap(&event).await; - } - _ => {} - } - } - RelayMessage::EndOfStoredEvents(subscription_id) => { - if *subscription_id == app_state.gift_wrap_sub_id { - let signal = SignalKind::GiftWrapStatus(UnwrappingStatus::Processing); - app_state.signal.send(signal).await; - } - } - RelayMessage::Auth { challenge } => { - if challenges.insert(challenge.clone()) { - let req = AuthRequest::new(challenge, relay_url); - // Send a signal to the ingester to handle the auth request - app_state.signal.send(SignalKind::Auth(req)).await; - } - } - RelayMessage::Ok { - event_id, message, .. - } => { - // Keep track of events sent by Coop - app_state.sent_ids.write().await.insert(event_id); - - // Keep track of events that need to be resent - match MachineReadablePrefix::parse(&message) { - Some(MachineReadablePrefix::AuthRequired) => { - app_state - .resend_queue - .write() - .await - .insert(event_id, relay_url); - } - Some(_) => {} - None => {} - } - } - _ => {} - } - } - - Ok(()) - } - - async fn process_nostr_signals(view: WeakEntity, cx: &mut AsyncWindowContext) { + async fn handle_signals(view: WeakEntity, cx: &mut AsyncWindowContext) { let app_state = app_state(); let mut is_open_proxy_modal = false; @@ -604,14 +359,17 @@ impl ChatSpace { this.event_to_message(gift_wrap_id, event, window, cx); }); } - SignalKind::RelaysNotFound => { + SignalKind::GossipRelaysNotFound => { view.update(cx, |this, cx| { this.set_required_relays(cx); }) .ok(); } - SignalKind::Notice(msg) => { - window.push_notification(msg.as_str(), cx); + SignalKind::MessagingRelaysNotFound => { + view.update(cx, |this, cx| { + this.set_required_relays(cx); + }) + .ok(); } }; }) @@ -619,177 +377,6 @@ impl ChatSpace { } } - /// Checks if an event is belong to the current user - async fn is_self_event(event: &Event) -> Result { - let client = nostr_client(); - let signer = client.signer().await?; - let public_key = signer.get_public_key().await?; - - Ok(public_key == event.pubkey) - } - - /// Fetches a single event by kind and public key - pub async fn fetch_single_event(kind: Kind, public_key: PublicKey) { - let client = nostr_client(); - let app_state = app_state(); - let filter = Filter::new().kind(kind).author(public_key).limit(1); - - if let Err(e) = client.subscribe(filter, app_state.auto_close_opts).await { - log::info!("Failed to subscribe: {e}"); - } - } - - /// Fetches gift wrap events for a given public key and relays - pub async fn fetch_gift_wrap(relays: Vec<&RelayUrl>, public_key: PublicKey) { - let client = nostr_client(); - let id = app_state().gift_wrap_sub_id.clone(); - let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key); - - if client - .subscribe_with_id_to(relays.clone(), id, filter, None) - .await - .is_ok() - { - log::info!("Subscribed to messages in: {relays:?}"); - } - } - - /// Fetches metadata for a list of public keys - async fn fetch_metadata_for_pubkeys(public_keys: HashSet) { - if public_keys.is_empty() { - return; - } - - let client = nostr_client(); - let app_state = app_state(); - - let kinds = vec![Kind::Metadata, Kind::ContactList, Kind::RelayList]; - let limit = public_keys.len() * kinds.len() + 20; - - // A filter to fetch metadata - let filter = Filter::new().authors(public_keys).kinds(kinds).limit(limit); - - client - .subscribe_to(BOOTSTRAP_RELAYS, filter, app_state.auto_close_opts) - .await - .ok(); - } - - /// Stores an unwrapped event in local database with reference to original - async fn set_unwrapped_event(gift_wrap: EventId, unwrapped: &Event) -> Result<(), Error> { - let client = nostr_client(); - - // Save unwrapped event - client.database().save_event(unwrapped).await?; - - // Create a reference event pointing to the unwrapped event - let event = EventBuilder::new(Kind::ApplicationSpecificData, "") - .tags(vec![Tag::identifier(gift_wrap), Tag::event(unwrapped.id)]) - .sign(&Keys::generate()) - .await?; - - // Save reference event - client.database().save_event(&event).await?; - - Ok(()) - } - - /// Retrieves a previously unwrapped event from local database - async fn get_unwrapped_event(root: EventId) -> Result { - let client = nostr_client(); - let filter = Filter::new() - .kind(Kind::ApplicationSpecificData) - .identifier(root) - .limit(1); - - if let Some(event) = client.database().query(filter).await?.first_owned() { - let target_id = event.tags.event_ids().collect_vec()[0]; - - if let Some(event) = client.database().event_by_id(target_id).await? { - Ok(event) - } else { - Err(anyhow!("Event not found.")) - } - } else { - Err(anyhow!("Event is not cached yet.")) - } - } - - /// Unwraps a gift-wrapped event and processes its contents. - async fn unwrap_gift_wrap(target: &Event) { - let client = nostr_client(); - let app_state = app_state(); - let mut message: Option = None; - - if let Ok(event) = Self::get_unwrapped_event(target.id).await { - message = Some(event); - } else if let Ok(unwrapped) = client.unwrap_gift_wrap(target).await { - // Sign the unwrapped event with a RANDOM KEYS - if let Ok(event) = unwrapped.rumor.sign_with_keys(&Keys::generate()) { - // Save this event to the database for future use. - if let Err(e) = Self::set_unwrapped_event(target.id, &event).await { - log::warn!("Failed to cache unwrapped event: {e}") - } - - message = Some(event); - } - } - - if let Some(event) = message { - // Send all pubkeys to the metadata batch to sync data - for public_key in event.all_pubkeys() { - app_state.ingester.send(public_key).await; - } - - match event.created_at >= app_state.init_at { - // New message: send a signal to notify the UI - true => { - app_state - .signal - .send(SignalKind::NewMessage((target.id, event))) - .await; - } - // Old message: Coop is probably processing the user's messages during initial load - false => { - app_state - .gift_wrap_processing - .store(true, Ordering::Release); - } - } - } - } - - fn process_registry_event( - &mut self, - event: &RegistryEvent, - window: &mut Window, - cx: &mut Context, - ) { - match event { - RegistryEvent::Open(room) => { - if let Some(room) = room.upgrade() { - self.dock.update(cx, |this, cx| { - let panel = chat::init(room, window, cx); - this.add_panel(Arc::new(panel), DockPlacement::Center, window, cx); - }); - } else { - window.push_notification(t!("common.room_error"), cx); - } - } - RegistryEvent::Close(..) => { - self.dock.update(cx, |this, cx| { - this.focus_tab_panel(window, cx); - - cx.defer_in(window, |_, window, cx| { - window.dispatch_action(Box::new(ClosePanel), cx); - window.close_all_modals(cx); - }); - }); - } - _ => {} - }; - } - fn auth(&mut self, req: AuthRequest, window: &mut Window, cx: &mut Context) { let settings = AppSettings::global(cx); @@ -835,16 +422,17 @@ impl ChatSpace { relay.resubscribe().await?; // Get all failed events that need to be resent - let mut queue = app_state.resend_queue.write().await; + let mut event_tracker = app_state.event_tracker.write().await; - let ids: Vec = queue + let ids: Vec = event_tracker + .resend_queue .iter() .filter(|(_, url)| relay_url == *url) .map(|(id, _)| *id) .collect(); for id in ids.into_iter() { - if let Some(relay_url) = queue.remove(&id) { + if let Some(relay_url) = event_tracker.resend_queue.remove(&id) { if let Some(event) = client.database().event_by_id(&id).await? { let event_id = relay.send_event(&event).await?; @@ -854,8 +442,8 @@ impl ChatSpace { success: HashSet::from([relay_url]), }; - app_state.sent_ids.write().await.insert(event_id); - app_state.resent_ids.write().await.push(output); + event_tracker.sent_ids.insert(event_id); + event_tracker.resent_ids.push(output); } } } diff --git a/crates/coop/src/main.rs b/crates/coop/src/main.rs index c15f1c9..045c539 100644 --- a/crates/coop/src/main.rs +++ b/crates/coop/src/main.rs @@ -1,8 +1,8 @@ use std::sync::Arc; +use app_state::constants::{APP_ID, APP_NAME}; +use app_state::{app_state, nostr_client}; use assets::Assets; -use global::constants::{APP_ID, APP_NAME}; -use global::{app_state, nostr_client}; use gpui::{ point, px, size, AppContext, Application, Bounds, KeyBinding, Menu, MenuItem, SharedString, TitlebarOptions, WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind, diff --git a/crates/coop/src/views/account.rs b/crates/coop/src/views/account.rs index a8118df..1970bfb 100644 --- a/crates/coop/src/views/account.rs +++ b/crates/coop/src/views/account.rs @@ -1,10 +1,11 @@ use std::time::Duration; use anyhow::Error; +use app_state::constants::{ACCOUNT_IDENTIFIER, BUNKER_TIMEOUT}; +use app_state::state::SignalKind; +use app_state::{app_state, nostr_client}; use client_keys::ClientKeys; use common::display::RenderedProfile; -use global::constants::{ACCOUNT_IDENTIFIER, BUNKER_TIMEOUT}; -use global::{app_state, nostr_client, SignalKind}; use gpui::prelude::FluentBuilder; use gpui::{ div, relative, rems, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter, diff --git a/crates/coop/src/views/chat/mod.rs b/crates/coop/src/views/chat/mod.rs index a8cc88a..81e2f24 100644 --- a/crates/coop/src/views/chat/mod.rs +++ b/crates/coop/src/views/chat/mod.rs @@ -1,9 +1,9 @@ use std::collections::{HashMap, HashSet}; use std::time::Duration; +use app_state::{app_state, nostr_client}; use common::display::{RenderedProfile, RenderedTimestamp}; use common::nip96::nip96_upload; -use global::{app_state, nostr_client}; use gpui::prelude::FluentBuilder; use gpui::{ div, img, list, px, red, relative, rems, svg, white, Action, AnyElement, App, AppContext, @@ -170,7 +170,8 @@ impl Chat { cx.spawn_in(window, async move |this, cx| { let app_state = app_state(); - let sent_ids = app_state.sent_ids.read().await; + let event_tracker = app_state.event_tracker.read().await; + let sent_ids = event_tracker.sent_ids(); this.update_in(cx, |this, _window, cx| { if !sent_ids.contains(&gift_wrap_id) { @@ -1240,6 +1241,7 @@ impl Chat { let task: Task, Error>> = cx.background_spawn(async move { let client = nostr_client(); let app_state = app_state(); + let event_tracker = app_state.event_tracker.read().await; let mut relays: Vec = vec![]; let filter = Filter::new() @@ -1249,7 +1251,7 @@ impl Chat { if let Some(event) = client.database().query(filter).await?.first_owned() { if let Some(Ok(id)) = event.tags.identifier().map(EventId::parse) { - if let Some(urls) = app_state.seen_on_relays.read().await.get(&id).cloned() { + if let Some(urls) = event_tracker.seen_on_relays.get(&id).cloned() { relays.extend(urls); } } diff --git a/crates/coop/src/views/compose.rs b/crates/coop/src/views/compose.rs index bfb590c..d0a55e6 100644 --- a/crates/coop/src/views/compose.rs +++ b/crates/coop/src/views/compose.rs @@ -2,10 +2,10 @@ use std::ops::Range; use std::time::Duration; use anyhow::{anyhow, Error}; +use app_state::constants::BOOTSTRAP_RELAYS; +use app_state::{app_state, nostr_client}; use common::display::{RenderedProfile, TextUtils}; use common::nip05::nip05_profile; -use global::constants::BOOTSTRAP_RELAYS; -use global::{app_state, nostr_client}; use gpui::prelude::FluentBuilder; use gpui::{ div, px, relative, rems, uniform_list, App, AppContext, Context, Entity, InteractiveElement, diff --git a/crates/coop/src/views/edit_profile.rs b/crates/coop/src/views/edit_profile.rs index 9cd96b8..ed0dc77 100644 --- a/crates/coop/src/views/edit_profile.rs +++ b/crates/coop/src/views/edit_profile.rs @@ -1,8 +1,8 @@ use std::str::FromStr; use std::time::Duration; +use app_state::nostr_client; use common::nip96::nip96_upload; -use global::nostr_client; use gpui::prelude::FluentBuilder; use gpui::{ div, img, App, AppContext, Context, Entity, Flatten, IntoElement, ParentElement, diff --git a/crates/coop/src/views/login.rs b/crates/coop/src/views/login.rs index 0477123..306a02a 100644 --- a/crates/coop/src/views/login.rs +++ b/crates/coop/src/views/login.rs @@ -1,8 +1,8 @@ use std::time::Duration; +use app_state::constants::{ACCOUNT_IDENTIFIER, BUNKER_TIMEOUT}; +use app_state::nostr_client; use client_keys::ClientKeys; -use global::constants::{ACCOUNT_IDENTIFIER, BUNKER_TIMEOUT}; -use global::nostr_client; use gpui::prelude::FluentBuilder; use gpui::{ div, relative, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, diff --git a/crates/coop/src/views/new_account.rs b/crates/coop/src/views/new_account.rs index 79c4dff..b963557 100644 --- a/crates/coop/src/views/new_account.rs +++ b/crates/coop/src/views/new_account.rs @@ -1,7 +1,7 @@ use anyhow::anyhow; +use app_state::constants::{ACCOUNT_IDENTIFIER, NIP17_RELAYS, NIP65_RELAYS}; +use app_state::nostr_client; use common::nip96::nip96_upload; -use global::constants::{ACCOUNT_IDENTIFIER, NIP17_RELAYS, NIP65_RELAYS}; -use global::nostr_client; use gpui::{ div, relative, rems, AnyElement, App, AppContext, AsyncWindowContext, Context, Entity, EventEmitter, Flatten, FocusHandle, Focusable, IntoElement, ParentElement, PathPromptOptions, diff --git a/crates/coop/src/views/onboarding.rs b/crates/coop/src/views/onboarding.rs index bbe17f6..3966a73 100644 --- a/crates/coop/src/views/onboarding.rs +++ b/crates/coop/src/views/onboarding.rs @@ -1,10 +1,12 @@ use std::sync::Arc; use std::time::Duration; +use app_state::constants::{ + ACCOUNT_IDENTIFIER, APP_NAME, NOSTR_CONNECT_RELAY, NOSTR_CONNECT_TIMEOUT, +}; +use app_state::nostr_client; use client_keys::ClientKeys; use common::display::TextUtils; -use global::constants::{ACCOUNT_IDENTIFIER, APP_NAME, NOSTR_CONNECT_RELAY, NOSTR_CONNECT_TIMEOUT}; -use global::nostr_client; use gpui::prelude::FluentBuilder; use gpui::{ div, img, px, relative, svg, AnyElement, App, AppContext, ClipboardItem, Context, Entity, diff --git a/crates/coop/src/views/screening.rs b/crates/coop/src/views/screening.rs index d4606d4..905d63c 100644 --- a/crates/coop/src/views/screening.rs +++ b/crates/coop/src/views/screening.rs @@ -1,9 +1,9 @@ use std::time::Duration; +use app_state::constants::BOOTSTRAP_RELAYS; +use app_state::nostr_client; use common::display::{shorten_pubkey, RenderedProfile, RenderedTimestamp}; use common::nip05::nip05_verify; -use global::constants::BOOTSTRAP_RELAYS; -use global::nostr_client; use gpui::prelude::FluentBuilder; use gpui::{ div, px, relative, rems, uniform_list, App, AppContext, Context, Div, Entity, diff --git a/crates/coop/src/views/setup_relay.rs b/crates/coop/src/views/setup_relay.rs index 2e112f6..4d87ff3 100644 --- a/crates/coop/src/views/setup_relay.rs +++ b/crates/coop/src/views/setup_relay.rs @@ -1,8 +1,8 @@ use std::time::Duration; use anyhow::{anyhow, Error}; -use global::constants::NIP17_RELAYS; -use global::{app_state, nostr_client}; +use app_state::constants::NIP17_RELAYS; +use app_state::{app_state, nostr_client}; use gpui::prelude::FluentBuilder; use gpui::{ div, px, uniform_list, App, AppContext, Context, Entity, InteractiveElement, IntoElement, diff --git a/crates/coop/src/views/sidebar/mod.rs b/crates/coop/src/views/sidebar/mod.rs index 43c7658..45c224c 100644 --- a/crates/coop/src/views/sidebar/mod.rs +++ b/crates/coop/src/views/sidebar/mod.rs @@ -3,10 +3,11 @@ use std::ops::Range; use std::time::Duration; use anyhow::{anyhow, Error}; +use app_state::constants::{BOOTSTRAP_RELAYS, SEARCH_RELAYS}; +use app_state::state::UnwrappingStatus; +use app_state::{app_state, nostr_client}; use common::debounced_delay::DebouncedDelay; use common::display::{RenderedTimestamp, TextUtils}; -use global::constants::{BOOTSTRAP_RELAYS, SEARCH_RELAYS}; -use global::{app_state, nostr_client, UnwrappingStatus}; use gpui::prelude::FluentBuilder; use gpui::{ deferred, div, relative, uniform_list, AnyElement, App, AppContext, Context, Entity, diff --git a/crates/coop/src/views/user_profile.rs b/crates/coop/src/views/user_profile.rs index c144762..903625b 100644 --- a/crates/coop/src/views/user_profile.rs +++ b/crates/coop/src/views/user_profile.rs @@ -1,8 +1,8 @@ use std::time::Duration; +use app_state::nostr_client; use common::display::RenderedProfile; use common::nip05::nip05_verify; -use global::nostr_client; use gpui::prelude::FluentBuilder; use gpui::{ div, relative, rems, App, AppContext, ClipboardItem, Context, Entity, IntoElement, diff --git a/crates/global/src/lib.rs b/crates/global/src/lib.rs deleted file mode 100644 index c21b7a0..0000000 --- a/crates/global/src/lib.rs +++ /dev/null @@ -1,261 +0,0 @@ -use std::collections::{HashMap, HashSet}; -use std::sync::atomic::AtomicBool; -use std::sync::OnceLock; -use std::time::Duration; - -use flume::{Receiver, Sender}; -use nostr_lmdb::NostrLMDB; -use nostr_sdk::prelude::*; -use paths::nostr_file; -use smol::lock::RwLock; - -use crate::paths::support_dir; - -pub mod constants; -pub mod paths; - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub struct AuthRequest { - pub url: RelayUrl, - pub challenge: String, - pub sending: bool, -} - -impl AuthRequest { - pub fn new(challenge: impl Into, url: RelayUrl) -> Self { - Self { - challenge: challenge.into(), - sending: false, - url, - } - } -} - -#[derive(Debug, Clone)] -pub enum Notice { - RelayFailed(RelayUrl), - AuthFailed(RelayUrl), - Custom(String), -} - -impl Notice { - pub fn as_str(&self) -> String { - match self { - Notice::AuthFailed(url) => format!("Authenticate failed for relay {url}"), - Notice::RelayFailed(url) => format!("Failed to connect the relay {url}"), - Notice::Custom(msg) => msg.into(), - } - } -} - -#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord)] -pub enum UnwrappingStatus { - #[default] - Initialized, - Processing, - Complete, -} - -/// Signals sent through the global event channel to notify UI -#[derive(Debug)] -pub enum SignalKind { - /// A signal to notify UI that the client's signer has been set - SignerSet(PublicKey), - - /// A signal to notify UI that the client's signer has been unset - SignerUnset, - - /// A signal to notify UI that the relay requires authentication - Auth(AuthRequest), - - /// A signal to notify UI that the browser proxy service is down - ProxyDown, - - /// A signal to notify UI that a new profile has been received - NewProfile(Profile), - - /// A signal to notify UI that a new gift wrap event has been received - NewMessage((EventId, Event)), - - /// A signal to notify UI that no DM relays for current user was found - RelaysNotFound, - - /// A signal to notify UI that gift wrap status has changed - GiftWrapStatus(UnwrappingStatus), - - /// A signal to notify UI that there are errors or notices occurred - Notice(Notice), -} - -#[derive(Debug)] -pub struct Signal { - rx: Receiver, - tx: Sender, -} - -impl Default for Signal { - fn default() -> Self { - Self::new() - } -} - -impl Signal { - pub fn new() -> Self { - let (tx, rx) = flume::bounded::(2048); - Self { rx, tx } - } - - pub fn receiver(&self) -> &Receiver { - &self.rx - } - - pub async fn send(&self, kind: SignalKind) { - if let Err(e) = self.tx.send_async(kind).await { - log::error!("Failed to send signal: {e}"); - } - } -} - -#[derive(Debug)] -pub struct Ingester { - rx: Receiver, - tx: Sender, -} - -impl Default for Ingester { - fn default() -> Self { - Self::new() - } -} - -impl Ingester { - pub fn new() -> Self { - let (tx, rx) = flume::bounded::(1024); - Self { rx, tx } - } - - pub fn receiver(&self) -> &Receiver { - &self.rx - } - - pub async fn send(&self, public_key: PublicKey) { - if let Err(e) = self.tx.send_async(public_key).await { - log::error!("Failed to send public key: {e}"); - } - } -} - -/// A simple storage to store all states that using across the application. -#[derive(Debug)] -pub struct AppState { - /// The timestamp when the application was initialized. - pub init_at: Timestamp, - - /// The timestamp when the application was last used. - pub last_used_at: Option, - - /// Whether this is the first run of the application. - pub is_first_run: AtomicBool, - - /// Subscription ID for listening to gift wrap events from relays. - pub gift_wrap_sub_id: SubscriptionId, - - /// Auto-close options for relay subscriptions - pub auto_close_opts: Option, - - /// Whether gift wrap processing is in progress. - pub gift_wrap_processing: AtomicBool, - - /// Tracking events sent by Coop in the current session - pub sent_ids: RwLock>, - - /// Tracking events seen on which relays in the current session - pub seen_on_relays: RwLock>>, - - /// Tracking events that have been resent by Coop in the current session - pub resent_ids: RwLock>>, - - /// Temporarily store events that need to be resent later - pub resend_queue: RwLock>, - - /// Signal channel for communication between Nostr and GPUI - pub signal: Signal, - - /// Ingester channel for processing public keys - pub ingester: Ingester, -} - -impl Default for AppState { - fn default() -> Self { - Self::new() - } -} - -impl AppState { - pub fn new() -> Self { - let init_at = Timestamp::now(); - let first_run = first_run(); - let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); - - let signal = Signal::default(); - let ingester = Ingester::default(); - - Self { - init_at, - signal, - ingester, - last_used_at: None, - is_first_run: AtomicBool::new(first_run), - gift_wrap_sub_id: SubscriptionId::new("inbox"), - gift_wrap_processing: AtomicBool::new(false), - auto_close_opts: Some(opts), - sent_ids: RwLock::new(HashSet::new()), - seen_on_relays: RwLock::new(HashMap::new()), - resent_ids: RwLock::new(Vec::new()), - resend_queue: RwLock::new(HashMap::new()), - } - } -} - -static NOSTR_CLIENT: OnceLock = OnceLock::new(); -static APP_STATE: OnceLock = OnceLock::new(); - -pub fn nostr_client() -> &'static Client { - NOSTR_CLIENT.get_or_init(|| { - // rustls uses the `aws_lc_rs` provider by default - // This only errors if the default provider has already - // been installed. We can ignore this `Result`. - rustls::crypto::aws_lc_rs::default_provider() - .install_default() - .ok(); - - let lmdb = NostrLMDB::open(nostr_file()).expect("Database is NOT initialized"); - - let opts = ClientOptions::new() - .gossip(true) - .automatic_authentication(false) - .verify_subscriptions(false) - .sleep_when_idle(SleepWhenIdle::Enabled { - timeout: Duration::from_secs(600), - }); - - ClientBuilder::default().database(lmdb).opts(opts).build() - }) -} - -pub fn app_state() -> &'static AppState { - APP_STATE.get_or_init(AppState::new) -} - -fn first_run() -> bool { - let flag = support_dir().join(format!(".{}-first_run", env!("CARGO_PKG_VERSION"))); - - if !flag.exists() { - if std::fs::write(&flag, "").is_err() { - return false; - } - true // First run - } else { - false // Not first run - } -} diff --git a/crates/registry/Cargo.toml b/crates/registry/Cargo.toml index 87b4c92..fe596c9 100644 --- a/crates/registry/Cargo.toml +++ b/crates/registry/Cargo.toml @@ -6,7 +6,7 @@ publish.workspace = true [dependencies] common = { path = "../common" } -global = { path = "../global" } +app_state = { path = "../app_state" } settings = { path = "../settings" } gpui.workspace = true diff --git a/crates/registry/src/lib.rs b/crates/registry/src/lib.rs index 9231535..85c2720 100644 --- a/crates/registry/src/lib.rs +++ b/crates/registry/src/lib.rs @@ -2,10 +2,11 @@ use std::cmp::Reverse; use std::collections::{HashMap, HashSet}; use anyhow::Error; +use app_state::nostr_client; +use app_state::state::UnwrappingStatus; use common::event::EventUtils; use fuzzy_matcher::skim::SkimMatcherV2; use fuzzy_matcher::FuzzyMatcher; -use global::{nostr_client, UnwrappingStatus}; use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, Task, WeakEntity, Window}; use itertools::Itertools; use nostr_sdk::prelude::*; diff --git a/crates/registry/src/room.rs b/crates/registry/src/room.rs index 45dd11a..0cad19c 100644 --- a/crates/registry/src/room.rs +++ b/crates/registry/src/room.rs @@ -4,12 +4,11 @@ use std::hash::{Hash, Hasher}; use std::time::Duration; use anyhow::{anyhow, Error}; +use app_state::constants::SEND_RETRY; +use app_state::{app_state, nostr_client}; use common::display::RenderedProfile; use common::event::EventUtils; -use global::constants::SEND_RETRY; -use global::{app_state, nostr_client}; use gpui::{App, AppContext, Context, EventEmitter, SharedString, SharedUri, Task}; -use itertools::Itertools; use nostr_sdk::prelude::*; use crate::Registry; @@ -358,18 +357,6 @@ impl Room { }) } - pub fn disconnect(&self, relays: Vec, cx: &App) -> Task> { - cx.background_spawn(async move { - let client = nostr_client(); - - for relay in relays.into_iter() { - client.disconnect_relay(relay).await?; - } - - Ok(()) - }) - } - /// Loads all messages for this room from the database pub fn load_messages(&self, cx: &App) -> Task, Error>> { let members = self.members.clone(); @@ -378,13 +365,14 @@ impl Room { let client = nostr_client(); let signer = client.signer().await?; let public_key = signer.get_public_key().await?; - let sent_ids = app_state() - .sent_ids + let sent_ids: Vec = app_state() + .event_tracker .read() .await + .sent_ids() .iter() .copied() - .collect_vec(); + .collect(); // Get seen events from database let filter = Filter::new() @@ -508,12 +496,17 @@ impl Room { let rumor = rumor.clone(); let event = EventBuilder::gift_wrap(&signer, &receiver, rumor, vec![]).await?; - let Ok(relay_urls) = Self::messaging_relays(receiver).await else { + let gossip = app_state.gossip.read().await; + let urls = gossip.messaging_relays(&receiver); + + // Check if there are any relays to send the event to + if urls.is_empty() { reports.push(SendReport::new(receiver).not_found()); continue; - }; + } - match client.send_event_to(relay_urls, &event).await { + // Send the event to the relays + match client.send_event_to(urls, &event).await { Ok(output) => { let id = output.id().to_owned(); let auth_required = output.failed.iter().any(|m| m.1.starts_with("auth-")); @@ -522,7 +515,8 @@ impl Room { if auth_required { // Wait for authenticated and resent event successfully for attempt in 0..=SEND_RETRY { - let ids = app_state.resent_ids.read().await; + let retry_manager = app_state.event_tracker.read().await; + let ids = retry_manager.resent_ids(); // Check if event was successfully resent if let Some(output) = ids.iter().find(|e| e.id() == &id).cloned() { @@ -555,8 +549,15 @@ impl Room { // Only send a backup message to current user if sent successfully to others if reports.iter().all(|r| r.is_sent_success()) && backup { - if let Ok(relay_urls) = Self::messaging_relays(public_key).await { - match client.send_event_to(relay_urls, &event).await { + let gossip = app_state.gossip.read().await; + let urls = gossip.messaging_relays(&public_key); + + // Check if there are any relays to send the event to + if urls.is_empty() { + reports.push(SendReport::new(public_key).not_found()); + } else { + // Send the event to the relays + match client.send_event_to(urls, &event).await { Ok(output) => { reports.push(SendReport::new(public_key).status(output)); } @@ -564,8 +565,6 @@ impl Room { reports.push(SendReport::new(public_key).error(e.to_string())); } } - } else { - reports.push(SendReport::new(public_key).not_found()); } } else { reports.push(SendReport::new(public_key).on_hold(event)); @@ -583,6 +582,8 @@ impl Room { ) -> Task, Error>> { cx.background_spawn(async move { let client = nostr_client(); + let app_state = app_state(); + let mut resend_reports = vec![]; for report in reports.into_iter() { @@ -611,8 +612,15 @@ impl Room { // Process the on hold event if it exists if let Some(event) = report.on_hold { - if let Ok(relay_urls) = Self::messaging_relays(receiver).await { - match client.send_event_to(relay_urls, &event).await { + let gossip = app_state.gossip.read().await; + let urls = gossip.messaging_relays(&receiver); + + // Check if there are any relays to send the event to + if urls.is_empty() { + resend_reports.push(SendReport::new(receiver).not_found()); + } else { + // Send the event to the relays + match client.send_event_to(urls, &event).await { Ok(output) => { resend_reports.push(SendReport::new(receiver).status(output)); } @@ -620,8 +628,6 @@ impl Room { resend_reports.push(SendReport::new(receiver).error(e.to_string())); } } - } else { - resend_reports.push(SendReport::new(receiver).not_found()); } } } @@ -629,36 +635,4 @@ impl Room { Ok(resend_reports) }) } - - /// Gets messaging relays for public key - async fn messaging_relays(public_key: PublicKey) -> Result, Error> { - let client = nostr_client(); - let mut relay_urls = vec![]; - - let filter = Filter::new() - .kind(Kind::InboxRelays) - .author(public_key) - .limit(1); - - if let Some(event) = client.database().query(filter).await?.first_owned() { - let urls: Vec = nip17::extract_owned_relay_list(event).collect(); - - // Check if at least one URL exists - if urls.is_empty() { - return Err(anyhow!("Not found")); - } - - // Connect to relays - for url in urls.iter() { - client.add_relay(url).await?; - client.connect_relay(url).await?; - } - - relay_urls.extend(urls.into_iter().take(3).unique()); - } else { - return Err(anyhow!("Not found")); - } - - Ok(relay_urls) - } } diff --git a/crates/settings/Cargo.toml b/crates/settings/Cargo.toml index f0ecef4..b031cce 100644 --- a/crates/settings/Cargo.toml +++ b/crates/settings/Cargo.toml @@ -5,7 +5,7 @@ edition.workspace = true publish.workspace = true [dependencies] -global = { path = "../global" } +app_state = { path = "../app_state" } nostr-sdk.workspace = true gpui.workspace = true diff --git a/crates/settings/src/lib.rs b/crates/settings/src/lib.rs index 3a893db..7b63b2a 100644 --- a/crates/settings/src/lib.rs +++ b/crates/settings/src/lib.rs @@ -1,6 +1,6 @@ use anyhow::anyhow; -use global::constants::SETTINGS_IDENTIFIER; -use global::nostr_client; +use app_state::constants::SETTINGS_IDENTIFIER; +use app_state::nostr_client; use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task}; use nostr_sdk::prelude::*; use serde::{Deserialize, Serialize}; diff --git a/crates/signer_proxy/Cargo.toml b/crates/signer_proxy/Cargo.toml index 37a4f55..527e9a2 100644 --- a/crates/signer_proxy/Cargo.toml +++ b/crates/signer_proxy/Cargo.toml @@ -5,7 +5,7 @@ edition.workspace = true publish.workspace = true [dependencies] -global = { path = "../global" } +app_state = { path = "../app_state" } nostr.workspace = true smol.workspace = true