From 193df36daeb5da3946dc07c453fd30101f902fc7 Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Thu, 8 Oct 2026 20:58:58 +0700 Subject: [PATCH] feat: add group management to the sidebar --- Cargo.lock | 212 ++++---- crates/nip29/src/group.rs | 9 + crates/nip29/src/protocol.rs | 15 + crates/nip29_ui/src/details.rs | 71 +-- crates/nip29_ui/src/invite.rs | 2 +- crates/nip29_ui/src/lib.rs | 120 ++--- crates/nip29_ui/src/metadata.rs | 11 +- crates/nip29_ui/src/util.rs | 91 ++++ crates/workspace/src/dialogs/import.rs | 18 +- crates/workspace/src/lib.rs | 16 +- crates/workspace/src/panels/mod.rs | 2 - crates/workspace/src/panels/requests.rs | 62 --- crates/workspace/src/panels/search.rs | 576 --------------------- crates/workspace/src/sidebar/group_view.rs | 17 + 14 files changed, 299 insertions(+), 923 deletions(-) create mode 100644 crates/nip29_ui/src/util.rs delete mode 100644 crates/workspace/src/panels/requests.rs delete mode 100644 crates/workspace/src/panels/search.rs diff --git a/Cargo.lock b/Cargo.lock index 7a399e2a..276799ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -45,7 +45,7 @@ dependencies = [ "accesskit", "accesskit_consumer", "hashbrown 0.16.1", - "objc2 0.5.2", + "objc2 0.5.3", "objc2-app-kit 0.2.2", "objc2-foundation 0.2.2", ] @@ -196,9 +196,9 @@ checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "arbitrary" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +checksum = "3bc62ac97cc33321f50863d514c3bc38a453947a8f9e781137e47c7401020aed" [[package]] name = "arg_enum_proc_macro" @@ -442,8 +442,7 @@ dependencies = [ [[package]] name = "async-utility" version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "188f83b9a198af8c336e505611edb00d6d2ac5c694241c5a4f9a12316938cfe9" +source = "git+https://github.com/yukibtc/async-utility?rev=7d3a79983d0e848aafed9d321637af7334e01349#7d3a79983d0e848aafed9d321637af7334e01349" dependencies = [ "futures-util", "gloo-timers", @@ -813,7 +812,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" dependencies = [ - "objc2 0.5.2", + "objc2 0.5.3", ] [[package]] @@ -822,7 +821,7 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" dependencies = [ - "objc2 0.6.4", + "objc2 0.6.5", ] [[package]] @@ -1719,9 +1718,9 @@ dependencies = [ [[package]] name = "ctutils" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +checksum = "03bb0e1cc970d482d121d9a1744999169b69a07470b3d644a7894e53fcaf4574" dependencies = [ "cmov", ] @@ -1866,7 +1865,7 @@ dependencies = [ "bitflags 2.13.2", "block2 0.6.2", "libc", - "objc2 0.6.4", + "objc2 0.6.5", ] [[package]] @@ -1939,20 +1938,20 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "either" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" +checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" [[package]] name = "embed-resource" -version = "3.0.11" +version = "3.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbfdaacccebec3b28e4866b8973543c7647797db5ada1bdab552e48fe665fbbd" +checksum = "6168487b5e7eb2673e6efe1200b1848016e436a6ca0b8031bf04bd9d493a249f" dependencies = [ "cc", "memchr", "rustc_version", - "toml 1.1.6+spec-1.1.0", + "toml 1.1.7+spec-1.1.0", "vswhom", "winreg", ] @@ -2694,7 +2693,7 @@ dependencies = [ "markdown", "markup5ever_rcdom", "num-traits", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-app-kit 0.3.2", "objc2-foundation 0.3.2", "raw-window-handle", @@ -2808,7 +2807,7 @@ dependencies = [ "log", "mach2", "metal", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-foundation 0.3.2", "objc2-quartz-core 0.3.2", "parking_lot", @@ -2943,7 +2942,7 @@ dependencies = [ "itertools 0.14.0", "log", "objc", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-app-kit 0.3.2", "objc2-core-graphics", "objc2-core-media", @@ -3282,9 +3281,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.19" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16" +checksum = "7d29020232d6aa3fb1daca64c1127cf662cf97f254ae16c18c05b8ab635fc118" dependencies = [ "atomic-waker", "bytes", @@ -3558,9 +3557,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" +checksum = "2c3e324da4c95177d6291d4c8730197c0d1822f8a9766814a4a44fa5ab797c9c" dependencies = [ "atomic-waker", "bytes", @@ -3847,9 +3846,9 @@ dependencies = [ [[package]] name = "inventory" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" +checksum = "6928282826c822ad91bf1c9a1cb90a30ba1c26770749929b4656cd6be829cd7c" dependencies = [ "rustversion", ] @@ -4263,7 +4262,7 @@ checksum = "fd604973958ddcc11b561193c0fb96ba146506ef2f231ef2e7c35fd2cbc9beca" dependencies = [ "cc", "log", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-foundation 0.3.2", "time", "uuid", @@ -4602,7 +4601,7 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" [[package]] name = "nostr" version = "0.45.5" -source = "git+https://github.com/rust-nostr/nostr#1c687bb852eb6dc90cd5e05a9571d0b20becc4b8" +source = "git+https://github.com/rust-nostr/nostr#463f0c2d48e1d3c9e295cc231715d8a3855c65d3" dependencies = [ "aes", "base64 0.22.1", @@ -4628,7 +4627,7 @@ dependencies = [ [[package]] name = "nostr-blossom" version = "0.45.1" -source = "git+https://github.com/rust-nostr/nostr#1c687bb852eb6dc90cd5e05a9571d0b20becc4b8" +source = "git+https://github.com/rust-nostr/nostr#463f0c2d48e1d3c9e295cc231715d8a3855c65d3" dependencies = [ "base64 0.22.1", "bitcoin_hashes 1.2.0", @@ -4641,7 +4640,7 @@ dependencies = [ [[package]] name = "nostr-connect" version = "0.45.2" -source = "git+https://github.com/rust-nostr/nostr#1c687bb852eb6dc90cd5e05a9571d0b20becc4b8" +source = "git+https://github.com/rust-nostr/nostr#463f0c2d48e1d3c9e295cc231715d8a3855c65d3" dependencies = [ "async-utility", "futures-core", @@ -4655,7 +4654,7 @@ dependencies = [ [[package]] name = "nostr-database" version = "0.45.2" -source = "git+https://github.com/rust-nostr/nostr#1c687bb852eb6dc90cd5e05a9571d0b20becc4b8" +source = "git+https://github.com/rust-nostr/nostr#463f0c2d48e1d3c9e295cc231715d8a3855c65d3" dependencies = [ "nostr", "opaquerr 0.2.0", @@ -4664,7 +4663,7 @@ dependencies = [ [[package]] name = "nostr-gossip" version = "0.45.1" -source = "git+https://github.com/rust-nostr/nostr#1c687bb852eb6dc90cd5e05a9571d0b20becc4b8" +source = "git+https://github.com/rust-nostr/nostr#463f0c2d48e1d3c9e295cc231715d8a3855c65d3" dependencies = [ "nostr", "opaquerr 0.2.0", @@ -4673,7 +4672,7 @@ dependencies = [ [[package]] name = "nostr-gossip-memory" version = "0.45.1" -source = "git+https://github.com/rust-nostr/nostr#1c687bb852eb6dc90cd5e05a9571d0b20becc4b8" +source = "git+https://github.com/rust-nostr/nostr#463f0c2d48e1d3c9e295cc231715d8a3855c65d3" dependencies = [ "indexmap", "lru", @@ -4685,7 +4684,7 @@ dependencies = [ [[package]] name = "nostr-lmdb" version = "0.45.3" -source = "git+https://github.com/rust-nostr/nostr#1c687bb852eb6dc90cd5e05a9571d0b20becc4b8" +source = "git+https://github.com/rust-nostr/nostr#463f0c2d48e1d3c9e295cc231715d8a3855c65d3" dependencies = [ "async-utility", "flatbuffers", @@ -4700,7 +4699,7 @@ dependencies = [ [[package]] name = "nostr-memory" version = "0.45.2" -source = "git+https://github.com/rust-nostr/nostr#1c687bb852eb6dc90cd5e05a9571d0b20becc4b8" +source = "git+https://github.com/rust-nostr/nostr#463f0c2d48e1d3c9e295cc231715d8a3855c65d3" dependencies = [ "btreecap", "nostr", @@ -4711,7 +4710,7 @@ dependencies = [ [[package]] name = "nostr-sdk" version = "0.45.4" -source = "git+https://github.com/rust-nostr/nostr#1c687bb852eb6dc90cd5e05a9571d0b20becc4b8" +source = "git+https://github.com/rust-nostr/nostr#463f0c2d48e1d3c9e295cc231715d8a3855c65d3" dependencies = [ "async-utility", "async-wsocket", @@ -4732,9 +4731,9 @@ dependencies = [ [[package]] name = "notify-rust" -version = "4.18.1" +version = "4.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe430c879247371c47fdedd328ad4e301e6b30b533c5fd5db1c06d75253d0e46" +checksum = "4587364a9a0074333429b3df75a30a205340c56a536ca3eb6ca0e59b87bbf8af" dependencies = [ "futures-lite", "log", @@ -4909,9 +4908,9 @@ checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" [[package]] name = "objc2" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +checksum = "19d5490aaf8f1d7cf7688dfa9b0ce07900e168852c45cd2c03f534dfd27cfd0b" dependencies = [ "objc-sys", "objc2-encode", @@ -4919,9 +4918,9 @@ dependencies = [ [[package]] name = "objc2" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +checksum = "08849bbd4767dfae9457696856ae1c84fe4e0281bbe4a7abff2d0e06fb7981f8" dependencies = [ "objc2-encode", ] @@ -4935,7 +4934,7 @@ dependencies = [ "bitflags 2.13.2", "block2 0.5.1", "libc", - "objc2 0.5.2", + "objc2 0.5.3", "objc2-core-data 0.2.2", "objc2-core-image 0.2.2", "objc2-foundation 0.2.2", @@ -4951,7 +4950,7 @@ dependencies = [ "bitflags 2.13.2", "block2 0.6.2", "libc", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-cloud-kit", "objc2-core-data 0.3.2", "objc2-core-foundation", @@ -4970,7 +4969,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" dependencies = [ "bitflags 2.13.2", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-foundation 0.3.2", ] @@ -4981,7 +4980,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2" dependencies = [ "dispatch2", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-core-audio-types", "objc2-core-foundation", ] @@ -4993,7 +4992,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c" dependencies = [ "bitflags 2.13.2", - "objc2 0.6.4", + "objc2 0.6.5", ] [[package]] @@ -5004,7 +5003,7 @@ checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ "bitflags 2.13.2", "block2 0.5.1", - "objc2 0.5.2", + "objc2 0.5.3", "objc2-foundation 0.2.2", ] @@ -5015,7 +5014,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" dependencies = [ "bitflags 2.13.2", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-foundation 0.3.2", ] @@ -5027,7 +5026,7 @@ checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ "bitflags 2.13.2", "dispatch2", - "objc2 0.6.4", + "objc2 0.6.5", ] [[package]] @@ -5038,7 +5037,7 @@ checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ "bitflags 2.13.2", "dispatch2", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-core-foundation", "objc2-io-surface", ] @@ -5050,7 +5049,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" dependencies = [ "block2 0.5.1", - "objc2 0.5.2", + "objc2 0.5.3", "objc2-foundation 0.2.2", "objc2-metal 0.2.2", ] @@ -5061,7 +5060,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" dependencies = [ - "objc2 0.6.4", + "objc2 0.6.5", "objc2-foundation 0.3.2", ] @@ -5071,7 +5070,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" dependencies = [ - "objc2 0.6.4", + "objc2 0.6.5", "objc2-foundation 0.3.2", ] @@ -5083,7 +5082,7 @@ checksum = "05ec576860167a15dd9fce7fbee7512beb4e31f532159d3482d1f9c6caedf31d" dependencies = [ "bitflags 2.13.2", "dispatch2", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-core-audio", "objc2-core-audio-types", "objc2-core-foundation", @@ -5097,7 +5096,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" dependencies = [ "bitflags 2.13.2", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-core-foundation", "objc2-core-graphics", ] @@ -5109,7 +5108,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" dependencies = [ "bitflags 2.13.2", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-core-foundation", "objc2-core-graphics", "objc2-io-surface", @@ -5130,7 +5129,7 @@ dependencies = [ "bitflags 2.13.2", "block2 0.5.1", "libc", - "objc2 0.5.2", + "objc2 0.5.3", ] [[package]] @@ -5142,7 +5141,7 @@ dependencies = [ "bitflags 2.13.2", "block2 0.6.2", "libc", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-core-foundation", ] @@ -5153,7 +5152,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ "bitflags 2.13.2", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-core-foundation", ] @@ -5165,7 +5164,7 @@ checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ "bitflags 2.13.2", "block2 0.5.1", - "objc2 0.5.2", + "objc2 0.5.3", "objc2-foundation 0.2.2", ] @@ -5177,7 +5176,7 @@ checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" dependencies = [ "bitflags 2.13.2", "block2 0.6.2", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-foundation 0.3.2", ] @@ -5189,7 +5188,7 @@ checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ "bitflags 2.13.2", "block2 0.5.1", - "objc2 0.5.2", + "objc2 0.5.3", "objc2-foundation 0.2.2", "objc2-metal 0.2.2", ] @@ -5201,7 +5200,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ "bitflags 2.13.2", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-core-foundation", "objc2-foundation 0.3.2", "objc2-metal 0.3.2", @@ -5215,7 +5214,7 @@ checksum = "74b7c5390f477482f001bc354d6571a70db7e4f8d5288e860c45521fbce11394" dependencies = [ "block2 0.6.2", "dispatch2", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-core-graphics", "objc2-core-media", "objc2-foundation 0.3.2", @@ -5229,7 +5228,7 @@ checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" dependencies = [ "bitflags 2.13.2", "block2 0.6.2", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-core-location", "objc2-foundation 0.3.2", ] @@ -5784,7 +5783,7 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.25.15+spec-1.1.0", + "toml_edit 0.25.16+spec-1.1.0", ] [[package]] @@ -6113,7 +6112,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40d213455a5f1dc59214213c7330e074ddf8114c9a42411eb890c767357ce135" dependencies = [ - "objc2 0.6.4", + "objc2 0.6.5", "objc2-core-foundation", "objc2-foundation 0.3.2", "objc2-quartz-core 0.3.2", @@ -6347,9 +6346,9 @@ dependencies = [ [[package]] name = "rust-embed" -version = "8.12.0" +version = "8.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9e7760e252aaba7b09f4be00e36476cf585bdb68a53552ac954cdf504ab4bc9" +checksum = "19afa5b4b6a611de00bd1bdae6ae6f39084c9399f0679c3f52d8469cf335cc23" dependencies = [ "rust-embed-impl", "rust-embed-utils", @@ -6358,9 +6357,9 @@ dependencies = [ [[package]] name = "rust-embed-impl" -version = "8.12.0" +version = "8.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bcfc4d6f53af43755f7a723e4b6b8794fcce052a178dd8c6c1dadc5f5343097" +checksum = "e0d8afda6374eac59e066abee06d265247ebbaf3006cf878e2879e8356e34053" dependencies = [ "mime_guess", "proc-macro2", @@ -6372,9 +6371,9 @@ dependencies = [ [[package]] name = "rust-embed-utils" -version = "8.12.0" +version = "8.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ffa149f6aa81b58a5b3011d01a857c4ed12c7a732d2c51947a4c7c692185f0" +checksum = "0d84e8ba78bd384263e5922f084cbe1b081c3b7e69add59c8fb097b879ba968a" dependencies = [ "globset", "sha2 0.11.0", @@ -6799,9 +6798,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +checksum = "4e7523beb55eece201a2356bee0bbca0d1ab466c14c07703b2e0ee6d42cb0c2c" dependencies = [ "serde_core", ] @@ -7703,14 +7702,14 @@ dependencies = [ [[package]] name = "toml" -version = "1.1.6+spec-1.1.0" +version = "1.1.7+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "920602543f0911ab71da12c50d59701da54c196d1a2bf5cb4b75667f137a406a" +checksum = "7874cd34dd99040fc24e30923aa64a60a81ff3ddfe2b8129ccc84ba24949deed" dependencies = [ "indexmap", "serde_core", - "serde_spanned 1.1.1", - "toml_datetime 1.1.1+spec-1.1.0", + "serde_spanned 1.1.2", + "toml_datetime 1.1.2+spec-1.1.0", "toml_parser", "toml_writer", "winnow 1.0.4", @@ -7727,9 +7726,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "1.1.1+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053" dependencies = [ "serde_core", ] @@ -7750,21 +7749,21 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.15+spec-1.1.0" +version = "0.25.16+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614" +checksum = "88f38f3af8bf71841c67a990f4e2b52143b2d0a1a994c72513ff4c14977fc722" dependencies = [ "indexmap", - "toml_datetime 1.1.1+spec-1.1.0", + "toml_datetime 1.1.2+spec-1.1.0", "toml_parser", "winnow 1.0.4", ] [[package]] name = "toml_parser" -version = "1.1.3+spec-1.1.0" +version = "1.1.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +checksum = "bed810ee007d8e7f9cf20b4774e7198142fa27186e0f805871da7f3c39eaf333" dependencies = [ "winnow 1.0.4", ] @@ -7777,9 +7776,9 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "toml_writer" -version = "1.1.2+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" +checksum = "06bdbd8cfc056b8d2e2e85f29b56a3bdbecb527cef81eb39e3e7b98af4652770" [[package]] name = "tower" @@ -7967,9 +7966,9 @@ dependencies = [ [[package]] name = "unicase" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" +checksum = "357cc3acc6a036009fd6c973ed009037c732d60d0b4f6c673e9041497482a28f" [[package]] name = "unicode-bidi" @@ -8279,9 +8278,9 @@ dependencies = [ [[package]] name = "want" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +checksum = "ec4cdd0dd910afe868b7ef477227d8d538b46b3075031afee8a9f2acb0a2ed0b" dependencies = [ "try-lock", ] @@ -8510,7 +8509,7 @@ dependencies = [ "jni", "log", "ndk-context", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-app-kit 0.3.2", "objc2-foundation 0.3.2", "url", @@ -8677,7 +8676,7 @@ dependencies = [ "log", "naga", "ndk-sys", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-core-foundation", "objc2-foundation 0.3.2", "objc2-metal 0.3.2", @@ -9128,15 +9127,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-sys" version = "0.61.2" @@ -9379,12 +9369,12 @@ dependencies = [ [[package]] name = "winreg" -version = "0.55.0" +version = "0.56.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10" dependencies = [ "cfg-if", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -9763,18 +9753,18 @@ checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" [[package]] name = "zerocopy" -version = "0.8.59" +version = "0.8.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6df92bf3d9227be3d53173901ddbffac2babc27ae50f397776ffd6dc33f800cb" +checksum = "879fb705ce98c32e41ebdb970fbe1204f8492423b314c6ab0354c3e7b5542866" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.59" +version = "0.8.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac4f328cf2f05d084e496c3e9c3f33ed0a183656a16e1fcec4d464d8373aec82" +checksum = "708882a28301d604fa039cc7727607a98d04c4b86dc76ec9cc683f805d709759" dependencies = [ "proc-macro2", "quote", @@ -9804,9 +9794,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.9.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" dependencies = [ "zeroize_derive", ] diff --git a/crates/nip29/src/group.rs b/crates/nip29/src/group.rs index 032fe68e..a497150f 100644 --- a/crates/nip29/src/group.rs +++ b/crates/nip29/src/group.rs @@ -226,6 +226,15 @@ impl Group { self.dispatch(move |id, prev| id.remove_user(public_key, prev), cx) } + pub fn put_user( + &mut self, + public_key: PublicKey, + roles: Vec, + cx: &mut Context, + ) -> Task> { + self.dispatch(move |id, prev| id.put_user(public_key, &roles, prev), cx) + } + pub fn edit_metadata( &mut self, metadata: &GroupMetadata, diff --git a/crates/nip29/src/protocol.rs b/crates/nip29/src/protocol.rs index 6fc3ab4d..596ec1f1 100644 --- a/crates/nip29/src/protocol.rs +++ b/crates/nip29/src/protocol.rs @@ -134,6 +134,21 @@ impl GroupId { .tag_maybe(previous) } + pub fn put_user( + &self, + public_key: PublicKey, + roles: &[String], + previous: Option, + ) -> EventBuilder { + EventBuilder::new(Kind::GroupPutUser, "") + .tag(self.h_tag()) + .tag(Tag::custom( + "p", + std::iter::once(public_key.to_hex()).chain(roles.iter().cloned()), + )) + .tag_maybe(previous) + } + pub fn edit_metadata(&self, metadata: &GroupMetadata, previous: Option) -> EventBuilder { EventBuilder::new(Kind::GroupEditMetadata, "") .tags(self.metadata_tags(metadata)) diff --git a/crates/nip29_ui/src/details.rs b/crates/nip29_ui/src/details.rs index c4271acb..6a4b8bc3 100644 --- a/crates/nip29_ui/src/details.rs +++ b/crates/nip29_ui/src/details.rs @@ -1,19 +1,17 @@ use gpui::{ AnyElement, App, IntoElement, ParentElement, SharedString, Styled, WeakEntity, Window, div, px, + relative, }; use nip29::{Group, GroupMetadata}; -use nostr_sdk::prelude::PublicKey; -use person::PersonRegistry; use theme::ActiveTheme; use ui::avatar::Avatar; use ui::{Sizable, StyledExt, WindowExtension, h_flex, v_flex}; pub(crate) fn open(group: WeakEntity, window: &mut Window, cx: &mut App) { window.open_dialog(cx, move |this, _window, cx| { - this.width(px(460.)) - .max_h(px(600.)) + this.max_h(px(600.)) .show_close(true) - .title("Group details") + .title("About Group") .child(body(&group, cx)) }); } @@ -89,21 +87,7 @@ fn body(group: &WeakEntity, cx: &App) -> AnyElement { } } - let admins = group.admins().entries(); - - if !admins.is_empty() { - body = body.child(section( - "Admins", - v_flex() - .gap_1() - .children(admins.iter().map(|(key, roles)| admin(key, roles, cx))) - .into_any_element(), - cx, - )); - } - let roles = group.roles().roles(); - if !roles.is_empty() { body = body.child(section( "Roles", @@ -112,21 +96,7 @@ fn body(group: &WeakEntity, cx: &App) -> AnyElement { )); } - let members: Vec<&PublicKey> = group.members().iter().collect(); - - if !members.is_empty() { - body = body.child(section( - "Members", - v_flex() - .gap_1() - .children(members.into_iter().map(|key| person(key, cx))) - .into_any_element(), - cx, - )); - } - let pins = group.pins().pins(); - if !pins.is_empty() { body = body.child(section( "Pinned", @@ -138,7 +108,7 @@ fn body(group: &WeakEntity, cx: &App) -> AnyElement { div() .truncate() .text_color(cx.theme().text_muted) - .child(crate::pin_label(group, pin)) + .child(crate::util::pin_label(group, pin)) .into_any_element() }) .collect::>(), @@ -209,38 +179,15 @@ fn wrap(items: impl IntoIterator) -> AnyElement { .into_any_element() } -fn admin(public_key: &PublicKey, roles: &[String], cx: &App) -> AnyElement { - h_flex() - .justify_between() - .gap_2() - .child(person(public_key, cx)) - .children(roles.iter().map(|role| chip(role.clone(), cx))) - .into_any_element() -} - -fn person(public_key: &PublicKey, cx: &App) -> AnyElement { - let persons = PersonRegistry::global(cx); - let person = persons.read(cx).get(public_key, cx); - - h_flex() - .gap_2() - .min_w_0() - .child( - Avatar::new(person.avatar()) - .seed(person.avatar_seed()) - .xsmall(), - ) - .child(div().truncate().child(person.name())) - .into_any_element() -} - fn chip(label: impl Into, cx: &App) -> AnyElement { div() - .px_1p5() - .py_0p5() + .px_2() + .py_1() .rounded(cx.theme().radius) - .bg(cx.theme().element_background) + .bg(cx.theme().secondary_background) + .text_color(cx.theme().secondary_foreground) .text_xs() + .line_height(relative(1.)) .child(label.into()) .into_any_element() } diff --git a/crates/nip29_ui/src/invite.rs b/crates/nip29_ui/src/invite.rs index 8f7d2688..86da9ad9 100644 --- a/crates/nip29_ui/src/invite.rs +++ b/crates/nip29_ui/src/invite.rs @@ -11,7 +11,7 @@ use ui::input::InputState; use ui::notification::Notification; use ui::{WindowExtension, v_flex}; -use crate::field; +use crate::util::field; const CODE_LEN: usize = 12; diff --git a/crates/nip29_ui/src/lib.rs b/crates/nip29_ui/src/lib.rs index d56f1fec..3708f1c7 100644 --- a/crates/nip29_ui/src/lib.rs +++ b/crates/nip29_ui/src/lib.rs @@ -4,8 +4,8 @@ use anyhow::Result; use chat_ui::text::rendered_text; use gpui::prelude::FluentBuilder; use gpui::{ - Action, AnyElement, App, AppContext, Context, Div, Entity, EventEmitter, FocusHandle, - Focusable, FollowMode, IntoElement, ListAlignment, ListScrollEvent, ListState, ParentElement, + Action, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, + FollowMode, IntoElement, ListAlignment, ListScrollEvent, ListState, ParentElement, PathPromptOptions, Render, SharedString, Styled, Subscription, Task, WeakEntity, Window, div, list, px, }; @@ -21,17 +21,19 @@ use ui::avatar::Avatar; use ui::button::{Button, ButtonVariant, ButtonVariants}; use ui::dialog::DialogButtonProps; use ui::dock::{Panel, PanelEvent}; -use ui::input::{Input, InputEvent, InputState, Textarea, TextareaState}; +use ui::input::{InputEvent, Textarea, TextareaState}; use ui::markdown::RenderedText; use ui::message::WelcomeMessage; use ui::notification::Notification; use ui::scroll::Scrollbar; use ui::{Disableable, Icon, IconName, Sizable, WindowExtension, h_flex, v_flex}; +use util::{attachment_name, display_name, opens_run, pin_label, report}; mod details; mod invite; mod message; mod metadata; +mod util; /// Ask the shell to show the sidebar's information for a group. #[derive(Action, Clone, PartialEq, Eq, Deserialize)] @@ -49,8 +51,8 @@ pub struct HideGroupMeta { pub group: String, } +const ADMIN_ROLE: &str = "admin"; const LOAD_OLDER_THRESHOLD: usize = 20; -const RUN_WINDOW_SECS: u64 = 300; enum Notice { Relay(String), @@ -69,46 +71,6 @@ impl Notice { } } -fn display_name(group: &Group) -> SharedString { - group - .metadata() - .and_then(|metadata| metadata.name()) - .map_or_else( - || SharedString::from(group.key().id().as_str()), - SharedString::from, - ) -} - -fn attachment_name(url: &Url) -> SharedString { - url.path_segments() - .and_then(|mut segments| segments.next_back()) - .filter(|name| !name.is_empty()) - .map_or_else( - || SharedString::from(url.as_str()), - |name| SharedString::from(name.to_owned()), - ) -} - -fn opens_run(rows: &[Row], index: usize) -> bool { - let (Some(current), Some(previous)) = ( - rows.get(index), - index.checked_sub(1).and_then(|index| rows.get(index)), - ) else { - return true; - }; - - if !matches!((current, previous), (Row::Message(_), Row::Message(_))) { - return true; - } - - current.author() != previous.author() - || current - .created_at() - .as_secs() - .saturating_sub(previous.created_at().as_secs()) - > RUN_WINDOW_SECS -} - pub fn init(group: Entity, window: &mut Window, cx: &mut App) -> Entity { let key = group.read(cx).key().clone(); let groups = GroupsRegistry::global(cx); @@ -120,33 +82,6 @@ pub fn init(group: Entity, window: &mut Window, cx: &mut App) -> Entity, cx: &App) -> Div { - v_flex() - .gap_1() - .text_sm() - .text_color(cx.theme().text_muted) - .child(label) - .child(Input::new(input)) -} - -pub(crate) fn pin_label(group: &Group, pin: &Pin) -> SharedString { - match pin { - Pin::Address(coordinate) => SharedString::from(format!("{coordinate}")), - Pin::Event(id) => { - let snippet = group - .message(*id) - .and_then(|message| message.content.lines().next()) - .map(str::trim) - .filter(|line| !line.is_empty()); - - match snippet { - Some(snippet) => SharedString::from(snippet.chars().take(48).collect::()), - None => SharedString::from(id.to_hex().chars().take(8).collect::()), - } - } - } -} - /// Build the group's action buttons for the sidebar: info, admin actions and leave. pub fn actions(group: WeakEntity, cx: &App) -> AnyElement { let nostr = NostrRegistry::global(cx); @@ -259,18 +194,37 @@ pub fn remove_member( } } -/// Await a group write and surface any relay error as a notification. -pub(crate) fn report(window: &mut Window, cx: &mut App, task: Task>) { - window - .spawn(cx, async move |cx| -> Result<()> { - if let Err(error) = task.await { - cx.update(|window, cx| { - window.push_notification(Notification::error(error.to_string()), cx); - })?; - } - Ok(()) - }) - .detach(); +/// Grant a member the admin role and surface any relay error as a notification. +pub fn make_admin( + group: WeakEntity, + public_key: PublicKey, + window: &mut Window, + cx: &mut App, +) { + put_user(group, public_key, vec![ADMIN_ROLE.to_owned()], window, cx); +} + +/// Revoke a member's roles, returning them to a regular member. +pub fn remove_admin( + group: WeakEntity, + public_key: PublicKey, + window: &mut Window, + cx: &mut App, +) { + put_user(group, public_key, Vec::new(), window, cx); +} + +fn put_user( + group: WeakEntity, + public_key: PublicKey, + roles: Vec, + window: &mut Window, + cx: &mut App, +) { + match group.update(cx, |group, cx| group.put_user(public_key, roles, cx)) { + Ok(task) => report(window, cx, task), + Err(error) => log::warn!("nip29: updating a member's roles failed: {error}"), + } } pub struct GroupPanel { diff --git a/crates/nip29_ui/src/metadata.rs b/crates/nip29_ui/src/metadata.rs index bc122846..7524043b 100644 --- a/crates/nip29_ui/src/metadata.rs +++ b/crates/nip29_ui/src/metadata.rs @@ -1,11 +1,11 @@ -use gpui::{App, AppContext, Entity, ParentElement, Styled, WeakEntity, Window, px}; +use gpui::{App, AppContext, Entity, ParentElement, Styled, WeakEntity, Window}; use nip29::{Group, GroupMetadata}; use ui::dialog::DialogButtonProps; use ui::input::InputState; use ui::switch::Switch; use ui::{WindowExtension, v_flex}; -use crate::field; +use crate::util::field; #[derive(Clone, Copy, Default)] struct Flags { @@ -58,9 +58,8 @@ pub(crate) fn open(group: WeakEntity, window: &mut Window, cx: &mut App) window.open_dialog(cx, move |this, _window, cx| { let current = *flags.read(cx); - this.width(px(420.)) - .confirm() - .title("Edit group") + this.confirm() + .title("Edit Group") .button_props( DialogButtonProps::default() .cancel_text("Cancel") @@ -138,7 +137,7 @@ pub(crate) fn open(group: WeakEntity, window: &mut Window, cx: &mut App) next.livekit = current.livekit; match group.update(cx, |group, cx| group.edit_metadata(&next, cx)) { - Ok(task) => crate::report(window, cx, task), + Ok(task) => crate::util::report(window, cx, task), Err(error) => log::warn!("nip29: editing the group failed: {error}"), } diff --git a/crates/nip29_ui/src/util.rs b/crates/nip29_ui/src/util.rs new file mode 100644 index 00000000..dfc451a4 --- /dev/null +++ b/crates/nip29_ui/src/util.rs @@ -0,0 +1,91 @@ +use anyhow::Result; +use gpui::{App, Div, Entity, ParentElement, SharedString, Styled, Task, Window}; +use nip29::{Group, Pin, Row}; +use nostr_sdk::prelude::Url; +use theme::ActiveTheme; +use ui::input::{Input, InputState}; +use ui::notification::Notification; +use ui::{WindowExtension, v_flex}; + +const RUN_WINDOW_SECS: u64 = 300; + +pub(crate) fn display_name(group: &Group) -> SharedString { + group + .metadata() + .and_then(|metadata| metadata.name()) + .map_or_else( + || SharedString::from(group.key().id().as_str()), + SharedString::from, + ) +} + +pub(crate) fn attachment_name(url: &Url) -> SharedString { + url.path_segments() + .and_then(|mut segments| segments.next_back()) + .filter(|name| !name.is_empty()) + .map_or_else( + || SharedString::from(url.as_str()), + |name| SharedString::from(name.to_owned()), + ) +} + +pub(crate) fn opens_run(rows: &[Row], index: usize) -> bool { + let (Some(current), Some(previous)) = ( + rows.get(index), + index.checked_sub(1).and_then(|index| rows.get(index)), + ) else { + return true; + }; + + if !matches!((current, previous), (Row::Message(_), Row::Message(_))) { + return true; + } + + current.author() != previous.author() + || current + .created_at() + .as_secs() + .saturating_sub(previous.created_at().as_secs()) + > RUN_WINDOW_SECS +} + +pub(crate) fn field(label: &'static str, input: &Entity, cx: &App) -> Div { + v_flex() + .gap_1() + .text_sm() + .text_color(cx.theme().text_muted) + .child(label) + .child(Input::new(input)) +} + +pub(crate) fn pin_label(group: &Group, pin: &Pin) -> SharedString { + match pin { + Pin::Address(coordinate) => SharedString::from(format!("{coordinate}")), + Pin::Event(id) => { + let snippet = group + .message(*id) + .and_then(|message| message.content.lines().next()) + .map(str::trim) + .filter(|line| !line.is_empty()); + + match snippet { + Some(snippet) => SharedString::from(snippet.chars().take(48).collect::()), + None => SharedString::from(id.to_hex().chars().take(8).collect::()), + } + } + } +} + +/// Await a group write and surface any relay error as a notification. +pub(crate) fn report(window: &mut Window, cx: &mut App, task: Task>) { + window + .spawn(cx, async move |cx| -> Result<()> { + if let Err(error) = task.await { + cx.update(|window, cx| { + window.push_notification(Notification::error(error.to_string()), cx); + })?; + } + Ok(()) + }) + .detach(); +} diff --git a/crates/workspace/src/dialogs/import.rs b/crates/workspace/src/dialogs/import.rs index 6a1cba78..f9262594 100644 --- a/crates/workspace/src/dialogs/import.rs +++ b/crates/workspace/src/dialogs/import.rs @@ -2,7 +2,7 @@ use anyhow::{Error, anyhow}; use gpui::prelude::FluentBuilder; use gpui::{ App, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled, - Subscription, Task, Window, div, px, + Subscription, Task, Window, div, px, relative, }; use instant::Duration; use nostr_connect::prelude::*; @@ -227,12 +227,12 @@ impl Render for ImportIdentity { .text_sm() .child( v_flex() - .gap_2() + .gap_1() .child( v_flex() .gap_1() .text_color(cx.theme().text_muted) - .child("Continue with existing key or bunker connection") + .child("Use secret key or bunker connection") .child(Input::new(&self.key_input)), ) .when(require_password, |this| { @@ -247,16 +247,24 @@ impl Render for ImportIdentity { .when(bunker_warning, |this| { this.child( div() + .p_2() + .rounded(cx.theme().radius) + .bg(cx.theme().warning_background) .text_xs() - .text_color(cx.theme().text_warning) + .text_color(cx.theme().warning_foreground) + .line_height(relative(1.2)) .child(div().child(BUNKER_WARN)), ) }) .when(key_warning, |this| { this.child( div() + .p_2() + .rounded(cx.theme().radius) + .bg(cx.theme().warning_background) .text_xs() - .text_color(cx.theme().text_warning) + .text_color(cx.theme().warning_foreground) + .line_height(relative(1.2)) .child(div().child(KEY_WARN)), ) }), diff --git a/crates/workspace/src/lib.rs b/crates/workspace/src/lib.rs index bf6d7f0e..1971226c 100644 --- a/crates/workspace/src/lib.rs +++ b/crates/workspace/src/lib.rs @@ -23,7 +23,7 @@ use ui::{IconName, Root, Sizable, WindowExtension, h_flex, v_flex}; use crate::dialogs::restore::RestoreEncryption; use crate::dialogs::{join_group, new_chat, new_group, profile, relays, settings}; -use crate::panels::{backup, browse, contact_list, greeter, inbox, requests, search}; +use crate::panels::{backup, browse, contact_list, greeter, inbox}; use crate::sidebar::Sidebar; mod dialogs; @@ -53,9 +53,7 @@ enum Command { ShowBackup, ShowContactList, ShowInbox, - ShowRequests, ShowBrowse, - ShowSearch, NewChat, NewGroup, JoinGroup, @@ -294,22 +292,10 @@ impl Workspace { Command::ShowInbox => { self.add_panel_to_dock(inbox::init(window, cx), DockPlacement::Center, window, cx); } - Command::ShowRequests => { - self.add_panel_to_dock( - requests::init(window, cx), - DockPlacement::Center, - window, - cx, - ); - } Command::ShowBrowse => { let panel = browse::init(self.dock.downgrade(), window, cx); self.add_panel_to_dock(panel, DockPlacement::Center, window, cx); } - Command::ShowSearch => { - let panel = search::init(self.dock.downgrade(), window, cx); - self.add_panel_to_dock(panel, DockPlacement::Center, window, cx); - } Command::NewChat => { new_chat::open(self.dock.downgrade(), window, cx); } diff --git a/crates/workspace/src/panels/mod.rs b/crates/workspace/src/panels/mod.rs index ebbcf76e..680bcb96 100644 --- a/crates/workspace/src/panels/mod.rs +++ b/crates/workspace/src/panels/mod.rs @@ -3,5 +3,3 @@ pub mod browse; pub mod contact_list; pub mod greeter; pub mod inbox; -pub mod requests; -pub mod search; diff --git a/crates/workspace/src/panels/requests.rs b/crates/workspace/src/panels/requests.rs deleted file mode 100644 index cbb67c9d..00000000 --- a/crates/workspace/src/panels/requests.rs +++ /dev/null @@ -1,62 +0,0 @@ -use gpui::{ - AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, - IntoElement, ParentElement, Render, SharedString, Styled, Window, -}; -use theme::ActiveTheme; -use ui::dock::{Panel, PanelEvent}; -use ui::{Icon, IconName, Sizable, h_flex}; - -pub fn init(window: &mut Window, cx: &mut App) -> Entity { - cx.new(|cx| RequestsPanel::new(window, cx)) -} - -pub struct RequestsPanel { - name: SharedString, - focus_handle: FocusHandle, -} - -impl RequestsPanel { - fn new(_window: &mut Window, cx: &mut App) -> Self { - Self { - name: "Requests".into(), - focus_handle: cx.focus_handle(), - } - } -} - -impl Panel for RequestsPanel { - fn panel_id(&self) -> SharedString { - self.name.clone() - } - - fn title(&self, cx: &App) -> AnyElement { - h_flex() - .gap_1p5() - .child( - Icon::new(IconName::Invite) - .small() - .text_color(cx.theme().icon_muted), - ) - .child(self.name.clone()) - .into_any_element() - } -} - -impl EventEmitter for RequestsPanel {} - -impl Focusable for RequestsPanel { - fn focus_handle(&self, _: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl Render for RequestsPanel { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - h_flex() - .size_full() - .justify_center() - .text_sm() - .text_color(cx.theme().text_muted) - .child(self.name.clone()) - } -} diff --git a/crates/workspace/src/panels/search.rs b/crates/workspace/src/panels/search.rs deleted file mode 100644 index 056fbb0e..00000000 --- a/crates/workspace/src/panels/search.rs +++ /dev/null @@ -1,576 +0,0 @@ -use std::collections::HashSet; -use std::ops::Range; - -use anyhow::Error; -use chat::{ChatRegistry, Room, RoomKind}; -use common::DebouncedDelay; -use gpui::prelude::FluentBuilder; -use gpui::{ - AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, - IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Task, WeakEntity, - Window, div, uniform_list, -}; -use instant::Duration; -use nostr_sdk::prelude::*; -use person::PersonRegistry; -use settings::AppSettings; -use smallvec::{SmallVec, smallvec}; -use state::{FIND_DELAY, NostrRegistry}; -use theme::ActiveTheme; -use ui::avatar::Avatar; -use ui::button::{Button, ButtonVariants}; -use ui::dock::{DockArea, DockPlacement, Panel, PanelEvent, PanelHandle}; -use ui::input::{Input, InputEvent, InputState}; -use ui::nav::Nav; -use ui::notification::Notification; -use ui::{Icon, IconName, Selectable, Sizable, StyledExt, WindowExtension, h_flex, v_flex}; - -const INPUT_PLACEHOLDER: &str = "Find or start a conversation"; - -pub fn init(dock: WeakEntity, window: &mut Window, cx: &mut App) -> Entity { - cx.new(|cx| SearchPanel::new(dock, window, cx)) -} - -pub struct SearchPanel { - name: SharedString, - focus_handle: FocusHandle, - /// The dock a started chat opens in - dock: WeakEntity, - - /// Find input state - find_input: Entity, - - /// Debounced delay for find input - find_debouncer: DebouncedDelay, - - /// Whether a search is in progress - finding: bool, - - /// Find results - find_results: Entity>>, - - /// Async find operation - find_task: Option>>, - - /// Selected public keys - selected_pkeys: Entity>, - - /// User's contacts - contact_list: Entity>>, - - /// Async tasks - tasks: SmallVec<[Task>; 1]>, - - /// Event subscriptions - _subscriptions: SmallVec<[Subscription; 1]>, -} - -impl SearchPanel { - fn new(dock: WeakEntity, window: &mut Window, cx: &mut Context) -> Self { - let contact_list = cx.new(|_| None); - let selected_pkeys = cx.new(|_| HashSet::new()); - let find_results = cx.new(|_| None); - let find_input = cx.new(|cx| { - InputState::new(window, cx) - .placeholder(INPUT_PLACEHOLDER) - .clean_on_escape() - }); - - let mut subscriptions = smallvec![]; - - subscriptions.push( - // Subscribe to find input events - cx.subscribe_in(&find_input, window, |this, state, event, window, cx| { - let delay = Duration::from_millis(FIND_DELAY); - - match event { - InputEvent::PressEnter { .. } => { - this.search(window, cx); - } - InputEvent::Change => { - if state.read(cx).value().is_empty() { - // Clear results when input is empty - this.reset(window, cx); - } else { - // Run debounced search - this.find_debouncer - .fire_new(delay, window, cx, |this, window, cx| { - this.debounced_search(window, cx) - }); - } - } - InputEvent::Focus => { - this.get_contact_list(window, cx); - } - _ => {} - }; - }), - ); - - Self { - name: "Search".into(), - focus_handle: cx.focus_handle(), - dock, - find_input, - find_debouncer: DebouncedDelay::new(), - find_results, - find_task: None, - finding: false, - contact_list, - selected_pkeys, - tasks: smallvec![], - _subscriptions: subscriptions, - } - } - - /// Get the contact list. - fn get_contact_list(&mut self, window: &mut Window, cx: &mut Context) { - let nostr = NostrRegistry::global(cx); - let client = nostr.read(cx).client(); - - let Some(public_key) = nostr.read(cx).current_user() else { - return; - }; - - let task: Task, Error>> = cx.background_spawn(async move { - let filter = Filter::new() - .author(public_key) - .kind(Kind::ContactList) - .limit(1); - - let contacts: HashSet = client - .database() - .query(filter) - .await? - .into_iter() - .next() - .map(|event| event.tags.public_keys().collect()) - .unwrap_or_default(); - - Ok(contacts) - }); - - self.tasks.push(cx.spawn_in(window, async move |this, cx| { - match task.await { - Ok(contacts) => { - this.update(cx, |this, cx| { - this.set_contact_list(contacts, cx); - })?; - } - Err(e) => { - cx.update(|window, cx| { - window.push_notification( - Notification::error(e.to_string()).autohide(false), - cx, - ); - })?; - } - }; - - Ok(()) - })); - } - - /// Set the contact list with new contacts. - fn set_contact_list(&mut self, contacts: I, cx: &mut Context) - where - I: IntoIterator, - { - self.contact_list.update(cx, |this, cx| { - *this = Some(contacts.into_iter().collect()); - cx.notify(); - }); - } - - /// Trigger the debounced search - fn debounced_search(&self, window: &mut Window, cx: &mut Context) -> Task<()> { - cx.spawn_in(window, async move |this, cx| { - this.update_in(cx, |this, window, cx| { - this.search(window, cx); - }) - .ok(); - }) - } - - /// Search - fn search(&mut self, window: &mut Window, cx: &mut Context) { - // Get query - let query = self.find_input.read(cx).value(); - - // Return if the query is empty - if query.is_empty() { - return; - } - - // Block the input until the search completes - self.set_finding(true, window, cx); - - // Create the search task - let nostr = NostrRegistry::global(cx); - let find_users = nostr.read(cx).search(&query, cx); - - // Run task in the main thread - self.find_task = Some(cx.spawn_in(window, async move |this, cx| { - let rooms = find_users.await?; - - // Update the UI with the search results - this.update_in(cx, |this, window, cx| { - this.set_results(rooms, cx); - this.set_finding(false, window, cx); - })?; - - Ok(()) - })); - } - - /// Set the results of the search - fn set_results(&mut self, results: Vec, cx: &mut Context) { - self.find_results.update(cx, |this, cx| { - *this = Some(results); - cx.notify(); - }); - } - - /// Set the finding status - fn set_finding(&mut self, status: bool, window: &mut Window, cx: &mut Context) { - // Disable the input to prevent duplicate requests - self.find_input.update(cx, |this, cx| { - this.set_loading(status, window, cx); - }); - // Set the search status - self.finding = status; - cx.notify(); - } - - fn reset(&mut self, window: &mut Window, cx: &mut Context) { - // Clear all search results - self.find_results.update(cx, |this, cx| { - *this = None; - cx.notify(); - }); - - // Clear all selected public keys - self.selected_pkeys.update(cx, |this, cx| { - this.clear(); - cx.notify(); - }); - - // Reset the search status - self.set_finding(false, window, cx); - - // Cancel the current search task - self.find_task = None; - cx.notify(); - } - - /// Select a public key in the search panel. - fn select(&mut self, public_key: &PublicKey, cx: &mut Context) { - self.selected_pkeys.update(cx, |this, cx| { - if this.contains(public_key) { - this.remove(public_key); - } else { - this.insert(public_key.to_owned()); - } - cx.notify(); - }); - } - - /// Check if a public key is selected in the search panel. - fn is_selected(&self, public_key: &PublicKey, cx: &App) -> bool { - self.selected_pkeys.read(cx).contains(public_key) - } - - /// Get all selected public keys in the search panel. - fn get_selected(&self, cx: &Context) -> HashSet { - self.selected_pkeys.read(cx).clone() - } - - /// Create a new room - fn create_room(&mut self, window: &mut Window, cx: &mut Context) { - let chat = ChatRegistry::global(cx); - let async_chat = chat.downgrade(); - let dock = self.dock.clone(); - - let nostr = NostrRegistry::global(cx); - let Some(public_key) = nostr.read(cx).current_user() else { - return; - }; - - // Get all selected public keys - let receivers = self.get_selected(cx); - - self.tasks.push(cx.spawn_in(window, async move |this, cx| { - // Create a new room and register it - let room = async_chat.update_in(cx, |chat, _window, cx| { - let room = cx.new(|_| { - Room::new(public_key, receivers) - .organize(&public_key) - .kind(RoomKind::Ongoing) - }); - chat.track_room(&room, cx); - room - })?; - - // Open it in the dock - cx.update(|window, cx| { - ui::dock::add_panel_to( - &dock, - PanelHandle::new(chat_ui::init(room.downgrade(), window, cx)), - DockPlacement::Center, - window, - cx, - ); - })?; - - // Reset the find panel - this.update_in(cx, |this, window, cx| { - this.reset(window, cx); - })?; - - Ok(()) - })); - } - - /// Render the search results - fn render_results( - &self, - range: Range, - cx: &Context, - ) -> Vec> { - let hide_avatar = AppSettings::get_hide_avatar(cx); - let persons = PersonRegistry::global(cx); - - // Get the results - let Some(results) = self.find_results.read(cx) else { - return vec![]; - }; - - // Map the results to a list of elements - results - .get(range.clone()) - .into_iter() - .flatten() - .enumerate() - .map(|(ix, public_key)| { - let selected = self.is_selected(public_key, cx); - let profile = persons.read(cx).get(public_key, cx); - let pkey_clone = public_key.to_owned(); - let handler = cx.listener(move |this, _ev, _window, cx| { - this.select(&pkey_clone, cx); - }); - - Nav::new(range.start + ix) - .label(profile.name()) - .text_sm() - .font_medium() - .when(!hide_avatar, |this| { - this.prefix( - Avatar::from_source(profile.avatar()) - .seed(profile.avatar_seed()) - .small() - .flex_shrink_0(), - ) - }) - .on_click(handler) - .selected(selected) - .into_any_element() - }) - .collect() - } - - /// Render the contact list - fn render_contacts( - &self, - range: Range, - cx: &Context, - ) -> Vec> { - let hide_avatar = AppSettings::get_hide_avatar(cx); - let persons = PersonRegistry::global(cx); - - // Get the contact list - let Some(contacts) = self.contact_list.read(cx) else { - return vec![]; - }; - - // Map the contact list to a list of elements - contacts - .get(range.clone()) - .into_iter() - .flatten() - .enumerate() - .map(|(ix, public_key)| { - let selected = self.is_selected(public_key, cx); - let profile = persons.read(cx).get(public_key, cx); - let pkey_clone = public_key.to_owned(); - let handler = cx.listener(move |this, _ev, _window, cx| { - this.select(&pkey_clone, cx); - }); - - Nav::new(range.start + ix) - .label(profile.name().trim()) - .text_sm() - .font_medium() - .when(!hide_avatar, |this| { - this.prefix( - Avatar::from_source(profile.avatar()) - .seed(profile.avatar_seed()) - .small() - .flex_shrink_0(), - ) - }) - .on_click(handler) - .selected(selected) - .into_any_element() - }) - .collect() - } -} - -impl Panel for SearchPanel { - fn panel_id(&self) -> SharedString { - self.name.clone() - } - - fn title(&self, cx: &App) -> AnyElement { - h_flex() - .gap_1p5() - .child( - Icon::new(IconName::Search) - .small() - .text_color(cx.theme().icon_muted), - ) - .child(self.name.clone()) - .into_any_element() - } -} - -impl EventEmitter for SearchPanel {} - -impl Focusable for SearchPanel { - fn focus_handle(&self, _: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl Render for SearchPanel { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let nostr = NostrRegistry::global(cx); - let chat = ChatRegistry::global(cx); - let logged_in = nostr.read(cx).current_user().is_some(); - let loading = chat.read(cx).loading() && logged_in; - - // Set button label based on total selected users - let button_label = if self.selected_pkeys.read(cx).len() > 1 { - "Create Group DM" - } else { - "Create DM" - }; - - v_flex() - .size_full() - .gap_3() - .p_2() - .child( - h_flex().child( - Input::new(&self.find_input) - .small() - .text_xs() - .disabled(loading) - .when( - !self.find_input.read(cx).presentation().is_loading(), - |this| { - this.suffix( - Button::new("find-icon") - .icon(IconName::Search) - .tooltip("Press Enter to search") - .transparent() - .small(), - ) - }, - ), - ), - ) - .child( - v_flex() - .flex_1() - .gap_3() - .when_some(self.find_results.read(cx).as_ref(), |this, results| { - this.child( - v_flex() - .gap_1() - .flex_1() - .border_b_1() - .border_color(cx.theme().border_variant) - .child( - h_flex() - .gap_0p5() - .text_xs() - .font_semibold() - .text_color(cx.theme().text_muted) - .child(Icon::new(IconName::ChevronDown)) - .child("Results"), - ) - .child( - uniform_list( - "rooms", - results.len(), - cx.processor(move |this, range, _window, cx| { - this.render_results(range, cx) - }), - ) - .flex_1() - .h_full(), - ), - ) - }) - .when_some(self.contact_list.read(cx).as_ref(), |this, contacts| { - this.child( - v_flex() - .gap_1() - .flex_1() - .child( - h_flex() - .gap_0p5() - .text_xs() - .font_semibold() - .text_color(cx.theme().text_muted) - .child(Icon::new(IconName::ChevronDown).small()) - .child("Contacts"), - ) - .child( - uniform_list( - "contacts", - contacts.len(), - cx.processor(|this, range, _window, cx| { - this.render_contacts(range, cx) - }), - ) - .flex_1() - .h_full(), - ), - ) - }), - ) - .when(!self.selected_pkeys.read(cx).is_empty(), |this| { - this.child( - div() - .absolute() - .bottom_2() - .left_0() - .h_9() - .w_full() - .px_4() - .child( - Button::new("create") - .label(button_label) - .primary() - .rounded() - .shadow_md() - .on_click(cx.listener(move |this, _ev, window, cx| { - this.create_room(window, cx); - })), - ), - ) - }) - } -} diff --git a/crates/workspace/src/sidebar/group_view.rs b/crates/workspace/src/sidebar/group_view.rs index bde29a49..19d5b38e 100644 --- a/crates/workspace/src/sidebar/group_view.rs +++ b/crates/workspace/src/sidebar/group_view.rs @@ -256,6 +256,7 @@ impl Sidebar { let persons = PersonRegistry::global(cx); let person = persons.read(cx).get(public_key, cx); let public_key = *public_key; + let member_is_admin = !roles.is_empty(); let group = group.clone(); @@ -297,6 +298,22 @@ impl Sidebar { return menu; } + let menu = menu.separator().item(if member_is_admin { + PopupMenuItem::new("Remove admin").on_click({ + let group = group.clone(); + move |_event, window, cx| { + nip29_ui::remove_admin(group.clone(), public_key, window, cx); + } + }) + } else { + PopupMenuItem::new("Make admin").on_click({ + let group = group.clone(); + move |_event, window, cx| { + nip29_ui::make_admin(group.clone(), public_key, window, cx); + } + }) + }); + menu.separator() .item(PopupMenuItem::new("Remove").on_click({ let group = group.clone();