From 6b872527ada7e4621efaa50447cb87dee1de68dd Mon Sep 17 00:00:00 2001 From: reya Date: Sat, 4 Apr 2026 02:22:08 +0000 Subject: [PATCH] chore: simplify codebase and prepare for multi-platforms (#28) Reviewed-on: https://git.reya.su/reya/coop/pulls/28 --- Cargo.lock | 210 ++++----- Cargo.toml | 1 - crates/chat/src/lib.rs | 119 ++++-- crates/chat/src/message.rs | 2 +- crates/chat/src/room.rs | 2 +- crates/chat_ui/src/lib.rs | 2 +- crates/common/src/display.rs | 23 +- crates/common/src/event.rs | 6 +- crates/coop/src/dialogs/accounts.rs | 4 +- crates/coop/src/dialogs/connect.rs | 2 +- crates/coop/src/dialogs/screening.rs | 14 +- crates/coop/src/main.rs | 6 +- crates/coop/src/sidebar/mod.rs | 2 +- crates/coop/src/workspace.rs | 275 +++--------- crates/device/src/lib.rs | 207 +++------ crates/person/src/lib.rs | 2 +- crates/relay_auth/src/lib.rs | 13 +- crates/state/Cargo.toml | 1 - crates/state/src/lib.rs | 613 +++++++++++---------------- crates/state/src/signer.rs | 2 +- crates/ui/src/notification.rs | 21 +- 21 files changed, 599 insertions(+), 928 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index caf8f70..95e2427 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1203,7 +1203,7 @@ dependencies = [ [[package]] name = "collections" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#3d29a0641e7f95b0581f60fa210ed3d23383a776" +source = "git+https://github.com/zed-industries/zed#a9dd7e9f062933336b04c07145caaf19893c0f89" dependencies = [ "indexmap", "rustc-hash 2.1.2", @@ -1711,7 +1711,7 @@ dependencies = [ [[package]] name = "derive_refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#3d29a0641e7f95b0581f60fa210ed3d23383a776" +source = "git+https://github.com/zed-industries/zed#a9dd7e9f062933336b04c07145caaf19893c0f89" dependencies = [ "proc-macro2", "quote", @@ -2065,6 +2065,16 @@ dependencies = [ "zune-inflate", ] +[[package]] +name = "faster-hex" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7223ae2d2f179b803433d9c830478527e92b8117eab39460edae7f1614d9fb73" +dependencies = [ + "heapless 0.8.0", + "serde", +] + [[package]] name = "fastrand" version = "1.9.0" @@ -2691,7 +2701,7 @@ dependencies = [ [[package]] name = "gpui" version = "0.2.2" -source = "git+https://github.com/zed-industries/zed#3d29a0641e7f95b0581f60fa210ed3d23383a776" +source = "git+https://github.com/zed-industries/zed#a9dd7e9f062933336b04c07145caaf19893c0f89" dependencies = [ "anyhow", "async-channel 2.5.0", @@ -2771,7 +2781,7 @@ dependencies = [ [[package]] name = "gpui_linux" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#3d29a0641e7f95b0581f60fa210ed3d23383a776" +source = "git+https://github.com/zed-industries/zed#a9dd7e9f062933336b04c07145caaf19893c0f89" dependencies = [ "anyhow", "as-raw-xcb-connection", @@ -2819,7 +2829,7 @@ dependencies = [ [[package]] name = "gpui_macos" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#3d29a0641e7f95b0581f60fa210ed3d23383a776" +source = "git+https://github.com/zed-industries/zed#a9dd7e9f062933336b04c07145caaf19893c0f89" dependencies = [ "anyhow", "async-task", @@ -2847,6 +2857,7 @@ dependencies = [ "media", "metal", "objc", + "objc2-app-kit", "parking_lot", "pathfinder_geometry", "raw-window-handle", @@ -2861,7 +2872,7 @@ dependencies = [ [[package]] name = "gpui_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#3d29a0641e7f95b0581f60fa210ed3d23383a776" +source = "git+https://github.com/zed-industries/zed#a9dd7e9f062933336b04c07145caaf19893c0f89" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -2872,7 +2883,7 @@ dependencies = [ [[package]] name = "gpui_platform" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#3d29a0641e7f95b0581f60fa210ed3d23383a776" +source = "git+https://github.com/zed-industries/zed#a9dd7e9f062933336b04c07145caaf19893c0f89" dependencies = [ "console_error_panic_hook", "gpui", @@ -2885,7 +2896,7 @@ dependencies = [ [[package]] name = "gpui_tokio" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#3d29a0641e7f95b0581f60fa210ed3d23383a776" +source = "git+https://github.com/zed-industries/zed#a9dd7e9f062933336b04c07145caaf19893c0f89" dependencies = [ "anyhow", "gpui", @@ -2896,7 +2907,7 @@ dependencies = [ [[package]] name = "gpui_util" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#3d29a0641e7f95b0581f60fa210ed3d23383a776" +source = "git+https://github.com/zed-industries/zed#a9dd7e9f062933336b04c07145caaf19893c0f89" dependencies = [ "anyhow", "log", @@ -2905,7 +2916,7 @@ dependencies = [ [[package]] name = "gpui_web" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#3d29a0641e7f95b0581f60fa210ed3d23383a776" +source = "git+https://github.com/zed-industries/zed#a9dd7e9f062933336b04c07145caaf19893c0f89" dependencies = [ "anyhow", "console_error_panic_hook", @@ -2929,7 +2940,7 @@ dependencies = [ [[package]] name = "gpui_wgpu" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#3d29a0641e7f95b0581f60fa210ed3d23383a776" +source = "git+https://github.com/zed-industries/zed#a9dd7e9f062933336b04c07145caaf19893c0f89" dependencies = [ "anyhow", "bytemuck", @@ -2957,7 +2968,7 @@ dependencies = [ [[package]] name = "gpui_windows" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#3d29a0641e7f95b0581f60fa210ed3d23383a776" +source = "git+https://github.com/zed-industries/zed#a9dd7e9f062933336b04c07145caaf19893c0f89" dependencies = [ "anyhow", "collections", @@ -3073,6 +3084,16 @@ dependencies = [ "foldhash 0.2.0", ] +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] + [[package]] name = "heapless" version = "0.9.2" @@ -3219,7 +3240,7 @@ dependencies = [ [[package]] name = "http_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#3d29a0641e7f95b0581f60fa210ed3d23383a776" +source = "git+https://github.com/zed-industries/zed#a9dd7e9f062933336b04c07145caaf19893c0f89" dependencies = [ "anyhow", "async-compression", @@ -3244,7 +3265,7 @@ dependencies = [ [[package]] name = "http_client_tls" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#3d29a0641e7f95b0581f60fa210ed3d23383a776" +source = "git+https://github.com/zed-industries/zed#a9dd7e9f062933336b04c07145caaf19893c0f89" dependencies = [ "rustls", "rustls-platform-verifier", @@ -3258,9 +3279,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hyper" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" dependencies = [ "atomic-waker", "bytes", @@ -3272,7 +3293,6 @@ dependencies = [ "httparse", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", @@ -3720,9 +3740,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.93" +version = "0.3.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "797146bb2677299a1eb6b7b50a890f4c361b29ef967addf5b2fa45dae1bb6d7d" +checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9" dependencies = [ "cfg-if", "futures-util", @@ -4084,7 +4104,7 @@ dependencies = [ [[package]] name = "media" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#3d29a0641e7f95b0581f60fa210ed3d23383a776" +source = "git+https://github.com/zed-industries/zed#a9dd7e9f062933336b04c07145caaf19893c0f89" dependencies = [ "anyhow", "bindgen", @@ -4335,7 +4355,7 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" [[package]] name = "nostr" version = "0.44.1" -source = "git+https://github.com/rust-nostr/nostr#94fe83552d21f0f412a34e7c1fccc47b552e30f4" +source = "git+https://github.com/rust-nostr/nostr#6503a7a86e904800de54c8592a6811f5f17cd805" dependencies = [ "aes", "base64", @@ -4345,7 +4365,7 @@ dependencies = [ "cbc", "chacha20", "chacha20poly1305", - "hex", + "faster-hex", "rand 0.9.2", "scrypt", "secp256k1", @@ -4359,7 +4379,7 @@ dependencies = [ [[package]] name = "nostr-blossom" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#94fe83552d21f0f412a34e7c1fccc47b552e30f4" +source = "git+https://github.com/rust-nostr/nostr#6503a7a86e904800de54c8592a6811f5f17cd805" dependencies = [ "base64", "nostr", @@ -4370,7 +4390,7 @@ dependencies = [ [[package]] name = "nostr-connect" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#94fe83552d21f0f412a34e7c1fccc47b552e30f4" +source = "git+https://github.com/rust-nostr/nostr#6503a7a86e904800de54c8592a6811f5f17cd805" dependencies = [ "async-utility", "futures-core", @@ -4383,7 +4403,7 @@ dependencies = [ [[package]] name = "nostr-database" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#94fe83552d21f0f412a34e7c1fccc47b552e30f4" +source = "git+https://github.com/rust-nostr/nostr#6503a7a86e904800de54c8592a6811f5f17cd805" dependencies = [ "btreecap", "flatbuffers", @@ -4393,7 +4413,7 @@ dependencies = [ [[package]] name = "nostr-gossip" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#94fe83552d21f0f412a34e7c1fccc47b552e30f4" +source = "git+https://github.com/rust-nostr/nostr#6503a7a86e904800de54c8592a6811f5f17cd805" dependencies = [ "nostr", ] @@ -4401,7 +4421,7 @@ dependencies = [ [[package]] name = "nostr-gossip-memory" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#94fe83552d21f0f412a34e7c1fccc47b552e30f4" +source = "git+https://github.com/rust-nostr/nostr#6503a7a86e904800de54c8592a6811f5f17cd805" dependencies = [ "indexmap", "lru", @@ -4413,7 +4433,7 @@ dependencies = [ [[package]] name = "nostr-lmdb" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#94fe83552d21f0f412a34e7c1fccc47b552e30f4" +source = "git+https://github.com/rust-nostr/nostr#6503a7a86e904800de54c8592a6811f5f17cd805" dependencies = [ "async-utility", "flume 0.12.0", @@ -4424,26 +4444,15 @@ dependencies = [ "tracing", ] -[[package]] -name = "nostr-memory" -version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#94fe83552d21f0f412a34e7c1fccc47b552e30f4" -dependencies = [ - "btreecap", - "nostr", - "nostr-database", - "tokio", -] - [[package]] name = "nostr-sdk" version = "0.44.1" -source = "git+https://github.com/rust-nostr/nostr#94fe83552d21f0f412a34e7c1fccc47b552e30f4" +source = "git+https://github.com/rust-nostr/nostr#6503a7a86e904800de54c8592a6811f5f17cd805" dependencies = [ "async-utility", "async-wsocket", + "faster-hex", "futures", - "hex", "lru", "negentropy", "nostr", @@ -4615,6 +4624,16 @@ dependencies = [ "objc2-encode", ] +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "objc2", + "objc2-foundation", +] + [[package]] name = "objc2-core-foundation" version = "0.3.2" @@ -4912,7 +4931,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perf" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#3d29a0641e7f95b0581f60fa210ed3d23383a776" +source = "git+https://github.com/zed-industries/zed#a9dd7e9f062933336b04c07145caaf19893c0f89" dependencies = [ "collections", "serde", @@ -5187,7 +5206,7 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.25.8+spec-1.1.0", + "toml_edit 0.25.9+spec-1.1.0", ] [[package]] @@ -5624,7 +5643,7 @@ dependencies = [ [[package]] name = "refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#3d29a0641e7f95b0581f60fa210ed3d23383a776" +source = "git+https://github.com/zed-industries/zed#a9dd7e9f062933336b04c07145caaf19893c0f89" dependencies = [ "derive_refineable", ] @@ -5723,7 +5742,7 @@ dependencies = [ [[package]] name = "reqwest_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#3d29a0641e7f95b0581f60fa210ed3d23383a776" +source = "git+https://github.com/zed-industries/zed#a9dd7e9f062933336b04c07145caaf19893c0f89" dependencies = [ "anyhow", "bytes", @@ -5781,9 +5800,9 @@ dependencies = [ [[package]] name = "rope" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#3d29a0641e7f95b0581f60fa210ed3d23383a776" +source = "git+https://github.com/zed-industries/zed#a9dd7e9f062933336b04c07145caaf19893c0f89" dependencies = [ - "heapless", + "heapless 0.9.2", "log", "rayon", "sum_tree", @@ -6043,7 +6062,7 @@ dependencies = [ [[package]] name = "scheduler" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#3d29a0641e7f95b0581f60fa210ed3d23383a776" +source = "git+https://github.com/zed-industries/zed#a9dd7e9f062933336b04c07145caaf19893c0f89" dependencies = [ "async-task", "backtrace", @@ -6301,9 +6320,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "876ac351060d4f882bb1032b6369eb0aef79ad9df1ea8bc404874d8cc3d0cd98" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ "serde_core", ] @@ -6583,7 +6602,6 @@ dependencies = [ "nostr-connect", "nostr-gossip-memory", "nostr-lmdb", - "nostr-memory", "nostr-sdk", "petname", "rustls", @@ -6645,9 +6663,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sum_tree" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#3d29a0641e7f95b0581f60fa210ed3d23383a776" +source = "git+https://github.com/zed-industries/zed#a9dd7e9f062933336b04c07145caaf19893c0f89" dependencies = [ - "heapless", + "heapless 0.9.2", "log", "rayon", "tracing", @@ -7169,7 +7187,7 @@ checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ "indexmap", "serde_core", - "serde_spanned 1.1.0", + "serde_spanned 1.1.1", "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "toml_writer", @@ -7196,9 +7214,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "1.1.0+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97251a7c317e03ad83774a8752a7e81fb6067740609f75ea2b585b569a59198f" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ "serde_core", ] @@ -7219,21 +7237,21 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.8+spec-1.1.0" +version = "0.25.9+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16bff38f1d86c47f9ff0647e6838d7bb362522bdf44006c7068c2b1e606f1f3c" +checksum = "da053d28fe57e2c9d21b48261e14e7b4c8b670b54d2c684847b91feaf4c7dac5" dependencies = [ "indexmap", - "toml_datetime 1.1.0+spec-1.1.0", + "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", "winnow 1.0.1", ] [[package]] name = "toml_parser" -version = "1.1.0+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011" +checksum = "39ca317ebc49f06bd748bfba29533eac9485569dc9bf80b849024b025e814fb9" dependencies = [ "winnow 1.0.1", ] @@ -7246,9 +7264,9 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "toml_writer" -version = "1.1.0+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d282ade6016312faf3e41e57ebbba0c073e4056dab1232ab1cb624199648f8ed" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" [[package]] name = "tower" @@ -7618,7 +7636,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "util" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#3d29a0641e7f95b0581f60fa210ed3d23383a776" +source = "git+https://github.com/zed-industries/zed#a9dd7e9f062933336b04c07145caaf19893c0f89" dependencies = [ "anyhow", "async-fs", @@ -7657,7 +7675,7 @@ dependencies = [ [[package]] name = "util_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#3d29a0641e7f95b0581f60fa210ed3d23383a776" +source = "git+https://github.com/zed-industries/zed#a9dd7e9f062933336b04c07145caaf19893c0f89" dependencies = [ "perf", "quote", @@ -7813,9 +7831,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.116" +version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dc0882f7b5bb01ae8c5215a1230832694481c1a4be062fd410e12ea3da5b631" +checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0" dependencies = [ "cfg-if", "once_cell", @@ -7826,9 +7844,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.66" +version = "0.4.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19280959e2844181895ef62f065c63e0ca07ece4771b53d89bfdb967d97cbf05" +checksum = "03623de6905b7206edd0a75f69f747f134b7f0a2323392d664448bf2d3c5d87e" dependencies = [ "js-sys", "wasm-bindgen", @@ -7836,9 +7854,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.116" +version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75973d3066e01d035dbedaad2864c398df42f8dd7b1ea057c35b8407c015b537" +checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -7846,9 +7864,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.116" +version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91af5e4be765819e0bcfee7322c14374dc821e35e72fa663a830bbc7dc199eac" +checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2" dependencies = [ "bumpalo", "proc-macro2", @@ -7859,9 +7877,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.116" +version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9bf0406a78f02f336bf1e451799cca198e8acde4ffa278f0fb20487b150a633" +checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b" dependencies = [ "unicode-ident", ] @@ -7927,9 +7945,9 @@ dependencies = [ [[package]] name = "wayland-backend" -version = "0.3.14" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa75f400b7f719bcd68b3f47cd939ba654cedeef690f486db71331eec4c6a406" +checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d" dependencies = [ "cc", "downcast-rs", @@ -7941,9 +7959,9 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.13" +version = "0.31.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab51d9f7c071abeee76007e2b742499e535148035bb835f97aaed1338cf516c3" +checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" dependencies = [ "bitflags 2.11.0", "rustix 1.1.4", @@ -7953,9 +7971,9 @@ dependencies = [ [[package]] name = "wayland-cursor" -version = "0.31.13" +version = "0.31.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b3298683470fbdc6ca40151dfc48c8f2fd4c41a26e13042f801f85002384091" +checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d" dependencies = [ "rustix 1.1.4", "wayland-client", @@ -7964,9 +7982,9 @@ dependencies = [ [[package]] name = "wayland-protocols" -version = "0.32.11" +version = "0.32.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b23b5df31ceff1328f06ac607591d5ba360cf58f90c8fad4ac8d3a55a3c4aec7" +checksum = "563a85523cade2429938e790815fd7319062103b9f4a2dc806e9b53b95982d8f" dependencies = [ "bitflags 2.11.0", "wayland-backend", @@ -7976,9 +7994,9 @@ dependencies = [ [[package]] name = "wayland-protocols-plasma" -version = "0.3.11" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d392fc283a87774afc9beefcd6f931582bb97fe0e6ced0b306a62cb1d026527c" +checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91" dependencies = [ "bitflags 2.11.0", "wayland-backend", @@ -7989,9 +8007,9 @@ dependencies = [ [[package]] name = "wayland-protocols-wlr" -version = "0.3.11" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78248e4cc0eff8163370ba5c158630dcae1f3497a586b826eca2ef5f348d6235" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" dependencies = [ "bitflags 2.11.0", "wayland-backend", @@ -8002,9 +8020,9 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.9" +version = "0.31.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c86287151a309799b821ca709b7345a048a2956af05957c89cb824ab919fa4e3" +checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a" dependencies = [ "proc-macro2", "quick-xml 0.39.2", @@ -8013,9 +8031,9 @@ dependencies = [ [[package]] name = "wayland-sys" -version = "0.31.10" +version = "0.31.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374f6b70e8e0d6bf9461a32988fd553b59ff630964924dad6e4a4eb6bd538d17" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" dependencies = [ "dlib", "log", @@ -8025,9 +8043,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.93" +version = "0.3.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "749466a37ee189057f54748b200186b59a03417a117267baf3fd89cecc9fb837" +checksum = "cd70027e39b12f0849461e08ffc50b9cd7688d942c1c8e3c7b22273236b4dd0a" dependencies = [ "js-sys", "wasm-bindgen", @@ -9474,7 +9492,7 @@ dependencies = [ [[package]] name = "zlog" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#3d29a0641e7f95b0581f60fa210ed3d23383a776" +source = "git+https://github.com/zed-industries/zed#a9dd7e9f062933336b04c07145caaf19893c0f89" dependencies = [ "anyhow", "chrono", @@ -9491,7 +9509,7 @@ checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" [[package]] name = "ztracing" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#3d29a0641e7f95b0581f60fa210ed3d23383a776" +source = "git+https://github.com/zed-industries/zed#a9dd7e9f062933336b04c07145caaf19893c0f89" dependencies = [ "tracing", "tracing-subscriber", @@ -9502,7 +9520,7 @@ dependencies = [ [[package]] name = "ztracing_macro" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#3d29a0641e7f95b0581f60fa210ed3d23383a776" +source = "git+https://github.com/zed-industries/zed#a9dd7e9f062933336b04c07145caaf19893c0f89" [[package]] name = "zune-core" diff --git a/Cargo.toml b/Cargo.toml index 98d7a38..cc9b945 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,6 @@ reqwest_client = { git = "https://github.com/zed-industries/zed" } # Nostr nostr-lmdb = { git = "https://github.com/rust-nostr/nostr" } -nostr-memory = { git = "https://github.com/rust-nostr/nostr" } nostr-connect = { git = "https://github.com/rust-nostr/nostr" } nostr-blossom = { git = "https://github.com/rust-nostr/nostr" } nostr-gossip-memory = { git = "https://github.com/rust-nostr/nostr" } diff --git a/crates/chat/src/lib.rs b/crates/chat/src/lib.rs index bdf59ec..887f53a 100644 --- a/crates/chat/src/lib.rs +++ b/crates/chat/src/lib.rs @@ -6,7 +6,8 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::time::Duration; use anyhow::{Context as AnyhowContext, Error, anyhow}; -use common::EventUtils; +use common::EventExt; +use device::{DeviceEvent, DeviceRegistry}; use fuzzy_matcher::FuzzyMatcher; use fuzzy_matcher::skim::SkimMatcherV2; use gpui::{ @@ -41,8 +42,6 @@ pub enum ChatEvent { CloseRoom(u64), /// An event to notify UI about a new chat request Ping, - /// An event to notify UI that the chat registry has subscribed to messaging relays - Subscribed, /// An error occurred Error(SharedString), } @@ -81,6 +80,9 @@ type GiftWrapId = EventId; /// Chat Registry #[derive(Debug)] pub struct ChatRegistry { + /// Whether the chat registry is currently initializing. + pub initializing: bool, + /// Chat rooms rooms: Vec>, @@ -106,7 +108,7 @@ pub struct ChatRegistry { tasks: SmallVec<[Task>; 2]>, /// Subscriptions - _subscriptions: SmallVec<[Subscription; 1]>, + _subscriptions: SmallVec<[Subscription; 2]>, } impl EventEmitter for ChatRegistry {} @@ -125,22 +127,48 @@ impl ChatRegistry { /// Create a new chat registry instance fn new(window: &mut Window, cx: &mut Context) -> Self { let nostr = NostrRegistry::global(cx); + let device = DeviceRegistry::global(cx); + let (tx, rx) = flume::unbounded::(); let mut subscriptions = smallvec![]; subscriptions.push( // Subscribe to the signer event - cx.subscribe(&nostr, |this, _state, event, cx| { - match event { - StateEvent::SignerSet => { - this.reset(cx); - this.get_rooms(cx); - } - StateEvent::RelayConnected => { - this.get_contact_list(cx); - this.get_messages(cx) - } - _ => {} + cx.subscribe_in(&nostr, window, |this, state, event, window, cx| { + if event == &StateEvent::SignerSet { + this.reset(cx); + this.get_contact_list(cx); + this.get_rooms(cx); + + let signer = state.read(cx).signer(); + cx.spawn_in(window, async move |this, cx| { + let user_signer = signer.get().await; + this.update(cx, |this, cx| { + this.get_messages(user_signer, cx); + }) + .ok(); + }) + .detach(); + }; + }), + ); + + subscriptions.push( + // Subscribe to the device event + cx.subscribe_in(&device, window, |_this, _s, event, window, cx| { + if event == &DeviceEvent::Set { + let nostr = NostrRegistry::global(cx); + let signer = nostr.read(cx).signer(); + + cx.spawn_in(window, async move |this, cx| { + if let Some(device_signer) = signer.get_encryption_signer().await { + this.update(cx, |this, cx| { + this.get_messages(device_signer, cx); + }) + .ok(); + } + }) + .detach(); }; }), ); @@ -153,6 +181,7 @@ impl ChatRegistry { }); Self { + initializing: true, rooms: vec![], trashes: cx.new(|_| BTreeSet::default()), seens: Arc::new(RwLock::new(HashMap::default())), @@ -306,7 +335,7 @@ impl ChatRegistry { } /// Get contact list from relays - pub fn get_contact_list(&mut self, cx: &mut Context) { + fn get_contact_list(&mut self, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); let signer = nostr.read(cx).signer(); @@ -336,15 +365,18 @@ impl ChatRegistry { self.tasks.push(task); } - /// Get all messages for current user - pub fn get_messages(&mut self, cx: &mut Context) { - let task = self.subscribe(cx); + /// Get all messages for the provided signer + fn get_messages(&mut self, signer: T, cx: &mut Context) + where + T: NostrSigner + 'static, + { + let task = self.subscribe_gift_wrap_events(signer, cx); self.tasks.push(cx.spawn(async move |this, cx| { match task.await { Ok(_) => { - this.update(cx, |_this, cx| { - cx.emit(ChatEvent::Subscribed); + this.update(cx, |this, cx| { + this.set_initializing(false, cx); })?; } Err(e) => { @@ -365,6 +397,7 @@ impl ChatRegistry { cx.background_spawn(async move { let public_key = signer.get_public_key().await?; + let id = SubscriptionId::new("inbox-relay"); // Construct filter for inbox relays let filter = Filter::new() @@ -375,12 +408,12 @@ impl ChatRegistry { // Stream events from user's write relays let mut stream = client .stream_events(filter) + .with_id(id) .timeout(Duration::from_secs(TIMEOUT)) .await?; while let Some((_url, res)) = stream.next().await { if let Ok(event) = res { - log::debug!("Got event: {:?}", event); let urls: Vec = nip17::extract_owned_relay_list(event).collect(); return Ok(urls); } @@ -390,18 +423,20 @@ impl ChatRegistry { }) } - /// Continuously get gift wrap events for the current user in their messaging relays - fn subscribe(&self, cx: &App) -> Task> { + /// Continuously get gift wrap events for the signer + fn subscribe_gift_wrap_events(&self, signer: T, cx: &App) -> Task> + where + T: NostrSigner + 'static, + { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let signer = nostr.read(cx).signer(); let urls = self.get_messaging_relays(cx); cx.background_spawn(async move { let urls = urls.await?; let public_key = signer.get_public_key().await?; let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key); - let id = SubscriptionId::new(USER_GIFTWRAP); + let id = SubscriptionId::new(format!("{}-msg", public_key.to_hex())); // Ensure relay connections for url in urls.iter() { @@ -425,6 +460,37 @@ impl ChatRegistry { }) } + /// Refresh the chat registry, fetching messages and contact list from relays. + pub fn refresh(&mut self, window: &mut Window, cx: &mut Context) { + self.reset(cx); + self.get_contact_list(cx); + self.get_rooms(cx); + + let nostr = NostrRegistry::global(cx); + let signer = nostr.read(cx).signer(); + + cx.spawn_in(window, async move |this, cx| { + let user_signer = signer.get().await; + let device_signer = signer.get_encryption_signer().await; + + this.update(cx, |this, cx| { + this.get_messages(user_signer, cx); + + if let Some(device_signer) = device_signer { + this.get_messages(device_signer, cx); + } + }) + .ok(); + }) + .detach(); + } + + /// Set the initializing status of the chat registry + fn set_initializing(&mut self, initializing: bool, cx: &mut Context) { + self.initializing = initializing; + cx.notify(); + } + /// Get the loading status of the chat registry pub fn loading(&self) -> bool { self.tracking_flag.load(Ordering::Acquire) @@ -577,6 +643,7 @@ impl ChatRegistry { /// Reset the registry. pub fn reset(&mut self, cx: &mut Context) { + self.initializing = true; self.rooms.clear(); self.trashes.update(cx, |this, cx| { this.clear(); diff --git a/crates/chat/src/message.rs b/crates/chat/src/message.rs index cf4b293..de91023 100644 --- a/crates/chat/src/message.rs +++ b/crates/chat/src/message.rs @@ -1,7 +1,7 @@ use std::hash::Hash; use std::ops::Range; -use common::{EventUtils, NostrParser}; +use common::{EventExt, NostrParser}; use gpui::SharedString; use nostr_sdk::prelude::*; diff --git a/crates/chat/src/room.rs b/crates/chat/src/room.rs index ea69c2d..ee1d79f 100644 --- a/crates/chat/src/room.rs +++ b/crates/chat/src/room.rs @@ -3,7 +3,7 @@ use std::hash::{Hash, Hasher}; use std::time::Duration; use anyhow::{Error, anyhow}; -use common::EventUtils; +use common::EventExt; use gpui::{App, AppContext, Context, EventEmitter, SharedString, Task}; use itertools::Itertools; use nostr_sdk::prelude::*; diff --git a/crates/chat_ui/src/lib.rs b/crates/chat_ui/src/lib.rs index 15c374d..09dd12c 100644 --- a/crates/chat_ui/src/lib.rs +++ b/crates/chat_ui/src/lib.rs @@ -4,7 +4,7 @@ use std::sync::Arc; pub use actions::*; use anyhow::{Context as AnyhowContext, Error}; use chat::{ChatRegistry, Message, RenderedMessage, Room, RoomEvent, SendReport, SendStatus}; -use common::RenderedTimestamp; +use common::TimestampExt; use gpui::prelude::FluentBuilder; use gpui::{ AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle, diff --git a/crates/common/src/display.rs b/crates/common/src/display.rs index 79ed85f..a288168 100644 --- a/crates/common/src/display.rs +++ b/crates/common/src/display.rs @@ -1,11 +1,10 @@ use std::sync::Arc; -use anyhow::{Error, anyhow}; use chrono::{Local, TimeZone}; use gpui::{Image, ImageFormat, SharedString}; use nostr_sdk::prelude::*; -use qrcode::render::svg; use qrcode::QrCode; +use qrcode::render::svg; const NOW: &str = "now"; const SECONDS_IN_MINUTE: i64 = 60; @@ -13,12 +12,12 @@ const MINUTES_IN_HOUR: i64 = 60; const HOURS_IN_DAY: i64 = 24; const DAYS_IN_MONTH: i64 = 30; -pub trait RenderedTimestamp { +pub trait TimestampExt { fn to_human_time(&self) -> SharedString; fn to_ago(&self) -> SharedString; } -impl RenderedTimestamp for Timestamp { +impl TimestampExt for Timestamp { fn to_human_time(&self) -> SharedString { let input_time = match Local.timestamp_opt(self.as_secs() as i64, 0) { chrono::LocalResult::Single(time) => time, @@ -61,23 +60,11 @@ impl RenderedTimestamp for Timestamp { } } -pub trait TextUtils { - fn to_public_key(&self) -> Result; +pub trait StringExt { fn to_qr(&self) -> Option>; } -impl> TextUtils for T { - fn to_public_key(&self) -> Result { - let s = self.as_ref(); - if s.starts_with("nprofile1") { - Ok(Nip19Profile::from_bech32(s)?.public_key) - } else if s.starts_with("npub1") { - Ok(PublicKey::parse(s)?) - } else { - Err(anyhow!("Invalid public key")) - } - } - +impl> StringExt for T { fn to_qr(&self) -> Option> { let s = self.as_ref(); let code = QrCode::new(s).unwrap(); diff --git a/crates/common/src/event.rs b/crates/common/src/event.rs index 03833c7..194cddf 100644 --- a/crates/common/src/event.rs +++ b/crates/common/src/event.rs @@ -3,12 +3,12 @@ use std::hash::{DefaultHasher, Hash, Hasher}; use itertools::Itertools; use nostr_sdk::prelude::*; -pub trait EventUtils { +pub trait EventExt { fn uniq_id(&self) -> u64; fn extract_public_keys(&self) -> Vec; } -impl EventUtils for Event { +impl EventExt for Event { fn uniq_id(&self) -> u64 { let mut hasher = DefaultHasher::new(); let mut pubkeys: Vec = self.extract_public_keys(); @@ -25,7 +25,7 @@ impl EventUtils for Event { } } -impl EventUtils for UnsignedEvent { +impl EventExt for UnsignedEvent { fn uniq_id(&self) -> u64 { let mut hasher = DefaultHasher::new(); let mut pubkeys: Vec = vec![]; diff --git a/crates/coop/src/dialogs/accounts.rs b/crates/coop/src/dialogs/accounts.rs index 49748f3..c780046 100644 --- a/crates/coop/src/dialogs/accounts.rs +++ b/crates/coop/src/dialogs/accounts.rs @@ -91,7 +91,7 @@ impl AccountSelector { fn login(&mut self, public_key: PublicKey, window: &mut Window, cx: &mut Context) { let nostr = NostrRegistry::global(cx); - let task = nostr.read(cx).get_signer(&public_key, cx); + let task = nostr.read(cx).get_secret(public_key, cx); // Mark the public key as being logged in self.set_logging_in(public_key, cx); @@ -117,7 +117,7 @@ impl AccountSelector { let nostr = NostrRegistry::global(cx); nostr.update(cx, |this, cx| { - this.remove_signer(&public_key, cx); + this.remove_secret(&public_key, cx); }); } diff --git a/crates/coop/src/dialogs/connect.rs b/crates/coop/src/dialogs/connect.rs index 8176587..537cdc1 100644 --- a/crates/coop/src/dialogs/connect.rs +++ b/crates/coop/src/dialogs/connect.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use std::time::Duration; -use common::TextUtils; +use common::StringExt; use gpui::prelude::FluentBuilder; use gpui::{ AppContext, Context, Entity, Image, IntoElement, ParentElement, Render, SharedString, Styled, diff --git a/crates/coop/src/dialogs/screening.rs b/crates/coop/src/dialogs/screening.rs index 07e5af0..3a13e7a 100644 --- a/crates/coop/src/dialogs/screening.rs +++ b/crates/coop/src/dialogs/screening.rs @@ -2,21 +2,21 @@ use std::collections::HashMap; use std::time::Duration; use anyhow::{Context as AnyhowContext, Error}; -use common::RenderedTimestamp; +use common::TimestampExt; use gpui::prelude::FluentBuilder; use gpui::{ - div, px, relative, uniform_list, App, AppContext, Context, Div, Entity, InteractiveElement, - IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Task, Window, + App, AppContext, Context, Div, Entity, InteractiveElement, IntoElement, ParentElement, Render, + SharedString, Styled, Subscription, Task, Window, div, px, relative, uniform_list, }; use nostr_sdk::prelude::*; -use person::{shorten_pubkey, Person, PersonRegistry}; -use smallvec::{smallvec, SmallVec}; -use state::{NostrAddress, NostrRegistry, BOOTSTRAP_RELAYS, TIMEOUT}; +use person::{Person, PersonRegistry, shorten_pubkey}; +use smallvec::{SmallVec, smallvec}; +use state::{BOOTSTRAP_RELAYS, NostrAddress, NostrRegistry, TIMEOUT}; use theme::ActiveTheme; use ui::avatar::Avatar; use ui::button::{Button, ButtonVariants}; use ui::indicator::Indicator; -use ui::{h_flex, v_flex, Icon, IconName, Sizable, StyledExt, WindowExtension}; +use ui::{Icon, IconName, Sizable, StyledExt, WindowExtension, h_flex, v_flex}; pub fn init(public_key: PublicKey, window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Screening::new(public_key, window, cx)) diff --git a/crates/coop/src/main.rs b/crates/coop/src/main.rs index 2903aad..b7d1edd 100644 --- a/crates/coop/src/main.rs +++ b/crates/coop/src/main.rs @@ -92,14 +92,14 @@ fn main() { // Initialize relay auth registry relay_auth::init(window, cx); - // Initialize app registry - chat::init(window, cx); - // Initialize device signer // // NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md device::init(window, cx); + // Initialize app registry + chat::init(window, cx); + // Initialize auto update auto_update::init(window, cx); diff --git a/crates/coop/src/sidebar/mod.rs b/crates/coop/src/sidebar/mod.rs index 902c28b..3cb3f9f 100644 --- a/crates/coop/src/sidebar/mod.rs +++ b/crates/coop/src/sidebar/mod.rs @@ -4,7 +4,7 @@ use std::time::Duration; use anyhow::{Context as AnyhowContext, Error}; use chat::{ChatEvent, ChatRegistry, Room, RoomKind}; -use common::{DebouncedDelay, RenderedTimestamp}; +use common::{DebouncedDelay, TimestampExt}; use entry::RoomEntry; use gpui::prelude::FluentBuilder; use gpui::{ diff --git a/crates/coop/src/workspace.rs b/crates/coop/src/workspace.rs index b88b981..090f91a 100644 --- a/crates/coop/src/workspace.rs +++ b/crates/coop/src/workspace.rs @@ -1,5 +1,3 @@ -use std::cell::Cell; -use std::rc::Rc; use std::sync::Arc; use ::settings::AppSettings; @@ -13,7 +11,7 @@ use gpui::{ relative, }; use nostr_sdk::prelude::*; -use person::PersonRegistry; +use person::{PersonRegistry, shorten_pubkey}; use serde::Deserialize; use smallvec::{SmallVec, smallvec}; use state::{NostrRegistry, StateEvent}; @@ -24,7 +22,7 @@ use ui::button::{Button, ButtonVariants}; use ui::dock::{ClosePanel, DockArea, DockItem, DockPlacement, PanelView}; use ui::menu::{DropdownMenu, PopupMenuItem}; use ui::notification::{Notification, NotificationKind}; -use ui::{Disableable, Icon, IconName, Root, Sizable, WindowExtension, h_flex, v_flex}; +use ui::{Icon, IconName, Root, Sizable, WindowExtension, h_flex, v_flex}; use crate::dialogs::restore::RestoreEncryption; use crate::dialogs::{accounts, settings}; @@ -51,7 +49,6 @@ enum Command { ToggleTheme, ToggleAccount, - RefreshRelayList, RefreshMessagingRelays, BackupEncryption, ImportEncryption, @@ -73,14 +70,8 @@ pub struct Workspace { /// App's Dock Area dock: Entity, - /// Whether a user's relay list is connected - relay_connected: bool, - - /// Whether the inbox is connected - inbox_connected: bool, - /// Event subscriptions - _subscriptions: SmallVec<[Subscription; 6]>, + _subscriptions: SmallVec<[Subscription; 5]>, } impl Workspace { @@ -88,7 +79,6 @@ impl Workspace { let chat = ChatRegistry::global(cx); let device = DeviceRegistry::global(cx); let nostr = NostrRegistry::global(cx); - let npubs = nostr.read(cx).npubs(); let titlebar = cx.new(|_| TitleBar::new()); let dock = cx.new(|cx| DockArea::new(window, cx)); @@ -102,15 +92,6 @@ impl Workspace { }), ); - subscriptions.push( - // Observe the npubs entity - cx.observe_in(&npubs, window, move |this, npubs, window, cx| { - if !npubs.read(cx).is_empty() { - this.account_selector(window, cx); - } - }), - ); - subscriptions.push( // Subscribe to the signer events cx.subscribe_in(&nostr, window, move |this, _state, event, window, cx| { @@ -141,28 +122,14 @@ impl Workspace { window.push_notification(note, cx); } - StateEvent::FetchingRelayList => { - let note = Notification::new() - .id::() - .message("Getting relay list...") - .with_kind(NotificationKind::Info); - - window.push_notification(note, cx); - } - StateEvent::RelayNotConfigured => { - this.relay_warning(window, cx); - } - StateEvent::RelayConnected => { - window.clear_notification::(cx); - this.set_relay_connected(true, cx); - } StateEvent::SignerSet => { this.set_center_layout(window, cx); - this.set_relay_connected(false, cx); - this.set_inbox_connected(false, cx); // Clear the signer notification window.clear_notification::(cx); } + StateEvent::Show => { + this.account_selector(window, cx); + } _ => {} }; }), @@ -174,10 +141,11 @@ impl Workspace { match event { DeviceEvent::Requesting => { const MSG: &str = - "Please open the other client and approve the encryption key request"; + "Coop has sent a request for an encryption key. Please open the other client then approve the request."; let note = Notification::new() .id::() + .autohide(false) .title("Wait for approval") .message(MSG) .with_kind(NotificationKind::Info); @@ -187,6 +155,7 @@ impl Workspace { DeviceEvent::Creating => { let note = Notification::new() .id::() + .autohide(false) .message("Creating encryption key") .with_kind(NotificationKind::Info); @@ -200,26 +169,6 @@ impl Workspace { window.push_notification(note, cx); } - DeviceEvent::NotSet { reason } => { - let note = Notification::new() - .id::() - .title("Cannot setup the encryption key") - .message(reason) - .autohide(false) - .with_kind(NotificationKind::Error); - - window.push_notification(note, cx); - } - DeviceEvent::NotSubscribe { reason } => { - let note = Notification::new() - .id::() - .title("Cannot getting messages") - .message(reason) - .autohide(false) - .with_kind(NotificationKind::Error); - - window.push_notification(note, cx); - } DeviceEvent::Error(error) => { window.push_notification(Notification::error(error).autohide(false), cx); } @@ -255,9 +204,6 @@ impl Workspace { }); }); } - ChatEvent::Subscribed => { - this.set_inbox_connected(true, cx); - } ChatEvent::Error(error) => { window.push_notification(Notification::error(error).autohide(false), cx); } @@ -285,8 +231,6 @@ impl Workspace { Self { titlebar, dock, - relay_connected: false, - inbox_connected: false, _subscriptions: subscriptions, } } @@ -318,18 +262,6 @@ impl Workspace { .collect() } - /// Set whether the relay list is connected - fn set_relay_connected(&mut self, connected: bool, cx: &mut Context) { - self.relay_connected = connected; - cx.notify(); - } - - /// Set whether the inbox is connected - fn set_inbox_connected(&mut self, connected: bool, cx: &mut Context) { - self.inbox_connected = connected; - cx.notify(); - } - /// Set the dock layout fn set_layout(&mut self, window: &mut Window, cx: &mut Context) { let left = DockItem::panel(Arc::new(sidebar::init(window, cx))); @@ -414,8 +346,9 @@ impl Workspace { } Command::RefreshMessagingRelays => { let chat = ChatRegistry::global(cx); + // Trigger a refresh of the chat registry chat.update(cx, |this, cx| { - this.get_messages(cx); + this.refresh(window, cx); }); } Command::ShowRelayList => { @@ -428,16 +361,6 @@ impl Workspace { ); }); } - Command::RefreshRelayList => { - let nostr = NostrRegistry::global(cx); - let signer = nostr.read(cx).signer(); - - if let Some(public_key) = signer.public_key() { - nostr.update(cx, |this, cx| { - this.ensure_relay_list(&public_key, cx); - }); - } - } Command::RefreshEncryption => { let device = DeviceRegistry::global(cx); device.update(cx, |this, cx| { @@ -630,55 +553,6 @@ impl Workspace { }); } - fn relay_warning(&mut self, window: &mut Window, cx: &mut Context) { - const BODY: &str = "Coop cannot found your gossip relay list. \ - Maybe you haven't set it yet or relay not responsed"; - - let nostr = NostrRegistry::global(cx); - let signer = nostr.read(cx).signer(); - - let Some(public_key) = signer.public_key() else { - return; - }; - - let entity = nostr.downgrade(); - let loading = Rc::new(Cell::new(false)); - - let note = Notification::new() - .autohide(false) - .id::() - .icon(IconName::Relay) - .title("Gossip Relays are required") - .message(BODY) - .action(move |_this, _window, _cx| { - let entity = entity.clone(); - let public_key = public_key.to_owned(); - - Button::new("retry") - .label("Retry") - .small() - .primary() - .loading(loading.get()) - .disabled(loading.get()) - .on_click({ - let loading = Rc::clone(&loading); - - move |_ev, _window, cx| { - // Set loading state to true - loading.set(true); - // Retry - entity - .update(cx, |this, cx| { - this.ensure_relay_list(&public_key, cx); - }) - .ok(); - } - }) - }); - - window.push_notification(note, cx); - } - fn titlebar_left(&mut self, cx: &mut Context) -> impl IntoElement { let nostr = NostrRegistry::global(cx); let signer = nostr.read(cx).signer(); @@ -759,19 +633,24 @@ impl Workspace { } fn titlebar_right(&mut self, cx: &mut Context) -> impl IntoElement { - let relay_connected = self.relay_connected; - let inbox_connected = self.inbox_connected; + let chat = ChatRegistry::global(cx); + let initializing = chat.read(cx).initializing; + let trash_messages = chat.read(cx).count_trash_messages(cx); + + let device = DeviceRegistry::global(cx); + let device_initializing = device.read(cx).initializing; let nostr = NostrRegistry::global(cx); let signer = nostr.read(cx).signer(); - let trashes = ChatRegistry::global(cx); - let trash_messages = trashes.read(cx).count_trash_messages(cx); - let Some(public_key) = signer.public_key() else { return div(); }; + let persons = PersonRegistry::global(cx); + let profile = persons.read(cx).get(&public_key, cx); + let announcement = profile.announcement(); + h_flex() .when(!cx.theme().platform.is_mac(), |this| this.pr_2()) .gap_2() @@ -813,53 +692,42 @@ impl Workspace { .tooltip("Decoupled encryption key") .small() .ghost() - .dropdown_menu(move |this, _window, cx| { - let device = DeviceRegistry::global(cx); - let subscribing = device.read(cx).subscribing; - let requesting = device.read(cx).requesting; - + .loading(device_initializing) + .when(device_initializing, |this| { + this.label("Dekey") + .xsmall() + .tooltip("Loading decoupled encryption key...") + }) + .dropdown_menu(move |this, _window, _cx| { this.min_w(px(260.)) .label("Encryption Key") - .when(requesting, |this| { + .when_some(announcement.as_ref(), |this, announcement| { + let name = announcement.client_name(); + let pkey = shorten_pubkey(announcement.public_key(), 8); + this.item(PopupMenuItem::element(move |_window, cx| { h_flex() - .px_1() - .w_full() - .gap_2() + .gap_1() .text_sm() .child( - div() - .size_1p5() - .rounded_full() - .bg(cx.theme().icon_accent), + Icon::new(IconName::Device) + .small() + .text_color(cx.theme().icon_muted), ) - .child(SharedString::from("Waiting for approval...")) + .child(name.clone()) + })) + .item(PopupMenuItem::element(move |_window, cx| { + h_flex() + .gap_1() + .text_sm() + .child( + Icon::new(IconName::UserKey) + .small() + .text_color(cx.theme().icon_muted), + ) + .child(SharedString::from(pkey.clone())) })) }) - .item(PopupMenuItem::element(move |_window, cx| { - h_flex() - .px_1() - .w_full() - .gap_2() - .text_sm() - .when(!subscribing, |this| { - this.text_color(cx.theme().text_muted) - }) - .child(div().size_1p5().rounded_full().map(|this| { - if subscribing { - this.bg(cx.theme().icon_accent) - } else { - this.bg(cx.theme().icon_muted) - } - })) - .map(|this| { - if subscribing { - this.child("Listening for messages") - } else { - this.child("Idle") - } - }) - })) .separator() .menu_with_icon( "Backup", @@ -889,17 +757,13 @@ impl Workspace { .icon(IconName::Inbox) .small() .ghost() - .loading(!inbox_connected) - .disabled(!inbox_connected) - .when(!inbox_connected, |this| { - this.tooltip("Connecting to the user's messaging relays...") + .loading(initializing) + .when(initializing, |this| { + this.label("Inbox") + .xsmall() + .tooltip("Getting inbox messages...") }) - .when(inbox_connected, |this| this.indicator()) .dropdown_menu(move |this, _window, cx| { - let chat = ChatRegistry::global(cx); - let persons = PersonRegistry::global(cx); - let profile = persons.read(cx).get(&public_key, cx); - let urls: Vec<(SharedString, SharedString)> = profile .messaging_relays() .iter() @@ -950,38 +814,17 @@ impl Workspace { Box::new(Command::RefreshMessagingRelays), ) .menu_with_icon( - "Update relays", + "Manage gossip relays", + IconName::Relay, + Box::new(Command::ShowRelayList), + ) + .menu_with_icon( + "Manage messaging relays", IconName::Settings, Box::new(Command::ShowMessaging), ) }), ) - .child( - Button::new("relay-list") - .icon(IconName::Relay) - .small() - .ghost() - .loading(!relay_connected) - .disabled(!relay_connected) - .when(!relay_connected, |this| { - this.tooltip("Connecting to the user's relay list...") - }) - .when(relay_connected, |this| this.indicator()) - .dropdown_menu(move |this, _window, _cx| { - this.label("User's Relay List") - .separator() - .menu_with_icon( - "Reload", - IconName::Refresh, - Box::new(Command::RefreshRelayList), - ) - .menu_with_icon( - "Update", - IconName::Settings, - Box::new(Command::ShowRelayList), - ) - }), - ) } } diff --git a/crates/device/src/lib.rs b/crates/device/src/lib.rs index 61b4f69..add0642 100644 --- a/crates/device/src/lib.rs +++ b/crates/device/src/lib.rs @@ -1,5 +1,5 @@ use std::cell::Cell; -use std::collections::{HashMap, HashSet}; +use std::collections::HashSet; use std::path::PathBuf; use std::rc::Rc; use std::time::Duration; @@ -11,12 +11,12 @@ use gpui::{ }; use nostr_sdk::prelude::*; use person::PersonRegistry; -use state::{Announcement, DEVICE_GIFTWRAP, NostrRegistry, StateEvent, TIMEOUT, app_name}; +use state::{Announcement, NostrRegistry, StateEvent, TIMEOUT, app_name}; use theme::ActiveTheme; use ui::avatar::Avatar; -use ui::button::{Button, ButtonVariants}; -use ui::notification::Notification; -use ui::{Disableable, IconName, Sizable, StyledExt, WindowExtension, h_flex, v_flex}; +use ui::button::Button; +use ui::notification::{Notification, NotificationKind}; +use ui::{Disableable, Sizable, StyledExt, WindowExtension, h_flex, v_flex}; const IDENTIFIER: &str = "coop:device"; const MSG: &str = "You've requested an encryption key from another device. \ @@ -39,10 +39,6 @@ pub enum DeviceEvent { Requesting, /// The device is creating a new encryption key Creating, - /// Encryption key is not set - NotSet { reason: SharedString }, - /// An event to notify that Coop isn't subscribed to gift wrap events - NotSubscribe { reason: SharedString }, /// An error occurred Error(SharedString), } @@ -54,24 +50,6 @@ impl DeviceEvent { { Self::Error(error.into()) } - - pub fn not_subscribe(reason: T) -> Self - where - T: Into, - { - Self::NotSubscribe { - reason: reason.into(), - } - } - - pub fn not_set(reason: T) -> Self - where - T: Into, - { - Self::NotSet { - reason: reason.into(), - } - } } /// Device Registry @@ -79,14 +57,11 @@ impl DeviceEvent { /// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md #[derive(Debug)] pub struct DeviceRegistry { - /// Whether the registry is currently subscribing to gift wrap events - pub subscribing: bool, - - /// Whether the registry is waiting for encryption key approval from other devices - pub requesting: bool, + /// Whether the registry is currently initializing + pub initializing: bool, /// Whether there is a pending request for encryption key approval - pub has_pending_request: bool, + pub pending_request: bool, /// Async tasks tasks: Vec>>, @@ -112,17 +87,11 @@ impl DeviceRegistry { fn new(window: &mut Window, cx: &mut Context) -> Self { let nostr = NostrRegistry::global(cx); - // Get announcement when signer is set + // Subscribe to nostr state events let subscription = cx.subscribe_in(&nostr, window, |this, _e, event, _window, cx| { - match event { - StateEvent::SignerSet => { - this.set_subscribing(false, cx); - this.set_requesting(false, cx); - } - StateEvent::RelayConnected => { - this.get_announcement(cx); - } - _ => {} + if event == &StateEvent::SignerSet { + this.set_initializing(true, cx); + this.get_announcement(cx); }; }); @@ -131,9 +100,8 @@ impl DeviceRegistry { }); Self { - subscribing: false, - requesting: false, - has_pending_request: false, + initializing: true, + pending_request: false, tasks: vec![], _subscription: Some(subscription), } @@ -198,21 +166,15 @@ impl DeviceRegistry { })); } - /// Set whether the registry is currently subscribing to gift wrap events - fn set_subscribing(&mut self, subscribing: bool, cx: &mut Context) { - self.subscribing = subscribing; - cx.notify(); - } - - /// Set whether the registry is waiting for encryption key approval from other devices - fn set_requesting(&mut self, requesting: bool, cx: &mut Context) { - self.requesting = requesting; + /// Set whether the registry is currently initializing + fn set_initializing(&mut self, initializing: bool, cx: &mut Context) { + self.initializing = initializing; cx.notify(); } /// Set whether there is a pending request for encryption key approval - fn set_has_pending_request(&mut self, pending: bool, cx: &mut Context) { - self.has_pending_request = pending; + fn set_pending_request(&mut self, pending: bool, cx: &mut Context) { + self.pending_request = pending; cx.notify(); } @@ -229,76 +191,14 @@ impl DeviceRegistry { // Update state this.update(cx, |this, cx| { + this.set_initializing(false, cx); cx.emit(DeviceEvent::Set); - this.get_messages(cx); })?; Ok(()) })); } - /// Get all messages for encryption keys - fn get_messages(&mut self, cx: &mut Context) { - let task = self.subscribe_to_giftwrap_events(cx); - - self.tasks.push(cx.spawn(async move |this, cx| { - if let Err(e) = task.await { - this.update(cx, |_this, cx| { - cx.emit(DeviceEvent::not_subscribe(e.to_string())); - })?; - } else { - this.update(cx, |this, cx| { - this.set_subscribing(true, cx); - })?; - } - Ok(()) - })); - } - - /// Continuously get gift wrap events for the current user in their messaging relays - fn subscribe_to_giftwrap_events(&self, cx: &App) -> Task> { - let persons = PersonRegistry::global(cx); - let nostr = NostrRegistry::global(cx); - let client = nostr.read(cx).client(); - let signer = nostr.read(cx).signer(); - - let Some(user) = signer.public_key() else { - return Task::ready(Err(anyhow!("User not found"))); - }; - - let profile = persons.read(cx).get(&user, cx); - let relays = profile.messaging_relays().clone(); - - cx.background_spawn(async move { - let encryption = signer.get_encryption_signer().await.context("not found")?; - let public_key = encryption.get_public_key().await?; - - let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key); - let id = SubscriptionId::new(DEVICE_GIFTWRAP); - - // Ensure user has relays configured - if relays.is_empty() { - return Err(anyhow!("No messaging relays found")); - } - - // Ensure relays are connected - for url in relays.iter() { - client.add_relay(url).and_connect().await?; - } - - // Construct target for subscription - let target: HashMap = relays - .into_iter() - .map(|relay| (relay, filter.clone())) - .collect(); - - // Subscribe - client.subscribe(target).with_id(id).await?; - - Ok(()) - }) - } - /// Backup the encryption's secret key to a file pub fn backup(&self, path: PathBuf, cx: &App) -> Task> { let nostr = NostrRegistry::global(cx); @@ -431,30 +331,27 @@ impl DeviceRegistry { // Get encryption key from the database and compare with the announcement let task: Task> = cx.background_spawn(async move { - if let Ok(keys) = get_keys(&client).await { - if keys.public_key() != device_pubkey { - return Err(anyhow!("Encryption Key doesn't match the announcement")); - }; - Ok(keys) - } else { - Err(anyhow!("Encryption Key not found. Please create a new key")) - } + let keys = get_keys(&client).await?; + + // Compare the public key from the announcement with the one from the database + if keys.public_key() != device_pubkey { + return Err(anyhow!("Encryption Key doesn't match the announcement")); + }; + + Ok(keys) }); self.tasks.push(cx.spawn(async move |this, cx| { - match task.await { - Ok(keys) => { - this.update(cx, |this, cx| { - this.set_signer(keys, cx); - this.wait_for_request(cx); - })?; - } - Err(e) => { - this.update(cx, |_this, cx| { - cx.emit(DeviceEvent::not_set(e.to_string())); - })?; - } - }; + if let Ok(keys) = task.await { + this.update(cx, |this, cx| { + this.set_signer(keys, cx); + this.wait_for_request(cx); + })?; + } else { + this.update(cx, |this, cx| { + this.request(cx); + })?; + } Ok(()) })); } @@ -467,21 +364,16 @@ impl DeviceRegistry { self.tasks.push(cx.background_spawn(async move { let public_key = signer.get_public_key().await?; + let id = SubscriptionId::new("dekey-requests"); // Construct a filter for encryption key requests - let now = Filter::new() + let filter = Filter::new() .kind(Kind::Custom(4454)) .author(public_key) .since(Timestamp::now()); - // Construct a filter for the last encryption key request - let last = Filter::new() - .kind(Kind::Custom(4454)) - .author(public_key) - .limit(1); - // Subscribe to the device key requests on user's write relays - client.subscribe(vec![now, last]).await?; + client.subscribe(vec![filter]).with_id(id).await?; Ok(()) })); @@ -537,7 +429,7 @@ impl DeviceRegistry { } Ok(None) => { this.update(cx, |this, cx| { - this.set_requesting(true, cx); + this.set_initializing(false, cx); this.wait_for_approval(cx); cx.emit(DeviceEvent::Requesting); @@ -602,12 +494,11 @@ impl DeviceRegistry { Ok(keys) => { this.update(cx, |this, cx| { this.set_signer(keys, cx); - this.set_requesting(false, cx); })?; } Err(e) => { this.update(cx, |_this, cx| { - cx.emit(DeviceEvent::not_set(e.to_string())); + cx.emit(DeviceEvent::error(e.to_string())); })?; } } @@ -683,10 +574,10 @@ impl DeviceRegistry { /// Handle encryption request fn ask_for_approval(&mut self, event: Event, window: &mut Window, cx: &mut Context) { // Ignore if there is already a pending request - if self.has_pending_request { + if self.pending_request { return; } - self.set_has_pending_request(true, cx); + self.set_pending_request(true, cx); // Show notification let notification = self.notification(event, cx); @@ -706,8 +597,8 @@ impl DeviceRegistry { Notification::new() .type_id::(key) .autohide(false) - .icon(IconName::UserKey) - .title(SharedString::from("New request")) + .with_kind(NotificationKind::Info) + .title("Encryption Key Request") .content(move |_this, _window, cx| { v_flex() .gap_2() @@ -730,7 +621,7 @@ impl DeviceRegistry { .font_semibold() .text_xs() .text_color(cx.theme().text_muted) - .child(SharedString::from("Requester:")), + .child(SharedString::from("From:")), ) .child( div() @@ -777,8 +668,6 @@ impl DeviceRegistry { Button::new("approve") .label("Approve") - .small() - .primary() .loading(loading.get()) .disabled(loading.get()) .on_click({ diff --git a/crates/person/src/lib.rs b/crates/person/src/lib.rs index 7f969c4..3f72f99 100644 --- a/crates/person/src/lib.rs +++ b/crates/person/src/lib.rs @@ -4,7 +4,7 @@ use std::rc::Rc; use std::time::Duration; use anyhow::{Error, anyhow}; -use common::EventUtils; +use common::EventExt; use gpui::{App, AppContext, Context, Entity, Global, Task, Window}; use nostr_sdk::prelude::*; use smallvec::{SmallVec, smallvec}; diff --git a/crates/relay_auth/src/lib.rs b/crates/relay_auth/src/lib.rs index 25a2106..4bac09a 100644 --- a/crates/relay_auth/src/lib.rs +++ b/crates/relay_auth/src/lib.rs @@ -15,9 +15,9 @@ use settings::{AppSettings, AuthMode}; use smallvec::{SmallVec, smallvec}; use state::NostrRegistry; use theme::ActiveTheme; -use ui::button::{Button, ButtonVariants}; -use ui::notification::Notification; -use ui::{Disableable, IconName, Sizable, StyledExt, WindowExtension, v_flex}; +use ui::button::Button; +use ui::notification::{Notification, NotificationKind}; +use ui::{Disableable, WindowExtension, v_flex}; const AUTH_MESSAGE: &str = "Approve the authentication request to allow Coop to continue sending or receiving events."; @@ -327,8 +327,8 @@ impl RelayAuth { Notification::new() .type_id::(challenge) .autohide(false) - .icon(IconName::Warning) - .title(SharedString::from("Authentication Required")) + .with_kind(NotificationKind::Info) + .title("Authentication Required") .content(move |_this, _window, cx| { v_flex() .gap_2() @@ -344,7 +344,6 @@ impl RelayAuth { .px_1p5() .rounded_sm() .text_xs() - .font_semibold() .bg(cx.theme().elevated_surface_background) .text_color(cx.theme().text) .child(url.clone()), @@ -357,8 +356,6 @@ impl RelayAuth { Button::new("approve") .label("Approve") - .small() - .primary() .loading(loading.get()) .disabled(loading.get()) .on_click({ diff --git a/crates/state/Cargo.toml b/crates/state/Cargo.toml index ae39528..f3384d7 100644 --- a/crates/state/Cargo.toml +++ b/crates/state/Cargo.toml @@ -10,7 +10,6 @@ common = { path = "../common" } nostr.workspace = true nostr-sdk.workspace = true nostr-lmdb.workspace = true -nostr-memory.workspace = true nostr-gossip-memory.workspace = true nostr-connect.workspace = true nostr-blossom.workspace = true diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs index 1734786..9f177eb 100644 --- a/crates/state/src/lib.rs +++ b/crates/state/src/lib.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; @@ -8,7 +9,6 @@ use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, SharedString, use nostr_connect::prelude::*; use nostr_gossip_memory::prelude::*; use nostr_lmdb::prelude::*; -use nostr_memory::prelude::*; use nostr_sdk::prelude::*; mod blossom; @@ -44,18 +44,14 @@ impl Global for GlobalNostrRegistry {} /// Signer event. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub enum StateEvent { - /// Creating the signer - Creating, /// Connecting to the bootstrapping relay Connecting, /// Connected to the bootstrapping relay Connected, - /// Fetching the relay list - FetchingRelayList, - /// User has not set up NIP-65 relays - RelayNotConfigured, - /// Connected to NIP-65 relays - RelayConnected, + /// Creating the signer + Creating, + /// Show the identity dialog + Show, /// A new signer has been set SignerSet, /// An error occurred @@ -80,16 +76,19 @@ pub struct NostrRegistry { /// Nostr signer signer: Arc, - /// Local public keys + /// All local stored identities npubs: Entity>, - /// App keys + /// Keys directory + key_dir: PathBuf, + + /// Master app keys used for various operations. /// - /// Used for Nostr Connect and NIP-4e operations + /// Example: Nostr Connect and NIP-4e operations app_keys: Keys, /// Tasks for asynchronous operations - tasks: Vec>, + tasks: Vec>>, } impl EventEmitter for NostrRegistry {} @@ -107,55 +106,57 @@ impl NostrRegistry { /// Create a new nostr instance fn new(window: &mut Window, cx: &mut Context) -> Self { + let key_dir = config_dir().join("keys"); + let app_keys = get_or_init_app_keys(cx).unwrap_or(Keys::generate()); + // Construct the nostr signer - let app_keys = get_or_init_app_keys().unwrap_or(Keys::generate()); let signer = Arc::new(CoopSigner::new(app_keys.clone())); - // Construct the nostr npubs entity - let npubs = cx.new(|_| vec![]); + // Get all local stored npubs + let npubs = cx.new(|_| match Self::discover(&key_dir) { + Ok(npubs) => npubs, + Err(e) => { + log::error!("Failed to discover npubs: {e}"); + vec![] + } + }); - // Construct the nostr client builder - let mut builder = ClientBuilder::default() + // Construct the nostr lmdb instance + let lmdb = cx.foreground_executor().block_on(async move { + NostrLmdb::open(config_dir().join("nostr")) + .await + .expect("Failed to initialize database") + }); + + // Construct the nostr client + let client = ClientBuilder::default() .signer(signer.clone()) + .database(lmdb) .gossip(NostrGossipMemory::unbounded()) - .gossip_config( - GossipConfig::default() - .no_background_refresh() - .sync_idle_timeout(Duration::from_secs(TIMEOUT)) - .sync_initial_timeout(Duration::from_millis(600)), - ) .automatic_authentication(false) - .verify_subscriptions(false) .connect_timeout(Duration::from_secs(10)) .sleep_when_idle(SleepWhenIdle::Enabled { timeout: Duration::from_secs(600), - }); - - // Add database if not in debug mode - if !cfg!(debug_assertions) { - // Construct the nostr lmdb instance - let lmdb = cx.foreground_executor().block_on(async move { - NostrLmdb::open(config_dir().join("nostr")) - .await - .expect("Failed to initialize database") - }); - builder = builder.database(lmdb); - } else { - builder = builder.database(MemoryDatabase::unbounded()) - } - - // Build the nostr client - let client = builder.build(); + }) + .build(); // Run at the end of current cycle cx.defer_in(window, |this, _window, cx| { this.connect(cx); + // Create an identity if none exists + if this.npubs.read(cx).is_empty() { + this.create_identity(cx); + } else { + // Show the identity dialog + cx.emit(StateEvent::Show); + } }); Self { client, signer, npubs, + key_dir, app_keys, tasks: vec![], } @@ -181,6 +182,33 @@ impl NostrRegistry { self.app_keys.clone() } + /// Discover all npubs in the keys directory + fn discover(dir: &PathBuf) -> Result, Error> { + // Ensure keys directory exists + std::fs::create_dir_all(dir)?; + + let files = std::fs::read_dir(dir)?; + let mut entries = Vec::new(); + let mut npubs: Vec = Vec::new(); + + for file in files.flatten() { + let metadata = file.metadata()?; + let modified_time = metadata.modified()?; + let name = file.file_name().into_string().unwrap().replace(".npub", ""); + entries.push((modified_time, name)); + } + + // Sort by modification time (most recent first) + entries.sort_by(|a, b| b.0.cmp(&a.0)); + + for (_, name) in entries { + let public_key = PublicKey::parse(&name)?; + npubs.push(public_key); + } + + Ok(npubs) + } + /// Connect to the bootstrapping relays fn connect(&mut self, cx: &mut Context) { let client = self.client(); @@ -219,105 +247,143 @@ impl NostrRegistry { // Emit connecting event cx.emit(StateEvent::Connecting); - self.tasks - .push(cx.spawn(async move |this, cx| match task.await { - Ok(_) => { - this.update(cx, |this, cx| { - cx.emit(StateEvent::Connected); - this.get_npubs(cx); - }) - .ok(); - } - Err(e) => { - this.update(cx, |_this, cx| { - cx.emit(StateEvent::error(e.to_string())); - }) - .ok(); - } - })); - } - - /// Get all used npubs - fn get_npubs(&mut self, cx: &mut Context) { - let npubs = self.npubs.downgrade(); - - let task: Task, Error>> = cx.background_spawn(async move { - let dir = config_dir().join("keys"); - // Ensure keys directory exists - smol::fs::create_dir_all(&dir).await?; - - let mut files = smol::fs::read_dir(&dir).await?; - let mut entries = Vec::new(); - - while let Some(Ok(entry)) = files.next().await { - let metadata = entry.metadata().await?; - let modified_time = metadata.modified()?; - let name = entry - .file_name() - .into_string() - .unwrap() - .replace(".npub", ""); - - entries.push((modified_time, name)); - } - - // Sort by modification time (most recent first) - entries.sort_by(|a, b| b.0.cmp(&a.0)); - - let mut npubs = Vec::new(); - - for (_, name) in entries { - let public_key = PublicKey::parse(&name)?; - npubs.push(public_key); - } - - Ok(npubs) - }); - self.tasks.push(cx.spawn(async move |this, cx| { - match task.await { - Ok(public_keys) => match public_keys.is_empty() { - true => { - this.update(cx, |this, cx| { - this.create_identity(cx); - }) - .ok(); - } - false => { - // TODO: auto login - npubs - .update(cx, |this, cx| { - this.extend(public_keys); - cx.notify(); - }) - .ok(); - } - }, - Err(e) => { - this.update(cx, |_this, cx| { - cx.emit(StateEvent::error(e.to_string())); - }) - .ok(); - } + if let Err(e) = task.await { + this.update(cx, |_this, cx| { + cx.emit(StateEvent::error(e.to_string())); + })?; + } else { + this.update(cx, |_this, cx| { + cx.emit(StateEvent::Connected); + })?; } + + Ok(()) })); } + /// Get the secret for a given npub. + pub fn get_secret( + &self, + public_key: PublicKey, + cx: &App, + ) -> Task, Error>> { + let npub = public_key.to_bech32().unwrap(); + let key_path = self.key_dir.join(format!("{}.npub", npub)); + let app_keys = self.app_keys.clone(); + + if let Ok(payload) = std::fs::read_to_string(key_path) { + if !payload.is_empty() { + cx.background_spawn(async move { + let decrypted = app_keys.nip44_decrypt(&public_key, &payload).await?; + let secret = SecretKey::parse(&decrypted)?; + let keys = Keys::new(secret); + + Ok(keys.into_nostr_signer()) + }) + } else { + self.get_secret_keyring(&npub, cx) + } + } else { + self.get_secret_keyring(&npub, cx) + } + } + + /// Get the secret for a given npub in the OS credentials store. + #[deprecated = "Use get_secret instead"] + fn get_secret_keyring( + &self, + user: &str, + cx: &App, + ) -> Task, Error>> { + let read = cx.read_credentials(user); + let app_keys = self.app_keys.clone(); + + cx.background_spawn(async move { + let (_, secret) = read + .await + .map_err(|_| anyhow!("Failed to get signer. Please re-import the secret key"))? + .ok_or_else(|| anyhow!("Failed to get signer. Please re-import the secret key"))?; + + // Try to parse as a direct secret key first + if let Ok(secret_key) = SecretKey::from_slice(&secret) { + return Ok(Keys::new(secret_key).into_nostr_signer()); + } + + // Convert the secret into string + let sec = String::from_utf8(secret) + .map_err(|_| anyhow!("Failed to parse secret as UTF-8"))?; + + // Try to parse as a NIP-46 URI + let uri = + NostrConnectUri::parse(&sec).map_err(|_| anyhow!("Failed to parse NIP-46 URI"))?; + + let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT); + let mut nip46 = NostrConnect::new(uri, app_keys, timeout, None)?; + + // Set the auth URL handler + nip46.auth_url_handler(CoopAuthUrlHandler); + + Ok(nip46.into_nostr_signer()) + }) + } + + /// Add a new npub to the keys directory + fn write_secret( + &self, + public_key: PublicKey, + secret: String, + cx: &App, + ) -> Task> { + let npub = public_key.to_bech32().unwrap(); + let key_path = self.key_dir.join(format!("{}.npub", npub)); + let app_keys = self.app_keys.clone(); + + cx.background_spawn(async move { + // If the secret starts with "bunker://" (nostr connect), use it directly; otherwise, encrypt it + let content = if secret.starts_with("bunker://") { + secret + } else { + app_keys.nip44_encrypt(&public_key, &secret).await? + }; + + // Write the encrypted secret to the keys directory + smol::fs::write(key_path, &content).await?; + + Ok(()) + }) + } + + /// Remove a secret + pub fn remove_secret(&mut self, public_key: &PublicKey, cx: &mut Context) { + let public_key = public_key.to_owned(); + let npub = public_key.to_bech32().unwrap(); + + let keys_dir = config_dir().join("keys"); + let key_path = keys_dir.join(format!("{}.npub", npub)); + + // Remove the secret file from the keys directory + std::fs::remove_file(key_path).ok(); + + self.npubs.update(cx, |this, cx| { + this.retain(|k| k != &public_key); + cx.notify(); + }); + } + /// Create a new identity - fn create_identity(&mut self, cx: &mut Context) { + pub fn create_identity(&mut self, cx: &mut Context) { let client = self.client(); let keys = Keys::generate(); let async_keys = keys.clone(); - let username = keys.public_key().to_bech32().unwrap(); - let secret = keys.secret_key().to_secret_bytes(); - - // Create a write credential task - let write_credential = cx.write_credentials(&username, &username, &secret); - // Emit creating event cx.emit(StateEvent::Creating); + // Create the write secret task + let write_secret = + self.write_secret(keys.public_key(), keys.secret_key().to_secret_hex(), cx); + // Run async tasks in background let task: Task> = cx.background_spawn(async move { let signer = async_keys.into_nostr_signer(); @@ -362,14 +428,10 @@ impl NostrRegistry { let event = EventBuilder::nip17_relay_list(relays).sign(&signer).await?; // Publish messaging relay list event - client - .send_event(&event) - .to_nip65() - .ack_policy(AckPolicy::none()) - .await?; + client.send_event(&event).to_nip65().await?; // Write user's credentials to the system keyring - write_credential.await?; + write_secret.await?; Ok(()) }); @@ -379,58 +441,19 @@ impl NostrRegistry { Ok(_) => { this.update(cx, |this, cx| { this.set_signer(keys, cx); - }) - .ok(); + })?; } Err(e) => { this.update(cx, |_this, cx| { cx.emit(StateEvent::error(e.to_string())); - }) - .ok(); + })?; } }; + + Ok(()) })); } - /// Get the signer in keyring by username - pub fn get_signer( - &self, - public_key: &PublicKey, - cx: &App, - ) -> Task, Error>> { - let username = public_key.to_bech32().unwrap(); - let app_keys = self.app_keys.clone(); - let read_credential = cx.read_credentials(&username); - - cx.spawn(async move |_cx| { - let (_, secret) = read_credential - .await - .map_err(|_| anyhow!("Failed to get signer. Please re-import the secret key"))? - .ok_or_else(|| anyhow!("Failed to get signer. Please re-import the secret key"))?; - - // Try to parse as a direct secret key first - if let Ok(secret_key) = SecretKey::from_slice(&secret) { - return Ok(Keys::new(secret_key).into_nostr_signer()); - } - - // Convert the secret into string - let sec = String::from_utf8(secret) - .map_err(|_| anyhow!("Failed to parse secret as UTF-8"))?; - - // Try to parse as a NIP-46 URI - let uri = - NostrConnectUri::parse(&sec).map_err(|_| anyhow!("Failed to parse NIP-46 URI"))?; - - let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT); - let mut nip46 = NostrConnect::new(uri, app_keys, timeout, None)?; - - // Set the auth URL handler - nip46.auth_url_handler(CoopAuthUrlHandler); - - Ok(nip46.into_nostr_signer()) - }) - } - /// Set the signer for the nostr client and verify the public key pub fn set_signer(&mut self, new: T, cx: &mut Context) where @@ -449,15 +472,6 @@ impl NostrRegistry { let signer = client.signer().context("Signer not found")?; let public_key = signer.get_public_key().await?; - let npub = public_key.to_bech32().unwrap(); - let keys_dir = config_dir().join("keys"); - - // Ensure keys directory exists - smol::fs::create_dir_all(&keys_dir).await?; - - let key_path = keys_dir.join(format!("{}.npub", npub)); - smol::fs::write(key_path, "").await?; - log::info!("Signer's public key: {}", public_key); Ok(public_key) }); @@ -465,9 +479,7 @@ impl NostrRegistry { self.tasks.push(cx.spawn(async move |this, cx| { match task.await { Ok(public_key) => { - // Update states this.update(cx, |this, cx| { - this.ensure_relay_list(&public_key, cx); // Add public key to npubs if not already present this.npubs.update(cx, |this, cx| { if !this.contains(&public_key) { @@ -475,65 +487,43 @@ impl NostrRegistry { cx.notify(); } }); + // Emit signer changed event cx.emit(StateEvent::SignerSet); - }) - .ok(); + })?; } Err(e) => { this.update(cx, |_this, cx| { cx.emit(StateEvent::error(e.to_string())); - }) - .ok(); + })?; } }; - })); - } - /// Remove a signer from the keyring - pub fn remove_signer(&mut self, public_key: &PublicKey, cx: &mut Context) { - let public_key = public_key.to_owned(); - let npub = public_key.to_bech32().unwrap(); - let keys_dir = config_dir().join("keys"); - - self.tasks.push(cx.spawn(async move |this, cx| { - let key_path = keys_dir.join(format!("{}.npub", npub)); - smol::fs::remove_file(key_path).await.ok(); - - this.update(cx, |this, cx| { - this.npubs().update(cx, |this, cx| { - this.retain(|k| k != &public_key); - cx.notify(); - }); - }) - .ok(); + Ok(()) })); } /// Add a key signer to keyring pub fn add_key_signer(&mut self, keys: &Keys, cx: &mut Context) { let keys = keys.clone(); - let username = keys.public_key().to_bech32().unwrap(); - let secret = keys.secret_key().to_secret_bytes(); - - // Write the credential to the keyring - let write_credential = cx.write_credentials(&username, "keys", &secret); + let write_secret = + self.write_secret(keys.public_key(), keys.secret_key().to_secret_hex(), cx); self.tasks.push(cx.spawn(async move |this, cx| { - match write_credential.await { + match write_secret.await { Ok(_) => { this.update(cx, |this, cx| { this.set_signer(keys, cx); - }) - .ok(); + })?; } Err(e) => { this.update(cx, |_this, cx| { cx.emit(StateEvent::error(e.to_string())); - }) - .ok(); + })?; } }; + + Ok(()) })); } @@ -554,154 +544,35 @@ impl NostrRegistry { self.tasks.push(cx.spawn(async move |this, cx| { match task.await { Ok((public_key, uri)) => { - let username = public_key.to_bech32().unwrap(); - let write_credential = this - .read_with(cx, |_this, cx| { - cx.write_credentials( - &username, - "nostrconnect", - uri.to_string().as_bytes(), - ) - }) - .unwrap(); + // Create the write secret task + let write_secret = this.read_with(cx, |this, cx| { + this.write_secret(public_key, uri.to_string(), cx) + })?; - match write_credential.await { + match write_secret.await { Ok(_) => { this.update(cx, |this, cx| { this.set_signer(nip46, cx); - }) - .ok(); + })?; } Err(e) => { this.update(cx, |_this, cx| { cx.emit(StateEvent::error(e.to_string())); - }) - .ok(); + })?; } } } Err(e) => { this.update(cx, |_this, cx| { cx.emit(StateEvent::error(e.to_string())); - }) - .ok(); + })?; } }; - })); - } - /// Ensure the relay list is fetched for the given public key - pub fn ensure_relay_list(&mut self, public_key: &PublicKey, cx: &mut Context) { - let task = self.get_event(public_key, Kind::RelayList, cx); - - // Emit a fetching event before starting the task - cx.emit(StateEvent::FetchingRelayList); - - self.tasks.push(cx.spawn(async move |this, cx| { - match task.await { - Ok(event) => { - this.update(cx, |this, cx| { - this.ensure_connection(&event, cx); - }) - .ok(); - } - Err(e) => { - this.update(cx, |_this, cx| { - cx.emit(StateEvent::RelayNotConfigured); - cx.emit(StateEvent::error(e.to_string())); - }) - .ok(); - } - }; - })); - } - - /// Ensure that the user is connected to the relay specified in the NIP-65 event. - pub fn ensure_connection(&mut self, event: &Event, cx: &mut Context) { - let client = self.client(); - // Extract the relay list from the event - let relays: Vec<(RelayUrl, Option)> = nip65::extract_relay_list(event) - .map(|(url, metadata)| (url.to_owned(), metadata.to_owned())) - .collect(); - - let task: Task> = cx.background_spawn(async move { - for (url, metadata) in relays.into_iter() { - match metadata { - Some(RelayMetadata::Read) => { - client - .add_relay(url) - .capabilities(RelayCapabilities::READ) - .connect_timeout(Duration::from_secs(TIMEOUT)) - .and_connect() - .await?; - } - Some(RelayMetadata::Write) => { - client - .add_relay(url) - .capabilities(RelayCapabilities::WRITE) - .connect_timeout(Duration::from_secs(TIMEOUT)) - .and_connect() - .await?; - } - None => { - client - .add_relay(url) - .capabilities(RelayCapabilities::NONE) - .connect_timeout(Duration::from_secs(TIMEOUT)) - .and_connect() - .await?; - } - } - } Ok(()) - }); - - self.tasks.push(cx.spawn(async move |this, cx| { - match task.await { - Ok(_) => { - this.update(cx, |_this, cx| { - cx.emit(StateEvent::RelayConnected); - }) - .ok(); - } - Err(e) => { - this.update(cx, |_this, cx| { - cx.emit(StateEvent::RelayNotConfigured); - cx.emit(StateEvent::error(e.to_string())); - }) - .ok(); - } - }; })); } - /// Get an event with the given author and kind. - pub fn get_event( - &self, - author: &PublicKey, - kind: Kind, - cx: &App, - ) -> Task> { - let client = self.client(); - let public_key = *author; - - cx.background_spawn(async move { - let filter = Filter::new().kind(kind).author(public_key).limit(1); - let mut stream = client - .stream_events(filter) - .timeout(Duration::from_millis(800)) - .await?; - - while let Some((_url, res)) = stream.next().await { - if let Ok(event) = res { - return Ok(event); - } - } - - Err(anyhow!("No event found")) - }) - } - /// Get the public key of a NIP-05 address pub fn get_address(&self, addr: Nip05Address, cx: &App) -> Task> { let client = self.client(); @@ -857,29 +728,33 @@ impl NostrRegistry { } } -/// Get or create a new app keys -fn get_or_init_app_keys() -> Result { - let dir = config_dir().join(".app_keys"); - - let content = match std::fs::read(&dir) { - Ok(content) => content, - Err(_) => { - // Generate new keys if file doesn't exist - let keys = Keys::generate(); - let secret_key = keys.secret_key(); - - // Create directory and write secret key - std::fs::create_dir_all(dir.parent().unwrap())?; - std::fs::write(&dir, secret_key.to_secret_bytes())?; - - return Ok(keys); +/// Get or create new app keys +fn get_or_init_app_keys(cx: &App) -> Result { + let read = cx.read_credentials(CLIENT_NAME); + let stored_keys: Option = cx.foreground_executor().block_on(async move { + if let Ok(Some((_, secret))) = read.await { + SecretKey::from_slice(&secret).map(Keys::new).ok() + } else { + None } - }; + }); - let secret_key = SecretKey::from_slice(&content)?; - let keys = Keys::new(secret_key); + if let Some(keys) = stored_keys { + Ok(keys) + } else { + let keys = Keys::generate(); + let user = keys.public_key().to_hex(); + let secret = keys.secret_key().to_secret_bytes(); + let write = cx.write_credentials(CLIENT_NAME, &user, &secret); - Ok(keys) + cx.foreground_executor().block_on(async move { + if let Err(e) = write.await { + log::error!("Keyring not available or panic: {e}") + } + }); + + Ok(keys) + } } fn default_relay_list() -> Vec<(RelayUrl, Option)> { @@ -911,7 +786,7 @@ fn default_messaging_relays() -> Vec { vec![ RelayUrl::parse("wss://nos.lol").unwrap(), RelayUrl::parse("wss://nip17.com").unwrap(), - RelayUrl::parse("wss://relay.0xchat.com").unwrap(), + RelayUrl::parse("wss://auth.nostr1.com").unwrap(), ] } diff --git a/crates/state/src/signer.rs b/crates/state/src/signer.rs index 262e611..c6e9b20 100644 --- a/crates/state/src/signer.rs +++ b/crates/state/src/signer.rs @@ -42,7 +42,7 @@ impl CoopSigner { /// Get public key /// /// Ensure to call this method after the signer has been initialized. - /// Otherwise, this method will panic. + /// Otherwise, it will panic. pub fn public_key(&self) -> Option { *self.signer_pkey.read_blocking() } diff --git a/crates/ui/src/notification.rs b/crates/ui/src/notification.rs index b280e1a..30590b6 100644 --- a/crates/ui/src/notification.rs +++ b/crates/ui/src/notification.rs @@ -295,10 +295,13 @@ impl Render for Notification { .clone() .map(|builder| builder(self, window, cx)); - let action = self - .action_builder - .clone() - .map(|builder| builder(self, window, cx).small().mr_3p5()); + let action = self.action_builder.clone().map(|builder| { + builder(self, window, cx) + .xsmall() + .primary() + .px_3() + .font_semibold() + }); let icon = match self.kind { None => self.icon.clone(), @@ -360,14 +363,8 @@ impl Render for Notification { }) .when_some(content, |this, content| this.child(content)) .when_some(action, |this, action| { - this.child( - h_flex() - .w_full() - .flex_1() - .gap_1() - .justify_end() - .child(action), - ) + this.gap_2() + .child(h_flex().w_full().flex_1().justify_end().child(action)) }), ) .child(