From f7610cc9c96db46d264df4a826d440958f10d1e3 Mon Sep 17 00:00:00 2001 From: reya <123083837+reyamir@users.noreply.github.com> Date: Sun, 6 Apr 2025 15:29:36 +0700 Subject: [PATCH] feat: Chat Folders (#14) * add room kinds * add folders * adjust design * update * refactor * cache * update --- Cargo.lock | 261 +++++++++++------ assets/icons/bubble-fill.svg | 3 + assets/icons/folder-fill.svg | 3 + assets/icons/folder-open-fill.svg | 3 + assets/icons/folder.svg | 3 + crates/account/src/lib.rs | 23 +- crates/app/src/chat_space.rs | 2 +- crates/app/src/main.rs | 124 +++++--- crates/app/src/views/chat.rs | 2 +- crates/app/src/views/sidebar/compose.rs | 83 +++++- crates/app/src/views/sidebar/folder.rs | 323 +++++++++++++++++++++ crates/app/src/views/sidebar/mod.rs | 364 ++++++++++-------------- crates/chats/src/lib.rs | 160 ++++++++--- crates/chats/src/room.rs | 186 ++++++------ crates/global/src/constants.rs | 2 +- crates/ui/src/icon.rs | 8 + crates/ui/src/scroll/scrollable.rs | 2 +- crates/ui/src/scroll/scrollbar.rs | 4 +- 18 files changed, 1052 insertions(+), 504 deletions(-) create mode 100644 assets/icons/bubble-fill.svg create mode 100644 assets/icons/folder-fill.svg create mode 100644 assets/icons/folder-open-fill.svg create mode 100644 assets/icons/folder.svg create mode 100644 crates/app/src/views/sidebar/folder.rs diff --git a/Cargo.lock b/Cargo.lock index 79c73b8..cf0c7bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -466,9 +466,9 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.12.6" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dabb68eb3a7aa08b46fddfd59a3d55c978243557a90ab804769f7e20e67d2b01" +checksum = "19b756939cb2f8dc900aa6dcd505e6e2428e9cae7ff7b028c49e3946efa70878" dependencies = [ "aws-lc-sys", "zeroize", @@ -476,9 +476,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.27.1" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77926887776171ced7d662120a75998e444d3750c951abfe07f90da130514b1f" +checksum = "b9f7720b74ed28ca77f90769a71fd8c637a0137f6fae4ae947e1050229cff57f" dependencies = [ "bindgen 0.69.5", "cc", @@ -545,9 +545,9 @@ dependencies = [ [[package]] name = "bindgen" -version = "0.70.1" +version = "0.71.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" +checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" dependencies = [ "bitflags 2.9.0", "cexpr", @@ -558,7 +558,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash 1.1.0", + "rustc-hash 2.1.1", "shlex", "syn 2.0.100", ] @@ -869,9 +869,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.17" +version = "1.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a" +checksum = "525046617d8376e3db1deffb079e91cef90a89fc3ca5c185bbf8c9ecdd15cd5c" dependencies = [ "jobserver", "libc", @@ -921,6 +921,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "cgl" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff" +dependencies = [ + "libc", +] + [[package]] name = "chacha20" version = "0.9.1" @@ -1029,9 +1038,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.34" +version = "4.5.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e958897981290da2a852763fe9cdb89cd36977a5d729023127095fa94d95e2ff" +checksum = "d8aa86934b44c19c50f87cc2790e19f54f7a67aedb64101c2e1a2e5ecfb73944" dependencies = [ "clap_builder", "clap_derive", @@ -1039,9 +1048,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.34" +version = "4.5.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83b0f35019843db2160b5bb19ae09b4e6411ac33fc6a712003c33e03090e2489" +checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9" dependencies = [ "anstream", "anstyle", @@ -1149,10 +1158,11 @@ dependencies = [ [[package]] name = "collections" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#f6c81a0595f518bf2f245a199248d50245afacac" +source = "git+https://github.com/zed-industries/zed#c1259c136e41b5870d15271190198ff0075ba35c" dependencies = [ "indexmap", "rustc-hash 2.1.1", + "workspace-hack", ] [[package]] @@ -1330,17 +1340,44 @@ dependencies = [ ] [[package]] -name = "core-text" -version = "20.1.0" +name = "core-graphics2" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9d2790b5c08465d49f8dc05c8bcae9fea467855947db39b0f8145c091aaced5" +checksum = "7e4583956b9806b69f73fcb23aee05eb3620efc282972f08f6a6db7504f8334d" dependencies = [ - "core-foundation 0.9.4", - "core-graphics 0.23.2", + "bitflags 2.9.0", + "block", + "cfg-if", + "core-foundation 0.10.0", + "libc", +] + +[[package]] +name = "core-text" +version = "21.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a593227b66cbd4007b2a050dfdd9e1d1318311409c8d600dc82ba1b15ca9c130" +dependencies = [ + "core-foundation 0.10.0", + "core-graphics 0.24.0", "foreign-types", "libc", ] +[[package]] +name = "core-video" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d45e71d5be22206bed53c3c3cb99315fc4c3d31b8963808c6bc4538168c4f8ef" +dependencies = [ + "block", + "core-foundation 0.10.0", + "core-graphics2", + "io-surface", + "libc", + "metal", +] + [[package]] name = "core_maths" version = "0.1.1" @@ -1522,11 +1559,12 @@ dependencies = [ [[package]] name = "derive_refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#f6c81a0595f518bf2f245a199248d50245afacac" +source = "git+https://github.com/zed-industries/zed#c1259c136e41b5870d15271190198ff0075ba35c" dependencies = [ "proc-macro2", "quote", "syn 1.0.109", + "workspace-hack", ] [[package]] @@ -1729,9 +1767,9 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" dependencies = [ "libc", "windows-sys 0.59.0", @@ -1839,9 +1877,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc" +checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" dependencies = [ "crc32fast", "miniz_oxide", @@ -1886,12 +1924,12 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "font-kit" version = "0.14.1" -source = "git+https://github.com/zed-industries/font-kit?rev=40391b7#40391b7c0041d8a8572af2afa3de32ae088f0120" +source = "git+https://github.com/zed-industries/font-kit?rev=5474cfad4b719a72ec8ed2cb7327b2b01fd10568#5474cfad4b719a72ec8ed2cb7327b2b01fd10568" dependencies = [ "bitflags 2.9.0", "byteorder", - "core-foundation 0.9.4", - "core-graphics 0.23.2", + "core-foundation 0.10.0", + "core-graphics 0.24.0", "core-text", "dirs 5.0.1", "dwrote", @@ -1909,9 +1947,9 @@ dependencies = [ [[package]] name = "font-types" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d868ec188a98bb014c606072edd47e52e7ab7297db943b0b28503121e1d037bd" +checksum = "1fa6a5e5a77b5f3f7f9e32879f484aa5b3632ddfbe568a16266c904a6f32cdaf" dependencies = [ "bytemuck", ] @@ -2282,13 +2320,13 @@ dependencies = [ [[package]] name = "gpui" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#f6c81a0595f518bf2f245a199248d50245afacac" +source = "git+https://github.com/zed-industries/zed#c1259c136e41b5870d15271190198ff0075ba35c" dependencies = [ "anyhow", "as-raw-xcb-connection", "ashpd", "async-task", - "bindgen 0.70.1", + "bindgen 0.71.1", "blade-graphics", "blade-macros", "blade-util", @@ -2299,10 +2337,11 @@ dependencies = [ "cbindgen", "cocoa 0.26.0", "collections", - "core-foundation 0.9.4", + "core-foundation 0.10.0", "core-foundation-sys", - "core-graphics 0.23.2", + "core-graphics 0.24.0", "core-text", + "core-video", "cosmic-text", "ctor", "derive_more", @@ -2359,8 +2398,9 @@ dependencies = [ "wayland-protocols", "wayland-protocols-plasma", "windows", - "windows-core 0.61.0", + "windows-core", "windows-numerics", + "workspace-hack", "x11-clipboard", "x11rb", "xim", @@ -2370,11 +2410,12 @@ dependencies = [ [[package]] name = "gpui_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#f6c81a0595f518bf2f245a199248d50245afacac" +source = "git+https://github.com/zed-industries/zed#c1259c136e41b5870d15271190198ff0075ba35c" dependencies = [ "proc-macro2", "quote", "syn 1.0.109", + "workspace-hack", ] [[package]] @@ -2599,7 +2640,7 @@ dependencies = [ [[package]] name = "http_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#f6c81a0595f518bf2f245a199248d50245afacac" +source = "git+https://github.com/zed-industries/zed#c1259c136e41b5870d15271190198ff0075ba35c" dependencies = [ "anyhow", "bytes", @@ -2610,15 +2651,17 @@ dependencies = [ "serde", "serde_json", "url", + "workspace-hack", ] [[package]] name = "http_client_tls" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#f6c81a0595f518bf2f245a199248d50245afacac" +source = "git+https://github.com/zed-industries/zed#c1259c136e41b5870d15271190198ff0075ba35c" dependencies = [ "rustls", "rustls-platform-verifier", + "workspace-hack", ] [[package]] @@ -2668,9 +2711,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" dependencies = [ "bytes", "futures-channel", @@ -2678,6 +2721,7 @@ dependencies = [ "http", "http-body", "hyper", + "libc", "pin-project-lite", "socket2", "tokio", @@ -2687,9 +2731,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.62" +version = "0.1.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2fd658b06e56721792c5df4475705b6cda790e9298d19d2f8af083457bcd127" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -2697,7 +2741,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.52.0", + "windows-core", ] [[package]] @@ -2895,9 +2939,9 @@ checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408" [[package]] name = "indexmap" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", "hashbrown 0.15.2", @@ -2946,6 +2990,19 @@ dependencies = [ "rustversion", ] +[[package]] +name = "io-surface" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8283575d5f0b2e7447ec0840363879d71c0fa325d4c699d5b45208ea4a51f45e" +dependencies = [ + "cgl", + "core-foundation 0.10.0", + "core-foundation-sys", + "leaky-cow", + "libc", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -3054,10 +3111,11 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "jobserver" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" dependencies = [ + "getrandom 0.3.2", "libc", ] @@ -3118,6 +3176,21 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +[[package]] +name = "leak" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd100e01f1154f2908dfa7d02219aeab25d0b9c7fa955164192e3245255a0c73" + +[[package]] +name = "leaky-cow" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40a8225d44241fd324a8af2806ba635fc7c8a7e9a7de4d5cf3ef54e71f5926fc" +dependencies = [ + "leak", +] + [[package]] name = "lebe" version = "0.5.2" @@ -3329,15 +3402,17 @@ dependencies = [ [[package]] name = "media" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#f6c81a0595f518bf2f245a199248d50245afacac" +source = "git+https://github.com/zed-industries/zed#c1259c136e41b5870d15271190198ff0075ba35c" dependencies = [ "anyhow", - "bindgen 0.70.1", - "core-foundation 0.9.4", + "bindgen 0.71.1", + "core-foundation 0.10.0", + "core-video", "ctor", "foreign-types", "metal", "objc", + "workspace-hack", ] [[package]] @@ -3366,9 +3441,9 @@ dependencies = [ [[package]] name = "metal" -version = "0.31.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f569fb946490b5743ad69813cb19629130ce9374034abe31614a36402d18f99e" +checksum = "7ecfd3296f8c56b7c1f6fbac3c71cefa9d78ce009850c45000015f206dc7fa21" dependencies = [ "bitflags 2.9.0", "block", @@ -3403,9 +3478,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.5" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" +checksum = "ff70ce3e48ae43fa075863cef62e8b43b71a4f2382229920e0df362592919430" dependencies = [ "adler2", "simd-adler32", @@ -3508,7 +3583,7 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" [[package]] name = "nostr" version = "0.40.0" -source = "git+https://github.com/rust-nostr/nostr#31180d7dd2aed5afb895945fef9359646f22ff3e" +source = "git+https://github.com/rust-nostr/nostr#1347878e90392db54a263d60021fa0410a3b05cb" dependencies = [ "aes", "base64", @@ -3533,7 +3608,7 @@ dependencies = [ [[package]] name = "nostr-connect" version = "0.40.0" -source = "git+https://github.com/rust-nostr/nostr#31180d7dd2aed5afb895945fef9359646f22ff3e" +source = "git+https://github.com/rust-nostr/nostr#1347878e90392db54a263d60021fa0410a3b05cb" dependencies = [ "async-utility", "nostr", @@ -3545,7 +3620,7 @@ dependencies = [ [[package]] name = "nostr-database" version = "0.40.0" -source = "git+https://github.com/rust-nostr/nostr#31180d7dd2aed5afb895945fef9359646f22ff3e" +source = "git+https://github.com/rust-nostr/nostr#1347878e90392db54a263d60021fa0410a3b05cb" dependencies = [ "flatbuffers", "lru", @@ -3556,7 +3631,7 @@ dependencies = [ [[package]] name = "nostr-lmdb" version = "0.40.0" -source = "git+https://github.com/rust-nostr/nostr#31180d7dd2aed5afb895945fef9359646f22ff3e" +source = "git+https://github.com/rust-nostr/nostr#1347878e90392db54a263d60021fa0410a3b05cb" dependencies = [ "async-utility", "heed", @@ -3569,7 +3644,7 @@ dependencies = [ [[package]] name = "nostr-relay-pool" version = "0.40.0" -source = "git+https://github.com/rust-nostr/nostr#31180d7dd2aed5afb895945fef9359646f22ff3e" +source = "git+https://github.com/rust-nostr/nostr#1347878e90392db54a263d60021fa0410a3b05cb" dependencies = [ "async-utility", "async-wsocket", @@ -3586,7 +3661,7 @@ dependencies = [ [[package]] name = "nostr-sdk" version = "0.40.0" -source = "git+https://github.com/rust-nostr/nostr#31180d7dd2aed5afb895945fef9359646f22ff3e" +source = "git+https://github.com/rust-nostr/nostr#1347878e90392db54a263d60021fa0410a3b05cb" dependencies = [ "async-utility", "nostr", @@ -3942,9 +4017,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.2" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2806eaa3524762875e21c3dcd057bc4b7bfa01ce4da8d46be1cd43649e1cc6b" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "oneshot" @@ -4333,9 +4408,9 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.31" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5316f57387668042f561aae71480de936257848f9c43ce528e311d89a07cadeb" +checksum = "664ec5419c51e34154eec046ebcba56312d5a2fc3b09a06da188e1ad21afadf6" dependencies = [ "proc-macro2", "syn 2.0.100", @@ -4412,9 +4487,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quick-xml" -version = "0.37.3" +version = "0.37.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf763ab1c7a3aa408be466efc86efe35ed1bd3dd74173ed39d6b0d0a6f0ba148" +checksum = "a4ce8c88de324ff838700f36fb6ab86c96df0e3c4ab6ef3a9b2044465cce1369" dependencies = [ "memchr", ] @@ -4692,9 +4767,10 @@ dependencies = [ [[package]] name = "refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#f6c81a0595f518bf2f245a199248d50245afacac" +source = "git+https://github.com/zed-industries/zed#c1259c136e41b5870d15271190198ff0075ba35c" dependencies = [ "derive_refineable", + "workspace-hack", ] [[package]] @@ -4821,7 +4897,7 @@ dependencies = [ [[package]] name = "reqwest_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#f6c81a0595f518bf2f245a199248d50245afacac" +source = "git+https://github.com/zed-industries/zed#c1259c136e41b5870d15271190198ff0075ba35c" dependencies = [ "anyhow", "bytes", @@ -4834,6 +4910,7 @@ dependencies = [ "serde", "smol", "tokio", + "workspace-hack", ] [[package]] @@ -4956,9 +5033,9 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.3" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e56a18552996ac8d29ecc3b190b4fdbb2d91ca4ec396de7bbffaf43f3d637e96" +checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" dependencies = [ "bitflags 2.9.0", "errno", @@ -5233,10 +5310,11 @@ checksum = "c2fdfc24bc566f839a2da4c4295b82db7d25a24253867d5c64355abb5799bdbe" [[package]] name = "semantic_version" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#f6c81a0595f518bf2f245a199248d50245afacac" +source = "git+https://github.com/zed-industries/zed#c1259c136e41b5870d15271190198ff0075ba35c" dependencies = [ "anyhow", "serde", + "workspace-hack", ] [[package]] @@ -5442,9 +5520,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" [[package]] name = "smol" @@ -5471,9 +5549,9 @@ checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" [[package]] name = "socket2" -version = "0.5.8" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" dependencies = [ "libc", "windows-sys 0.52.0", @@ -5555,11 +5633,12 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sum_tree" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#f6c81a0595f518bf2f245a199248d50245afacac" +source = "git+https://github.com/zed-industries/zed#c1259c136e41b5870d15271190198ff0075ba35c" dependencies = [ "arrayvec", "log", "rayon", + "workspace-hack", ] [[package]] @@ -5658,9 +5737,9 @@ dependencies = [ [[package]] name = "swash" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13d5bbc2aa266907ed8ee977c9c9e16363cc2b001266104e13397b57f1d15f71" +checksum = "fae9a562c7b46107d9c78cd78b75bbe1e991c16734c0aee8ff0ee711fb8b620a" dependencies = [ "skrifa", "yazi", @@ -5795,7 +5874,7 @@ dependencies = [ "fastrand 2.3.0", "getrandom 0.3.2", "once_cell", - "rustix 1.0.3", + "rustix 1.0.5", "windows-sys 0.59.0", ] @@ -6431,7 +6510,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "util" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#f6c81a0595f518bf2f245a199248d50245afacac" +source = "git+https://github.com/zed-industries/zed#c1259c136e41b5870d15271190198ff0075ba35c" dependencies = [ "anyhow", "async-fs", @@ -6452,6 +6531,7 @@ dependencies = [ "take-until", "tendril", "unicase", + "workspace-hack", ] [[package]] @@ -6870,7 +6950,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419" dependencies = [ "windows-collections", - "windows-core 0.61.0", + "windows-core", "windows-future", "windows-link", "windows-numerics", @@ -6882,16 +6962,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" dependencies = [ - "windows-core 0.61.0", -] - -[[package]] -name = "windows-core" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" -dependencies = [ - "windows-targets 0.52.6", + "windows-core", ] [[package]] @@ -6913,7 +6984,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32" dependencies = [ - "windows-core 0.61.0", + "windows-core", "windows-link", ] @@ -6951,7 +7022,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ - "windows-core 0.61.0", + "windows-core", "windows-link", ] @@ -7338,6 +7409,12 @@ dependencies = [ "bitflags 2.9.0", ] +[[package]] +name = "workspace-hack" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "beffa227304dbaea3ad6a06ac674f9bc83a3dec3b7f63eeb442de37e7cb6bb01" + [[package]] name = "write16" version = "1.0.0" diff --git a/assets/icons/bubble-fill.svg b/assets/icons/bubble-fill.svg new file mode 100644 index 0000000..2f7d50c --- /dev/null +++ b/assets/icons/bubble-fill.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/folder-fill.svg b/assets/icons/folder-fill.svg new file mode 100644 index 0000000..febae36 --- /dev/null +++ b/assets/icons/folder-fill.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/folder-open-fill.svg b/assets/icons/folder-open-fill.svg new file mode 100644 index 0000000..834720f --- /dev/null +++ b/assets/icons/folder-open-fill.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/folder.svg b/assets/icons/folder.svg new file mode 100644 index 0000000..25aef4c --- /dev/null +++ b/assets/icons/folder.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/account/src/lib.rs b/crates/account/src/lib.rs index 98e0549..161dc9d 100644 --- a/crates/account/src/lib.rs +++ b/crates/account/src/lib.rs @@ -118,31 +118,34 @@ impl Account { let user = profile.public_key; let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); - // Create a contact list filter - let contacts = Filter::new().kind(Kind::ContactList).author(user).limit(1); + let metadata = Filter::new() + .kinds(vec![ + Kind::Metadata, + Kind::ContactList, + Kind::InboxRelays, + Kind::MuteList, + Kind::SimpleGroups, + ]) + .author(user) + .limit(10); - // Create a user's data filter let data = Filter::new() .author(user) .since(Timestamp::now()) .kinds(vec![ Kind::Metadata, Kind::ContactList, + Kind::MuteList, + Kind::SimpleGroups, Kind::InboxRelays, Kind::RelayList, ]); - // Create a filter for getting all gift wrapped events send to current user let msg = Filter::new().kind(Kind::GiftWrap).pubkey(user); - - // Create a filter to continuously receive new messages. let new_msg = Filter::new().kind(Kind::GiftWrap).pubkey(user).limit(0); let task: Task> = cx.background_spawn(async move { - // Only subscribe to the latest contact list - client.subscribe(contacts, Some(opts)).await?; - - // Continuously receive new user's data since now + client.subscribe(metadata, Some(opts)).await?; client.subscribe(data, None).await?; let sub_id = SubscriptionId::new(ALL_MESSAGES_SUB_ID); diff --git a/crates/app/src/chat_space.rs b/crates/app/src/chat_space.rs index c16687c..5626a9a 100644 --- a/crates/app/src/chat_space.rs +++ b/crates/app/src/chat_space.rs @@ -134,7 +134,7 @@ impl ChatSpace { ); self.dock.update(cx, |this, cx| { - this.set_left_dock(left, Some(px(240.)), true, window, cx); + this.set_left_dock(left, Some(px(260.)), true, window, cx); this.set_center(center, window, cx); }); } diff --git a/crates/app/src/main.rs b/crates/app/src/main.rs index ae50f50..3a7b71f 100644 --- a/crates/app/src/main.rs +++ b/crates/app/src/main.rs @@ -1,3 +1,4 @@ +use anyhow::{anyhow, Error}; use asset::Assets; use chats::ChatRegistry; use futures::{select, FutureExt}; @@ -16,8 +17,8 @@ use gpui::{point, SharedString, TitlebarOptions}; #[cfg(target_os = "linux")] use gpui::{WindowBackgroundAppearance, WindowDecorations}; use nostr_sdk::{ - pool::prelude::ReqExitPolicy, Event, Filter, Keys, Kind, PublicKey, RelayMessage, - RelayPoolNotification, SubscribeAutoCloseOptions, SubscriptionId, + pool::prelude::ReqExitPolicy, Event, EventBuilder, EventId, Filter, JsonUtil, Keys, Kind, + PublicKey, RelayMessage, RelayPoolNotification, SubscribeAutoCloseOptions, SubscriptionId, Tag, }; use smol::Timer; use std::{collections::HashSet, mem, sync::Arc, time::Duration}; @@ -44,7 +45,7 @@ fn main() { _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); let (event_tx, event_rx) = smol::channel::bounded::(1024); - let (batch_tx, batch_rx) = smol::channel::bounded::>(100); + let (batch_tx, batch_rx) = smol::channel::bounded::>(500); // Initialize nostr client let client = get_client(); @@ -68,8 +69,8 @@ fn main() { // Handle batch metadata app.background_executor() .spawn(async move { - const BATCH_SIZE: usize = 20; - const BATCH_TIMEOUT: Duration = Duration::from_millis(500); + const BATCH_SIZE: usize = 500; + const BATCH_TIMEOUT: Duration = Duration::from_millis(300); let mut batch: HashSet = HashSet::new(); @@ -82,7 +83,7 @@ fn main() { Ok(keys) => { batch.extend(keys); if batch.len() >= BATCH_SIZE { - handle_metadata(mem::take(&mut batch)).await; + sync_metadata(mem::take(&mut batch)).await; } } Err(_) => break, @@ -90,7 +91,7 @@ fn main() { } _ = timeout => { if !batch.is_empty() { - handle_metadata(mem::take(&mut batch)).await; + sync_metadata(mem::take(&mut batch)).await; } } } @@ -115,40 +116,60 @@ fn main() { } => { match event.kind { Kind::GiftWrap => { - if let Ok(gift) = client.unwrap_gift_wrap(&event).await { - // Sign the rumor with the generated keys, - // this event will be used for internal only, - // and NEVER send to relays. - if let Ok(event) = gift.rumor.sign_with_keys(&rng_keys) { - let mut pubkeys = vec![]; - pubkeys.extend(event.tags.public_keys()); - pubkeys.push(event.pubkey); - - // Save the event to the database, use for query directly. - _ = client.database().save_event(&event).await; - - // Send this event to the GPUI - if new_id == *subscription_id { - _ = event_tx.send(Signal::Event(event)).await; + let event = match get_unwrapped(event.id).await { + Ok(event) => event, + Err(_) => match client.unwrap_gift_wrap(&event).await { + Ok(unwrap) => { + match unwrap.rumor.sign_with_keys(&rng_keys) { + Ok(ev) => { + set_unwrapped(event.id, &ev, &rng_keys) + .await + .ok(); + ev + } + Err(_) => continue, + } } + Err(_) => continue, + }, + }; - // Send all pubkeys to the batch - _ = batch_tx.send(pubkeys).await; - } + let mut pubkeys = vec![]; + pubkeys.extend(event.tags.public_keys()); + pubkeys.push(event.pubkey); + + // Send all pubkeys to the batch to sync metadata + batch_tx.send(pubkeys).await.ok(); + + // Save the event to the database, use for query directly. + client.database().save_event(&event).await.ok(); + + // Send this event to the GPUI + if new_id == *subscription_id { + event_tx.send(Signal::Event(event)).await.ok(); } } Kind::ContactList => { - let pubkeys = - event.tags.public_keys().copied().collect::>(); + if let Ok(signer) = client.signer().await { + if let Ok(public_key) = signer.get_public_key().await { + if public_key == event.pubkey { + let pubkeys = event + .tags + .public_keys() + .copied() + .collect::>(); - handle_metadata(pubkeys).await; + batch_tx.send(pubkeys).await.ok(); + } + } + } } _ => {} } } RelayMessage::EndOfStoredEvents(subscription_id) => { if all_id == *subscription_id { - _ = event_tx.send(Signal::Eose).await; + event_tx.send(Signal::Eose).await.ok(); } } _ => {} @@ -221,7 +242,7 @@ fn main() { cx.update(|window, cx| { match signal { Signal::Eose => { - chats.update(cx, |this, cx| this.load_chat_rooms(window, cx)); + chats.update(cx, |this, cx| this.load_rooms(window, cx)); } Signal::Event(event) => { chats.update(cx, |this, cx| { @@ -242,17 +263,48 @@ fn main() { }); } -async fn handle_metadata(buffer: HashSet) { +async fn set_unwrapped(root: EventId, event: &Event, keys: &Keys) -> Result<(), Error> { let client = get_client(); + let event = EventBuilder::new(Kind::Custom(9001), event.as_json()) + .tags(vec![Tag::event(root)]) + .sign(keys) + .await?; - let opts = SubscribeAutoCloseOptions::default() - .exit_policy(ReqExitPolicy::ExitOnEOSE) - .idle_timeout(Some(Duration::from_secs(1))); + client.database().save_event(&event).await?; + + Ok(()) +} + +async fn get_unwrapped(gift_wrap: EventId) -> Result { + let client = get_client(); + let filter = Filter::new() + .kind(Kind::Custom(9001)) + .event(gift_wrap) + .limit(1); + + if let Some(event) = client.database().query(filter).await?.first_owned() { + let parsed = Event::from_json(event.content)?; + Ok(parsed) + } else { + Err(anyhow!("Event not found")) + } +} + +async fn sync_metadata(buffer: HashSet) { + let client = get_client(); + let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); + + let kinds = vec![ + Kind::Metadata, + Kind::ContactList, + Kind::InboxRelays, + Kind::UserStatus, + ]; let filter = Filter::new() .authors(buffer.iter().cloned()) - .limit(100) - .kinds(vec![Kind::Metadata, Kind::UserStatus]); + .limit(buffer.len() * kinds.len()) + .kinds(kinds); if let Err(e) = client .subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts)) diff --git a/crates/app/src/views/chat.rs b/crates/app/src/views/chat.rs index 1c63b25..9f3e182 100644 --- a/crates/app/src/views/chat.rs +++ b/crates/app/src/views/chat.rs @@ -467,7 +467,7 @@ impl Panel for Chat { .child(img(face).size_4()) })), ) - .when_some(this.name(), |this, name| this.child(name)) + .when_some(this.subject(), |this, name| this.child(name)) .into_any() }) } diff --git a/crates/app/src/views/sidebar/compose.rs b/crates/app/src/views/sidebar/compose.rs index 741bb93..463b74c 100644 --- a/crates/app/src/views/sidebar/compose.rs +++ b/crates/app/src/views/sidebar/compose.rs @@ -1,17 +1,20 @@ -use chats::{room::Room, ChatRegistry}; +use chats::{ + room::{Room, RoomKind}, + ChatRegistry, +}; use common::{profile::NostrProfile, utils::random_name}; use global::get_client; use gpui::{ div, img, impl_internal_actions, prelude::FluentBuilder, px, relative, uniform_list, App, - AppContext, Context, Entity, FocusHandle, InteractiveElement, IntoElement, ParentElement, - Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Task, TextAlign, - Window, + AppContext, ClickEvent, Context, Div, Entity, FocusHandle, InteractiveElement, IntoElement, + ParentElement, Render, RenderOnce, SharedString, StatefulInteractiveElement, Styled, + Subscription, Task, TextAlign, Window, }; use nostr_sdk::prelude::*; use serde::Deserialize; use smallvec::{smallvec, SmallVec}; use smol::Timer; -use std::{collections::HashSet, time::Duration}; +use std::{collections::HashSet, rc::Rc, time::Duration}; use ui::{ button::{Button, ButtonRounded}, input::{InputEvent, TextInput}, @@ -153,7 +156,7 @@ impl Compose { let signer = client.signer().await?; // [IMPORTANT] // Make sure this event is never send, - // this event existed just use for convert to Coop's Chat Room later. + // this event existed just use for convert to Coop's Room later. let event = EventBuilder::private_msg_rumor(*pubkeys.last().unwrap(), "") .tags(tags) .sign(&signer) @@ -165,17 +168,16 @@ impl Compose { cx.spawn_in(window, async move |this, cx| { if let Ok(event) = event.await { cx.update(|window, cx| { - // Stop loading spinner this.update(cx, |this, cx| { this.set_submitting(false, cx); }) .ok(); let chats = ChatRegistry::global(cx); - let room = Room::new(&event, cx); + let room = Room::new(&event, RoomKind::Ongoing); - chats.update(cx, |state, cx| { - match state.push_room(room, cx) { + chats.update(cx, |chats, cx| { + match chats.push(room, cx) { Ok(_) => { // TODO: automatically open newly created chat panel window.close_modal(cx); @@ -500,3 +502,64 @@ impl Render for Compose { ) } } + +type Handler = Rc; + +#[derive(IntoElement)] +pub struct ComposeButton { + base: Div, + label: SharedString, + handler: Handler, +} + +impl ComposeButton { + pub fn new(label: impl Into) -> Self { + Self { + base: div(), + label: label.into(), + handler: Rc::new(|_, _, _| {}), + } + } + + pub fn on_click( + mut self, + handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.handler = Rc::new(handler); + self + } +} + +impl RenderOnce for ComposeButton { + fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + let handler = self.handler.clone(); + + self.base + .id("compose") + .flex() + .items_center() + .gap_2() + .px_1() + .h_7() + .text_xs() + .font_semibold() + .rounded(px(cx.theme().radius)) + .hover(|this| this.bg(cx.theme().base.step(cx, ColorScaleStep::THREE))) + .child( + div() + .size_6() + .flex() + .items_center() + .justify_center() + .rounded_full() + .bg(cx.theme().accent.step(cx, ColorScaleStep::NINE)) + .child( + Icon::new(IconName::ComposeFill) + .small() + .text_color(cx.theme().base.darken(cx)), + ), + ) + .child(self.label.clone()) + .on_click(move |ev, window, cx| handler(ev, window, cx)) + } +} diff --git a/crates/app/src/views/sidebar/folder.rs b/crates/app/src/views/sidebar/folder.rs new file mode 100644 index 0000000..090a9b1 --- /dev/null +++ b/crates/app/src/views/sidebar/folder.rs @@ -0,0 +1,323 @@ +use std::rc::Rc; + +use gpui::{ + div, prelude::FluentBuilder, px, App, ClickEvent, Img, InteractiveElement, IntoElement, + ParentElement as _, RenderOnce, SharedString, StatefulInteractiveElement, Styled as _, Window, +}; +use ui::{ + theme::{scale::ColorScaleStep, ActiveTheme}, + Collapsible, Icon, IconName, StyledExt, +}; + +type Handler = Rc; + +#[derive(IntoElement)] +pub struct Parent { + icon: Option, + active_icon: Option, + label: SharedString, + items: Vec, + collapsed: bool, + handler: Handler, +} + +impl Parent { + pub fn new(label: impl Into) -> Self { + Self { + label: label.into(), + icon: None, + active_icon: None, + items: Vec::new(), + collapsed: false, + handler: Rc::new(|_, _, _| {}), + } + } + + pub fn icon(mut self, icon: impl Into) -> Self { + self.icon = Some(icon.into()); + self + } + + pub fn active_icon(mut self, icon: impl Into) -> Self { + self.active_icon = Some(icon.into()); + self + } + + pub fn collapsed(mut self, collapsed: bool) -> Self { + self.collapsed = collapsed; + self + } + + pub fn child(mut self, child: impl Into) -> Self { + self.items.push(child.into()); + self + } + + #[allow(dead_code)] + pub fn children(mut self, children: impl IntoIterator>) -> Self { + self.items = children.into_iter().map(Into::into).collect(); + self + } + + pub fn on_click( + mut self, + handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.handler = Rc::new(handler); + self + } +} + +impl Collapsible for Parent { + fn is_collapsed(&self) -> bool { + self.collapsed + } + + fn collapsed(mut self, collapsed: bool) -> Self { + self.collapsed = collapsed; + self + } +} + +impl RenderOnce for Parent { + fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + let handler = self.handler.clone(); + + div() + .flex() + .flex_col() + .gap_1() + .child( + div() + .id(self.label.clone()) + .flex() + .items_center() + .gap_2() + .px_2() + .h_6() + .rounded(px(cx.theme().radius)) + .text_xs() + .text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN)) + .font_semibold() + .when_some(self.icon, |this, icon| { + this.map(|this| { + if self.collapsed { + this.child(icon.size_4()) + } else { + this.when_some(self.active_icon, |this, icon| { + this.child(icon.size_4()) + }) + } + }) + }) + .child(self.label.clone()) + .hover(|this| this.bg(cx.theme().base.step(cx, ColorScaleStep::THREE))) + .on_click(move |ev, window, cx| handler(ev, window, cx)), + ) + .when(!self.collapsed, |this| { + this.child(div().flex().flex_col().gap_1().pl_3().children(self.items)) + }) + } +} + +#[derive(IntoElement)] +pub struct Folder { + icon: Option, + active_icon: Option, + label: SharedString, + items: Vec, + collapsed: bool, + handler: Handler, +} + +impl Folder { + pub fn new(label: impl Into) -> Self { + Self { + label: label.into(), + icon: None, + active_icon: None, + items: Vec::new(), + collapsed: false, + handler: Rc::new(|_, _, _| {}), + } + } + + pub fn icon(mut self, icon: impl Into) -> Self { + self.icon = Some(icon.into()); + self + } + + pub fn active_icon(mut self, icon: impl Into) -> Self { + self.active_icon = Some(icon.into()); + self + } + + pub fn collapsed(mut self, collapsed: bool) -> Self { + self.collapsed = collapsed; + self + } + + pub fn children(mut self, children: impl IntoIterator>) -> Self { + self.items = children.into_iter().map(Into::into).collect(); + self + } + + pub fn on_click( + mut self, + handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.handler = Rc::new(handler); + self + } +} + +impl Collapsible for Folder { + fn is_collapsed(&self) -> bool { + self.collapsed + } + + fn collapsed(mut self, collapsed: bool) -> Self { + self.collapsed = collapsed; + self + } +} + +impl RenderOnce for Folder { + fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + let handler = self.handler.clone(); + + div() + .flex() + .flex_col() + .gap_1() + .child( + div() + .id(self.label.clone()) + .flex() + .items_center() + .gap_2() + .px_2() + .h_6() + .rounded(px(cx.theme().radius)) + .text_xs() + .text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN)) + .font_semibold() + .when_some(self.icon, |this, icon| { + this.map(|this| { + if self.collapsed { + this.child(icon.size_4()) + } else { + this.when_some(self.active_icon, |this, icon| { + this.child(icon.size_4()) + }) + } + }) + }) + .child(self.label.clone()) + .hover(|this| this.bg(cx.theme().base.step(cx, ColorScaleStep::THREE))) + .on_click(move |ev, window, cx| handler(ev, window, cx)), + ) + .when(!self.collapsed, |this| { + this.child(div().flex().flex_col().gap_1().pl_6().children(self.items)) + }) + } +} + +#[derive(IntoElement)] +pub struct FolderItem { + ix: usize, + img: Option, + label: Option, + description: Option, + handler: Handler, +} + +impl FolderItem { + pub fn new(ix: usize) -> Self { + Self { + ix, + img: None, + label: None, + description: None, + handler: Rc::new(|_, _, _| {}), + } + } + + pub fn label(mut self, label: impl Into) -> Self { + self.label = Some(label.into()); + self + } + + pub fn description(mut self, description: impl Into) -> Self { + self.description = Some(description.into()); + self + } + + pub fn img(mut self, img: Option) -> Self { + self.img = img; + self + } + + pub fn on_click( + mut self, + handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.handler = Rc::new(handler); + self + } +} + +impl RenderOnce for FolderItem { + fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + let handler = self.handler.clone(); + + div() + .id(self.ix) + .h_6() + .px_2() + .w_full() + .flex() + .items_center() + .justify_between() + .text_xs() + .rounded(px(cx.theme().radius)) + .child( + div() + .flex_1() + .flex() + .items_center() + .gap_2() + .truncate() + .font_medium() + .map(|this| { + if let Some(img) = self.img { + this.child(img.size_4().flex_shrink_0()) + } else { + this.child( + div() + .flex() + .justify_center() + .items_center() + .size_4() + .rounded_full() + .bg(cx.theme().accent.step(cx, ColorScaleStep::THREE)) + .child(Icon::new(IconName::GroupFill).size_2().text_color( + cx.theme().accent.step(cx, ColorScaleStep::TWELVE), + )), + ) + } + }) + .when_some(self.label, |this, label| this.child(label)), + ) + .when_some(self.description, |this, description| { + this.child( + div() + .flex_shrink_0() + .text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN)) + .child(description), + ) + }) + .hover(|this| this.bg(cx.theme().base.step(cx, ColorScaleStep::FOUR))) + .on_click(move |ev, window, cx| handler(ev, window, cx)) + } +} diff --git a/crates/app/src/views/sidebar/mod.rs b/crates/app/src/views/sidebar/mod.rs index 9054ae6..da11d0d 100644 --- a/crates/app/src/views/sidebar/mod.rs +++ b/crates/app/src/views/sidebar/mod.rs @@ -1,23 +1,28 @@ -use chats::{room::Room, ChatRegistry}; -use compose::Compose; +use chats::{ + room::{Room, RoomKind}, + ChatRegistry, +}; +use compose::{Compose, ComposeButton}; +use folder::{Folder, FolderItem, Parent}; use gpui::{ - div, img, percentage, prelude::FluentBuilder, px, relative, uniform_list, AnyElement, App, - AppContext, Context, Div, Empty, Entity, EventEmitter, FocusHandle, Focusable, - InteractiveElement, IntoElement, ParentElement, Render, SharedString, Stateful, - StatefulInteractiveElement, Styled, Window, + div, img, prelude::FluentBuilder, px, AnyElement, App, AppContext, Context, Entity, + EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement, Render, SharedString, Styled, + Window, }; use ui::{ button::{Button, ButtonRounded, ButtonVariants}, dock_area::panel::{Panel, PanelEvent}, popup_menu::PopupMenu, + scroll::ScrollbarAxis, skeleton::Skeleton, theme::{scale::ColorScaleStep, ActiveTheme}, - ContextModal, Disableable, Icon, IconName, Sizable, StyledExt, + ContextModal, Disableable, IconName, StyledExt, }; use crate::chat_space::{AddPanel, PanelKind}; mod compose; +mod folder; pub fn init(window: &mut Window, cx: &mut App) -> Entity { Sidebar::new(window, cx) @@ -26,8 +31,10 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity { pub struct Sidebar { name: SharedString, focus_handle: FocusHandle, - label: SharedString, - is_collapsed: bool, + ongoing: bool, + incoming: bool, + trusted: bool, + unknown: bool, } impl Sidebar { @@ -37,13 +44,14 @@ impl Sidebar { fn view(_window: &mut Window, cx: &mut Context) -> Self { let focus_handle = cx.focus_handle(); - let label = SharedString::from("Inbox"); Self { - name: "Sidebar".into(), - is_collapsed: false, + name: "Chat Sidebar".into(), + ongoing: false, + incoming: false, + trusted: true, + unknown: true, focus_handle, - label, } } @@ -80,63 +88,37 @@ impl Sidebar { }) } - fn render_room(&self, ix: usize, room: &Entity, cx: &Context) -> Stateful
{ - let room = room.read(cx); - - div() - .id(ix) - .px_1() - .h_8() - .w_full() - .flex() - .items_center() - .justify_between() - .text_xs() - .rounded(px(cx.theme().radius)) - .hover(|this| this.bg(cx.theme().base.step(cx, ColorScaleStep::FOUR))) - .child(div().flex_1().truncate().font_medium().map(|this| { - if room.is_group() { - this.flex() - .items_center() - .gap_2() - .child( - div() - .flex() - .justify_center() - .items_center() - .size_6() - .rounded_full() - .bg(cx.theme().accent.step(cx, ColorScaleStep::THREE)) - .child(Icon::new(IconName::GroupFill).size_3().text_color( - cx.theme().accent.step(cx, ColorScaleStep::TWELVE), - )), - ) - .when_some(room.name(), |this, name| this.child(name)) - } else { - this.when_some(room.first_member(), |this, member| { - this.flex() - .items_center() - .gap_2() - .child(img(member.avatar.clone()).size_6().flex_shrink_0()) - .child(member.name.clone()) - }) - } - })) - .child( - div() - .flex_shrink_0() - .text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN)) - .child(room.ago()), - ) - .on_click({ - let id = room.id; - - cx.listener(move |this, _, window, cx| { - this.open(id, window, cx); - }) - }) + fn open_room(&self, id: u64, window: &mut Window, cx: &mut Context) { + window.dispatch_action( + Box::new(AddPanel::new( + PanelKind::Room(id), + ui::dock_area::dock::DockPlacement::Center, + )), + cx, + ); } + fn ongoing(&mut self, cx: &mut Context) { + self.ongoing = !self.ongoing; + cx.notify(); + } + + fn incoming(&mut self, cx: &mut Context) { + self.incoming = !self.incoming; + cx.notify(); + } + + fn trusted(&mut self, cx: &mut Context) { + self.trusted = !self.trusted; + cx.notify(); + } + + fn unknown(&mut self, cx: &mut Context) { + self.unknown = !self.unknown; + cx.notify(); + } + + #[allow(dead_code)] fn render_skeleton(&self, total: i32) -> impl IntoIterator { (0..total).map(|_| { div() @@ -151,20 +133,49 @@ impl Sidebar { }) } - fn open(&self, id: u64, window: &mut Window, cx: &mut Context) { - window.dispatch_action( - Box::new(AddPanel::new( - PanelKind::Room(id), - ui::dock_area::dock::DockPlacement::Center, - )), - cx, - ); + fn render_items(rooms: &Vec<&Entity>, cx: &Context) -> Vec { + let mut items = Vec::with_capacity(rooms.len()); + + for room in rooms { + let room = room.read(cx); + let room_id = room.id; + let ago = room.last_seen().ago(); + let Some(member) = room.first_member() else { + continue; + }; + + let label = if room.is_group() { + room.subject().unwrap_or("Unnamed".into()) + } else { + member.name.clone() + }; + + let img = if !room.is_group() { + Some(img(member.avatar.clone())) + } else { + None + }; + + let item = FolderItem::new(room_id as usize) + .label(label) + .description(ago) + .img(img) + .on_click({ + cx.listener(move |this, _, window, cx| { + this.open_room(room_id, window, cx); + }) + }); + + items.push(item); + } + + items } } impl Panel for Sidebar { fn panel_id(&self) -> SharedString { - "Sidebar".into() + self.name.clone() } fn title(&self, _cx: &App) -> AnyElement { @@ -190,150 +201,77 @@ impl Focusable for Sidebar { impl Render for Sidebar { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let entity = cx.entity(); + let registry = ChatRegistry::global(cx).read(cx); + let rooms = registry.rooms(cx); + let loading = registry.loading(); + + let ongoing = rooms.get(&RoomKind::Ongoing); + let trusted = rooms.get(&RoomKind::Trusted); + let unknown = rooms.get(&RoomKind::Unknown); div() + .scrollable(cx.entity_id(), ScrollbarAxis::Vertical) + .size_full() .flex() .flex_col() - .size_full() - .child( - div() - .px_2() - .py_3() - .w_full() - .flex_shrink_0() - .flex() - .flex_col() - .gap_1() + .gap_3() + .px_2() + .py_3() + .child(ComposeButton::new("New Message").on_click(cx.listener( + |this, _, window, cx| { + this.render_compose(window, cx); + }, + ))) + .map(|this| { + if loading { + this.children(self.render_skeleton(6)) + } else { + this.when_some(ongoing, |this, rooms| { + this.child( + Folder::new("Ongoing") + .icon(IconName::FolderFill) + .active_icon(IconName::FolderOpenFill) + .collapsed(self.ongoing) + .on_click(cx.listener(move |this, _, _, cx| { + this.ongoing(cx); + })) + .children(Self::render_items(rooms, cx)), + ) + }) .child( - div() - .id("new_message") - .flex() - .items_center() - .gap_2() - .px_1() - .h_7() - .text_xs() - .font_semibold() - .rounded(px(cx.theme().radius)) - .child( - div() - .size_6() - .flex() - .items_center() - .justify_center() - .rounded_full() - .bg(cx.theme().accent.step(cx, ColorScaleStep::NINE)) - .child( - Icon::new(IconName::ComposeFill) - .small() - .text_color(cx.theme().base.darken(cx)), - ), - ) - .child("New Message") - .hover(|this| this.bg(cx.theme().base.step(cx, ColorScaleStep::THREE))) - .on_click(cx.listener(|this, _, window, cx| { - // Open compose modal - this.render_compose(window, cx); - })), - ) - .child(Empty), - ) - .child( - div() - .px_2() - .w_full() - .flex_1() - .flex() - .flex_col() - .gap_1() - .child( - div() - .id("inbox_header") - .px_1() - .h_7() - .flex() - .items_center() - .flex_shrink_0() - .rounded(px(cx.theme().radius)) - .text_xs() - .font_semibold() - .child( - Icon::new(IconName::ChevronDown) - .size_6() - .when(self.is_collapsed, |this| { - this.rotate(percentage(270. / 360.)) - }), - ) - .child(self.label.clone()) - .hover(|this| this.bg(cx.theme().base.step(cx, ColorScaleStep::THREE))) - .on_click(cx.listener(move |view, _event, _window, cx| { - view.is_collapsed = !view.is_collapsed; - cx.notify(); - })), - ) - .when(!self.is_collapsed, |this| { - this.flex_1().w_full().map(|this| { - let state = ChatRegistry::global(cx); - let is_loading = state.read(cx).is_loading(); - let len = state.read(cx).rooms().len(); - - if is_loading { - this.children(self.render_skeleton(5)) - } else if state.read(cx).rooms().is_empty() { + Parent::new("Incoming") + .icon(IconName::FolderFill) + .active_icon(IconName::FolderOpenFill) + .collapsed(self.incoming) + .on_click(cx.listener(move |this, _, _, cx| { + this.incoming(cx); + })) + .when_some(trusted, |this, rooms| { this.child( - div() - .px_1() - .w_full() - .h_20() - .flex() - .flex_col() - .items_center() - .justify_center() - .text_center() - .rounded(px(cx.theme().radius)) - .bg(cx.theme().base.step(cx, ColorScaleStep::THREE)) - .child( - div() - .text_xs() - .font_semibold() - .line_height(relative(1.2)) - .child("No chats"), - ) - .child( - div() - .text_xs() - .text_color( - cx.theme() - .base - .step(cx, ColorScaleStep::ELEVEN), - ) - .child("Recent chats will appear here."), - ), + Folder::new("Trusted") + .icon(IconName::FolderFill) + .active_icon(IconName::FolderOpenFill) + .collapsed(self.trusted) + .on_click(cx.listener(move |this, _, _, cx| { + this.trusted(cx); + })) + .children(Self::render_items(rooms, cx)), ) - } else { + }) + .when_some(unknown, |this, rooms| { this.child( - uniform_list( - entity, - "rooms", - len, - move |this, range, _, cx| { - let mut items = vec![]; - - for ix in range { - if let Some(room) = state.read(cx).rooms().get(ix) { - items.push(this.render_room(ix, room, cx)); - } - } - - items - }, - ) - .size_full(), + Folder::new("Unknown") + .icon(IconName::FolderFill) + .active_icon(IconName::FolderOpenFill) + .collapsed(self.unknown) + .on_click(cx.listener(move |this, _, _, cx| { + this.unknown(cx); + })) + .children(Self::render_items(rooms, cx)), ) - } - }) - }), - ) + }), + ) + } + }) } } diff --git a/crates/chats/src/lib.rs b/crates/chats/src/lib.rs index 0359ba8..e58ced0 100644 --- a/crates/chats/src/lib.rs +++ b/crates/chats/src/lib.rs @@ -1,11 +1,13 @@ -use std::cmp::Reverse; +use std::{cmp::Reverse, collections::HashMap}; use anyhow::anyhow; use common::{last_seen::LastSeen, utils::room_hash}; use global::get_client; -use gpui::{App, AppContext, Context, Entity, Global, Task, Window}; +use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task, Window}; use itertools::Itertools; use nostr_sdk::prelude::*; +use room::RoomKind; +use smallvec::{smallvec, SmallVec}; use crate::room::Room; @@ -13,7 +15,7 @@ pub mod message; pub mod room; pub fn init(cx: &mut App) { - ChatRegistry::set_global(cx.new(|_| ChatRegistry::new()), cx); + ChatRegistry::set_global(cx.new(ChatRegistry::new), cx); } struct GlobalChatRegistry(Entity); @@ -22,7 +24,9 @@ impl Global for GlobalChatRegistry {} pub struct ChatRegistry { rooms: Vec>, - is_loading: bool, + loading: bool, + #[allow(dead_code)] + subscriptions: SmallVec<[Subscription; 1]>, } impl ChatRegistry { @@ -34,21 +38,40 @@ impl ChatRegistry { cx.set_global(GlobalChatRegistry(state)); } - fn new() -> Self { + fn new(cx: &mut Context) -> Self { + let mut subscriptions = smallvec![]; + + subscriptions.push(cx.observe_new::(|this, _, cx| { + let load_metadata = this.load_metadata(cx); + + cx.spawn(async move |this, cx| { + if let Ok(profiles) = load_metadata.await { + cx.update(|cx| { + this.update(cx, |this, cx| { + this.update_members(profiles, cx); + }) + .ok(); + }) + .ok(); + } + }) + .detach(); + })); + Self { rooms: vec![], - is_loading: true, + loading: true, + subscriptions, } } - pub fn current_rooms_ids(&self, cx: &mut Context) -> Vec { - self.rooms.iter().map(|room| room.read(cx).id).collect() - } - - pub fn load_chat_rooms(&mut self, window: &mut Window, cx: &mut Context) { + pub fn load_rooms(&mut self, window: &mut Window, cx: &mut Context) { let client = get_client(); + let room_ids = self.room_ids(cx); - let task: Task, Error>> = cx.background_spawn(async move { + type LoadResult = Result, Error>; + + let task: Task = cx.background_spawn(async move { let signer = client.signer().await?; let public_key = signer.get_public_key().await?; @@ -64,11 +87,31 @@ impl ChatRegistry { let recv_events = client.database().query(recv).await?; let events = send_events.merge(recv_events); - let result: Vec = events + let mut room_map: HashMap = HashMap::new(); + + for event in events .into_iter() .filter(|ev| ev.tags.public_keys().peekable().peek().is_some()) - .unique_by(room_hash) - .sorted_by_key(|ev| Reverse(ev.created_at)) + { + let hash = room_hash(&event); + + if !room_ids.iter().any(|id| id == &hash) { + let filter = Filter::new().kind(Kind::ContactList).pubkey(event.pubkey); + let is_trust = client.database().count(filter).await? >= 1; + + room_map + .entry(hash) + .and_modify(|(_, count, trusted)| { + *count += 1; + *trusted = is_trust; + }) + .or_insert((event, 1, is_trust)); + } + } + + let result: Vec<(Event, usize, bool)> = room_map + .into_values() + .sorted_by_key(|(ev, _, _)| Reverse(ev.created_at)) .collect(); Ok(result) @@ -76,30 +119,27 @@ impl ChatRegistry { cx.spawn_in(window, async move |this, cx| { if let Ok(events) = task.await { + let rooms: Vec> = events + .into_iter() + .map(|(event, count, trusted)| { + let kind = if count > 2 { + // If frequency count is greater than 2, mark this room as ongoing + RoomKind::Ongoing + } else if trusted { + RoomKind::Trusted + } else { + RoomKind::Unknown + }; + + cx.new(|_| Room::new(&event, kind)).unwrap() + }) + .collect(); + cx.update(|_, cx| { this.update(cx, |this, cx| { - if !events.is_empty() { - let current_ids = this.current_rooms_ids(cx); - let items: Vec> = events - .into_iter() - .filter_map(|ev| { - let new = room_hash(&ev); - // Filter all seen rooms - if !current_ids.iter().any(|this| this == &new) { - Some(Room::new(&ev, cx)) - } else { - None - } - }) - .collect(); - - this.is_loading = false; - this.rooms.extend(items); - this.rooms - .sort_by_key(|room| Reverse(room.read(cx).last_seen())); - } else { - this.is_loading = false; - } + this.rooms.extend(rooms); + this.rooms.sort_by_key(|r| Reverse(r.read(cx).last_seen())); + this.loading = false; cx.notify(); }) @@ -111,14 +151,40 @@ impl ChatRegistry { .detach(); } - pub fn rooms(&self) -> &[Entity] { - &self.rooms + /// Get the IDs of all rooms. + pub fn room_ids(&self, cx: &mut Context) -> Vec { + self.rooms.iter().map(|room| room.read(cx).id).collect() } - pub fn is_loading(&self) -> bool { - self.is_loading + /// Get all rooms. + pub fn rooms(&self, cx: &App) -> HashMap>> { + let mut groups = HashMap::new(); + groups.insert(RoomKind::Ongoing, Vec::new()); + groups.insert(RoomKind::Trusted, Vec::new()); + groups.insert(RoomKind::Unknown, Vec::new()); + + for room in self.rooms.iter() { + let kind = room.read(cx).kind(); + groups.entry(kind).or_insert_with(Vec::new).push(room); + } + + groups } + /// Get rooms by their kind. + pub fn rooms_by_kind(&self, kind: RoomKind, cx: &App) -> Vec<&Entity> { + self.rooms + .iter() + .filter(|room| room.read(cx).kind() == kind) + .collect() + } + + /// Get the loading status of the rooms. + pub fn loading(&self) -> bool { + self.loading + } + + /// Get a room by its ID. pub fn get(&self, id: &u64, cx: &App) -> Option> { self.rooms .iter() @@ -126,11 +192,10 @@ impl ChatRegistry { .cloned() } - pub fn push_room( - &mut self, - room: Entity, - cx: &mut Context, - ) -> Result<(), anyhow::Error> { + /// Push a room to the list. + pub fn push(&mut self, room: Room, cx: &mut Context) -> Result<(), anyhow::Error> { + let room = cx.new(|_| room); + if !self .rooms .iter() @@ -145,6 +210,7 @@ impl ChatRegistry { } } + /// Push a message to a room. pub fn push_message(&mut self, event: Event, window: &mut Window, cx: &mut Context) { let id = room_hash(&event); @@ -158,7 +224,7 @@ impl ChatRegistry { self.rooms .sort_by_key(|room| Reverse(room.read(cx).last_seen())); } else { - let new_room = Room::new(&event, cx); + let new_room = cx.new(|_| Room::new(&event, RoomKind::default())); // Push the new room to the front of the list self.rooms.insert(0, new_room); diff --git a/crates/chats/src/room.rs b/crates/chats/src/room.rs index 90b4657..915e003 100644 --- a/crates/chats/src/room.rs +++ b/crates/chats/src/room.rs @@ -7,10 +7,10 @@ use common::{ utils::{compare, room_hash}, }; use global::get_client; -use gpui::{App, AppContext, Context, Entity, EventEmitter, SharedString, Task, Window}; +use gpui::{App, AppContext, Context, EventEmitter, SharedString, Task, Window}; use itertools::Itertools; use nostr_sdk::prelude::*; -use smallvec::{smallvec, SmallVec}; +use smallvec::SmallVec; use crate::message::{Message, RoomMessage}; @@ -19,13 +19,25 @@ pub struct IncomingEvent { pub event: RoomMessage, } +#[derive(Clone, Copy, Hash, Debug, PartialEq, Eq, Default)] +pub enum RoomKind { + Ongoing, + Trusted, + #[default] + Unknown, +} + pub struct Room { pub id: u64, pub last_seen: LastSeen, /// Subject of the room - pub name: Option, + pub subject: Option, /// All members of the room pub members: Arc>, + /// Kind + pub kind: RoomKind, + /// All public keys of the room members + pubkeys: Vec, } impl EventEmitter for Room {} @@ -37,66 +49,34 @@ impl PartialEq for Room { } impl Room { - pub fn new(event: &Event, cx: &mut App) -> Entity { + /// Create a new room from an Nostr Event + pub fn new(event: &Event, kind: RoomKind) -> Self { let id = room_hash(event); let last_seen = LastSeen(event.created_at); // Get the subject from the event's tags - let name = if let Some(tag) = event.tags.find(TagKind::Subject) { + let subject = if let Some(tag) = event.tags.find(TagKind::Subject) { tag.content().map(|s| s.to_owned().into()) } else { None }; - // Create a task for loading metadata - let load_metadata = Self::load_metadata(event, cx); + // Get all public keys from the event's tags + let mut pubkeys = vec![]; + pubkeys.extend(event.tags.public_keys().collect::>()); + pubkeys.push(event.pubkey); - // Create a new GPUI's Entity - cx.new(|cx| { - let this = Self { - id, - last_seen, - name, - members: Arc::new(smallvec![]), - }; - - cx.spawn(async move |this, cx| { - if let Ok(profiles) = load_metadata.await { - cx.update(|cx| { - this.update(cx, |this: &mut Room, cx| { - // Update the room's name if it's not already set - if this.name.is_none() { - let mut name = profiles - .iter() - .take(2) - .map(|profile| profile.name.to_string()) - .collect::>() - .join(", "); - - if profiles.len() > 2 { - name = format!("{}, +{}", name, profiles.len() - 2); - } - - this.name = Some(name.into()) - }; - - let mut new_members = SmallVec::new(); - new_members.extend(profiles); - this.members = Arc::new(new_members); - - cx.notify(); - }) - .ok(); - }) - .ok(); - } - }) - .detach(); - - this - }) + Self { + id, + last_seen, + subject, + kind, + members: Arc::new(SmallVec::with_capacity(pubkeys.len())), + pubkeys, + } } + /// Get room's id pub fn id(&self) -> u64 { self.id } @@ -116,12 +96,17 @@ impl Room { /// Collect room's member's public keys pub fn public_keys(&self) -> Vec { - self.members.iter().map(|m| m.public_key).collect() + self.pubkeys.clone() } /// Get room's display name - pub fn name(&self) -> Option { - self.name.clone() + pub fn subject(&self) -> Option { + self.subject.clone() + } + + /// Get room's kind + pub fn kind(&self) -> RoomKind { + self.kind } /// Determine if room is a group @@ -145,6 +130,31 @@ impl Room { self.last_seen.ago() } + pub fn update_members(&mut self, profiles: Vec, cx: &mut Context) { + // Update the room's name if it's not already set + if self.subject.is_none() { + // Merge all members into a single name + let mut name = profiles + .iter() + .take(2) + .map(|profile| profile.name.to_string()) + .collect::>() + .join(", "); + + // Create a specific name for group + if profiles.len() > 2 { + name = format!("{}, +{}", name, profiles.len() - 2); + } + + self.subject = Some(name.into()); + }; + + // Update the room's members + self.members = Arc::new(profiles.into()); + + cx.notify(); + } + /// Verify messaging_relays for all room's members pub fn messaging_relays(&self, cx: &App) -> Task, Error>> { let client = get_client(); @@ -208,6 +218,38 @@ impl Room { }) } + /// Load metadata for all members + pub fn load_metadata(&self, cx: &mut Context) -> Task, Error>> { + let client = get_client(); + let pubkeys = self.public_keys(); + + cx.background_spawn(async move { + let signer = client.signer().await?; + let signer_pubkey = signer.get_public_key().await?; + let mut profiles = Vec::with_capacity(pubkeys.len()); + + for public_key in pubkeys.into_iter() { + let metadata = client + .database() + .metadata(public_key) + .await? + .unwrap_or_default(); + + // Convert metadata to profile + let profile = NostrProfile::new(public_key, metadata); + + if public_key == signer_pubkey { + // Room's owner always push to the end of the vector + profiles.push(profile); + } else { + profiles.insert(0, profile); + } + } + + Ok(profiles) + }) + } + /// Load room messages pub fn load_messages(&self, cx: &App) -> Task, Error>> { let client = get_client(); @@ -350,40 +392,4 @@ impl Room { }) .detach(); } - - /// Load metadata for all members - fn load_metadata(event: &Event, cx: &App) -> Task, Error>> { - let client = get_client(); - let mut pubkeys = vec![]; - - // Get all pubkeys from event's tags - pubkeys.extend(event.tags.public_keys().collect::>()); - pubkeys.push(event.pubkey); - - cx.background_spawn(async move { - let signer = client.signer().await?; - let signer_pubkey = signer.get_public_key().await?; - let mut profiles = Vec::with_capacity(pubkeys.len()); - - for public_key in pubkeys.into_iter() { - let metadata = client - .database() - .metadata(public_key) - .await? - .unwrap_or_default(); - - // Convert metadata to profile - let profile = NostrProfile::new(public_key, metadata); - - if public_key == signer_pubkey { - // Room's owner always push to the end of the vector - profiles.push(profile); - } else { - profiles.insert(0, profile); - } - } - - Ok(profiles) - }) - } } diff --git a/crates/global/src/constants.rs b/crates/global/src/constants.rs index 5adcc14..8d914b9 100644 --- a/crates/global/src/constants.rs +++ b/crates/global/src/constants.rs @@ -5,9 +5,9 @@ pub const APP_ID: &str = "su.reya.coop"; pub const BOOTSTRAP_RELAYS: [&str; 5] = [ "wss://relay.damus.io", "wss://relay.primal.net", - "wss://purplepag.es", "wss://user.kindpag.es", "wss://relaydiscovery.com", + "wss://purplepag.es", ]; /// Subscriptions diff --git a/crates/ui/src/icon.rs b/crates/ui/src/icon.rs index d70bf04..f21071d 100644 --- a/crates/ui/src/icon.rs +++ b/crates/ui/src/icon.rs @@ -20,6 +20,7 @@ pub enum IconName { Bell, BookOpen, Bot, + BubbleFill, Calendar, ChartPie, Check, @@ -42,6 +43,9 @@ pub enum IconName { Eye, EyeOff, Frame, + Folder, + FolderFill, + FolderOpenFill, GalleryVerticalEnd, GitHub, Globe, @@ -104,6 +108,7 @@ impl IconName { Self::Bell => "icons/bell.svg", Self::BookOpen => "icons/book-open.svg", Self::Bot => "icons/bot.svg", + Self::BubbleFill => "icons/bubble-fill.svg", Self::Calendar => "icons/calendar.svg", Self::ChartPie => "icons/chart-pie.svg", Self::Check => "icons/check.svg", @@ -126,6 +131,9 @@ impl IconName { Self::Eye => "icons/eye.svg", Self::EyeOff => "icons/eye-off.svg", Self::Frame => "icons/frame.svg", + Self::Folder => "icons/folder.svg", + Self::FolderFill => "icons/folder-fill.svg", + Self::FolderOpenFill => "icons/folder-open-fill.svg", Self::GalleryVerticalEnd => "icons/gallery-vertical-end.svg", Self::GitHub => "icons/github.svg", Self::Globe => "icons/globe.svg", diff --git a/crates/ui/src/scroll/scrollable.rs b/crates/ui/src/scroll/scrollable.rs index b09068a..2b263df 100644 --- a/crates/ui/src/scroll/scrollable.rs +++ b/crates/ui/src/scroll/scrollable.rs @@ -209,8 +209,8 @@ where ), ) .into_any_element(); - let element_id = element.request_layout(window, cx); + let element_id = element.request_layout(window, cx); let layout_id = window.request_layout(style, vec![element_id], cx); (layout_id, element) diff --git a/crates/ui/src/scroll/scrollbar.rs b/crates/ui/src/scroll/scrollbar.rs index c1b2ee2..2f343ef 100644 --- a/crates/ui/src/scroll/scrollbar.rs +++ b/crates/ui/src/scroll/scrollbar.rs @@ -36,8 +36,8 @@ pub(crate) const WIDTH: Pixels = px(12.); const MIN_THUMB_SIZE: f32 = 80.; const THUMB_RADIUS: Pixels = Pixels(4.0); const THUMB_INSET: Pixels = Pixels(3.); -const FADE_OUT_DURATION: f32 = 3.0; -const FADE_OUT_DELAY: f32 = 2.0; +const FADE_OUT_DURATION: f32 = 2.0; +const FADE_OUT_DELAY: f32 = 1.2; pub trait ScrollHandleOffsetable { fn offset(&self) -> Point;