From 6510e45e4e8f3809179b7b6516f1ef5155c78633 Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Wed, 23 Sep 2026 08:21:37 +0700 Subject: [PATCH 1/9] update deps --- Cargo.lock | 299 +++++++++++++++++++++++++------------- crates/ui/src/dock/mod.rs | 16 +- 2 files changed, 195 insertions(+), 120 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4d3732cb..583adc7d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -987,9 +987,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.4.6" +version = "1.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3eb0f42d6c360dc3f8a821f6bf2fdea7f72bfd36b3076eb0e6d1e9e0752fff4" +checksum = "54413ede23c2daf518f35156dfde027feb2374004d63bd497f983c8db9c0e313" dependencies = [ "find-msvc-tools", "jobserver", @@ -1339,7 +1339,7 @@ dependencies = [ "hmac 0.12.1", "nostr", "nostr-sdk", - "rand 0.10.2", + "rand 0.10.3", "serde", "serde_json", "sha2 0.10.9", @@ -2190,9 +2190,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" +checksum = "ef25905e51abafe4dcea6c15fec58c57b601cdbd0ee53d22ea1d3016c587d39b" [[package]] name = "fixedbitset" @@ -2711,14 +2711,16 @@ dependencies = [ [[package]] name = "gpui-base" -version = "0.6.1" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d45dcaaeac889bf1e7757db1beb26c9043c8ea3156651facc11c6be56bb6722" +checksum = "c9e904ee0d351f4e85b967e26ae986d3aa195a8d9d35fcd18184a2c40989694e" dependencies = [ "aho-corasick", "anyhow", + "ashpd", "async-channel", "chrono", + "data-url", "futures", "gpui-pre", "gpui-pre-macros", @@ -2742,13 +2744,14 @@ dependencies = [ "tracing", "unicode-segmentation", "web-time", + "windows 0.58.0", ] [[package]] name = "gpui-pre" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a9ca98fdcad06276de623c2f48ebd710f1e7724eebef2f2673c583e85dc8335" +checksum = "a0437c0b83e636a92bd1a39fa1d05fb632ae671289537497b35871ffbe231b84" dependencies = [ "accesskit", "anyhow", @@ -2774,7 +2777,6 @@ dependencies = [ "gpui-pre-sum-tree", "gpui-pre-util", "gpui-pre-util-macros", - "gpui-pre-ztracing", "heapless 0.9.3", "image", "inventory", @@ -2782,6 +2784,8 @@ dependencies = [ "log", "lyon", "num_cpus", + "objc2-core-foundation", + "objc2-core-video", "parking", "parking_lot", "pin-project", @@ -2802,7 +2806,6 @@ dependencies = [ "strum", "taffy", "thiserror 2.0.20", - "tracing", "ttf-parser", "url", "usvg", @@ -2816,9 +2819,9 @@ dependencies = [ [[package]] name = "gpui-pre-apple" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fc4da82c5c2dbb25443655cfbc4305a2e2d1380bc0b815e80664fdfbf4cfdb" +checksum = "3af534f0746eb9e014114bcc0ee00dc250c4b8d75df2e437180c403aa65649c1" dependencies = [ "anyhow", "block", @@ -2830,7 +2833,6 @@ dependencies = [ "etagere", "foreign-types", "gpui-pre", - "gpui-pre-collections", "image", "log", "metal", @@ -2841,9 +2843,9 @@ dependencies = [ [[package]] name = "gpui-pre-collections" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "626a09f683ff6f3b2f708341bdcea9d479af5b14a6a2fccec9cff935ec0751b7" +checksum = "15e7a65204039187d1f4b3d4c2e0233eeac54d4acb0b13f9dc1a3b5adad26c1f" dependencies = [ "gpui-pre-util", "indexmap", @@ -2852,9 +2854,9 @@ dependencies = [ [[package]] name = "gpui-pre-derive-refineable" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56d06116a2b857588f6ea7bfb447b3554ba985e780b5995ae47780252da152f3" +checksum = "79dfb4a05f3cd9893d7768dbf5dba1bbbe99cab7394e89b360ccb3b2bcb6b34e" dependencies = [ "proc-macro2", "quote", @@ -2863,9 +2865,9 @@ dependencies = [ [[package]] name = "gpui-pre-http-client" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0d2071c582c1a0f71b8c7c00607a3b9cd18f81fb3510d41845db3447b2a11c9" +checksum = "cfd5a7542c77887a5aff997d78c95884119c8463409569b63fcf060956da1682" dependencies = [ "anyhow", "async-compression", @@ -2884,9 +2886,9 @@ dependencies = [ [[package]] name = "gpui-pre-http-client-tls" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b6f3936802580259d5af9bcb2ab37fb0d7002caeed449eaabb007c62aea7341" +checksum = "72aed510adb5e7723f8f57b9751007e3451d46a84edb59b92227178cf59cc19c" dependencies = [ "log", "rustls", @@ -2896,9 +2898,9 @@ dependencies = [ [[package]] name = "gpui-pre-linux" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6b427589681ec1fed27951fd82edc70a7e288bb2487ede26dbbd07c2ce23d4f" +checksum = "aab655cda1d6dcd549ac869e8678babc84cf573ab6794a1fe1a39c6c70c60b48" dependencies = [ "accesskit", "accesskit_unix", @@ -2943,15 +2945,14 @@ dependencies = [ [[package]] name = "gpui-pre-macos" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806eac0cb5c0eebc0b032c6450c11d283264f9ac05754dda83fc80d0b86b4ee8" +checksum = "951d17e41a72067ad2d35c60e08a62e8ad7fa9400b41c802e26cfb459f6c05da" dependencies = [ "accesskit", "accesskit_macos", "anyhow", "async-task", - "block", "block2 0.6.2", "cocoa 0.26.1", "core-foundation 0.10.1", @@ -2965,17 +2966,17 @@ dependencies = [ "gpui-pre", "gpui-pre-apple", "gpui-pre-collections", - "gpui-pre-media", "gpui-pre-util", "image", "itertools 0.14.0", "libc", "log", "mach2", - "metal", "objc", "objc2 0.6.4", "objc2-app-kit 0.3.2", + "objc2-core-graphics", + "objc2-core-media", "objc2-foundation 0.3.2", "objc2-screen-capture-kit", "objc2-user-notifications", @@ -2991,9 +2992,9 @@ dependencies = [ [[package]] name = "gpui-pre-macros" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06139051bf1db51949bfbb4ee338208bd64b4c35340d8c1a6a8200744e28fe1" +checksum = "6324092b40ea8ec7c327a4a28427e366a489b4d5d23b5d1bd0243fc2f4ca7a9e" dependencies = [ "heck 0.5.0", "proc-macro-crate", @@ -3002,26 +3003,11 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "gpui-pre-media" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "500acb13f818ff234021c1b9eae8592b0cbe017a64e26908c736b7421b1c6213" -dependencies = [ - "anyhow", - "bindgen", - "core-foundation 0.10.1", - "core-video", - "foreign-types", - "metal", - "objc", -] - [[package]] name = "gpui-pre-perf" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0973bbb02bf46940ac0861d9dce0fc03a3109634172b69108d1bc13fa29d76c" +checksum = "b94bff26ccd8105f6206144f8dab16a2dc02e7a68f39dada02cdf492f0afd940" dependencies = [ "gpui-pre-collections", "serde", @@ -3030,9 +3016,9 @@ dependencies = [ [[package]] name = "gpui-pre-platform" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b613512e29f1e7b825bfa7e50a447388ff1347044350a475e4c98a892a985c8d" +checksum = "8423d1e0a693d2f9f16c0326bb01b5e81ec0531854d596a18476934ac6880478" dependencies = [ "console_error_panic_hook", "gpui-pre", @@ -3044,9 +3030,9 @@ dependencies = [ [[package]] name = "gpui-pre-refineable" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "428f23703ead5601618ffea73c137897b98a94ce8d30e257f22be947732dfbf9" +checksum = "93b4b0a8d296a3ee5beb1136bc48f8167a6028eb1e243b56851dcd8c9fc6d7a1" dependencies = [ "gpui-pre-derive-refineable", ] @@ -3103,9 +3089,9 @@ dependencies = [ [[package]] name = "gpui-pre-reqwest-client" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05d4e3d2eb2ab777597879e46047247447b9d93cef568e63d545c9deb1b3e57" +checksum = "8f32ccea5d1a7c8d2868d7b346c2c1f76aa3f86ec9e0575c4e7340af3afd9e9d" dependencies = [ "anyhow", "bytes", @@ -3121,9 +3107,9 @@ dependencies = [ [[package]] name = "gpui-pre-scheduler" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dd1b7ff2e7bf43302ae982cd786a22c16c52e16682e67b66cfbe2cb2bc3a72d" +checksum = "ff7b10b3d2fc3fda24847a04436eca9531612c8414874a37ddc005f20e521e12" dependencies = [ "async-task", "backtrace", @@ -3138,9 +3124,9 @@ dependencies = [ [[package]] name = "gpui-pre-shared-string" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "424a4dd6a570473d57acf1df2bfb9f3028d4b568abcaeb9252e0f13047b50c03" +checksum = "fe17f81bd98a0253140ea79f7f76442d7499bd8d17db302861af8e46c11d38df" dependencies = [ "schemars", "serde", @@ -3149,9 +3135,9 @@ dependencies = [ [[package]] name = "gpui-pre-sum-tree" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4855718e82630d29198b515d2b027361ef63dd19b6021cefe945789dcff82372" +checksum = "0a6d35e998a51d24248dad731de1f09d386cbbf46a04edde3b67f469b2ce3f16" dependencies = [ "gpui-pre-ztracing", "heapless 0.9.3", @@ -3162,9 +3148,9 @@ dependencies = [ [[package]] name = "gpui-pre-util" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e973148165346c0f9b3f5072b321d7c2f33807c93daea67fe298c005c0e0c2a8" +checksum = "6a026a30c578930d20974fd8c3d77de36223ac7ba193bd4259e7238929bc8806" dependencies = [ "anyhow", "log", @@ -3173,9 +3159,9 @@ dependencies = [ [[package]] name = "gpui-pre-util-macros" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f71f6212d3772a70988c7d64627c7284dec73c17992ac4f9f0cdfdd46cbb677" +checksum = "5cbf79627a6afc626d4e887dad9d2d92bb069413e8cc692ba3e90bff219eb7a9" dependencies = [ "gpui-pre-perf", "quote", @@ -3184,9 +3170,9 @@ dependencies = [ [[package]] name = "gpui-pre-web" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f0daada903f106be946d87e25f5bc85ad6cf60d6d82cb8f8aed945417a9c1a3" +checksum = "34be433b9fcb370b1fe9a1c7fe911f0eb6058791ef8167dcf6c5a5deb624975c" dependencies = [ "anyhow", "console_error_panic_hook", @@ -3213,9 +3199,9 @@ dependencies = [ [[package]] name = "gpui-pre-wgpu" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "640b666a16ddf9504e2eb3e7a997acb2b2adfaa7859925bdd36fc3acfd8f30a7" +checksum = "05515b32799b1767d69300921c1b562cd53104965211a0e8905acdc7aa18e693" dependencies = [ "anyhow", "bytemuck", @@ -3240,9 +3226,9 @@ dependencies = [ [[package]] name = "gpui-pre-windows" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c12c7d84ec422feb2f064117d572447998381e5c219fcad69fe19f10d0b7aaea" +checksum = "27662e65bfcf4445d2cccfad91f1570ec07f4b1b5de98400a9f207fd19c464cf" dependencies = [ "accesskit", "accesskit_windows", @@ -3269,9 +3255,9 @@ dependencies = [ [[package]] name = "gpui-pre-zlog" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98352858fac3d4d05b46d6014ad04bad313652a691d4e21dc55fa96fb4acb7cf" +checksum = "f3a5af0b734a391a84872eb770b3ee9830091ec2487b79d06371fe14eefb2b5d" dependencies = [ "anyhow", "chrono", @@ -3281,9 +3267,9 @@ dependencies = [ [[package]] name = "gpui-pre-ztracing" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d63a3c7276875b4d7e6d3961e92a138e9233e86a591bf318f2e54df4ad58102" +checksum = "a24cb19f78c5bc3aab8ddf1e8e0bd6b402fbcb9c7f112f64ddbedef8f8768a59" dependencies = [ "gpui-pre-zlog", "gpui-pre-ztracing-macro", @@ -3293,9 +3279,9 @@ dependencies = [ [[package]] name = "gpui-pre-ztracing-macro" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84e2a5ff79002223edfcfa76c6f9f79f4f60f6ec545ccc27832f12bd46348b39" +checksum = "a1bb25850ad31b88bbdef454afb93ec4cdb81f5e200ba5b3f1a0368111780d2c" [[package]] name = "gpui-updater-core" @@ -3621,9 +3607,9 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.9" +version = "0.27.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +checksum = "dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53" dependencies = [ "http", "hyper", @@ -4131,9 +4117,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6480ccc157a1389bb2e4891b24751b0f798ba640d22386f23143fbcc89da195a" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" dependencies = [ "libc", ] @@ -4617,8 +4603,8 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" [[package]] name = "nostr" -version = "0.45.4" -source = "git+https://github.com/rust-nostr/nostr#b230cecf9dbb38e0228e6fff4544ed9d261326fc" +version = "0.45.5" +source = "git+https://github.com/rust-nostr/nostr#01f1950c671b35f3d1186ca907373dd86ee3b336" dependencies = [ "aes", "base64 0.22.1", @@ -4630,7 +4616,7 @@ dependencies = [ "chacha20poly1305", "faster-hex", "opaquerr 0.2.0", - "rand 0.10.2", + "rand 0.10.3", "scrypt", "secp256k1", "serde", @@ -4643,8 +4629,8 @@ dependencies = [ [[package]] name = "nostr-blossom" -version = "0.45.0" -source = "git+https://github.com/rust-nostr/nostr#b230cecf9dbb38e0228e6fff4544ed9d261326fc" +version = "0.45.1" +source = "git+https://github.com/rust-nostr/nostr#01f1950c671b35f3d1186ca907373dd86ee3b336" dependencies = [ "base64 0.22.1", "bitcoin_hashes 1.2.0", @@ -4656,8 +4642,8 @@ dependencies = [ [[package]] name = "nostr-connect" -version = "0.45.1" -source = "git+https://github.com/rust-nostr/nostr#b230cecf9dbb38e0228e6fff4544ed9d261326fc" +version = "0.45.2" +source = "git+https://github.com/rust-nostr/nostr#01f1950c671b35f3d1186ca907373dd86ee3b336" dependencies = [ "async-utility", "futures-core", @@ -4670,8 +4656,8 @@ dependencies = [ [[package]] name = "nostr-database" -version = "0.45.1" -source = "git+https://github.com/rust-nostr/nostr#b230cecf9dbb38e0228e6fff4544ed9d261326fc" +version = "0.45.2" +source = "git+https://github.com/rust-nostr/nostr#01f1950c671b35f3d1186ca907373dd86ee3b336" dependencies = [ "nostr", "opaquerr 0.2.0", @@ -4679,8 +4665,8 @@ dependencies = [ [[package]] name = "nostr-gossip" -version = "0.45.0" -source = "git+https://github.com/rust-nostr/nostr#b230cecf9dbb38e0228e6fff4544ed9d261326fc" +version = "0.45.1" +source = "git+https://github.com/rust-nostr/nostr#01f1950c671b35f3d1186ca907373dd86ee3b336" dependencies = [ "nostr", "opaquerr 0.2.0", @@ -4688,8 +4674,8 @@ dependencies = [ [[package]] name = "nostr-gossip-memory" -version = "0.45.0" -source = "git+https://github.com/rust-nostr/nostr#b230cecf9dbb38e0228e6fff4544ed9d261326fc" +version = "0.45.1" +source = "git+https://github.com/rust-nostr/nostr#01f1950c671b35f3d1186ca907373dd86ee3b336" dependencies = [ "indexmap", "lru", @@ -4700,8 +4686,8 @@ dependencies = [ [[package]] name = "nostr-lmdb" -version = "0.45.2" -source = "git+https://github.com/rust-nostr/nostr#b230cecf9dbb38e0228e6fff4544ed9d261326fc" +version = "0.45.3" +source = "git+https://github.com/rust-nostr/nostr#01f1950c671b35f3d1186ca907373dd86ee3b336" dependencies = [ "async-utility", "flatbuffers", @@ -4715,8 +4701,8 @@ dependencies = [ [[package]] name = "nostr-memory" -version = "0.45.1" -source = "git+https://github.com/rust-nostr/nostr#b230cecf9dbb38e0228e6fff4544ed9d261326fc" +version = "0.45.2" +source = "git+https://github.com/rust-nostr/nostr#01f1950c671b35f3d1186ca907373dd86ee3b336" dependencies = [ "btreecap", "nostr", @@ -4726,8 +4712,8 @@ dependencies = [ [[package]] name = "nostr-sdk" -version = "0.45.2" -source = "git+https://github.com/rust-nostr/nostr#b230cecf9dbb38e0228e6fff4544ed9d261326fc" +version = "0.45.4" +source = "git+https://github.com/rust-nostr/nostr#01f1950c671b35f3d1186ca907373dd86ee3b336" dependencies = [ "async-utility", "async-wsocket", @@ -4739,7 +4725,7 @@ dependencies = [ "nostr-database", "nostr-gossip", "opaquerr 0.2.0", - "rand 0.10.2", + "rand 0.10.3", "tokio", "tokio-stream", "tracing", @@ -4990,6 +4976,28 @@ dependencies = [ "objc2-foundation 0.3.2", ] +[[package]] +name = "objc2-core-audio" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2" +dependencies = [ + "dispatch2", + "objc2 0.6.4", + "objc2-core-audio-types", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-core-audio-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.4", +] + [[package]] name = "objc2-core-data" version = "0.2.2" @@ -5069,6 +5077,21 @@ dependencies = [ "objc2-foundation 0.3.2", ] +[[package]] +name = "objc2-core-media" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05ec576860167a15dd9fce7fbee7512beb4e31f532159d3482d1f9c6caedf31d" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "objc2 0.6.4", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-core-video", +] + [[package]] name = "objc2-core-text" version = "0.3.2" @@ -5193,8 +5216,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74b7c5390f477482f001bc354d6571a70db7e4f8d5288e860c45521fbce11394" dependencies = [ "block2 0.6.2", + "dispatch2", "objc2 0.6.4", "objc2-core-graphics", + "objc2-core-media", "objc2-foundation 0.3.2", ] @@ -5902,7 +5927,7 @@ dependencies = [ "bytes", "getrandom 0.4.3", "lru-slab", - "rand 0.10.2", + "rand 0.10.3", "rand_pcg", "ring", "rustc-hash 2.1.3", @@ -5973,9 +5998,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.10.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" dependencies = [ "chacha20 0.10.2", "getrandom 0.4.3", @@ -8004,9 +8029,9 @@ checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" [[package]] name = "unicode-id" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ba288e709927c043cbe476718d37be306be53fb1fafecd0dbe36d072be2580" +checksum = "65dd75ab2d9d379dbe86d4059e91d8b5a6e7054865eaba1ddbfc7a817ff96392" [[package]] name = "unicode-ident" @@ -8790,6 +8815,16 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core 0.58.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows" version = "0.61.3" @@ -8858,6 +8893,19 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement 0.58.0", + "windows-interface 0.58.0", + "windows-result 0.2.0", + "windows-strings 0.1.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows-core" version = "0.61.2" @@ -8917,6 +8965,17 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "windows-implement" version = "0.60.2" @@ -8939,6 +8998,17 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "windows-interface" version = "0.59.3" @@ -9013,6 +9083,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-result" version = "0.3.4" @@ -9031,6 +9110,16 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result 0.2.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows-strings" version = "0.3.1" diff --git a/crates/ui/src/dock/mod.rs b/crates/ui/src/dock/mod.rs index 575f0f84..a30bcdca 100644 --- a/crates/ui/src/dock/mod.rs +++ b/crates/ui/src/dock/mod.rs @@ -14,7 +14,7 @@ use gpui::{ pub use gpui_base::dock::{DockArea, DockLayout, DockPlacement}; use gpui_base::dock::{ DockAreaRenderer, DockContext, DragPanel, DropIndicator, InsertTarget, NodeId, PaneNode, - PaneRef, PanelId, TabGroupContext, TabGroupRenderer, TileContext, TilesRenderer, + PaneRef, PanelId, TabGroupContext, TabGroupRenderer, }; use gpui_base::{Placement, ResizeHandleContext, Side}; use theme::{ActiveTheme, TABBAR_HEIGHT}; @@ -167,7 +167,6 @@ fn left_top_group(node: &PaneNode) -> Option { match node.kind() { PaneRef::Tabs { .. } => Some(node.id()), PaneRef::Split { children, .. } => children.first().and_then(left_top_group), - PaneRef::Tiles { .. } => None, } } @@ -182,7 +181,6 @@ fn right_top_group(node: &PaneNode) -> Option { }; child.and_then(right_top_group) } - PaneRef::Tiles { .. } => None, } } @@ -263,18 +261,6 @@ impl DockAreaRenderer for DockSkin { fn tab_group_renderer(&self) -> Rc { Rc::new(TabGroupSkin::new(self.shared.clone())) } - - fn tiles_renderer(&self) -> Rc { - Rc::new(NoTiles) - } -} - -struct NoTiles; - -impl TilesRenderer for NoTiles { - fn render_drag_bar(&self, _: &TileContext, _: &mut Window, _: &mut App) -> AnyElement { - Empty.into_any_element() - } } /// The payload a dock's resize handle drags; the handle is the affordance. -- 2.54.0 From 7ba289440b47489c4b0e9755eff318269fe18bab Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Wed, 23 Sep 2026 08:53:20 +0700 Subject: [PATCH 2/9] refactor panel open flow --- crates/chat/src/lib.rs | 35 ++------ crates/community/src/community.rs | 2 - crates/community/src/lib.rs | 23 +---- crates/community_ui/src/lib.rs | 20 ++--- crates/ui/src/dock/mod.rs | 13 +++ crates/workspace/src/dialogs/new_chat.rs | 29 +++++-- crates/workspace/src/lib.rs | 103 ++++------------------- crates/workspace/src/panels/search.rs | 34 ++++++-- crates/workspace/src/sidebar/mod.rs | 54 +++++++----- 9 files changed, 122 insertions(+), 191 deletions(-) diff --git a/crates/chat/src/lib.rs b/crates/chat/src/lib.rs index d9b22462..1da7beaf 100644 --- a/crates/chat/src/lib.rs +++ b/crates/chat/src/lib.rs @@ -9,7 +9,7 @@ use fuzzy_matcher::FuzzyMatcher; use fuzzy_matcher::skim::SkimMatcherV2; use gpui::{ App, AppContext, Context, Entity, EventEmitter, Global, SharedString, Subscription, Task, - WeakEntity, Window, + WeakEntity, }; use instant::Duration; use nostr_sdk::prelude::*; @@ -37,15 +37,8 @@ impl Global for GlobalChatRegistry {} /// Chat event. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub enum ChatEvent { - /// An event to open a room by its ID - OpenRoom(u64), - /// An event to close a room by its ID - 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(String), } @@ -543,34 +536,16 @@ impl ChatRegistry { cx.notify(); } - /// Emit an open room event. - /// - /// If the room is new, add it to the registry. - pub fn emit_room(&mut self, room: &Entity, window: &mut Window, cx: &mut Context) { - // Get the room's ID. + /// Track a room so it is listed and can be looked up by id. + pub fn track_room(&mut self, room: &Entity, cx: &mut Context) { let id = room.read(cx).id; - // If the room is new, add it to the registry and index. if let hash_map::Entry::Vacant(e) = self.room_index.entry(id) { let entity = room.to_owned(); e.insert(entity.clone()); + self.rooms.insert(0, entity); - } - - // Emit the open room event deferred to avoid re-entrant reads - cx.defer_in(window, move |_this, _window, cx| { - cx.emit(ChatEvent::OpenRoom(id)); - }); - } - - /// Close a room. - pub fn close_room(&mut self, id: u64, window: &mut Window, cx: &mut Context) { - if self.room_index.contains_key(&id) { - self.room_index.remove(&id); - self.rooms.retain(|r| r.read(cx).id != id); - cx.defer_in(window, move |_this, _window, cx| { - cx.emit(ChatEvent::CloseRoom(id)); - }); + cx.notify(); } } diff --git a/crates/community/src/community.rs b/crates/community/src/community.rs index 51656464..fd183829 100644 --- a/crates/community/src/community.rs +++ b/crates/community/src/community.rs @@ -107,8 +107,6 @@ impl SubscriptionKey { #[derive(Debug, Clone)] pub enum CommunityEvent { Updated(CommunityId), - Open(CommunityId), - Close(CommunityId), Channel(CommunityId, ChannelId), /// History exists here that no held key can open. Unreadable(CommunityId), diff --git a/crates/community/src/lib.rs b/crates/community/src/lib.rs index 5b0dd7dd..71ca90fe 100644 --- a/crates/community/src/lib.rs +++ b/crates/community/src/lib.rs @@ -7,7 +7,7 @@ pub use concord::cord03::{ChatMessage, ReplyRef}; use concord::state::CommunityState; pub use concord::{ChannelId, CommunityId, Epoch}; use futures::future::{Either, select}; -use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, Subscription, Task, Window}; +use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, Subscription, Task}; use nostr_sdk::prelude::*; use smallvec::{SmallVec, smallvec}; use state::NostrRegistry; @@ -225,27 +225,6 @@ impl CommunityRegistry { self.index.get(id).cloned() } - /// Ask the workspace to open a community's panel. - pub fn emit_community( - &mut self, - community: &Entity, - window: &mut Window, - cx: &mut Context, - ) { - let id = community.read(cx).id(); - - cx.defer_in(window, move |_this, _window, cx| { - cx.emit(CommunityEvent::Open(id)); - }); - } - - /// Ask the workspace to close a community's panel. - pub fn emit_close(&mut self, id: CommunityId, window: &mut Window, cx: &mut Context) { - cx.defer_in(window, move |_this, _window, cx| { - cx.emit(CommunityEvent::Close(id)); - }); - } - /// Create a community owned by the current account and begin tracking it. pub fn create(&mut self, metadata: CommunityMetadata, cx: &mut Context) { let nostr = NostrRegistry::global(cx); diff --git a/crates/community_ui/src/lib.rs b/crates/community_ui/src/lib.rs index 524d532b..73177e7b 100644 --- a/crates/community_ui/src/lib.rs +++ b/crates/community_ui/src/lib.rs @@ -145,21 +145,19 @@ impl CommunityPanel { window, |_this, _community, event, window, cx| { match event { - CommunityEvent::Updated(_) - | CommunityEvent::Unreadable(_) - | CommunityEvent::Failed(_) => { - cx.defer_in(window, |this, window, cx| this.reload(window, cx)); - } CommunityEvent::Channel(..) => { - cx.defer_in(window, |this, window, cx| this.load(window, cx)); + cx.defer_in(window, |this, window, cx| { + this.load(window, cx); + }); } CommunityEvent::Error(error) => { - window.push_notification( - Notification::error(error.clone()).autohide(false), - cx, - ); + window.push_notification(Notification::error(error.clone()), cx); + } + _ => { + cx.defer_in(window, |this, window, cx| { + this.reload(window, cx); + }); } - CommunityEvent::Open(_) | CommunityEvent::Close(_) => {} }; }, )); diff --git a/crates/ui/src/dock/mod.rs b/crates/ui/src/dock/mod.rs index a30bcdca..55ca7115 100644 --- a/crates/ui/src/dock/mod.rs +++ b/crates/ui/src/dock/mod.rs @@ -95,6 +95,19 @@ pub fn add_panel( area.add_panel_view(Arc::new(panel), placement, None, window, cx); } +/// Add a panel to a dock reached through a weak handle. +pub fn add_panel_to( + dock: &WeakEntity, + panel: PanelHandle, + placement: DockPlacement, + window: &mut Window, + cx: &mut App, +) { + let _ = dock.update(cx, |area, cx| { + add_panel(area, panel, placement, window, cx); + }); +} + /// The panel in any region of `area` whose logical id is `key`. fn find_panel(area: &DockArea, key: &SharedString, cx: &App) -> Option<(PanelId, NodeId, usize)> { let placements = [ diff --git a/crates/workspace/src/dialogs/new_chat.rs b/crates/workspace/src/dialogs/new_chat.rs index 96710dc3..8e766050 100644 --- a/crates/workspace/src/dialogs/new_chat.rs +++ b/crates/workspace/src/dialogs/new_chat.rs @@ -2,17 +2,18 @@ use chat::{ChatRegistry, Room, RoomKind}; use gpui::prelude::FluentBuilder; use gpui::{ App, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled, - Subscription, Window, div, px, + Subscription, WeakEntity, Window, div, px, }; use nostr_sdk::prelude::*; use state::NostrRegistry; use theme::ActiveTheme; use ui::button::{Button, ButtonVariants}; +use ui::dock::{DockArea, DockPlacement, PanelHandle}; use ui::input::{Input, InputEvent, InputState}; use ui::{StyledExt, WindowExtension, v_flex}; -pub fn open(window: &mut Window, cx: &mut App) { - let view = cx.new(|cx| NewChat::new(window, cx)); +pub fn open(dock: WeakEntity, window: &mut Window, cx: &mut App) { + let view = cx.new(|cx| NewChat::new(dock, window, cx)); window.open_modal(cx, move |this, _window, _cx| { this.width(px(420.)).title("New chat").child(view.clone()) @@ -20,6 +21,9 @@ pub fn open(window: &mut Window, cx: &mut App) { } pub struct NewChat { + /// The dock a started chat opens in + dock: WeakEntity, + /// Public key input input: Entity, @@ -31,7 +35,7 @@ pub struct NewChat { } impl NewChat { - fn new(window: &mut Window, cx: &mut Context) -> Self { + fn new(dock: WeakEntity, window: &mut Window, cx: &mut Context) -> Self { let input = cx.new(|cx| InputState::new(window, cx).placeholder("npub")); let subscription = cx.subscribe_in(&input, window, |this, _input, event, window, cx| { @@ -41,6 +45,7 @@ impl NewChat { }); Self { + dock, input, error: None, _subscription: Some(subscription), @@ -56,6 +61,8 @@ impl NewChat { }; let nostr = NostrRegistry::global(cx); + let chat = ChatRegistry::global(cx); + let Some(current_user) = nostr.read(cx).current_user() else { self.set_error("You are not signed in", cx); return; @@ -70,12 +77,20 @@ impl NewChat { .organize(¤t_user) .kind(RoomKind::Ongoing); - let chat = ChatRegistry::global(cx); - chat.update(cx, |chat, cx| { + let room = chat.update(cx, |chat, cx| { let room = cx.new(|_| room); - chat.emit_room(&room, window, cx); + chat.track_room(&room, cx); + room }); + ui::dock::add_panel_to( + &self.dock, + PanelHandle::new(chat_ui::init(room.downgrade(), window, cx)), + DockPlacement::Center, + window, + cx, + ); + window.close_modal(cx); } diff --git a/crates/workspace/src/lib.rs b/crates/workspace/src/lib.rs index 3af3c2cb..69e9a575 100644 --- a/crates/workspace/src/lib.rs +++ b/crates/workspace/src/lib.rs @@ -6,13 +6,11 @@ use anyhow::Error; use auto_update::AutoUpdater; use chat::{ChatEvent, ChatRegistry}; use common::download_dir; -use community::{CommunityEvent, CommunityRegistry}; -use community_ui::CommunityPanel; use device::{DeviceEvent, DeviceRegistry}; use gpui::prelude::FluentBuilder; use gpui::{ Action, AnyElement, App, AppContext, Context, Entity, InteractiveElement, IntoElement, - ParentElement, Render, SharedString, Styled, Subscription, Task, WeakEntity, Window, div, px, + ParentElement, Render, SharedString, Styled, Subscription, Task, Window, div, px, }; use nostr_sdk::prelude::*; use person::{PersonRegistry, shorten_pubkey}; @@ -21,7 +19,7 @@ use smallvec::{SmallVec, smallvec}; use state::{NostrRegistry, StateEvent}; use theme::{ActiveTheme, SIDEBAR_WIDTH, Theme, ThemeRegistry}; use ui::button::{Button, ButtonVariants}; -use ui::dock::{self, ClosePanel, DockArea, DockLayout, DockPlacement, Panel, PanelHandle}; +use ui::dock::{self, DockArea, DockLayout, DockPlacement, Panel, PanelHandle}; use ui::menu::{DropdownMenu, PopupMenuItem}; use ui::notification::{Notification, NotificationKind}; use ui::{Icon, IconName, Root, Sizable, WindowExtension, h_flex, v_flex}; @@ -72,8 +70,6 @@ enum Command { pub struct Workspace { dock: Entity, title_bar_chrome: Rc, - /// The community panel currently docked, if any - community_panel: Option>, /// Async tasks tasks: Vec>>, /// Event subscriptions @@ -83,12 +79,11 @@ pub struct Workspace { impl Workspace { fn new(window: &mut Window, cx: &mut Context) -> Self { let chat = ChatRegistry::global(cx); - let communities = CommunityRegistry::global(cx); let device = DeviceRegistry::global(cx); let nostr = NostrRegistry::global(cx); - let sidebar = cx.new(|cx| Sidebar::new(window, cx)); let (dock, title_bar_chrome) = dock::dock_area("coop", window, cx); + let sidebar = cx.new(|cx| Sidebar::new(window, dock.downgrade(), cx)); let mut subscriptions = smallvec![]; @@ -164,7 +159,7 @@ impl Workspace { subscriptions.push( // Observe all events emitted by the chat registry - cx.subscribe_in(&chat, window, move |this, chat, ev, window, cx| { + 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."; @@ -187,28 +182,6 @@ impl Workspace { cx, ); } - ChatEvent::OpenRoom(id) => { - if let Some(room) = chat.read(cx).room(id, cx) { - this.add_panel_to_dock( - chat_ui::init(room, window, cx), - DockPlacement::Center, - window, - cx, - ); - } - } - ChatEvent::CloseRoom(..) => { - this.dock.update(cx, |area, cx| { - // Force focus to the tab panel - ui::dock::focus_tab_panel(area, window, cx); - - // Dispatch the close panel action - cx.defer_in(window, |_, window, cx| { - window.dispatch_action(Box::new(ClosePanel), cx); - window.close_all_modals(cx); - }); - }); - } ChatEvent::Error(error) => { window.push_notification(Notification::error(error).autohide(false), cx); } @@ -217,71 +190,24 @@ impl Workspace { }), ); - subscriptions.push( - // Observe all events emitted by the community registry - cx.subscribe_in( - &communities, - window, - move |this, communities, event, window, cx| match event { - CommunityEvent::Open(id) => { - if let Some(community) = communities.read(cx).community(id) { - let panel = community_ui::init(community, window, cx); - - this.community_panel = Some(panel.downgrade()); - this.add_panel_to_dock(panel, DockPlacement::Center, window, cx); - } - } - CommunityEvent::Close(_) => { - let Some(panel) = this - .community_panel - .take() - .and_then(|panel| panel.upgrade()) - else { - return; - }; - - this.dock.update(cx, |area, cx| { - ui::dock::add_panel( - area, - PanelHandle::new(panel), - DockPlacement::Center, - window, - cx, - ); - ui::dock::focus_tab_panel(area, window, cx); - - cx.defer_in(window, |_, window, cx| { - window.dispatch_action(Box::new(ClosePanel), cx); - }); - }); - } - _ => {} - }, - ), - ); - cx.defer_in(window, move |this, window, cx| { let sidebar = PanelHandle::new(sidebar); - - this.dock.update(cx, |area, cx| { - let left = DockLayout::tabs().panel_view(Arc::new(sidebar), cx); - area.set_dock(DockPlacement::Left, left, window, cx); - area.set_dock_size(DockPlacement::Left, SIDEBAR_WIDTH, window, cx); - }); - let greeter = PanelHandle::new(greeter::init(window, cx)); - let center = DockLayout::v_split() - .child(DockLayout::tabs().panel_view(Arc::new(greeter), cx), None); - this.dock.update(cx, |area, cx| { - area.set_center(center, window, cx); + this.dock.update(cx, |this, cx| { + let left = DockLayout::tabs().panel_view(Arc::new(sidebar), cx); + let center = DockLayout::v_split() + .child(DockLayout::tabs().panel_view(Arc::new(greeter), cx), None); + + this.set_dock(DockPlacement::Left, left, window, cx); + this.set_dock_size(DockPlacement::Left, SIDEBAR_WIDTH, window, cx); + this.set_center(center, window, cx); }); }); Self { dock, title_bar_chrome, - community_panel: None, tasks: vec![], _subscriptions: subscriptions, } @@ -365,10 +291,11 @@ impl Workspace { self.add_panel_to_dock(browse::init(window, cx), DockPlacement::Center, window, cx); } Command::ShowSearch => { - self.add_panel_to_dock(search::init(window, cx), DockPlacement::Center, window, cx); + let panel = search::init(self.dock.downgrade(), window, cx); + self.add_panel_to_dock(panel, DockPlacement::Center, window, cx); } Command::NewChat => { - new_chat::open(window, cx); + new_chat::open(self.dock.downgrade(), window, cx); } Command::NewCommunity => { new_community::open(window, cx); diff --git a/crates/workspace/src/panels/search.rs b/crates/workspace/src/panels/search.rs index 38261cda..64feab68 100644 --- a/crates/workspace/src/panels/search.rs +++ b/crates/workspace/src/panels/search.rs @@ -7,8 +7,8 @@ use common::DebouncedDelay; use gpui::prelude::FluentBuilder; use gpui::{ AnyElement, App, AppContext, Context, ElementId, Entity, EventEmitter, FocusHandle, Focusable, - IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Task, Window, div, - uniform_list, + IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Task, WeakEntity, + Window, div, uniform_list, }; use instant::Duration; use nostr_sdk::prelude::*; @@ -17,7 +17,7 @@ use smallvec::{SmallVec, smallvec}; use state::{FIND_DELAY, NostrRegistry}; use theme::ActiveTheme; use ui::button::{Button, ButtonVariants}; -use ui::dock::{Panel, PanelEvent}; +use ui::dock::{DockArea, DockPlacement, Panel, PanelEvent, PanelHandle}; use ui::input::{Input, InputEvent, InputState}; use ui::notification::Notification; use ui::{Icon, IconName, Selectable, Sizable, StyledExt, WindowExtension, h_flex, v_flex}; @@ -26,13 +26,15 @@ use crate::sidebar::{TreeRow, TreeRowKind}; const INPUT_PLACEHOLDER: &str = "Find or start a conversation"; -pub fn init(window: &mut Window, cx: &mut App) -> Entity { - cx.new(|cx| SearchPanel::new(window, cx)) +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, @@ -63,7 +65,7 @@ pub struct SearchPanel { } impl SearchPanel { - fn new(window: &mut Window, cx: &mut Context) -> Self { + 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); @@ -107,6 +109,7 @@ impl SearchPanel { Self { name: "Search".into(), focus_handle: cx.focus_handle(), + dock, find_input, find_debouncer: DebouncedDelay::new(), find_results, @@ -285,6 +288,7 @@ impl SearchPanel { 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 { @@ -295,14 +299,26 @@ impl SearchPanel { let receivers = self.get_selected(cx); self.tasks.push(cx.spawn_in(window, async move |this, cx| { - // Create a new room and emit it - async_chat.update_in(cx, |this, _window, 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) }); - this.emit_room(&room, _window, cx); + 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 diff --git a/crates/workspace/src/sidebar/mod.rs b/crates/workspace/src/sidebar/mod.rs index 8e709ad6..55e02bc3 100644 --- a/crates/workspace/src/sidebar/mod.rs +++ b/crates/workspace/src/sidebar/mod.rs @@ -2,7 +2,7 @@ use std::ops::Range; use std::rc::Rc; use auto_update::AutoUpdater; -use chat::{ChatEvent, ChatRegistry, RoomKind}; +use chat::{ChatEvent, ChatRegistry, Room, RoomKind}; use common::TimestampExt; use community::{ChannelId, Community, CommunityEvent, CommunityRegistry}; use gpui::prelude::FluentBuilder; @@ -20,7 +20,7 @@ use state::NostrRegistry; use theme::{ActiveTheme, TABBAR_HEIGHT}; use ui::avatar::Avatar; use ui::button::{Button, ButtonVariants}; -use ui::dock::{Panel, PanelEvent}; +use ui::dock::{DockArea, DockPlacement, Panel, PanelEvent, PanelHandle}; use ui::indicator::Indicator; use ui::menu::{DropdownMenu, PopupMenuItem}; use ui::nav_item::NavItem; @@ -46,6 +46,8 @@ pub struct Sidebar { scroll_handles: [UniformListScrollHandle; 3], /// Scroll state of the channel and member lists community_scroll: ScrollHandle, + /// The dock the sidebar opens its panels in + dock: WeakEntity, active_tab: SidebarTab, /// The community the sidebar is browsing, if any community: Option>, @@ -59,7 +61,7 @@ pub struct Sidebar { } impl Sidebar { - pub fn new(window: &mut Window, cx: &mut Context) -> Self { + pub fn new(window: &mut Window, dock: WeakEntity, cx: &mut Context) -> Self { let chat = ChatRegistry::global(cx); let communities = CommunityRegistry::global(cx); let nostr = NostrRegistry::global(cx); @@ -98,6 +100,7 @@ impl Sidebar { UniformListScrollHandle::new(), ], community_scroll: ScrollHandle::default(), + dock, active_tab: SidebarTab::Recents, community: None, channels_open: true, @@ -130,25 +133,34 @@ impl Sidebar { settings.record_recent_community(id, cx); }); - CommunityRegistry::global(cx).update(cx, |registry, cx| { - registry.emit_community(&community, window, cx); - }); - self.community = Some(community.downgrade()); + + ui::dock::add_panel_to( + &self.dock, + PanelHandle::new(community_ui::init(community, window, cx)), + DockPlacement::Center, + window, + cx, + ); + cx.notify(); } - fn close_community(&mut self, window: &mut Window, cx: &mut Context) { - let Some(community) = self.community.take() else { + fn open_room(&mut self, room: Entity, window: &mut Window, cx: &mut Context) { + ui::dock::add_panel_to( + &self.dock, + PanelHandle::new(chat_ui::init(room.downgrade(), window, cx)), + DockPlacement::Center, + window, + cx, + ); + } + + /// Leave the community view, returning the sidebar to its tab list. + fn reset_community(&mut self, cx: &mut Context) { + if self.community.take().is_none() { return; - }; - - if let Ok(id) = community.read_with(cx, |community, _cx| community.id()) { - CommunityRegistry::global(cx).update(cx, |registry, cx| { - registry.emit_close(id, window, cx); - }); } - cx.notify(); } @@ -250,8 +262,8 @@ impl Sidebar { .tooltip("Back") .ghost() .small() - .on_click(cx.listener(|this, _event, window, cx| { - this.close_community(window, cx); + .on_click(cx.listener(|this, _event, _window, cx| { + this.reset_community(cx); })), ); } @@ -407,10 +419,8 @@ fn render_rows(range: Range, rows: &[SidebarRow], cx: &Context) let created_at = room.read(cx).created_at.to_ago(); let room_clone = room.clone(); - let handler = cx.listener(move |_this, _event, window, cx| { - ChatRegistry::global(cx).update(cx, |chat, cx| { - chat.emit_room(&room_clone, window, cx); - }); + let handler = cx.listener(move |this, _event, window, cx| { + this.open_room(room_clone.clone(), window, cx); }); TreeRow::new( -- 2.54.0 From 273ddabde549997d4252f90bd9018140abf820b5 Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Wed, 23 Sep 2026 14:54:34 +0700 Subject: [PATCH 3/9] clean up the sidebar --- assets/icons/arrow-down-circle.svg | 3 + crates/ui/src/dock/panel.rs | 3 - crates/ui/src/icon.rs | 2 + crates/workspace/src/lib.rs | 185 +--- crates/workspace/src/sidebar/mod.rs | 968 +++++++++++---------- crates/workspace/src/sidebar/onboarding.rs | 74 -- 6 files changed, 517 insertions(+), 718 deletions(-) create mode 100644 assets/icons/arrow-down-circle.svg delete mode 100644 crates/workspace/src/sidebar/onboarding.rs diff --git a/assets/icons/arrow-down-circle.svg b/assets/icons/arrow-down-circle.svg new file mode 100644 index 00000000..fea6250e --- /dev/null +++ b/assets/icons/arrow-down-circle.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/ui/src/dock/panel.rs b/crates/ui/src/dock/panel.rs index af43c481..b4fdff01 100644 --- a/crates/ui/src/dock/panel.rs +++ b/crates/ui/src/dock/panel.rs @@ -19,9 +19,6 @@ pub enum PanelEvent { pub trait Panel: EventEmitter + Render + Focusable { /// The name of the panel used to serialize, deserialize and identify the panel. - /// - /// This is used to identify the panel when deserializing the panel. - /// Once you have defined a panel id, this must not be changed. fn panel_id(&self) -> SharedString; /// The title of the panel diff --git a/crates/ui/src/icon.rs b/crates/ui/src/icon.rs index 9b935db8..fef5db14 100644 --- a/crates/ui/src/icon.rs +++ b/crates/ui/src/icon.rs @@ -22,6 +22,7 @@ impl From for Icon { pub enum IconName { ArrowLeft, ArrowRight, + ArrowDownCircle, Boom, Book, ChevronDown, @@ -99,6 +100,7 @@ impl IconNamed for IconName { match self { Self::ArrowLeft => "icons/arrow-left.svg", Self::ArrowRight => "icons/arrow-right.svg", + Self::ArrowDownCircle => "icons/arrow-down-circle.svg", Self::Boom => "icons/boom.svg", Self::Book => "icons/book.svg", Self::ChevronDown => "icons/chevron-down.svg", diff --git a/crates/workspace/src/lib.rs b/crates/workspace/src/lib.rs index 69e9a575..dff8213a 100644 --- a/crates/workspace/src/lib.rs +++ b/crates/workspace/src/lib.rs @@ -1,4 +1,3 @@ -use std::rc::Rc; use std::sync::Arc; use ::settings::AppSettings; @@ -7,22 +6,19 @@ use auto_update::AutoUpdater; use chat::{ChatEvent, ChatRegistry}; use common::download_dir; use device::{DeviceEvent, DeviceRegistry}; -use gpui::prelude::FluentBuilder; use gpui::{ - Action, AnyElement, App, AppContext, Context, Entity, InteractiveElement, IntoElement, - ParentElement, Render, SharedString, Styled, Subscription, Task, Window, div, px, + Action, App, AppContext, Context, Entity, InteractiveElement, IntoElement, ParentElement, + Render, SharedString, Styled, Subscription, Task, Window, div, px, }; use nostr_sdk::prelude::*; -use person::{PersonRegistry, shorten_pubkey}; use serde::Deserialize; use smallvec::{SmallVec, smallvec}; use state::{NostrRegistry, StateEvent}; use theme::{ActiveTheme, SIDEBAR_WIDTH, Theme, ThemeRegistry}; use ui::button::{Button, ButtonVariants}; use ui::dock::{self, DockArea, DockLayout, DockPlacement, Panel, PanelHandle}; -use ui::menu::{DropdownMenu, PopupMenuItem}; use ui::notification::{Notification, NotificationKind}; -use ui::{Icon, IconName, Root, Sizable, WindowExtension, h_flex, v_flex}; +use ui::{IconName, Root, Sizable, WindowExtension, h_flex, v_flex}; use crate::dialogs::restore::RestoreEncryption; use crate::dialogs::{new_chat, new_community, settings}; @@ -69,7 +65,6 @@ enum Command { pub struct Workspace { dock: Entity, - title_bar_chrome: Rc, /// Async tasks tasks: Vec>>, /// Event subscriptions @@ -82,7 +77,7 @@ impl Workspace { let device = DeviceRegistry::global(cx); let nostr = NostrRegistry::global(cx); - let (dock, title_bar_chrome) = dock::dock_area("coop", window, cx); + let (dock, _) = dock::dock_area("coop", window, cx); let sidebar = cx.new(|cx| Sidebar::new(window, dock.downgrade(), cx)); let mut subscriptions = smallvec![]; @@ -207,7 +202,6 @@ impl Workspace { Self { dock, - title_bar_chrome, tasks: vec![], _subscriptions: subscriptions, } @@ -510,174 +504,6 @@ impl Workspace { })) }); } - - fn titlebar_right(_window: &mut Window, cx: &mut App) -> AnyElement { - let auto_updater = AutoUpdater::try_global(cx); - let chat = ChatRegistry::global(cx); - let nip4e_enabled = AppSettings::get_nip4e(cx); - let nostr = NostrRegistry::global(cx); - - let Some(public_key) = nostr.read(cx).current_user() else { - return div().into_any_element(); - }; - - let persons = PersonRegistry::global(cx); - let profile = persons.read(cx).get(&public_key, cx); - let announcement = profile.announcement(); - - let updater_status = auto_updater.as_ref().and_then(|updater| { - let updater = updater.read(cx); - (!updater.idle()).then(|| updater.status()) - }); - - let staged_update = auto_updater - .as_ref() - .is_some_and(|updater| updater.read(cx).staged()); - - h_flex() - .when(!cx.theme().platform.is_mac(), |this| this.pr_2()) - .gap_2() - .when_some(updater_status, |this, status| { - this.child(div().text_xs().italic().child(status)) - }) - .when(staged_update, |this| { - this.child( - Button::new("restart-to-update") - .label("Restart to Update") - .tooltip("Quit and relaunch into the installed update") - .small() - .ghost() - .on_click(|_event, _window, cx| { - if let Some(auto_updater) = AutoUpdater::try_global(cx) { - auto_updater.update(cx, |this, cx| this.restart(cx)); - } - }), - ) - }) - .when(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), - ) - }), - ) - }) - .child( - Button::new("inbox") - .icon(IconName::Inbox) - .small() - .ghost() - .dropdown_menu(move |this, _window, cx| { - let urls: Vec<(SharedString, SharedString)> = profile - .messaging_relays() - .iter() - .map(|url| { - ( - SharedString::from(url.to_string()), - chat.read(cx).count_messages(url).to_string().into(), - ) - }) - .collect(); - - // Header - let menu = this.min_w(px(260.)).label("Messaging Relays"); - - // Content - let menu = urls.into_iter().fold(menu, |this, (url, count)| { - this.item(PopupMenuItem::element(move |_window, cx| { - h_flex() - .px_1() - .w_full() - .text_sm() - .justify_between() - .child(url.clone()) - .child( - div() - .text_xs() - .text_color(cx.theme().text_muted) - .child(count.clone()), - ) - })) - }); - - // Footer - menu.separator() - .menu_with_icon( - "Manage gossip relays", - IconName::Relay, - Box::new(Command::ShowRelayList), - ) - .menu_with_icon( - "Manage messaging relays", - IconName::Relay, - Box::new(Command::ShowMessaging), - ) - .separator() - .menu_with_icon( - "Reload", - IconName::Refresh, - Box::new(Command::RefreshMessagingRelays), - ) - }), - ) - .into_any_element() - } } impl Render for Workspace { @@ -685,9 +511,6 @@ impl Render for Workspace { let modal_layer = Root::render_modal_layer(window, cx); let notification_layer = Root::render_notification_layer(window, cx); - // Render the title bar chrome - self.title_bar_chrome.set_trailing(Self::titlebar_right); - div() .id("workspace") .on_action(cx.listener(Self::on_command)) diff --git a/crates/workspace/src/sidebar/mod.rs b/crates/workspace/src/sidebar/mod.rs index 55e02bc3..ef937d44 100644 --- a/crates/workspace/src/sidebar/mod.rs +++ b/crates/workspace/src/sidebar/mod.rs @@ -7,10 +7,10 @@ use common::TimestampExt; use community::{ChannelId, Community, CommunityEvent, CommunityRegistry}; use gpui::prelude::FluentBuilder; use gpui::{ - AnyElement, App, Context, ElementId, Entity, EventEmitter, FocusHandle, Focusable, + AnyElement, App, Context, Div, ElementId, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, ObjectFit, ParentElement, Render, ScrollHandle, SharedString, - StatefulInteractiveElement, Styled, StyledImage, Subscription, UniformListScrollHandle, - WeakEntity, Window, div, img, px, retain_all, uniform_list, + Stateful, StatefulInteractiveElement, Styled, StyledImage, Subscription, + UniformListScrollHandle, WeakEntity, Window, div, img, px, retain_all, uniform_list, }; use nostr_sdk::prelude::*; use person::PersonRegistry; @@ -32,8 +32,8 @@ use ui::{ }; use crate::Command; +use crate::dialogs::import; -mod onboarding; mod tab; mod tree; @@ -51,11 +51,9 @@ pub struct Sidebar { active_tab: SidebarTab, /// The community the sidebar is browsing, if any community: Option>, - /// Expanded state of the community's sections channels_open: bool, admins_open: bool, members_open: bool, - /// Whether there are new chat requests new_requests: bool, _subscriptions: SmallVec<[Subscription; 4]>, } @@ -64,7 +62,6 @@ impl Sidebar { pub fn new(window: &mut Window, dock: WeakEntity, cx: &mut Context) -> Self { let chat = ChatRegistry::global(cx); let communities = CommunityRegistry::global(cx); - let nostr = NostrRegistry::global(cx); let mut subscriptions = smallvec![]; @@ -82,16 +79,11 @@ impl Sidebar { window, |_this, _communities, event, window, cx| { if let CommunityEvent::Error(error) = event { - window - .push_notification(Notification::error(error.clone()).autohide(false), cx); + window.push_notification(Notification::error(error.clone()), cx); } }, )); - subscriptions.push(cx.observe(&nostr, |_this, _nostr, cx| cx.notify())); - - subscriptions.push(cx.observe(&communities, |_this, _communities, cx| cx.notify())); - Self { focus_handle: cx.focus_handle(), scroll_handles: [ @@ -115,7 +107,6 @@ impl Sidebar { if self.active_tab == tab { return; } - self.active_tab = tab; cx.notify(); } @@ -164,11 +155,11 @@ impl Sidebar { cx.notify(); } - fn render_user(&self, window: &mut Window, cx: &mut Context) -> AnyElement { + fn render_user(&self, cx: &mut Context) -> Stateful
{ let nostr = NostrRegistry::global(cx); let current_user = nostr.read(cx).current_user(); - let mut row = h_flex() + h_flex() .id("sidebar-user") .w_full() .h(TABBAR_HEIGHT) @@ -178,387 +169,139 @@ impl Sidebar { .px_2() .when(cfg!(target_os = "macos"), |this| { this.pl(px(TRAFFIC_LIGHT_PADDING)) - }); + }) + .when_some(current_user.as_ref(), |this, public_key| { + let persons = PersonRegistry::global(cx); + let profile = persons.read(cx).get(public_key, cx); + let avatar = profile.avatar(); + let avatar_seed = profile.avatar_seed(); + let name = profile.name(); - if let Some(public_key) = current_user.as_ref() { - let persons = PersonRegistry::global(cx); - let profile = persons.read(cx).get(public_key, cx); - let avatar = profile.avatar(); - let avatar_seed = profile.avatar_seed(); - let name = profile.name(); + this.child( + Button::new("current-user") + .child( + Avatar::new(avatar.clone()) + .seed(avatar_seed.clone()) + .xsmall(), + ) + .small() + .caret() + .compact() + .transparent() + .dropdown_menu(move |this, _window, cx| { + let avatar = avatar.clone(); + let avatar_seed = avatar_seed.clone(); + let name = name.clone(); - row = row.child( - Button::new("current-user") - .child( - Avatar::new(avatar.clone()) - .seed(avatar_seed.clone()) - .xsmall(), - ) - .small() - .caret() - .compact() - .transparent() - .dropdown_menu(move |this, _window, cx| { - let avatar = avatar.clone(); - let avatar_seed = avatar_seed.clone(); - let name = name.clone(); - - this.min_w(px(256.)) - .item(PopupMenuItem::element(move |_window, cx| { - h_flex() - .gap_1p5() - .text_xs() - .text_color(cx.theme().text_muted) - .child( - Avatar::new(avatar.clone()) - .seed(avatar_seed.clone()) - .xsmall(), - ) - .child(name.clone()) - })) - .separator() - .menu_with_icon("Inbox", IconName::Inbox, Box::new(Command::ShowInbox)) - .menu_with_icon( - "Search", - IconName::Search, - Box::new(Command::ShowSearch), - ) - .menu_with_icon( - "Profile", - IconName::Profile, - Box::new(Command::ShowProfile), - ) - .menu_with_icon( - "Contact List", - IconName::Book, - Box::new(Command::ShowContactList), - ) - .menu_with_icon( - "Backup", - IconName::UserKey, - Box::new(Command::ShowBackup), - ) - .menu_with_icon("Themes", IconName::Sun, Box::new(Command::ToggleTheme)) - .when(AutoUpdater::is_available(cx), |this| { - this.separator().menu_with_icon( - "Check for Updates", - IconName::Device, - Box::new(Command::Update), + this.min_w(px(256.)) + .item(PopupMenuItem::element(move |_window, cx| { + h_flex() + .gap_1p5() + .text_xs() + .text_color(cx.theme().text_muted) + .child( + Avatar::new(avatar.clone()) + .seed(avatar_seed.clone()) + .xsmall(), + ) + .child(name.clone()) + })) + .separator() + .menu_with_icon( + "Inbox", + IconName::Inbox, + Box::new(Command::ShowInbox), ) - }) - .menu_with_icon( - "Settings", - IconName::Settings, - Box::new(Command::ShowSettings), - ) - }), - ); - } - - if self.community.is_some() { - row = row.child(div().flex_1()).child( - Button::new("sidebar-back") - .icon(IconName::ArrowLeft) - .tooltip("Back") - .ghost() - .small() - .on_click(cx.listener(|this, _event, _window, cx| { - this.reset_community(cx); - })), - ); - } - - title_bar_drag_handlers(row, window, cx).into_any_element() - } -} - -fn recent_communities(cx: &App) -> Vec> { - const LIMIT: usize = 3; - - let registry = CommunityRegistry::global(cx); - let communities = registry.read(cx).communities(); - let recent = AppSettings::get_recent_communities(cx); - - let mut rows: Vec> = recent - .iter() - .filter_map(|id| { - communities - .iter() - .find(|community| community.read(cx).id().to_hex() == *id) - }) - .take(LIMIT) - .cloned() - .collect(); - - if rows.is_empty() { - rows = communities.iter().take(LIMIT).cloned().collect(); - } - - rows -} - -fn rows_for(tab: SidebarTab, cx: &App) -> Vec { - match tab { - SidebarTab::Recents => { - let chat = ChatRegistry::global(cx); - let rooms = chat.read(cx).rooms(&RoomKind::Ongoing, cx); - let registry = CommunityRegistry::global(cx); - let community_count = registry.read(cx).communities().len(); - let communities = recent_communities(cx); - - if communities.is_empty() && rooms.is_empty() { - return vec![SidebarRow::Hint { - text: "Nothing recent yet".into(), - }]; - } - - let mut rows = Vec::new(); - - if !communities.is_empty() { - rows.push(SidebarRow::Section { - label: "Communities".into(), - count: community_count, - }); - rows.extend( - communities - .into_iter() - .map(|community| SidebarRow::Community { community }), - ); - rows.push(SidebarRow::Action { - label: "Show all communities".into(), - tab: SidebarTab::Communities, - }); - } - - if !rooms.is_empty() { - rows.push(SidebarRow::Section { - label: "Chats".into(), - count: rooms.len(), - }); - rows.extend( - rooms - .into_iter() - .take(5) - .map(|room| SidebarRow::Room { room }), - ); - rows.push(SidebarRow::Action { - label: "Show all chats".into(), - tab: SidebarTab::Chats, - }); - } - - rows - } - SidebarTab::Chats => { - let chat = ChatRegistry::global(cx); - let chat = chat.read(cx); - let messages = chat.rooms(&RoomKind::Ongoing, cx); - - let mut rows = vec![SidebarRow::Section { - label: "Chats".into(), - count: messages.len(), - }]; - - if messages.is_empty() { - rows.push(SidebarRow::Hint { - text: "No conversations yet".into(), - }); - } else { - rows.extend(messages.into_iter().map(|room| SidebarRow::Room { room })); - } - - rows - } - SidebarTab::Communities => { - let registry = CommunityRegistry::global(cx); - let communities = registry.read(cx).communities(); - - let mut rows = vec![SidebarRow::Section { - label: "Communities".into(), - count: communities.len(), - }]; - - if communities.is_empty() { - rows.push(SidebarRow::Hint { - text: "No communities yet".into(), - }); - } else { - rows.extend( - communities - .iter() - .cloned() - .map(|community| SidebarRow::Community { community }), - ); - } - - rows - } - } -} - -fn render_rows(range: Range, rows: &[SidebarRow], cx: &Context) -> Vec { - rows.get(range.clone()) - .into_iter() - .flatten() - .enumerate() - .map(|(offset, row)| { - let index = range.start + offset; - - match row { - SidebarRow::Section { label, count } => TreeRow::new( - ElementId::NamedInteger("tree-row".into(), index as u64), - TreeRowKind::Section, - label.clone(), + .menu_with_icon( + "Search", + IconName::Search, + Box::new(Command::ShowSearch), + ) + .menu_with_icon( + "Profile", + IconName::Profile, + Box::new(Command::ShowProfile), + ) + .menu_with_icon( + "Contact List", + IconName::Book, + Box::new(Command::ShowContactList), + ) + .menu_with_icon( + "Backup", + IconName::UserKey, + Box::new(Command::ShowBackup), + ) + .menu_with_icon( + "Themes", + IconName::Sun, + Box::new(Command::ToggleTheme), + ) + .when(AutoUpdater::is_available(cx), |this| { + this.separator().menu_with_icon( + "Check for Updates", + IconName::Device, + Box::new(Command::Update), + ) + }) + .menu_with_icon( + "Settings", + IconName::Settings, + Box::new(Command::ShowSettings), + ) + }), ) - .count(*count) - .into_any_element(), - SidebarRow::Room { room } => { - let name = room.read(cx).display_name(cx); - let picture = room.read(cx).display_image(cx); - let seed = room.read(cx).display_image_seed(cx); - let created_at = room.read(cx).created_at.to_ago(); - let room_clone = room.clone(); - - let handler = cx.listener(move |this, _event, window, cx| { - this.open_room(room_clone.clone(), window, cx); - }); - - TreeRow::new( - ElementId::NamedInteger("tree-row".into(), index as u64), - TreeRowKind::Room, - name, - ) - .avatar(seed) - .picture(picture) - .created_at(created_at) - .on_click(handler) - .into_any_element() - } - SidebarRow::Community { community } => { - let name = community.read(cx).name(); - let seed = community.read(cx).id().to_hex(); - let picture = community.read(cx).icon(); - let community = community.clone(); - - TreeRow::new( - ElementId::NamedInteger("tree-row".into(), index as u64), - TreeRowKind::Community, - name, - ) - .avatar(seed) - .picture(picture) - .on_click(cx.listener(move |this, _event, window, cx| { - this.open_community(community.clone(), window, cx); - })) - .into_any_element() - } - SidebarRow::Action { label, tab } => { - let tab = *tab; - - TreeRow::new( - ElementId::NamedInteger("tree-row".into(), index as u64), - TreeRowKind::Action, - label.clone(), - ) - .icon(IconName::ArrowRight) - .on_click(cx.listener(move |this, _event, _window, cx| { - this.select_tab(tab, cx); - })) - .into_any_element() - } - SidebarRow::Hint { text } => TreeRow::new( - ElementId::NamedInteger("tree-row".into(), index as u64), - TreeRowKind::Hint, - text.clone(), + }) + .child(div().flex_1()) + .when_some(AutoUpdater::try_global(cx), |this, updater| { + this.child(self.render_updater(updater, cx)) + }) + .when(self.community.is_some(), |this| { + this.child( + Button::new("sidebar-back") + .icon(IconName::ArrowLeft) + .tooltip("Back") + .ghost() + .small() + .on_click(cx.listener(|this, _event, _window, cx| { + this.reset_community(cx); + })), ) - .into_any_element(), - } - }) - .collect() -} - -impl Panel for Sidebar { - fn panel_id(&self) -> SharedString { - "Sidebar".into() + }) } - fn title(&self, _cx: &App) -> AnyElement { - SharedString::from("Sidebar").into_any_element() - } + fn render_updater(&self, updater: Entity, cx: &mut App) -> AnyElement { + let status = updater.read(cx).status(); + let staged = updater.read(cx).staged(); - fn closable(&self, _cx: &App) -> bool { - false - } - - fn zoomable(&self, _cx: &App) -> bool { - false - } -} - -impl EventEmitter for Sidebar {} - -impl Focusable for Sidebar { - fn focus_handle(&self, _: &App) -> gpui::FocusHandle { - self.focus_handle.clone() - } -} - -impl Render for Sidebar { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let (logged_in, ready) = { - let nostr = NostrRegistry::global(cx); - ( - nostr.read(cx).current_user().is_some(), - nostr.read(cx).ready(), - ) - }; - - if !logged_in { - if !ready { - return v_flex() - .size_full() - .bg(cx.theme().surface_background) - .border_r_1() - .border_color(cx.theme().border_variant) - .into_any_element(); - } - return onboarding::render(window, cx).into_any_element(); - } - - let community = self - .community - .clone() - .and_then(|community| community.upgrade()); - - if community.is_none() { - self.community = None; - } - - v_flex() - .image_cache(retain_all("sidebar")) - .size_full() - .relative() + h_flex() .gap_2() - .bg(cx.theme().surface_background) - .border_r_1() - .border_color(cx.theme().border_variant) - .child(self.render_user(window, cx)) - .map(|this| match community { - Some(community) => this.child(self.render_community(community, cx)), - None => this.child(self.render_tabs(cx)), + .text_xs() + .child(status) + .when(staged, |this| { + this.child( + Button::new("restart-to-update") + .icon(IconName::ArrowDownCircle) + .tooltip("Quit and relaunch into the installed update") + .small() + .ghost() + .on_click(move |_, _window, cx| { + updater.update(cx, |this, cx| { + this.restart(cx); + }); + }), + ) }) .into_any_element() } -} -impl Sidebar { fn render_tabs(&mut self, cx: &mut Context) -> AnyElement { let chat = ChatRegistry::global(cx); let loading = chat.read(cx).loading(); let sidebar = cx.entity().downgrade(); let active_tab = self.active_tab; - let rows = Rc::new(rows_for(active_tab, cx)); + let rows = Rc::new(self.rows_for(active_tab, cx)); let scroll_handle = &self.scroll_handles[active_tab.index()]; v_flex() @@ -653,8 +396,8 @@ impl Sidebar { uniform_list( active_tab.list_id(), rows.len(), - cx.processor(move |_this, range, _window, cx| { - render_rows(range, rows.as_slice(), cx) + cx.processor(move |this, range, _window, cx| { + this.render_rows(range, rows.as_slice(), cx) }), ) .track_scroll(scroll_handle) @@ -742,7 +485,7 @@ impl Sidebar { .pb_2() .track_scroll(&self.community_scroll) .overflow_y_scroll() - .child(section_row( + .child(self.section_row( "channels", "Channels", channels.len(), @@ -752,10 +495,10 @@ impl Sidebar { )) .when(self.channels_open, |this| { this.children(channels.into_iter().map(|(id, name, private)| { - channel_row(id, name, private, active == Some(id), &community, cx) + self.channel_row(id, name, private, active == Some(id), &community, cx) })) }) - .child(section_row( + .child(self.section_row( "admins", "Admins", admins.len(), @@ -767,10 +510,10 @@ impl Sidebar { this.children( admins .iter() - .map(|public_key| member_row("community-admin", *public_key, cx)), + .map(|public_key| self.member_row("community-admin", *public_key, cx)), ) }) - .child(section_row( + .child(self.section_row( "members", "Members", members.len(), @@ -782,7 +525,7 @@ impl Sidebar { this.children( members .iter() - .map(|public_key| member_row("member", *public_key, cx)), + .map(|public_key| self.member_row("member", *public_key, cx)), ) }); @@ -806,87 +549,392 @@ impl Sidebar { .child(Scrollbar::vertical(&self.community_scroll)) .into_any_element() } -} -fn section_row( - id: &'static str, - label: &'static str, - count: usize, - open: bool, - toggle: impl Fn(&mut Sidebar) + 'static, - cx: &mut Context, -) -> AnyElement { - div() - .flex_shrink_0() - .child( - TreeRow::new(ElementId::Name(id.into()), TreeRowKind::Section, label) - .icon(if open { - IconName::CaretDown + fn recent_communities(&self, cx: &App) -> Vec> { + const LIMIT: usize = 3; + + let registry = CommunityRegistry::global(cx); + let communities = registry.read(cx).communities(); + let recent = AppSettings::get_recent_communities(cx); + + let mut rows: Vec> = recent + .iter() + .filter_map(|id| { + communities + .iter() + .find(|community| community.read(cx).id().to_hex() == *id) + }) + .take(LIMIT) + .cloned() + .collect(); + + if rows.is_empty() { + rows = communities.iter().take(LIMIT).cloned().collect(); + } + + rows + } + + fn rows_for(&self, tab: SidebarTab, cx: &App) -> Vec { + match tab { + SidebarTab::Recents => { + let chat = ChatRegistry::global(cx); + let rooms = chat.read(cx).rooms(&RoomKind::Ongoing, cx); + let registry = CommunityRegistry::global(cx); + let community_count = registry.read(cx).communities().len(); + let communities = self.recent_communities(cx); + + if communities.is_empty() && rooms.is_empty() { + return vec![SidebarRow::Hint { + text: "Nothing recent yet".into(), + }]; + } + + let mut rows = Vec::new(); + + if !communities.is_empty() { + rows.push(SidebarRow::Section { + label: "Communities".into(), + count: community_count, + }); + rows.extend( + communities + .into_iter() + .map(|community| SidebarRow::Community { community }), + ); + rows.push(SidebarRow::Action { + label: "Show all communities".into(), + tab: SidebarTab::Communities, + }); + } + + if !rooms.is_empty() { + rows.push(SidebarRow::Section { + label: "Chats".into(), + count: rooms.len(), + }); + rows.extend( + rooms + .into_iter() + .take(5) + .map(|room| SidebarRow::Room { room }), + ); + rows.push(SidebarRow::Action { + label: "Show all chats".into(), + tab: SidebarTab::Chats, + }); + } + + rows + } + SidebarTab::Chats => { + let chat = ChatRegistry::global(cx); + let chat = chat.read(cx); + let messages = chat.rooms(&RoomKind::Ongoing, cx); + + let mut rows = vec![SidebarRow::Section { + label: "Chats".into(), + count: messages.len(), + }]; + + if messages.is_empty() { + rows.push(SidebarRow::Hint { + text: "No conversations yet".into(), + }); } else { - IconName::CaretRight + rows.extend(messages.into_iter().map(|room| SidebarRow::Room { room })); + } + + rows + } + SidebarTab::Communities => { + let registry = CommunityRegistry::global(cx); + let communities = registry.read(cx).communities(); + + let mut rows = vec![SidebarRow::Section { + label: "Communities".into(), + count: communities.len(), + }]; + + if communities.is_empty() { + rows.push(SidebarRow::Hint { + text: "No communities yet".into(), + }); + } else { + rows.extend( + communities + .iter() + .cloned() + .map(|community| SidebarRow::Community { community }), + ); + } + + rows + } + } + } + + fn render_rows( + &self, + range: Range, + rows: &[SidebarRow], + cx: &Context, + ) -> Vec { + rows.get(range.clone()) + .into_iter() + .flatten() + .enumerate() + .map(|(offset, row)| { + let index = range.start + offset; + + match row { + SidebarRow::Section { label, count } => TreeRow::new( + ElementId::NamedInteger("tree-row".into(), index as u64), + TreeRowKind::Section, + label.clone(), + ) + .count(*count) + .into_any_element(), + SidebarRow::Room { room } => { + let name = room.read(cx).display_name(cx); + let picture = room.read(cx).display_image(cx); + let seed = room.read(cx).display_image_seed(cx); + let created_at = room.read(cx).created_at.to_ago(); + let room_clone = room.clone(); + + let handler = cx.listener(move |this, _event, window, cx| { + this.open_room(room_clone.clone(), window, cx); + }); + + TreeRow::new( + ElementId::NamedInteger("tree-row".into(), index as u64), + TreeRowKind::Room, + name, + ) + .avatar(seed) + .picture(picture) + .created_at(created_at) + .on_click(handler) + .into_any_element() + } + SidebarRow::Community { community } => { + let name = community.read(cx).name(); + let seed = community.read(cx).id().to_hex(); + let picture = community.read(cx).icon(); + let community = community.clone(); + + TreeRow::new( + ElementId::NamedInteger("tree-row".into(), index as u64), + TreeRowKind::Community, + name, + ) + .avatar(seed) + .picture(picture) + .on_click(cx.listener(move |this, _event, window, cx| { + this.open_community(community.clone(), window, cx); + })) + .into_any_element() + } + SidebarRow::Action { label, tab } => { + let tab = *tab; + + TreeRow::new( + ElementId::NamedInteger("tree-row".into(), index as u64), + TreeRowKind::Action, + label.clone(), + ) + .icon(IconName::ArrowRight) + .on_click(cx.listener(move |this, _event, _window, cx| { + this.select_tab(tab, cx); + })) + .into_any_element() + } + SidebarRow::Hint { text } => TreeRow::new( + ElementId::NamedInteger("tree-row".into(), index as u64), + TreeRowKind::Hint, + text.clone(), + ) + .into_any_element(), + } + }) + .collect() + } + + fn section_row( + &self, + id: &'static str, + label: &'static str, + count: usize, + open: bool, + toggle: impl Fn(&mut Sidebar) + 'static, + cx: &mut Context, + ) -> AnyElement { + div() + .flex_shrink_0() + .child( + TreeRow::new(ElementId::Name(id.into()), TreeRowKind::Section, label) + .icon(if open { + IconName::CaretDown + } else { + IconName::CaretRight + }) + .count(count) + .on_click(cx.listener(move |this, _event, _window, cx| { + toggle(this); + cx.notify(); + })), + ) + .into_any_element() + } + + fn channel_row( + &self, + id: ChannelId, + name: String, + private: bool, + selected: bool, + community: &Entity, + cx: &mut Context, + ) -> AnyElement { + let community = community.clone(); + + div() + .flex_shrink_0() + .rounded(cx.theme().radius) + .child( + TreeRow::new( + ElementId::Name(SharedString::from(format!( + "community-channel-{}", + id.to_hex() + ))), + TreeRowKind::Room, + name, + ) + .icon(if private { + IconName::Lock + } else { + IconName::Message }) - .count(count) - .on_click(cx.listener(move |this, _event, _window, cx| { - toggle(this); + .on_click(cx.listener(move |_this, _event, _window, cx| { + community.update(cx, |community, cx| community.set_active_channel(id, cx)); cx.notify(); })), - ) - .into_any_element() + ) + .when(selected, |this| this.bg(cx.theme().ghost_element_active)) + .into_any_element() + } + + fn member_row(&self, prefix: &str, public_key: PublicKey, cx: &App) -> AnyElement { + let persons = PersonRegistry::global(cx); + let person = persons.read(cx).get(&public_key, cx); + + div() + .flex_shrink_0() + .child( + TreeRow::new( + ElementId::Name(SharedString::from(format!( + "{prefix}-{}", + public_key.to_hex() + ))), + TreeRowKind::Room, + person.name(), + ) + .avatar(person.avatar_seed()) + .picture(person.avatar()), + ) + .into_any_element() + } } -fn channel_row( - id: ChannelId, - name: String, - private: bool, - selected: bool, - community: &Entity, - cx: &mut Context, -) -> AnyElement { - let community = community.clone(); +impl Panel for Sidebar { + fn panel_id(&self) -> SharedString { + "Sidebar".into() + } - div() - .flex_shrink_0() - .rounded(cx.theme().radius) - .child( - TreeRow::new( - ElementId::Name(SharedString::from(format!( - "community-channel-{}", - id.to_hex() - ))), - TreeRowKind::Room, - name, - ) - .icon(if private { - IconName::Lock - } else { - IconName::Message + fn title(&self, _cx: &App) -> AnyElement { + SharedString::from("Sidebar").into_any_element() + } + + fn closable(&self, _cx: &App) -> bool { + false + } + + fn zoomable(&self, _cx: &App) -> bool { + false + } +} + +impl EventEmitter for Sidebar {} + +impl Focusable for Sidebar { + fn focus_handle(&self, _: &App) -> gpui::FocusHandle { + self.focus_handle.clone() + } +} + +impl Render for Sidebar { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let nostr = NostrRegistry::global(cx); + let logged_in = { nostr.read(cx).current_user().is_some() }; + + let community = self + .community + .clone() + .and_then(|community| community.upgrade()); + + v_flex() + .image_cache(retain_all("sidebar")) + .size_full() + .relative() + .gap_2() + .bg(cx.theme().surface_background) + .border_r_1() + .border_color(cx.theme().border_variant) + .when(!logged_in, |this| { + this.relative() + .child(title_bar_drag_handlers( + div() + .id("onboarding-drag") + .absolute() + .top_0() + .left_0() + .h(TABBAR_HEIGHT) + .w_full(), + window, + cx, + )) + .child( + v_flex() + .size_full() + .justify_end() + .gap_4() + .p_4() + .child( + v_flex().child(div().font_semibold().child("Coop")).child( + div() + .text_xs() + .text_color(cx.theme().text_muted) + .child("TODO"), + ), + ) + .child( + Button::new("import-identity") + .label("Import identity") + .primary() + .font_semibold() + .h_8() + .w_full() + .on_click(|_, window, cx| { + import::open(window, cx); + }), + ), + ) }) - .on_click(cx.listener(move |_this, _event, _window, cx| { - community.update(cx, |community, cx| community.set_active_channel(id, cx)); - cx.notify(); - })), - ) - .when(selected, |this| this.bg(cx.theme().ghost_element_active)) - .into_any_element() -} - -fn member_row(prefix: &str, public_key: PublicKey, cx: &App) -> AnyElement { - let persons = PersonRegistry::global(cx); - let person = persons.read(cx).get(&public_key, cx); - - div() - .flex_shrink_0() - .child( - TreeRow::new( - ElementId::Name(SharedString::from(format!( - "{prefix}-{}", - public_key.to_hex() - ))), - TreeRowKind::Room, - person.name(), - ) - .avatar(person.avatar_seed()) - .picture(person.avatar()), - ) - .into_any_element() + .child(title_bar_drag_handlers(self.render_user(cx), window, cx)) + .map(|this| match community { + Some(community) => this.child(self.render_community(community, cx)), + None => this.child(self.render_tabs(cx)), + }) + .into_any_element() + } } diff --git a/crates/workspace/src/sidebar/onboarding.rs b/crates/workspace/src/sidebar/onboarding.rs deleted file mode 100644 index abf2d9d4..00000000 --- a/crates/workspace/src/sidebar/onboarding.rs +++ /dev/null @@ -1,74 +0,0 @@ -use gpui::{App, InteractiveElement, IntoElement, ParentElement, Styled, Window, div, svg}; -use theme::{ActiveTheme, TABBAR_HEIGHT}; -use ui::button::{Button, ButtonVariants}; -use ui::{StyledExt, h_flex, title_bar_drag_handlers, v_flex}; - -use crate::dialogs::import; - -const TITLE: &str = "Welcome to Coop!"; -const DESCRIPTION: &str = "Chat Freely, Stay Private on Nostr."; - -pub(super) fn render(window: &mut Window, cx: &mut App) -> impl IntoElement { - v_flex() - .size_full() - .relative() - .bg(cx.theme().surface_background) - .child(title_bar_drag_handlers( - div() - .id("onboarding-drag") - .absolute() - .top_0() - .left_0() - .h(TABBAR_HEIGHT) - .w_full(), - window, - cx, - )) - .child( - v_flex() - .size_full() - .justify_end() - .gap_4() - .p_4() - .child( - h_flex() - .gap_2() - .child( - svg() - .path("brand/coop.svg") - .size_8() - .text_color(cx.theme().icon_muted), - ) - .child( - v_flex().child(div().font_semibold().child(TITLE)).child( - div() - .text_xs() - .text_color(cx.theme().text_muted) - .child(DESCRIPTION), - ), - ), - ) - .child( - v_flex() - .gap_2() - .w_full() - .child( - Button::new("join-now") - .label("Join now") - .primary() - .font_semibold() - .h_8() - .w_full(), - ) - .child( - Button::new("import-identity") - .label("Import identity") - .secondary() - .font_semibold() - .h_8() - .w_full() - .on_click(|_event, window, cx| import::open(window, cx)), - ), - ), - ) -} -- 2.54.0 From c61be4a1390e3e7d541b2a78e1156c2b25605ecc Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Wed, 23 Sep 2026 20:19:42 +0700 Subject: [PATCH 4/9] update sidebar --- crates/auto_update/src/lib.rs | 7 +- crates/ui/src/tab/mod.rs | 143 ++++-- crates/ui/src/tab/tab_bar.rs | 22 +- crates/workspace/src/sidebar/mod.rs | 739 ++++++++++++--------------- crates/workspace/src/sidebar/tab.rs | 103 +--- crates/workspace/src/sidebar/tree.rs | 67 ++- 6 files changed, 485 insertions(+), 596 deletions(-) diff --git a/crates/auto_update/src/lib.rs b/crates/auto_update/src/lib.rs index 92b4c63d..bf094871 100644 --- a/crates/auto_update/src/lib.rs +++ b/crates/auto_update/src/lib.rs @@ -31,7 +31,7 @@ fn uses_managed_updates() -> bool { // Allow opting out of in-app updates via an explicit environment variable. || std::env::var(COOP_UPDATE_EXPLANATION).is_ok() // The Snap package sets `COOP_BUNDLE_TYPE=snap` (see snapcraft.yaml.in). - || std::env::var(COOP_BUNDLE_TYPE).is_ok_and(|value| value == "snap") + || std::env::var(COOP_BUNDLE_TYPE).is_ok_and(|value | value == "snap") } /// Initialize the auto-update system. @@ -129,6 +129,11 @@ impl AutoUpdater { matches!(self.status, UpdateStatus::Idle) } + /// Whether the running version is the newest release, so the status line can be hidden. + pub fn up_to_date(&self) -> bool { + matches!(self.status, UpdateStatus::Idle | UpdateStatus::UpToDate) + } + /// Whether a verified update is installed and waiting for a restart. pub fn staged(&self) -> bool { matches!(self.status, UpdateStatus::Staged(_)) diff --git a/crates/ui/src/tab/mod.rs b/crates/ui/src/tab/mod.rs index 812927df..c76a9ac3 100644 --- a/crates/ui/src/tab/mod.rs +++ b/crates/ui/src/tab/mod.rs @@ -25,6 +25,7 @@ pub struct Tab { children: Vec, pub(super) disabled: bool, pub(super) selected: bool, + pub(super) segmented: bool, on_click: Option>, } @@ -69,6 +70,7 @@ impl Default for Tab { children: Vec::new(), disabled: false, selected: false, + segmented: false, prefix: None, suffix: None, on_click: None, @@ -132,6 +134,12 @@ impl Tab { self.tab_bar_prefix = Some(tab_bar_prefix); self } + + /// Render the tab as a segment inside a segmented control. + pub(crate) fn segmented(mut self, segmented: bool) -> Self { + self.segmented = segmented; + self + } } impl ParentElement for Tab { @@ -167,74 +175,111 @@ impl Styled for Tab { impl RenderOnce for Tab { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { - let fg = if self.disabled { + let Self { + ix, + base, + label, + icon, + prefix, + suffix, + children, + disabled, + selected, + segmented, + on_click, + .. + } = self; + + let fg = if disabled { cx.theme().text_muted - } else if self.selected { + } else if selected { cx.theme().tab_active_foreground } else { cx.theme().tab_foreground }; - self.base - .id(self.ix) - .flex() - .flex_wrap() - .gap_1() + let content = h_flex() + .flex_1() + .map(|this| { + if segmented { + this.h(px(24.)) + } else { + this.h(px(30.)) + } + }) + .line_height(relative(1.)) + .whitespace_nowrap() .items_center() - .flex_shrink_0() - .h(TABBAR_HEIGHT) - .relative() + .justify_center() .overflow_hidden() + .when(segmented, |this| this.px_1()) + .when(!segmented, |this| this.flex_shrink_0().px_3()) + .map(|this| match icon { + Some(icon) => this.w(px(38.)).child(icon.size_4()), + None => this + .map(|this| match label { + Some(label) => this.child(label), + None => this, + }) + .children(children), + }); + + base.id(ix) + .flex() + .items_center() .text_color(fg) - .text_sm() - .when_some(self.prefix, |this, prefix| this.child(prefix)) - .child( - h_flex() + .when(segmented, |this| { + this.text_xs() .flex_1() - .h(px(30.)) - .line_height(relative(1.)) - .whitespace_nowrap() - .items_center() - .justify_center() - .overflow_hidden() + .h(px(24.)) + .rounded(cx.theme().radius) + .when(selected && !disabled, |this| { + this.bg(cx.theme().tab_active_background) + .when(cx.theme().shadow, |this| this.shadow_sm()) + }) + .when(!selected && !disabled, |this| { + this.hover(|this| this.bg(cx.theme().tab_hover_background)) + }) + }) + .when(!segmented, |this| { + this.text_sm() + .flex_wrap() + .gap_1() .flex_shrink_0() - .px_3() - .map(|this| match self.icon { - Some(icon) => this.w(px(38.)).child(icon.size_4()), - None => this - .map(|this| match self.label { - Some(label) => this.child(label), - None => this, - }) - .children(self.children), - }), - ) - .when_some(self.suffix, |this, suffix| { + .h(TABBAR_HEIGHT) + .relative() + .overflow_hidden() + }) + .when_some(prefix, |this, prefix| this.child(prefix)) + .child(content) + .when_some(suffix, |this, suffix| { this.child(div().pr_2().child(suffix)) }) .on_mouse_down(MouseButton::Left, |_ev, _window, cx| { cx.stop_propagation(); }) - .when(!self.disabled, |this| { - this.when_some(self.on_click.clone(), |this, on_click| { + .when(!disabled, |this| { + this.when_some(on_click, |this, on_click| { this.on_click(move |event, window, cx| on_click(event, window, cx)) }) }) - .child( - div() - .absolute() - .bottom_0() - .left_0() - .right_0() - .h_0p5() - .when(self.selected && !self.disabled, |this| { - this.bg(cx.theme().element_active) - }) - .when(!self.selected && !self.disabled, |this| { - this.invisible().group_hover("", |this| { - this.visible().bg(cx.theme().secondary_background) + .when(!segmented, |this| { + this.child( + div() + .absolute() + .bottom_0() + .left_0() + .right_0() + .h_0p5() + .when(selected && !disabled, |this| { + this.bg(cx.theme().element_active) }) - }), - ) + .when(!selected && !disabled, |this| { + this.invisible().group_hover("", |this| { + this.visible().bg(cx.theme().secondary_background) + }) + }), + ) + }) } } diff --git a/crates/ui/src/tab/tab_bar.rs b/crates/ui/src/tab/tab_bar.rs index 11a6463e..b86863e5 100644 --- a/crates/ui/src/tab/tab_bar.rs +++ b/crates/ui/src/tab/tab_bar.rs @@ -7,6 +7,7 @@ use gpui::{ Window, div, px, }; use smallvec::SmallVec; +use theme::ActiveTheme; use super::Tab; use crate::button::{Button, ButtonVariants as _}; @@ -25,6 +26,7 @@ pub struct TabBar { last_empty_space: AnyElement, selected_index: Option, menu: bool, + segmented: bool, #[allow(clippy::type_complexity)] on_click: Option>, } @@ -43,9 +45,16 @@ impl TabBar { selected_index: None, on_click: None, menu: false, + segmented: false, } } + /// Render the tabs as a segmented control inside a pill-shaped track. + pub fn segmented(mut self, segmented: bool) -> Self { + self.segmented = segmented; + self + } + /// Set whether to show the menu button when tabs overflow, default is false. pub fn menu(mut self, menu: bool) -> Self { self.menu = menu; @@ -113,10 +122,11 @@ impl Styled for TabBar { } impl RenderOnce for TabBar { - fn render(self, _: &mut Window, _cx: &mut App) -> impl IntoElement { + fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { let mut item_labels = Vec::new(); let selected_index = self.selected_index; let on_click = self.on_click.clone(); + let segmented = self.segmented; self.base .group("tab-bar") @@ -124,22 +134,28 @@ impl RenderOnce for TabBar { .flex() .items_center() .refine_style(&self.style) + .when(segmented, |this| { + this.bg(cx.theme().tab_background) + .p_0p5() + .rounded(cx.theme().radius) + }) .when_some(self.prefix, |this, prefix| this.child(prefix)) .child( h_flex() .id("tabs") .flex_1() - .overflow_x_scroll() + .when(!segmented, |this| this.overflow_x_scroll()) .when_some(self.scroll_handle, |this, scroll_handle| { this.track_scroll(&scroll_handle) }) - .gap(px(0.)) + .gap_1() .children(self.children.into_iter().enumerate().map(|(ix, child)| { item_labels.push((child.label.clone(), child.disabled)); let tab_bar_prefix = child.tab_bar_prefix.unwrap_or(true); child .ix(ix) .tab_bar_prefix(tab_bar_prefix) + .segmented(segmented) .when_some(self.selected_index, |this, selected_ix| { this.selected(selected_ix == ix) }) diff --git a/crates/workspace/src/sidebar/mod.rs b/crates/workspace/src/sidebar/mod.rs index ef937d44..81139445 100644 --- a/crates/workspace/src/sidebar/mod.rs +++ b/crates/workspace/src/sidebar/mod.rs @@ -2,19 +2,18 @@ use std::ops::Range; use std::rc::Rc; use auto_update::AutoUpdater; -use chat::{ChatEvent, ChatRegistry, Room, RoomKind}; +use chat::{ChatEvent, ChatRegistry, RoomKind}; use common::TimestampExt; use community::{ChannelId, Community, CommunityEvent, CommunityRegistry}; use gpui::prelude::FluentBuilder; use gpui::{ AnyElement, App, Context, Div, ElementId, Entity, EventEmitter, FocusHandle, Focusable, - InteractiveElement, IntoElement, ObjectFit, ParentElement, Render, ScrollHandle, SharedString, - Stateful, StatefulInteractiveElement, Styled, StyledImage, Subscription, - UniformListScrollHandle, WeakEntity, Window, div, img, px, retain_all, uniform_list, + InteractiveElement, IntoElement, ObjectFit, ParentElement, Render, SharedString, Stateful, + Styled, StyledImage, Subscription, UniformListScrollHandle, WeakEntity, Window, div, img, px, + retain_all, uniform_list, }; use nostr_sdk::prelude::*; use person::PersonRegistry; -use settings::AppSettings; use smallvec::{SmallVec, smallvec}; use state::NostrRegistry; use theme::{ActiveTheme, TABBAR_HEIGHT}; @@ -26,9 +25,11 @@ use ui::menu::{DropdownMenu, PopupMenuItem}; use ui::nav_item::NavItem; use ui::notification::Notification; use ui::scroll::Scrollbar; +use ui::tab::Tab; +use ui::tab::tab_bar::TabBar; use ui::{ - Icon, IconName, Sizable, StyledExt, TRAFFIC_LIGHT_PADDING, WindowExtension, h_flex, - title_bar_drag_handlers, v_flex, + Disableable, Icon, IconName, Sizable, StyledExt, TRAFFIC_LIGHT_PADDING, WindowExtension, + h_flex, title_bar_drag_handlers, v_flex, }; use crate::Command; @@ -37,15 +38,15 @@ use crate::dialogs::import; mod tab; mod tree; -use tab::{SidebarTab, TabBar}; -use tree::SidebarRow; +use tab::SidebarTab; +use tree::{CommunityRow, CommunitySection, SidebarRow}; pub(crate) use tree::{TreeRow, TreeRowKind}; pub struct Sidebar { focus_handle: FocusHandle, - scroll_handles: [UniformListScrollHandle; 3], - /// Scroll state of the channel and member lists - community_scroll: ScrollHandle, + scroll_handles: [UniformListScrollHandle; 2], + /// Scroll state of the community's channel and member lists + community_scroll: UniformListScrollHandle, /// The dock the sidebar opens its panels in dock: WeakEntity, active_tab: SidebarTab, @@ -89,11 +90,10 @@ impl Sidebar { scroll_handles: [ UniformListScrollHandle::new(), UniformListScrollHandle::new(), - UniformListScrollHandle::new(), ], - community_scroll: ScrollHandle::default(), + community_scroll: UniformListScrollHandle::new(), dock, - active_tab: SidebarTab::Recents, + active_tab: SidebarTab::Inbox, community: None, channels_open: true, admins_open: true, @@ -111,42 +111,6 @@ impl Sidebar { cx.notify(); } - fn open_community( - &mut self, - community: Entity, - window: &mut Window, - cx: &mut Context, - ) { - let id = community.read(cx).id().to_hex(); - let settings = AppSettings::global(cx); - - settings.update(cx, |settings, cx| { - settings.record_recent_community(id, cx); - }); - - self.community = Some(community.downgrade()); - - ui::dock::add_panel_to( - &self.dock, - PanelHandle::new(community_ui::init(community, window, cx)), - DockPlacement::Center, - window, - cx, - ); - - cx.notify(); - } - - fn open_room(&mut self, room: Entity, window: &mut Window, cx: &mut Context) { - ui::dock::add_panel_to( - &self.dock, - PanelHandle::new(chat_ui::init(room.downgrade(), window, cx)), - DockPlacement::Center, - window, - cx, - ); - } - /// Leave the community view, returning the sidebar to its tab list. fn reset_community(&mut self, cx: &mut Context) { if self.community.take().is_none() { @@ -155,9 +119,12 @@ impl Sidebar { cx.notify(); } - fn render_user(&self, cx: &mut Context) -> Stateful
{ - let nostr = NostrRegistry::global(cx); - let current_user = nostr.read(cx).current_user(); + fn render_user(&self, current_user: &PublicKey, cx: &mut Context) -> Stateful
{ + let persons = PersonRegistry::global(cx); + let profile = persons.read(cx).get(current_user, cx); + let avatar = profile.avatar(); + let avatar_seed = profile.avatar_seed(); + let name = profile.name(); h_flex() .id("sidebar-user") @@ -170,88 +137,66 @@ impl Sidebar { .when(cfg!(target_os = "macos"), |this| { this.pl(px(TRAFFIC_LIGHT_PADDING)) }) - .when_some(current_user.as_ref(), |this, public_key| { - let persons = PersonRegistry::global(cx); - let profile = persons.read(cx).get(public_key, cx); - let avatar = profile.avatar(); - let avatar_seed = profile.avatar_seed(); - let name = profile.name(); + .child( + Button::new("current-user") + .child( + Avatar::new(avatar.clone()) + .seed(avatar_seed.clone()) + .xsmall(), + ) + .small() + .caret() + .compact() + .transparent() + .dropdown_menu(move |this, _window, _cx| { + let avatar = avatar.clone(); + let avatar_seed = avatar_seed.clone(); + let name = name.clone(); - this.child( - Button::new("current-user") - .child( - Avatar::new(avatar.clone()) - .seed(avatar_seed.clone()) - .xsmall(), - ) - .small() - .caret() - .compact() - .transparent() - .dropdown_menu(move |this, _window, cx| { - let avatar = avatar.clone(); - let avatar_seed = avatar_seed.clone(); - let name = name.clone(); - - this.min_w(px(256.)) - .item(PopupMenuItem::element(move |_window, cx| { - h_flex() - .gap_1p5() - .text_xs() - .text_color(cx.theme().text_muted) - .child( - Avatar::new(avatar.clone()) - .seed(avatar_seed.clone()) - .xsmall(), - ) - .child(name.clone()) - })) - .separator() - .menu_with_icon( - "Inbox", - IconName::Inbox, - Box::new(Command::ShowInbox), - ) - .menu_with_icon( - "Search", - IconName::Search, - Box::new(Command::ShowSearch), - ) - .menu_with_icon( - "Profile", - IconName::Profile, - Box::new(Command::ShowProfile), - ) - .menu_with_icon( - "Contact List", - IconName::Book, - Box::new(Command::ShowContactList), - ) - .menu_with_icon( - "Backup", - IconName::UserKey, - Box::new(Command::ShowBackup), - ) - .menu_with_icon( - "Themes", - IconName::Sun, - Box::new(Command::ToggleTheme), - ) - .when(AutoUpdater::is_available(cx), |this| { - this.separator().menu_with_icon( - "Check for Updates", - IconName::Device, - Box::new(Command::Update), + this.min_w(px(256.)) + .item(PopupMenuItem::element(move |_window, cx| { + h_flex() + .gap_1p5() + .text_xs() + .text_color(cx.theme().text_muted) + .child( + Avatar::new(avatar.clone()) + .seed(avatar_seed.clone()) + .xsmall(), ) - }) - .menu_with_icon( - "Settings", - IconName::Settings, - Box::new(Command::ShowSettings), - ) - }), - ) - }) + .child(name.clone()) + })) + .separator() + .menu_with_icon("Inbox", IconName::Inbox, Box::new(Command::ShowInbox)) + .menu_with_icon( + "Search", + IconName::Search, + Box::new(Command::ShowSearch), + ) + .menu_with_icon( + "Profile", + IconName::Profile, + Box::new(Command::ShowProfile), + ) + .menu_with_icon( + "Contact List", + IconName::Book, + Box::new(Command::ShowContactList), + ) + .menu_with_icon( + "Backup", + IconName::UserKey, + Box::new(Command::ShowBackup), + ) + .menu_with_icon("Themes", IconName::Sun, Box::new(Command::ToggleTheme)) + .separator() + .menu_with_icon( + "Settings", + IconName::Settings, + Box::new(Command::ShowSettings), + ) + }), + ) .child(div().flex_1()) .when_some(AutoUpdater::try_global(cx), |this, updater| { this.child(self.render_updater(updater, cx)) @@ -272,12 +217,21 @@ impl Sidebar { fn render_updater(&self, updater: Entity, cx: &mut App) -> AnyElement { let status = updater.read(cx).status(); + let up_to_date = updater.read(cx).up_to_date(); let staged = updater.read(cx).staged(); h_flex() .gap_2() - .text_xs() - .child(status) + .when(!up_to_date, |this| { + this.child( + Button::new("update-status") + .icon(IconName::ArrowDownCircle) + .tooltip(status) + .small() + .warning() + .disabled(true), + ) + }) .when(staged, |this| { this.child( Button::new("restart-to-update") @@ -296,9 +250,6 @@ impl Sidebar { } fn render_tabs(&mut self, cx: &mut Context) -> AnyElement { - let chat = ChatRegistry::global(cx); - let loading = chat.read(cx).loading(); - let sidebar = cx.entity().downgrade(); let active_tab = self.active_tab; let rows = Rc::new(self.rows_for(active_tab, cx)); @@ -308,90 +259,95 @@ impl Sidebar { .size_full() .flex_1() .min_h_0() - .gap_1() - .when(active_tab.chat(), |this| { - this.child( + .gap_2() + .child( + div().px_2().child( + TabBar::new("sidebar-tabs") + .segmented(true) + .selected_index(active_tab.index()) + .child(Tab::new().label(SidebarTab::Inbox.label())) + .child(Tab::new().label(SidebarTab::Communities.label())) + .on_click({ + let sidebar = sidebar.clone(); + move |index, _window, cx| { + let Some(tab) = SidebarTab::ALL.get(*index).copied() else { + return; + }; + if let Err(error) = + sidebar.update(cx, |this, cx| this.select_tab(tab, cx)) + { + log::error!("Failed to switch sidebar tab: {error}"); + } + } + }), + ), + ) + .map(|this| match active_tab { + SidebarTab::Inbox => this.child( v_flex() .px_2() .gap_1() .child( NavItem::new( - "nav-contacts", - "Contacts", - Icon::new(IconName::Book).small(), - ) - .on_click(|_event, window, cx| { - window.dispatch_action(Box::new(Command::ShowContactList), cx) - }), - ) - .child( - NavItem::new( - "nav-requests", - "Requests", - Icon::new(IconName::Invite).small(), - ) - .when(self.new_requests, |this| { - this.suffix(div().size_1().rounded_full().bg(cx.theme().cursor)) - }) - .on_click({ - let sidebar = sidebar.clone(); - move |_event, window, cx| { - if let Err(error) = sidebar.update(cx, |this, cx| { - this.new_requests = false; - cx.notify(); - }) { - log::error!("Failed to clear new requests: {error}"); - } - window.dispatch_action(Box::new(Command::ShowRequests), cx); - } - }), - ) - .child( - NavItem::new( - "nav-new-chat", - "New chat", - Icon::new(IconName::Plus).small(), + "new-chat", + "New Chat", + Icon::new(IconName::Message).small(), ) .on_click(|_event, window, cx| { window.dispatch_action(Box::new(Command::NewChat), cx) }), + ) + .child( + NavItem::new("reqs", "Requests", Icon::new(IconName::Invite).small()) + .when(self.new_requests, |this| { + this.suffix(div().size_1().rounded_full().bg(cx.theme().cursor)) + }) + .on_click({ + let sidebar = sidebar.clone(); + move |_event, window, cx| { + if let Err(error) = sidebar.update(cx, |this, cx| { + this.new_requests = false; + cx.notify(); + }) { + log::error!("Failed to clear new requests: {error}"); + } + window.dispatch_action(Box::new(Command::ShowRequests), cx); + } + }), + ) + .child( + NavItem::new("contacts", "Contacts", Icon::new(IconName::Book).small()) + .on_click(|_event, window, cx| { + window.dispatch_action(Box::new(Command::ShowContactList), cx) + }), ), - ) - }) - .when(active_tab.community(), |this| { - this.child( + ), + SidebarTab::Communities => this.child( v_flex() .px_2() .gap_1() .child( NavItem::new( - "nav-browse", - "Browse", - Icon::new(IconName::Compass).small(), + "new-community", + "New Community", + Icon::new(IconName::Group).small(), ) - .on_click(|_event, window, cx| { - window.dispatch_action(Box::new(Command::ShowBrowse), cx) + .on_click(|_, window, cx| { + window.dispatch_action(Box::new(Command::NewCommunity), cx) }), ) .child( - NavItem::new( - "nav-new-community", - "New community", - Icon::new(IconName::Plus).small(), - ) - .on_click(|_event, window, cx| { - window.dispatch_action(Box::new(Command::NewCommunity), cx) - }), + NavItem::new("browse", "Browse", Icon::new(IconName::Compass).small()) + .on_click(|_, window, cx| { + window.dispatch_action(Box::new(Command::ShowBrowse), cx) + }), ), - ) + ), }) .child( - v_flex() - .size_full() - .flex_1() + div() .min_h_0() - .gap_1() - .pb_12() + .flex_1() .child( uniform_list( active_tab.list_id(), @@ -401,46 +357,11 @@ impl Sidebar { }), ) .track_scroll(scroll_handle) - .flex_1() .h_full() .px_2(), ) .child(Scrollbar::vertical(scroll_handle)), ) - .child(TabBar::new(active_tab).on_select({ - let sidebar = sidebar.clone(); - move |tab, _window, cx| { - if let Err(error) = sidebar.update(cx, |this, cx| this.select_tab(tab, cx)) { - log::error!("Failed to switch sidebar tab: {error}"); - } - } - })) - .when(loading, |this| { - this.child( - div() - .absolute() - .bottom_16() - .left_0() - .h_9() - .w_full() - .px_8() - .child( - h_flex() - .gap_2() - .w_full() - .h_9() - .justify_center() - .bg(cx.theme().background.opacity(0.85)) - .when(cx.theme().shadow, |this| this.shadow_md()) - .rounded_full() - .text_xs() - .font_semibold() - .text_color(cx.theme().text_muted) - .child(Indicator::new().small().color(cx.theme().icon_accent)) - .child("Getting messages..."), - ), - ) - }) .into_any_element() } @@ -449,7 +370,7 @@ impl Sidebar { community: Entity, cx: &mut Context, ) -> AnyElement { - let (channels, active, admins, members, banner) = { + let (banner, rows) = { let community = community.read(cx); let owner = community.state().owner; let mut admins = Vec::new(); @@ -463,71 +384,45 @@ impl Sidebar { } } - ( - community - .channels() - .iter() - .map(|channel| (channel.id, channel.name.clone(), channel.private)) - .collect::>(), - community.active_channel(), - admins, - members, - community.banner(), - ) + let active = community.active_channel(); + let banner = community.banner(); + + let mut rows = vec![CommunityRow::Section(CommunitySection::Channels)]; + if self.channels_open { + rows.extend( + community + .channels() + .iter() + .map(|channel| CommunityRow::Channel { + id: channel.id, + name: channel.name.clone().into(), + private: channel.private, + selected: active == Some(channel.id), + }), + ); + } + + rows.push(CommunityRow::Section(CommunitySection::Admins)); + if self.admins_open { + rows.extend(admins.into_iter().map(|public_key| CommunityRow::Member { + prefix: "community-admin", + public_key, + })); + } + + rows.push(CommunityRow::Section(CommunitySection::Members)); + if self.members_open { + rows.extend(members.into_iter().map(|public_key| CommunityRow::Member { + prefix: "member", + public_key, + })); + } + + (banner, rows) }; - let sections = v_flex() - .id("community-sections") - .flex_1() - .min_h_0() - .w_full() - .px_2() - .pb_2() - .track_scroll(&self.community_scroll) - .overflow_y_scroll() - .child(self.section_row( - "channels", - "Channels", - channels.len(), - self.channels_open, - |sidebar| sidebar.channels_open = !sidebar.channels_open, - cx, - )) - .when(self.channels_open, |this| { - this.children(channels.into_iter().map(|(id, name, private)| { - self.channel_row(id, name, private, active == Some(id), &community, cx) - })) - }) - .child(self.section_row( - "admins", - "Admins", - admins.len(), - self.admins_open, - |sidebar| sidebar.admins_open = !sidebar.admins_open, - cx, - )) - .when(self.admins_open, |this| { - this.children( - admins - .iter() - .map(|public_key| self.member_row("community-admin", *public_key, cx)), - ) - }) - .child(self.section_row( - "members", - "Members", - members.len(), - self.members_open, - |sidebar| sidebar.members_open = !sidebar.members_open, - cx, - )) - .when(self.members_open, |this| { - this.children( - members - .iter() - .map(|public_key| self.member_row("member", *public_key, cx)), - ) - }); + let rows = Rc::new(rows); + let scroll_handle = &self.community_scroll; v_flex() .flex_1() @@ -539,110 +434,94 @@ impl Sidebar { div().px_2().child( img(banner) .w_full() - .h(px(80.)) - .rounded(cx.theme().radius) + .h_20() + .rounded(cx.theme().radius_lg) .object_fit(ObjectFit::Cover), ), ) }) - .child(sections) - .child(Scrollbar::vertical(&self.community_scroll)) + .child( + div() + .min_h_0() + .flex_1() + .child( + uniform_list( + "community-rows", + rows.len(), + cx.processor(move |this, range, _window, cx| { + this.render_community_rows(range, rows.as_slice(), &community, cx) + }), + ) + .track_scroll(scroll_handle) + .h_full() + .px_2(), + ) + .child(Scrollbar::vertical(scroll_handle)), + ) .into_any_element() } - fn recent_communities(&self, cx: &App) -> Vec> { - const LIMIT: usize = 3; - - let registry = CommunityRegistry::global(cx); - let communities = registry.read(cx).communities(); - let recent = AppSettings::get_recent_communities(cx); - - let mut rows: Vec> = recent - .iter() - .filter_map(|id| { - communities - .iter() - .find(|community| community.read(cx).id().to_hex() == *id) + fn render_community_rows( + &self, + range: Range, + rows: &[CommunityRow], + community: &Entity, + cx: &mut Context, + ) -> Vec { + rows.get(range) + .into_iter() + .flatten() + .map(|row| match row { + CommunityRow::Section(CommunitySection::Channels) => self.section_row( + "channels", + "Channels", + self.channels_open, + |sidebar| sidebar.channels_open = !sidebar.channels_open, + cx, + ), + CommunityRow::Section(CommunitySection::Admins) => self.section_row( + "admins", + "Admins", + self.admins_open, + |sidebar| sidebar.admins_open = !sidebar.admins_open, + cx, + ), + CommunityRow::Section(CommunitySection::Members) => self.section_row( + "members", + "Members", + self.members_open, + |sidebar| sidebar.members_open = !sidebar.members_open, + cx, + ), + CommunityRow::Channel { + id, + name, + private, + selected, + } => self.channel_row(*id, name.clone(), *private, *selected, community, cx), + CommunityRow::Member { prefix, public_key } => { + self.member_row(prefix, *public_key, cx) + } }) - .take(LIMIT) - .cloned() - .collect(); - - if rows.is_empty() { - rows = communities.iter().take(LIMIT).cloned().collect(); - } - - rows + .collect() } fn rows_for(&self, tab: SidebarTab, cx: &App) -> Vec { match tab { - SidebarTab::Recents => { + SidebarTab::Inbox => { let chat = ChatRegistry::global(cx); let rooms = chat.read(cx).rooms(&RoomKind::Ongoing, cx); - let registry = CommunityRegistry::global(cx); - let community_count = registry.read(cx).communities().len(); - let communities = self.recent_communities(cx); - - if communities.is_empty() && rooms.is_empty() { - return vec![SidebarRow::Hint { - text: "Nothing recent yet".into(), - }]; - } - - let mut rows = Vec::new(); - - if !communities.is_empty() { - rows.push(SidebarRow::Section { - label: "Communities".into(), - count: community_count, - }); - rows.extend( - communities - .into_iter() - .map(|community| SidebarRow::Community { community }), - ); - rows.push(SidebarRow::Action { - label: "Show all communities".into(), - tab: SidebarTab::Communities, - }); - } - - if !rooms.is_empty() { - rows.push(SidebarRow::Section { - label: "Chats".into(), - count: rooms.len(), - }); - rows.extend( - rooms - .into_iter() - .take(5) - .map(|room| SidebarRow::Room { room }), - ); - rows.push(SidebarRow::Action { - label: "Show all chats".into(), - tab: SidebarTab::Chats, - }); - } - - rows - } - SidebarTab::Chats => { - let chat = ChatRegistry::global(cx); - let chat = chat.read(cx); - let messages = chat.rooms(&RoomKind::Ongoing, cx); let mut rows = vec![SidebarRow::Section { label: "Chats".into(), - count: messages.len(), }]; - if messages.is_empty() { + if rooms.is_empty() { rows.push(SidebarRow::Hint { text: "No conversations yet".into(), }); } else { - rows.extend(messages.into_iter().map(|room| SidebarRow::Room { room })); + rows.extend(rooms.into_iter().map(|room| SidebarRow::Room { room })); } rows @@ -653,7 +532,6 @@ impl Sidebar { let mut rows = vec![SidebarRow::Section { label: "Communities".into(), - count: communities.len(), }]; if communities.is_empty() { @@ -688,23 +566,19 @@ impl Sidebar { let index = range.start + offset; match row { - SidebarRow::Section { label, count } => TreeRow::new( + SidebarRow::Section { label } => TreeRow::new( ElementId::NamedInteger("tree-row".into(), index as u64), TreeRowKind::Section, label.clone(), ) - .count(*count) .into_any_element(), SidebarRow::Room { room } => { let name = room.read(cx).display_name(cx); let picture = room.read(cx).display_image(cx); let seed = room.read(cx).display_image_seed(cx); let created_at = room.read(cx).created_at.to_ago(); - let room_clone = room.clone(); - - let handler = cx.listener(move |this, _event, window, cx| { - this.open_room(room_clone.clone(), window, cx); - }); + let dock = self.dock.clone(); + let room = room.clone(); TreeRow::new( ElementId::NamedInteger("tree-row".into(), index as u64), @@ -714,13 +588,25 @@ impl Sidebar { .avatar(seed) .picture(picture) .created_at(created_at) - .on_click(handler) + // Run in an app context: docking a new panel reads every + // docked panel's id, which would panic inside a sidebar update. + .on_click(move |_event, window, cx| { + ui::dock::add_panel_to( + &dock, + PanelHandle::new(chat_ui::init(room.downgrade(), window, cx)), + DockPlacement::Center, + window, + cx, + ); + }) .into_any_element() } SidebarRow::Community { community } => { let name = community.read(cx).name(); let seed = community.read(cx).id().to_hex(); let picture = community.read(cx).icon(); + let dock = self.dock.clone(); + let sidebar = cx.entity().downgrade(); let community = community.clone(); TreeRow::new( @@ -730,23 +616,22 @@ impl Sidebar { ) .avatar(seed) .picture(picture) - .on_click(cx.listener(move |this, _event, window, cx| { - this.open_community(community.clone(), window, cx); - })) - .into_any_element() - } - SidebarRow::Action { label, tab } => { - let tab = *tab; + .on_click(move |_event, window, cx| { + ui::dock::add_panel_to( + &dock, + PanelHandle::new(community_ui::init(community.clone(), window, cx)), + DockPlacement::Center, + window, + cx, + ); - TreeRow::new( - ElementId::NamedInteger("tree-row".into(), index as u64), - TreeRowKind::Action, - label.clone(), - ) - .icon(IconName::ArrowRight) - .on_click(cx.listener(move |this, _event, _window, cx| { - this.select_tab(tab, cx); - })) + if let Err(error) = sidebar.update(cx, |this, cx| { + this.community = Some(community.downgrade()); + cx.notify(); + }) { + log::error!("Failed to show community in sidebar: {error}"); + } + }) .into_any_element() } SidebarRow::Hint { text } => TreeRow::new( @@ -764,7 +649,6 @@ impl Sidebar { &self, id: &'static str, label: &'static str, - count: usize, open: bool, toggle: impl Fn(&mut Sidebar) + 'static, cx: &mut Context, @@ -778,7 +662,6 @@ impl Sidebar { } else { IconName::CaretRight }) - .count(count) .on_click(cx.listener(move |this, _event, _window, cx| { toggle(this); cx.notify(); @@ -790,7 +673,7 @@ impl Sidebar { fn channel_row( &self, id: ChannelId, - name: String, + name: SharedString, private: bool, selected: bool, community: &Entity, @@ -875,7 +758,11 @@ impl Focusable for Sidebar { impl Render for Sidebar { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let nostr = NostrRegistry::global(cx); - let logged_in = { nostr.read(cx).current_user().is_some() }; + let current_user = nostr.read(cx).current_user(); + let logged_in = current_user.is_some(); + + let chat = ChatRegistry::global(cx); + let loading = chat.read(cx).loading(); let community = self .community @@ -890,6 +777,13 @@ impl Render for Sidebar { .bg(cx.theme().surface_background) .border_r_1() .border_color(cx.theme().border_variant) + .when_some(current_user.as_ref(), |this, current_user| { + this.child(title_bar_drag_handlers( + self.render_user(current_user, cx), + window, + cx, + )) + }) .when(!logged_in, |this| { this.relative() .child(title_bar_drag_handlers( @@ -930,11 +824,36 @@ impl Render for Sidebar { ), ) }) - .child(title_bar_drag_handlers(self.render_user(cx), window, cx)) .map(|this| match community { Some(community) => this.child(self.render_community(community, cx)), None => this.child(self.render_tabs(cx)), }) + .when(loading && logged_in, |this| { + this.child( + div() + .absolute() + .bottom_4() + .left_0() + .h_9() + .w_full() + .px_8() + .child( + h_flex() + .gap_2() + .w_full() + .h_9() + .justify_center() + .bg(cx.theme().background.opacity(0.85)) + .when(cx.theme().shadow, |this| this.shadow_md()) + .rounded_full() + .text_xs() + .font_semibold() + .text_color(cx.theme().text_muted) + .child(Indicator::new().small().color(cx.theme().icon_accent)) + .child("Getting messages..."), + ), + ) + }) .into_any_element() } } diff --git a/crates/workspace/src/sidebar/tab.rs b/crates/workspace/src/sidebar/tab.rs index eda0d77b..1fccd595 100644 --- a/crates/workspace/src/sidebar/tab.rs +++ b/crates/workspace/src/sidebar/tab.rs @@ -1,121 +1,30 @@ -use std::rc::Rc; - -use gpui::prelude::FluentBuilder; -use gpui::{App, InteractiveElement, IntoElement, ParentElement, RenderOnce, Styled, Window, div}; -use theme::ActiveTheme; -use ui::button::{Button, ButtonVariants}; -use ui::{IconName, Selectable, h_flex}; - #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum SidebarTab { - Recents, - Chats, + Inbox, Communities, } impl SidebarTab { - pub const ALL: [SidebarTab; 3] = [Self::Recents, Self::Chats, Self::Communities]; + pub const ALL: [SidebarTab; 2] = [Self::Inbox, Self::Communities]; pub fn label(self) -> &'static str { match self { - Self::Recents => "Recents", - Self::Chats => "Chats", + Self::Inbox => "Inbox", Self::Communities => "Communities", } } - pub fn icon(self) -> IconName { - match self { - Self::Recents => IconName::History, - Self::Chats => IconName::Message, - Self::Communities => IconName::Group, - } - } - pub fn list_id(self) -> &'static str { match self { - Self::Recents => "sidebar-recents", - Self::Chats => "sidebar-chats", + Self::Inbox => "sidebar-inbox", Self::Communities => "sidebar-communities", } } pub fn index(self) -> usize { match self { - Self::Recents => 0, - Self::Chats => 1, - Self::Communities => 2, + Self::Inbox => 0, + Self::Communities => 1, } } - - pub fn chat(self) -> bool { - matches!(self, Self::Chats) - } - - pub fn community(self) -> bool { - matches!(self, Self::Communities) - } -} - -#[derive(IntoElement)] -#[allow(clippy::type_complexity)] -pub struct TabBar { - active: SidebarTab, - on_select: Option>, -} - -impl TabBar { - pub fn new(active: SidebarTab) -> Self { - Self { - active, - on_select: None, - } - } - - pub fn on_select( - mut self, - handler: impl Fn(SidebarTab, &mut Window, &mut App) + 'static, - ) -> Self { - self.on_select = Some(Rc::new(handler)); - self - } -} - -impl RenderOnce for TabBar { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let Self { active, on_select } = self; - - div() - .id("sidebar-tabs") - .absolute() - .bottom_3() - .left_0() - .w_full() - .px_4() - .child( - h_flex() - .w_full() - .p_1() - .gap_1() - .rounded_full() - .bg(cx.theme().background) - .when(cx.theme().shadow, |this| this.shadow_md()) - .children(SidebarTab::ALL.into_iter().map(|tab| { - let on_select = on_select.clone(); - - Button::new(format!("tab-{}", tab.list_id())) - .icon(tab.icon()) - .ghost() - .flex_1() - .rounded() - .selected(tab == active) - .tooltip(tab.label()) - .on_click(move |_event, window, cx| { - if let Some(on_select) = on_select.as_ref() { - on_select(tab, window, cx); - } - }) - })), - ) - } } diff --git a/crates/workspace/src/sidebar/tree.rs b/crates/workspace/src/sidebar/tree.rs index 20323851..0910b4d3 100644 --- a/crates/workspace/src/sidebar/tree.rs +++ b/crates/workspace/src/sidebar/tree.rs @@ -1,36 +1,45 @@ use std::rc::Rc; use chat::Room; -use community::Community; +use community::{ChannelId, Community}; use gpui::prelude::FluentBuilder; use gpui::{ App, ClickEvent, ElementId, Entity, ImageSource, InteractiveElement, IntoElement, ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window, div, px, }; +use nostr_sdk::prelude::PublicKey; use settings::AppSettings; use theme::ActiveTheme; use ui::avatar::{Avatar, PixelAvatar}; use ui::{Icon, IconName, Selectable, Sizable, StyledExt, h_flex}; -use super::tab::SidebarTab; - pub enum SidebarRow { - Section { - label: SharedString, - count: usize, + Section { label: SharedString }, + Room { room: Entity }, + Community { community: Entity }, + Hint { text: SharedString }, +} + +/// A collapsible group of rows in the sidebar's community view. +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum CommunitySection { + Channels, + Admins, + Members, +} + +/// A row in the sidebar's community view. +pub enum CommunityRow { + Section(CommunitySection), + Channel { + id: ChannelId, + name: SharedString, + private: bool, + selected: bool, }, - Room { - room: Entity, - }, - Community { - community: Entity, - }, - Action { - label: SharedString, - tab: SidebarTab, - }, - Hint { - text: SharedString, + Member { + prefix: &'static str, + public_key: PublicKey, }, } @@ -39,7 +48,6 @@ pub enum TreeRowKind { Section, Room, Community, - Action, Hint, } @@ -51,7 +59,6 @@ pub struct TreeRow { avatar: Option, picture: Option, icon: Option, - count: Option, created_at: Option, selected: bool, #[allow(clippy::type_complexity)] @@ -71,7 +78,6 @@ impl TreeRow { avatar: None, picture: None, icon: None, - count: None, created_at: None, selected: false, on_click: None, @@ -96,11 +102,6 @@ impl TreeRow { self } - pub fn count(mut self, count: usize) -> Self { - self.count = Some(count); - self - } - pub fn created_at(mut self, created_at: impl Into) -> Self { self.created_at = Some(created_at.into()); self @@ -133,7 +134,6 @@ impl RenderOnce for TreeRow { let is_section = self.kind == TreeRowKind::Section; let is_room = self.kind == TreeRowKind::Room; let is_community = self.kind == TreeRowKind::Community; - let is_action = self.kind == TreeRowKind::Action; let is_hint = self.kind == TreeRowKind::Hint; let is_selected = self.selected; @@ -172,7 +172,6 @@ impl RenderOnce for TreeRow { h_flex() .id(self.id) - .h_8() .w_full() .px_2() .gap_2() @@ -182,10 +181,8 @@ impl RenderOnce for TreeRow { .text_color(cx.theme().text_placeholder) .font_semibold() }) - .when(is_room || is_community, |this| this.text_sm()) - .when(is_action, |this| { - this.text_sm().text_color(cx.theme().text_muted) - }) + .when(is_room || is_community, |this| this.text_sm().h_10()) + .h_8() .when(is_hint, |this| { this.text_xs() .font_normal() @@ -203,6 +200,7 @@ impl RenderOnce for TreeRow { .when(is_room, |this| this.font_medium()) .child(self.label), ) + .child(div().flex_1()) .when(is_selected, |this| { this.child( Icon::new(IconName::CheckCircle) @@ -211,11 +209,8 @@ impl RenderOnce for TreeRow { .text_color(cx.theme().icon_accent), ) }) - .when_some(self.count, |this, count| { - this.child(div().flex_shrink_0().font_normal().child(count.to_string())) - }) .when_some(self.created_at, |this, created_at| { - this.child(div().flex_1()).child( + this.child( div() .flex_shrink_0() .text_color(cx.theme().text_placeholder) -- 2.54.0 From 610bf816b0cc223b74875bef0d467d71b5832166 Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Thu, 24 Sep 2026 08:39:02 +0700 Subject: [PATCH 5/9] update --- crates/workspace/src/sidebar/mod.rs | 67 +++++++++------------------- crates/workspace/src/sidebar/tab.rs | 8 ++++ crates/workspace/src/sidebar/tree.rs | 13 +----- 3 files changed, 30 insertions(+), 58 deletions(-) diff --git a/crates/workspace/src/sidebar/mod.rs b/crates/workspace/src/sidebar/mod.rs index 81139445..ea57c43f 100644 --- a/crates/workspace/src/sidebar/mod.rs +++ b/crates/workspace/src/sidebar/mod.rs @@ -344,6 +344,14 @@ impl Sidebar { ), ), }) + .child( + div() + .px_4() + .text_xs() + .font_semibold() + .text_color(cx.theme().text_placeholder) + .child(active_tab.list_title()), + ) .child( div() .min_h_0() @@ -510,44 +518,21 @@ impl Sidebar { match tab { SidebarTab::Inbox => { let chat = ChatRegistry::global(cx); - let rooms = chat.read(cx).rooms(&RoomKind::Ongoing, cx); - - let mut rows = vec![SidebarRow::Section { - label: "Chats".into(), - }]; - - if rooms.is_empty() { - rows.push(SidebarRow::Hint { - text: "No conversations yet".into(), - }); - } else { - rows.extend(rooms.into_iter().map(|room| SidebarRow::Room { room })); - } - - rows + chat.read(cx) + .rooms(&RoomKind::Ongoing, cx) + .into_iter() + .map(|room| SidebarRow::Room { room }) + .collect() } SidebarTab::Communities => { let registry = CommunityRegistry::global(cx); - let communities = registry.read(cx).communities(); - - let mut rows = vec![SidebarRow::Section { - label: "Communities".into(), - }]; - - if communities.is_empty() { - rows.push(SidebarRow::Hint { - text: "No communities yet".into(), - }); - } else { - rows.extend( - communities - .iter() - .cloned() - .map(|community| SidebarRow::Community { community }), - ); - } - - rows + registry + .read(cx) + .communities() + .iter() + .cloned() + .map(|community| SidebarRow::Community { community }) + .collect() } } } @@ -566,12 +551,6 @@ impl Sidebar { let index = range.start + offset; match row { - SidebarRow::Section { label } => TreeRow::new( - ElementId::NamedInteger("tree-row".into(), index as u64), - TreeRowKind::Section, - label.clone(), - ) - .into_any_element(), SidebarRow::Room { room } => { let name = room.read(cx).display_name(cx); let picture = room.read(cx).display_image(cx); @@ -634,12 +613,6 @@ impl Sidebar { }) .into_any_element() } - SidebarRow::Hint { text } => TreeRow::new( - ElementId::NamedInteger("tree-row".into(), index as u64), - TreeRowKind::Hint, - text.clone(), - ) - .into_any_element(), } }) .collect() diff --git a/crates/workspace/src/sidebar/tab.rs b/crates/workspace/src/sidebar/tab.rs index 1fccd595..5698be6a 100644 --- a/crates/workspace/src/sidebar/tab.rs +++ b/crates/workspace/src/sidebar/tab.rs @@ -14,6 +14,14 @@ impl SidebarTab { } } + /// Heading for the tab's list of items. + pub fn list_title(self) -> &'static str { + match self { + Self::Inbox => "Direct Messages", + Self::Communities => "Communities", + } + } + pub fn list_id(self) -> &'static str { match self { Self::Inbox => "sidebar-inbox", diff --git a/crates/workspace/src/sidebar/tree.rs b/crates/workspace/src/sidebar/tree.rs index 0910b4d3..79c0760b 100644 --- a/crates/workspace/src/sidebar/tree.rs +++ b/crates/workspace/src/sidebar/tree.rs @@ -14,10 +14,8 @@ use ui::avatar::{Avatar, PixelAvatar}; use ui::{Icon, IconName, Selectable, Sizable, StyledExt, h_flex}; pub enum SidebarRow { - Section { label: SharedString }, Room { room: Entity }, Community { community: Entity }, - Hint { text: SharedString }, } /// A collapsible group of rows in the sidebar's community view. @@ -48,7 +46,6 @@ pub enum TreeRowKind { Section, Room, Community, - Hint, } #[derive(IntoElement)] @@ -134,7 +131,6 @@ impl RenderOnce for TreeRow { let is_section = self.kind == TreeRowKind::Section; let is_room = self.kind == TreeRowKind::Room; let is_community = self.kind == TreeRowKind::Community; - let is_hint = self.kind == TreeRowKind::Hint; let is_selected = self.selected; let avatar = if hide_avatar { @@ -173,6 +169,7 @@ impl RenderOnce for TreeRow { h_flex() .id(self.id) .w_full() + .h_10() .px_2() .gap_2() .rounded(cx.theme().radius) @@ -181,13 +178,7 @@ impl RenderOnce for TreeRow { .text_color(cx.theme().text_placeholder) .font_semibold() }) - .when(is_room || is_community, |this| this.text_sm().h_10()) - .h_8() - .when(is_hint, |this| { - this.text_xs() - .font_normal() - .text_color(cx.theme().text_placeholder) - }) + .when(is_room || is_community, |this| this.text_sm()) .when_some(avatar, |this, avatar| this.child(avatar)) .child( h_flex() -- 2.54.0 From 2a5080458311b72363c3a843258d35905a512d56 Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Thu, 24 Sep 2026 13:54:04 +0700 Subject: [PATCH 6/9] update tabbar --- assets/icons/panel-left-open.svg | 2 +- assets/icons/panel-left.svg | 2 +- crates/chat_ui/src/lib.rs | 5 +- crates/community/src/community.rs | 11 +- crates/community_ui/src/lib.rs | 7 +- crates/ui/src/avatar.rs | 2 +- crates/ui/src/dock/mod.rs | 209 +++---------------------- crates/ui/src/tab/mod.rs | 72 ++++----- crates/ui/src/tab/tab_bar.rs | 1 + crates/workspace/src/panels/greeter.rs | 5 +- crates/workspace/src/sidebar/mod.rs | 2 +- crates/workspace/src/sidebar/tree.rs | 4 +- 12 files changed, 80 insertions(+), 242 deletions(-) diff --git a/assets/icons/panel-left-open.svg b/assets/icons/panel-left-open.svg index 1281f6e3..62bae092 100644 --- a/assets/icons/panel-left-open.svg +++ b/assets/icons/panel-left-open.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/panel-left.svg b/assets/icons/panel-left.svg index 1c98d39b..6cac95c5 100644 --- a/assets/icons/panel-left.svg +++ b/assets/icons/panel-left.svg @@ -1,3 +1,3 @@ - + diff --git a/crates/chat_ui/src/lib.rs b/crates/chat_ui/src/lib.rs index c6a23955..5f1c9b91 100644 --- a/crates/chat_ui/src/lib.rs +++ b/crates/chat_ui/src/lib.rs @@ -1983,12 +1983,13 @@ impl Panel for ChatPanel { let seed = this.display_image_seed(cx); h_flex() - .gap_1p5() + .gap_1() + .text_xs() .child(Avatar::new(picture).seed(seed).xsmall()) .child(label) .into_any_element() }) - .unwrap_or(div().child("Unknown").into_any_element()) + .unwrap_or(div().text_xs().child("Unknown").into_any_element()) } fn toolbar_buttons(&self, _window: &Window, _cx: &App) -> Vec