diff --git a/Cargo.lock b/Cargo.lock index 5cd980b..4df62c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "accesskit" -version = "0.24.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5351dcebb14b579ccab05f288596b2ae097005be7ee50a7c3d4ca9d0d5a66f6a" +checksum = "d3b7f7f85a7e5f68090000ed7622545829afd484d210358702ae4cb97dd0c320" dependencies = [ "uuid", ] @@ -25,16 +25,6 @@ dependencies = [ "zvariant", ] -[[package]] -name = "accesskit_consumer" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53cf47daed85312e763fbf85ceca136e0d7abc68e0a7e12abe11f48172bc3b10" -dependencies = [ - "accesskit", - "hashbrown 0.16.1", -] - [[package]] name = "accesskit_consumer" version = "0.36.0" @@ -46,13 +36,23 @@ dependencies = [ ] [[package]] -name = "accesskit_macos" -version = "0.26.1" +name = "accesskit_consumer" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c5c87e8d94f2ec10cce590aadff24c76f576dab5502d45d0aed9fc3065d4451" +checksum = "f950720ce064757a1b629caad3a408e8d2c63bb01f29b8a3ff8daa331053ffeb" dependencies = [ "accesskit", - "accesskit_consumer 0.36.0", + "hashbrown 0.16.1", +] + +[[package]] +name = "accesskit_macos" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17cb8b66cef272d48161b02a6317cc2bdd5f98bb0a5e79c68f704a5862aa396b" +dependencies = [ + "accesskit", + "accesskit_consumer 0.37.0", "hashbrown 0.16.1", "objc2 0.5.2", "objc2-app-kit 0.2.2", @@ -67,11 +67,11 @@ checksum = "b016ca8db0ea0ea2ceff29a9d6240391492d960716aa471967c00e8cc8cb197c" dependencies = [ "accesskit", "accesskit_atspi_common", - "async-channel 2.5.0", + "async-channel", "async-executor", "async-task", "atspi", - "futures-lite 2.6.1", + "futures-lite", "futures-util", "serde", "zbus", @@ -79,12 +79,12 @@ dependencies = [ [[package]] name = "accesskit_windows" -version = "0.32.1" +version = "0.33.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eff7009f1a532e917d66970a1e80c965140c6cfbbabbdde3d64e5431e6c78e21" +checksum = "36e93ac7bf50b964f1cbb75f741629a4e950571baa1ef1274457ab5a80d9bcc2" dependencies = [ "accesskit", - "accesskit_consumer 0.35.0", + "accesskit_consumer 0.37.0", "hashbrown 0.16.1", "static_assertions", "windows 0.62.2", @@ -112,7 +112,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "generic-array", ] @@ -123,17 +123,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", - "cipher", - "cpufeatures", + "cipher 0.4.4", + "cpufeatures 0.2.17", "zeroize", ] -[[package]] -name = "ahash" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0453232ace82dee0dd0b4c87a59bd90f7b53b314f3e0f61fe2ee7c8a16482289" - [[package]] name = "ahash" version = "0.8.12" @@ -247,9 +241,9 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "ar_archive_writer" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b" +checksum = "4087686b4b0a3427190bae57a1d9a478dbb2d40c5dc1bd6e2b6d797913bdd348" dependencies = [ "object", ] @@ -316,7 +310,7 @@ dependencies = [ "enumflags2", "futures-channel", "futures-util", - "getrandom 0.4.2", + "getrandom 0.4.3", "serde", "serde_repr", "wayland-backend", @@ -327,7 +321,7 @@ dependencies = [ [[package]] name = "assets" -version = "1.0.0-beta4" +version = "1.0.0-beta5" dependencies = [ "anyhow", "gpui", @@ -341,23 +335,12 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" dependencies = [ - "event-listener 5.4.1", + "event-listener", "event-listener-strategy", "futures-core", "pin-project-lite", ] -[[package]] -name = "async-channel" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" -dependencies = [ - "concurrent-queue", - "event-listener 2.5.3", - "futures-core", -] - [[package]] name = "async-channel" version = "2.5.0" @@ -390,8 +373,8 @@ checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" dependencies = [ "async-task", "concurrent-queue", - "fastrand 2.4.1", - "futures-lite 2.6.1", + "fastrand", + "futures-lite", "pin-project-lite", "slab", ] @@ -404,22 +387,7 @@ checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" dependencies = [ "async-lock", "blocking", - "futures-lite 2.6.1", -] - -[[package]] -name = "async-global-executor" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" -dependencies = [ - "async-channel 2.5.0", - "async-executor", - "async-io", - "async-lock", - "blocking", - "futures-lite 2.6.1", - "once_cell", + "futures-lite", ] [[package]] @@ -432,7 +400,7 @@ dependencies = [ "cfg-if", "concurrent-queue", "futures-io", - "futures-lite 2.6.1", + "futures-lite", "parking", "polling", "rustix 1.1.4", @@ -446,7 +414,7 @@ version = "3.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" dependencies = [ - "event-listener 5.4.1", + "event-listener", "event-listener-strategy", "pin-project-lite", ] @@ -459,7 +427,7 @@ checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" dependencies = [ "async-io", "blocking", - "futures-lite 2.6.1", + "futures-lite", ] [[package]] @@ -468,15 +436,15 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" dependencies = [ - "async-channel 2.5.0", + "async-channel", "async-io", "async-lock", "async-signal", "async-task", "blocking", "cfg-if", - "event-listener 5.4.1", - "futures-lite 2.6.1", + "event-listener", + "futures-lite", "rustix 1.1.4", ] @@ -509,47 +477,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "async-std" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b" -dependencies = [ - "async-channel 1.9.0", - "async-global-executor", - "async-io", - "async-lock", - "async-process", - "crossbeam-utils", - "futures-channel", - "futures-core", - "futures-io", - "futures-lite 2.6.1", - "gloo-timers", - "kv-log-macro", - "log", - "memchr", - "once_cell", - "pin-project-lite", - "pin-utils", - "slab", - "wasm-bindgen-futures", -] - -[[package]] -name = "async-tar" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1937db2d56578aa3919b9bdb0e5100693fd7d1c0f145c53eb81fbb03e217550" -dependencies = [ - "async-std", - "filetime", - "libc", - "pin-project", - "redox_syscall 0.2.16", - "xattr", -] - [[package]] name = "async-task" version = "4.7.1" @@ -581,13 +508,14 @@ dependencies = [ [[package]] name = "async-wsocket" -version = "0.13.1" -source = "git+https://github.com/shadowylab/async-wsocket?rev=0fed6c9c6aec7393ee0e9cf3933d76914ab427d3#0fed6c9c6aec7393ee0e9cf3933d76914ab427d3" +version = "0.14.0" +source = "git+https://github.com/shadowylab/async-wsocket?rev=07c0fda670d05d695929cab55f41761b90aef9a0#07c0fda670d05d695929cab55f41761b90aef9a0" dependencies = [ "futures", "futures-util", "js-sys", "tokio", + "tokio-happy-eyeballs", "tokio-rustls", "tokio-socks", "tokio-tungstenite", @@ -597,19 +525,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "async_zip" -version = "0.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d8c50d65ce1b0e0cb65a785ff615f78860d7754290647d3b983208daa4f85e6" -dependencies = [ - "async-compression", - "crc32fast", - "futures-lite 2.6.1", - "pin-project", - "thiserror 2.0.18", -] - [[package]] name = "atomic" version = "0.5.3" @@ -661,7 +576,7 @@ dependencies = [ [[package]] name = "auto_update" -version = "1.0.0-beta4" +version = "1.0.0-beta5" dependencies = [ "anyhow", "common", @@ -769,12 +684,6 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "base64ct" -version = "1.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" - [[package]] name = "bech32" version = "0.11.1" @@ -787,7 +696,7 @@ version = "0.71.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "cexpr", "clang-sys", "itertools 0.13.0", @@ -835,15 +744,15 @@ checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" [[package]] name = "bitcoin-io" -version = "0.1.100" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11301df0b06f22dea7bb1916403fdd88a371031e495c49b8f96931b28189e175" +checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953" [[package]] name = "bitcoin_hashes" -version = "0.14.100" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c9901a56e133a1fc86eeb1113e2591f45f4682451ca893bff494d2f88918e3f" +checksum = "4ed83caece3afc59919481b33b472e1432d1abc4641ed9100be142ef5110b406" dependencies = [ "bitcoin-io", "hex-conservative", @@ -858,9 +767,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" dependencies = [ "serde_core", ] @@ -889,6 +798,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + [[package]] name = "block-padding" version = "0.3.3" @@ -922,18 +840,18 @@ version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" dependencies = [ - "async-channel 2.5.0", + "async-channel", "async-task", "futures-io", - "futures-lite 2.6.1", + "futures-lite", "piper", ] [[package]] name = "borsh" -version = "1.6.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a" +checksum = "2f3f6da4992df95bbcd9af42a6c7dcb994498fc9048230405f3b36ff7cd3f145" dependencies = [ "bytes", "cfg_aliases", @@ -1001,9 +919,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" [[package]] name = "bzip2" @@ -1020,7 +938,7 @@ version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "polling", "rustix 1.1.4", "slab", @@ -1045,7 +963,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" dependencies = [ - "cipher", + "cipher 0.4.4", ] [[package]] @@ -1068,9 +986,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.63" +version = "1.2.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" +checksum = "dad887fd958be91b5098c0248def011f4523ab786cd411be668777e55063501f" dependencies = [ "find-msvc-tools", "jobserver", @@ -1121,8 +1039,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" dependencies = [ "cfg-if", - "cipher", - "cpufeatures", + "cipher 0.4.4", + "cpufeatures 0.2.17", ] [[package]] @@ -1133,14 +1051,14 @@ checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" dependencies = [ "aead", "chacha20", - "cipher", + "cipher 0.4.4", "poly1305", "zeroize", ] [[package]] name = "chat" -version = "1.0.0-beta4" +version = "1.0.0-beta5" dependencies = [ "anyhow", "common", @@ -1163,7 +1081,7 @@ dependencies = [ [[package]] name = "chat_ui" -version = "1.0.0-beta4" +version = "1.0.0-beta5" dependencies = [ "anyhow", "chat", @@ -1191,9 +1109,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", "js-sys", @@ -1209,11 +1127,22 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common", - "inout", + "crypto-common 0.1.7", + "inout 0.1.4", "zeroize", ] +[[package]] +name = "cipher" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" +dependencies = [ + "block-buffer 0.12.1", + "crypto-common 0.2.2", + "inout 0.2.2", +] + [[package]] name = "clang-sys" version = "1.8.1" @@ -1274,6 +1203,12 @@ dependencies = [ "cc", ] +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + [[package]] name = "cocoa" version = "0.25.0" @@ -1296,7 +1231,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f79398230a6e2c08f5c9760610eb6924b52aa9e7950a619602baba59dcbbdbb2" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "block", "cocoa-foundation 0.2.0", "core-foundation 0.10.0", @@ -1326,7 +1261,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e14045fb83be07b5acf1c0884b2180461635b433455fa35d1cd6f17f1450679d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "block", "core-foundation 0.10.0", "core-graphics-types 0.2.0", @@ -1348,8 +1283,9 @@ dependencies = [ [[package]] name = "collections" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d" +source = "git+https://github.com/zed-industries/zed#69b602c797a62f09318916d24a98c930533fbdc8" dependencies = [ + "gpui_util", "indexmap", "rustc-hash 2.1.2", ] @@ -1376,19 +1312,9 @@ dependencies = [ "memchr", ] -[[package]] -name = "command-fds" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b60b5124979fccd9addd89d8b97a1d6eebb4950694520c75ddd722535ea443f" -dependencies = [ - "nix 0.31.3", - "thiserror 2.0.18", -] - [[package]] name = "common" -version = "1.0.0-beta4" +version = "1.0.0-beta5" dependencies = [ "anyhow", "bech32", @@ -1414,7 +1340,6 @@ checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf" dependencies = [ "bzip2", "compression-core", - "deflate64", "flate2", "memchr", ] @@ -1485,7 +1410,7 @@ dependencies = [ [[package]] name = "coop" -version = "1.0.0-beta4" +version = "1.0.0-beta5" dependencies = [ "anyhow", "assets", @@ -1518,7 +1443,6 @@ dependencies = [ "smol", "state", "theme", - "title_bar", "tracing-subscriber", "ui", "webbrowser", @@ -1526,7 +1450,7 @@ dependencies = [ [[package]] name = "coop_web" -version = "1.0.0-beta4" +version = "1.0.0-beta5" dependencies = [ "anyhow", "assets", @@ -1606,7 +1530,7 @@ version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "core-foundation 0.10.0", "core-graphics-types 0.2.0", "foreign-types", @@ -1619,7 +1543,7 @@ version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32eb7c354ae9f6d437a6039099ce7ecd049337a8109b23d73e48e8ffba8e9cd5" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "core-foundation 0.9.4", "core-graphics-types 0.1.3", "foreign-types", @@ -1643,7 +1567,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "core-foundation 0.10.0", "libc", ] @@ -1654,7 +1578,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4416167a69126e617f8d0a214af0e3c1dbdeffcb100ddf72dcd1a1ac9893c146" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "block", "cfg-if", "core-foundation 0.10.0", @@ -1702,7 +1626,7 @@ version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be17b688510d934ce13f48a2beba700e11583e281e0fda99c22bb256a14eda73" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "fontdb", "harfrust", "linebender_resource_handle", @@ -1710,7 +1634,7 @@ dependencies = [ "rangemap", "rustc-hash 2.1.2", "self_cell", - "skrifa", + "skrifa 0.40.0", "smol_str", "swash", "sys-locale", @@ -1729,6 +1653,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -1788,6 +1721,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + [[package]] name = "ctor" version = "1.0.7" @@ -1798,6 +1740,15 @@ dependencies = [ "linktime-proc-macro", ] +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + [[package]] name = "data-encoding" version = "2.11.0" @@ -1810,12 +1761,6 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" -[[package]] -name = "deflate64" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2" - [[package]] name = "derive_more" version = "2.1.1" @@ -1842,7 +1787,7 @@ dependencies = [ [[package]] name = "derive_refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d" +source = "git+https://github.com/zed-industries/zed#69b602c797a62f09318916d24a98c930533fbdc8" dependencies = [ "proc-macro2", "quote", @@ -1851,7 +1796,7 @@ dependencies = [ [[package]] name = "device" -version = "1.0.0-beta4" +version = "1.0.0-beta5" dependencies = [ "anyhow", "common", @@ -1863,6 +1808,7 @@ dependencies = [ "person", "serde", "serde_json", + "settings", "smallvec", "smol", "state", @@ -1876,11 +1822,22 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "crypto-common", + "block-buffer 0.10.4", + "crypto-common 0.1.7", "subtle", ] +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "crypto-common 0.2.2", + "ctutils", +] + [[package]] name = "dirs" version = "5.0.1" @@ -1935,7 +1892,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "block2 0.6.2", "libc", "objc2 0.6.4", @@ -1961,15 +1918,6 @@ dependencies = [ "libloading", ] -[[package]] -name = "dlv-list" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68df3f2b690c1b86e65ef7830956aededf3cb0a16f898f79b9a6f421a7b6211b" -dependencies = [ - "rand 0.8.6", -] - [[package]] name = "document-features" version = "0.2.12" @@ -2140,12 +2088,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "event-listener" -version = "2.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" - [[package]] name = "event-listener" version = "5.4.1" @@ -2163,7 +2105,7 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" dependencies = [ - "event-listener 5.4.1", + "event-listener", "pin-project-lite", ] @@ -2192,15 +2134,6 @@ dependencies = [ "serde", ] -[[package]] -name = "fastrand" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" -dependencies = [ - "instant", -] - [[package]] name = "fastrand" version = "2.4.1" @@ -2225,15 +2158,6 @@ dependencies = [ "simd-adler32", ] -[[package]] -name = "file-locker" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75ae8b5984a4863d8a32109a848d038bd6d914f20f010cc141375f7a183c41cf" -dependencies = [ - "nix 0.29.0", -] - [[package]] name = "filedescriptor" version = "0.8.3" @@ -2245,16 +2169,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "filetime" -version = "0.2.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" -dependencies = [ - "cfg-if", - "libc", -] - [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -2273,7 +2187,7 @@ version = "25.12.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35f6839d7b3b98adde531effaf34f0c2badc6f4735d26fe74709d8e513a96ef3" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "rustc_version", ] @@ -2332,7 +2246,7 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" dependencies = [ - "fastrand 2.4.1", + "fastrand", "futures-core", "futures-sink", "spin 0.9.8", @@ -2382,7 +2296,7 @@ checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905" dependencies = [ "fontconfig-parser", "log", - "memmap2 0.9.10", + "memmap2", "slotmap", "tinyvec", "ttf-parser", @@ -2424,16 +2338,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "freedesktop_entry_parser" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db9c27b72f19a99a895f8ca89e2d26e4ef31013376e56fdafef697627306c3e4" -dependencies = [ - "nom 7.1.3", - "thiserror 1.0.69", -] - [[package]] name = "freetype-sys" version = "0.20.1" @@ -2460,16 +2364,6 @@ dependencies = [ "serde", ] -[[package]] -name = "futf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" -dependencies = [ - "mac", - "new_debug_unreachable", -] - [[package]] name = "futures" version = "0.3.32" @@ -2503,7 +2397,7 @@ checksum = "175cd8cca9e1d45b87f18ffa75088f2099e3c4fe5e2f83e42de112560bea8ea6" dependencies = [ "fixedbitset", "futures-core", - "futures-lite 2.6.1", + "futures-lite", "pin-project", "smallvec", ] @@ -2531,28 +2425,13 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" -[[package]] -name = "futures-lite" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" -dependencies = [ - "fastrand 1.9.0", - "futures-core", - "futures-io", - "memchr", - "parking", - "pin-project-lite", - "waker-fn", -] - [[package]] name = "futures-lite" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" dependencies = [ - "fastrand 2.4.1", + "fastrand", "futures-core", "futures-io", "parking", @@ -2666,15 +2545,14 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", - "wasip2", - "wasip3", + "rand_core 0.10.1", ] [[package]] @@ -2786,7 +2664,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "gpu-descriptor-types", "hashbrown 0.15.5", ] @@ -2797,20 +2675,20 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", ] [[package]] name = "gpui" version = "0.2.2" -source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d" +source = "git+https://github.com/zed-industries/zed#69b602c797a62f09318916d24a98c930533fbdc8" dependencies = [ "accesskit", "anyhow", - "async-channel 2.5.0", + "async-channel", "async-task", "bindgen", - "bitflags 2.11.1", + "bitflags 2.13.0", "block", "cbindgen", "chrono", @@ -2833,6 +2711,7 @@ dependencies = [ "gpui_macros", "gpui_shared_string", "gpui_util", + "heapless 0.9.3", "http_client", "image", "inventory", @@ -2884,14 +2763,14 @@ dependencies = [ [[package]] name = "gpui_linux" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d" +source = "git+https://github.com/zed-industries/zed#69b602c797a62f09318916d24a98c930533fbdc8" dependencies = [ "accesskit", "accesskit_unix", "anyhow", "as-raw-xcb-connection", "ashpd", - "bitflags 2.11.1", + "bitflags 2.13.0", "bytemuck", "calloop", "calloop-wayland-source", @@ -2899,6 +2778,7 @@ dependencies = [ "filedescriptor", "futures", "gpui", + "gpui_util", "gpui_wgpu", "http_client", "image", @@ -2917,7 +2797,6 @@ dependencies = [ "strum", "swash", "url", - "util", "uuid", "wayland-backend", "wayland-client", @@ -2935,7 +2814,7 @@ dependencies = [ [[package]] name = "gpui_macos" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d" +source = "git+https://github.com/zed-industries/zed#69b602c797a62f09318916d24a98c930533fbdc8" dependencies = [ "accesskit", "accesskit_macos", @@ -2957,6 +2836,7 @@ dependencies = [ "foreign-types", "futures", "gpui", + "gpui_util", "image", "itertools 0.14.0", "libc", @@ -2965,14 +2845,15 @@ dependencies = [ "media", "metal", "objc", + "objc2 0.6.4", "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", "parking_lot", "pathfinder_geometry", "raw-window-handle", "semver", "smallvec", "strum", - "util", "uuid", "zed-font-kit", ] @@ -2980,7 +2861,7 @@ dependencies = [ [[package]] name = "gpui_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d" +source = "git+https://github.com/zed-industries/zed#69b602c797a62f09318916d24a98c930533fbdc8" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -2991,7 +2872,7 @@ dependencies = [ [[package]] name = "gpui_platform" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d" +source = "git+https://github.com/zed-industries/zed#69b602c797a62f09318916d24a98c930533fbdc8" dependencies = [ "console_error_panic_hook", "gpui", @@ -3004,7 +2885,7 @@ dependencies = [ [[package]] name = "gpui_shared_string" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d" +source = "git+https://github.com/zed-industries/zed#69b602c797a62f09318916d24a98c930533fbdc8" dependencies = [ "schemars", "serde", @@ -3014,27 +2895,28 @@ dependencies = [ [[package]] name = "gpui_tokio" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d" +source = "git+https://github.com/zed-industries/zed#69b602c797a62f09318916d24a98c930533fbdc8" dependencies = [ "anyhow", "gpui", + "gpui_util", "tokio", - "util", ] [[package]] name = "gpui_util" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d" +source = "git+https://github.com/zed-industries/zed#69b602c797a62f09318916d24a98c930533fbdc8" dependencies = [ "anyhow", "log", + "which", ] [[package]] name = "gpui_web" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d" +source = "git+https://github.com/zed-industries/zed#69b602c797a62f09318916d24a98c930533fbdc8" dependencies = [ "anyhow", "console_error_panic_hook", @@ -3058,7 +2940,7 @@ dependencies = [ [[package]] name = "gpui_wgpu" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d" +source = "git+https://github.com/zed-industries/zed#69b602c797a62f09318916d24a98c930533fbdc8" dependencies = [ "anyhow", "bytemuck", @@ -3087,15 +2969,17 @@ dependencies = [ [[package]] name = "gpui_windows" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d" +source = "git+https://github.com/zed-industries/zed#69b602c797a62f09318916d24a98c930533fbdc8" dependencies = [ "accesskit", "accesskit_windows", "anyhow", "collections", + "dunce", "etagere", "futures", "gpui", + "gpui_util", "image", "itertools 0.14.0", "log", @@ -3103,7 +2987,6 @@ dependencies = [ "rand 0.9.4", "raw-window-handle", "smallvec", - "util", "uuid", "windows 0.61.3", "windows-core 0.61.2", @@ -3119,9 +3002,9 @@ checksum = "b40ca9252762c466af32d0b1002e91e4e1bc5398f77455e55474deb466355ff5" [[package]] name = "h2" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" dependencies = [ "atomic-waker", "bytes", @@ -3154,10 +3037,10 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9da2e5ae821f6e96664977bf974d6d6a2d6682f9ccee23e62ec1d134246845f9" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "bytemuck", "core_maths", - "read-fonts", + "read-fonts 0.37.0", "smallvec", ] @@ -3170,15 +3053,6 @@ dependencies = [ "byteorder", ] -[[package]] -name = "hashbrown" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" -dependencies = [ - "ahash 0.4.8", -] - [[package]] name = "hashbrown" version = "0.14.5" @@ -3249,7 +3123,7 @@ version = "0.20.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d4f449bab7320c56003d37732a917e18798e2f1709d80263face2b4f9436ddb" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "byteorder", "heed-traits", "heed-types", @@ -3310,7 +3184,7 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ - "hmac", + "hmac 0.12.1", ] [[package]] @@ -3319,7 +3193,16 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", ] [[package]] @@ -3333,9 +3216,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" dependencies = [ "bytes", "itoa", @@ -3367,12 +3250,10 @@ dependencies = [ [[package]] name = "http_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d" +source = "git+https://github.com/zed-industries/zed#69b602c797a62f09318916d24a98c930533fbdc8" dependencies = [ "anyhow", "async-compression", - "async-fs", - "async-tar", "bytes", "derive_more", "futures", @@ -3383,16 +3264,13 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "sha2", - "tempfile", "url", - "util", ] [[package]] name = "http_client_tls" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d" +source = "git+https://github.com/zed-industries/zed#69b602c797a62f09318916d24a98c930533fbdc8" dependencies = [ "rustls", "rustls-platform-verifier", @@ -3404,6 +3282,15 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "hybrid-array" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "1.10.1" @@ -3439,7 +3326,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.7", + "webpki-roots 1.0.8", ] [[package]] @@ -3571,12 +3458,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - [[package]] name = "idna" version = "1.1.0" @@ -3640,9 +3521,9 @@ checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" [[package]] name = "imgref" -version = "1.12.1" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40fac9d56ed6437b198fddba683305e8e2d651aa42647f00f5ae542e7f5c94a2" +checksum = "89194689a993ab15268672e99e7b0e19da2da3268ac682e8f02d29d4d1434cd7" [[package]] name = "indexmap" @@ -3676,12 +3557,12 @@ dependencies = [ ] [[package]] -name = "instant" -version = "0.1.13" +name = "inout" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" dependencies = [ - "cfg-if", + "hybrid-array", ] [[package]] @@ -3765,6 +3646,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" @@ -3857,13 +3747,12 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.99" +version = "0.3.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" +checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" dependencies = [ "cfg-if", "futures-util", - "once_cell", "wasm-bindgen", ] @@ -3895,15 +3784,6 @@ dependencies = [ "smallvec", ] -[[package]] -name = "kv-log-macro" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" -dependencies = [ - "log", -] - [[package]] name = "lazy_static" version = "1.5.0" @@ -3925,12 +3805,6 @@ dependencies = [ "leak", ] -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - [[package]] name = "lebe" version = "0.5.3" @@ -3951,9 +3825,9 @@ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libfuzzer-sys" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d" +checksum = "a9fd2f41a1cba099f79a0b6b6c35656cf7c03351a7bae8ff0f28f25270f929d2" dependencies = [ "arbitrary", "cc", @@ -3981,10 +3855,10 @@ version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "libc", "plain", - "redox_syscall 0.8.0", + "redox_syscall 0.8.1", ] [[package]] @@ -3993,34 +3867,11 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4" -[[package]] -name = "linicon" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ee8c5653188a809616c97296180a0547a61dba205bcdcbdd261dbd022a25fd9" -dependencies = [ - "file-locker", - "freedesktop_entry_parser", - "linicon-theme", - "memmap2 0.5.10", - "thiserror 1.0.69", -] - -[[package]] -name = "linicon-theme" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4f8240c33bb08c5d8b8cdea87b683b05e61037aa76ff26bef40672cc6ecbb80" -dependencies = [ - "freedesktop_entry_parser", - "rust-ini", -] - [[package]] name = "link-section" -version = "0.18.1" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "014e440054ce8170890229eeef5bcda955305e056ec713de40ed366944483f09" +checksum = "c2b1dd6fe32e55c0fc0ea9493aa57459ca3cf4ff3c857c7d0302290150da6e4f" [[package]] name = "linkify" @@ -4083,9 +3934,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.30" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" dependencies = [ "serde_core", "value-bag", @@ -4177,12 +4028,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "mac" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" - [[package]] name = "mach2" version = "0.5.0" @@ -4227,13 +4072,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ "cfg-if", - "digest", + "digest 0.10.7", ] [[package]] name = "media" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d" +source = "git+https://github.com/zed-industries/zed#69b602c797a62f09318916d24a98c930533fbdc8" dependencies = [ "anyhow", "bindgen", @@ -4247,18 +4092,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.8.1" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" - -[[package]] -name = "memmap2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" -dependencies = [ - "libc", -] +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] name = "memmap2" @@ -4284,7 +4120,7 @@ version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7047791b5bc903b8cd963014b355f71dc9864a9a0b727057676c1dcae5cbc15" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "block", "core-graphics-types 0.2.0", "foreign-types", @@ -4353,7 +4189,7 @@ source = "git+https://github.com/zed-industries/wgpu.git?rev=357a0c56e0070480ad9 dependencies = [ "arrayvec", "bit-set", - "bitflags 2.11.1", + "bitflags 2.13.0", "cfg-if", "cfg_aliases", "codespan-reporting", @@ -4407,30 +4243,6 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" -[[package]] -name = "nix" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" -dependencies = [ - "bitflags 2.11.1", - "cfg-if", - "cfg_aliases", - "libc", -] - -[[package]] -name = "nix" -version = "0.31.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" -dependencies = [ - "bitflags 2.11.1", - "cfg-if", - "cfg_aliases", - "libc", -] - [[package]] name = "no_std_io2" version = "0.9.4" @@ -4467,8 +4279,8 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" [[package]] name = "nostr" -version = "0.44.1" -source = "git+https://github.com/rust-nostr/nostr?rev=46d66396467e07c3dfb71e5102104ecb7e9c6b64#46d66396467e07c3dfb71e5102104ecb7e9c6b64" +version = "0.45.0-alpha.1" +source = "git+https://github.com/rust-nostr/nostr#e47b5726edb1bb4964ffad84301982a0066aebb1" dependencies = [ "aes", "base64", @@ -4479,7 +4291,7 @@ dependencies = [ "chacha20", "chacha20poly1305", "faster-hex", - "rand 0.9.4", + "rand 0.10.1", "scrypt", "secp256k1", "serde", @@ -4491,8 +4303,8 @@ dependencies = [ [[package]] name = "nostr-blossom" -version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr?rev=46d66396467e07c3dfb71e5102104ecb7e9c6b64#46d66396467e07c3dfb71e5102104ecb7e9c6b64" +version = "0.45.0-alpha.1" +source = "git+https://github.com/rust-nostr/nostr#e47b5726edb1bb4964ffad84301982a0066aebb1" dependencies = [ "base64", "nostr", @@ -4502,8 +4314,8 @@ dependencies = [ [[package]] name = "nostr-connect" -version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr?rev=46d66396467e07c3dfb71e5102104ecb7e9c6b64#46d66396467e07c3dfb71e5102104ecb7e9c6b64" +version = "0.45.0-alpha.1" +source = "git+https://github.com/rust-nostr/nostr#e47b5726edb1bb4964ffad84301982a0066aebb1" dependencies = [ "async-utility", "futures-core", @@ -4515,8 +4327,8 @@ dependencies = [ [[package]] name = "nostr-database" -version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr?rev=46d66396467e07c3dfb71e5102104ecb7e9c6b64#46d66396467e07c3dfb71e5102104ecb7e9c6b64" +version = "0.45.0-alpha.1" +source = "git+https://github.com/rust-nostr/nostr#e47b5726edb1bb4964ffad84301982a0066aebb1" dependencies = [ "btreecap", "flatbuffers", @@ -4525,16 +4337,16 @@ dependencies = [ [[package]] name = "nostr-gossip" -version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr?rev=46d66396467e07c3dfb71e5102104ecb7e9c6b64#46d66396467e07c3dfb71e5102104ecb7e9c6b64" +version = "0.45.0-alpha.1" +source = "git+https://github.com/rust-nostr/nostr#e47b5726edb1bb4964ffad84301982a0066aebb1" dependencies = [ "nostr", ] [[package]] name = "nostr-gossip-memory" -version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr?rev=46d66396467e07c3dfb71e5102104ecb7e9c6b64#46d66396467e07c3dfb71e5102104ecb7e9c6b64" +version = "0.45.0-alpha.1" +source = "git+https://github.com/rust-nostr/nostr#e47b5726edb1bb4964ffad84301982a0066aebb1" dependencies = [ "indexmap", "lru", @@ -4545,8 +4357,8 @@ dependencies = [ [[package]] name = "nostr-lmdb" -version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr?rev=46d66396467e07c3dfb71e5102104ecb7e9c6b64#46d66396467e07c3dfb71e5102104ecb7e9c6b64" +version = "0.45.0-alpha.1" +source = "git+https://github.com/rust-nostr/nostr#e47b5726edb1bb4964ffad84301982a0066aebb1" dependencies = [ "async-utility", "flume 0.12.0", @@ -4559,8 +4371,8 @@ dependencies = [ [[package]] name = "nostr-sdk" -version = "0.44.1" -source = "git+https://github.com/rust-nostr/nostr?rev=46d66396467e07c3dfb71e5102104ecb7e9c6b64#46d66396467e07c3dfb71e5102104ecb7e9c6b64" +version = "0.45.0-alpha.1" +source = "git+https://github.com/rust-nostr/nostr#e47b5726edb1bb4964ffad84301982a0066aebb1" dependencies = [ "async-utility", "async-wsocket", @@ -4759,7 +4571,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "block2 0.5.1", "libc", "objc2 0.5.2", @@ -4775,7 +4587,9 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ + "bitflags 2.13.0", "objc2 0.6.4", + "objc2-core-foundation", "objc2-foundation 0.3.2", ] @@ -4785,7 +4599,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -4797,7 +4611,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "dispatch2", "objc2 0.6.4", ] @@ -4826,7 +4640,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "block2 0.5.1", "libc", "objc2 0.5.2", @@ -4838,7 +4652,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "objc2 0.6.4", "objc2-core-foundation", ] @@ -4849,7 +4663,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -4861,7 +4675,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "block2 0.6.2", "objc2 0.6.4", "objc2-foundation 0.3.2", @@ -4873,7 +4687,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -4886,7 +4700,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "objc2 0.6.4", "objc2-core-foundation", "objc2-foundation 0.3.2", @@ -4951,21 +4765,21 @@ dependencies = [ "async-lock", "blocking", "cbc", - "cipher", - "digest", + "cipher 0.4.4", + "digest 0.10.7", "endi", - "futures-lite 2.6.1", + "futures-lite", "futures-util", - "getrandom 0.4.2", + "getrandom 0.4.3", "hkdf", - "hmac", + "hmac 0.12.1", "md-5", "num", "num-bigint-dig", - "pbkdf2", + "pbkdf2 0.12.2", "serde", "serde_bytes", - "sha2", + "sha2 0.10.9", "subtle", "zbus", "zbus_macros", @@ -5011,16 +4825,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "ordered-multimap" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c672c7ad9ec066e428c00eb917124a06f08db19e2584de982cc34b1f4c12485" -dependencies = [ - "dlv-list", - "hashbrown 0.9.1", -] - [[package]] name = "ordered-stream" version = "0.2.0" @@ -5070,17 +4874,6 @@ dependencies = [ "windows-link 0.2.1", ] -[[package]] -name = "password-hash" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" -dependencies = [ - "base64ct", - "rand_core 0.6.4", - "subtle", -] - [[package]] name = "paste" version = "1.0.15" @@ -5124,8 +4917,18 @@ version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ - "digest", - "hmac", + "digest 0.10.7", + "hmac 0.12.1", +] + +[[package]] +name = "pbkdf2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629" +dependencies = [ + "digest 0.11.3", + "hmac 0.13.0", ] [[package]] @@ -5137,7 +4940,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perf" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d" +source = "git+https://github.com/zed-industries/zed#69b602c797a62f09318916d24a98c930533fbdc8" dependencies = [ "collections", "serde", @@ -5146,7 +4949,7 @@ dependencies = [ [[package]] name = "person" -version = "1.0.0-beta4" +version = "1.0.0-beta5" dependencies = [ "anyhow", "common", @@ -5168,7 +4971,7 @@ checksum = "9cd31dcfdbbd7431a807ef4df6edd6473228e94d5c805e8cf671227a21bad068" dependencies = [ "anyhow", "clap", - "itertools 0.14.0", + "itertools 0.15.0", "proc-macro2", "quote", "rand 0.8.6", @@ -5211,7 +5014,7 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" dependencies = [ - "fastrand 2.4.1", + "fastrand", "phf_shared 0.13.1", ] @@ -5291,12 +5094,6 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - [[package]] name = "piper" version = "0.2.5" @@ -5304,7 +5101,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" dependencies = [ "atomic-waker", - "fastrand 2.4.1", + "fastrand", "futures-io", ] @@ -5339,7 +5136,7 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "crc32fast", "fdeflate", "flate2", @@ -5378,7 +5175,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.17", "opaque-debug", "universal-hash", ] @@ -5524,7 +5321,7 @@ version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "getopts", "memchr", "pulldown-cmark-escape", @@ -5683,6 +5480,16 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "getrandom 0.4.3", + "rand_core 0.10.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -5721,6 +5528,12 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "range-alloc" version = "0.1.5" @@ -5833,12 +5646,13 @@ dependencies = [ ] [[package]] -name = "redox_syscall" -version = "0.2.16" +name = "read-fonts" +version = "0.39.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +checksum = "c4ed38b89c2c77ff968c524145ad65fb010f38af5c7a224b53b81d47ac2daa81" dependencies = [ - "bitflags 1.3.2", + "bytemuck", + "font-types", ] [[package]] @@ -5847,16 +5661,16 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", ] [[package]] name = "redox_syscall" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c7591fa2c6b601dfcfe5f043f65a1c39fcdf50efefcd7f1572e538c1f4b398d" +checksum = "5b44b894f2a6e36457d665d1e08c3866add6ed5e70050c1b4ba8a8ddedb02ce7" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", ] [[package]] @@ -5904,16 +5718,16 @@ dependencies = [ [[package]] name = "refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d" +source = "git+https://github.com/zed-industries/zed#69b602c797a62f09318916d24a98c930533fbdc8" dependencies = [ "derive_refineable", ] [[package]] name = "regex" -version = "1.12.3" +version = "1.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" dependencies = [ "aho-corasick", "memchr", @@ -5934,13 +5748,13 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "relay_auth" -version = "1.0.0-beta4" +version = "1.0.0-beta5" dependencies = [ "anyhow", "common", @@ -5971,6 +5785,7 @@ dependencies = [ "base64", "bytes", "futures-core", + "h2", "http", "http-body", "http-body-util", @@ -5997,13 +5812,13 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 1.0.7", + "webpki-roots 1.0.8", ] [[package]] name = "reqwest_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d" +source = "git+https://github.com/zed-industries/zed#69b602c797a62f09318916d24a98c930533fbdc8" dependencies = [ "anyhow", "bytes", @@ -6104,20 +5919,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1" dependencies = [ "globset", - "sha2", + "sha2 0.10.9", "walkdir", ] -[[package]] -name = "rust-ini" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63471c4aa97a1cf8332a5f97709a79a4234698de6a1f5087faf66f2dae810e22" -dependencies = [ - "cfg-if", - "ordered-multimap", -] - [[package]] name = "rustc-demangle" version = "0.1.27" @@ -6151,7 +5956,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "errno", "libc", "linux-raw-sys 0.4.15", @@ -6164,7 +5969,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "errno", "libc", "linux-raw-sys 0.12.1", @@ -6189,9 +5994,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -6269,7 +6074,7 @@ version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "bytemuck", "core_maths", "log", @@ -6289,11 +6094,12 @@ checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "salsa20" -version = "0.10.2" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +checksum = "2f874456e72520ff1375a06c588eaf074b0f01f9e9e1aada45bd9b7954a6e42c" dependencies = [ - "cipher", + "cfg-if", + "cipher 0.5.2", ] [[package]] @@ -6317,7 +6123,7 @@ dependencies = [ [[package]] name = "scheduler" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d" +source = "git+https://github.com/zed-industries/zed#69b602c797a62f09318916d24a98c930533fbdc8" dependencies = [ "async-task", "backtrace", @@ -6392,14 +6198,14 @@ dependencies = [ [[package]] name = "scrypt" -version = "0.11.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +checksum = "d87af57419b594aa23fa95f09f0e06d80d84ba01c26148c43844cad6ff4485f0" dependencies = [ - "password-hash", - "pbkdf2", + "cfg-if", + "pbkdf2 0.13.0", "salsa20", - "sha2", + "sha2 0.11.0", ] [[package]] @@ -6433,7 +6239,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "core-foundation 0.10.0", "core-foundation-sys", "libc", @@ -6540,19 +6346,6 @@ dependencies = [ "zmij", ] -[[package]] -name = "serde_json_lenient" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e033097bf0d2b59a62b42c18ebbb797503839b26afdda2c4e1415cb6c813540" -dependencies = [ - "indexmap", - "itoa", - "memchr", - "ryu", - "serde", -] - [[package]] name = "serde_repr" version = "0.1.20" @@ -6596,7 +6389,7 @@ dependencies = [ [[package]] name = "settings" -version = "1.0.0-beta4" +version = "1.0.0-beta5" dependencies = [ "anyhow", "common", @@ -6618,8 +6411,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", ] [[package]] @@ -6635,8 +6428,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] @@ -6723,7 +6527,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fbdfe3d2475fbd7ddd1f3e5cf8288a30eb3e5f95832829570cd88115a7434ac" dependencies = [ "bytemuck", - "read-fonts", + "read-fonts 0.37.0", +] + +[[package]] +name = "skrifa" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c34617370ae968efb7161bb2beb517d9084659aae19e24b89e3db25b46e4564" +dependencies = [ + "bytemuck", + "read-fonts 0.39.2", ] [[package]] @@ -6743,9 +6557,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "smol" @@ -6753,7 +6567,7 @@ version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a33bd3e260892199c3ccfc487c88b2da2265080acb316cd920da72fdfd7c599f" dependencies = [ - "async-channel 2.5.0", + "async-channel", "async-executor", "async-fs", "async-io", @@ -6761,7 +6575,7 @@ dependencies = [ "async-net", "async-process", "blocking", - "futures-lite 2.6.1", + "futures-lite", ] [[package]] @@ -6808,7 +6622,7 @@ version = "0.4.0+sdk-1.4.341.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9571ea910ebd84c86af4b3ed27f9dbdc6ad06f17c5f96146b2b671e2976744f" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", ] [[package]] @@ -6853,7 +6667,7 @@ dependencies = [ [[package]] name = "state" -version = "1.0.0-beta4" +version = "1.0.0-beta5" dependencies = [ "anyhow", "common", @@ -6940,7 +6754,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sum_tree" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d" +source = "git+https://github.com/zed-industries/zed#69b602c797a62f09318916d24a98c930533fbdc8" dependencies = [ "heapless 0.9.3", "log", @@ -7045,20 +6859,20 @@ dependencies = [ [[package]] name = "swash" -version = "0.2.7" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "842f3cd369c2ba38966204f983eaa5e54a8e84a7d7159ed36ade2b6c335aae64" +checksum = "0811b01ca2c4e8718760713911feaf4675c24f94e50530a015ec646cfb622f7c" dependencies = [ - "skrifa", + "skrifa 0.42.1", "yazi", "zeno", ] [[package]] name = "syn" -version = "2.0.117" +version = "2.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" dependencies = [ "proc-macro2", "quote", @@ -7123,7 +6937,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -7150,12 +6964,6 @@ dependencies = [ "slotmap", ] -[[package]] -name = "take-until" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bdb6fa0dfa67b38c1e66b7041ba9dcf23b99d8121907cd31c807a332f7a0bbb" - [[package]] name = "tao-core-video-sys" version = "0.2.0" @@ -7174,24 +6982,13 @@ version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ - "fastrand 2.4.1", - "getrandom 0.4.2", + "fastrand", + "getrandom 0.4.3", "once_cell", "rustix 1.1.4", "windows-sys 0.61.2", ] -[[package]] -name = "tendril" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" -dependencies = [ - "futf", - "mac", - "utf-8", -] - [[package]] name = "termcolor" version = "1.4.1" @@ -7203,7 +7000,7 @@ dependencies = [ [[package]] name = "theme" -version = "1.0.0-beta4" +version = "1.0.0-beta5" dependencies = [ "anyhow", "gpui", @@ -7338,23 +7135,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" -[[package]] -name = "title_bar" -version = "1.0.0-beta4" -dependencies = [ - "anyhow", - "common", - "gpui", - "linicon", - "log", - "nostr-sdk", - "smallvec", - "smol", - "theme", - "ui", - "windows 0.61.3", -] - [[package]] name = "tokio" version = "1.52.3" @@ -7370,6 +7150,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "tokio-happy-eyeballs" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8564c32dfb6f4257f8bc6edfc178a34af97520e0b7b9815500c55eb3d092f29f" +dependencies = [ + "tokio", +] + [[package]] name = "tokio-macros" version = "2.7.0" @@ -7557,7 +7346,7 @@ version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "bytes", "futures-util", "http", @@ -7733,7 +7522,7 @@ dependencies = [ [[package]] name = "ui" -version = "1.0.0-beta4" +version = "1.0.0-beta5" dependencies = [ "anyhow", "common", @@ -7814,9 +7603,9 @@ checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" [[package]] name = "unicode-segmentation" -version = "1.13.2" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "unicode-vo" @@ -7842,7 +7631,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "subtle", ] @@ -7922,49 +7711,10 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" -[[package]] -name = "util" -version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d" -dependencies = [ - "anyhow", - "async-fs", - "async_zip", - "collections", - "command-fds", - "dirs 6.0.0", - "dunce", - "futures", - "futures-lite 1.13.0", - "globset", - "gpui_util", - "itertools 0.14.0", - "libc", - "log", - "mach2", - "nix 0.29.0", - "percent-encoding", - "regex", - "rust-embed", - "schemars", - "serde", - "serde_json", - "serde_json_lenient", - "shlex 1.3.0", - "smol", - "take-until", - "tempfile", - "tendril", - "unicase", - "url", - "walkdir", - "which", -] - [[package]] name = "util_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d" +source = "git+https://github.com/zed-industries/zed#69b602c797a62f09318916d24a98c930533fbdc8" dependencies = [ "perf", "quote", @@ -7973,11 +7723,11 @@ dependencies = [ [[package]] name = "uuid" -version = "1.23.2" +version = "1.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7" +checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" dependencies = [ - "getrandom 0.4.2", + "getrandom 0.4.3", "js-sys", "serde_core", "sha1_smol", @@ -8096,20 +7846,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.3+wasi-0.2.9" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ - "wit-bindgen 0.57.1", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen 0.51.0", + "wit-bindgen", ] [[package]] @@ -8120,9 +7861,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.122" +version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" +checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" dependencies = [ "cfg-if", "once_cell", @@ -8133,9 +7874,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.72" +version = "0.4.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" +checksum = "503b14d284f2c8dac03b819967e155ea753f573586193b2b2c95990cb5d69280" dependencies = [ "js-sys", "wasm-bindgen", @@ -8143,9 +7884,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.122" +version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" +checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -8153,9 +7894,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.122" +version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" +checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" dependencies = [ "bumpalo", "proc-macro2", @@ -8166,35 +7907,13 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.122" +version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" +checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - [[package]] name = "wasm-streams" version = "0.4.2" @@ -8220,18 +7939,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags 2.11.1", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - [[package]] name = "wayland-backend" version = "0.3.15" @@ -8252,7 +7959,7 @@ version = "0.31.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "rustix 1.1.4", "wayland-backend", "wayland-scanner", @@ -8275,7 +7982,7 @@ version = "0.32.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "563a85523cade2429938e790815fd7319062103b9f4a2dc806e9b53b95982d8f" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "wayland-backend", "wayland-client", "wayland-scanner", @@ -8287,7 +7994,7 @@ version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -8300,7 +8007,7 @@ version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -8332,9 +8039,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.99" +version = "0.3.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" +checksum = "a6430a72df5eb332242960fe84b3002a241163998241eb596d4f739b9757061d" dependencies = [ "js-sys", "wasm-bindgen", @@ -8372,14 +8079,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e" dependencies = [ - "webpki-root-certs 1.0.7", + "webpki-root-certs 1.0.8", ] [[package]] name = "webpki-root-certs" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" +checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267" dependencies = [ "rustls-pki-types", ] @@ -8390,14 +8097,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.7", + "webpki-roots 1.0.8", ] [[package]] name = "webpki-roots" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf" dependencies = [ "rustls-pki-types", ] @@ -8414,7 +8121,7 @@ version = "29.0.3" source = "git+https://github.com/zed-industries/wgpu.git?rev=357a0c56e0070480ad9daea5d2eaa83150b79e88#357a0c56e0070480ad9daea5d2eaa83150b79e88" dependencies = [ "arrayvec", - "bitflags 2.11.1", + "bitflags 2.13.0", "bytemuck", "cfg-if", "cfg_aliases", @@ -8445,7 +8152,7 @@ dependencies = [ "arrayvec", "bit-set", "bit-vec", - "bitflags 2.11.1", + "bitflags 2.13.0", "bytemuck", "cfg_aliases", "document-features", @@ -8502,7 +8209,7 @@ dependencies = [ "arrayvec", "ash", "bit-set", - "bitflags 2.11.1", + "bitflags 2.13.0", "block2 0.6.2", "bytemuck", "cfg-if", @@ -8560,7 +8267,7 @@ name = "wgpu-types" version = "29.0.3" source = "git+https://github.com/zed-industries/wgpu.git?rev=357a0c56e0070480ad9daea5d2eaa83150b79e88#357a0c56e0070480ad9daea5d2eaa83150b79e88" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "bytemuck", "js-sys", "log", @@ -9258,100 +8965,12 @@ dependencies = [ "winapi", ] -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - [[package]] name = "wit-bindgen" version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck 0.5.0", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck 0.5.0", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags 2.11.1", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - [[package]] name = "writeable" version = "0.6.3" @@ -9398,15 +9017,6 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" -[[package]] -name = "xattr" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc" -dependencies = [ - "libc", -] - [[package]] name = "xcb" version = "1.7.0" @@ -9438,7 +9048,7 @@ name = "xim-parser" version = "0.2.1" source = "git+https://github.com/zed-industries/xim-rs.git?rev=16f35a2c881b815a2b6cdfd6687988e84f8447d8#16f35a2c881b815a2b6cdfd6687988e84f8447d8" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", ] [[package]] @@ -9449,7 +9059,7 @@ checksum = "8d66ca9352cbd4eecbbc40871d8a11b4ac8107cfc528a6e14d7c19c69d0e1ac9" dependencies = [ "as-raw-xcb-connection", "libc", - "memmap2 0.9.10", + "memmap2", "xkeysym", ] @@ -9496,9 +9106,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -9533,9 +9143,9 @@ dependencies = [ "async-trait", "blocking", "enumflags2", - "event-listener 5.4.1", + "event-listener", "futures-core", - "futures-lite 2.6.1", + "futures-lite", "hex", "libc", "ordered-stream", @@ -9619,7 +9229,7 @@ name = "zed-font-kit" version = "0.14.1-zed" source = "git+https://github.com/zed-industries/font-kit?rev=94b0f28166665e8fd2f53ff6d268a14955c82269#94b0f28166665e8fd2f53ff6d268a14955c82269" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "byteorder", "core-foundation 0.10.0", "core-graphics 0.24.0", @@ -9713,7 +9323,7 @@ name = "zed-xim" version = "0.4.0-zed" source = "git+https://github.com/zed-industries/xim-rs.git?rev=16f35a2c881b815a2b6cdfd6687988e84f8447d8#16f35a2c881b815a2b6cdfd6687988e84f8447d8" dependencies = [ - "ahash 0.8.12", + "ahash", "hashbrown 0.14.5", "log", "x11rb", @@ -9729,18 +9339,18 @@ checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" [[package]] name = "zerocopy" -version = "0.8.50" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.50" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" dependencies = [ "proc-macro2", "quote", @@ -9770,18 +9380,18 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" dependencies = [ "zeroize_derive", ] [[package]] name = "zeroize_derive" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" dependencies = [ "proc-macro2", "quote", @@ -9824,7 +9434,7 @@ dependencies = [ [[package]] name = "zlog" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d" +source = "git+https://github.com/zed-industries/zed#69b602c797a62f09318916d24a98c930533fbdc8" dependencies = [ "anyhow", "chrono", @@ -9841,7 +9451,7 @@ checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" [[package]] name = "ztracing" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d" +source = "git+https://github.com/zed-industries/zed#69b602c797a62f09318916d24a98c930533fbdc8" dependencies = [ "tracing", "tracing-subscriber", @@ -9852,7 +9462,7 @@ dependencies = [ [[package]] name = "ztracing_macro" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d" +source = "git+https://github.com/zed-industries/zed#69b602c797a62f09318916d24a98c930533fbdc8" [[package]] name = "zune-core" diff --git a/Cargo.toml b/Cargo.toml index b53f547..ca1839a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,12 +19,12 @@ gpui_tokio = { git = "https://github.com/zed-industries/zed" } reqwest_client = { git = "https://github.com/zed-industries/zed" } # Nostr -nostr-lmdb = { git = "https://github.com/rust-nostr/nostr", rev = "46d66396467e07c3dfb71e5102104ecb7e9c6b64" } -nostr-connect = { git = "https://github.com/rust-nostr/nostr", rev = "46d66396467e07c3dfb71e5102104ecb7e9c6b64" } -nostr-blossom = { git = "https://github.com/rust-nostr/nostr", rev = "46d66396467e07c3dfb71e5102104ecb7e9c6b64" } -nostr-gossip-memory = { git = "https://github.com/rust-nostr/nostr", rev = "46d66396467e07c3dfb71e5102104ecb7e9c6b64" } -nostr-sdk = { git = "https://github.com/rust-nostr/nostr", rev = "46d66396467e07c3dfb71e5102104ecb7e9c6b64" } -nostr = { git = "https://github.com/rust-nostr/nostr", features = [ "nip96", "nip59", "nip49", "nip44" ], rev = "46d66396467e07c3dfb71e5102104ecb7e9c6b64" } +nostr-lmdb = { git = "https://github.com/rust-nostr/nostr", } +nostr-connect = { git = "https://github.com/rust-nostr/nostr" } +nostr-blossom = { git = "https://github.com/rust-nostr/nostr" } +nostr-gossip-memory = { git = "https://github.com/rust-nostr/nostr" } +nostr-sdk = { git = "https://github.com/rust-nostr/nostr" } +nostr = { git = "https://github.com/rust-nostr/nostr", features = [ "nip59", "nip49", "nip44" ] } # Others anyhow = "1.0.44" @@ -34,7 +34,7 @@ itertools = "0.13.0" log = "0.4" oneshot = "0.1.10" flume = { version = "0.11.1", default-features = false, features = ["async", "select"] } -rust-embed = "8.5.0" +rust-embed = { version = "8.5", features = ["include-exclude"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" schemars = "1" diff --git a/crates/chat/src/lib.rs b/crates/chat/src/lib.rs index a33dcf0..a1c135a 100644 --- a/crates/chat/src/lib.rs +++ b/crates/chat/src/lib.rs @@ -7,7 +7,6 @@ use std::time::Duration; use anyhow::{Context as AnyhowContext, Error, anyhow}; use common::EventExt; -use device::{DeviceEvent, DeviceRegistry}; use fuzzy_matcher::FuzzyMatcher; use fuzzy_matcher::skim::SkimMatcherV2; use gpui::{ @@ -17,7 +16,7 @@ use gpui::{ use nostr_sdk::prelude::*; use smallvec::{SmallVec, smallvec}; use smol::lock::RwLock; -use state::{CoopSigner, DEVICE_GIFTWRAP, NostrRegistry, StateEvent, TIMEOUT, USER_GIFTWRAP}; +use state::{DEVICE_GIFTWRAP, NostrRegistry, USER_GIFTWRAP}; mod message; mod room; @@ -42,6 +41,8 @@ pub enum ChatEvent { CloseRoom(u64), /// An event to notify UI about a new chat request Ping, + /// No Inbox Relays found, the app is not ready to subscribe messages + InboxRelayNotFound, /// An error occurred Error(SharedString), } @@ -49,6 +50,10 @@ pub enum ChatEvent { /// Channel signal. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] enum Signal { + /// Inbox Relays found, the app is ready to subscribe messages + InboxReady(Box), + /// No Inbox Relays found, the app is not ready to subscribe messages + InboxRelayNotFound, /// Message received from relay pool Message(NewMessage), /// Eose received from relay pool @@ -62,6 +67,14 @@ impl Signal { Self::Message(NewMessage::new(gift_wrap, rumor)) } + pub fn inbox_ready(event: &Event) -> Self { + Self::InboxReady(Box::new(event.to_owned())) + } + + pub fn inbox_relay_not_found() -> Self { + Self::InboxRelayNotFound + } + pub fn eose() -> Self { Self::Eose } @@ -74,15 +87,9 @@ impl Signal { } } -type Dekey = bool; -type GiftWrapId = EventId; - /// Chat Registry #[derive(Debug)] pub struct ChatRegistry { - /// Whether the chat registry is currently initializing. - pub initializing: bool, - /// Chat rooms rooms: Vec>, @@ -93,10 +100,13 @@ pub struct ChatRegistry { seens: Arc>>>, /// Mapping of unwrapped event ids to their gift wrap event ids - event_map: Arc>>, + event_map: Arc>>, /// Tracking the status of unwrapping gift wrap events. - tracking_flag: Arc, + tracking: Arc, + + /// Whether the messaging relays have been found. + msg_relays_existed: Arc, /// Channel for sending signals to the UI. signal_tx: flume::Sender, @@ -127,66 +137,36 @@ impl ChatRegistry { /// Create a new chat registry instance fn new(window: &mut Window, cx: &mut Context) -> Self { let nostr = NostrRegistry::global(cx); - let device = DeviceRegistry::global(cx); + let user_signer = nostr.read(cx).signer.clone(); let (tx, rx) = flume::unbounded::(); let mut subscriptions = smallvec![]; subscriptions.push( // Subscribe to the signer event - cx.subscribe_in(&nostr, window, |this, state, event, window, cx| { - if event == &StateEvent::SignerSet { + cx.observe(&user_signer, |this, signer, cx| { + if let Some(keys) = signer.read(cx).clone() { this.reset(cx); - this.get_contact_list(cx); + this.handle_notifications(keys, cx); + this.get_metadata(cx); this.get_rooms(cx); - - let signer = state.read(cx).signer(); - cx.spawn_in(window, async move |this, cx| { - let user_signer = signer.get().await; - this.update(cx, |this, cx| { - this.get_messages(user_signer, cx); - }) - .ok(); - }) - .detach(); - }; - }), - ); - - subscriptions.push( - // Subscribe to the device event - cx.subscribe_in(&device, window, |_this, _s, event, window, cx| { - if event == &DeviceEvent::Set { - let nostr = NostrRegistry::global(cx); - let signer = nostr.read(cx).signer(); - - cx.spawn_in(window, async move |this, cx| { - if let Some(device_signer) = signer.get_encryption_signer().await { - this.update(cx, |this, cx| { - this.get_messages(device_signer, cx); - }) - .ok(); - } - }) - .detach(); }; }), ); // Run at the end of the current cycle cx.defer_in(window, |this, _window, cx| { - this.get_rooms(cx); - this.handle_notifications(cx); this.tracking(cx); + this.get_rooms(cx); }); Self { - initializing: true, rooms: vec![], trashes: cx.new(|_| BTreeSet::default()), seens: Arc::new(RwLock::new(HashMap::default())), event_map: Arc::new(RwLock::new(HashMap::default())), - tracking_flag: Arc::new(AtomicBool::new(false)), + tracking: Arc::new(AtomicBool::new(false)), + msg_relays_existed: Arc::new(AtomicBool::new(false)), signal_rx: rx, signal_tx: tx, tasks: smallvec![], @@ -195,11 +175,13 @@ impl ChatRegistry { } /// Handle nostr notifications - fn handle_notifications(&mut self, cx: &mut Context) { + fn handle_notifications(&mut self, signer: Keys, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let signer = nostr.read(cx).signer(); - let status = self.tracking_flag.clone(); + + let tracking = self.tracking.clone(); + let msg_relays_existed = self.msg_relays_existed.clone(); + let seens = self.seens.clone(); let event_map = self.event_map.clone(); let trashes = self.trashes.downgrade(); @@ -223,10 +205,7 @@ impl ChatRegistry { }; match *message { - RelayMessage::Event { - event, - subscription_id, - } => { + RelayMessage::Event { event, .. } => { // Keep track of which relays have seen this event { let mut seens = seens.write().await; @@ -238,6 +217,18 @@ impl ChatRegistry { continue; } + // Handle msg relays event to determine when the app is ready to subscribe + if event.kind == Kind::InboxRelays { + let current_user = signer.get_public_key_async().await?; + + if event.pubkey == current_user { + // Mark that the msg relays have been found + msg_relays_existed.store(true, Ordering::Release); + // Emit the inbox ready signal + tx.send_async(Signal::inbox_ready(&event)).await?; + } + } + // Skip non-gift wrap events if event.kind != Kind::GiftWrap { continue; @@ -249,21 +240,12 @@ impl ChatRegistry { // Map the rumor id to the gift wrap event id for later lookup { let mut event_map = event_map.write().await; - let dekey = subscription_id.as_ref() == &sub_id1; - event_map.insert(rumor.id.unwrap(), (event.id, dekey)); - } - - if rumor.kind != Kind::PrivateDirectMessage - || rumor.kind != Kind::Custom(15) - { - log::info!("Rumor is not releated to NIP17"); - continue; + event_map.insert(rumor.id.unwrap(), event.id); } // Check if the rumor has a recipient if rumor.tags.is_empty() { - let signal = - Signal::error(event.as_ref(), "Recipient is missing"); + let signal = Signal::error(&event, "Recipient is missing"); tx.send_async(signal).await?; } @@ -273,7 +255,7 @@ impl ChatRegistry { tx.send_async(signal).await?; } else { // Mark the chat still processing new messages - status.store(true, Ordering::Release); + tracking.store(true, Ordering::Release); } } Err(e) => { @@ -283,10 +265,10 @@ impl ChatRegistry { } } } - RelayMessage::EndOfStoredEvents(id) => { - if id.as_ref() == &sub_id1 || id.as_ref() == &sub_id2 { - tx.send_async(Signal::eose()).await?; - } + RelayMessage::EndOfStoredEvents(id) + if (id.as_ref() == &sub_id1 || id.as_ref() == &sub_id2) => + { + tx.send_async(Signal::eose()).await?; } _ => {} } @@ -303,6 +285,16 @@ impl ChatRegistry { this.new_message(message, cx); })?; } + Signal::InboxReady(event) => { + this.update(cx, |this, cx| { + this.get_messages(&event, cx); + })?; + } + Signal::InboxRelayNotFound => { + this.update(cx, |_this, cx| { + cx.emit(ChatEvent::InboxRelayNotFound); + })?; + } Signal::Eose => { this.update(cx, |this, cx| { this.get_rooms(cx); @@ -323,7 +315,7 @@ impl ChatRegistry { /// Tracking the status of unwrapping gift wrap events. fn tracking(&mut self, cx: &mut Context) { - let status = self.tracking_flag.clone(); + let status = self.tracking.clone(); let tx = self.signal_tx.clone(); self.tasks.push(cx.background_spawn(async move { @@ -341,107 +333,71 @@ impl ChatRegistry { })); } - /// Get contact list from relays - fn get_contact_list(&mut self, cx: &mut Context) { + /// Get all necessary metadata from relays for current user + pub fn get_metadata(&mut self, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let signer = nostr.read(cx).signer(); - let Some(public_key) = signer.public_key() else { + let Some(public_key) = nostr.read(cx).signer_pubkey(cx) else { return; }; - let task: Task> = cx.background_spawn(async move { - let id = SubscriptionId::new("contact-list"); - let opts = SubscribeAutoCloseOptions::default() - .exit_policy(ReqExitPolicy::ExitOnEOSE) - .timeout(Some(Duration::from_secs(TIMEOUT))); + self.tasks.push(cx.background_spawn(async move { + let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); - // Construct filter for inbox relays - let filter = Filter::new() + // Construct filter for msg relays + let msg_relays = Filter::new() + .kind(Kind::InboxRelays) + .author(public_key) + .limit(1); + + // Construct filter for contact list + let contact_list = Filter::new() .kind(Kind::ContactList) .author(public_key) .limit(1); // Subscribe - client.subscribe(filter).close_on(opts).with_id(id).await?; + client + .subscribe(vec![msg_relays, contact_list]) + .close_on(opts) + .await?; Ok(()) - }); + })); - self.tasks.push(task); - } + let tx = self.signal_tx.clone(); + let msg_relays_existed = self.msg_relays_existed.clone(); - /// Get all messages for the provided signer - fn get_messages(&mut self, signer: T, cx: &mut Context) - where - T: NostrSigner + 'static, - { - let task = self.subscribe_gift_wrap_events(signer, cx); + // Reset the status flag + msg_relays_existed.store(false, Ordering::Release); - self.tasks.push(cx.spawn(async move |this, cx| { - match task.await { - Ok(_) => { - this.update(cx, |this, cx| { - this.set_initializing(false, cx); - })?; - } - Err(e) => { - this.update(cx, |_this, cx| { - cx.emit(ChatEvent::Error(SharedString::from(e.to_string()))); - })?; - } + // Wait for the msg relays to be found or timeout + self.tasks.push(cx.background_spawn(async move { + // Wait for 5 seconds + smol::Timer::after(Duration::from_secs(5)).await; + + // Then check if the msg relays have been found + if !msg_relays_existed.load(Ordering::Acquire) { + tx.send_async(Signal::inbox_relay_not_found()).await?; } + Ok(()) })); } - // Get messaging relay list for current user - fn get_messaging_relays(&self, cx: &App) -> Task, Error>> { + /// Get all messages for the provided signer + fn get_messages(&mut self, msg_relays: &Event, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let signer = nostr.read(cx).signer(); + let urls: Vec = nip17::extract_relay_list(msg_relays).collect(); - cx.background_spawn(async move { - let public_key = signer.get_public_key().await?; - let id = SubscriptionId::new("inbox-relay"); + let Some(signer) = nostr.read(cx).signer(cx) else { + return; + }; - // Construct filter for inbox relays - let filter = Filter::new() - .kind(Kind::InboxRelays) - .author(public_key) - .limit(1); - - // Stream events from user's write relays - let mut stream = client - .stream_events(filter) - .with_id(id) - .timeout(Duration::from_secs(TIMEOUT)) - .await?; - - while let Some((_url, res)) = stream.next().await { - if let Ok(event) = res { - let urls: Vec = nip17::extract_owned_relay_list(event).collect(); - return Ok(urls); - } - } - - Err(anyhow!("Messaging Relays not found")) - }) - } - - /// Continuously get gift wrap events for the signer - fn subscribe_gift_wrap_events(&self, signer: T, cx: &App) -> Task> - where - T: NostrSigner + 'static, - { - let nostr = NostrRegistry::global(cx); - let client = nostr.read(cx).client(); - let urls = self.get_messaging_relays(cx); - - cx.background_spawn(async move { - let urls = urls.await?; - let public_key = signer.get_public_key().await?; + let task: Task> = cx.background_spawn(async move { + let public_key = signer.get_public_key_async().await?; let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key); let id = SubscriptionId::new(format!("{}-msg", public_key.to_hex())); @@ -464,43 +420,28 @@ impl ChatRegistry { ); Ok(()) - }) + }); + + self.tasks.push(cx.spawn(async move |this, cx| { + if let Err(e) = task.await { + this.update(cx, |_this, cx| { + cx.emit(ChatEvent::Error(SharedString::from(e.to_string()))); + })?; + } + Ok(()) + })); } /// Refresh the chat registry, fetching messages and contact list from relays. - pub fn refresh(&mut self, window: &mut Window, cx: &mut Context) { + pub fn refresh(&mut self, cx: &mut Context) { self.reset(cx); - self.get_contact_list(cx); + self.get_metadata(cx); self.get_rooms(cx); - - let nostr = NostrRegistry::global(cx); - let signer = nostr.read(cx).signer(); - - cx.spawn_in(window, async move |this, cx| { - let user_signer = signer.get().await; - let device_signer = signer.get_encryption_signer().await; - - this.update(cx, |this, cx| { - this.get_messages(user_signer, cx); - - if let Some(device_signer) = device_signer { - this.get_messages(device_signer, cx); - } - }) - .ok(); - }) - .detach(); - } - - /// Set the initializing status of the chat registry - fn set_initializing(&mut self, initializing: bool, cx: &mut Context) { - self.initializing = initializing; - cx.notify(); } /// Get the loading status of the chat registry pub fn loading(&self) -> bool { - self.tracking_flag.load(Ordering::Acquire) + self.tracking.load(Ordering::Acquire) } /// Get a weak reference to a room by its ID. @@ -552,7 +493,7 @@ impl ChatRegistry { self.event_map .read_blocking() .get(id) - .map(|(id, _dekey)| self.seen_on(id)) + .map(|id| self.seen_on(id)) } /// Get the relays that have seen a given gift wrap id. @@ -564,26 +505,18 @@ impl ChatRegistry { .unwrap_or_default() } - /// Check if a given rumor was encrypted by the dekey. - pub fn encrypted_by_dekey(&self, id: &EventId) -> bool { - self.event_map - .read_blocking() - .get(id) - .map(|(_, dekey)| *dekey) - .unwrap_or(false) - } - /// Add a new room to the start of list. pub fn add_room(&mut self, room: I, cx: &mut Context) where I: Into + 'static, { let nostr = NostrRegistry::global(cx); - let client = nostr.read(cx).client(); + + let Some(public_key) = nostr.read(cx).signer_pubkey(cx) else { + return; + }; cx.spawn(async move |this, cx| { - let signer = client.signer()?; - let public_key = signer.get_public_key().await.ok()?; let room: Room = room.into().organize(&public_key); this.update(cx, |this, cx| { @@ -650,7 +583,6 @@ impl ChatRegistry { /// Reset the registry. pub fn reset(&mut self, cx: &mut Context) { - self.initializing = true; self.rooms.clear(); self.trashes.update(cx, |this, cx| { this.clear(); @@ -689,7 +621,13 @@ impl ChatRegistry { /// Load all rooms from the database. pub fn get_rooms(&mut self, cx: &mut Context) { - let task = self.get_rooms_from_database(cx); + let nostr = NostrRegistry::global(cx); + + let Some(public_key) = nostr.read(cx).signer_pubkey(cx) else { + return; + }; + + let task = self.get_rooms_from_database(public_key, cx); self.tasks.push(cx.spawn(async move |this, cx| { match task.await { @@ -709,14 +647,15 @@ impl ChatRegistry { } /// Create a task to load rooms from the database - fn get_rooms_from_database(&self, cx: &App) -> Task, Error>> { + fn get_rooms_from_database( + &self, + public_key: PublicKey, + cx: &App, + ) -> Task, Error>> { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); cx.background_spawn(async move { - let signer = client.signer().context("Signer not found")?; - let public_key = signer.get_public_key().await?; - // Get contacts let contacts = client .database() @@ -793,15 +732,15 @@ impl ChatRegistry { /// Updates room ordering based on the most recent messages. pub fn new_message(&mut self, message: NewMessage, cx: &mut Context) { let nostr = NostrRegistry::global(cx); - let signer = nostr.read(cx).signer(); + + let Some(public_key) = nostr.read(cx).signer_pubkey(cx) else { + return; + }; match self.rooms.iter().find(|e| e.read(cx).id == message.room) { Some(room) => { room.update(cx, |this, cx| { - if this.kind == RoomKind::Request - && let Some(public_key) = signer.public_key() - && message.rumor.pubkey == public_key - { + if this.kind == RoomKind::Request && message.rumor.pubkey == public_key { this.set_ongoing(cx); } this.push_message(message, cx); @@ -830,7 +769,7 @@ impl ChatRegistry { /// Unwraps a gift-wrapped event and processes its contents. async fn extract_rumor( client: &Client, - signer: &Arc, + signer: &Keys, gift_wrap: &Event, ) -> Result { // Try to get cached rumor first @@ -854,8 +793,9 @@ async fn extract_rumor( } /// Helper method to try unwrapping with different signers -async fn try_unwrap(signer: &Arc, gift_wrap: &Event) -> Result { - // Try with the device signer first +async fn try_unwrap(signer: &Keys, gift_wrap: &Event) -> Result { + /* + * // Try with the device signer first if let Some(signer) = signer.get_encryption_signer().await { log::info!("trying with encryption key"); if let Ok(unwrapped) = try_unwrap_with(gift_wrap, &signer).await { @@ -865,19 +805,17 @@ async fn try_unwrap(signer: &Arc, gift_wrap: &Event) -> Result(gift_wrap: &Event, signer: &T) -> Result -where - T: NostrSigner + 'static, -{ +async fn try_unwrap_with(gift_wrap: &Event, signer: &Keys) -> Result { // Get the sealed event let seal = signer - .nip44_decrypt(&gift_wrap.pubkey, &gift_wrap.content) + .nip44_decrypt_async(&gift_wrap.pubkey, &gift_wrap.content) .await?; // Verify the sealed event @@ -885,7 +823,10 @@ where seal.verify_with_ctx(&SECP256K1)?; // Get the rumor event - let rumor = signer.nip44_decrypt(&seal.pubkey, &seal.content).await?; + let rumor = signer + .nip44_decrypt_async(&seal.pubkey, &seal.content) + .await?; + let rumor = UnsignedEvent::from_json(rumor)?; Ok(UnwrappedGift { @@ -906,26 +847,17 @@ async fn set_rumor(client: &Client, id: EventId, rumor: &UnsignedEvent) -> Resul tags.push(Tag::identifier(id)); // Add a reference to the rumor's author - tags.push(Tag::custom( - TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::A)), - [author], - )); + tags.push(Tag::custom("a", [author])); // Add a conversation id - tags.push(Tag::custom( - TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::C)), - [conversation.to_string()], - )); + tags.push(Tag::custom("c", [conversation.to_string()])); // 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], - )); + for receiver in rumor.tags.public_keys() { + tags.push(Tag::custom("P", [receiver])); } // Convert rumor to json @@ -934,7 +866,7 @@ async fn set_rumor(client: &Client, id: EventId, rumor: &UnsignedEvent) -> Resul // Construct the event let event = EventBuilder::new(Kind::ApplicationSpecificData, content) .tags(tags) - .sign(&Keys::generate()) + .finalize_async(&Keys::generate()) .await?; // Save the event to the database @@ -960,7 +892,7 @@ async fn get_rumor(client: &Client, gift_wrap: EventId) -> Result u64 { let mut hasher = DefaultHasher::new(); - let mut pubkeys: Vec = rumor.tags.public_keys().copied().collect(); + let mut pubkeys: Vec = rumor.tags.public_keys().collect(); pubkeys.push(rumor.pubkey); pubkeys.sort(); pubkeys.dedup(); diff --git a/crates/chat/src/message.rs b/crates/chat/src/message.rs index 3234226..1d9217c 100644 --- a/crates/chat/src/message.rs +++ b/crates/chat/src/message.rs @@ -5,6 +5,106 @@ use common::{EventExt, NostrParser, extract_and_remove_media_urls}; use gpui::{SharedString, SharedUri}; use nostr_sdk::prelude::*; +/// Rendered message. +#[derive(Debug, Clone)] +pub struct Message { + pub id: EventId, + /// Author's public key + pub author: PublicKey, + /// The content/text of the message + pub content: String, + /// List of media URLs in the message + pub media: Vec, + /// Message created time as unix timestamp + pub created_at: Timestamp, + /// List of mentioned public keys in the message + pub mentions: Vec, + /// List of event of the message this message is a reply to + pub replies_to: Vec, +} + +impl From<&Event> for Message { + fn from(val: &Event) -> Self { + let mentions = extract_mentions(&val.content); + let replies_to = extract_reply_ids(&val.tags); + let (media, string) = extract_and_remove_media_urls(&val.content); + + Self { + id: val.id, + author: val.pubkey, + content: string, + media, + created_at: val.created_at, + mentions, + replies_to, + } + } +} + +impl From<&UnsignedEvent> for Message { + fn from(val: &UnsignedEvent) -> Self { + let mentions = extract_mentions(&val.content); + let replies_to = extract_reply_ids(&val.tags); + let (media, string) = extract_and_remove_media_urls(&val.content); + + Self { + // Event ID must be known + id: val.id.unwrap(), + author: val.pubkey, + content: string, + media, + created_at: val.created_at, + mentions, + replies_to, + } + } +} + +impl From<&NewMessage> for Message { + fn from(val: &NewMessage) -> Self { + let mentions = extract_mentions(&val.rumor.content); + let replies_to = extract_reply_ids(&val.rumor.tags); + let (media, string) = extract_and_remove_media_urls(&val.rumor.content); + + Self { + // Event ID must be known + id: val.rumor.id.unwrap(), + author: val.rumor.pubkey, + content: string, + media, + created_at: val.rumor.created_at, + mentions, + replies_to, + } + } +} + +impl Eq for Message {} + +impl PartialEq for Message { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl Ord for Message { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.created_at.cmp(&other.created_at) + } +} + +impl PartialOrd for Message { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Hash for Message { + fn hash(&self, state: &mut H) { + self.id.hash(state); + } +} + /// New message. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct NewMessage { @@ -44,74 +144,6 @@ impl FailedMessage { } } -/// Message. -#[derive(Debug, Clone, Hash, PartialEq, Eq)] -pub enum Message { - User(RenderedMessage), - Warning(String, Timestamp), - System(Timestamp), -} - -impl Message { - pub fn user(user: I) -> Self - where - I: Into, - { - Self::User(user.into()) - } - - pub fn warning(content: I) -> Self - where - I: Into, - { - Self::Warning(content.into(), Timestamp::now()) - } - - pub fn system() -> Self { - Self::System(Timestamp::default()) - } - - fn timestamp(&self) -> &Timestamp { - match self { - Message::User(msg) => &msg.created_at, - Message::Warning(_, ts) => ts, - Message::System(ts) => ts, - } - } -} - -impl From<&NewMessage> for Message { - fn from(val: &NewMessage) -> Self { - Self::User(val.into()) - } -} - -impl From<&UnsignedEvent> for Message { - fn from(val: &UnsignedEvent) -> Self { - Self::User(val.into()) - } -} - -impl Ord for Message { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - match (self, other) { - // System always comes first - (Message::System(_), Message::System(_)) => self.timestamp().cmp(other.timestamp()), - (Message::System(_), _) => std::cmp::Ordering::Less, - (_, Message::System(_)) => std::cmp::Ordering::Greater, - - // For non-system messages, compare by timestamp - _ => self.timestamp().cmp(other.timestamp()), - } - } -} - -impl PartialOrd for Message { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - #[derive(Debug, Clone)] pub struct Mention { pub public_key: PublicKey, @@ -124,106 +156,6 @@ impl Mention { } } -/// Rendered message. -#[derive(Debug, Clone)] -pub struct RenderedMessage { - pub id: EventId, - /// Author's public key - pub author: PublicKey, - /// The content/text of the message - pub content: String, - /// List of media URLs in the message - pub media: Vec, - /// Message created time as unix timestamp - pub created_at: Timestamp, - /// List of mentioned public keys in the message - pub mentions: Vec, - /// List of event of the message this message is a reply to - pub replies_to: Vec, -} - -impl From<&Event> for RenderedMessage { - fn from(val: &Event) -> Self { - let mentions = extract_mentions(&val.content); - let replies_to = extract_reply_ids(&val.tags); - let (media, string) = extract_and_remove_media_urls(&val.content); - - Self { - id: val.id, - author: val.pubkey, - content: string, - media, - created_at: val.created_at, - mentions, - replies_to, - } - } -} - -impl From<&UnsignedEvent> for RenderedMessage { - fn from(val: &UnsignedEvent) -> Self { - let mentions = extract_mentions(&val.content); - let replies_to = extract_reply_ids(&val.tags); - let (media, string) = extract_and_remove_media_urls(&val.content); - - Self { - // Event ID must be known - id: val.id.unwrap(), - author: val.pubkey, - content: string, - media, - created_at: val.created_at, - mentions, - replies_to, - } - } -} - -impl From<&NewMessage> for RenderedMessage { - fn from(val: &NewMessage) -> Self { - let mentions = extract_mentions(&val.rumor.content); - let replies_to = extract_reply_ids(&val.rumor.tags); - let (media, string) = extract_and_remove_media_urls(&val.rumor.content); - - Self { - // Event ID must be known - id: val.rumor.id.unwrap(), - author: val.rumor.pubkey, - content: string, - media, - created_at: val.rumor.created_at, - mentions, - replies_to, - } - } -} - -impl Eq for RenderedMessage {} - -impl PartialEq for RenderedMessage { - fn eq(&self, other: &Self) -> bool { - self.id == other.id - } -} - -impl Ord for RenderedMessage { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.created_at.cmp(&other.created_at) - } -} - -impl PartialOrd for RenderedMessage { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Hash for RenderedMessage { - fn hash(&self, state: &mut H) { - self.id.hash(state); - } -} - /// Extracts all mentions (public keys) from a content string. fn extract_mentions(content: &str) -> Vec { let parser = NostrParser::new(); @@ -242,13 +174,13 @@ fn extract_mentions(content: &str) -> Vec { fn extract_reply_ids(inner: &Tags) -> Vec { let mut replies_to = vec![]; - for tag in inner.filter(TagKind::e()) { + for tag in inner.iter().filter(|tag| tag.kind() == "e") { if let Some(id) = tag.content().and_then(|id| EventId::parse(id).ok()) { replies_to.push(id); } } - for tag in inner.filter(TagKind::q()) { + for tag in inner.iter().filter(|tag| tag.kind() == "q") { if let Some(id) = tag.content().and_then(|id| EventId::parse(id).ok()) { replies_to.push(id); } diff --git a/crates/chat/src/room.rs b/crates/chat/src/room.rs index c77a1fb..50c5827 100644 --- a/crates/chat/src/room.rs +++ b/crates/chat/src/room.rs @@ -4,6 +4,7 @@ use std::time::Duration; use anyhow::{Error, anyhow}; use common::EventExt; +use device::DeviceRegistry; use gpui::{App, AppContext, Context, EventEmitter, SharedString, Task}; use itertools::Itertools; use nostr_sdk::prelude::*; @@ -21,7 +22,7 @@ pub struct SendReport { pub receiver: PublicKey, pub gift_wrap_id: Option, pub error: Option, - pub output: Option>, + pub output: Option>, } impl SendReport { @@ -41,7 +42,7 @@ impl SendReport { } /// Set the output. - pub fn output(mut self, output: Output) -> Self { + pub fn output(mut self, output: Output) -> Self { self.output = Some(output); self } @@ -171,7 +172,8 @@ impl From<&UnsignedEvent> for Room { let members = val.extract_public_keys(); let subject = val .tags - .find(TagKind::Subject) + .iter() + .find(|tag| tag.kind() == "subject") .and_then(|tag| tag.content().map(|s| s.to_owned().into())); Room { @@ -205,7 +207,7 @@ impl Room { // WARNING: never sign this event let mut event = EventBuilder::new(Kind::PrivateDirectMessage, "") .tags(tags) - .build(author); + .finalize_unsigned(author); // Ensure that the ID is set event.ensure_id(); @@ -425,7 +427,7 @@ impl Room { let nostr = NostrRegistry::global(cx); // Get current user's public key - let sender = nostr.read(cx).signer().public_key()?; + let sender = nostr.read(cx).signer_pubkey(cx)?; // Get all members, excluding the sender let members: Vec = self @@ -440,9 +442,7 @@ impl Room { // Add subject tag if present if let Some(value) = self.subject.as_ref() { - tags.push(Tag::from_standardized_without_cell(TagStandard::Subject( - value.to_string(), - ))); + tags.push(Tag::custom("subject", vec![value.to_string()])); } // Add all reply tags @@ -452,19 +452,20 @@ impl Room { // Add all receiver tags for member in members.into_iter() { - tags.push(Tag::from_standardized_without_cell( - TagStandard::PublicKey { + tags.push( + Nip01Tag::PublicKey { public_key: member.public_key(), - relay_url: member.messaging_relay_hint(), - alias: None, - uppercase: false, - }, - )); + relay_hint: member.messaging_relay_hint(), + } + .to_tag(), + ); } // Construct a direct message rumor event // WARNING: never sign and send this event to relays - let mut event = EventBuilder::new(kind, content).tags(tags).build(sender); + let mut event = EventBuilder::new(kind, content) + .tags(tags) + .finalize_unsigned(sender); // Ensure that the ID is set event.ensure_id(); @@ -475,13 +476,18 @@ impl Room { /// Send rumor event to all members's messaging relays pub fn send(&self, rumor: UnsignedEvent, cx: &App) -> Option>> { let config = self.config.clone(); - let persons = PersonRegistry::global(cx); + + let device = DeviceRegistry::global(cx); + let encryption_signer = device.read(cx).signer(cx); + let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let signer = nostr.read(cx).signer(); // Get current user's public key - let public_key = nostr.read(cx).signer().public_key()?; + let user_signer = nostr.read(cx).signer(cx)?; + let public_key = nostr.read(cx).signer_pubkey(cx)?; + + let persons = PersonRegistry::global(cx); let sender = persons.read(cx).get(&public_key, cx); // Get all members (excluding sender) @@ -496,9 +502,6 @@ impl Room { let signer_kind = config.signer_kind(); let backup = config.backup(); - let user_signer = signer.get().await; - let encryption_signer = signer.get_encryption_signer().await; - let mut sents = 0; let mut reports = Vec::new(); @@ -592,17 +595,14 @@ impl Room { } // Helper function to send a gift-wrapped event -async fn send_gift_wrap( +async fn send_gift_wrap( client: &Client, - signer: &T, + signer: &Keys, receiver: &Person, rumor: &UnsignedEvent, config: &SignerKind, -) -> Result -where - T: NostrSigner + 'static, -{ - let k_tag = Tag::custom(TagKind::k(), vec!["14"]); +) -> Result { + let k_tag = Tag::custom("k", vec!["14"]); let mut extra_tags = vec![k_tag]; // Determine the receiver public key based on the config @@ -627,7 +627,10 @@ where }; // Construct the gift wrap event - let event = EventBuilder::gift_wrap(signer, &receiver, rumor.clone(), extra_tags).await?; + let event = nip59::GiftWrapBuilder::new(receiver, rumor.clone()) + .extra_tags(extra_tags) + .finalize_async(signer) + .await?; // Send the gift wrap event and collect the report let report = client diff --git a/crates/chat_ui/src/lib.rs b/crates/chat_ui/src/lib.rs index 310e75f..624c6f5 100644 --- a/crates/chat_ui/src/lib.rs +++ b/crates/chat_ui/src/lib.rs @@ -3,15 +3,15 @@ use std::sync::Arc; pub use actions::*; use anyhow::{Context as AnyhowContext, Error}; -use chat::{ChatRegistry, Message, RenderedMessage, Room, RoomEvent, SendReport, SendStatus}; +use chat::{ChatRegistry, Message, Room, RoomEvent, SendReport, SendStatus}; use common::{TimestampExt, coop_cache}; use gpui::prelude::FluentBuilder; use gpui::{ AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, ListAlignment, ListOffset, ListState, MouseButton, ObjectFit, ParentElement, PathPromptOptions, Render, SharedString, SharedUri, - StatefulInteractiveElement, Styled, StyledImage, Subscription, Task, WeakEntity, Window, - deferred, div, img, list, px, red, relative, svg, white, + StatefulInteractiveElement, Styled, StyledImage, Subscription, Task, WeakEntity, Window, div, + img, list, px, red, relative, svg, white, }; use itertools::Itertools; use nostr_sdk::prelude::*; @@ -38,9 +38,6 @@ use crate::text::RenderedText; mod actions; mod text; -const ANNOUNCEMENT: &str = - "This conversation is private. Only members can see each other's messages."; - pub fn init(room: WeakEntity, window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| ChatPanel::new(room, window, cx)) } @@ -101,7 +98,7 @@ impl ChatPanel { let reports_by_id = cx.new(|_| BTreeMap::new()); // Define list of messages - let messages = BTreeSet::from([Message::system()]); + let messages = BTreeSet::default(); let list_state = ListState::new(messages.len(), ListAlignment::Bottom, px(1024.)); // Get room id and name @@ -234,7 +231,7 @@ impl ChatPanel { match &*status { SendStatus::Ok { id, relay } => { if output.id() == id { - output.success.insert(relay.clone()); + output.success.insert(relay.clone(), EventSendStatus::Sent); } } SendStatus::Failed { id, relay, message } => { @@ -470,37 +467,19 @@ impl ChatPanel { self.reports_by_id.read(cx).get(id).is_some() } - /// Check if a message was encrypted by the dekey - fn encrypted_by_dekey(&self, id: &EventId, cx: &App) -> bool { - let chat = ChatRegistry::global(cx); - chat.read(cx).encrypted_by_dekey(id) - } - /// Get all sent reports for a message by its ID fn sent_reports(&self, id: &EventId, cx: &App) -> Option> { self.reports_by_id.read(cx).get(id).cloned() } /// Get a message by its ID - fn message(&self, id: &EventId) -> Option<&RenderedMessage> { - self.messages.iter().find_map(|msg| { - if let Message::User(rendered) = msg - && &rendered.id == id - { - return Some(rendered); - } - None - }) + fn message(&self, id: &EventId) -> Option<&Message> { + self.messages.iter().find(|msg| &msg.id == id) } - fn scroll_to(&self, id: EventId) { - if let Some(ix) = self.messages.iter().position(|m| { - if let Message::User(msg) = m { - msg.id == id - } else { - false - } - }) { + /// Scroll to a message by its ID + fn scroll_to(&self, id: &EventId) { + if let Some(ix) = self.messages.iter().position(|msg| &msg.id == id) { self.list_state.scroll_to_reveal_item(ix); } } @@ -620,13 +599,19 @@ impl ChatPanel { }) .is_err() { - window.push_notification( - Notification::error("Failed to change subject").autohide(false), - cx, - ); + window.push_notification(Notification::error("Failed to change subject"), cx); } } Command::ChangeSigner(kind) => { + let settings = AppSettings::global(cx); + let is_nip4e_enabled = settings.read(cx).is_nip4e_enabled(cx); + let is_force_nip4e = *kind == SignerKind::Encryption || *kind == SignerKind::Auto; + + if !is_nip4e_enabled && is_force_nip4e { + window.push_notification("Decoupling Encryption Key is not enabled", cx); + return; + } + if self .room .update(cx, |this, cx| { @@ -634,10 +619,7 @@ impl ChatPanel { }) .is_err() { - window.push_notification( - Notification::error("Failed to change signer").autohide(false), - cx, - ); + window.push_notification(Notification::error("Failed to change signer"), cx); } } Command::ToggleBackup => { @@ -648,10 +630,7 @@ impl ChatPanel { }) .is_err() { - window.push_notification( - Notification::error("Failed to toggle backup").autohide(false), - cx, - ); + window.push_notification(Notification::error("Failed to toggle backup"), cx); } } Command::Copy(public_key) => { @@ -748,9 +727,11 @@ impl ChatPanel { cx.open_url(&content); } - fn render_announcement(&self, ix: usize, cx: &Context) -> AnyElement { + fn render_announcement(&self, cx: &Context) -> AnyElement { + const MSG: &str = + "This conversation is private. Only members can see each other's messages."; + v_flex() - .id(ix) .h_40() .w_full() .gap_3() @@ -767,7 +748,7 @@ impl ChatPanel { .size_12() .text_color(cx.theme().ghost_element_active), ) - .child(SharedString::from(ANNOUNCEMENT)) + .child(MSG) .into_any_element() } @@ -804,6 +785,34 @@ impl ChatPanel { .into_any_element() } + fn is_group_start(&self, ix: usize) -> bool { + // 5 minutes + const GROUP_WINDOW: u64 = 300; + + if ix == 0 { + return true; + } + + let mut iter = self.messages.iter(); + + if let Some(previous) = iter.nth(ix - 1) + && let Some(current) = iter.next() + { + if current.author != previous.author { + return true; + } + + let gap = current + .created_at + .as_secs() + .saturating_sub(previous.created_at.as_secs()); + + return gap > GROUP_WINDOW; + } + + false + } + fn render_message( &mut self, ix: usize, @@ -811,24 +820,17 @@ impl ChatPanel { cx: &mut Context, ) -> AnyElement { if let Some(message) = self.messages.iter().nth(ix) { - match message { - Message::User(rendered) => { - let persons = PersonRegistry::global(cx); - let text = self - .rendered_texts_by_id - .entry(rendered.id) - .or_insert_with(|| { - RenderedText::new(&rendered.content, &rendered.mentions, &persons, cx) - }) - .element(ix.into(), window, cx); + let persons = PersonRegistry::global(cx); + let show_author = self.is_group_start(ix); + let text = self + .rendered_texts_by_id + .entry(message.id) + .or_insert_with(|| { + RenderedText::new(&message.content, &message.mentions, &persons, cx) + }) + .element(ix.into(), window, cx); - self.render_text_message(ix, rendered, text, cx) - } - Message::Warning(content, _timestamp) => { - self.render_warning(ix, SharedString::from(content), cx) - } - Message::System(_timestamp) => self.render_announcement(ix, cx), - } + self.render_text_message(ix, message, text, show_author, cx) } else { self.render_warning(ix, SharedString::from("Message not found"), cx) } @@ -837,8 +839,9 @@ impl ChatPanel { fn render_text_message( &self, ix: usize, - message: &RenderedMessage, + message: &Message, rendered_text: AnyElement, + show_author: bool, cx: &Context, ) -> AnyElement { let id = message.id; @@ -848,7 +851,6 @@ impl ChatPanel { let replies = message.replies_to.as_slice(); let has_replies = !replies.is_empty(); let has_reports = self.has_reports(&id, cx); - let encrypted_by_dekey = self.encrypted_by_dekey(&id, cx); // Hide avatar setting let hide_avatar = AppSettings::get_hide_avatar(cx); @@ -865,17 +867,21 @@ impl ChatPanel { .flex() .gap_3() .when(!hide_avatar, |this| { - this.child( - Avatar::new(author.avatar()) - .flex_shrink_0() - .relative() - .dropdown_menu(move |this, _window, _cx| { - this.menu("Public Key", Box::new(Command::Copy(pk))) - .menu("View Relays", Box::new(Command::Relays(pk))) - .separator() - .menu("View on njump.me", Box::new(Command::Njump(pk))) - }), - ) + if show_author { + this.child( + Avatar::new(author.avatar()) + .flex_shrink_0() + .relative() + .dropdown_menu(move |this, _window, _cx| { + this.menu("Public Key", Box::new(Command::Copy(pk))) + .menu("View Relays", Box::new(Command::Relays(pk))) + .separator() + .menu("View on njump.me", Box::new(Command::Njump(pk))) + }), + ) + } else { + this.child(div().flex_shrink_0().w(px(32.))) + } }) .child( v_flex() @@ -883,33 +889,24 @@ impl ChatPanel { .w_full() .flex_initial() .overflow_hidden() - .child( - h_flex() - .gap_2() - .text_sm() - .text_color(cx.theme().text_placeholder) - .child( - div() - .font_semibold() - .text_color(cx.theme().text) - .child(author.name()), - ) - .when(encrypted_by_dekey, |this| { - this.child( - Button::new(format!("dekey-{ix}")) - .icon(IconName::Shield) - .ghost() - .xsmall() - .px_4() - .tooltip("Encrypted by Dekey") - .disabled(true), + .when(show_author, |this| { + this.child( + h_flex() + .gap_2() + .text_sm() + .text_color(cx.theme().text_placeholder) + .child( + div() + .font_semibold() + .text_color(cx.theme().text) + .child(author.name()), ) - }) - .child(message.created_at.to_human_time()) - .when(has_reports, |this| { - this.child(deferred(self.render_sent_reports(&id, cx))) - }), - ) + .child(message.created_at.to_human_time()) + .when(has_reports, |this| { + this.child(self.render_sent_reports(&id, cx)) + }), + ) + }) .when(has_replies, |this| { this.children(self.render_message_replies(replies, cx)) }) @@ -1027,7 +1024,7 @@ impl ChatPanel { .on_click({ let id = *id; cx.listener(move |this, _event, _window, _cx| { - this.scroll_to(id); + this.scroll_to(&id); }) }), ); @@ -1176,7 +1173,7 @@ impl ChatPanel { .text_xs() .font_semibold() .line_height(relative(1.25)) - .child(SharedString::from(url.to_string())), + .child(SharedString::from(url.0.to_string())), ) .child( div() @@ -1518,15 +1515,28 @@ impl Render for ChatPanel { v_flex() .flex_1() .relative() - .child( - list( - self.list_state.clone(), - cx.processor(move |this, ix, window, cx| { - this.render_message(ix, window, cx) - }), - ) - .size_full(), - ) + .map(|this| { + if self.messages.is_empty() { + this.child( + div() + .size_full() + .flex() + .items_center() + .justify_end() + .child(self.render_announcement(cx)), + ) + } else { + this.child( + list( + self.list_state.clone(), + cx.processor(move |this, ix, window, cx| { + this.render_message(ix, window, cx) + }), + ) + .size_full(), + ) + } + }) .child(Scrollbar::vertical(&self.list_state)), ) .child( @@ -1552,7 +1562,7 @@ impl Render for ChatPanel { this.upload(window, cx); })), ) - .child(Input::new(&self.input).appearance(false).text_sm().flex_1()) + .child(Input::new(&self.input).appearance(false).flex_1()) .child( h_flex() .pl_1() diff --git a/crates/common/src/event.rs b/crates/common/src/event.rs index 194cddf..e115775 100644 --- a/crates/common/src/event.rs +++ b/crates/common/src/event.rs @@ -18,7 +18,7 @@ impl EventExt for Event { } fn extract_public_keys(&self) -> Vec { - let mut public_keys: Vec = self.tags.public_keys().copied().collect(); + let mut public_keys: Vec = self.tags.public_keys().collect(); public_keys.push(self.pubkey); public_keys.into_iter().unique().collect() @@ -46,7 +46,7 @@ impl EventExt for UnsignedEvent { } fn extract_public_keys(&self) -> Vec { - let mut public_keys: Vec = self.tags.public_keys().copied().collect(); + let mut public_keys: Vec = self.tags.public_keys().collect(); public_keys.push(self.pubkey); public_keys.into_iter().unique().sorted().collect() } diff --git a/crates/device/Cargo.toml b/crates/device/Cargo.toml index 5b73a5e..4f4cf11 100644 --- a/crates/device/Cargo.toml +++ b/crates/device/Cargo.toml @@ -10,6 +10,7 @@ state = { path = "../state" } person = { path = "../person" } ui = { path = "../ui" } theme = { path = "../theme" } +settings = { path = "../settings" } gpui.workspace = true nostr-sdk.workspace = true diff --git a/crates/device/src/lib.rs b/crates/device/src/lib.rs index c5ab2c7..6aeb16c 100644 --- a/crates/device/src/lib.rs +++ b/crates/device/src/lib.rs @@ -2,6 +2,8 @@ use std::cell::Cell; use std::collections::HashSet; use std::path::PathBuf; use std::rc::Rc; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; use std::time::Duration; use anyhow::{Context as AnyhowContext, Error, anyhow}; @@ -11,7 +13,9 @@ use gpui::{ }; use nostr_sdk::prelude::*; use person::PersonRegistry; -use state::{Announcement, CLIENT_NAME, NostrRegistry, StateEvent, TIMEOUT}; +use settings::AppSettings; +use smallvec::{SmallVec, smallvec}; +use state::{Announcement, CLIENT_NAME, NostrRegistry}; use theme::ActiveTheme; use ui::avatar::Avatar; use ui::button::Button; @@ -19,8 +23,6 @@ use ui::notification::{Notification, NotificationKind}; use ui::{Disableable, Sizable, StyledExt, WindowExtension, h_flex, v_flex}; const IDENTIFIER: &str = "coop:device"; -const MSG: &str = "You've requested an encryption key from another device. \ - Approve to allow Coop to share with it."; pub fn init(window: &mut Window, cx: &mut App) { DeviceRegistry::set_global(cx.new(|cx| DeviceRegistry::new(window, cx)), cx); @@ -35,10 +37,10 @@ impl Global for GlobalDeviceRegistry {} pub enum DeviceEvent { /// A new encryption signer has been set Set, + /// User have not setup encryption key + NotSet, /// The device is requesting an encryption key Requesting, - /// The device is creating a new encryption key - Creating, /// An error occurred Error(SharedString), } @@ -57,17 +59,20 @@ impl DeviceEvent { /// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md #[derive(Debug)] pub struct DeviceRegistry { - /// Whether the registry is currently initializing - pub initializing: bool, - /// Whether there is a pending request for encryption key approval pub pending_request: bool, + /// Whether an announcement has been made for this device + pub announcement_existed: Arc, + + /// Signer + signer: Entity>, + /// Async tasks tasks: Vec>>, /// Event subscription - _subscription: Option, + _subscriptions: SmallVec<[Subscription; 2]>, } impl EventEmitter for DeviceRegistry {} @@ -85,31 +90,53 @@ impl DeviceRegistry { /// Create a new device registry instance fn new(window: &mut Window, cx: &mut Context) -> Self { - let nostr = NostrRegistry::global(cx); + let signer = cx.new(|_| None); - // Subscribe to nostr state events - let subscription = cx.subscribe_in(&nostr, window, |this, _e, event, _window, cx| { - if event == &StateEvent::SignerSet { - this.set_initializing(true, cx); - this.get_announcement(cx); - }; - }); + let nostr = NostrRegistry::global(cx); + let user_signer = nostr.read(cx).signer.clone(); + + let settings = AppSettings::global(cx); + let is_nip4e_enabled = settings.read(cx).is_nip4e_enabled(cx); + + let mut subscriptions = smallvec![]; + + subscriptions.push( + // Subscribe to nostr state events + cx.observe(&settings, move |this, settings, cx| { + if settings.read(cx).is_nip4e_enabled(cx) { + this.get_announcement(cx); + }; + }), + ); + + subscriptions.push( + // Observe the user signer + cx.observe(&user_signer, move |this, signer, cx| { + if signer.read(cx).is_some() && is_nip4e_enabled { + this.get_announcement(cx); + }; + }), + ); cx.defer_in(window, |this, window, cx| { this.handle_notifications(window, cx); }); Self { - initializing: true, + signer, pending_request: false, + announcement_existed: Arc::new(AtomicBool::new(false)), tasks: vec![], - _subscription: Some(subscription), + _subscriptions: subscriptions, } } fn handle_notifications(&mut self, window: &mut Window, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); + let current_user = nostr.read(cx).signer_pubkey(cx); + + let announcement_existed = self.announcement_existed.clone(); let (tx, rx) = flume::bounded::(100); self.tasks.push(cx.background_spawn(async move { @@ -126,15 +153,15 @@ impl DeviceRegistry { } match event.kind { - Kind::Custom(4454) => { - if verify_author(&client, event.as_ref()).await { - tx.send_async(event.into_owned()).await?; - } + Kind::Custom(10044) if current_user == Some(event.pubkey) => { + announcement_existed.store(true, Ordering::Relaxed); + tx.send_async(event.into_owned()).await?; } - Kind::Custom(4455) => { - if verify_author(&client, event.as_ref()).await { - tx.send_async(event.into_owned()).await?; - } + Kind::Custom(4454) if current_user == Some(event.pubkey) => { + tx.send_async(event.into_owned()).await?; + } + Kind::Custom(4455) if current_user == Some(event.pubkey) => { + tx.send_async(event.into_owned()).await?; } _ => {} } @@ -147,6 +174,11 @@ impl DeviceRegistry { self.tasks.push(cx.spawn_in(window, async move |this, cx| { while let Ok(event) = rx.recv_async().await { match event.kind { + Kind::Custom(10044) => { + this.update_in(cx, |this, _window, cx| { + this.set_encryption(&event, cx); + })?; + } // New request event from other device Kind::Custom(4454) => { this.update_in(cx, |this, window, cx| { @@ -166,37 +198,24 @@ impl DeviceRegistry { })); } - /// Set whether the registry is currently initializing - fn set_initializing(&mut self, initializing: bool, cx: &mut Context) { - self.initializing = initializing; - cx.notify(); - } - /// Set whether there is a pending request for encryption key approval fn set_pending_request(&mut self, pending: bool, cx: &mut Context) { self.pending_request = pending; cx.notify(); } + /// Get the signer + pub fn signer(&self, cx: &App) -> Option { + self.signer.read(cx).clone() + } + /// Set the decoupled encryption key for the current user - fn set_signer(&mut self, new: S, cx: &mut Context) - where - S: NostrSigner + 'static, - { - let nostr = NostrRegistry::global(cx); - let signer = nostr.read(cx).signer(); - - self.tasks.push(cx.spawn(async move |this, cx| { - signer.set_encryption_signer(new).await; - - // Update state - this.update(cx, |this, cx| { - this.set_initializing(false, cx); - cx.emit(DeviceEvent::Set); - })?; - - Ok(()) - })); + fn set_signer(&mut self, new: Keys, cx: &mut Context) { + self.signer.update(cx, |this, cx| { + *this = Some(new); + cx.notify(); + }); + cx.emit(DeviceEvent::Set); } /// Backup the encryption's secret key to a file @@ -204,8 +223,12 @@ impl DeviceRegistry { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); + let Some(signer) = nostr.read(cx).signer(cx) else { + return Task::ready(Err(anyhow!("Signer is required"))); + }; + cx.background_spawn(async move { - let keys = get_keys(&client).await?; + let keys = get_keys(&client, &signer).await?; let content = keys.secret_key().to_bech32()?; smol::fs::write(path, &content).await?; @@ -219,45 +242,48 @@ impl DeviceRegistry { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let task: Task> = cx.background_spawn(async move { - let signer = client.signer().context("Signer not found")?; - let public_key = signer.get_public_key().await?; + let Some(current_user) = nostr.read(cx).signer_pubkey(cx) else { + return; + }; + + self.tasks.push(cx.background_spawn(async move { + let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); // Construct the filter for the device announcement event let filter = Filter::new() .kind(Kind::Custom(10044)) - .author(public_key) + .author(current_user) .limit(1); - // Stream events from user's write relays - let mut stream = client - .stream_events(filter) - .timeout(Duration::from_secs(TIMEOUT)) + client + .subscribe(filter) + .close_on(opts) + .with_id(SubscriptionId::new("nip4e")) .await?; - while let Some((_url, res)) = stream.next().await { - if let Ok(event) = res { - return Ok(event); - } - } + Ok(()) + })); - Err(anyhow!("Announcement not found")) - }); + let announcement_existed = self.announcement_existed.clone(); self.tasks.push(cx.spawn(async move |this, cx| { - match task.await { - Ok(event) => { - // Set encryption key from the announcement event - this.update(cx, |this, cx| { - this.set_encryption(&event, cx); - })?; - } - Err(_) => { - // User has no announcement, create a new one - this.update(cx, |this, cx| { - this.set_announcement(Keys::generate(), cx); - })?; - } + if !cx + .background_spawn(async move { + // Wait for 5 seconds + smol::Timer::after(Duration::from_secs(5)).await; + + // Then check if the msg relays have been found + if !announcement_existed.load(Ordering::Acquire) { + return true; + } + + false + }) + .await + { + this.update(cx, |_this, cx| { + cx.emit(DeviceEvent::NotSet); + })?; } Ok(()) @@ -268,9 +294,6 @@ impl DeviceRegistry { pub fn set_announcement(&mut self, keys: Keys, cx: &mut Context) { let task = self.create_encryption(keys, cx); - // Notify that we're creating a new encryption key - cx.emit(DeviceEvent::Creating); - self.tasks.push(cx.spawn(async move |this, cx| { match task.await { Ok(keys) => { @@ -297,15 +320,19 @@ impl DeviceRegistry { let secret = keys.secret_key().to_secret_hex(); let n = keys.public_key(); + let Some(signer) = nostr.read(cx).signer(cx) else { + return Task::ready(Err(anyhow!("Signer is required"))); + }; + cx.background_spawn(async move { // Construct an announcement event - let builder = EventBuilder::new(Kind::Custom(10044), "").tags(vec![ - Tag::custom(TagKind::custom("n"), vec![n]), - Tag::client(CLIENT_NAME), - ]); - - // Sign the event with user's signer - let event = client.sign_event_builder(builder).await?; + let event = EventBuilder::new(Kind::Custom(10044), "") + .tags(vec![ + Tag::custom("n", vec![n]), + Tag::custom("client", vec![CLIENT_NAME]), + ]) + .finalize_async(&signer) + .await?; // Publish announcement client @@ -315,7 +342,7 @@ impl DeviceRegistry { .await?; // Save device keys to the database - set_keys(&client, &secret).await?; + set_keys(&client, &signer, &secret).await?; Ok(keys) }) @@ -326,12 +353,16 @@ impl DeviceRegistry { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); + let Some(signer) = nostr.read(cx).signer(cx) else { + return; + }; + let announcement = Announcement::from(event); let device_pubkey = announcement.public_key(); // Get encryption key from the database and compare with the announcement let task: Task> = cx.background_spawn(async move { - let keys = get_keys(&client).await?; + let keys = get_keys(&client, &signer).await?; // Compare the public key from the announcement with the one from the database if keys.public_key() != device_pubkey { @@ -360,10 +391,13 @@ impl DeviceRegistry { fn wait_for_request(&mut self, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let signer = nostr.read(cx).signer(); + + let Some(signer) = nostr.read(cx).signer(cx) else { + return; + }; self.tasks.push(cx.background_spawn(async move { - let public_key = signer.get_public_key().await?; + let public_key = signer.get_public_key_async().await?; let id = SubscriptionId::new("dekey-requests"); // Construct a filter for encryption key requests @@ -383,13 +417,18 @@ impl DeviceRegistry { pub fn request(&mut self, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let signer = nostr.read(cx).signer(); - let app_keys = nostr.read(cx).keys(); - let app_pubkey = app_keys.public_key(); + let Some(signer) = nostr.read(cx).signer(cx) else { + return; + }; + + let Ok(app_keys) = get_or_init_app_keys(cx) else { + return; + }; let task: Task, Error>> = cx.background_spawn(async move { - let public_key = signer.get_public_key().await?; + let app_pubkey = app_keys.public_key(); + let public_key = signer.get_public_key_async().await?; // Construct a filter to get the latest approval event let filter = Filter::new() @@ -404,13 +443,13 @@ impl DeviceRegistry { // No approval event found, construct a request event None => { // Construct an event for device key request - let builder = EventBuilder::new(Kind::Custom(4454), "").tags(vec![ - Tag::custom(TagKind::custom("P"), vec![app_pubkey]), - Tag::client(CLIENT_NAME), - ]); - - // Sign the event with user's signer - let event = client.sign_event_builder(builder).await?; + let event = EventBuilder::new(Kind::Custom(4454), "") + .tags(vec![ + Tag::custom("P", vec![app_pubkey]), + Tag::custom("client", vec![CLIENT_NAME]), + ]) + .finalize_async(&signer) + .await?; // Send the event to write relays client.send_event(&event).to_nip65().await?; @@ -429,10 +468,7 @@ impl DeviceRegistry { } Ok(None) => { this.update(cx, |this, cx| { - this.set_initializing(false, cx); this.wait_for_approval(cx); - - cx.emit(DeviceEvent::Requesting); })?; } Err(e) => { @@ -449,10 +485,15 @@ impl DeviceRegistry { fn wait_for_approval(&mut self, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let signer = nostr.read(cx).signer(); + + let Some(signer) = nostr.read(cx).signer(cx) else { + return; + }; + + cx.emit(DeviceEvent::Requesting); self.tasks.push(cx.background_spawn(async move { - let public_key = signer.get_public_key().await?; + let public_key = signer.get_public_key_async().await?; // Construct a filter for device key requests let filter = Filter::new() @@ -469,19 +510,21 @@ impl DeviceRegistry { /// Parse the approval event to get encryption key then set it fn extract_encryption(&mut self, event: Event, cx: &mut Context) { - let nostr = NostrRegistry::global(cx); - let app_keys = nostr.read(cx).keys(); + let Ok(app_keys) = get_or_init_app_keys(cx) else { + return; + }; let task: Task> = cx.background_spawn(async move { let master = event .tags - .find(TagKind::custom("P")) + .iter() + .find(|tag| tag.kind() == "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 = app_keys.nip44_decrypt(&master, payload).await?; + let decrypted = app_keys.nip44_decrypt_async(&master, payload).await?; let secret = SecretKey::from_hex(&decrypted)?; let keys = Keys::new(secret); @@ -511,37 +554,42 @@ impl DeviceRegistry { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); + let Some(signer) = nostr.read(cx).signer(cx) else { + return; + }; + // Get user's write relays let event = event.clone(); let id: SharedString = event.id.to_hex().into(); let task: Task> = cx.background_spawn(async move { // Get device keys - let keys = get_keys(&client).await?; + let keys = get_keys(&client, &signer).await?; let secret = keys.secret_key().to_secret_hex(); // Extract the target public key from the event tags let target = event .tags - .find(TagKind::custom("P")) + .iter() + .find(|tag| tag.kind() == "P") .and_then(|tag| tag.content()) .and_then(|content| PublicKey::parse(content).ok()) .context("Target is not a valid public key")?; // Encrypt the device keys with the user's signer - let payload = keys.nip44_encrypt(&target, &secret).await?; + let payload = keys.nip44_encrypt_async(&target, &secret).await?; // Construct the response event // // P tag: the current device's public key // p tag: the requester's public key - let builder = EventBuilder::new(Kind::Custom(4455), payload).tags(vec![ - Tag::custom(TagKind::custom("P"), vec![keys.public_key().to_hex()]), - Tag::public_key(target), - ]); - - // Sign the builder - let event = client.sign_event_builder(builder).await?; + let event = EventBuilder::new(Kind::Custom(4455), payload) + .tags(vec![ + Tag::custom("P", vec![keys.public_key().to_hex()]), + Tag::public_key(target), + ]) + .finalize_async(&signer) + .await?; // Send the response event to the user's relay list client.send_event(&event).to_nip65().await?; @@ -586,6 +634,9 @@ impl DeviceRegistry { /// Build a notification for the encryption request. fn notification(&self, event: Event, cx: &Context) -> Notification { + const MSG: &str = "You've requested an encryption key from another device. \ + Approve to allow Coop to share with it."; + let request = Announcement::from(&event); let persons = PersonRegistry::global(cx); let profile = persons.read(cx).get(&request.public_key(), cx); @@ -688,29 +739,44 @@ impl DeviceRegistry { struct DeviceNotification; -/// Verify the author of an event -async fn verify_author(client: &Client, event: &Event) -> bool { - if let Some(signer) = client.signer() - && let Ok(public_key) = signer.get_public_key().await - { - return public_key == event.pubkey; +/// Get or create new app keys +fn get_or_init_app_keys(cx: &App) -> Result { + let read = cx.read_credentials(CLIENT_NAME); + let stored_keys: Option = cx.foreground_executor().block_on(async move { + if let Ok(Some((_, secret))) = read.await { + SecretKey::from_slice(&secret).map(Keys::new).ok() + } else { + None + } + }); + + if let Some(keys) = stored_keys { + Ok(keys) + } else { + let keys = Keys::generate(); + let user = keys.public_key().to_hex(); + let secret = keys.secret_key().to_secret_bytes(); + let write = cx.write_credentials(CLIENT_NAME, &user, &secret); + + cx.foreground_executor().block_on(async move { + if let Err(e) = write.await { + log::error!("Keyring not available or panic: {e}") + } + }); + + Ok(keys) } - false } /// Encrypt and store device keys in the local database. -async fn set_keys(client: &Client, secret: &str) -> Result<(), Error> { - let signer = client.signer().context("Signer not found")?; - let public_key = signer.get_public_key().await?; - - // Encrypt the value - let content = signer.nip44_encrypt(&public_key, secret).await?; +async fn set_keys(client: &Client, signer: &Keys, secret: &str) -> Result<(), Error> { + let public_key = signer.get_public_key_async().await?; + let content = signer.nip44_encrypt_async(&public_key, secret).await?; // Construct the application data event let event = EventBuilder::new(Kind::ApplicationSpecificData, content) .tag(Tag::identifier(IDENTIFIER)) - .build(public_key) - .sign(&Keys::generate()) + .finalize_async(signer) .await?; // Save the event to the database @@ -720,9 +786,8 @@ async fn set_keys(client: &Client, secret: &str) -> Result<(), Error> { } /// Get device keys from the local database. -async fn get_keys(client: &Client) -> Result { - let signer = client.signer().context("Signer not found")?; - let public_key = signer.get_public_key().await?; +async fn get_keys(client: &Client, signer: &Keys) -> Result { + let public_key = signer.get_public_key_async().await?; let filter = Filter::new() .kind(Kind::ApplicationSpecificData) @@ -730,7 +795,10 @@ async fn get_keys(client: &Client) -> Result { .author(public_key); if let Some(event) = client.database().query(filter).await?.first() { - let content = signer.nip44_decrypt(&public_key, &event.content).await?; + let content = signer + .nip44_decrypt_async(&public_key, &event.content) + .await?; + let secret = SecretKey::parse(&content)?; let keys = Keys::new(secret); diff --git a/crates/person/src/lib.rs b/crates/person/src/lib.rs index 3f72f99..b8423f1 100644 --- a/crates/person/src/lib.rs +++ b/crates/person/src/lib.rs @@ -242,7 +242,7 @@ impl PersonRegistry { /// Set messaging relays for a person fn set_messaging_relays(&mut self, event: &Event, cx: &mut App) { - let urls: Vec = nip17::extract_relay_list(event).cloned().collect(); + let urls: Vec = nip17::extract_relay_list(event).collect(); if let Some(person) = self.persons.get(&event.pubkey) { person.update(cx, |person, cx| { diff --git a/crates/relay_auth/src/lib.rs b/crates/relay_auth/src/lib.rs index 4bac09a..8d32e61 100644 --- a/crates/relay_auth/src/lib.rs +++ b/crates/relay_auth/src/lib.rs @@ -193,15 +193,20 @@ impl RelayAuth { fn auth(&self, req: &Arc, cx: &App) -> Task> { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let req = req.clone(); + + let Some(signer) = nostr.read(cx).signer(cx) else { + return Task::ready(Err(anyhow!("Signer is required"))); + }; // Get all pending events for the relay + let req = req.clone(); let pending_events = self.get_pending_events(req.url(), cx); cx.background_spawn(async move { // Construct event - let builder = EventBuilder::auth(req.challenge(), req.url().clone()); - let event = client.sign_event_builder(builder).await?; + let event = EventBuilder::auth(req.challenge(), req.url().clone()) + .finalize_async(&signer) + .await?; // Get the event ID let id = event.id; @@ -217,8 +222,6 @@ impl RelayAuth { .send_msg(ClientMessage::Auth(Cow::Borrowed(&event))) .await?; - log::info!("Sending AUTH event"); - while let Some(notification) = notifications.next().await { match notification { RelayNotification::Message { message } => { @@ -272,30 +275,22 @@ impl RelayAuth { this.update_in(cx, |this, window, cx| { window.clear_notification_by_id::(challenge, cx); - match result { - Ok(_) => { - // Clear pending events for the authenticated relay - this.clear_pending_events(url, cx); + if let Err(e) = result { + window + .push_notification(Notification::error(e.to_string()).autohide(false), cx); + } else { + // Clear pending events for the authenticated relay + this.clear_pending_events(url, cx); - // Save the authenticated relay to automatically authenticate future requests - settings.update(cx, |this, cx| { - this.add_trusted_relay(url, cx); - }); + let domain = url.domain().unwrap_or_default(); + let msg = format!("Relay {} has been authenticated", domain); - window.push_notification( - Notification::success(format!( - "Relay {} has been authenticated", - url.domain().unwrap_or_default() - )), - cx, - ); - } - Err(e) => { - window.push_notification( - Notification::error(e.to_string()).autohide(false), - cx, - ); - } + window.push_notification(Notification::success(msg), cx); + + // Save the authenticated relay to automatically authenticate future requests + settings.update(cx, |this, cx| { + this.add_trusted_relay(url, cx); + }); } }) .ok(); diff --git a/crates/settings/src/lib.rs b/crates/settings/src/lib.rs index d081d31..e450a1c 100644 --- a/crates/settings/src/lib.rs +++ b/crates/settings/src/lib.rs @@ -1,4 +1,3 @@ -use std::collections::{HashMap, HashSet}; use std::fmt::Display; use std::rc::Rc; @@ -20,13 +19,15 @@ macro_rules! setting_accessors { $( paste::paste! { pub fn [](cx: &App) -> $type { - Self::global(cx).read(cx).values.$field.clone() + Self::global(cx).read(cx).inner.read(cx).$field.clone() } pub fn [](value: $type, cx: &mut App) { Self::global(cx).update(cx, |this, cx| { - this.values.$field = value; - cx.notify(); + this.inner.update(cx, |inner, cx| { + inner.$field = value; + cx.notify(); + }); }); } } @@ -40,9 +41,9 @@ setting_accessors! { pub theme_mode: ThemeMode, pub hide_avatar: bool, pub screening: bool, + pub nip4e: bool, pub auth_mode: AuthMode, - pub trusted_relays: HashSet, - pub room_configs: HashMap, + pub trusted_relays: Vec, pub file_server: Url, } @@ -66,10 +67,10 @@ impl Display for AuthMode { /// Signer kind #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] pub enum SignerKind { - #[default] Auto, - User, Encryption, + #[default] + User, } impl SignerKind { @@ -97,7 +98,7 @@ impl RoomConfig { pub fn new() -> Self { Self { backup: true, - signer_kind: SignerKind::Auto, + signer_kind: SignerKind::default(), } } @@ -137,14 +138,14 @@ pub struct Settings { /// Enable screening for unknown chat requests pub screening: bool, + /// Enable decoupling encryption key + pub nip4e: bool, + /// Authentication mode pub auth_mode: AuthMode, /// Trusted relays; Coop will automatically authenticate with these relays - pub trusted_relays: HashSet, - - /// Configuration for each chat room - pub room_configs: HashMap, + pub trusted_relays: Vec, /// Server for blossom media attachments pub file_server: Url, @@ -157,9 +158,9 @@ impl Default for Settings { theme_mode: ThemeMode::default(), hide_avatar: false, screening: true, + nip4e: false, auth_mode: AuthMode::default(), - trusted_relays: HashSet::default(), - room_configs: HashMap::default(), + trusted_relays: vec![], file_server: Url::parse("https://blossom.band/").unwrap(), } } @@ -178,7 +179,7 @@ impl Global for GlobalAppSettings {} /// Application settings pub struct AppSettings { /// Settings - values: Settings, + inner: Entity, /// Event subscriptions _subscriptions: SmallVec<[Subscription; 2]>, @@ -196,11 +197,12 @@ impl AppSettings { } fn new(window: &mut Window, cx: &mut Context) -> Self { + let inner = cx.new(|_| Settings::default()); let mut subscriptions = smallvec![]; subscriptions.push( // Observe and automatically save settings on changes - cx.observe_self(|this, cx| { + cx.observe(&inner, |this, _inner, cx| { this.save(cx); }), ); @@ -211,15 +213,17 @@ impl AppSettings { }); Self { - values: Settings::default(), + inner, _subscriptions: subscriptions, } } /// Update settings fn set_settings(&mut self, settings: Settings, cx: &mut Context) { - self.values = settings; - cx.notify(); + self.inner.update(cx, |this, cx| { + *this = settings; + cx.notify(); + }); } /// Load settings @@ -249,19 +253,16 @@ impl AppSettings { /// Save settings pub fn save(&mut self, cx: &mut Context) { - let settings = self.values.clone(); + let settings = self.inner.read(cx); - let task: Task> = cx.background_spawn(async move { - let path = config_dir().join(".settings"); - let content = serde_json::to_string(&settings)?; - - // Write settings to file - smol::fs::write(&path, content).await?; - - Ok(()) - }); - - task.detach(); + if let Ok(content) = serde_json::to_string(&settings) { + cx.background_spawn(async move { + let path = config_dir().join(".settings"); + // Write settings to file + smol::fs::write(&path, content).await.ok(); + }) + .detach(); + } } /// Set theme @@ -270,8 +271,10 @@ impl AppSettings { T: Into, { // Update settings - self.values.theme = Some(theme.into()); - cx.notify(); + self.inner.update(cx, |this, cx| { + this.theme = Some(theme.into()); + cx.notify(); + }); // Apply the new theme self.apply_theme(window, cx); @@ -279,16 +282,17 @@ impl AppSettings { /// Reset theme pub fn reset_theme(&mut self, window: &mut Window, cx: &mut Context) { - self.values.theme = None; - cx.notify(); - + self.inner.update(cx, |this, cx| { + this.theme = None; + cx.notify(); + }); self.apply_theme(window, cx); } /// Apply theme pub fn apply_theme(&mut self, window: &mut Window, cx: &mut Context) { - if let Some(name) = self.values.theme.as_ref() { - let mode = self.values.theme_mode; + if let Some(name) = self.inner.read(cx).theme.as_ref() { + let mode = self.inner.read(cx).theme_mode; if let Ok(new_theme) = ThemeFamily::from_assets(name) { Theme::apply_theme(Rc::new(new_theme), Some(window), cx); @@ -301,26 +305,32 @@ impl AppSettings { } } + /// Check if decoupling encryption key is enabled + pub fn is_nip4e_enabled(&self, cx: &App) -> bool { + self.inner.read(cx).nip4e + } + /// Check if the given relay is already authenticated - pub fn trusted_relay(&self, url: &RelayUrl, _cx: &App) -> bool { - self.values.trusted_relays.iter().any(|relay| { - relay.as_str_without_trailing_slash() == url.as_str_without_trailing_slash() - }) + pub fn trusted_relay(&self, url: &RelayUrl, cx: &App) -> bool { + self.inner + .read(cx) + .trusted_relays + .iter() + .any(|relay| relay == url.as_str_without_trailing_slash()) } /// Add a relay to the trusted list pub fn add_trusted_relay(&mut self, url: &RelayUrl, cx: &mut Context) { - self.values.trusted_relays.insert(url.clone()); - cx.notify(); - } - - /// Add a room configuration - pub fn add_room_config(&mut self, id: u64, config: RoomConfig, cx: &mut Context) { - self.values - .room_configs - .entry(id) - .and_modify(|this| *this = config) - .or_default(); - cx.notify(); + self.inner.update(cx, |this, cx| { + if !this + .trusted_relays + .iter() + .any(|relay| relay == url.as_str_without_trailing_slash()) + { + this.trusted_relays + .push(url.as_str_without_trailing_slash().to_string()); + cx.notify(); + } + }); } } diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs index a20325c..7c92d98 100644 --- a/crates/state/src/lib.rs +++ b/crates/state/src/lib.rs @@ -1,9 +1,7 @@ use std::collections::HashMap; -use std::path::PathBuf; -use std::sync::Arc; use std::time::Duration; -use anyhow::{Context as AnyhowContext, Error, anyhow}; +use anyhow::{Error, anyhow}; use common::config_dir; use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, SharedString, Task, Window}; use nostr_connect::prelude::*; @@ -13,15 +11,13 @@ use nostr_sdk::prelude::*; mod blossom; mod constants; -mod device; mod nip05; -mod signer; +mod nip4e; pub use blossom::*; pub use constants::*; -pub use device::*; +pub use nip4e::*; pub use nip05::*; -pub use signer::*; pub fn init(window: &mut Window, cx: &mut App) { // rustls uses the `aws_lc_rs` provider by default @@ -48,12 +44,6 @@ pub enum StateEvent { Connecting, /// Connected to the bootstrapping relay Connected, - /// Creating the signer - Creating, - /// Show the identity dialog - Show, - /// A new signer has been set - SignerSet, /// An error occurred Error(SharedString), } @@ -73,19 +63,8 @@ pub struct NostrRegistry { /// Nostr client client: Client, - /// Nostr signer - signer: Arc, - - /// All local stored identities - npubs: Entity>, - - /// Keys directory - key_dir: PathBuf, - - /// Master app keys used for various operations. - /// - /// Example: Nostr Connect and NIP-4e operations - app_keys: Keys, + /// Currently active signer + pub signer: Entity>, /// Tasks for asynchronous operations tasks: Vec>>, @@ -106,20 +85,7 @@ impl NostrRegistry { /// Create a new nostr instance fn new(window: &mut Window, cx: &mut Context) -> Self { - let key_dir = config_dir().join("keys"); - let app_keys = get_or_init_app_keys(cx).unwrap_or(Keys::generate()); - - // Construct the nostr signer - let signer = Arc::new(CoopSigner::new(app_keys.clone())); - - // Get all local stored npubs - let npubs = cx.new(|_| match Self::discover(&key_dir) { - Ok(npubs) => npubs, - Err(e) => { - log::error!("Failed to discover npubs: {e}"); - vec![] - } - }); + let signer = cx.new(|_| None); // Construct the nostr lmdb instance let lmdb = cx.foreground_executor().block_on(async move { @@ -130,16 +96,9 @@ impl NostrRegistry { // Construct the nostr client let client = ClientBuilder::default() - .signer(signer.clone()) .database(lmdb) .gossip(NostrGossipMemory::unbounded()) - .gossip_config( - GossipConfig::default() - .sync_initial_timeout(Duration::from_millis(100)) - .sync_idle_timeout(Duration::from_millis(100)) - .no_background_refresh(), - ) - .automatic_authentication(false) + .gossip_config(GossipConfig::default().no_background_refresh()) .connect_timeout(Duration::from_secs(10)) .sleep_when_idle(SleepWhenIdle::Enabled { timeout: Duration::from_secs(600), @@ -149,21 +108,11 @@ impl NostrRegistry { // Run at the end of current cycle cx.defer_in(window, |this, _window, cx| { this.connect(cx); - // Create an identity if none exists - if this.npubs.read(cx).is_empty() { - this.create_identity(cx); - } else { - // Show the identity dialog - cx.emit(StateEvent::Show); - } }); Self { client, signer, - npubs, - key_dir, - app_keys, tasks: vec![], } } @@ -173,46 +122,22 @@ impl NostrRegistry { self.client.clone() } - /// Get the nostr signer - pub fn signer(&self) -> Arc { - self.signer.clone() + /// Get the signer + pub fn signer(&self, cx: &App) -> Option { + self.signer.read(cx).clone() } - /// Get the npubs entity - pub fn npubs(&self) -> Entity> { - self.npubs.clone() + /// Get the public key of the signer + pub fn signer_pubkey(&self, cx: &App) -> Option { + self.signer.read(cx).as_ref().map(|s| s.public_key()) } - /// Get the app keys - pub fn keys(&self) -> Keys { - self.app_keys.clone() - } - - /// Discover all npubs in the keys directory - fn discover(dir: &PathBuf) -> Result, Error> { - // Ensure keys directory exists - std::fs::create_dir_all(dir)?; - - let files = std::fs::read_dir(dir)?; - let mut entries = Vec::new(); - let mut npubs: Vec = Vec::new(); - - for file in files.flatten() { - let metadata = file.metadata()?; - let modified_time = metadata.modified()?; - let name = file.file_name().into_string().unwrap().replace(".npub", ""); - entries.push((modified_time, name)); - } - - // Sort by modification time (most recent first) - entries.sort_by(|a, b| b.0.cmp(&a.0)); - - for (_, name) in entries { - let public_key = PublicKey::parse(&name)?; - npubs.push(public_key); - } - - Ok(npubs) + /// Set the signer to the given keys + pub fn set_signer(&mut self, new_keys: Keys, cx: &mut Context) { + self.signer.update(cx, |this, cx| { + *this = Some(new_keys); + cx.notify(); + }); } /// Connect to the bootstrapping relays @@ -234,10 +159,7 @@ impl NostrRegistry { } // Connect to all added relays - client - .connect() - .and_wait(Duration::from_secs(TIMEOUT)) - .await; + client.connect().await; Ok(()) }); @@ -260,319 +182,8 @@ impl NostrRegistry { })); } - /// Get the secret for a given npub. - pub fn get_secret( - &self, - public_key: PublicKey, - cx: &App, - ) -> Task, Error>> { - let npub = public_key.to_bech32().unwrap(); - let key_path = self.key_dir.join(format!("{}.npub", npub)); - let app_keys = self.app_keys.clone(); - - if let Ok(payload) = std::fs::read_to_string(key_path) { - if !payload.is_empty() { - cx.background_spawn(async move { - let decrypted = app_keys.nip44_decrypt(&public_key, &payload).await?; - let secret = SecretKey::parse(&decrypted)?; - let keys = Keys::new(secret); - - Ok(keys.into_nostr_signer()) - }) - } else { - self.get_secret_keyring(&npub, cx) - } - } else { - self.get_secret_keyring(&npub, cx) - } - } - - /// Get the secret for a given npub in the OS credentials store. - #[deprecated = "Use get_secret instead"] - fn get_secret_keyring( - &self, - user: &str, - cx: &App, - ) -> Task, Error>> { - let read = cx.read_credentials(user); - let app_keys = self.app_keys.clone(); - - cx.background_spawn(async move { - let (_, secret) = read - .await - .map_err(|_| anyhow!("Failed to get signer. Please re-import the secret key"))? - .ok_or_else(|| anyhow!("Failed to get signer. Please re-import the secret key"))?; - - // Try to parse as a direct secret key first - if let Ok(secret_key) = SecretKey::from_slice(&secret) { - return Ok(Keys::new(secret_key).into_nostr_signer()); - } - - // Convert the secret into string - let sec = String::from_utf8(secret) - .map_err(|_| anyhow!("Failed to parse secret as UTF-8"))?; - - // Try to parse as a NIP-46 URI - let uri = - NostrConnectUri::parse(&sec).map_err(|_| anyhow!("Failed to parse NIP-46 URI"))?; - - let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT); - let mut nip46 = NostrConnect::new(uri, app_keys, timeout, None)?; - - // Set the auth URL handler - nip46.auth_url_handler(CoopAuthUrlHandler); - - Ok(nip46.into_nostr_signer()) - }) - } - - /// Add a new npub to the keys directory - fn write_secret( - &self, - public_key: PublicKey, - secret: String, - cx: &App, - ) -> Task> { - let npub = public_key.to_bech32().unwrap(); - let key_path = self.key_dir.join(format!("{}.npub", npub)); - let app_keys = self.app_keys.clone(); - - cx.background_spawn(async move { - // If the secret starts with "bunker://" (nostr connect), use it directly; otherwise, encrypt it - let content = if secret.starts_with("bunker://") { - secret - } else { - app_keys.nip44_encrypt(&public_key, &secret).await? - }; - - // Write the encrypted secret to the keys directory - smol::fs::write(key_path, &content).await?; - - Ok(()) - }) - } - - /// Remove a secret - pub fn remove_secret(&mut self, public_key: &PublicKey, cx: &mut Context) { - let public_key = public_key.to_owned(); - let npub = public_key.to_bech32().unwrap(); - - let keys_dir = config_dir().join("keys"); - let key_path = keys_dir.join(format!("{}.npub", npub)); - - // Remove the secret file from the keys directory - std::fs::remove_file(key_path).ok(); - - self.npubs.update(cx, |this, cx| { - this.retain(|k| k != &public_key); - cx.notify(); - }); - } - - /// Create a new identity - pub fn create_identity(&mut self, cx: &mut Context) { - let client = self.client(); - let keys = Keys::generate(); - let async_keys = keys.clone(); - - // Emit creating event - cx.emit(StateEvent::Creating); - - // Create the write secret task - let write_secret = - self.write_secret(keys.public_key(), keys.secret_key().to_secret_hex(), cx); - - // Run async tasks in background - let task: Task> = cx.background_spawn(async move { - let signer = async_keys.into_nostr_signer(); - - // Construct relay list event - let relay_list = default_relay_list(); - let event = EventBuilder::relay_list(relay_list).sign(&signer).await?; - - // Publish relay list - client - .send_event(&event) - .to(BOOTSTRAP_RELAYS) - .ack_policy(AckPolicy::none()) - .await?; - - // Construct the default metadata - let name = petname::petname(2, "-").unwrap_or("Cooper".to_string()); - let avatar = Url::parse(&format!("https://avatar.vercel.sh/{name}")).unwrap(); - let metadata = Metadata::new().display_name(&name).picture(avatar); - let event = EventBuilder::metadata(&metadata).sign(&signer).await?; - - // Publish metadata event - client - .send_event(&event) - .to_nip65() - .ack_policy(AckPolicy::none()) - .await?; - - // Construct the default contact list - let contacts = vec![Contact::new(PublicKey::parse(COOP_PUBKEY).unwrap())]; - let event = EventBuilder::contact_list(contacts).sign(&signer).await?; - - // Publish contact list event - client - .send_event(&event) - .to_nip65() - .ack_policy(AckPolicy::none()) - .await?; - - // Construct the default messaging relay list - let relays = default_messaging_relays(); - let event = EventBuilder::nip17_relay_list(relays).sign(&signer).await?; - - // Publish messaging relay list event - client.send_event(&event).to_nip65().await?; - - // Write user's credentials to the system keyring - write_secret.await?; - - Ok(()) - }); - - self.tasks.push(cx.spawn(async move |this, cx| { - match task.await { - Ok(_) => { - this.update(cx, |this, cx| { - this.set_signer(keys, cx); - })?; - } - Err(e) => { - this.update(cx, |_this, cx| { - cx.emit(StateEvent::error(e.to_string())); - })?; - } - }; - - Ok(()) - })); - } - - /// Set the signer for the nostr client and verify the public key - pub fn set_signer(&mut self, new: T, cx: &mut Context) - where - T: NostrSigner + 'static, - { - let client = self.client(); - let signer = self.signer(); - - // Create a task to update the signer and verify the public key - let task: Task> = cx.background_spawn(async move { - // Update signer and unsubscribe - signer.switch(new).await; - client.unsubscribe_all().await?; - - // Verify and get public key - let signer = client.signer().context("Signer not found")?; - let public_key = signer.get_public_key().await?; - - log::info!("Signer's public key: {}", public_key); - Ok(public_key) - }); - - self.tasks.push(cx.spawn(async move |this, cx| { - match task.await { - Ok(public_key) => { - this.update(cx, |this, cx| { - // Add public key to npubs if not already present - this.npubs.update(cx, |this, cx| { - if !this.contains(&public_key) { - this.push(public_key); - cx.notify(); - } - }); - - // Emit signer changed event - cx.emit(StateEvent::SignerSet); - })?; - } - Err(e) => { - this.update(cx, |_this, cx| { - cx.emit(StateEvent::error(e.to_string())); - })?; - } - }; - - Ok(()) - })); - } - - /// Add a key signer to keyring - pub fn add_key_signer(&mut self, keys: &Keys, cx: &mut Context) { - let keys = keys.clone(); - let write_secret = - self.write_secret(keys.public_key(), keys.secret_key().to_secret_hex(), cx); - - self.tasks.push(cx.spawn(async move |this, cx| { - match write_secret.await { - Ok(_) => { - this.update(cx, |this, cx| { - this.set_signer(keys, cx); - })?; - } - Err(e) => { - this.update(cx, |_this, cx| { - cx.emit(StateEvent::error(e.to_string())); - })?; - } - }; - - Ok(()) - })); - } - - /// Add a nostr connect signer to keyring - pub fn add_nip46_signer(&mut self, nip46: &NostrConnect, cx: &mut Context) { - let nip46 = nip46.clone(); - let async_nip46 = nip46.clone(); - - // Connect and verify the remote signer - let task: Task> = - cx.background_spawn(async move { - let uri = async_nip46.bunker_uri().await?; - let public_key = async_nip46.get_public_key().await?; - - Ok((public_key, uri)) - }); - - self.tasks.push(cx.spawn(async move |this, cx| { - match task.await { - Ok((public_key, uri)) => { - // Create the write secret task - let write_secret = this.read_with(cx, |this, cx| { - this.write_secret(public_key, uri.to_string(), cx) - })?; - - match write_secret.await { - Ok(_) => { - this.update(cx, |this, cx| { - this.set_signer(nip46, cx); - })?; - } - Err(e) => { - this.update(cx, |_this, cx| { - cx.emit(StateEvent::error(e.to_string())); - })?; - } - } - } - Err(e) => { - this.update(cx, |_this, cx| { - cx.emit(StateEvent::error(e.to_string())); - })?; - } - }; - - Ok(()) - })); - } - /// Get the public key of a NIP-05 address - pub fn get_address(&self, addr: Nip05Address, cx: &App) -> Task> { + pub fn query_address(&self, addr: Nip05Address, cx: &App) -> Task> { let client = self.client(); let http_client = cx.http_client(); @@ -609,7 +220,7 @@ impl NostrRegistry { // Get the address task if the query is a valid NIP-05 address let address_task = if let Ok(addr) = Nip05Address::parse(&query) { - Some(self.get_address(addr, cx)) + Some(self.query_address(addr, cx)) } else { None }; @@ -681,13 +292,19 @@ impl NostrRegistry { let client = self.client(); let query = query.to_string(); + let Some(signer) = self.signer.read(cx).clone() else { + return Task::ready(Err(anyhow!("Signer is required"))); + }; + cx.background_spawn(async move { // Construct a vertex request event - let builder = EventBuilder::new(Kind::Custom(5315), "").tags(vec![ - Tag::custom(TagKind::custom("param"), vec!["search", &query]), - Tag::custom(TagKind::custom("param"), vec!["limit", "10"]), - ]); - let event = client.sign_event_builder(builder).await?; + let event = EventBuilder::new(Kind::Custom(5315), "") + .tags(vec![ + Tag::custom("param", vec!["search", &query]), + Tag::custom("param", vec!["limit", "10"]), + ]) + .finalize_async(&signer) + .await?; // Send the event to vertex relays let output = client.send_event(&event).to(WOT_RELAYS).await?; @@ -737,78 +354,3 @@ impl NostrRegistry { }) } } - -/// Get or create new app keys -fn get_or_init_app_keys(cx: &App) -> Result { - let read = cx.read_credentials(CLIENT_NAME); - let stored_keys: Option = cx.foreground_executor().block_on(async move { - if let Ok(Some((_, secret))) = read.await { - SecretKey::from_slice(&secret).map(Keys::new).ok() - } else { - None - } - }); - - if let Some(keys) = stored_keys { - Ok(keys) - } else { - let keys = Keys::generate(); - let user = keys.public_key().to_hex(); - let secret = keys.secret_key().to_secret_bytes(); - let write = cx.write_credentials(CLIENT_NAME, &user, &secret); - - cx.foreground_executor().block_on(async move { - if let Err(e) = write.await { - log::error!("Keyring not available or panic: {e}") - } - }); - - Ok(keys) - } -} - -fn default_relay_list() -> Vec<(RelayUrl, Option)> { - vec![ - ( - RelayUrl::parse("wss://relay.nostr.net").unwrap(), - Some(RelayMetadata::Write), - ), - ( - RelayUrl::parse("wss://relay.primal.net").unwrap(), - Some(RelayMetadata::Write), - ), - ( - RelayUrl::parse("wss://relay.damus.io").unwrap(), - Some(RelayMetadata::Read), - ), - ( - RelayUrl::parse("wss://nos.lol").unwrap(), - Some(RelayMetadata::Read), - ), - ( - RelayUrl::parse("wss://nostr.superfriends.online").unwrap(), - None, - ), - ] -} - -fn default_messaging_relays() -> Vec { - vec![ - RelayUrl::parse("wss://nos.lol").unwrap(), - RelayUrl::parse("wss://nip17.com").unwrap(), - RelayUrl::parse("wss://auth.nostr1.com").unwrap(), - ] -} - -#[derive(Debug, Clone)] -pub struct CoopAuthUrlHandler; - -impl AuthUrlHandler for CoopAuthUrlHandler { - #[allow(mismatched_lifetime_syntaxes)] - fn on_auth_url(&self, auth_url: Url) -> BoxedFuture> { - Box::pin(async move { - webbrowser::open(auth_url.as_str())?; - Ok(()) - }) - } -} diff --git a/crates/state/src/device.rs b/crates/state/src/nip4e.rs similarity index 92% rename from crates/state/src/device.rs rename to crates/state/src/nip4e.rs index 4aae264..475795c 100644 --- a/crates/state/src/device.rs +++ b/crates/state/src/nip4e.rs @@ -16,14 +16,15 @@ impl From<&Event> for Announcement { let public_key = val .tags .iter() - .find(|tag| tag.kind().as_str() == "n") + .find(|tag| tag.kind() == "n") .and_then(|tag| tag.content()) .and_then(|c| PublicKey::parse(c).ok()) .unwrap_or(val.pubkey); let client_name = val .tags - .find(TagKind::Client) + .iter() + .find(|tag| tag.kind() == "client") .and_then(|tag| tag.content()) .map(|c| c.to_string()); diff --git a/crates/state/src/signer.rs b/crates/state/src/signer.rs deleted file mode 100644 index c6e9b20..0000000 --- a/crates/state/src/signer.rs +++ /dev/null @@ -1,134 +0,0 @@ -use std::borrow::Cow; -use std::result::Result; -use std::sync::Arc; - -use nostr_sdk::prelude::*; -use smol::lock::RwLock; - -#[derive(Debug)] -pub struct CoopSigner { - /// User's signer - signer: RwLock>, - - /// User's signer public key - signer_pkey: RwLock>, - - /// Specific signer for encryption purposes - encryption_signer: RwLock>>, -} - -impl CoopSigner { - pub fn new(signer: T) -> Self - where - T: IntoNostrSigner, - { - Self { - signer: RwLock::new(signer.into_nostr_signer()), - signer_pkey: RwLock::new(None), - encryption_signer: RwLock::new(None), - } - } - - /// Get the current signer. - pub async fn get(&self) -> Arc { - self.signer.read().await.clone() - } - - /// Get the encryption signer. - pub async fn get_encryption_signer(&self) -> Option> { - self.encryption_signer.read().await.clone() - } - - /// Get public key - /// - /// Ensure to call this method after the signer has been initialized. - /// Otherwise, it will panic. - pub fn public_key(&self) -> Option { - *self.signer_pkey.read_blocking() - } - - /// Switch the current signer to a new signer. - pub async fn switch(&self, new: T) - where - T: IntoNostrSigner, - { - let new_signer = new.into_nostr_signer(); - let public_key = new_signer.get_public_key().await.ok(); - let mut signer = self.signer.write().await; - let mut signer_pkey = self.signer_pkey.write().await; - let mut encryption_signer = self.encryption_signer.write().await; - - // Switch to the new signer - *signer = new_signer; - - // Update the public key - *signer_pkey = public_key; - - // Reset the encryption signer - *encryption_signer = None; - } - - /// Set the encryption signer. - pub async fn set_encryption_signer(&self, new: T) - where - T: IntoNostrSigner, - { - let mut encryption_signer = self.encryption_signer.write().await; - *encryption_signer = Some(new.into_nostr_signer()); - } -} - -impl NostrSigner for CoopSigner { - #[allow(mismatched_lifetime_syntaxes)] - fn backend(&self) -> SignerBackend { - SignerBackend::Custom(Cow::Borrowed("custom")) - } - - fn get_public_key<'a>(&'a self) -> BoxedFuture<'a, Result> { - Box::pin(async move { self.get().await.get_public_key().await }) - } - - fn sign_event<'a>( - &'a self, - unsigned: UnsignedEvent, - ) -> BoxedFuture<'a, Result> { - Box::pin(async move { self.get().await.sign_event(unsigned).await }) - } - - fn nip04_encrypt<'a>( - &'a self, - public_key: &'a PublicKey, - content: &'a str, - ) -> BoxedFuture<'a, Result> { - Box::pin(async move { self.get().await.nip04_encrypt(public_key, content).await }) - } - - fn nip04_decrypt<'a>( - &'a self, - public_key: &'a PublicKey, - encrypted_content: &'a str, - ) -> BoxedFuture<'a, Result> { - Box::pin(async move { - self.get() - .await - .nip04_decrypt(public_key, encrypted_content) - .await - }) - } - - fn nip44_encrypt<'a>( - &'a self, - public_key: &'a PublicKey, - content: &'a str, - ) -> BoxedFuture<'a, Result> { - Box::pin(async move { self.get().await.nip44_encrypt(public_key, content).await }) - } - - fn nip44_decrypt<'a>( - &'a self, - public_key: &'a PublicKey, - payload: &'a str, - ) -> BoxedFuture<'a, Result> { - Box::pin(async move { self.get().await.nip44_decrypt(public_key, payload).await }) - } -} diff --git a/crates/title_bar/Cargo.toml b/crates/title_bar/Cargo.toml deleted file mode 100644 index ee0cf1f..0000000 --- a/crates/title_bar/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "title_bar" -version.workspace = true -edition.workspace = true -publish.workspace = true - -[dependencies] -common = { path = "../common" } -theme = { path = "../theme" } -ui = { path = "../ui" } - -nostr-sdk.workspace = true -gpui.workspace = true -smol.workspace = true -smallvec.workspace = true -anyhow.workspace = true -log.workspace = true - -[target.'cfg(target_os = "windows")'.dependencies] -windows = { version = "0.61", features = ["Wdk_System_SystemServices"] } - -[target.'cfg(target_os = "linux")'.dependencies] -linicon = "2.3.0" diff --git a/crates/title_bar/src/lib.rs b/crates/title_bar/src/lib.rs deleted file mode 100644 index 426bd43..0000000 --- a/crates/title_bar/src/lib.rs +++ /dev/null @@ -1,172 +0,0 @@ -#[cfg(target_os = "linux")] -use gpui::MouseButton; -use gpui::prelude::FluentBuilder; -use gpui::{ - AnyElement, Context, Decorations, Hsla, InteractiveElement as _, IntoElement, ParentElement, - Pixels, Render, StatefulInteractiveElement as _, Styled, Window, WindowControlArea, px, -}; -use smallvec::{SmallVec, smallvec}; -use theme::{ActiveTheme, CLIENT_SIDE_DECORATION_ROUNDING, PlatformKind}; -use ui::h_flex; - -#[cfg(target_os = "linux")] -use crate::platforms::linux::LinuxWindowControls; -use crate::platforms::mac::TRAFFIC_LIGHT_PADDING; -use crate::platforms::windows::WindowsWindowControls; - -mod platforms; - -/// Titlebar -pub struct TitleBar { - /// Children elements of the title bar. - children: SmallVec<[AnyElement; 2]>, - - /// Whether the title bar is currently being moved. - should_move: bool, -} - -impl TitleBar { - pub fn new() -> Self { - Self { - children: smallvec![], - should_move: false, - } - } - - #[cfg(not(target_os = "windows"))] - pub fn height(&self, window: &mut Window) -> Pixels { - (1.75 * window.rem_size()).max(px(34.)) - } - - #[cfg(target_os = "windows")] - pub fn height(&self, _window: &mut Window) -> Pixels { - px(32.) - } - - pub fn titlebar_color(&self, window: &mut Window, cx: &mut Context) -> Hsla { - if cfg!(any(target_os = "linux", target_os = "freebsd")) { - if window.is_window_active() && !self.should_move { - cx.theme().title_bar - } else { - cx.theme().title_bar_inactive - } - } else { - cx.theme().title_bar - } - } - - pub fn set_children(&mut self, children: T) - where - T: IntoIterator, - { - self.children = children.into_iter().collect(); - } -} - -impl Default for TitleBar { - fn default() -> Self { - Self::new() - } -} - -impl Render for TitleBar { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let height = self.height(window); - let color = self.titlebar_color(window, cx); - let children = std::mem::take(&mut self.children); - - #[cfg(target_os = "linux")] - let supported_controls = window.window_controls(); - let decorations = window.window_decorations(); - - h_flex() - .window_control_area(WindowControlArea::Drag) - .h(height) - .w_full() - .map(|this| { - if window.is_fullscreen() { - this.px_2() - } else if cx.theme().platform.is_mac() { - this.pr_2().pl(px(TRAFFIC_LIGHT_PADDING)) - } else { - this.px_2() - } - }) - .map(|this| match decorations { - Decorations::Server => this, - Decorations::Client { tiling } => this - .when(!(tiling.top || tiling.right), |div| { - div.rounded_tr(CLIENT_SIDE_DECORATION_ROUNDING) - }) - .when(!(tiling.top || tiling.left), |div| { - div.rounded_tl(CLIENT_SIDE_DECORATION_ROUNDING) - }), - }) - .bg(color) - .border_b_1() - .border_color(cx.theme().border) - .content_stretch() - .child( - h_flex() - .id("title-bar") - .justify_between() - .w_full() - .when(cx.theme().platform.is_mac(), |this| { - this.on_click(|event, window, _| { - if event.click_count() == 2 { - window.titlebar_double_click(); - } - }) - }) - .when(cx.theme().platform.is_linux(), |this| { - this.on_click(|event, window, _| { - if event.click_count() == 2 { - window.zoom_window(); - } - }) - }) - .when(!cx.theme().platform.is_mac(), |this| this.pr_2()) - .children(children), - ) - .when(!window.is_fullscreen(), |this| match cx.theme().platform { - PlatformKind::Linux => { - #[cfg(target_os = "linux")] - if matches!(decorations, Decorations::Client { .. }) { - this.child(LinuxWindowControls::new(None)) - .when(supported_controls.window_menu, |this| { - this.on_mouse_down(MouseButton::Right, move |ev, window, _| { - window.show_window_menu(ev.position) - }) - }) - .on_mouse_move(cx.listener(move |this, _ev, window, _| { - if this.should_move { - this.should_move = false; - window.start_window_move(); - } - })) - .on_mouse_down_out(cx.listener(move |this, _ev, _window, _cx| { - this.should_move = false; - })) - .on_mouse_up( - MouseButton::Left, - cx.listener(move |this, _ev, _window, _cx| { - this.should_move = false; - }), - ) - .on_mouse_down( - MouseButton::Left, - cx.listener(move |this, _ev, _window, _cx| { - this.should_move = true; - }), - ) - } else { - this - } - #[cfg(not(target_os = "linux"))] - this - } - PlatformKind::Windows => this.child(WindowsWindowControls::new(height)), - PlatformKind::Mac => this, - }) - } -} diff --git a/crates/title_bar/src/platforms/linux.rs b/crates/title_bar/src/platforms/linux.rs deleted file mode 100644 index 1f9731d..0000000 --- a/crates/title_bar/src/platforms/linux.rs +++ /dev/null @@ -1,227 +0,0 @@ -use std::collections::HashMap; -use std::sync::OnceLock; - -use gpui::prelude::FluentBuilder; -use gpui::{ - svg, Action, App, InteractiveElement, IntoElement, MouseButton, ParentElement, RenderOnce, - SharedString, StatefulInteractiveElement, Styled, Window, -}; -use linicon::{lookup_icon, IconType}; -use theme::ActiveTheme; -use ui::{h_flex, Icon, IconName, Sizable}; - -#[derive(IntoElement)] -pub struct LinuxWindowControls { - close_window_action: Option>, -} - -impl LinuxWindowControls { - pub fn new(close_window_action: Option>) -> Self { - Self { - close_window_action, - } - } -} - -impl RenderOnce for LinuxWindowControls { - fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement { - let supported_controls = window.window_controls(); - - h_flex() - .id("linux-window-controls") - .gap_2() - .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) - .when(supported_controls.minimize, |this| { - this.child(WindowControl::new( - LinuxControl::Minimize, - IconName::WindowMinimize, - )) - }) - .when(supported_controls.maximize, |this| { - this.child({ - if window.is_maximized() { - WindowControl::new(LinuxControl::Restore, IconName::WindowRestore) - } else { - WindowControl::new(LinuxControl::Maximize, IconName::WindowMaximize) - } - }) - }) - .child( - WindowControl::new(LinuxControl::Close, IconName::WindowClose) - .when_some(self.close_window_action, |this, close_action| { - this.close_action(close_action) - }), - ) - } -} - -#[derive(IntoElement)] -pub struct WindowControl { - kind: LinuxControl, - fallback: IconName, - close_action: Option>, -} - -impl WindowControl { - pub fn new(kind: LinuxControl, fallback: IconName) -> Self { - Self { - kind, - fallback, - close_action: None, - } - } - - pub fn close_action(mut self, action: Box) -> Self { - self.close_action = Some(action); - self - } - - pub fn is_gnome(&self) -> bool { - matches!(detect_desktop_environment(), DesktopEnvironment::Gnome) - } -} - -impl RenderOnce for WindowControl { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let is_gnome = self.is_gnome(); - - h_flex() - .id(self.kind.as_icon_name()) - .group("") - .justify_center() - .items_center() - .rounded_full() - .size_6() - .when(is_gnome, |this| { - this.bg(cx.theme().ghost_element_background_alt) - .hover(|this| this.bg(cx.theme().ghost_element_hover)) - .active(|this| this.bg(cx.theme().ghost_element_active)) - }) - .map(|this| { - if let Some(Some(path)) = linux_controls().get(&self.kind).cloned() { - this.child( - svg() - .external_path(SharedString::from(path)) - .size_4() - .text_color(cx.theme().text), - ) - } else { - this.child(Icon::new(self.fallback).small().text_color(cx.theme().text)) - } - }) - .on_mouse_move(|_, _window, cx| cx.stop_propagation()) - .on_click(move |_, window, cx| { - cx.stop_propagation(); - match self.kind { - LinuxControl::Minimize => window.minimize_window(), - LinuxControl::Restore => window.zoom_window(), - LinuxControl::Maximize => window.zoom_window(), - LinuxControl::Close => cx.quit(), - } - }) - } -} - -static DE: OnceLock = OnceLock::new(); -static LINUX_CONTROLS: OnceLock>> = OnceLock::new(); - -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum DesktopEnvironment { - Gnome, - Kde, - Unknown, -} - -/// Detect the current desktop environment -pub fn detect_desktop_environment() -> &'static DesktopEnvironment { - DE.get_or_init(|| { - // Try to use environment variables first - if let Ok(output) = std::env::var("XDG_CURRENT_DESKTOP") { - let desktop = output.to_lowercase(); - if desktop.contains("gnome") { - return DesktopEnvironment::Gnome; - } else if desktop.contains("kde") { - return DesktopEnvironment::Kde; - } - } - - // Fallback detection methods - if let Ok(output) = std::env::var("DESKTOP_SESSION") { - let session = output.to_lowercase(); - if session.contains("gnome") { - return DesktopEnvironment::Gnome; - } else if session.contains("kde") || session.contains("plasma") { - return DesktopEnvironment::Kde; - } - } - - DesktopEnvironment::Unknown - }) -} - -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] -pub enum LinuxControl { - Minimize, - Restore, - Maximize, - Close, -} - -impl LinuxControl { - pub fn as_icon_name(&self) -> &'static str { - match self { - LinuxControl::Close => "window-close", - LinuxControl::Minimize => "window-minimize", - LinuxControl::Maximize => "window-maximize", - LinuxControl::Restore => "window-restore", - } - } -} - -fn linux_controls() -> &'static HashMap> { - LINUX_CONTROLS.get_or_init(|| { - let mut icons = HashMap::new(); - icons.insert(LinuxControl::Close, None); - icons.insert(LinuxControl::Minimize, None); - icons.insert(LinuxControl::Maximize, None); - icons.insert(LinuxControl::Restore, None); - - let icon_names = [ - (LinuxControl::Close, vec!["window-close", "dialog-close"]), - ( - LinuxControl::Minimize, - vec!["window-minimize", "window-lower"], - ), - ( - LinuxControl::Maximize, - vec!["window-maximize", "window-expand"], - ), - ( - LinuxControl::Restore, - vec!["window-restore", "window-return"], - ), - ]; - - for (control, icon_names) in icon_names { - for icon_name in icon_names { - // Try GNOME-style naming first - let mut control_icon = lookup_icon(format!("{icon_name}-symbolic")) - .find(|icon| matches!(icon, Ok(icon) if icon.icon_type == IconType::SVG)); - - // If not found, try KDE-style naming - if control_icon.is_none() { - control_icon = lookup_icon(icon_name) - .find(|icon| matches!(icon, Ok(icon) if icon.icon_type == IconType::SVG)); - } - - if let Some(Ok(icon)) = control_icon { - icons - .entry(control) - .and_modify(|v| *v = Some(icon.path.to_string_lossy().to_string())); - } - } - } - - icons - }) -} diff --git a/crates/title_bar/src/platforms/mac.rs b/crates/title_bar/src/platforms/mac.rs deleted file mode 100644 index 526e245..0000000 --- a/crates/title_bar/src/platforms/mac.rs +++ /dev/null @@ -1,6 +0,0 @@ -/// Use pixels here instead of a rem-based size because the macOS traffic -/// lights are a static size, and don't scale with the rest of the UI. -/// -/// Magic number: There is one extra pixel of padding on the left side due to -/// the 1px border around the window on macOS apps. -pub const TRAFFIC_LIGHT_PADDING: f32 = 80.; diff --git a/crates/title_bar/src/platforms/mod.rs b/crates/title_bar/src/platforms/mod.rs deleted file mode 100644 index e0ff781..0000000 --- a/crates/title_bar/src/platforms/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -#[cfg(target_os = "linux")] -pub mod linux; -pub mod mac; -pub mod windows; diff --git a/crates/title_bar/src/platforms/windows.rs b/crates/title_bar/src/platforms/windows.rs deleted file mode 100644 index f28180b..0000000 --- a/crates/title_bar/src/platforms/windows.rs +++ /dev/null @@ -1,147 +0,0 @@ -use gpui::prelude::FluentBuilder; -use gpui::{ - div, px, App, ElementId, Hsla, InteractiveElement, IntoElement, ParentElement, Pixels, - RenderOnce, Rgba, StatefulInteractiveElement, Styled, Window, WindowControlArea, -}; -use theme::ActiveTheme; -use ui::h_flex; - -#[derive(IntoElement)] -pub struct WindowsWindowControls { - button_height: Pixels, -} - -impl WindowsWindowControls { - pub fn new(button_height: Pixels) -> Self { - Self { button_height } - } - - #[cfg(not(target_os = "windows"))] - fn get_font() -> &'static str { - "Segoe Fluent Icons" - } - - #[cfg(target_os = "windows")] - fn get_font() -> &'static str { - use windows::Wdk::System::SystemServices::RtlGetVersion; - - let mut version = unsafe { std::mem::zeroed() }; - let status = unsafe { RtlGetVersion(&mut version) }; - - if status.is_ok() && version.dwBuildNumber >= 22000 { - "Segoe Fluent Icons" - } else { - "Segoe MDL2 Assets" - } - } -} - -impl RenderOnce for WindowsWindowControls { - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - let close_button_hover_color = Rgba { - r: 232.0 / 255.0, - g: 17.0 / 255.0, - b: 32.0 / 255.0, - a: 1.0, - }; - - let button_hover_color = cx.theme().ghost_element_hover; - let button_active_color = cx.theme().ghost_element_active; - - div() - .id("windows-window-controls") - .font_family(Self::get_font()) - .flex() - .flex_row() - .justify_center() - .content_stretch() - .max_h(self.button_height) - .min_h(self.button_height) - .child(WindowsCaptionButton::new( - "minimize", - WindowsCaptionButtonIcon::Minimize, - button_hover_color, - button_active_color, - )) - .child(WindowsCaptionButton::new( - "maximize-or-restore", - if window.is_maximized() { - WindowsCaptionButtonIcon::Restore - } else { - WindowsCaptionButtonIcon::Maximize - }, - button_hover_color, - button_active_color, - )) - .child(WindowsCaptionButton::new( - "close", - WindowsCaptionButtonIcon::Close, - close_button_hover_color, - button_active_color, - )) - } -} - -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] -enum WindowsCaptionButtonIcon { - Minimize, - Restore, - Maximize, - Close, -} - -#[derive(IntoElement)] -struct WindowsCaptionButton { - id: ElementId, - icon: WindowsCaptionButtonIcon, - hover_background_color: Hsla, - active_background_color: Hsla, -} - -impl WindowsCaptionButton { - pub fn new( - id: impl Into, - icon: WindowsCaptionButtonIcon, - hover_background_color: impl Into, - active_background_color: impl Into, - ) -> Self { - Self { - id: id.into(), - icon, - hover_background_color: hover_background_color.into(), - active_background_color: active_background_color.into(), - } - } -} - -impl RenderOnce for WindowsCaptionButton { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - h_flex() - .id(self.id) - .justify_center() - .content_center() - .occlude() - .w(px(36.)) - .h_full() - .text_size(px(10.0)) - .hover(|style| style.bg(self.hover_background_color)) - .active(|style| style.bg(self.active_background_color)) - .map(|this| match self.icon { - WindowsCaptionButtonIcon::Close => { - this.window_control_area(WindowControlArea::Close) - } - WindowsCaptionButtonIcon::Maximize | WindowsCaptionButtonIcon::Restore => { - this.window_control_area(WindowControlArea::Max) - } - WindowsCaptionButtonIcon::Minimize => { - this.window_control_area(WindowControlArea::Min) - } - }) - .child(match self.icon { - WindowsCaptionButtonIcon::Minimize => "\u{e921}", - WindowsCaptionButtonIcon::Restore => "\u{e923}", - WindowsCaptionButtonIcon::Maximize => "\u{e922}", - WindowsCaptionButtonIcon::Close => "\u{e8bb}", - }) - } -} diff --git a/crates/ui/src/input/display_map/mod.rs b/crates/ui/src/input/display_map/mod.rs index 41fcfc6..11738a5 100644 --- a/crates/ui/src/input/display_map/mod.rs +++ b/crates/ui/src/input/display_map/mod.rs @@ -1,12 +1,4 @@ -/// Display mapping system for Editor/Input. -/// -/// This module implements a layered display mapping architecture: -/// - **WrapMap**: Handles soft-wrapping (buffer → wrap rows) -/// - **FoldMap**: Handles folding (wrap rows → display rows) -/// - **DisplayMap**: Public facade for Editor/Input -/// -/// The goal is to provide a clean, unified API where Editor only needs to know -/// about `BufferPoint ↔ DisplayPoint` mapping, without worrying about internal wrap/fold complexity. +#[allow(clippy::module_inception)] mod display_map; mod fold_map; #[cfg(not(target_family = "wasm"))] diff --git a/crates/ui/src/input/display_map/text_wrapper.rs b/crates/ui/src/input/display_map/text_wrapper.rs index 99acd14..dadb133 100644 --- a/crates/ui/src/input/display_map/text_wrapper.rs +++ b/crates/ui/src/input/display_map/text_wrapper.rs @@ -1,8 +1,7 @@ use std::ops::Range; -use gpui::Half; use gpui::{ - App, Font, LineFragment, Pixels, Point, ShapedLine, Size, TextAlign, Window, point, px, + App, Font, Half, LineFragment, Pixels, Point, ShapedLine, Size, TextAlign, Window, point, px, size, }; use ropey::Rope; @@ -97,7 +96,7 @@ impl TextWrapper { /// Get the line item by row index. #[inline] pub(crate) fn line(&self, row: usize) -> Option<&LineItem> { - self.lines.iter().skip(row).next() + self.lines.get(row) } pub(crate) fn set_wrap_width(&mut self, wrap_width: Option, cx: &mut App) { @@ -228,7 +227,7 @@ impl TextWrapper { }); } - if self.lines.len() == 0 { + if self.lines.is_empty() { self.lines = new_lines; } else { self.lines.splice(rows_range, new_lines); @@ -246,7 +245,7 @@ impl TextWrapper { /// /// If the `text` is the same as the current text, do nothing. fn update_all(&mut self, text: &Rope, cx: &mut App) { - self.update(text, &(0..text.len()), &text, cx); + self.update(text, &(0..text.len()), text, cx); } /// Return display point (with soft wrap) from the given byte offset in the text. @@ -278,7 +277,8 @@ impl TextWrapper { // Otherwise return the eof of the line. let last_range = line.wrapped_lines.last().unwrap_or(&(0..0)); let ix = line.lines_len().saturating_sub(1); - return WrapDisplayPoint::new(wrapped_row + ix, ix, last_range.len()); + + WrapDisplayPoint::new(wrapped_row + ix, ix, last_range.len()) } /// Return byte offset in the text from the given display point (with soft wrap). @@ -301,7 +301,7 @@ impl TextWrapper { wrapped_row += line.lines_len(); } - return self.text.len(); + self.text.len() } pub(crate) fn display_point_to_point(&self, point: WrapDisplayPoint) -> TreeSitterPoint { @@ -580,351 +580,3 @@ impl LineLayout { } } } - -#[cfg(test)] -mod tests { - use super::*; - use std::rc::Rc; - - use gpui::{Boundary, FontFeatures, FontStyle, FontWeight, px}; - - #[test] - fn test_update() { - let font = gpui::Font { - family: "Arial".into(), - weight: FontWeight::default(), - style: FontStyle::Normal, - features: FontFeatures::default(), - fallbacks: None, - }; - - let mut wrapper = TextWrapper::new(font, px(14.), None); - let mut text = Rope::from( - "Hello, 世界!\r\nThis is second line.\nThis is third line.\n这里是第 4 行。", - ); - - fn fake_wrap_line(_line: &str, _wrap_width: Pixels) -> Vec { - vec![] - } - - #[track_caller] - fn assert_wrapper_lines(text: &Rope, wrapper: &TextWrapper, expected_lines: &[&[&str]]) { - let mut actual_lines = vec![]; - let mut offset = 0; - for line in wrapper.lines.iter() { - actual_lines.push( - line.wrapped_lines - .iter() - .map(|range| text.slice(offset + range.start..offset + range.end)) - .collect::>(), - ); - // +1 \n - offset += line.len() + 1; - } - assert_eq!(actual_lines, expected_lines); - } - - wrapper._update(&text, &(0..text.len()), &text, &mut fake_wrap_line); - assert_eq!(wrapper.lines.len(), 4); - assert_wrapper_lines( - &text, - &wrapper, - &[ - &["Hello, 世界!\r"], - &["This is second line."], - &["This is third line."], - &["这里是第 4 行。"], - ], - ); - - // Add a new text to end - let range = text.len()..text.len(); - let new_text = "New text"; - text.replace(range.clone(), new_text); - wrapper._update(&text, &range, &Rope::from(new_text), &mut fake_wrap_line); - assert_eq!( - text.to_string(), - "Hello, 世界!\r\nThis is second line.\nThis is third line.\n这里是第 4 行。New text" - ); - assert_eq!(wrapper.lines.len(), 4); - assert_eq!(wrapper.lines.len(), 4); - assert_wrapper_lines( - &text, - &wrapper, - &[ - &["Hello, 世界!\r"], - &["This is second line."], - &["This is third line."], - &["这里是第 4 行。New text"], - ], - ); - - // Replace first line `Hello` to `AAA` - let range = 0..5; - let new_text = "AAA"; - text.replace(range.clone(), new_text); - wrapper._update(&text, &range, &Rope::from(new_text), &mut fake_wrap_line); - assert_eq!( - text.to_string(), - "AAA, 世界!\r\nThis is second line.\nThis is third line.\n这里是第 4 行。New text" - ); - assert_eq!(wrapper.lines.len(), 4); - assert_wrapper_lines( - &text, - &wrapper, - &[ - &["AAA, 世界!\r"], - &["This is second line."], - &["This is third line."], - &["这里是第 4 行。New text"], - ], - ); - - // Remove the second line - let start_offset = text.line_start_offset(1); - let end_offset = text.line_end_offset(1); - let range = start_offset..end_offset + 1; - text.replace(range.clone(), ""); - wrapper._update(&text, &range, &Rope::from(""), &mut fake_wrap_line); - assert_eq!( - text.to_string(), - "AAA, 世界!\r\nThis is third line.\n这里是第 4 行。New text" - ); - assert_eq!(wrapper.lines.len(), 3); - assert_wrapper_lines( - &text, - &wrapper, - &[ - &["AAA, 世界!\r"], - &["This is third line."], - &["这里是第 4 行。New text"], - ], - ); - - // Replace the first 2 lines to "This is a new line." - let range = text.line_start_offset(0)..text.line_end_offset(1) + 1; - let new_text = "This is a new line.\nThis is new line 2.\n"; - text.replace(range.clone(), new_text); - wrapper._update(&text, &range, &Rope::from(new_text), &mut fake_wrap_line); - assert_eq!( - text.to_string(), - "This is a new line.\nThis is new line 2.\n这里是第 4 行。New text" - ); - assert_eq!(wrapper.lines.len(), 3); - assert_wrapper_lines( - &text, - &wrapper, - &[ - &["This is a new line."], - &["This is new line 2."], - &["这里是第 4 行。New text"], - ], - ); - - // Add a new line at the end - let range = text.len()..text.len(); - let new_text = "\nThis is a new line at the end."; - text.replace(range.clone(), new_text); - wrapper._update(&text, &range, &Rope::from(new_text), &mut fake_wrap_line); - assert_eq!( - text.to_string(), - "This is a new line.\nThis is new line 2.\n这里是第 4 行。New text\nThis is a new line at the end." - ); - assert_eq!(wrapper.lines.len(), 4); - assert_wrapper_lines( - &text, - &wrapper, - &[ - &["This is a new line."], - &["This is new line 2."], - &["这里是第 4 行。New text"], - &["This is a new line at the end."], - ], - ); - - // Add a new line at the beginning - let range = 0..0; - let new_text = "This is a new line at the beginning.\n"; - text.replace(range.clone(), new_text); - wrapper._update(&text, &range, &Rope::from(new_text), &mut fake_wrap_line); - assert_eq!( - text.to_string(), - "This is a new line at the beginning.\nThis is a new line.\nThis is new line 2.\n这里是第 4 行。New text\nThis is a new line at the end." - ); - assert_eq!(wrapper.lines.len(), 5); - assert_wrapper_lines( - &text, - &wrapper, - &[ - &["This is a new line at the beginning."], - &["This is a new line."], - &["This is new line 2."], - &["这里是第 4 行。New text"], - &["This is a new line at the end."], - ], - ); - - // Remove all to at least one line in `lines`. - let range = 0..text.len(); - let new_text = ""; - text.replace(range.clone(), new_text); - wrapper._update(&text, &range, &Rope::from(new_text), &mut fake_wrap_line); - assert_eq!(text.to_string(), ""); - assert_eq!(wrapper.lines.len(), 1); - assert_eq!(wrapper.lines[0].wrapped_lines, vec![0..0]); - - // Test update_all - let range = 0..text.len(); - let new_text = "This is a full text.\nThis is a second line."; - text.replace(range.clone(), new_text); - wrapper._update(&text, &range, &text, &mut fake_wrap_line); - assert_eq!( - text.to_string(), - "This is a full text.\nThis is a second line." - ); - assert_eq!(wrapper.lines.len(), 2); - } - - #[test] - fn test_line_layout() { - let mut line_layout = LineLayout::new(); - - let line1 = ShapedLine::default().with_len(100); - let line2 = ShapedLine::default().with_len(50); - let wrapped_lines = smallvec::smallvec![line1, line2]; - line_layout.set_wrapped_lines(wrapped_lines); - assert_eq!(line_layout.len(), 150); - assert_eq!(line_layout.wrapped_lines.len(), 2); - } - - #[test] - fn test_position_for_index_prefers_first_leading_empty_visual_line() { - let mut line_layout = LineLayout::new(); - line_layout.set_wrapped_lines(smallvec::smallvec![ - ShapedLine::default(), - ShapedLine::default(), - ShapedLine::default().with_len(3), - ]); - - let last_layout = LastLayout { - visible_range: 0..1, - visible_buffer_lines: vec![0], - visible_line_byte_offsets: vec![0], - visible_top: px(0.), - visible_range_offset: 0..0, - lines: Rc::new(vec![]), - line_height: px(20.), - wrap_width: None, - line_number_width: px(0.), - cursor_bounds: None, - text_align: TextAlign::Left, - content_width: px(0.), - }; - - assert_eq!( - line_layout.position_for_index(0, &last_layout, false), - Some(point(px(0.), px(0.))) - ); - } - - #[test] - fn test_offset_to_display_point() { - let font = gpui::Font { - family: "Arial".into(), - weight: FontWeight::default(), - style: FontStyle::Normal, - features: FontFeatures::default(), - fallbacks: None, - }; - - let mut wrapper = TextWrapper::new(font, px(14.), None); - wrapper.text = Rope::from( - "Hello, 世界!\r\nThis is second line.\nThis is third line.\n这里是第 4 行。", - ); - wrapper.lines = vec![ - // range: 0..15 - LineItem { - line: Rope::from("Hello, 世界!\r"), - wrapped_lines: vec![0..15], - }, - // range: 16..36 - LineItem { - line: Rope::from("This is second line."), - wrapped_lines: vec![0..10, 10..20], - }, - // range: 37..56 - LineItem { - line: Rope::from("This is third line."), - wrapped_lines: vec![0..9, 9..15, 15..20], - }, - // range: 57..79 - LineItem { - line: Rope::from("这里是第 4 行。"), - wrapped_lines: vec![0..22], - }, - ]; - - assert_eq!( - wrapper.offset_to_display_point(12), - WrapDisplayPoint::new(0, 0, 12) - ); - assert_eq!( - wrapper.offset_to_display_point(15), - WrapDisplayPoint::new(0, 0, 15) - ); - - assert_eq!( - wrapper.offset_to_display_point(16), - WrapDisplayPoint::new(1, 0, 0) - ); - assert_eq!( - wrapper.offset_to_display_point(21), - WrapDisplayPoint::new(1, 0, 5) - ); - assert_eq!( - wrapper.offset_to_display_point(27), - WrapDisplayPoint::new(2, 1, 1) - ); - assert_eq!( - wrapper.offset_to_display_point(37), - WrapDisplayPoint::new(3, 0, 0) - ); - assert_eq!( - wrapper.offset_to_display_point(54), - WrapDisplayPoint::new(5, 2, 2) - ); - assert_eq!( - wrapper.offset_to_display_point(59), - WrapDisplayPoint::new(6, 0, 2) - ); - - assert_eq!( - wrapper.display_point_to_offset(WrapDisplayPoint::new(6, 0, 2)), - 59 - ); - assert_eq!( - wrapper.display_point_to_offset(WrapDisplayPoint::new(5, 2, 2)), - 54 - ); - assert_eq!( - wrapper.display_point_to_offset(WrapDisplayPoint::new(3, 0, 0)), - 37 - ); - assert_eq!( - wrapper.display_point_to_offset(WrapDisplayPoint::new(2, 1, 1)), - 27 - ); - assert_eq!( - wrapper.display_point_to_offset(WrapDisplayPoint::new(1, 0, 5)), - 21 - ); - assert_eq!( - wrapper.display_point_to_offset(WrapDisplayPoint::new(1, 0, 0)), - 16 - ); - assert_eq!( - wrapper.display_point_to_offset(WrapDisplayPoint::new(0, 0, 15)), - 15 - ); - } -} diff --git a/crates/ui/src/input/element.rs b/crates/ui/src/input/element.rs index c53166e..8367e27 100644 --- a/crates/ui/src/input/element.rs +++ b/crates/ui/src/input/element.rs @@ -131,11 +131,14 @@ impl Element for EditorScrollbar { window: &mut Window, cx: &mut App, ) -> (LayoutId, Self::RequestLayoutState) { - let mut style = Style::default(); - style.position = Position::Absolute; - style.size.width = relative(1.).into(); - style.size.height = relative(1.).into(); - + let style = Style { + position: Position::Absolute, + size: Size { + width: relative(1.).into(), + height: relative(1.).into(), + }, + ..Default::default() + }; (window.request_layout(style, [], cx), ()) } @@ -309,13 +312,12 @@ impl TextElement { let mut cursor_bounds = None; // If the input has a fixed height (Otherwise is auto-grow), we need to add a bottom margin to the input. - let top_bottom_margin = if state.mode.is_auto_grow() { - line_height - } else if visible_range.len() < BOTTOM_MARGIN_ROWS * 8 { - line_height - } else { - BOTTOM_MARGIN_ROWS * line_height - }; + let top_bottom_margin = + if state.mode.is_auto_grow() || visible_range.len() < BOTTOM_MARGIN_ROWS * 8 { + line_height + } else { + BOTTOM_MARGIN_ROWS * line_height + }; // The cursor corresponds to the current cursor position in the text no only the line. let mut cursor_pos = None; diff --git a/crates/ui/src/input/input.rs b/crates/ui/src/input/input.rs index 68d4e1f..8187819 100644 --- a/crates/ui/src/input/input.rs +++ b/crates/ui/src/input/input.rs @@ -18,7 +18,7 @@ pub(crate) fn input_style(disabled: bool, cx: &App) -> (Hsla, Hsla) { if disabled { (cx.theme().surface_background, cx.theme().text_muted) } else { - (cx.theme().surface_background, cx.theme().text) + (cx.theme().elevated_surface_background, cx.theme().text) } } diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index 701a522..2cadb08 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -6,6 +6,7 @@ pub use index_path::IndexPath; pub use kbd::*; pub use root::{Root, window_paddings}; pub use styled::*; +pub use title_bar::*; pub use window_ext::*; pub use crate::Disableable; @@ -41,6 +42,7 @@ mod index_path; mod kbd; mod root; mod styled; +mod title_bar; mod window_ext; /// Initialize the UI module. diff --git a/crates/ui/src/notification.rs b/crates/ui/src/notification.rs index bf720be..4779a37 100644 --- a/crates/ui/src/notification.rs +++ b/crates/ui/src/notification.rs @@ -298,10 +298,10 @@ impl Render for Notification { let action = self.action_builder.clone().map(|builder| { builder(self, window, cx) - .xsmall() + .small() .primary() - .px_3() - .font_semibold() + .px_4() + .font_medium() }); let icon = match self.kind { @@ -364,8 +364,14 @@ impl Render for Notification { }) .when_some(content, |this, content| this.child(content)) .when_some(action, |this, action| { - this.gap_2() - .child(h_flex().w_full().flex_1().justify_end().child(action)) + this.gap_2().child( + h_flex() + .mt_2() + .w_full() + .flex_1() + .justify_end() + .child(action), + ) }), ) .child( diff --git a/crates/ui/src/title_bar.rs b/crates/ui/src/title_bar.rs new file mode 100644 index 0000000..79e60e0 --- /dev/null +++ b/crates/ui/src/title_bar.rs @@ -0,0 +1,352 @@ +use std::rc::Rc; + +use gpui::prelude::FluentBuilder as _; +use gpui::{ + AnyElement, App, ClickEvent, Context, Decorations, Hsla, InteractiveElement, IntoElement, + MouseButton, ParentElement, Pixels, Render, RenderOnce, StatefulInteractiveElement as _, + StyleRefinement, Styled, TitlebarOptions, Window, WindowControlArea, div, px, +}; +use smallvec::SmallVec; +use theme::ActiveTheme; + +use crate::{Icon, IconName, InteractiveElementExt as _, Sizable as _, StyledExt, h_flex}; + +pub const TITLE_BAR_HEIGHT: Pixels = px(34.); +#[cfg(target_os = "macos")] +pub const TRAFFIC_LIGHT_PADDING: f32 = 80.; + +/// TitleBar used to customize the appearance of the title bar. +/// +/// We can put some elements inside the title bar. +#[derive(IntoElement)] +#[allow(clippy::type_complexity)] +pub struct TitleBar { + style: StyleRefinement, + children: SmallVec<[AnyElement; 1]>, + on_close_window: Option>>, +} + +impl TitleBar { + /// Create a new TitleBar. + pub fn new() -> Self { + Self { + style: StyleRefinement::default(), + children: SmallVec::new(), + on_close_window: None, + } + } + + /// Returns the default title bar options for compatible with the [`crate::TitleBar`]. + pub fn title_bar_options() -> TitlebarOptions { + TitlebarOptions { + title: None, + appears_transparent: true, + traffic_light_position: Some(gpui::point(px(9.0), px(9.0))), + } + } + + /// Add custom for close window event, default is None, then click X button will call `window.remove_window()`. + /// Linux only, this will do nothing on other platforms. + pub fn on_close_window( + mut self, + f: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, + ) -> Self { + if cfg!(target_os = "linux") { + self.on_close_window = Some(Rc::new(Box::new(f))); + } + self + } +} + +impl Default for TitleBar { + fn default() -> Self { + Self::new() + } +} + +// The Windows control buttons have a fixed width of 35px. +// +// We don't need implementation the click event for the control buttons. +// If user clicked in the bounds, the window event will be triggered. +#[derive(IntoElement, Clone)] +#[allow(clippy::type_complexity)] +enum ControlIcon { + Minimize, + Restore, + Maximize, + Close { + on_close_window: Option>>, + }, +} + +impl ControlIcon { + fn minimize() -> Self { + Self::Minimize + } + + fn restore() -> Self { + Self::Restore + } + + fn maximize() -> Self { + Self::Maximize + } + + #[allow(clippy::type_complexity)] + fn close(on_close_window: Option>>) -> Self { + Self::Close { on_close_window } + } + + fn id(&self) -> &'static str { + match self { + Self::Minimize => "minimize", + Self::Restore => "restore", + Self::Maximize => "maximize", + Self::Close { .. } => "close", + } + } + + fn icon(&self) -> IconName { + match self { + Self::Minimize => IconName::WindowMinimize, + Self::Restore => IconName::WindowRestore, + Self::Maximize => IconName::WindowMaximize, + Self::Close { .. } => IconName::WindowClose, + } + } + + fn window_control_area(&self) -> WindowControlArea { + match self { + Self::Minimize => WindowControlArea::Min, + Self::Restore | Self::Maximize => WindowControlArea::Max, + Self::Close { .. } => WindowControlArea::Close, + } + } + + fn is_close(&self) -> bool { + matches!(self, Self::Close { .. }) + } + + #[inline] + fn hover_fg(&self, cx: &App) -> Hsla { + if self.is_close() { + cx.theme().danger_foreground + } else { + cx.theme().text + } + } + + #[inline] + fn hover_bg(&self, cx: &App) -> Hsla { + if self.is_close() { + cx.theme().danger_background + } else { + cx.theme().ghost_element_hover + } + } + + #[inline] + fn active_bg(&self, cx: &mut App) -> Hsla { + if self.is_close() { + cx.theme().danger_active + } else { + cx.theme().ghost_element_active + } + } +} + +impl RenderOnce for ControlIcon { + fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { + let is_linux = cfg!(target_os = "linux"); + let is_windows = cfg!(target_os = "windows"); + + let icon = self.clone(); + let hover_fg = self.hover_fg(cx); + let hover_bg = self.hover_bg(cx); + let active_bg = self.active_bg(cx); + + let on_close_window = match &self { + ControlIcon::Close { on_close_window } => on_close_window.clone(), + _ => None, + }; + + div() + .id(self.id()) + .flex() + .w(TITLE_BAR_HEIGHT) + .h_full() + .flex_shrink_0() + .justify_center() + .content_center() + .items_center() + .text_color(cx.theme().text) + .hover(|style| style.bg(hover_bg).text_color(hover_fg)) + .active(|style| style.bg(active_bg).text_color(hover_fg)) + .when(is_windows, |this| { + this.window_control_area(self.window_control_area()) + }) + .when(is_linux, |this| { + this.on_mouse_down(MouseButton::Left, move |_, window, cx| { + window.prevent_default(); + cx.stop_propagation(); + }) + .on_click(move |_, window, cx| { + cx.stop_propagation(); + match icon { + Self::Minimize => window.minimize_window(), + Self::Restore | Self::Maximize => window.zoom_window(), + Self::Close { .. } => { + if let Some(f) = on_close_window.clone() { + f(&ClickEvent::default(), window, cx); + } else { + window.remove_window(); + } + } + } + }) + }) + .child(Icon::new(self.icon()).small()) + } +} + +#[derive(IntoElement)] +#[allow(clippy::type_complexity)] +struct WindowControls { + on_close_window: Option>>, +} + +impl RenderOnce for WindowControls { + fn render(self, window: &mut Window, _: &mut App) -> impl IntoElement { + if cfg!(target_os = "macos") || cfg!(target_family = "wasm") { + return div().id("window-controls"); + } + + h_flex() + .id("window-controls") + .items_center() + .flex_shrink_0() + .h_full() + .child(ControlIcon::minimize()) + .child(if window.is_maximized() { + ControlIcon::restore() + } else { + ControlIcon::maximize() + }) + .child(ControlIcon::close(self.on_close_window)) + } +} + +impl Styled for TitleBar { + fn style(&mut self) -> &mut gpui::StyleRefinement { + &mut self.style + } +} + +impl ParentElement for TitleBar { + fn extend(&mut self, elements: impl IntoIterator) { + self.children.extend(elements); + } +} + +struct TitleBarState { + should_move: bool, +} + +impl Render for TitleBarState { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + div() + } +} + +impl RenderOnce for TitleBar { + fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { + let is_client_decorated = matches!(window.window_decorations(), Decorations::Client { .. }); + let is_web = cfg!(target_family = "wasm"); + let is_linux = cfg!(target_os = "linux"); + let is_macos = cfg!(target_os = "macos"); + + let state = window.use_state(cx, |_, _| TitleBarState { should_move: false }); + + div().flex_shrink_0().child( + div() + .id("title-bar") + .flex() + .flex_row() + .items_center() + .justify_between() + .h(TITLE_BAR_HEIGHT) + .map(|this| { + if window.is_fullscreen() { + this.px_2() + } else if cx.theme().platform.is_mac() { + this.pr_2().pl(px(TRAFFIC_LIGHT_PADDING)) + } else { + this.px_2() + } + }) + .border_b_1() + .border_color(cx.theme().border) + .bg(cx.theme().title_bar) + .refine_style(&self.style) + .when(is_linux, |this| { + this.on_double_click(|_, window, _| window.zoom_window()) + }) + .when(is_macos, |this| { + this.on_double_click(|_, window, _| window.titlebar_double_click()) + }) + .on_mouse_down_out(window.listener_for(&state, |state, _, _, _| { + state.should_move = false; + })) + .on_mouse_down( + MouseButton::Left, + window.listener_for(&state, |state, _, _, _| { + state.should_move = true; + }), + ) + .on_mouse_up( + MouseButton::Left, + window.listener_for(&state, |state, _, _, _| { + state.should_move = false; + }), + ) + .on_mouse_move(window.listener_for(&state, |state, _, window, _| { + if state.should_move { + state.should_move = false; + window.start_window_move(); + } + })) + .child( + h_flex() + .id("bar") + .h_full() + .justify_between() + .flex_shrink_0() + .flex_1() + .when(!is_web, |this| { + this.window_control_area(WindowControlArea::Drag) + .when(window.is_fullscreen(), |this| this.pl_3()) + .when(is_linux && is_client_decorated, |this| { + this.child( + div() + .top_0() + .left_0() + .absolute() + .size_full() + .h_full() + .on_mouse_down( + MouseButton::Right, + move |ev, window, _| { + window.show_window_menu(ev.position) + }, + ), + ) + }) + }) + .children(self.children), + ) + .child(WindowControls { + on_close_window: self.on_close_window, + }), + ) + } +} diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index db49431..5b1384c 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -29,7 +29,6 @@ icons = [ [dependencies] assets = { path = "../crates/assets" } ui = { path = "../crates/ui" } -title_bar = { path = "../crates/title_bar" } theme = { path = "../crates/theme" } common = { path = "../crates/common" } state = { path = "../crates/state" } diff --git a/desktop/src/dialogs/accounts.rs b/desktop/src/dialogs/accounts.rs deleted file mode 100644 index c780046..0000000 --- a/desktop/src/dialogs/accounts.rs +++ /dev/null @@ -1,257 +0,0 @@ -use anyhow::Error; -use gpui::prelude::FluentBuilder; -use gpui::{ - App, AppContext, Context, Entity, InteractiveElement, IntoElement, ParentElement, Render, - SharedString, StatefulInteractiveElement, Styled, Subscription, Task, Window, div, px, -}; -use nostr_sdk::prelude::*; -use person::PersonRegistry; -use state::{NostrRegistry, StateEvent}; -use theme::ActiveTheme; -use ui::avatar::Avatar; -use ui::button::{Button, ButtonVariants}; -use ui::indicator::Indicator; -use ui::{Disableable, Icon, IconName, Sizable, WindowExtension, h_flex, v_flex}; - -use crate::dialogs::connect::ConnectSigner; -use crate::dialogs::import::ImportKey; - -pub fn init(window: &mut Window, cx: &mut App) -> Entity { - cx.new(|cx| AccountSelector::new(window, cx)) -} - -/// Account selector -pub struct AccountSelector { - /// Public key currently being chosen for login - logging_in: Entity>, - - /// The error message displayed when an error occurs. - error: Entity>, - - /// Async tasks - tasks: Vec>>, - - /// Subscription to the signer events - _subscription: Option, -} - -impl AccountSelector { - pub fn new(window: &mut Window, cx: &mut Context) -> Self { - let logging_in = cx.new(|_| None); - let error = cx.new(|_| None); - - // Subscribe to the signer events - let nostr = NostrRegistry::global(cx); - let subscription = cx.subscribe_in(&nostr, window, |this, _state, event, window, cx| { - match event { - StateEvent::SignerSet => { - window.close_all_modals(cx); - window.refresh(); - } - StateEvent::Error(e) => { - this.set_error(e.to_string(), cx); - } - _ => {} - }; - }); - - Self { - logging_in, - error, - tasks: vec![], - _subscription: Some(subscription), - } - } - - fn logging_in(&self, public_key: &PublicKey, cx: &App) -> bool { - self.logging_in.read(cx) == &Some(*public_key) - } - - fn set_logging_in(&mut self, public_key: PublicKey, cx: &mut Context) { - self.logging_in.update(cx, |this, cx| { - *this = Some(public_key); - cx.notify(); - }); - } - - fn set_error(&mut self, error: T, cx: &mut Context) - where - T: Into, - { - self.error.update(cx, |this, cx| { - *this = Some(error.into()); - cx.notify(); - }); - - self.logging_in.update(cx, |this, cx| { - *this = None; - cx.notify(); - }) - } - - fn login(&mut self, public_key: PublicKey, window: &mut Window, cx: &mut Context) { - let nostr = NostrRegistry::global(cx); - let task = nostr.read(cx).get_secret(public_key, cx); - - // Mark the public key as being logged in - self.set_logging_in(public_key, cx); - - self.tasks.push(cx.spawn_in(window, async move |this, cx| { - match task.await { - Ok(signer) => { - nostr.update(cx, |this, cx| { - this.set_signer(signer, cx); - }); - } - Err(e) => { - this.update(cx, |this, cx| { - this.set_error(e.to_string(), cx); - })?; - } - }; - Ok(()) - })); - } - - fn remove(&mut self, public_key: PublicKey, cx: &mut Context) { - let nostr = NostrRegistry::global(cx); - - nostr.update(cx, |this, cx| { - this.remove_secret(&public_key, cx); - }); - } - - fn open_import(&mut self, window: &mut Window, cx: &mut Context) { - let import = cx.new(|cx| ImportKey::new(window, cx)); - - window.open_modal(cx, move |this, _window, _cx| { - this.width(px(460.)) - .title("Import a Secret Key or Bunker Connection") - .show_close(true) - .pb_2() - .child(import.clone()) - }); - } - - fn open_connect(&mut self, window: &mut Window, cx: &mut Context) { - let connect = cx.new(|cx| ConnectSigner::new(window, cx)); - - window.open_modal(cx, move |this, _window, _cx| { - this.width(px(460.)) - .title("Scan QR Code to Connect") - .show_close(true) - .pb_2() - .child(connect.clone()) - }); - } -} - -impl Render for AccountSelector { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let persons = PersonRegistry::global(cx); - let nostr = NostrRegistry::global(cx); - let npubs = nostr.read(cx).npubs(); - let loading = self.logging_in.read(cx).is_some(); - - v_flex() - .size_full() - .gap_2() - .when_some(self.error.read(cx).as_ref(), |this, error| { - this.child( - div() - .italic() - .text_xs() - .text_center() - .text_color(cx.theme().text_danger) - .child(error.clone()), - ) - }) - .children({ - let mut items = vec![]; - - for (ix, public_key) in npubs.read(cx).iter().enumerate() { - let profile = persons.read(cx).get(public_key, cx); - let logging_in = self.logging_in(public_key, cx); - - items.push( - h_flex() - .id(ix) - .group("") - .px_2() - .h_10() - .justify_between() - .w_full() - .rounded(cx.theme().radius) - .bg(cx.theme().ghost_element_background) - .hover(|this| this.bg(cx.theme().ghost_element_hover)) - .child( - h_flex() - .gap_2() - .child(Avatar::new(profile.avatar()).small()) - .child(div().text_sm().child(profile.name())), - ) - .when(logging_in, |this| this.child(Indicator::new().small())) - .when(!logging_in, |this| { - this.child( - h_flex() - .gap_1() - .invisible() - .group_hover("", |this| this.visible()) - .child( - Button::new(format!("del-{ix}")) - .icon(IconName::Close) - .ghost() - .small() - .disabled(logging_in) - .on_click(cx.listener({ - let public_key = *public_key; - move |this, _ev, _window, cx| { - cx.stop_propagation(); - this.remove(public_key, cx); - } - })), - ), - ) - }) - .when(!logging_in, |this| { - let public_key = *public_key; - this.on_click(cx.listener(move |this, _ev, window, cx| { - this.login(public_key, window, cx); - })) - }), - ); - } - - items - }) - .child(div().w_full().h_px().bg(cx.theme().border_variant)) - .child( - h_flex() - .gap_1() - .justify_end() - .w_full() - .child( - Button::new("input") - .icon(Icon::new(IconName::Usb)) - .label("Import") - .ghost() - .small() - .disabled(loading) - .on_click(cx.listener(move |this, _ev, window, cx| { - this.open_import(window, cx); - })), - ) - .child( - Button::new("qr") - .icon(Icon::new(IconName::Scan)) - .label("Scan QR to connect") - .ghost() - .small() - .disabled(loading) - .on_click(cx.listener(move |this, _ev, window, cx| { - this.open_connect(window, cx); - })), - ), - ) - } -} diff --git a/desktop/src/dialogs/connect.rs b/desktop/src/dialogs/connect.rs deleted file mode 100644 index 537cdc1..0000000 --- a/desktop/src/dialogs/connect.rs +++ /dev/null @@ -1,115 +0,0 @@ -use std::sync::Arc; -use std::time::Duration; - -use common::StringExt; -use gpui::prelude::FluentBuilder; -use gpui::{ - AppContext, Context, Entity, Image, IntoElement, ParentElement, Render, SharedString, Styled, - Subscription, Window, div, img, px, -}; -use nostr_connect::prelude::*; -use state::{ - CLIENT_NAME, CoopAuthUrlHandler, NOSTR_CONNECT_RELAY, NOSTR_CONNECT_TIMEOUT, NostrRegistry, - StateEvent, -}; -use theme::ActiveTheme; -use ui::v_flex; - -pub struct ConnectSigner { - /// QR Code - qr_code: Option>, - - /// Error message - error: Entity>, - - /// Subscription to the signer event - _subscription: Option, -} - -impl ConnectSigner { - pub fn new(window: &mut Window, cx: &mut Context) -> Self { - let error = cx.new(|_| None); - - let nostr = NostrRegistry::global(cx); - let app_keys = nostr.read(cx).keys(); - - let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT); - let relay = RelayUrl::parse(NOSTR_CONNECT_RELAY).unwrap(); - - // Generate the nostr connect uri - let uri = NostrConnectUri::client(app_keys.public_key(), vec![relay], CLIENT_NAME); - - // Generate the nostr connect - let mut signer = NostrConnect::new(uri.clone(), app_keys.clone(), timeout, None).unwrap(); - - // Handle the auth request - signer.auth_url_handler(CoopAuthUrlHandler); - - // Generate a QR code for quick connection - let qr_code = uri.to_string().to_qr(); - - // Set signer in the background - nostr.update(cx, |this, cx| { - this.add_nip46_signer(&signer, cx); - }); - - // Subscribe to the signer event - let subscription = cx.subscribe_in(&nostr, window, |this, _state, event, _window, cx| { - if let StateEvent::Error(e) = event { - this.set_error(e, cx); - } - }); - - Self { - qr_code, - error, - _subscription: Some(subscription), - } - } - - fn set_error(&mut self, message: S, cx: &mut Context) - where - S: Into, - { - self.error.update(cx, |this, cx| { - *this = Some(message.into()); - cx.notify(); - }); - } -} - -impl Render for ConnectSigner { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - const MSG: &str = "Scan with any Nostr Connect-compatible app to connect"; - - v_flex() - .size_full() - .items_center() - .justify_center() - .p_4() - .when_some(self.qr_code.as_ref(), |this, qr| { - this.child( - img(qr.clone()) - .size(px(256.)) - .rounded(cx.theme().radius_lg) - .border_1() - .border_color(cx.theme().border), - ) - }) - .when_some(self.error.read(cx).as_ref(), |this, error| { - this.child( - div() - .text_xs() - .text_center() - .text_color(cx.theme().text_danger) - .child(error.clone()), - ) - }) - .child( - div() - .text_xs() - .text_color(cx.theme().text_muted) - .child(SharedString::from(MSG)), - ) - } -} diff --git a/desktop/src/dialogs/import.rs b/desktop/src/dialogs/import.rs index 7810a53..0b690c6 100644 --- a/desktop/src/dialogs/import.rs +++ b/desktop/src/dialogs/import.rs @@ -7,15 +7,14 @@ use gpui::{ Subscription, Task, Window, div, }; use nostr_connect::prelude::*; -use smallvec::{SmallVec, smallvec}; -use state::{CoopAuthUrlHandler, NostrRegistry, StateEvent}; +use state::NostrRegistry; use theme::ActiveTheme; use ui::button::{Button, ButtonVariants}; use ui::input::{Input, InputEvent, InputState}; -use ui::{Disableable, v_flex}; +use ui::{Disableable, WindowExtension, v_flex}; #[derive(Debug)] -pub struct ImportKey { +pub struct ImportIdentity { /// Secret key input key_input: Entity, @@ -25,73 +24,43 @@ pub struct ImportKey { /// Error message error: Entity>, - /// Countdown timer for nostr connect - countdown: Entity>, - /// Whether the user is currently loading loading: bool, /// Async tasks tasks: Vec>>, - /// Event subscriptions - _subscriptions: SmallVec<[Subscription; 2]>, + /// Input subscription + _subscription: Option, } -impl ImportKey { +impl ImportIdentity { pub fn new(window: &mut Window, cx: &mut Context) -> Self { - let nostr = NostrRegistry::global(cx); let key_input = cx.new(|cx| InputState::new(window, cx).masked(true)); let pass_input = cx.new(|cx| InputState::new(window, cx).masked(true)); let error = cx.new(|_| None); - let countdown = cx.new(|_| None); - let mut subscriptions = smallvec![]; - - subscriptions.push( - // Subscribe to key input events and process login when the user presses enter + let input_subscription = cx.subscribe_in(&key_input, window, |this, _input, event, window, cx| { if let InputEvent::PressEnter { .. } = event { this.login(window, cx); }; - }), - ); - - subscriptions.push( - // Subscribe to the nostr signer event - cx.subscribe_in(&nostr, window, |this, _state, event, _window, cx| { - if let StateEvent::Error(e) = event { - this.set_error(e, cx); - } - }), - ); + }); Self { key_input, pass_input, error, - countdown, loading: false, tasks: vec![], - _subscriptions: subscriptions, + _subscription: Some(input_subscription), } } fn login(&mut self, window: &mut Window, cx: &mut Context) { - if self.loading { - return; - }; - // Prevent duplicate login requests - self.set_loading(true, cx); - let value = self.key_input.read(cx).value(); let password = self.pass_input.read(cx).value(); - if value.starts_with("bunker://") { - self.bunker(&value, window, cx); - return; - } - if value.starts_with("ncryptsec1") { self.ncryptsec(value, password, window, cx); return; @@ -103,52 +72,14 @@ impl ImportKey { // Update the signer nostr.update(cx, |this, cx| { - this.add_key_signer(&keys, cx); + this.set_signer(keys, cx); }); + window.close_modal(cx); } else { self.set_error("Invalid key", cx); } } - fn bunker(&mut self, content: &str, window: &mut Window, cx: &mut Context) { - let Ok(uri) = NostrConnectUri::parse(content) else { - self.set_error("Bunker is not valid", cx); - return; - }; - - let nostr = NostrRegistry::global(cx); - let app_keys = nostr.read(cx).keys(); - let timeout = Duration::from_secs(30); - - // Construct the nostr connect signer - let mut signer = NostrConnect::new(uri, app_keys.clone(), timeout, None).unwrap(); - - // Handle auth url with the default browser - signer.auth_url_handler(CoopAuthUrlHandler); - - // Set signer in the background - nostr.update(cx, |this, cx| { - this.add_nip46_signer(&signer, cx); - }); - - // Start countdown - self.tasks.push(cx.spawn_in(window, async move |this, cx| { - for i in (0..=30).rev() { - if i == 0 { - this.update(cx, |this, cx| { - this.set_countdown(None, cx); - })?; - } else { - this.update(cx, |this, cx| { - this.set_countdown(Some(i), cx); - })?; - } - cx.background_executor().timer(Duration::from_secs(1)).await; - } - Ok(()) - })); - } - fn ncryptsec(&mut self, content: S, pwd: S, window: &mut Window, cx: &mut Context) where S: Into, @@ -179,9 +110,10 @@ impl ImportKey { self.tasks.push(cx.spawn_in(window, async move |this, cx| { match task.await { Ok(keys) => { - nostr.update(cx, |this, cx| { - this.add_key_signer(&keys, cx); - }); + nostr.update_in(cx, |this, window, cx| { + this.set_signer(keys, cx); + window.close_modal(cx); + })?; } Err(e) => { this.update(cx, |this, cx| { @@ -198,12 +130,6 @@ impl ImportKey { where S: Into, { - // Reset the log in state - self.set_loading(false, cx); - - // Reset the countdown - self.set_countdown(None, cx); - // Update error message self.error.update(cx, |this, cx| { *this = Some(message.into()); @@ -224,22 +150,12 @@ impl ImportKey { Ok(()) })); } - - fn set_loading(&mut self, status: bool, cx: &mut Context) { - self.loading = status; - cx.notify(); - } - - fn set_countdown(&mut self, i: Option, cx: &mut Context) { - self.countdown.update(cx, |this, cx| { - *this = i; - cx.notify(); - }); - } } -impl Render for ImportKey { +impl Render for ImportIdentity { fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context) -> impl IntoElement { + const MSG: &str = "Coop isn't stored your identity secret in local device. Everything will be reset on the next login."; + v_flex() .size_full() .gap_2() @@ -249,7 +165,7 @@ impl Render for ImportKey { .gap_1() .text_sm() .text_color(cx.theme().text_muted) - .child("nsec or bunker://") + .child("nsec or ncryptsec://") .child(Input::new(&self.key_input)), ) .when( @@ -265,6 +181,7 @@ impl Render for ImportKey { ) }, ) + .child(div().text_xs().text_color(cx.theme().text_muted).child(MSG)) .child( Button::new("login") .label("Continue") @@ -275,18 +192,6 @@ impl Render for ImportKey { this.login(window, cx); })), ) - .when_some(self.countdown.read(cx).as_ref(), |this, i| { - this.child( - div() - .text_xs() - .text_center() - .text_color(cx.theme().text_muted) - .child(SharedString::from(format!( - "Approve connection request from your signer in {} seconds", - i - ))), - ) - }) .when_some(self.error.read(cx).as_ref(), |this, error| { this.child( div() diff --git a/desktop/src/dialogs/mod.rs b/desktop/src/dialogs/mod.rs index 831a2de..ffdf97b 100644 --- a/desktop/src/dialogs/mod.rs +++ b/desktop/src/dialogs/mod.rs @@ -1,5 +1,3 @@ -pub mod accounts; -pub mod connect; pub mod import; pub mod restore; pub mod screening; diff --git a/desktop/src/dialogs/screening.rs b/desktop/src/dialogs/screening.rs index 3a13e7a..3f5b375 100644 --- a/desktop/src/dialogs/screening.rs +++ b/desktop/src/dialogs/screening.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use std::time::Duration; -use anyhow::{Context as AnyhowContext, Error}; +use anyhow::Error; use common::TimestampExt; use gpui::prelude::FluentBuilder; use gpui::{ @@ -78,12 +78,13 @@ impl Screening { let client = nostr.read(cx).client(); let public_key = self.public_key; - let task: Task> = cx.background_spawn(async move { - let signer = client.signer().context("Signer not found")?; - let signer_pubkey = signer.get_public_key().await?; + let Some(current_user) = nostr.read(cx).signer_pubkey(cx) else { + return; + }; + let task: Task> = cx.background_spawn(async move { // Check if user is in contact list - let contacts = client.database().contacts_public_keys(signer_pubkey).await; + let contacts = client.database().contacts_public_keys(current_user).await; let followed = contacts.unwrap_or_default().contains(&public_key); Ok(followed) @@ -105,16 +106,17 @@ impl Screening { let client = nostr.read(cx).client(); let public_key = self.public_key; - let task: Task, Error>> = cx.background_spawn(async move { - let signer = client.signer().context("Signer not found")?; - let signer_pubkey = signer.get_public_key().await?; + let Some(current_user) = nostr.read(cx).signer_pubkey(cx) else { + return; + }; + let task: Task, Error>> = cx.background_spawn(async move { // Check mutual contacts let filter = Filter::new().kind(Kind::ContactList).pubkey(public_key); let mut mutual_contacts = vec![]; if let Ok(events) = client.database().query(filter).await { - for event in events.into_iter().filter(|ev| ev.pubkey != signer_pubkey) { + for event in events.into_iter().filter(|ev| ev.pubkey != current_user) { mutual_contacts.push(event.pubkey); } } @@ -224,10 +226,20 @@ impl Screening { let client = nostr.read(cx).client(); let public_key = self.public_key; + let Some(signer) = nostr.read(cx).signer(cx) else { + return; + }; + let task: Task> = cx.background_spawn(async move { - let tag = Tag::public_key_report(public_key, Report::Impersonation); - let builder = EventBuilder::report(vec![tag], ""); - let event = client.sign_event_builder(builder).await?; + let tag = Nip56Tag::PublicKey { + public_key, + report: Report::Impersonation, + } + .to_tag(); + + let event = EventBuilder::report(vec![tag], "") + .finalize_async(&signer) + .await?; // Send the report to the public relays client.send_event(&event).to(BOOTSTRAP_RELAYS).await?; diff --git a/desktop/src/dialogs/settings.rs b/desktop/src/dialogs/settings.rs index f6e67dc..5bf9081 100644 --- a/desktop/src/dialogs/settings.rs +++ b/desktop/src/dialogs/settings.rs @@ -56,17 +56,16 @@ impl Preferences { impl Render for Preferences { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - const SCREENING: &str = - "When opening a request, a popup will appear to help you identify the sender."; - const AVATAR: &str = - "Hide all avatar pictures to improve performance and protect your privacy."; - const MODE: &str = - "Choose whether to use the selected light or dark theme, or to follow the OS."; + const SCREENING: &str = "Show an screening dialog to verify the unknown sender."; + const AVATAR: &str = "Hide all avatar pictures to improve performance."; + const MODE: &str = "Use the selected light or dark theme, or to follow the OS."; + const NIP4E: &str = "Use a dedicated key to encrypt and decrypt messages."; const AUTH: &str = "Choose the authentication behavior for relays."; const RESET: &str = "Reset the theme to the default one."; let screening = AppSettings::get_screening(cx); let hide_avatar = AppSettings::get_hide_avatar(cx); + let nip4e = AppSettings::get_nip4e(cx); let auth_mode = AppSettings::get_auth_mode(cx); let theme_mode = AppSettings::get_theme_mode(cx); @@ -207,6 +206,21 @@ impl Render for Preferences { ), ), ) + .child( + GroupBox::new() + .id("experiments") + .title("Experiments") + .fill() + .child( + Switch::new("nip4e") + .label("Decoupling Encryption Key") + .description(NIP4E) + .checked(nip4e) + .on_click(move |_, _window, cx| { + AppSettings::update_nip4e(!nip4e, cx); + }), + ), + ) .child( GroupBox::new() .id("media") diff --git a/desktop/src/panels/contact_list.rs b/desktop/src/panels/contact_list.rs index a9f86f2..7db2a29 100644 --- a/desktop/src/panels/contact_list.rs +++ b/desktop/src/panels/contact_list.rs @@ -1,7 +1,7 @@ use std::collections::HashSet; use std::time::Duration; -use anyhow::{Context as AnyhowContext, Error}; +use anyhow::Error; use gpui::prelude::FluentBuilder; use gpui::{ AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, @@ -82,11 +82,12 @@ impl ContactListPanel { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let task: Task, Error>> = cx.background_spawn(async move { - let signer = client.signer().context("Signer not found")?; - let public_key = signer.get_public_key().await?; - let contact_list = client.database().contacts_public_keys(public_key).await?; + let Some(public_key) = nostr.read(cx).signer_pubkey(cx) else { + return; + }; + let task: Task, Error>> = cx.background_spawn(async move { + let contact_list = client.database().contacts_public_keys(public_key).await?; Ok(contact_list) }); @@ -157,6 +158,10 @@ impl ContactListPanel { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); + let Some(signer) = nostr.read(cx).signer(cx) else { + return; + }; + // Get contacts let contacts: Vec = self .contacts @@ -169,8 +174,9 @@ impl ContactListPanel { let task: Task> = cx.background_spawn(async move { // Construct contact list event builder - let builder = EventBuilder::contact_list(contacts); - let event = client.sign_event_builder(builder).await?; + let event = ContactListBuilder::new(contacts) + .finalize_async(&signer) + .await?; // Set contact list client.send_event(&event).to_nip65().await?; diff --git a/desktop/src/panels/greeter.rs b/desktop/src/panels/greeter.rs index 97e1c37..07e973c 100644 --- a/desktop/src/panels/greeter.rs +++ b/desktop/src/panels/greeter.rs @@ -30,9 +30,8 @@ impl GreeterPanel { fn add_profile_panel(&mut self, window: &mut Window, cx: &mut Context) { let nostr = NostrRegistry::global(cx); - let signer = nostr.read(cx).signer(); - if let Some(public_key) = signer.public_key() { + if let Some(public_key) = nostr.read(cx).signer_pubkey(cx) { cx.spawn_in(window, async move |_this, cx| { cx.update(|window, cx| { Workspace::add_panel( diff --git a/desktop/src/panels/messaging_relays.rs b/desktop/src/panels/messaging_relays.rs index 1125c13..284c92c 100644 --- a/desktop/src/panels/messaging_relays.rs +++ b/desktop/src/panels/messaging_relays.rs @@ -1,7 +1,7 @@ use std::collections::HashSet; use std::time::Duration; -use anyhow::{Context as AnyhowContext, Error, anyhow}; +use anyhow::{Error, anyhow}; use gpui::prelude::FluentBuilder; use gpui::{ AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, @@ -83,17 +83,18 @@ impl MessagingRelayPanel { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let task: Task, Error>> = cx.background_spawn(async move { - let signer = client.signer().context("Signer not found")?; - let public_key = signer.get_public_key().await?; + let Some(public_key) = nostr.read(cx).signer_pubkey(cx) else { + return; + }; + let task: Task, Error>> = cx.background_spawn(async move { let filter = Filter::new() .kind(Kind::InboxRelays) .author(public_key) .limit(1); if let Some(event) = client.database().query(filter).await?.first_owned() { - Ok(nip17::extract_owned_relay_list(event).collect()) + Ok(nip17::extract_relay_list(&event).collect()) } else { Err(anyhow!("Not found.")) } @@ -171,11 +172,15 @@ impl MessagingRelayPanel { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); + let Some(signer) = nostr.read(cx).signer(cx) else { + return; + }; + // Construct event tags let tags: Vec = self .relays .iter() - .map(|relay| Tag::relay(relay.clone())) + .map(|relay| Nip17Tag::Relay(relay.to_owned()).to_tag()) .collect(); // Set updating state @@ -183,8 +188,10 @@ impl MessagingRelayPanel { let task: Task> = cx.background_spawn(async move { // Construct nip17 event builder - let builder = EventBuilder::new(Kind::InboxRelays, "").tags(tags); - let event = client.sign_event_builder(builder).await?; + let event = EventBuilder::new(Kind::InboxRelays, "") + .tags(tags) + .finalize_async(&signer) + .await?; // Set messaging relays client.send_event(&event).to_nip65().await?; diff --git a/desktop/src/panels/profile.rs b/desktop/src/panels/profile.rs index 606b6ae..6b70ace 100644 --- a/desktop/src/panels/profile.rs +++ b/desktop/src/panels/profile.rs @@ -1,7 +1,7 @@ use std::str::FromStr; use std::time::Duration; -use anyhow::{Context as AnyhowContext, Error}; +use anyhow::{Context as AnyhowContext, Error, anyhow}; use gpui::{ AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement, PathPromptOptions, Render, SharedString, Styled, Task, @@ -209,10 +209,15 @@ impl ProfilePanel { let client = nostr.read(cx).client(); let metadata = metadata.clone(); + let Some(signer) = nostr.read(cx).signer(cx) else { + return Task::ready(Err(anyhow!("Signer is required"))); + }; + cx.background_spawn(async move { // Build and sign the metadata event - let builder = EventBuilder::metadata(&metadata); - let event = client.sign_event_builder(builder).await?; + let event = EventBuilder::metadata(&metadata) + .finalize_async(&signer) + .await?; // Send event to user's relays client.send_event(&event).await?; diff --git a/desktop/src/panels/relay_list.rs b/desktop/src/panels/relay_list.rs index 8e8dfb8..26d4237 100644 --- a/desktop/src/panels/relay_list.rs +++ b/desktop/src/panels/relay_list.rs @@ -1,7 +1,7 @@ use std::collections::HashSet; use std::time::Duration; -use anyhow::{Context as AnyhowContext, Error, anyhow}; +use anyhow::{Error, anyhow}; use gpui::prelude::FluentBuilder; use gpui::{ Action, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, @@ -100,18 +100,19 @@ impl RelayListPanel { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); + let Some(public_key) = nostr.read(cx).signer_pubkey(cx) else { + return; + }; + let task: Task)>, Error>> = cx .background_spawn(async move { - let signer = client.signer().context("Signer not found")?; - let public_key = signer.get_public_key().await?; - let filter = Filter::new() .kind(Kind::RelayList) .author(public_key) .limit(1); if let Some(event) = client.database().query(filter).await?.first_owned() { - Ok(nip65::extract_owned_relay_list(event).collect()) + Ok(nip65::extract_relay_list(&event).collect()) } else { Err(anyhow!("Not found.")) } @@ -207,6 +208,10 @@ impl RelayListPanel { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); + let Some(signer) = nostr.read(cx).signer(cx) else { + return; + }; + // Get all relays let relays = self.relays.clone(); @@ -214,8 +219,9 @@ impl RelayListPanel { self.set_updating(true, cx); let task: Task> = cx.background_spawn(async move { - let builder = EventBuilder::relay_list(relays); - let event = client.sign_event_builder(builder).await?; + let event = EventBuilder::relay_list(relays) + .finalize_async(&signer) + .await?; // Set relay list for current user client.send_event(&event).await?; diff --git a/desktop/src/sidebar/mod.rs b/desktop/src/sidebar/mod.rs index fb74388..fa5598f 100644 --- a/desktop/src/sidebar/mod.rs +++ b/desktop/src/sidebar/mod.rs @@ -2,7 +2,7 @@ use std::collections::HashSet; use std::ops::Range; use std::time::Duration; -use anyhow::{Context as AnyhowContext, Error}; +use anyhow::Error; use chat::{ChatEvent, ChatRegistry, Room, RoomKind}; use common::{DebouncedDelay, TimestampExt, coop_cache}; use entry::RoomEntry; @@ -159,11 +159,12 @@ impl Sidebar { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let task: Task, Error>> = cx.background_spawn(async move { - let signer = client.signer().context("Signer not found")?; - let public_key = signer.get_public_key().await?; - let contacts = client.database().contacts_public_keys(public_key).await?; + let Some(public_key) = nostr.read(cx).signer_pubkey(cx) else { + return; + }; + let task: Task, Error>> = cx.background_spawn(async move { + let contacts = client.database().contacts_public_keys(public_key).await?; Ok(contacts) }); @@ -319,14 +320,14 @@ impl Sidebar { let async_chat = chat.downgrade(); let nostr = NostrRegistry::global(cx); - let signer = nostr.read(cx).signer(); + let Some(public_key) = nostr.read(cx).signer_pubkey(cx) else { + return; + }; // Get all selected public keys let receivers = self.get_selected(cx); self.tasks.push(cx.spawn_in(window, async move |this, cx| { - let public_key = signer.get_public_key().await?; - // Create a new room and emit it async_chat.update_in(cx, |this, _window, cx| { let room = cx.new(|_| { diff --git a/desktop/src/workspace.rs b/desktop/src/workspace.rs index 64f17f4..d33eefa 100644 --- a/desktop/src/workspace.rs +++ b/desktop/src/workspace.rs @@ -16,38 +16,31 @@ use serde::Deserialize; use smallvec::{SmallVec, smallvec}; use state::{IMAGE_CACHE_SIZE, NostrRegistry, StateEvent}; use theme::{ActiveTheme, SIDEBAR_WIDTH, Theme, ThemeRegistry}; -use title_bar::TitleBar; use ui::avatar::Avatar; use ui::button::{Button, ButtonVariants}; use ui::dock::{ClosePanel, DockArea, DockItem, DockPlacement, PanelView}; use ui::menu::{DropdownMenu, PopupMenuItem}; use ui::notification::{Notification, NotificationKind}; -use ui::{Icon, IconName, Root, Sizable, WindowExtension, h_flex, v_flex}; +use ui::{Icon, IconName, Root, Sizable, TitleBar, WindowExtension, h_flex, v_flex}; +use crate::dialogs::import::ImportIdentity; use crate::dialogs::restore::RestoreEncryption; -use crate::dialogs::{accounts, settings}; +use crate::dialogs::settings; use crate::panels::{backup, contact_list, greeter, messaging_relays, profile, relay_list, trash}; use crate::sidebar; -const PREPARE_MSG: &str = "Coop is preparing a new identity for you. This may take a moment..."; -const ENC_MSG: &str = "Encryption Key is a special key that used to encrypt and decrypt your messages. \ - Your identity is completely decoupled from all encryption processes to protect your privacy."; -const ENC_WARN: &str = "By resetting your encryption key, you will lose access to \ - all your encrypted messages before. This action cannot be undone."; - pub fn init(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Workspace::new(window, cx)) } struct DeviceNotifcation; -struct SignerNotifcation; struct RelayNotifcation; +struct MsgRelayNotification; #[derive(Action, Clone, PartialEq, Eq, Deserialize)] #[action(namespace = workspace, no_json)] enum Command { ToggleTheme, - ToggleAccount, RefreshMessagingRelays, BackupEncryption, @@ -64,9 +57,6 @@ enum Command { } pub struct Workspace { - /// App's Title Bar - titlebar: Entity, - /// App's Dock Area dock: Entity, @@ -74,7 +64,7 @@ pub struct Workspace { image_cache: Entity, /// Event subscriptions - _subscriptions: SmallVec<[Subscription; 5]>, + _subscriptions: SmallVec<[Subscription; 6]>, } impl Workspace { @@ -82,8 +72,8 @@ impl Workspace { let chat = ChatRegistry::global(cx); let device = DeviceRegistry::global(cx); let nostr = NostrRegistry::global(cx); + let signer = nostr.read(cx).signer.clone(); - let titlebar = cx.new(|_| TitleBar::new()); let dock = cx.new(|cx| DockArea::new(window, cx)); let image_cache = CoopImageCache::new(IMAGE_CACHE_SIZE, cx); @@ -97,19 +87,20 @@ impl Workspace { ); subscriptions.push( - // Subscribe to the signer events - cx.subscribe_in(&nostr, window, move |this, _state, event, window, cx| { - match event { - StateEvent::Creating => { - let note = Notification::new() - .id::() - .title("Preparing a new identity") - .message(PREPARE_MSG) - .autohide(false) - .with_kind(NotificationKind::Info); + // Observe the signer + cx.observe_in(&signer, window, |this, signer, window, cx| { + if signer.read(cx).is_some() { + this.set_center_layout(window, cx); + } else { + this.import_identity(window, cx); + } + }), + ); - window.push_notification(note, cx); - } + subscriptions.push( + // Subscribe to the nostr events + cx.subscribe_in(&nostr, window, move |this, state, event, window, cx| { + match event { StateEvent::Connecting => { let note = Notification::new() .id::() @@ -125,14 +116,10 @@ impl Workspace { .with_kind(NotificationKind::Success); window.push_notification(note, cx); - } - StateEvent::SignerSet => { - this.set_center_layout(window, cx); - // Clear the signer notification - window.clear_notification::(cx); - } - StateEvent::Show => { - this.account_selector(window, cx); + + if state.read(cx).signer.read(cx).is_none() { + this.import_identity(window, cx); + } } _ => {} }; @@ -145,7 +132,7 @@ impl Workspace { match event { DeviceEvent::Requesting => { const MSG: &str = - "Coop has sent a request for an encryption key. Please open the other client then approve the request."; + "Please open other client and approve the request for encryption key."; let note = Notification::new() .id::() @@ -156,12 +143,25 @@ impl Workspace { window.push_notification(note, cx); } - DeviceEvent::Creating => { + DeviceEvent::NotSet => { + const MSG: &str = + "User're not setup encryption key yet. Do you want to create one?"; + let note = Notification::new() .id::() - .autohide(false) - .message("Creating encryption key") - .with_kind(NotificationKind::Info); + .message(MSG) + .with_kind(NotificationKind::Info) + .action(|_this, _window, _cx| { + Button::new("retry").label("Retry").on_click( + move |_this, window, cx| { + let device = DeviceRegistry::global(cx); + device.update(cx, |this, cx| { + this.set_announcement(Keys::generate(), cx); + }); + window.clear_notification::(cx); + }, + ) + }); window.push_notification(note, cx); } @@ -184,6 +184,27 @@ impl Workspace { // Observe all events emitted by the chat registry cx.subscribe_in(&chat, window, move |this, chat, ev, window, cx| { match ev { + ChatEvent::InboxRelayNotFound => { + const MSG: &str = "Messaging Relays not found. Cannot receive messages."; + + window.push_notification( + Notification::warning(MSG) + .id::() + .autohide(false) + .action(|_this, _window, _cx| { + Button::new("retry").label("Retry").on_click( + move |_this, window, cx| { + let chat = ChatRegistry::global(cx); + chat.update(cx, |this, cx| { + this.get_metadata(cx); + }); + window.clear_notification::(cx); + }, + ) + }), + cx, + ); + } ChatEvent::OpenRoom(id) => { if let Some(room) = chat.read(cx).room(id, cx) { this.dock.update(cx, |this, cx| { @@ -233,7 +254,6 @@ impl Workspace { }); Self { - titlebar, dock, image_cache, _subscriptions: subscriptions, @@ -306,9 +326,8 @@ impl Workspace { } Command::ShowProfile => { let nostr = NostrRegistry::global(cx); - let signer = nostr.read(cx).signer(); - if let Some(public_key) = signer.public_key() { + if let Some(public_key) = nostr.read(cx).signer_pubkey(cx) { self.dock.update(cx, |this, cx| { this.add_panel( Arc::new(profile::init(public_key, window, cx)), @@ -353,7 +372,7 @@ impl Workspace { let chat = ChatRegistry::global(cx); // Trigger a refresh of the chat registry chat.update(cx, |this, cx| { - this.refresh(window, cx); + this.refresh(cx); }); } Command::ShowRelayList => { @@ -378,9 +397,6 @@ impl Workspace { Command::ToggleTheme => { self.theme_selector(window, cx); } - Command::ToggleAccount => { - self.account_selector(window, cx); - } Command::BackupEncryption => { let device = DeviceRegistry::global(cx).downgrade(); let save_dialog = cx.prompt_for_new_path(download_dir(), Some("encryption.txt")); @@ -423,6 +439,12 @@ impl Workspace { } fn confirm_reset_encryption(&mut self, window: &mut Window, cx: &mut Context) { + const ENC_MSG: &str = "Encryption Key is a special key that used to encrypt and decrypt your messages. \ + Your identity is completely decoupled from all encryption processes to protect your privacy."; + + const ENC_WARN: &str = "By resetting your encryption key, you will lose access to \ + all your encrypted messages before. This action cannot be undone."; + let device = DeviceRegistry::global(cx); let ent = device.downgrade(); @@ -457,24 +479,22 @@ impl Workspace { fn import_encryption(&mut self, window: &mut Window, cx: &mut Context) { let restore = cx.new(|cx| RestoreEncryption::new(window, cx)); - window.open_modal(cx, move |this, _window, _cx| { - this.width(px(520.)) + this.width(px(420.)) .title("Restore Encryption") .child(restore.clone()) }); } - fn account_selector(&mut self, window: &mut Window, cx: &mut Context) { - let accounts = accounts::init(window, cx); + fn import_identity(&mut self, window: &mut Window, cx: &mut Context) { + let import = cx.new(|cx| ImportIdentity::new(window, cx)); window.open_modal(cx, move |this, _window, _cx| { - this.width(px(520.)) - .title("Continue with") + this.width(px(420.)) .show_close(false) - .keyboard(false) .overlay_closable(false) - .child(accounts.clone()) + .title("Import Identity") + .child(import.clone()) }); } @@ -560,8 +580,7 @@ impl Workspace { fn titlebar_left(&mut self, cx: &mut Context) -> impl IntoElement { let nostr = NostrRegistry::global(cx); - let signer = nostr.read(cx).signer(); - let current_user = signer.public_key(); + let current_user = nostr.read(cx).signer_pubkey(cx); h_flex() .flex_shrink_0() @@ -571,7 +590,7 @@ impl Workspace { div() .text_xs() .text_color(cx.theme().text_muted) - .child(SharedString::from("Choose an account to continue...")), + .child(SharedString::from("Import your identity to continue")), ) }) .when_some(current_user.as_ref(), |this, public_key| { @@ -622,11 +641,6 @@ impl Workspace { Box::new(Command::ToggleTheme), ) .separator() - .menu_with_icon( - "Accounts", - IconName::Group, - Box::new(Command::ToggleAccount), - ) .menu_with_icon( "Settings", IconName::Settings, @@ -639,16 +653,12 @@ impl Workspace { fn titlebar_right(&mut self, cx: &mut Context) -> impl IntoElement { let chat = ChatRegistry::global(cx); - let initializing = chat.read(cx).initializing; let trash_messages = chat.read(cx).count_trash_messages(cx); - let device = DeviceRegistry::global(cx); - let device_initializing = device.read(cx).initializing; - + let is_nip4e_enabled = AppSettings::get_nip4e(cx); let nostr = NostrRegistry::global(cx); - let signer = nostr.read(cx).signer(); - let Some(public_key) = signer.public_key() else { + let Some(public_key) = nostr.read(cx).signer_pubkey(cx) else { return div(); }; @@ -691,83 +701,75 @@ impl Workspace { }), ) }) - .child( - Button::new("key") - .icon(IconName::UserKey) - .tooltip("Decoupled encryption key") - .small() - .ghost() - .loading(device_initializing) - .when(device_initializing, |this| { - this.label("Dekey") - .xsmall() - .tooltip("Loading decoupled encryption key...") - }) - .dropdown_menu(move |this, _window, _cx| { - this.min_w(px(260.)) - .label("Encryption Key") - .when_some(announcement.as_ref(), |this, announcement| { - let name = announcement.client_name(); - let pkey = shorten_pubkey(announcement.public_key(), 8); + .when(is_nip4e_enabled, |this| { + this.child( + Button::new("key") + .icon(IconName::UserKey) + .tooltip("Decoupled encryption key") + .small() + .ghost() + .dropdown_menu(move |this, _window, _cx| { + this.min_w(px(260.)) + .label("Encryption Key") + .when_some(announcement.as_ref(), |this, announcement| { + let name = announcement.client_name(); + let pkey = shorten_pubkey(announcement.public_key(), 8); - this.item(PopupMenuItem::element(move |_window, cx| { - h_flex() - .gap_1() - .text_sm() - .child( - Icon::new(IconName::Device) - .small() - .text_color(cx.theme().icon_muted), - ) - .child(name.clone()) - })) - .item(PopupMenuItem::element(move |_window, cx| { - h_flex() - .gap_1() - .text_sm() - .child( - Icon::new(IconName::UserKey) - .small() - .text_color(cx.theme().icon_muted), - ) - .child(SharedString::from(pkey.clone())) - })) - }) - .separator() - .menu_with_icon( - "Backup", - IconName::Shield, - Box::new(Command::BackupEncryption), - ) - .menu_with_icon( - "Restore from secret key", - IconName::Usb, - Box::new(Command::ImportEncryption), - ) - .separator() - .menu_with_icon( - "Reload", - IconName::Refresh, - Box::new(Command::RefreshEncryption), - ) - .menu_with_icon( - "Reset", - IconName::Warning, - Box::new(Command::ResetEncryption), - ) - }), - ) + this.item(PopupMenuItem::element(move |_window, cx| { + h_flex() + .gap_1() + .text_sm() + .child( + Icon::new(IconName::Device) + .small() + .text_color(cx.theme().icon_muted), + ) + .child(name.clone()) + })) + .item( + PopupMenuItem::element(move |_window, cx| { + h_flex() + .gap_1() + .text_sm() + .child( + Icon::new(IconName::UserKey) + .small() + .text_color(cx.theme().icon_muted), + ) + .child(SharedString::from(pkey.clone())) + }), + ) + }) + .separator() + .menu_with_icon( + "Backup", + IconName::Shield, + Box::new(Command::BackupEncryption), + ) + .menu_with_icon( + "Restore from secret key", + IconName::Usb, + Box::new(Command::ImportEncryption), + ) + .separator() + .menu_with_icon( + "Reload", + IconName::Refresh, + Box::new(Command::RefreshEncryption), + ) + .menu_with_icon( + "Reset", + IconName::Warning, + Box::new(Command::ResetEncryption), + ) + }), + ) + }) .child( Button::new("inbox") .icon(IconName::Inbox) .small() .ghost() - .loading(initializing) - .when(initializing, |this| { - this.label("Inbox") - .xsmall() - .tooltip("Getting inbox messages...") - }) .dropdown_menu(move |this, _window, cx| { let urls: Vec<(SharedString, SharedString)> = profile .messaging_relays() @@ -838,15 +840,6 @@ impl Render for Workspace { let modal_layer = Root::render_modal_layer(window, cx); let notification_layer = Root::render_notification_layer(window, cx); - // Titlebar elements - let left = self.titlebar_left(cx).into_any_element(); - let right = self.titlebar_right(cx).into_any_element(); - - // Update title bar children - self.titlebar.update(cx, |this, _cx| { - this.set_children(vec![left, right]); - }); - div() .id(SharedString::from("workspace")) .on_action(cx.listener(Self::on_command)) @@ -860,7 +853,11 @@ impl Render for Workspace { v_flex() .size_full() // Title Bar - .child(self.titlebar.clone()) + .child( + TitleBar::new() + .child(self.titlebar_left(cx)) + .child(self.titlebar_right(cx)), + ) // Dock .child(self.dock.clone()), ), diff --git a/rust-toolchain.toml b/rust-toolchain.toml index a3f50e4..65beb3c 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,5 +1,5 @@ [toolchain] -channel = "1.92" +channel = "1.96" profile = "minimal" components = ["rustfmt", "clippy"] targets = [