diff --git a/Cargo.lock b/Cargo.lock index 34f42c1..0d26faa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -102,7 +102,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -346,7 +346,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -396,9 +396,9 @@ dependencies = [ [[package]] name = "async-tar" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a42f905d4f623faf634bbd1e001e84e0efc24694afa64be9ad239bf6ca49e1f8" +checksum = "d1937db2d56578aa3919b9bdb0e5100693fd7d1c0f145c53eb81fbb03e217550" dependencies = [ "async-std", "filetime", @@ -422,7 +422,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -510,14 +510,14 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "av1-grain" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f3efb2ca85bc610acfa917b5aaa36f3fcbebed5b3182d7f877b02531c4b80c8" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" dependencies = [ "anyhow", "arrayvec", "log", - "nom", + "nom 8.0.0", "num-rational", "v_frame", ] @@ -584,7 +584,7 @@ version = "0.71.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "cexpr", "clang-sys", "itertools 0.13.0", @@ -595,7 +595,7 @@ dependencies = [ "regex", "rustc-hash 2.1.1", "shlex", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -604,7 +604,7 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "cexpr", "clang-sys", "itertools 0.13.0", @@ -615,7 +615,7 @@ dependencies = [ "regex", "rustc-hash 2.1.1", "shlex", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -691,11 +691,11 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.4" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -712,7 +712,7 @@ checksum = "e4deb8f595ce7f00dee3543ebf6fd9a20ea86fc421ab79600dac30876250bdae" dependencies = [ "ash", "ash-window", - "bitflags 2.9.4", + "bitflags 2.10.0", "bytemuck", "codespan-reporting", "glow", @@ -747,7 +747,7 @@ checksum = "27142319e2f4c264581067eaccb9f80acccdde60d8b4bf57cc50cd3152f109ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -847,7 +847,7 @@ checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -874,7 +874,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "log", "polling", "rustix 0.38.44", @@ -952,16 +952,16 @@ dependencies = [ "quote", "serde", "serde_json", - "syn 2.0.107", + "syn 2.0.108", "tempfile", "toml 0.8.23", ] [[package]] name = "cc" -version = "1.2.41" +version = "1.2.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7" +checksum = "739eb0f94557554b3ca9a86d2d37bebd49c5e6d0c1d2bda35ba5bdac830befc2" dependencies = [ "find-msvc-tools", "jobserver", @@ -981,7 +981,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" dependencies = [ - "nom", + "nom 7.1.3", ] [[package]] @@ -1105,7 +1105,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f79398230a6e2c08f5c9760610eb6924b52aa9e7950a619602baba59dcbbdbb2" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block", "cocoa-foundation 0.2.0", "core-foundation 0.10.0", @@ -1135,7 +1135,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e14045fb83be07b5acf1c0884b2180461635b433455fa35d1cd6f17f1450679d" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block", "core-foundation 0.10.0", "core-graphics-types 0.2.0", @@ -1157,7 +1157,7 @@ dependencies = [ [[package]] name = "collections" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#197d24437809a7d5bbd214f79e11b850367e35e2" +source = "git+https://github.com/zed-industries/zed#b7cc597d28409b67c7985f8b983bc28241255f09" dependencies = [ "indexmap", "rustc-hash 2.1.1", @@ -1279,6 +1279,7 @@ dependencies = [ "i18n", "indexset", "itertools 0.13.0", + "key_store", "log", "nostr", "nostr-connect", @@ -1345,7 +1346,7 @@ version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "core-foundation 0.10.0", "core-graphics-types 0.2.0", "foreign-types 0.5.0", @@ -1358,7 +1359,7 @@ version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32eb7c354ae9f6d437a6039099ce7ecd049337a8109b23d73e48e8ffba8e9cd5" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "core-foundation 0.9.4", "core-graphics-types 0.1.3", "foreign-types 0.5.0", @@ -1382,7 +1383,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "core-foundation 0.10.0", "libc", ] @@ -1393,7 +1394,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e4583956b9806b69f73fcb23aee05eb3620efc282972f08f6a6db7504f8334d" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block", "cfg-if", "core-foundation 0.10.0", @@ -1441,7 +1442,7 @@ version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da46a9d5a8905cc538a4a5bceb6a4510de7a51049c5588c0114efce102bcbbe8" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "fontdb 0.16.2", "log", "rangemap", @@ -1534,7 +1535,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" dependencies = [ "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -1573,9 +1574,9 @@ checksum = "26bf8fc351c5ed29b5c2f0cbbac1b209b74f60ecd62e675a998df72c49af5204" [[package]] name = "deranged" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", ] @@ -1590,17 +1591,17 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] name = "derive_refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#197d24437809a7d5bbd214f79e11b850367e35e2" +source = "git+https://github.com/zed-industries/zed#b7cc597d28409b67c7985f8b983bc28241255f09" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -1667,7 +1668,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "objc2", ] @@ -1679,7 +1680,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -1816,7 +1817,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -1836,7 +1837,7 @@ checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -1959,7 +1960,7 @@ checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -2015,15 +2016,15 @@ version = "25.9.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09b6620799e7340ebd9968d2e0708eb82cf1971e9a16821e2091b6d6e475eed5" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "rustc_version", ] [[package]] name = "flate2" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" dependencies = [ "crc32fast", "miniz_oxide", @@ -2106,7 +2107,7 @@ checksum = "b0299020c3ef3f60f526a4f64ab4a3d4ce116b1acbf24cdd22da0068e5d81dc3" dependencies = [ "fontconfig-parser", "log", - "memmap2 0.9.8", + "memmap2 0.9.9", "slotmap", "tinyvec", "ttf-parser 0.20.0", @@ -2120,7 +2121,7 @@ checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905" dependencies = [ "fontconfig-parser", "log", - "memmap2 0.9.8", + "memmap2 0.9.9", "slotmap", "tinyvec", "ttf-parser 0.25.1", @@ -2153,7 +2154,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -2183,7 +2184,7 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db9c27b72f19a99a895f8ca89e2d26e4ef31013376e56fdafef697627306c3e4" dependencies = [ - "nom", + "nom 7.1.3", "thiserror 1.0.69", ] @@ -2307,7 +2308,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -2414,9 +2415,9 @@ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "globset" -version = "0.4.17" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eab69130804d941f8075cfd713bf8848a2c3b3f201a9457a11e6f87e1ab62305" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" dependencies = [ "aho-corasick", "bstr", @@ -2466,7 +2467,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "gpu-alloc-types", ] @@ -2487,13 +2488,13 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", ] [[package]] name = "gpui" -version = "0.2.1" -source = "git+https://github.com/zed-industries/zed#197d24437809a7d5bbd214f79e11b850367e35e2" +version = "0.2.2" +source = "git+https://github.com/zed-industries/zed#b7cc597d28409b67c7985f8b983bc28241255f09" dependencies = [ "anyhow", "as-raw-xcb-connection", @@ -2588,18 +2589,18 @@ dependencies = [ [[package]] name = "gpui_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#197d24437809a7d5bbd214f79e11b850367e35e2" +source = "git+https://github.com/zed-industries/zed#b7cc597d28409b67c7985f8b983bc28241255f09" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] name = "gpui_tokio" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#197d24437809a7d5bbd214f79e11b850367e35e2" +source = "git+https://github.com/zed-industries/zed#b7cc597d28409b67c7985f8b983bc28241255f09" dependencies = [ "anyhow", "gpui", @@ -2692,7 +2693,7 @@ version = "0.20.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d4f449bab7320c56003d37732a917e18798e2f1709d80263face2b4f9436ddb" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "byteorder", "heed-traits", "heed-types", @@ -2784,11 +2785,11 @@ dependencies = [ [[package]] name = "home" -version = "0.5.11" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2828,7 +2829,7 @@ dependencies = [ [[package]] name = "http_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#197d24437809a7d5bbd214f79e11b850367e35e2" +source = "git+https://github.com/zed-industries/zed#b7cc597d28409b67c7985f8b983bc28241255f09" dependencies = [ "anyhow", "async-compression", @@ -2853,7 +2854,7 @@ dependencies = [ [[package]] name = "http_client_tls" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#197d24437809a7d5bbd214f79e11b850367e35e2" +source = "git+https://github.com/zed-industries/zed#b7cc597d28409b67c7985f8b983bc28241255f09" dependencies = [ "rustls", "rustls-platform-verifier", @@ -3197,7 +3198,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -3340,6 +3341,30 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "key_store" +version = "0.2.11" +dependencies = [ + "anyhow", + "common", + "futures", + "gpui", + "i18n", + "itertools 0.13.0", + "log", + "nostr", + "nostr-sdk", + "rust-i18n", + "serde", + "serde_json", + "settings", + "smallvec", + "smol", + "states", + "theme", + "ui", +] + [[package]] name = "khronos-egl" version = "6.0.0" @@ -3438,7 +3463,7 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "libc", "redox_syscall 0.5.18", ] @@ -3647,7 +3672,7 @@ dependencies = [ [[package]] name = "media" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#197d24437809a7d5bbd214f79e11b850367e35e2" +source = "git+https://github.com/zed-industries/zed#b7cc597d28409b67c7985f8b983bc28241255f09" dependencies = [ "anyhow", "bindgen 0.71.1", @@ -3676,9 +3701,9 @@ dependencies = [ [[package]] name = "memmap2" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843a98750cd611cc2965a8213b53b43e715f13c37a9e096c6408e69990961db7" +checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" dependencies = [ "libc", ] @@ -3698,7 +3723,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ecfd3296f8c56b7c1f6fbac3c71cefa9d78ce009850c45000015f206dc7fa21" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block", "core-graphics-types 0.1.3", "foreign-types 0.5.0", @@ -3780,7 +3805,7 @@ checksum = "2b977c445f26e49757f9aca3631c3b8b836942cb278d69a92e7b80d3b24da632" dependencies = [ "arrayvec", "bit-set", - "bitflags 2.9.4", + "bitflags 2.10.0", "cfg_aliases", "codespan-reporting", "half", @@ -3847,7 +3872,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "cfg-if", "cfg_aliases", "libc", @@ -3859,7 +3884,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "cfg-if", "cfg_aliases", "libc", @@ -3876,6 +3901,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + [[package]] name = "noop_proc_macro" version = "0.3.0" @@ -3894,7 +3928,7 @@ dependencies = [ [[package]] name = "nostr" version = "0.43.0" -source = "git+https://github.com/rust-nostr/nostr#6cf4905a881f60bc2deac603d178839779148bc2" +source = "git+https://github.com/rust-nostr/nostr#806c46e33b49b35cf93d4d892999384f554ecd98" dependencies = [ "aes", "base64", @@ -3918,7 +3952,7 @@ dependencies = [ [[package]] name = "nostr-connect" version = "0.43.0" -source = "git+https://github.com/rust-nostr/nostr#6cf4905a881f60bc2deac603d178839779148bc2" +source = "git+https://github.com/rust-nostr/nostr#806c46e33b49b35cf93d4d892999384f554ecd98" dependencies = [ "async-utility", "nostr", @@ -3930,7 +3964,7 @@ dependencies = [ [[package]] name = "nostr-database" version = "0.43.0" -source = "git+https://github.com/rust-nostr/nostr#6cf4905a881f60bc2deac603d178839779148bc2" +source = "git+https://github.com/rust-nostr/nostr#806c46e33b49b35cf93d4d892999384f554ecd98" dependencies = [ "flatbuffers", "lru", @@ -3941,7 +3975,7 @@ dependencies = [ [[package]] name = "nostr-lmdb" version = "0.43.0" -source = "git+https://github.com/rust-nostr/nostr#6cf4905a881f60bc2deac603d178839779148bc2" +source = "git+https://github.com/rust-nostr/nostr#806c46e33b49b35cf93d4d892999384f554ecd98" dependencies = [ "async-utility", "flume", @@ -3955,7 +3989,7 @@ dependencies = [ [[package]] name = "nostr-relay-pool" version = "0.43.0" -source = "git+https://github.com/rust-nostr/nostr#6cf4905a881f60bc2deac603d178839779148bc2" +source = "git+https://github.com/rust-nostr/nostr#806c46e33b49b35cf93d4d892999384f554ecd98" dependencies = [ "async-utility", "async-wsocket", @@ -3972,7 +4006,7 @@ dependencies = [ [[package]] name = "nostr-sdk" version = "0.43.0" -source = "git+https://github.com/rust-nostr/nostr#6cf4905a881f60bc2deac603d178839779148bc2" +source = "git+https://github.com/rust-nostr/nostr#806c46e33b49b35cf93d4d892999384f554ecd98" dependencies = [ "async-utility", "nostr", @@ -4065,7 +4099,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -4155,7 +4189,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "objc2", "objc2-core-foundation", "objc2-foundation", @@ -4168,7 +4202,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "dispatch2", "objc2", ] @@ -4185,7 +4219,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "objc2", "objc2-core-foundation", ] @@ -4196,7 +4230,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block2", "objc2", "objc2-foundation", @@ -4208,7 +4242,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "objc2", "objc2-core-foundation", "objc2-foundation", @@ -4221,7 +4255,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "objc2", "objc2-core-foundation", "objc2-foundation", @@ -4316,7 +4350,7 @@ version = "0.10.74" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24ad14dd45412269e1a30f52ad8f0664f0f4f4a89ee8fe28c3b3527021ebb654" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "cfg-if", "foreign-types 0.3.2", "libc", @@ -4333,7 +4367,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -4480,7 +4514,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perf" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#197d24437809a7d5bbd214f79e11b850367e35e2" +source = "git+https://github.com/zed-industries/zed#b7cc597d28409b67c7985f8b983bc28241255f09" dependencies = [ "collections", "serde", @@ -4517,7 +4551,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -4552,7 +4586,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -4603,7 +4637,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "crc32fast", "fdeflate", "flate2", @@ -4689,7 +4723,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -4720,14 +4754,14 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] @@ -4748,7 +4782,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" dependencies = [ "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -5059,7 +5093,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", ] [[package]] @@ -5090,13 +5124,13 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] name = "refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#197d24437809a7d5bbd214f79e11b850367e35e2" +source = "git+https://github.com/zed-industries/zed#b7cc597d28409b67c7985f8b983bc28241255f09" dependencies = [ "derive_refineable", ] @@ -5136,16 +5170,13 @@ version = "0.2.11" dependencies = [ "anyhow", "common", - "flume", "futures", "fuzzy-matcher", "gpui", "itertools 0.13.0", "log", "nostr", - "nostr-lmdb", "nostr-sdk", - "rustls", "serde", "serde_json", "settings", @@ -5206,7 +5237,7 @@ dependencies = [ [[package]] name = "reqwest_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#197d24437809a7d5bbd214f79e11b850367e35e2" +source = "git+https://github.com/zed-industries/zed#b7cc597d28409b67c7985f8b983bc28241255f09" dependencies = [ "anyhow", "bytes", @@ -5260,11 +5291,12 @@ dependencies = [ [[package]] name = "rope" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#197d24437809a7d5bbd214f79e11b850367e35e2" +source = "git+https://github.com/zed-industries/zed#b7cc597d28409b67c7985f8b983bc28241255f09" dependencies = [ "arrayvec", "log", "rayon", + "regex", "smallvec", "sum_tree", "unicode-segmentation", @@ -5297,7 +5329,7 @@ dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.107", + "syn 2.0.108", "walkdir", ] @@ -5340,7 +5372,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -5403,7 +5435,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys 0.4.15", @@ -5416,7 +5448,7 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys 0.11.0", @@ -5425,9 +5457,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.33" +version = "0.23.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "751e04a496ca00bb97a5e043158d23d66b5aabf2e1d5aa2a0aaebb1aafe6f82c" +checksum = "6a9586e9ee2b4f8fab52a0048ca7334d7024eef48e2cb9407e3497bb7cab7fa7" dependencies = [ "aws-lc-rs", "log", @@ -5521,7 +5553,7 @@ version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfb9cf8877777222e4a3bc7eb247e398b56baba500c38c1c46842431adc8b55c" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "bytemuck", "libm", "smallvec", @@ -5538,7 +5570,7 @@ version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "bytemuck", "core_maths", "log", @@ -5606,7 +5638,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -5688,7 +5720,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -5701,7 +5733,7 @@ version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "core-foundation 0.10.0", "core-foundation-sys", "libc", @@ -5720,14 +5752,14 @@ dependencies = [ [[package]] name = "self_cell" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749" +checksum = "16c2f82143577edb4921b71ede051dac62ca3c16084e918bf7b40c96ae10eb33" [[package]] name = "semantic_version" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#197d24437809a7d5bbd214f79e11b850367e35e2" +source = "git+https://github.com/zed-industries/zed#b7cc597d28409b67c7985f8b983bc28241255f09" dependencies = [ "anyhow", "serde", @@ -5766,7 +5798,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -5777,7 +5809,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -5824,7 +5856,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -6046,7 +6078,7 @@ version = "0.3.0+sdk-1.3.268.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", ] [[package]] @@ -6086,7 +6118,7 @@ checksum = "172175341049678163e979d9107ca3508046d4d2a7c6682bee46ac541b17db69" dependencies = [ "proc-macro-error2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -6147,7 +6179,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -6159,7 +6191,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -6171,7 +6203,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sum_tree" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#197d24437809a7d5bbd214f79e11b850367e35e2" +source = "git+https://github.com/zed-industries/zed#b7cc597d28409b67c7985f8b983bc28241255f09" dependencies = [ "arrayvec", "log", @@ -6180,15 +6212,15 @@ dependencies = [ [[package]] name = "sval" -version = "2.15.0" +version = "2.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d94c4464e595f0284970fd9c7e9013804d035d4a61ab74b113242c874c05814d" +checksum = "502b8906c4736190684646827fbab1e954357dfe541013bbd7994d033d53a1ca" [[package]] name = "sval_buffer" -version = "2.15.0" +version = "2.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0f46e34b20a39e6a2bf02b926983149b3af6609fd1ee8a6e63f6f340f3e2164" +checksum = "c4b854348b15b6c441bdd27ce9053569b016a0723eab2d015b1fd8e6abe4f708" dependencies = [ "sval", "sval_ref", @@ -6196,18 +6228,18 @@ dependencies = [ [[package]] name = "sval_dynamic" -version = "2.15.0" +version = "2.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03d0970e53c92ab5381d3b2db1828da8af945954d4234225f6dd9c3afbcef3f5" +checksum = "a0bd9e8b74410ddad37c6962587c5f9801a2caadba9e11f3f916ee3f31ae4a1f" dependencies = [ "sval", ] [[package]] name = "sval_fmt" -version = "2.15.0" +version = "2.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43e5e6e1613e1e7fc2e1a9fdd709622e54c122ceb067a60d170d75efd491a839" +checksum = "6fe17b8deb33a9441280b4266c2d257e166bafbaea6e66b4b34ca139c91766d9" dependencies = [ "itoa", "ryu", @@ -6216,9 +6248,9 @@ dependencies = [ [[package]] name = "sval_json" -version = "2.15.0" +version = "2.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aec382f7bfa6e367b23c9611f129b94eb7daaf3d8fae45a8d0a0211eb4d4c8e6" +checksum = "854addb048a5bafb1f496c98e0ab5b9b581c3843f03ca07c034ae110d3b7c623" dependencies = [ "itoa", "ryu", @@ -6227,9 +6259,9 @@ dependencies = [ [[package]] name = "sval_nested" -version = "2.15.0" +version = "2.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3049d0f99ce6297f8f7d9953b35a0103b7584d8f638de40e64edb7105fa578ae" +checksum = "96cf068f482108ff44ae8013477cb047a1665d5f1a635ad7cf79582c1845dce9" dependencies = [ "sval", "sval_buffer", @@ -6238,18 +6270,18 @@ dependencies = [ [[package]] name = "sval_ref" -version = "2.15.0" +version = "2.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f88913e77506085c0a8bf6912bb6558591a960faf5317df6c1d9b227224ca6e1" +checksum = "ed02126365ffe5ab8faa0abd9be54fbe68d03d607cd623725b0a71541f8aaa6f" dependencies = [ "sval", ] [[package]] name = "sval_serde" -version = "2.15.0" +version = "2.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f579fd7254f4be6cd7b450034f856b78523404655848789c451bacc6aa8b387d" +checksum = "a263383c6aa2076c4ef6011d3bae1b356edf6ea2613e3d8e8ebaa7b57dd707d5" dependencies = [ "serde_core", "sval", @@ -6296,9 +6328,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.107" +version = "2.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a26dbd934e5451d21ef060c018dae56fc073894c5a7896f882928a76e6d081b" +checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" dependencies = [ "proc-macro2", "quote", @@ -6331,7 +6363,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -6363,7 +6395,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -6506,7 +6538,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -6517,7 +6549,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -6676,7 +6708,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -6853,7 +6885,7 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "bytes", "futures-util", "http", @@ -6896,7 +6928,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -7082,9 +7114,9 @@ checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" [[package]] name = "unicode-ident" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" +checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06" [[package]] name = "unicode-linebreak" @@ -7207,7 +7239,7 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "util" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#197d24437809a7d5bbd214f79e11b850367e35e2" +source = "git+https://github.com/zed-industries/zed#b7cc597d28409b67c7985f8b983bc28241255f09" dependencies = [ "anyhow", "async-fs", @@ -7242,11 +7274,11 @@ dependencies = [ [[package]] name = "util_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#197d24437809a7d5bbd214f79e11b850367e35e2" +source = "git+https://github.com/zed-industries/zed#b7cc597d28409b67c7985f8b983bc28241255f09" dependencies = [ "perf", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -7422,7 +7454,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", "wasm-bindgen-shared", ] @@ -7457,7 +7489,7 @@ checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -7504,7 +7536,7 @@ version = "0.31.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "rustix 1.1.2", "wayland-backend", "wayland-scanner", @@ -7527,7 +7559,7 @@ version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "wayland-backend", "wayland-client", "wayland-scanner", @@ -7539,7 +7571,7 @@ version = "0.32.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "wayland-backend", "wayland-client", "wayland-scanner", @@ -7551,7 +7583,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23803551115ff9ea9bce586860c5c5a971e360825a0309264102a9495a5ff479" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "wayland-backend", "wayland-client", "wayland-protocols 0.31.2", @@ -7815,7 +7847,7 @@ checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -7826,7 +7858,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -7837,7 +7869,7 @@ checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -7848,7 +7880,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -8391,7 +8423,7 @@ name = "xim-parser" version = "0.2.1" source = "git+https://github.com/zed-industries/xim-rs.git?rev=16f35a2c881b815a2b6cdfd6687988e84f8447d8#16f35a2c881b815a2b6cdfd6687988e84f8447d8" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", ] [[package]] @@ -8402,7 +8434,7 @@ checksum = "8d66ca9352cbd4eecbbc40871d8a11b4ac8107cfc528a6e14d7c19c69d0e1ac9" dependencies = [ "as-raw-xcb-connection", "libc", - "memmap2 0.9.8", + "memmap2 0.9.9", "xkeysym", ] @@ -8455,7 +8487,7 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", "synstructure", ] @@ -8502,7 +8534,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", "zbus_names", "zvariant", "zvariant_utils", @@ -8525,7 +8557,7 @@ name = "zed-font-kit" version = "0.14.1-zed" source = "git+https://github.com/zed-industries/font-kit?rev=110523127440aefb11ce0cf280ae7c5071337ec5#110523127440aefb11ce0cf280ae7c5071337ec5" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "byteorder", "core-foundation 0.10.0", "core-graphics 0.24.0", @@ -8650,7 +8682,7 @@ checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -8670,7 +8702,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", "synstructure", ] @@ -8691,7 +8723,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -8724,7 +8756,7 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -8775,7 +8807,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", "zvariant_utils", ] @@ -8788,6 +8820,6 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.107", + "syn 2.0.108", "winnow", ] diff --git a/crates/common/src/display.rs b/crates/common/src/display.rs index 534ba7c..e008540 100644 --- a/crates/common/src/display.rs +++ b/crates/common/src/display.rs @@ -64,7 +64,7 @@ pub trait RenderedTimestamp { impl RenderedTimestamp for Timestamp { fn to_human_time(&self) -> SharedString { - let input_time = match Local.timestamp_opt(self.as_u64() as i64, 0) { + let input_time = match Local.timestamp_opt(self.as_secs() as i64, 0) { chrono::LocalResult::Single(time) => time, _ => return SharedString::from("9999"), }; @@ -85,7 +85,7 @@ impl RenderedTimestamp for Timestamp { } fn to_ago(&self) -> SharedString { - let input_time = match Local.timestamp_opt(self.as_u64() as i64, 0) { + let input_time = match Local.timestamp_opt(self.as_secs() as i64, 0) { chrono::LocalResult::Single(time) => time, _ => return SharedString::from("1m"), }; diff --git a/crates/coop/Cargo.toml b/crates/coop/Cargo.toml index d267aff..612bece 100644 --- a/crates/coop/Cargo.toml +++ b/crates/coop/Cargo.toml @@ -33,6 +33,7 @@ title_bar = { path = "../title_bar" } theme = { path = "../theme" } common = { path = "../common" } states = { path = "../states" } +key_store = { path = "../key_store" } registry = { path = "../registry" } settings = { path = "../settings" } auto_update = { path = "../auto_update" } diff --git a/crates/coop/src/actions.rs b/crates/coop/src/actions.rs index ea6d43d..cb7ec1b 100644 --- a/crates/coop/src/actions.rs +++ b/crates/coop/src/actions.rs @@ -1,8 +1,9 @@ use std::sync::Mutex; use gpui::{actions, App, AppContext}; +use key_store::backend::KeyItem; +use key_store::KeyStore; use nostr_connect::prelude::*; -use registry::keystore::KeyItem; use registry::Registry; use states::app_state; @@ -48,7 +49,7 @@ pub fn load_embedded_fonts(cx: &App) { pub fn reset(cx: &mut App) { let registry = Registry::global(cx); - let keystore = registry.read(cx).keystore(); + let backend = KeyStore::global(cx).read(cx).backend(); cx.spawn(async move |cx| { cx.background_spawn(async move { @@ -57,12 +58,12 @@ pub fn reset(cx: &mut App) { }) .await; - keystore + backend .delete_credentials(&KeyItem::User.to_string(), cx) .await .ok(); - keystore + backend .delete_credentials(&KeyItem::Bunker.to_string(), cx) .await .ok(); diff --git a/crates/coop/src/chatspace.rs b/crates/coop/src/chatspace.rs index 064d810..8126b5b 100644 --- a/crates/coop/src/chatspace.rs +++ b/crates/coop/src/chatspace.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use anyhow::{anyhow, Error}; use auto_update::AutoUpdater; -use common::display::RenderedProfile; +use common::display::{shorten_pubkey, RenderedProfile}; use common::event::EventUtils; use gpui::prelude::FluentBuilder; use gpui::{ @@ -14,14 +14,15 @@ use gpui::{ }; use i18n::{shared_t, t}; use itertools::Itertools; +use key_store::backend::KeyItem; +use key_store::KeyStore; use nostr_connect::prelude::*; use nostr_sdk::prelude::*; -use registry::keystore::KeyItem; use registry::{Registry, RegistryEvent}; use settings::AppSettings; use smallvec::{smallvec, SmallVec}; use states::constants::{BOOTSTRAP_RELAYS, DEFAULT_SIDEBAR_WIDTH}; -use states::state::{AuthRequest, SignalKind, UnwrappingStatus}; +use states::state::{Announcement, AuthRequest, Response, SignalKind, UnwrappingStatus}; use states::{app_state, default_nip17_relays, default_nip65_relays}; use theme::{ActiveTheme, Theme, ThemeMode}; use title_bar::TitleBar; @@ -74,7 +75,7 @@ pub struct ChatSpace { nip65_ready: bool, /// All subscriptions for observing the app state - _subscriptions: SmallVec<[Subscription; 4]>, + _subscriptions: SmallVec<[Subscription; 3]>, /// All long running tasks _tasks: SmallVec<[Task<()>; 5]>, @@ -83,7 +84,7 @@ pub struct ChatSpace { impl ChatSpace { pub fn new(window: &mut Window, cx: &mut Context) -> Self { let registry = Registry::global(cx); - let status = registry.read(cx).unwrapping_status.clone(); + let keystore = KeyStore::global(cx); let title_bar = cx.new(|_| TitleBar::new()); let dock = cx.new(|cx| DockArea::new(window, cx)); @@ -100,57 +101,38 @@ impl ChatSpace { ); subscriptions.push( - // Observe the keystore - cx.observe_in(®istry, window, |this, registry, window, cx| { - let has_keyring = registry.read(cx).initialized_keystore; - let use_filestore = registry.read(cx).is_using_file_keystore(); - let not_logged_in = registry.read(cx).signer_pubkey().is_none(); + // Observe device changes + cx.observe_in(&keystore, window, move |this, state, window, cx| { + if state.read(cx).initialized { + let backend = state.read(cx).backend(); - if use_filestore && not_logged_in { - this.render_keyring_installation(window, cx); - } + if state.read(cx).initialized { + if state.read(cx).is_using_file_keystore() { + this.render_keyring_installation(window, cx); + } - if has_keyring && not_logged_in { - let keystore = registry.read(cx).keystore(); + cx.spawn_in(window, async move |this, cx| { + let result = backend + .read_credentials(&KeyItem::User.to_string(), cx) + .await; - cx.spawn_in(window, async move |this, cx| { - let result = keystore - .read_credentials(&KeyItem::User.to_string(), cx) - .await; + this.update_in(cx, |this, window, cx| { + match result { + Ok(Some((user, secret))) => { + let public_key = PublicKey::parse(&user).unwrap(); + let secret = String::from_utf8(secret).unwrap(); - this.update_in(cx, |this, window, cx| { - match result { - Ok(Some((user, secret))) => { - let public_key = PublicKey::parse(&user).unwrap(); - let secret = String::from_utf8(secret).unwrap(); - this.set_account_layout(public_key, secret, window, cx); - } - _ => { - this.set_onboarding_layout(window, cx); - } - }; + this.set_account_layout(public_key, secret, window, cx); + } + _ => { + this.set_onboarding_layout(window, cx); + } + }; + }) + .ok(); }) - .ok(); - }) - .detach(); - } - }), - ); - - subscriptions.push( - // Observe the global registry's events - cx.observe_in(&status, window, move |this, status, window, cx| { - let status = status.read(cx); - let all_panels = this.get_all_panel_ids(cx); - - if matches!( - status, - UnwrappingStatus::Processing | UnwrappingStatus::Complete - ) { - Registry::global(cx).update(cx, |this, cx| { - this.load_rooms(window, cx); - this.refresh_rooms(all_panels, cx); - }); + .detach(); + } } }), ); @@ -238,9 +220,21 @@ impl ChatSpace { let settings = AppSettings::global(cx); match signal { + SignalKind::EncryptionNotSet => { + this.init_encryption(window, cx); + } + SignalKind::EncryptionSet(announcement) => { + this.load_encryption(announcement, window, cx); + } + SignalKind::EncryptionRequest(announcement) => { + this.render_request(announcement, window, cx); + } + SignalKind::EncryptionResponse(response) => { + this.receive_encryption(response, window, cx); + } SignalKind::SignerSet(public_key) => { - // Close the latest modal if it exists - window.close_modal(cx); + // Close all opened modals + window.close_all_modals(cx); // Load user's settings settings.update(cx, |this, cx| { @@ -256,15 +250,6 @@ impl ChatSpace { // Setup the default layout for current workspace this.set_default_layout(window, cx); } - SignalKind::SignerUnset => { - // Clear all current chat rooms - registry.update(cx, |this, cx| { - this.reset(cx); - }); - - // Setup the onboarding layout for current workspace - this.set_onboarding_layout(window, cx); - } SignalKind::Auth(req) => { let url = &req.url; let auto_auth = AppSettings::get_auto_auth(cx); @@ -281,10 +266,19 @@ impl ChatSpace { this.open_auth_request(req, window, cx); } } - SignalKind::GiftWrapStatus(status) => { - registry.update(cx, |this, cx| { - this.set_unwrapping_status(status, cx); - }); + SignalKind::GiftWrapStatus(s) => { + if matches!(s, UnwrappingStatus::Processing | UnwrappingStatus::Complete) { + let all_panels = this.get_all_panel_ids(cx); + + registry.update(cx, |this, cx| { + this.load_rooms(window, cx); + this.refresh_rooms(all_panels, cx); + + if s == UnwrappingStatus::Complete { + this.set_loading(false, cx); + } + }); + } } SignalKind::NewProfile(profile) => { registry.update(cx, |this, cx| { @@ -309,6 +303,92 @@ impl ChatSpace { } } + fn init_encryption(&mut self, window: &mut Window, cx: &mut Context) { + cx.spawn_in(window, async move |this, cx| { + let result = app_state().init_encryption_keys().await; + + this.update_in(cx, |_, window, cx| { + match result { + Ok(_) => { + window.push_notification(t!("encryption.notice"), cx); + } + Err(e) => { + // TODO: ask user to confirm re-running if failed + window.push_notification(e.to_string(), cx); + } + }; + }) + .ok(); + }) + .detach(); + } + + fn load_encryption(&self, ann: Announcement, window: &Window, cx: &Context) { + log::info!("Loading encryption keys: {ann:?}"); + + cx.spawn_in(window, async move |this, cx| { + let state = app_state(); + let result = state.load_encryption_keys(&ann).await; + + this.update_in(cx, |this, window, cx| { + match result { + Ok(_) => { + window.push_notification(t!("encryption.reinit"), cx); + } + Err(_) => { + this.request_encryption(ann, window, cx); + } + }; + }) + .ok(); + }) + .detach(); + } + + fn request_encryption(&self, ann: Announcement, window: &Window, cx: &Context) { + cx.spawn_in(window, async move |this, cx| { + let result = app_state().request_encryption_keys().await; + + this.update_in(cx, |this, window, cx| { + match result { + Ok(wait_for_approval) => { + if wait_for_approval { + this.render_pending(ann, window, cx); + } else { + window.push_notification(t!("encryption.success"), cx); + } + } + Err(e) => { + // TODO: ask user to confirm re-running if failed + window.push_notification(e.to_string(), cx); + } + }; + }) + .ok(); + }) + .detach(); + } + + fn receive_encryption(&self, res: Response, window: &Window, cx: &Context) { + cx.spawn_in(window, async move |this, cx| { + let result = app_state().receive_encryption_keys(res).await; + + this.update_in(cx, |_, window, cx| { + match result { + Ok(_) => { + window.push_notification(t!("encryption.success"), cx); + } + Err(e) => { + // TODO: ask user to confirm re-running if failed + window.push_notification(e.to_string(), cx); + } + }; + }) + .ok(); + }) + .detach(); + } + fn auth(&mut self, req: AuthRequest, window: &mut Window, cx: &mut Context) { let settings = AppSettings::global(cx); @@ -730,6 +810,132 @@ impl ChatSpace { }); } + fn render_request(&mut self, ann: Announcement, window: &mut Window, cx: &mut Context) { + let client_name = SharedString::from(ann.client().to_string()); + let target = ann.public_key(); + + let note = Notification::new() + .custom_id(SharedString::from(ann.id().to_hex())) + .autohide(false) + .icon(IconName::Info) + .title(shared_t!("request_encryption.label")) + .content(move |_window, cx| { + v_flex() + .gap_2() + .text_sm() + .child(shared_t!("request_encryption.body")) + .child( + v_flex() + .py_1() + .px_1p5() + .rounded_sm() + .text_xs() + .bg(cx.theme().warning_background) + .text_color(cx.theme().warning_foreground) + .child(client_name.clone()), + ) + .into_any_element() + }) + .action(move |_window, _cx| { + Button::new("approve") + .label(t!("common.approve")) + .small() + .primary() + .loading(false) + .disabled(false) + .on_click(move |_ev, _window, cx| { + cx.background_spawn(async move { + let state = app_state(); + state.response_encryption_keys(target).await.ok(); + }) + .detach(); + }) + }); + + window.push_notification(note, cx); + } + + fn render_pending(&mut self, ann: Announcement, window: &mut Window, cx: &mut Context) { + let client_name = SharedString::from(ann.client().to_string()); + let public_key = shorten_pubkey(ann.public_key(), 8); + let view = cx.entity().downgrade(); + + window.open_modal(cx, move |this, _window, cx| { + let view = view.clone(); + + this.overlay_closable(false) + .show_close(false) + .keyboard(false) + .confirm() + .width(px(460.)) + .button_props( + ModalButtonProps::default() + .cancel_text(t!("common.reset")) + .ok_text(t!("common.hide")), + ) + .title(shared_t!("pending_encryption.label")) + .child( + v_flex() + .gap_2() + .text_sm() + .child( + v_flex() + .justify_center() + .items_center() + .text_center() + .h_16() + .w_full() + .rounded(cx.theme().radius) + .bg(cx.theme().elevated_surface_background) + .font_semibold() + .child(client_name.clone()) + .child( + div() + .text_xs() + .text_color(cx.theme().text_muted) + .child(SharedString::from(&public_key)), + ), + ) + .child(shared_t!("pending_encryption.body_1", c = client_name)) + .child(shared_t!("pending_encryption.body_2")) + .child( + div() + .text_xs() + .text_color(cx.theme().warning_foreground) + .child(shared_t!("pending_encryption.body_3")), + ), + ) + .on_cancel(move |_ev, window, cx| { + _ = view.update(cx, |this, cx| { + this.render_reset(window, cx); + }); + // false to keep modal open + false + }) + }); + } + + fn render_reset(&mut self, window: &mut Window, cx: &mut Context) { + cx.spawn_in(window, async move |this, cx| { + let state = app_state(); + let result = state.init_encryption_keys().await; + + this.update_in(cx, |_, window, cx| { + match result { + Ok(_) => { + window.push_notification(t!("encryption.success"), cx); + window.close_all_modals(cx); + } + Err(e) => { + window.push_notification(e.to_string(), cx); + } + }; + }) + .ok(); + }) + .detach(); + } + fn render_setup_gossip_relays_modal(&mut self, window: &mut Window, cx: &mut App) { let relays = default_nip65_relays(); @@ -937,15 +1143,15 @@ impl ChatSpace { _window: &mut Window, cx: &Context, ) -> impl IntoElement { - let registry = Registry::read_global(cx); - let status = registry.unwrapping_status.read(cx); + let registry = Registry::global(cx); + let status = registry.read(cx).loading; h_flex() .gap_2() .h_6() .w_full() .child(compose_button()) - .when(status != &UnwrappingStatus::Complete, |this| { + .when(status, |this| { this.child(deferred( h_flex() .px_2() diff --git a/crates/coop/src/main.rs b/crates/coop/src/main.rs index d5df347..e3cb516 100644 --- a/crates/coop/src/main.rs +++ b/crates/coop/src/main.rs @@ -7,7 +7,7 @@ use gpui::{ WindowOptions, }; use states::app_state; -use states::constants::{APP_ID, APP_NAME}; +use states::constants::{APP_ID, CLIENT_NAME}; use ui::Root; use crate::actions::{load_embedded_fonts, quit, Quit}; @@ -63,7 +63,7 @@ fn main() { kind: WindowKind::Normal, app_id: Some(APP_ID.to_owned()), titlebar: Some(TitlebarOptions { - title: Some(SharedString::new_static(APP_NAME)), + title: Some(SharedString::new_static(CLIENT_NAME)), traffic_light_position: Some(point(px(9.0), px(9.0))), appears_transparent: true, }), @@ -86,6 +86,9 @@ fn main() { // Initialize app registry registry::init(cx); + // Initialize backend for credentials storage + key_store::init(cx); + // Initialize settings settings::init(cx); diff --git a/crates/coop/src/views/account.rs b/crates/coop/src/views/account.rs index acaa7ca..5ca73d6 100644 --- a/crates/coop/src/views/account.rs +++ b/crates/coop/src/views/account.rs @@ -9,8 +9,9 @@ use gpui::{ Window, }; use i18n::{shared_t, t}; +use key_store::backend::KeyItem; +use key_store::KeyStore; use nostr_connect::prelude::*; -use registry::keystore::KeyItem; use registry::Registry; use smallvec::{smallvec, SmallVec}; use states::app_state; @@ -116,7 +117,7 @@ impl Account { window: &mut Window, cx: &mut Context, ) { - let keystore = Registry::global(cx).read(cx).keystore(); + let keystore = KeyStore::global(cx).read(cx).backend(); // Handle connection in the background cx.spawn_in(window, async move |this, cx| { diff --git a/crates/coop/src/views/chat/mod.rs b/crates/coop/src/views/chat/mod.rs index d538cbb..3caf248 100644 --- a/crates/coop/src/views/chat/mod.rs +++ b/crates/coop/src/views/chat/mod.rs @@ -1,4 +1,4 @@ -use std::collections::{HashMap, HashSet}; +use std::collections::HashSet; use std::time::Duration; use common::display::{RenderedProfile, RenderedTimestamp}; @@ -17,7 +17,7 @@ use indexset::{BTreeMap, BTreeSet}; use itertools::Itertools; use nostr_sdk::prelude::*; use registry::message::{Message, RenderedMessage}; -use registry::room::{Room, RoomKind, RoomSignal, SendReport}; +use registry::room::{Room, RoomKind, RoomSignal, SendOptions, SendReport, SignerKind}; use registry::Registry; use serde::Deserialize; use settings::AppSettings; @@ -47,6 +47,10 @@ mod subject; #[action(namespace = chat, no_json)] pub struct SeenOn(pub EventId); +#[derive(Action, Clone, PartialEq, Eq, Deserialize)] +#[action(namespace = chat, no_json)] +pub struct SetSigner(pub SignerKind); + pub fn init(room: Entity, window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Chat::new(room, window, cx)) } @@ -54,7 +58,6 @@ pub fn init(room: Entity, window: &mut Window, cx: &mut App) -> Entity, - relays: Entity>>, // Messages list_state: ListState, @@ -64,6 +67,7 @@ pub struct Chat { // New Message input: Entity, + options: Entity, replies_to: Entity>, // Media Attachment @@ -75,20 +79,12 @@ pub struct Chat { focus_handle: FocusHandle, image_cache: Entity, - _subscriptions: SmallVec<[Subscription; 4]>, + _subscriptions: SmallVec<[Subscription; 3]>, _tasks: SmallVec<[Task<()>; 2]>, } impl Chat { pub fn new(room: Entity, window: &mut Window, cx: &mut Context) -> Self { - let attachments = cx.new(|_| vec![]); - let replies_to = cx.new(|_| HashSet::new()); - - let relays = cx.new(|_| { - let this: HashMap> = HashMap::new(); - this - }); - let input = cx.new(|cx| { InputState::new(window, cx) .placeholder(t!("chat.placeholder")) @@ -97,11 +93,16 @@ impl Chat { .clean_on_escape() }); + let attachments = cx.new(|_| vec![]); + let replies_to = cx.new(|_| HashSet::new()); + let options = cx.new(|_| SendOptions::default()); + + let id = room.read(cx).id.to_string().into(); let messages = BTreeSet::from([Message::system()]); let list_state = ListState::new(messages.len(), ListAlignment::Bottom, px(1024.)); let connect = room.read(cx).connect(cx); - let load_messages = room.read(cx).load_messages(cx); + let get_messages = room.read(cx).get_messages(cx); let mut subscriptions = smallvec![]; let mut tasks = smallvec![]; @@ -109,7 +110,7 @@ impl Chat { tasks.push( // Load all messages belonging to this room cx.spawn_in(window, async move |this, cx| { - let result = load_messages.await; + let result = get_messages.await; this.update_in(cx, |this, window, cx| { match result { @@ -126,24 +127,11 @@ impl Chat { ); tasks.push( - // Get messaging relays for all members - cx.spawn_in(window, async move |this, cx| { - let result = connect.await; - - this.update_in(cx, |this, _window, cx| { - match result { - Ok(relays) => { - this.relays.update(cx, |this, cx| { - this.extend(relays); - cx.notify(); - }); - } - Err(e) => { - this.insert_warning(e.to_string(), cx); - } - }; - }) - .ok(); + // Get messaging relays and encryption keys announcement for all members + cx.background_spawn(async move { + if let Err(e) = connect.await { + log::error!("Failed to initialize room: {e}"); + } }), ); @@ -189,23 +177,6 @@ impl Chat { }), ); - subscriptions.push( - // Observe the messaging relays of the room's members - cx.observe_in(&relays, window, |this, entity, _window, cx| { - let registry = Registry::global(cx); - let relays = entity.read(cx).clone(); - - for (public_key, urls) in relays.iter() { - if urls.is_empty() { - let profile = registry.read(cx).get_person(public_key, cx); - let content = t!("chat.nip17_not_found", u = profile.name()); - - this.insert_warning(content, cx); - } - } - }), - ); - subscriptions.push( // Observe when user close chat panel cx.on_release_in(window, move |this, window, cx| { @@ -219,19 +190,19 @@ impl Chat { ); Self { - id: room.read(cx).id.to_string().into(), - image_cache: RetainAllImageCache::new(cx), - focus_handle: cx.focus_handle(), - rendered_texts_by_id: BTreeMap::new(), - reports_by_id: BTreeMap::new(), - relays, + id, messages, room, list_state, input, replies_to, attachments, + options, + rendered_texts_by_id: BTreeMap::new(), + reports_by_id: BTreeMap::new(), uploading: false, + image_cache: RetainAllImageCache::new(cx), + focus_handle: cx.focus_handle(), _subscriptions: subscriptions, _tasks: tasks, } @@ -239,12 +210,12 @@ impl Chat { /// Load all messages belonging to this room fn load_messages(&mut self, window: &mut Window, cx: &mut Context) { - let load_messages = self.room.read(cx).load_messages(cx); + let get_messages = self.room.read(cx).get_messages(cx); self._tasks.push( // Run the task in the background cx.spawn_in(window, async move |this, cx| { - let result = load_messages.await; + let result = get_messages.await; this.update_in(cx, |this, window, cx| { match result { @@ -303,9 +274,6 @@ impl Chat { this.set_value("", window, cx); }); - // Get the backup setting - let backup = AppSettings::get_backup_messages(cx); - // Get replies_to if it's present let replies: Vec = self.replies_to.read(cx).iter().copied().collect(); @@ -317,14 +285,17 @@ impl Chat { let rumor_id = rumor.id.unwrap(); // Create a task for sending the message in the background - let send_message = room.send_message(rumor.clone(), backup, cx); + let opts = self.options.read(cx); + let send_message = room.send_message(&rumor, opts, cx); // Optimistically update message list cx.spawn_in(window, async move |this, cx| { - cx.background_executor() - .timer(Duration::from_millis(100)) - .await; + let delay = Duration::from_millis(100); + // Wait for the delay + cx.background_executor().timer(delay).await; + + // Update the message list and reset the states this.update_in(cx, |this, window, cx| { this.insert_message(Message::user(rumor), true, cx); this.remove_all_replies(cx); @@ -339,37 +310,39 @@ impl Chat { }) .detach(); - // Continue sending the message in the background - cx.spawn_in(window, async move |this, cx| { - let result = send_message.await; + self._tasks.push( + // Continue sending the message in the background + cx.spawn_in(window, async move |this, cx| { + let result = send_message.await; - this.update_in(cx, |this, window, cx| { - match result { - Ok(reports) => { - this.room.update(cx, |this, cx| { - if this.kind != RoomKind::Ongoing { - // Update the room kind to ongoing - // But keep the room kind if send failed - if reports.iter().all(|r| !r.is_sent_success()) { - this.kind = RoomKind::Ongoing; - cx.notify(); + this.update_in(cx, |this, window, cx| { + match result { + Ok(reports) => { + // Update room's status + this.room.update(cx, |this, cx| { + if this.kind != RoomKind::Ongoing { + // Update the room kind to ongoing, + // but keep the room kind if send failed + if reports.iter().all(|r| !r.is_sent_success()) { + this.kind = RoomKind::Ongoing; + cx.notify(); + } } - } - }); + }); - // Insert the sent reports - this.reports_by_id.insert(rumor_id, reports); + // Insert the sent reports + this.reports_by_id.insert(rumor_id, reports); - cx.notify(); + cx.notify(); + } + Err(e) => { + window.push_notification(e.to_string(), cx); + } } - Err(e) => { - window.push_notification(e.to_string(), cx); - } - } - }) - .ok(); - }) - .detach(); + }) + .ok(); + }), + ); } /// Resend a failed message @@ -432,6 +405,7 @@ impl Chat { } /// Insert a warning message into the chat panel + #[allow(dead_code)] fn insert_warning(&mut self, content: impl Into, cx: &mut Context) { let m = Message::warning(content.into()); self.insert_message(m, true, cx); @@ -473,6 +447,10 @@ impl Chat { registry.get_person(public_key, cx) } + fn signer_kind(&self, cx: &App) -> SignerKind { + self.options.read(cx).signer_kind + } + fn scroll_to(&self, id: EventId) { if let Some(ix) = self.messages.iter().position(|m| { if let Message::User(msg) = m { @@ -543,29 +521,24 @@ impl Chat { }) .ok(); - match Flatten::flatten(task.await.map_err(|e| e.into())) { - Ok(Some(url)) => { - this.update(cx, |this, cx| { + let result = Flatten::flatten(task.await.map_err(|e| e.into())); + + this.update_in(cx, |this, window, cx| { + match result { + Ok(Some(url)) => { this.add_attachment(url, cx); this.set_uploading(false, cx); - }) - .ok(); - } - Ok(None) => { - this.update_in(cx, |this, window, cx| { - window.push_notification("Failed to upload file", cx); + } + Ok(None) => { this.set_uploading(false, cx); - }) - .ok(); - } - Err(e) => { - this.update_in(cx, |this, window, cx| { + } + Err(e) => { window.push_notification(Notification::error(e.to_string()), cx); this.set_uploading(false, cx); - }) - .ok(); - } - } + } + }; + }) + .ok(); } Some(()) @@ -911,6 +884,27 @@ impl Chat { ), ) }) + .when(report.device_not_found, |this| { + this.child( + h_flex() + .flex_wrap() + .justify_center() + .p_2() + .h_20() + .w_full() + .text_sm() + .rounded(cx.theme().radius) + .bg(cx.theme().danger_background) + .text_color(cx.theme().danger_foreground) + .child( + div() + .flex_1() + .w_full() + .text_center() + .child(shared_t!("chat.device_not_found", u = name)), + ), + ) + }) .when_some(report.error.clone(), |this, error| { this.child( h_flex() @@ -1291,6 +1285,13 @@ impl Chat { }) .detach(); } + + fn on_set_encryption(&mut self, ev: &SetSigner, _: &mut Window, cx: &mut Context) { + self.options.update(cx, move |this, cx| { + this.signer_kind = ev.0; + cx.notify(); + }); + } } impl Panel for Chat { @@ -1334,8 +1335,11 @@ impl Focusable for Chat { impl Render for Chat { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let kind = self.signer_kind(cx); + v_flex() .on_action(cx.listener(Self::on_open_seen_on)) + .on_action(cx.listener(Self::on_set_encryption)) .image_cache(self.image_cache.clone()) .size_full() .child( @@ -1384,9 +1388,7 @@ impl Render for Chat { .items_end() .gap_2p5() .child( - div() - .flex() - .items_center() + h_flex() .gap_1() .text_color(cx.theme().text_muted) .child( @@ -1408,7 +1410,31 @@ impl Render for Chat { .large(), ), ) - .child(TextInput::new(&self.input)), + .child(TextInput::new(&self.input)) + .child( + Button::new("options") + .icon(IconName::Settings) + .ghost() + .large() + .popup_menu(move |this, _window, _cx| { + this.title("Encrypt by:") + .menu_with_check( + "Encryption Key", + matches!(kind, SignerKind::Encryption), + Box::new(SetSigner(SignerKind::Encryption)), + ) + .menu_with_check( + "User's Identity", + matches!(kind, SignerKind::User), + Box::new(SetSigner(SignerKind::User)), + ) + .menu_with_check( + "Auto", + matches!(kind, SignerKind::Auto), + Box::new(SetSigner(SignerKind::Auto)), + ) + }), + ), ), ), ) diff --git a/crates/coop/src/views/login.rs b/crates/coop/src/views/login.rs index 030e453..552c6c9 100644 --- a/crates/coop/src/views/login.rs +++ b/crates/coop/src/views/login.rs @@ -7,9 +7,9 @@ use gpui::{ Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Window, }; use i18n::{shared_t, t}; +use key_store::backend::KeyItem; +use key_store::KeyStore; use nostr_connect::prelude::*; -use registry::keystore::KeyItem; -use registry::Registry; use smallvec::{smallvec, SmallVec}; use states::app_state; use states::constants::BUNKER_TIMEOUT; @@ -174,7 +174,7 @@ impl Login { window: &mut Window, cx: &mut Context, ) { - let keystore = Registry::global(cx).read(cx).keystore(); + let keystore = KeyStore::global(cx).read(cx).backend(); let username = keys.public_key().to_hex(); let secret = keys.secret_key().to_secret_bytes(); let mut clean_uri = uri.to_string(); @@ -263,7 +263,7 @@ impl Login { } pub fn login_with_keys(&mut self, keys: Keys, cx: &mut Context) { - let keystore = Registry::global(cx).read(cx).keystore(); + let keystore = KeyStore::global(cx).read(cx).backend(); let username = keys.public_key().to_hex(); let secret = keys.secret_key().to_secret_hex().into_bytes(); diff --git a/crates/coop/src/views/new_account.rs b/crates/coop/src/views/new_account.rs index 941877c..072758f 100644 --- a/crates/coop/src/views/new_account.rs +++ b/crates/coop/src/views/new_account.rs @@ -7,9 +7,9 @@ use gpui::{ }; use gpui_tokio::Tokio; use i18n::{shared_t, t}; +use key_store::backend::KeyItem; +use key_store::KeyStore; use nostr_sdk::prelude::*; -use registry::keystore::KeyItem; -use registry::Registry; use settings::AppSettings; use smol::fs; use states::constants::BOOTSTRAP_RELAYS; @@ -106,7 +106,7 @@ impl NewAccount { } pub fn set_signer(&mut self, cx: &mut Context) { - let keystore = Registry::global(cx).read(cx).keystore(); + let keystore = KeyStore::global(cx).read(cx).backend(); let keys = self.temp_keys.read(cx).clone(); let username = keys.public_key().to_hex(); diff --git a/crates/coop/src/views/onboarding.rs b/crates/coop/src/views/onboarding.rs index f64dbd4..e4cd516 100644 --- a/crates/coop/src/views/onboarding.rs +++ b/crates/coop/src/views/onboarding.rs @@ -9,12 +9,12 @@ use gpui::{ SharedString, StatefulInteractiveElement, Styled, Task, Window, }; use i18n::{shared_t, t}; +use key_store::backend::KeyItem; +use key_store::KeyStore; use nostr_connect::prelude::*; -use registry::keystore::KeyItem; -use registry::Registry; use smallvec::{smallvec, SmallVec}; use states::app_state; -use states::constants::{APP_NAME, NOSTR_CONNECT_RELAY, NOSTR_CONNECT_TIMEOUT}; +use states::constants::{CLIENT_NAME, NOSTR_CONNECT_RELAY, NOSTR_CONNECT_TIMEOUT}; use theme::ActiveTheme; use ui::button::{Button, ButtonVariants}; use ui::dock_area::panel::{Panel, PanelEvent}; @@ -81,7 +81,7 @@ impl Onboarding { let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT); let relay = RelayUrl::parse(NOSTR_CONNECT_RELAY).unwrap(); - let uri = NostrConnectURI::client(app_keys.public_key(), vec![relay], APP_NAME); + let uri = NostrConnectURI::client(app_keys.public_key(), vec![relay], CLIENT_NAME); let qr_code = uri.to_string().to_qr(); // NIP46: https://github.com/nostr-protocol/nips/blob/master/46.md @@ -126,7 +126,7 @@ impl Onboarding { window: &mut Window, cx: &mut Context, ) { - let keystore = Registry::global(cx).read(cx).keystore(); + let keystore = KeyStore::global(cx).read(cx).backend(); let username = self.app_keys.public_key().to_hex(); let secret = self.app_keys.secret_key().to_secret_bytes(); let mut clean_uri = uri.to_string(); diff --git a/crates/coop/src/views/sidebar/list_item.rs b/crates/coop/src/views/sidebar/list_item.rs index 33363b1..9ec241e 100644 --- a/crates/coop/src/views/sidebar/list_item.rs +++ b/crates/coop/src/views/sidebar/list_item.rs @@ -52,8 +52,8 @@ impl RoomListItem { self } - pub fn public_key(mut self, public_key: PublicKey) -> Self { - self.public_key = Some(public_key); + pub fn public_key(mut self, public_key: &PublicKey) -> Self { + self.public_key = Some(public_key.to_owned()); self } diff --git a/crates/coop/src/views/sidebar/mod.rs b/crates/coop/src/views/sidebar/mod.rs index a1bcf1c..f0885d1 100644 --- a/crates/coop/src/views/sidebar/mod.rs +++ b/crates/coop/src/views/sidebar/mod.rs @@ -22,7 +22,6 @@ use settings::AppSettings; use smallvec::{smallvec, SmallVec}; use states::app_state; use states::constants::{BOOTSTRAP_RELAYS, SEARCH_RELAYS}; -use states::state::UnwrappingStatus; use theme::ActiveTheme; use ui::button::{Button, ButtonVariants}; use ui::dock_area::panel::{Panel, PanelEvent}; @@ -627,7 +626,7 @@ impl Sidebar { .name(this.display_name(cx)) .avatar(this.display_image(proxy, cx)) .created_at(this.created_at.to_ago()) - .public_key(this.members[0]) + .public_key(this.members.iter().nth(0).unwrap().0) .kind(this.kind) .on_click(handler), ) @@ -669,7 +668,7 @@ impl Focusable for Sidebar { impl Render for Sidebar { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let registry = Registry::read_global(cx); - let loading = registry.unwrapping_status.read(cx) != &UnwrappingStatus::Complete; + let loading = registry.loading; // Get rooms from either search results or the chat registry let rooms = if let Some(results) = self.local_result.read(cx).as_ref() { diff --git a/crates/key_store/Cargo.toml b/crates/key_store/Cargo.toml new file mode 100644 index 0000000..dc33a36 --- /dev/null +++ b/crates/key_store/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "key_store" +version.workspace = true +edition.workspace = true +publish.workspace = true + +[dependencies] +common = { path = "../common" } +states = { path = "../states" } +ui = { path = "../ui" } +theme = { path = "../theme" } +settings = { path = "../settings" } + +rust-i18n.workspace = true +i18n.workspace = true +gpui.workspace = true + +nostr.workspace = true +nostr-sdk.workspace = true + +anyhow.workspace = true +itertools.workspace = true +smallvec.workspace = true +smol.workspace = true +log.workspace = true +futures.workspace = true +serde.workspace = true +serde_json.workspace = true diff --git a/crates/registry/src/keystore.rs b/crates/key_store/src/backend.rs similarity index 94% rename from crates/registry/src/keystore.rs rename to crates/key_store/src/backend.rs index 4217c96..75dd466 100644 --- a/crates/registry/src/keystore.rs +++ b/crates/key_store/src/backend.rs @@ -14,8 +14,6 @@ use states::paths::config_dir; pub enum KeyItem { User, Bunker, - Client, - Encryption, } impl Display for KeyItem { @@ -23,8 +21,6 @@ impl Display for KeyItem { match self { Self::User => write!(f, "coop-user"), Self::Bunker => write!(f, "coop-bunker"), - Self::Client => write!(f, "coop-client"), - Self::Encryption => write!(f, "coop-encryption"), } } } @@ -35,7 +31,7 @@ impl From for String { } } -pub trait KeyStore: Any + Send + Sync { +pub trait KeyBackend: Any + Send + Sync { fn name(&self) -> &str; /// Reads the credentials from the provider. @@ -66,7 +62,7 @@ pub trait KeyStore: Any + Send + Sync { /// A credentials provider that stores credentials in the system keychain. pub struct KeyringProvider; -impl KeyStore for KeyringProvider { +impl KeyBackend for KeyringProvider { fn name(&self) -> &str { "keyring" } @@ -139,7 +135,7 @@ impl Default for FileProvider { } } -impl KeyStore for FileProvider { +impl KeyBackend for FileProvider { fn name(&self) -> &str { "file" } diff --git a/crates/key_store/src/lib.rs b/crates/key_store/src/lib.rs new file mode 100644 index 0000000..beeaff3 --- /dev/null +++ b/crates/key_store/src/lib.rs @@ -0,0 +1,96 @@ +use std::sync::{Arc, LazyLock}; + +use gpui::{App, AppContext, Context, Entity, Global, Task}; +use smallvec::{smallvec, SmallVec}; + +use crate::backend::{FileProvider, KeyBackend, KeyringProvider}; + +pub mod backend; + +static DISABLE_KEYRING: LazyLock = + LazyLock::new(|| std::env::var("DISABLE_KEYRING").is_ok_and(|value| !value.is_empty())); + +pub fn init(cx: &mut App) { + KeyStore::set_global(cx.new(KeyStore::new), cx); +} + +struct GlobalKeyStore(Entity); + +impl Global for GlobalKeyStore {} + +pub struct KeyStore { + /// Key Store for storing credentials + pub backend: Arc, + + /// Whether the keystore has been initialized + pub initialized: bool, + + /// Tasks for asynchronous operations + _tasks: SmallVec<[Task<()>; 1]>, +} + +impl KeyStore { + /// Retrieve the global keys state + pub fn global(cx: &App) -> Entity { + cx.global::().0.clone() + } + + /// Set the global keys instance + pub(crate) fn set_global(state: Entity, cx: &mut App) { + cx.set_global(GlobalKeyStore(state)); + } + + /// Create a new keys instance + pub(crate) fn new(cx: &mut Context) -> Self { + // Use the file system for keystore in development or when the user specifies it + let use_file_keystore = cfg!(debug_assertions) || *DISABLE_KEYRING; + + // Construct the key backend + let backend: Arc = if use_file_keystore { + Arc::new(FileProvider::default()) + } else { + Arc::new(KeyringProvider) + }; + + // Only used for testing keyring availability on the user's system + let read_credential = cx.read_credentials("Coop"); + + let mut tasks = smallvec![]; + + tasks.push( + // Verify the keyring availability + cx.spawn(async move |this, cx| { + let result = read_credential.await; + + this.update(cx, |this, cx| { + if let Err(e) = result { + log::error!("Keyring error: {e}"); + // For Linux: + // The user has not installed secret service on their system + // Fall back to the file provider + this.backend = Arc::new(FileProvider::default()); + } + this.initialized = true; + cx.notify(); + }) + .ok(); + }), + ); + + Self { + backend, + initialized: false, + _tasks: tasks, + } + } + + /// Returns the key backend. + pub fn backend(&self) -> Arc { + Arc::clone(&self.backend) + } + + /// Returns true if the keystore is a file key backend. + pub fn is_using_file_keystore(&self) -> bool { + self.backend.name() == "file" + } +} diff --git a/crates/registry/Cargo.toml b/crates/registry/Cargo.toml index 158757c..4ba17ac 100644 --- a/crates/registry/Cargo.toml +++ b/crates/registry/Cargo.toml @@ -12,16 +12,13 @@ settings = { path = "../settings" } gpui.workspace = true nostr.workspace = true nostr-sdk.workspace = true -nostr-lmdb.workspace = true anyhow.workspace = true itertools.workspace = true smallvec.workspace = true smol.workspace = true log.workspace = true -flume.workspace = true futures.workspace = true serde.workspace = true serde_json.workspace = true fuzzy-matcher = "0.3.7" -rustls = "0.23.23" diff --git a/crates/registry/src/lib.rs b/crates/registry/src/lib.rs index dd688ea..11badf9 100644 --- a/crates/registry/src/lib.rs +++ b/crates/registry/src/lib.rs @@ -1,6 +1,5 @@ use std::cmp::Reverse; use std::collections::{HashMap, HashSet}; -use std::sync::{Arc, LazyLock}; use anyhow::Error; use common::event::EventUtils; @@ -14,19 +13,12 @@ use room::RoomKind; use settings::AppSettings; use smallvec::{smallvec, SmallVec}; use states::app_state; -use states::constants::KEYRING_URL; -use states::state::UnwrappingStatus; -use crate::keystore::{FileProvider, KeyStore, KeyringProvider}; use crate::room::Room; -pub mod keystore; pub mod message; pub mod room; -pub static DISABLE_KEYRING: LazyLock = - LazyLock::new(|| std::env::var("DISABLE_KEYRING").is_ok_and(|value| !value.is_empty())); - pub fn init(cx: &mut App) { Registry::set_global(cx.new(Registry::new), cx); } @@ -49,14 +41,8 @@ pub struct Registry { /// Collection of all persons (user profiles) pub persons: HashMap>, - /// Status of the unwrapping process - pub unwrapping_status: Entity, - - /// Key Store for storing credentials - pub keystore: Arc, - - /// Whether the keystore has been initialized - pub initialized_keystore: bool, + /// Loading status of the registry + pub loading: bool, /// Public Key of the currently activated signer signer_pubkey: Option, @@ -85,39 +71,8 @@ impl Registry { /// Create a new registry instance pub(crate) fn new(cx: &mut Context) -> Self { - let unwrapping_status = cx.new(|_| UnwrappingStatus::default()); - let read_credential = cx.read_credentials(KEYRING_URL); - let initialized_keystore = cfg!(debug_assertions) || *DISABLE_KEYRING; - let keystore: Arc = if cfg!(debug_assertions) || *DISABLE_KEYRING { - Arc::new(FileProvider::default()) - } else { - Arc::new(KeyringProvider) - }; - let mut tasks = smallvec![]; - if !(cfg!(debug_assertions) || *DISABLE_KEYRING) { - tasks.push( - // Verify the keyring access - cx.spawn(async move |this, cx| { - let result = read_credential.await; - - this.update(cx, |this, cx| { - if let Err(e) = result { - log::error!("Keyring error: {e}"); - // For Linux: - // The user has not installed secret service on their system - // Fall back to the file provider - this.keystore = Arc::new(FileProvider::default()); - } - this.initialized_keystore = true; - cx.notify(); - }) - .ok(); - }), - ); - } - tasks.push( // Load all user profiles from the database cx.spawn(async move |this, cx| { @@ -136,12 +91,10 @@ impl Registry { ); Self { - unwrapping_status, - keystore, - initialized_keystore, rooms: vec![], persons: HashMap::new(), signer_pubkey: None, + loading: true, _tasks: tasks, } } @@ -165,16 +118,6 @@ impl Registry { }) } - /// Returns the keystore. - pub fn keystore(&self) -> Arc { - Arc::clone(&self.keystore) - } - - /// Returns true if the keystore is a file keystore. - pub fn is_using_file_keystore(&self) -> bool { - self.keystore.name() == "file" - } - /// Returns the public key of the currently activated signer. pub fn signer_pubkey(&self) -> Option { self.signer_pubkey @@ -233,6 +176,11 @@ impl Registry { } } + pub fn set_loading(&mut self, loading: bool, cx: &mut Context) { + self.loading = loading; + cx.notify(); + } + /// Get a room by its ID. pub fn room(&self, id: &u64, cx: &App) -> Option> { self.rooms @@ -297,24 +245,13 @@ impl Registry { pub fn search_by_public_key(&self, public_key: PublicKey, cx: &App) -> Vec> { self.rooms .iter() - .filter(|room| room.read(cx).members.contains(&public_key)) + .filter(|room| room.read(cx).members.contains_key(&public_key)) .cloned() .collect() } - /// Set the loading status of the registry. - pub fn set_unwrapping_status(&mut self, status: UnwrappingStatus, cx: &mut Context) { - self.unwrapping_status.update(cx, |this, cx| { - *this = status; - cx.notify(); - }); - } - /// Reset the registry. pub fn reset(&mut self, cx: &mut Context) { - // Reset the unwrapping status - self.set_unwrapping_status(UnwrappingStatus::default(), cx); - // Clear the current identity self.signer_pubkey = None; @@ -339,10 +276,7 @@ impl Registry { let authored_filter = Filter::new() .kind(Kind::ApplicationSpecificData) - .custom_tag( - SingleLetterTag::lowercase(Alphabet::A), - public_key.to_hex(), - ); + .custom_tag(SingleLetterTag::lowercase(Alphabet::A), public_key); let addressed_filter = Filter::new() .kind(Kind::ApplicationSpecificData) diff --git a/crates/registry/src/room.rs b/crates/registry/src/room.rs index 80dcb47..e383f95 100644 --- a/crates/registry/src/room.rs +++ b/crates/registry/src/room.rs @@ -7,20 +7,57 @@ use anyhow::{anyhow, Error}; use common::display::RenderedProfile; use common::event::EventUtils; use gpui::{App, AppContext, Context, EventEmitter, SharedString, SharedUri, Task}; -use itertools::Itertools; use nostr_sdk::prelude::*; +use serde::{Deserialize, Serialize}; use states::app_state; use states::constants::SEND_RETRY; use crate::Registry; +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Deserialize, Serialize)] +pub enum SignerKind { + Encryption, + User, + #[default] + Auto, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct SendOptions { + pub backup: bool, + pub signer_kind: SignerKind, +} + +impl SendOptions { + pub fn new() -> Self { + Self { + backup: true, + signer_kind: SignerKind::default(), + } + } + + pub fn backup(&self) -> bool { + self.backup + } +} + +impl Default for SendOptions { + fn default() -> Self { + Self::new() + } +} + #[derive(Debug, Clone)] pub struct SendReport { pub receiver: PublicKey, + pub status: Option>, pub error: Option, - pub on_hold: Option, + pub relays_not_found: bool, + pub device_not_found: bool, + + pub on_hold: Option, } impl SendReport { @@ -31,18 +68,17 @@ impl SendReport { error: None, on_hold: None, relays_not_found: false, + device_not_found: false, } } pub fn status(mut self, output: Output) -> Self { self.status = Some(output); - self.relays_not_found = false; self } pub fn error(mut self, error: impl Into) -> Self { self.error = Some(error.into()); - self.relays_not_found = false; self } @@ -51,11 +87,16 @@ impl SendReport { self } - pub fn not_found(mut self) -> Self { + pub fn relays_not_found(mut self) -> Self { self.relays_not_found = true; self } + pub fn device_not_found(mut self) -> Self { + self.device_not_found = true; + self + } + pub fn is_relay_error(&self) -> bool { self.error.is_some() || self.relays_not_found } @@ -82,6 +123,8 @@ pub enum RoomKind { Request, } +type DevicePublicKey = PublicKey; + #[derive(Debug)] pub struct Room { pub id: u64, @@ -89,7 +132,7 @@ pub struct Room { /// Subject of the room pub subject: Option, /// All members of the room - pub members: Vec, + pub members: HashMap>, /// Kind pub kind: RoomKind, } @@ -128,7 +171,11 @@ impl From<&Event> for Room { let created_at = val.created_at; // Get the members from the event's tags and event's pubkey - let members = val.all_pubkeys(); + let members: HashMap> = val + .all_pubkeys() + .into_iter() + .map(|public_key| (public_key, None)) + .collect(); // Get subject from tags let subject = val @@ -152,7 +199,11 @@ impl From<&UnsignedEvent> for Room { let created_at = val.created_at; // Get the members from the event's tags and event's pubkey - let members = val.all_pubkeys(); + let members: HashMap> = val + .all_pubkeys() + .into_iter() + .map(|public_key| (public_key, None)) + .collect(); // Get subject from tags let subject = val @@ -233,8 +284,8 @@ impl Room { } /// Returns the members of the room - pub fn members(&self) -> &Vec { - &self.members + pub fn members(&self) -> Vec { + self.members.keys().cloned().collect() } /// Checks if the room has more than two members (group) @@ -264,17 +315,17 @@ impl Room { /// /// This member is always different from the current user. fn display_member(&self, cx: &App) -> Profile { - let registry = Registry::read_global(cx); + let registry = Registry::global(cx); + let signer_pubkey = registry.read(cx).signer_pubkey(); - if let Some(public_key) = registry.signer_pubkey() { - for member in self.members() { - if member != &public_key { - return registry.get_person(member, cx); - } - } - } + let target_member = self + .members + .keys() + .find(|&member| Some(member) != signer_pubkey.as_ref()) + .or_else(|| self.members.keys().next()) + .expect("Room should have at least one member"); - registry.get_person(&self.members[0], cx) + registry.read(cx).get_person(target_member, cx) } /// Merge the names of the first two members of the room. @@ -284,7 +335,7 @@ impl Room { if self.is_group() { let profiles: Vec = self .members - .iter() + .keys() .map(|public_key| registry.get_person(public_key, cx)) .collect(); @@ -305,91 +356,9 @@ impl Room { } } - /// Connects to all members's messaging relays - pub fn connect(&self, cx: &App) -> Task>, Error>> { - let members = self.members.clone(); - - cx.background_spawn(async move { - let client = app_state().client(); - let signer = client.signer().await?; - let public_key = signer.get_public_key().await?; - - let mut relays = HashMap::new(); - let mut processed = HashSet::new(); - - for member in members.into_iter() { - if member == public_key { - continue; - }; - - relays.insert(member, vec![]); - - let filter = Filter::new() - .kind(Kind::InboxRelays) - .author(member) - .limit(1); - - let mut stream = client - .stream_events(filter, Duration::from_secs(10)) - .await?; - - if let Some(event) = stream.next().await { - if processed.insert(event.id) { - let public_key = event.pubkey; - let urls: Vec = nip17::extract_owned_relay_list(event).collect(); - - // Check if at least one URL exists - if urls.is_empty() { - continue; - } - - // Connect to relays - for url in urls.iter() { - client.add_relay(url).await?; - client.connect_relay(url).await?; - } - - relays.entry(public_key).and_modify(|v| v.extend(urls)); - } - } - } - - Ok(relays) - }) - } - - /// Loads all messages for this room from the database - pub fn load_messages(&self, cx: &App) -> Task, Error>> { - let conversation_id = self.id.to_string(); - - cx.background_spawn(async move { - let client = app_state().client(); - let filter = Filter::new() - .kind(Kind::ApplicationSpecificData) - .custom_tag( - SingleLetterTag::lowercase(Alphabet::C), - conversation_id.as_str(), - ); - - let stored = client.database().query(filter).await?; - let mut messages = Vec::with_capacity(stored.len()); - - for event in stored { - match UnsignedEvent::from_json(&event.content) { - Ok(rumor) => messages.push(rumor), - Err(e) => log::warn!("Failed to parse stored rumor: {e}"), - } - } - - messages.sort_by_key(|message| message.created_at); - - Ok(messages) - }) - } - /// Emits a new message signal to the current room - pub fn emit_message(&self, gift_wrap_id: EventId, event: UnsignedEvent, cx: &mut Context) { - cx.emit(RoomSignal::NewMessage((gift_wrap_id, event))); + pub fn emit_message(&self, id: EventId, event: UnsignedEvent, cx: &mut Context) { + cx.emit(RoomSignal::NewMessage((id, event))); } /// Emits a signal to refresh the current room's messages. @@ -397,9 +366,69 @@ impl Room { cx.emit(RoomSignal::Refresh); } + /// Get messaging relays and encryption keys announcement for each member + pub fn connect(&self, cx: &App) -> Task> { + let members = self.members(); + + cx.background_spawn(async move { + let client = app_state().client(); + let signer = client.signer().await?; + let public_key = signer.get_public_key().await?; + let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); + + for member in members.into_iter() { + if member == public_key { + continue; + }; + + let filter = Filter::new() + .kind(Kind::InboxRelays) + .author(member) + .limit(1); + + // Subscribe to get members messaging relays + client.subscribe(filter, Some(opts)).await?; + + let filter = Filter::new() + .kind(Kind::Custom(10044)) + .author(member) + .limit(1); + + // Subscribe to get members encryption keys announcement + client.subscribe(filter, Some(opts)).await?; + } + + Ok(()) + }) + } + + /// Get all messages belonging to the room + pub fn get_messages(&self, cx: &App) -> Task, Error>> { + let conversation_id = self.id.to_string(); + + cx.background_spawn(async move { + let client = app_state().client(); + let filter = Filter::new() + .kind(Kind::ApplicationSpecificData) + .custom_tag(SingleLetterTag::lowercase(Alphabet::C), conversation_id); + + let stored = client.database().query(filter).await?; + + let mut messages: Vec = stored + .into_iter() + .filter_map(|event| UnsignedEvent::from_json(&event.content).ok()) + .collect(); + + messages.sort_by_key(|message| message.created_at); + + Ok(messages) + }) + } + /// Create a new message event (unsigned) pub fn create_message(&self, content: &str, replies: &[EventId], cx: &App) -> UnsignedEvent { - let public_key = Registry::read_global(cx).signer_pubkey().unwrap(); + let registry = Registry::global(cx); + let public_key = registry.read(cx).signer_pubkey().unwrap(); let subject = self.subject.clone(); let mut tags = vec![]; @@ -407,7 +436,7 @@ impl Room { // Add receivers // // NOTE: current user will be removed from the list of receivers - for member in self.members.iter() { + for (member, _) in self.members.iter() { tags.push(Tag::public_key(member.to_owned())); } @@ -447,34 +476,42 @@ impl Room { /// Create a task to send a message to all room members pub fn send_message( &self, - rumor: UnsignedEvent, - backup: bool, + rumor: &UnsignedEvent, + opts: &SendOptions, cx: &App, ) -> Task, Error>> { let mut members = self.members.clone(); + let rumor = rumor.to_owned(); + let opts = opts.to_owned(); cx.background_spawn(async move { let states = app_state(); let client = states.client(); - let signer = client.signer().await?; - let public_key = signer.get_public_key().await?; + let device = states.device.read().await.encryption_keys.clone(); + + let user_signer = client.signer().await?; + let user_pubkey = user_signer.get_public_key().await?; // Collect relay hints for all participants (including current user) - let mut participants = members.clone(); - if !participants.contains(&public_key) { - participants.push(public_key); + let mut participants: Vec = members.keys().cloned().collect(); + + if !participants.contains(&user_pubkey) { + participants.push(user_pubkey); } + // Initialize relay cache let mut relay_cache: HashMap> = HashMap::new(); + for participant in participants.iter().cloned() { - let urls = Self::messaging_relays(participant).await; + let urls = states.messaging_relays(participant).await; relay_cache.insert(participant, urls); } // Update rumor with relay hints for each receiver let mut rumor = rumor; let mut tags_with_hints = Vec::new(); - for tag in rumor.tags.to_vec() { + + for tag in rumor.tags.into_iter() { if let Some(standard) = tag.as_standardized().cloned() { match standard { TagStandard::PublicKey { @@ -483,18 +520,18 @@ impl Room { uppercase, .. } => { - let relay_url = - relay_cache - .get(&public_key) - .and_then(|urls| urls.first().cloned()); + let relay_url = relay_cache + .get(&public_key) + .and_then(|urls| urls.first().cloned()); + let updated = TagStandard::PublicKey { public_key, relay_url, alias, uppercase, }; - tags_with_hints - .push(Tag::from_standardized_without_cell(updated)); + + tags_with_hints.push(Tag::from_standardized_without_cell(updated)); } _ => tags_with_hints.push(tag), } @@ -506,29 +543,42 @@ impl Room { // Remove the current user's public key from the list of receivers // Current user will be handled separately - members.retain(|&pk| pk != public_key); + let (public_key, device_pubkey) = members.remove_entry(&user_pubkey).unwrap(); + // Determine the signer will be used based on the provided options + let signer = Self::select_signer(&opts.signer_kind, device, user_signer)?; + + // Collect the send reports let mut reports: Vec = vec![]; - for receiver in members.into_iter() { - let rumor = rumor.clone(); - let event = EventBuilder::gift_wrap(&signer, &receiver, rumor, vec![]).await?; + for (receiver, device_pubkey) in members.into_iter() { let urls = relay_cache.get(&receiver).cloned().unwrap_or_default(); - // Check if there are any relays to send the event to + // Check if there are any relays to send the message to if urls.is_empty() { - reports.push(SendReport::new(receiver).not_found()); + reports.push(SendReport::new(receiver).relays_not_found()); continue; } + // Skip sending if using encryption keys but device not found + if device_pubkey.is_none() && matches!(opts.signer_kind, SignerKind::Encryption) { + reports.push(SendReport::new(receiver).device_not_found()); + continue; + } + + // Determine the receiver based on the signer kind + let rumor = rumor.clone(); + let target = Self::select_receiver(&opts.signer_kind, receiver, device_pubkey); + let event = EventBuilder::gift_wrap(&signer, &target, rumor, vec![]).await?; + // Send the event to the messaging 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-")); + let auth = output.failed.iter().any(|(_, s)| s.starts_with("auth-")); let report = SendReport::new(receiver).status(output); - if auth_required { + if auth { // Wait for authenticated and resent event successfully for attempt in 0..=SEND_RETRY { let retry_manager = states.tracker().read().await; @@ -561,15 +611,16 @@ impl Room { // Construct a gift wrap to back up to current user's owned messaging relays let rumor = rumor.clone(); - let event = EventBuilder::gift_wrap(&signer, &public_key, rumor, vec![]).await?; + let target = Self::select_receiver(&opts.signer_kind, public_key, device_pubkey); + let event = EventBuilder::gift_wrap(&signer, &target, rumor, vec![]).await?; // Only send a backup message to current user if sent successfully to others - if reports.iter().all(|r| r.is_sent_success()) && backup { + if opts.backup() && reports.iter().all(|r| r.is_sent_success()) { let urls = relay_cache.get(&public_key).cloned().unwrap_or_default(); // Check if there are any relays to send the event to if urls.is_empty() { - reports.push(SendReport::new(public_key).not_found()); + reports.push(SendReport::new(public_key).relays_not_found()); } else { // Send the event to the messaging relays match client.send_event_to(urls, &event).await { @@ -596,7 +647,8 @@ impl Room { cx: &App, ) -> Task, Error>> { cx.background_spawn(async move { - let client = app_state().client(); + let states = app_state(); + let client = states.client(); let mut resend_reports = vec![]; for report in reports.into_iter() { @@ -625,11 +677,11 @@ impl Room { // Process the on hold event if it exists if let Some(event) = report.on_hold { - let urls = Self::messaging_relays(receiver).await; + let urls = states.messaging_relays(receiver).await; // Check if there are any relays to send the event to if urls.is_empty() { - resend_reports.push(SendReport::new(receiver).not_found()); + resend_reports.push(SendReport::new(receiver).relays_not_found()); } else { // Send the event to the messaging relays match client.send_event_to(urls, &event).await { @@ -648,36 +700,24 @@ impl Room { }) } - /// Gets messaging relays for public key - async fn messaging_relays(public_key: PublicKey) -> Vec { - let client = app_state().client(); - let mut relay_urls = vec![]; - - let filter = Filter::new() - .kind(Kind::InboxRelays) - .author(public_key) - .limit(1); - - if let Ok(events) = client.database().query(filter).await { - if let Some(event) = events.first_owned() { - let urls: Vec = nip17::extract_owned_relay_list(event).collect(); - - // Connect to relays - for url in urls.iter() { - client.add_relay(url).await.ok(); - client.connect_relay(url).await.ok(); - } - - relay_urls.extend(urls.into_iter().take(3).unique()); + fn select_signer(kind: &SignerKind, device: Option, user: T) -> Result + where + T: NostrSigner, + { + match kind { + SignerKind::Encryption => { + Ok(device.ok_or_else(|| anyhow!("No encryption keys found"))?) } + SignerKind::User => Ok(user), + SignerKind::Auto => Ok(device.unwrap_or(user)), } + } - relay_urls + fn select_receiver(kind: &SignerKind, user: PublicKey, device: Option) -> PublicKey { + match kind { + SignerKind::Encryption => device.unwrap(), + SignerKind::User => user, + SignerKind::Auto => device.unwrap_or(user), + } } } - -#[cfg(test)] -mod tests { - use super::*; -} - diff --git a/crates/states/Cargo.toml b/crates/states/Cargo.toml index 74df1db..ebd0b6b 100644 --- a/crates/states/Cargo.toml +++ b/crates/states/Cargo.toml @@ -7,11 +7,12 @@ publish.workspace = true [dependencies] nostr-sdk.workspace = true nostr-lmdb.workspace = true + dirs.workspace = true smol.workspace = true flume.workspace = true log.workspace = true anyhow.workspace = true -whoami = "1.5.2" +whoami = "1.6.1" rustls = "0.23.23" diff --git a/crates/states/src/constants.rs b/crates/states/src/constants.rs index 99c47c7..355283a 100644 --- a/crates/states/src/constants.rs +++ b/crates/states/src/constants.rs @@ -1,9 +1,8 @@ -pub const APP_NAME: &str = "Coop"; +pub const CLIENT_NAME: &str = "Coop"; pub const APP_ID: &str = "su.reya.coop"; pub const APP_PUBKEY: &str = "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDc4MkNFRkQ2RkVGQURGNzUKUldSMTMvcisxdThzZUZraHc4Vno3NVNJek81VkJFUEV3MkJweGFxQXhpekdSU1JIekpqMG4yemMK"; pub const APP_UPDATER_ENDPOINT: &str = "https://coop-updater.reya.su/"; -pub const KEYRING_URL: &str = "Coop Safe Storage"; pub const SETTINGS_IDENTIFIER: &str = "coop:settings"; /// Bootstrap Relays. @@ -33,6 +32,9 @@ pub const NOSTR_CONNECT_TIMEOUT: u64 = 200; /// Default timeout (in seconds) for Nostr Connect (Bunker) pub const BUNKER_TIMEOUT: u64 = 30; +/// Default timeout (in seconds) for fetching events +pub const QUERY_TIMEOUT: u64 = 3; + /// Total metadata requests will be grouped. pub const METADATA_BATCH_LIMIT: usize = 100; diff --git a/crates/states/src/lib.rs b/crates/states/src/lib.rs index d234f39..11d90f5 100644 --- a/crates/states/src/lib.rs +++ b/crates/states/src/lib.rs @@ -1,7 +1,9 @@ use std::sync::OnceLock; use nostr_sdk::prelude::*; +use whoami::{devicename, platform}; +use crate::constants::CLIENT_NAME; use crate::state::AppState; pub mod constants; @@ -9,6 +11,7 @@ pub mod paths; pub mod state; static APP_STATE: OnceLock = OnceLock::new(); +static APP_NAME: OnceLock = OnceLock::new(); static NIP65_RELAYS: OnceLock)>> = OnceLock::new(); static NIP17_RELAYS: OnceLock> = OnceLock::new(); @@ -17,6 +20,15 @@ pub fn app_state() -> &'static AppState { APP_STATE.get_or_init(AppState::new) } +pub fn app_name() -> &'static String { + APP_NAME.get_or_init(|| { + let devicename = devicename(); + let platform = platform(); + + format!("{CLIENT_NAME} on {platform} ({devicename})") + }) +} + /// Default NIP-65 Relays. Used for new account pub fn default_nip65_relays() -> &'static Vec<(RelayUrl, Option)> { NIP65_RELAYS.get_or_init(|| { diff --git a/crates/states/src/state/device.rs b/crates/states/src/state/device.rs new file mode 100644 index 0000000..1ff5e90 --- /dev/null +++ b/crates/states/src/state/device.rs @@ -0,0 +1,25 @@ +use std::sync::Arc; + +use nostr_sdk::prelude::*; + +#[derive(Debug, Clone, Default)] +pub struct Device { + /// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md + /// + /// The client keys that used for communication between devices + pub client_keys: Option>, + + /// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md + /// + /// The encryption keys that used for encryption and decryption + pub encryption_keys: Option>, +} + +impl Device { + pub fn new() -> Self { + Self { + client_keys: None, + encryption_keys: None, + } + } +} diff --git a/crates/states/src/state/ingester.rs b/crates/states/src/state/ingester.rs new file mode 100644 index 0000000..7da3cb4 --- /dev/null +++ b/crates/states/src/state/ingester.rs @@ -0,0 +1,31 @@ +use flume::{Receiver, Sender}; +use nostr_sdk::prelude::*; + +#[derive(Debug, Clone)] +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}"); + } + } +} diff --git a/crates/states/src/state.rs b/crates/states/src/state/mod.rs similarity index 51% rename from crates/states/src/state.rs rename to crates/states/src/state/mod.rs index b80aaee..4802e93 100644 --- a/crates/states/src/state.rs +++ b/crates/states/src/state/mod.rs @@ -1,169 +1,31 @@ use std::borrow::Cow; -use std::collections::{hash_map::DefaultHasher, HashMap, HashSet}; +use std::collections::hash_map::DefaultHasher; +use std::collections::HashSet; use std::hash::{Hash, Hasher}; use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; use std::time::Duration; -use anyhow::{anyhow, Error}; -use flume::{Receiver, Sender}; +use anyhow::{anyhow, Context, Error}; use nostr_lmdb::NostrLMDB; use nostr_sdk::prelude::*; use smol::lock::RwLock; +use crate::app_name; use crate::constants::{ - BOOTSTRAP_RELAYS, METADATA_BATCH_LIMIT, METADATA_BATCH_TIMEOUT, SEARCH_RELAYS, + BOOTSTRAP_RELAYS, METADATA_BATCH_LIMIT, METADATA_BATCH_TIMEOUT, QUERY_TIMEOUT, SEARCH_RELAYS, }; use crate::paths::config_dir; +use crate::state::device::Device; +use crate::state::ingester::Ingester; +use crate::state::tracker::EventTracker; -const TIMEOUT: u64 = 5; +mod device; +mod ingester; +mod signal; +mod tracker; -#[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 a new profile has been received - NewProfile(Profile), - - /// A signal to notify UI that a new gift wrap event has been received - NewMessage((EventId, UnsignedEvent)), - - /// 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, Clone)] -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 fn sender(&self) -> &Sender { - &self.tx - } - - 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, Clone)] -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 - } -} +pub use signal::*; #[derive(Debug)] pub struct AppState { @@ -179,6 +41,9 @@ pub struct AppState { /// Ingester channel for processing public keys ingester: Ingester, + /// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md + pub device: RwLock, + /// The timestamp when the application was initialized. pub initialized_at: Timestamp, @@ -213,6 +78,7 @@ impl AppState { }); let client = ClientBuilder::default().database(lmdb).opts(opts).build(); + let device = RwLock::new(Device::default()); let event_tracker = RwLock::new(EventTracker::default()); let signal = Signal::default(); @@ -220,6 +86,7 @@ impl AppState { Self { client, + device, event_tracker, signal, ingester, @@ -233,6 +100,11 @@ impl AppState { &self.client } + /// Returns a reference to the device + pub fn device(&'static self) -> &'static RwLock { + &self.device + } + /// Returns a reference to the event tracker pub fn tracker(&'static self) -> &'static RwLock { &self.event_tracker @@ -262,7 +134,10 @@ impl AppState { // Get user's gossip relays self.get_nip65(pk).await.ok(); - // Exit the current loop + // Initialize client keys + self.init_client_keys().await.ok(); + + // Exit the loop break; } } @@ -355,6 +230,36 @@ impl AppState { } match event.kind { + // Encryption Keys announcement event + Kind::Custom(10044) => { + if let Ok(true) = self.is_self_authored(&event).await { + if let Ok(announcement) = self.extract_announcement(&event) { + self.signal + .send(SignalKind::EncryptionSet(announcement)) + .await; + } + } + } + // Encryption Keys request event + Kind::Custom(4454) => { + if let Ok(true) = self.is_self_authored(&event).await { + if let Ok(announcement) = self.extract_announcement(&event) { + self.signal + .send(SignalKind::EncryptionRequest(announcement)) + .await; + } + } + } + // Encryption Keys response event + Kind::Custom(4455) => { + if let Ok(true) = self.is_self_authored(&event).await { + if let Ok(response) = self.extract_response(&event) { + self.signal + .send(SignalKind::EncryptionResponse(response)) + .await; + } + } + } Kind::RelayList => { // Get events if relay list belongs to current user if let Ok(true) = self.is_self_authored(&event).await { @@ -370,6 +275,11 @@ impl AppState { log::error!("Failed to subscribe to contact list event: {e}"); } + // Fetch user's encryption announcement event + if let Err(e) = self.get_announcement(author).await { + log::error!("Failed to fetch encryption event: {e}"); + } + // Fetch user's messaging relays event if let Err(e) = self.get_nip17(author).await { log::error!("Failed to fetch messaging relays event: {e}"); @@ -404,7 +314,9 @@ impl AppState { self.signal.send(SignalKind::NewProfile(profile)).await; } Kind::GiftWrap => { - self.extract_rumor(&event).await; + if let Err(e) = self.extract_rumor(&event).await { + log::error!("Failed to extract rumor: {e}"); + } } _ => {} } @@ -428,14 +340,14 @@ impl AppState { event_id, message, .. } => { let msg = MachineReadablePrefix::parse(&message); - let mut event_tracker = self.event_tracker.write().await; + let mut tracker = self.event_tracker.write().await; // Keep track of events sent by Coop - event_tracker.sent_ids.insert(event_id); + 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); + tracker.resend_queue.insert(event_id, relay_url); } } _ => {} @@ -504,6 +416,47 @@ impl AppState { } } + /// Encrypt and store a key in the local database. + pub async fn set_keys(&self, kind: impl Into, value: String) -> Result<(), Error> { + let signer = self.client.signer().await?; + let public_key = signer.get_public_key().await?; + + // Encrypt the value + let content = signer.nip44_encrypt(&public_key, value.as_ref()).await?; + + // Construct the application data event + let event = EventBuilder::new(Kind::ApplicationSpecificData, content) + .tag(Tag::identifier(format!("coop:{}", kind.into()))) + .build(public_key) + .sign(&Keys::generate()) + .await?; + + // Save the event to the database + self.client.database().save_event(&event).await?; + + Ok(()) + } + + /// Get and decrypt a key from the local database. + pub async fn get_keys(&self, kind: impl Into) -> Result { + let signer = self.client.signer().await?; + let public_key = signer.get_public_key().await?; + + let filter = Filter::new() + .kind(Kind::ApplicationSpecificData) + .identifier(format!("coop:{}", kind.into())); + + if let Some(event) = self.client.database().query(filter).await?.first() { + let content = signer.nip44_decrypt(&public_key, &event.content).await?; + let secret = SecretKey::parse(&content)?; + let keys = Keys::new(secret); + + Ok(keys) + } else { + Err(anyhow!("Key not found")) + } + } + /// Check if event is published by current user async fn is_self_authored(&self, event: &Event) -> Result { let signer = self.client.signer().await?; @@ -552,7 +505,7 @@ impl AppState { /// Get and verify NIP-65 relays for a given public key pub async fn get_nip65(&self, public_key: PublicKey) -> Result<(), Error> { - let timeout = Duration::from_secs(TIMEOUT); + let timeout = Duration::from_secs(QUERY_TIMEOUT); let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); let filter = Filter::new() @@ -608,9 +561,269 @@ impl AppState { Ok(()) } + /// Initialize the client keys to communicate between clients + /// + /// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md + pub async fn init_client_keys(&self) -> Result<(), Error> { + // Get the keys from the database or generate new ones + let keys = self + .get_keys("client") + .await + .unwrap_or_else(|_| Keys::generate()); + + // Initialize the client keys + let mut device = self.device.write().await; + device.client_keys = Some(Arc::new(keys)); + + Ok(()) + } + + /// Get and verify encryption announcement for a given public key + /// + /// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md + pub async fn get_announcement(&self, public_key: PublicKey) -> Result<(), Error> { + let timeout = Duration::from_secs(QUERY_TIMEOUT); + let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); + + let filter = Filter::new() + .kind(Kind::Custom(10044)) + .author(public_key) + .limit(1); + + // Subscribe to events from user's nip65 relays + self.client.subscribe(filter.clone(), Some(opts)).await?; + + let tx = self.signal.sender().clone(); + let database = self.client.database().clone(); + + // Verify the received data after a timeout + smol::spawn(async move { + smol::Timer::after(timeout).await; + + if database.count(filter).await.unwrap_or(0) < 1 { + tx.send_async(SignalKind::EncryptionNotSet).await.ok(); + } + }) + .detach(); + + Ok(()) + } + + /// Generate encryption keys and announce them + /// + /// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md + pub async fn init_encryption_keys(&self) -> Result<(), Error> { + let signer = self.client.signer().await?; + let keys = Keys::generate(); + let public_key = keys.public_key(); + let secret = keys.secret_key().to_secret_hex(); + + // Initialize the encryption keys + let mut device = self.device.write().await; + device.encryption_keys = Some(Arc::new(keys)); + + // Store the encryption keys for future use + self.set_keys("encryption", secret).await?; + + // Construct the announcement event + let event = EventBuilder::new(Kind::Custom(10044), "") + .tags(vec![ + Tag::client(app_name()), + Tag::custom(TagKind::custom("n"), vec![public_key]), + ]) + .sign(&signer) + .await?; + + // Send the announcement event to the relays + self.client.send_event(&event).await?; + + Ok(()) + } + + /// User has previously set encryption keys, load them from storage + /// + /// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md + pub async fn load_encryption_keys(&self, announcement: &Announcement) -> Result<(), Error> { + let keys = self.get_keys("encryption").await?; + + // Check if the encryption keys match the announcement + if announcement.public_key() == keys.public_key() { + let mut device = self.device.write().await; + device.encryption_keys = Some(Arc::new(keys)); + + Ok(()) + } else { + Err(anyhow!("Not found")) + } + } + + /// Request encryption keys from other clients + /// + /// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md + pub async fn request_encryption_keys(&self) -> Result { + let mut wait_for_approval = false; + let device = self.device.read().await; + + // Client Keys are always known at this point + let Some(client_keys) = device.client_keys.as_ref() else { + return Err(anyhow!("Client Keys is required")); + }; + + let signer = self.client.signer().await?; + let public_key = signer.get_public_key().await?; + let client_pubkey = client_keys.get_public_key().await?; + + // Get the encryption keys response from the database first + let filter = Filter::new() + .kind(Kind::Custom(4455)) + .author(public_key) + .pubkey(client_pubkey) + .limit(1); + + match self.client.database().query(filter).await?.first_owned() { + // Found encryption keys that shared by other clients + Some(event) => { + let root_device = event + .tags + .find(TagKind::custom("P")) + .and_then(|tag| tag.content()) + .and_then(|content| PublicKey::parse(content).ok()) + .context("Invalid event's tags")?; + + let payload = event.content.as_str(); + let decrypted = client_keys.nip44_decrypt(&root_device, payload).await?; + + let secret = SecretKey::from_hex(&decrypted)?; + let keys = Keys::new(secret); + + // No longer need to hold the reader for device + drop(device); + + let mut device = self.device.write().await; + device.encryption_keys = Some(Arc::new(keys)); + } + None => { + // Construct encryption keys request event + let event = EventBuilder::new(Kind::Custom(4454), "") + .tags(vec![ + Tag::client(app_name()), + Tag::custom(TagKind::custom("pubkey"), vec![client_pubkey]), + ]) + .sign(&signer) + .await?; + + // Send a request for encryption keys from other devices + self.client.send_event(&event).await?; + + // Create a unique ID to control the subscription later + let subscription_id = SubscriptionId::new("request"); + + let filter = Filter::new() + .kind(Kind::Custom(4455)) + .author(public_key) + .pubkey(client_pubkey) + .since(Timestamp::now()); + + // Subscribe to the approval response event + self.client + .subscribe_with_id(subscription_id, filter, None) + .await?; + + wait_for_approval = true; + } + } + + Ok(wait_for_approval) + } + + /// Receive the encryption keys from other clients + /// + /// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md + pub async fn receive_encryption_keys(&self, res: Response) -> Result<(), Error> { + let device = self.device.read().await; + + // Client Keys are always known at this point + let Some(client_keys) = device.client_keys.as_ref() else { + return Err(anyhow!("Client Keys is required")); + }; + + let public_key = res.public_key(); + let payload = res.payload(); + + // Decrypt the payload using the client keys + let decrypted = client_keys.nip44_decrypt(&public_key, payload).await?; + let secret = SecretKey::parse(&decrypted)?; + let keys = Keys::new(secret); + + // No longer need to hold the reader for device + drop(device); + + let mut device = self.device.write().await; + device.encryption_keys = Some(Arc::new(keys)); + + Ok(()) + } + + /// Response the encryption keys request from other clients + /// + /// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md + pub async fn response_encryption_keys(&self, target: PublicKey) -> Result<(), Error> { + let device = self.device.read().await; + + // Client Keys are always known at this point + let Some(client_keys) = device.client_keys.as_ref() else { + return Err(anyhow!("Client Keys is required")); + }; + + let encryption = self.get_keys("encryption").await?; + let client_pubkey = client_keys.get_public_key().await?; + + // Encrypt the encryption keys with the client's signer + let payload = client_keys + .nip44_encrypt(&target, &encryption.secret_key().to_secret_hex()) + .await?; + + // Construct the response event + // + // P tag: the current client's public key + // p tag: the requester's public key + let event = EventBuilder::new(Kind::Custom(4455), payload) + .tags(vec![ + Tag::custom(TagKind::custom("P"), vec![client_pubkey]), + Tag::public_key(target), + ]) + .sign(client_keys) + .await?; + + // Get the current user's signer and public key + let signer = self.client.signer().await?; + let public_key = signer.get_public_key().await?; + + // Get the current user's relay list + let urls: Vec = self + .client + .database() + .relay_list(public_key) + .await? + .into_iter() + .filter_map(|(url, metadata)| { + if metadata.is_none() || metadata == Some(RelayMetadata::Read) { + Some(url) + } else { + None + } + }) + .collect(); + + // Send the response event to the user's relay list + self.client.send_event_to(urls, &event).await?; + + Ok(()) + } + /// Get and verify NIP-17 relays for a given public key pub async fn get_nip17(&self, public_key: PublicKey) -> Result<(), Error> { - let timeout = Duration::from_secs(TIMEOUT); + let timeout = Duration::from_secs(QUERY_TIMEOUT); let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); let filter = Filter::new() @@ -685,33 +898,87 @@ impl AppState { Ok(()) } + /// Gets messaging relays for public key + pub async fn messaging_relays(&self, public_key: PublicKey) -> Vec { + let mut relay_urls = vec![]; + + let filter = Filter::new() + .kind(Kind::InboxRelays) + .author(public_key) + .limit(1); + + if let Ok(events) = self.client.database().query(filter).await { + if let Some(event) = events.first_owned() { + let urls: Vec = nip17::extract_owned_relay_list(event).collect(); + + // Connect to relays + for url in urls.iter() { + self.client.add_relay(url).await.ok(); + self.client.connect_relay(url).await.ok(); + } + + relay_urls.extend(urls.into_iter().take(3)); + } + } + + relay_urls + } + + /// Re-subscribes to gift wrap events + pub async fn resubscribe_messages(&self) -> Result<(), Error> { + let signer = self.client.signer().await?; + let public_key = signer.get_public_key().await?; + let urls = self.messaging_relays(public_key).await; + + let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key); + let id = SubscriptionId::new("inbox"); + + // Unsubscribe the previous subscription + self.client.unsubscribe(&id).await; + + // Subscribe to gift wrap events + self.client + .subscribe_with_id_to(urls, id, filter, None) + .await?; + + Ok(()) + } + /// Stores an unwrapped event in local database with reference to original async fn set_rumor(&self, id: EventId, rumor: &UnsignedEvent) -> Result<(), Error> { - let rumor_id = rumor - .id - .ok_or_else(|| anyhow!("Rumor is missing an event id"))?; - let author_hex = rumor.pubkey.to_hex(); - let conversation = Self::conversation_id(rumor).to_string(); + let rumor_id = rumor.id.context("Rumor is missing an event id")?; + let author = rumor.pubkey; + let conversation = self.conversation_id(rumor).to_string(); let mut tags = rumor.tags.clone().to_vec(); + + // Add a unique identifier tags.push(Tag::identifier(id)); + + // Add a reference to the rumor's author tags.push(Tag::custom( TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::A)), - [author_hex], + [author], )); + + // Add a conversation id tags.push(Tag::custom( TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::C)), [conversation], )); + + // Add a reference to the rumor's id tags.push(Tag::event(rumor_id)); + // Add references to the rumor's participants for receiver in rumor.tags.public_keys().copied() { tags.push(Tag::custom( TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::P)), - [receiver.to_hex()], + [receiver], )); } + // Convert rumor to json let content = rumor.as_json(); let event = EventBuilder::new(Kind::ApplicationSpecificData, content) @@ -739,89 +1006,149 @@ impl AppState { } // Unwraps a gift-wrapped event and processes its contents. - async fn extract_rumor(&self, gift_wrap: &Event) { - let mut rumor: Option = None; - + async fn extract_rumor(&self, gift_wrap: &Event) -> Result<(), Error> { + // Try to get cached rumor first if let Ok(event) = self.get_rumor(gift_wrap.id).await { - rumor = Some(event); - } else if let Ok(unwrapped) = self.client.unwrap_gift_wrap(gift_wrap).await { + self.process_rumor(gift_wrap.id, event).await?; + return Ok(()); + } + + // Try to unwrap with the available signer + if let Ok(unwrapped) = self.try_unwrap_gift(gift_wrap).await { let sender = unwrapped.sender; let mut rumor_unsigned = unwrapped.rumor; - if !Self::verify_rumor_sender(sender, &rumor_unsigned) { - log::warn!( - "Ignoring gift wrap {}: seal pubkey {} mismatches rumor pubkey {}", - gift_wrap.id, - sender, - rumor_unsigned.pubkey - ); - } else { - rumor_unsigned.ensure_id(); + if !self.verify_rumor_sender(sender, &rumor_unsigned) { + return Err(anyhow!("Invalid rumor")); + }; - if let Err(e) = self.set_rumor(gift_wrap.id, &rumor_unsigned).await { - log::warn!("Failed to cache unwrapped event: {e}") - } else { - rumor = Some(rumor_unsigned); - } - } + // Generate event id for the rumor if it doesn't have one + rumor_unsigned.ensure_id(); + + self.set_rumor(gift_wrap.id, &rumor_unsigned).await?; + self.process_rumor(gift_wrap.id, rumor_unsigned).await?; + + return Ok(()); } - 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); - } - } - } + Ok(()) } - fn conversation_id(rumor: &UnsignedEvent) -> u64 { + // Helper method to try unwrapping with different signers + async fn try_unwrap_gift(&self, gift_wrap: &Event) -> Result { + // Try to unwrap with the device's encryption keys first + // NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md + if let Some(signer) = self.device.read().await.encryption_keys.as_ref() { + if let Ok(unwrapped) = UnwrappedGift::from_gift_wrap(signer, gift_wrap).await { + return Ok(unwrapped); + } + } + + // Try to unwrap with the user's signer + let signer = self.client.signer().await?; + if let Ok(unwrapped) = UnwrappedGift::from_gift_wrap(&signer, gift_wrap).await { + return Ok(unwrapped); + } + + Err(anyhow!("No signer available")) + } + + /// Process a rumor event. + async fn process_rumor(&self, id: EventId, event: UnsignedEvent) -> Result<(), Error> { + // 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((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); + } + } + + Ok(()) + } + + /// Get the conversation ID for a given rumor (message). + fn conversation_id(&self, rumor: &UnsignedEvent) -> u64 { let mut hasher = DefaultHasher::new(); let mut pubkeys: Vec = rumor.tags.public_keys().copied().collect(); pubkeys.push(rumor.pubkey); pubkeys.sort(); pubkeys.dedup(); pubkeys.hash(&mut hasher); + hasher.finish() } - fn verify_rumor_sender(sender: PublicKey, rumor: &UnsignedEvent) -> bool { + /// Verify that the sender of a rumor is the same as the sender of the event. + fn verify_rumor_sender(&self, sender: PublicKey, rumor: &UnsignedEvent) -> bool { rumor.pubkey == sender } + + /// Extract an encryption keys announcement from an event. + fn extract_announcement(&self, event: &Event) -> Result { + let public_key = event + .tags + .iter() + .find(|tag| tag.kind().as_str() == "n" || tag.kind().as_str() == "pubkey") + .and_then(|tag| tag.content()) + .and_then(|c| PublicKey::parse(c).ok()) + .context("Cannot parse public key from the event's tags")?; + + let client_name = event + .tags + .find(TagKind::Client) + .and_then(|tag| tag.content()) + .map(|c| c.to_string()) + .context("Cannot parse client name from the event's tags")?; + + Ok(Announcement::new(event.id, client_name, public_key)) + } + + /// Extract an encryption keys response from an event. + fn extract_response(&self, event: &Event) -> Result { + let payload = event.content.clone(); + let root_device = event + .tags + .find(TagKind::custom("P")) + .and_then(|tag| tag.content()) + .and_then(|c| PublicKey::parse(c).ok()) + .context("Cannot parse public key from the event's tags")?; + + Ok(Response::new(payload, root_device)) + } } #[cfg(test)] mod tests { use super::*; + use crate::app_state; #[test] fn verify_rumor_sender_accepts_matching_sender() { + let state = app_state(); + let keys = Keys::generate(); let public_key = keys.public_key(); let rumor = EventBuilder::text_note("hello").build(public_key); - assert!(AppState::verify_rumor_sender(public_key, &rumor)); + + assert!(state.verify_rumor_sender(public_key, &rumor)); } #[test] fn verify_rumor_sender_rejects_mismatched_sender() { + let state = app_state(); + let sender_keys = Keys::generate(); let rumor_keys = Keys::generate(); let rumor = EventBuilder::text_note("spoof").build(rumor_keys.public_key()); - assert!(!AppState::verify_rumor_sender( - sender_keys.public_key(), - &rumor - )); + + assert!(!state.verify_rumor_sender(sender_keys.public_key(), &rumor)); } } diff --git a/crates/states/src/state/signal.rs b/crates/states/src/state/signal.rs new file mode 100644 index 0000000..e220b76 --- /dev/null +++ b/crates/states/src/state/signal.rs @@ -0,0 +1,157 @@ +use flume::{Receiver, Sender}; +use nostr_sdk::prelude::*; + +#[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, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct Announcement { + id: EventId, + client: String, + public_key: PublicKey, +} + +impl Announcement { + pub fn new(id: EventId, client_name: String, public_key: PublicKey) -> Self { + Self { + id, + client: client_name, + public_key, + } + } + + pub fn id(&self) -> EventId { + self.id + } + + pub fn public_key(&self) -> PublicKey { + self.public_key + } + + pub fn client(&self) -> &str { + self.client.as_str() + } +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct Response { + payload: String, + public_key: PublicKey, +} + +impl Response { + pub fn new(payload: String, public_key: PublicKey) -> Self { + Self { + payload, + public_key, + } + } + + pub fn public_key(&self) -> PublicKey { + self.public_key + } + + pub fn payload(&self) -> &str { + self.payload.as_str() + } +} + +/// Signals sent through the global event channel to notify UI +#[derive(Debug)] +pub enum SignalKind { + /// NIP-4e + /// + /// A signal to notify UI that the user has not set encryption keys yet + EncryptionNotSet, + + /// NIP-4e + /// + /// A signal to notify UI that the user has set encryption keys + EncryptionSet(Announcement), + + /// NIP-4e + /// + /// A signal to notify UI that the user has responded to an encryption request + EncryptionResponse(Response), + + /// NIP-4e + /// + /// A signal to notify UI that the user has requested encryption keys from other devices + EncryptionRequest(Announcement), + + /// A signal to notify UI that the client's signer has been set + SignerSet(PublicKey), + + /// A signal to notify UI that the relay requires authentication + Auth(AuthRequest), + + /// 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, UnsignedEvent)), + + /// 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, Clone)] +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 fn sender(&self) -> &Sender { + &self.tx + } + + pub async fn send(&self, kind: SignalKind) { + if let Err(e) = self.tx.send_async(kind).await { + log::error!("Failed to send signal: {e}"); + } + } +} diff --git a/crates/states/src/state/tracker.rs b/crates/states/src/state/tracker.rs new file mode 100644 index 0000000..bcc7405 --- /dev/null +++ b/crates/states/src/state/tracker.rs @@ -0,0 +1,36 @@ +use std::collections::{HashMap, HashSet}; + +use nostr_sdk::prelude::*; + +#[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 + } +} diff --git a/crates/ui/src/popup_menu.rs b/crates/ui/src/popup_menu.rs index 849d0ca..a12a30c 100644 --- a/crates/ui/src/popup_menu.rs +++ b/crates/ui/src/popup_menu.rs @@ -76,6 +76,7 @@ pub trait PopupMenuExt: Styled + Selectable + InteractiveElement + IntoElement + impl PopupMenuExt for Button {} enum PopupMenuItem { + Title(SharedString), Separator, Item { icon: Option, @@ -314,6 +315,20 @@ impl PopupMenu { self } + /// Add a title menu item + pub fn title(mut self, label: impl Into) -> Self { + if self.menu_items.is_empty() { + return self; + } + + if let Some(PopupMenuItem::Title(_)) = self.menu_items.last() { + return self; + } + + self.menu_items.push(PopupMenuItem::Title(label.into())); + self + } + /// Add a separator Menu Item pub fn separator(mut self) -> Self { if self.menu_items.is_empty() { @@ -588,6 +603,15 @@ impl Render for PopupMenu { })); match item { + PopupMenuItem::Title(label) => { + this.child( + div() + .text_xs() + .font_semibold() + .text_color(cx.theme().text_muted) + .child(label.clone()) + ) + }, PopupMenuItem::Separator => this.h_auto().p_0().disabled(true).child( div() .rounded_none() diff --git a/locales/app.yml b/locales/app.yml index 607978b..a285a34 100644 --- a/locales/app.yml +++ b/locales/app.yml @@ -59,6 +59,10 @@ common: en: "Use default" configure: en: "Configure" + hide: + en: "Hide" + reset: + en: "Reset" keyring_disable: label: @@ -74,6 +78,30 @@ keyring_disable: body_5: en: "By clicking continue, you agree to store your credentials as plain text." +pending_encryption: + label: + en: "Wait for Approval" + body_1: + en: "Please open %{c} and approve the request for sharing encryption keys. Without access to them, Coop cannot decrypt your messages that are encrypted with encryption keys." + body_2: + en: "Or you can click the 'Reset' button to reset the encryption keys." + body_3: + en: "By resetting the encryption keys, you will not be able to view your messages that were encrypted with the old encryption keys." + +request_encryption: + label: + en: "Encryption Keys Request" + body: + en: "You've requested for the encryption keys from:" + +encryption: + notice: + en: "Encryption keys are being generated" + success: + en: "Encryption keys have been successfully set up" + reinit: + en: "Encryption keys are being reinitialized" + auto_update: updating: en: "Installing the new update..." @@ -381,6 +409,8 @@ chat: en: "Sent Reports" nip17_not_found: en: "%{u} has not set up Messaging Relays, so they won't receive your message." + device_not_found: + en: "You're sending with an encryption key, but %{u} has not set up an encryption key yet. Try sending with your identity instead." sidebar: reload_menu: