From 3246abace14cc566ccbfc335007160c34e04f9e3 Mon Sep 17 00:00:00 2001 From: reya <123083837+reyamir@users.noreply.github.com> Date: Thu, 10 Apr 2025 08:10:53 +0700 Subject: [PATCH] refactor chats (#15) * refactor * update * update * update * remove nostrprofile struct * update * refactor contacts * prevent double login --- Cargo.lock | 509 +++++++++++++++--------- Cargo.toml | 1 + crates/account/src/lib.rs | 35 +- crates/app/src/chat_space.rs | 3 +- crates/app/src/main.rs | 28 +- crates/app/src/views/chat.rs | 60 ++- crates/app/src/views/contacts.rs | 66 ++- crates/app/src/views/login.rs | 2 +- crates/app/src/views/new_account.rs | 2 +- crates/app/src/views/profile.rs | 2 +- crates/app/src/views/sidebar/compose.rs | 65 ++- crates/app/src/views/sidebar/mod.rs | 25 +- crates/chats/Cargo.toml | 1 + crates/chats/src/constants.rs | 5 + crates/chats/src/lib.rs | 239 +++++++---- crates/chats/src/message.rs | 116 +++++- crates/chats/src/room.rs | 491 +++++++++++++++-------- crates/common/src/last_seen.rs | 57 --- crates/common/src/lib.rs | 79 +++- crates/common/src/profile.rs | 33 +- crates/common/src/utils.rs | 82 ---- crates/global/src/lib.rs | 5 +- crates/ui/Cargo.toml | 7 - crates/ui/benches/text_benchmark.rs | 142 ------- crates/ui/src/switch.rs | 2 +- crates/ui/src/text.rs | 16 +- rust-toolchain.toml | 2 +- 27 files changed, 1166 insertions(+), 909 deletions(-) create mode 100644 crates/chats/src/constants.rs delete mode 100644 crates/common/src/last_seen.rs delete mode 100644 crates/common/src/utils.rs delete mode 100644 crates/ui/benches/text_benchmark.rs diff --git a/Cargo.lock b/Cargo.lock index cf0c7bc..e99018c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -97,12 +97,6 @@ dependencies = [ "libc", ] -[[package]] -name = "anes" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" - [[package]] name = "anstream" version = "0.6.18" @@ -750,9 +744,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.11.3" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0" +checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" dependencies = [ "memchr", "serde", @@ -834,12 +828,6 @@ dependencies = [ "wayland-client", ] -[[package]] -name = "cast" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" - [[package]] name = "cbc" version = "0.1.2" @@ -958,6 +946,7 @@ dependencies = [ name = "chats" version = "0.0.0" dependencies = [ + "account", "anyhow", "chrono", "common", @@ -987,33 +976,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "ciborium" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" -dependencies = [ - "ciborium-io", - "ciborium-ll", - "serde", -] - -[[package]] -name = "ciborium-io" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" - -[[package]] -name = "ciborium-ll" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" -dependencies = [ - "ciborium-io", - "half", -] - [[package]] name = "cipher" version = "0.4.4" @@ -1158,7 +1120,7 @@ dependencies = [ [[package]] name = "collections" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#c1259c136e41b5870d15271190198ff0075ba35c" +source = "git+https://github.com/zed-industries/zed#f0b7f355a29486566a8ed7c2be4acad4b305b881" dependencies = [ "indexmap", "rustc-hash 2.1.1", @@ -1317,6 +1279,19 @@ dependencies = [ "libc", ] +[[package]] +name = "core-graphics-helmer-fork" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32eb7c354ae9f6d437a6039099ce7ecd049337a8109b23d73e48e8ffba8e9cd5" +dependencies = [ + "bitflags 2.9.0", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types", + "libc", +] + [[package]] name = "core-graphics-types" version = "0.1.3" @@ -1428,42 +1403,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "criterion" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" -dependencies = [ - "anes", - "cast", - "ciborium", - "clap", - "criterion-plot", - "is-terminal", - "itertools 0.10.5", - "num-traits", - "once_cell", - "oorandom", - "plotters", - "rayon", - "regex", - "serde", - "serde_derive", - "serde_json", - "tinytemplate", - "walkdir", -] - -[[package]] -name = "criterion-plot" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" -dependencies = [ - "cast", - "itertools 0.10.5", -] - [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -1531,6 +1470,16 @@ version = "0.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f211af61d8efdd104f96e57adf5e426ba1bc3ed7a4ead616e15e5881fd79c4d" +[[package]] +name = "ctrlc" +version = "3.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "697b5419f348fd5ae2478e8018cb016c00a5881c7f46c717de98ffd135a5651c" +dependencies = [ + "nix", + "windows-sys 0.59.0", +] + [[package]] name = "data-encoding" version = "2.8.0" @@ -1559,7 +1508,7 @@ dependencies = [ [[package]] name = "derive_refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#c1259c136e41b5870d15271190198ff0075ba35c" +source = "git+https://github.com/zed-industries/zed#f0b7f355a29486566a8ed7c2be4acad4b305b881" dependencies = [ "proc-macro2", "quote", @@ -1619,6 +1568,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + [[package]] name = "displaydoc" version = "0.2.5" @@ -2320,7 +2275,7 @@ dependencies = [ [[package]] name = "gpui" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#c1259c136e41b5870d15271190198ff0075ba35c" +source = "git+https://github.com/zed-industries/zed#f0b7f355a29486566a8ed7c2be4acad4b305b881" dependencies = [ "anyhow", "as-raw-xcb-connection", @@ -2375,6 +2330,7 @@ dependencies = [ "raw-window-handle", "refineable", "resvg", + "scap", "schemars", "seahash", "semantic_version", @@ -2397,8 +2353,8 @@ dependencies = [ "wayland-cursor", "wayland-protocols", "wayland-protocols-plasma", - "windows", - "windows-core", + "windows 0.61.1", + "windows-core 0.61.0", "windows-numerics", "workspace-hack", "x11-clipboard", @@ -2410,7 +2366,7 @@ dependencies = [ [[package]] name = "gpui_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#c1259c136e41b5870d15271190198ff0075ba35c" +source = "git+https://github.com/zed-industries/zed#f0b7f355a29486566a8ed7c2be4acad4b305b881" dependencies = [ "proc-macro2", "quote", @@ -2445,9 +2401,9 @@ dependencies = [ [[package]] name = "half" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7db2ff139bba50379da6aa0766b52fdcb62cb5b263009b09ed58ba604e14bbd1" +checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" dependencies = [ "cfg-if", "crunchy", @@ -2523,12 +2479,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" -[[package]] -name = "hermit-abi" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e" - [[package]] name = "hex" version = "0.4.3" @@ -2640,7 +2590,7 @@ dependencies = [ [[package]] name = "http_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#c1259c136e41b5870d15271190198ff0075ba35c" +source = "git+https://github.com/zed-industries/zed#f0b7f355a29486566a8ed7c2be4acad4b305b881" dependencies = [ "anyhow", "bytes", @@ -2657,7 +2607,7 @@ dependencies = [ [[package]] name = "http_client_tls" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#c1259c136e41b5870d15271190198ff0075ba35c" +source = "git+https://github.com/zed-industries/zed#f0b7f355a29486566a8ed7c2be4acad4b305b881" dependencies = [ "rustls", "rustls-platform-verifier", @@ -2741,7 +2691,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core", + "windows-core 0.61.0", ] [[package]] @@ -3018,17 +2968,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "is-terminal" -version = "0.4.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" -dependencies = [ - "hermit-abi 0.5.0", - "libc", - "windows-sys 0.59.0", -] - [[package]] name = "is-wsl" version = "0.4.0" @@ -3045,15 +2984,6 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.12.1" @@ -3402,7 +3332,7 @@ dependencies = [ [[package]] name = "media" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#c1259c136e41b5870d15271190198ff0075ba35c" +source = "git+https://github.com/zed-industries/zed#f0b7f355a29486566a8ed7c2be4acad4b305b881" dependencies = [ "anyhow", "bindgen 0.71.1", @@ -3478,9 +3408,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.7" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff70ce3e48ae43fa075863cef62e8b43b71a4f2382229920e0df362592919430" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" dependencies = [ "adler2", "simd-adler32", @@ -3583,7 +3513,7 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" [[package]] name = "nostr" version = "0.40.0" -source = "git+https://github.com/rust-nostr/nostr#1347878e90392db54a263d60021fa0410a3b05cb" +source = "git+https://github.com/rust-nostr/nostr#f4f3f50aa637b3d288a6f1cf762260005b7b498a" dependencies = [ "aes", "base64", @@ -3608,7 +3538,7 @@ dependencies = [ [[package]] name = "nostr-connect" version = "0.40.0" -source = "git+https://github.com/rust-nostr/nostr#1347878e90392db54a263d60021fa0410a3b05cb" +source = "git+https://github.com/rust-nostr/nostr#f4f3f50aa637b3d288a6f1cf762260005b7b498a" dependencies = [ "async-utility", "nostr", @@ -3620,7 +3550,7 @@ dependencies = [ [[package]] name = "nostr-database" version = "0.40.0" -source = "git+https://github.com/rust-nostr/nostr#1347878e90392db54a263d60021fa0410a3b05cb" +source = "git+https://github.com/rust-nostr/nostr#f4f3f50aa637b3d288a6f1cf762260005b7b498a" dependencies = [ "flatbuffers", "lru", @@ -3631,7 +3561,7 @@ dependencies = [ [[package]] name = "nostr-lmdb" version = "0.40.0" -source = "git+https://github.com/rust-nostr/nostr#1347878e90392db54a263d60021fa0410a3b05cb" +source = "git+https://github.com/rust-nostr/nostr#f4f3f50aa637b3d288a6f1cf762260005b7b498a" dependencies = [ "async-utility", "heed", @@ -3644,7 +3574,7 @@ dependencies = [ [[package]] name = "nostr-relay-pool" version = "0.40.0" -source = "git+https://github.com/rust-nostr/nostr#1347878e90392db54a263d60021fa0410a3b05cb" +source = "git+https://github.com/rust-nostr/nostr#f4f3f50aa637b3d288a6f1cf762260005b7b498a" dependencies = [ "async-utility", "async-wsocket", @@ -3661,7 +3591,7 @@ dependencies = [ [[package]] name = "nostr-sdk" version = "0.40.0" -source = "git+https://github.com/rust-nostr/nostr#1347878e90392db54a263d60021fa0410a3b05cb" +source = "git+https://github.com/rust-nostr/nostr#f4f3f50aa637b3d288a6f1cf762260005b7b498a" dependencies = [ "async-utility", "nostr", @@ -3672,6 +3602,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "ntapi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +dependencies = [ + "winapi", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -3802,6 +3741,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" dependencies = [ "malloc_buf", + "objc_exception", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", ] [[package]] @@ -4006,6 +3957,24 @@ dependencies = [ "objc2-foundation", ] +[[package]] +name = "objc_exception" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" +dependencies = [ + "cc", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + [[package]] name = "object" version = "0.36.7" @@ -4062,12 +4031,6 @@ dependencies = [ "zvariant", ] -[[package]] -name = "oorandom" -version = "11.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" - [[package]] name = "opaque-debug" version = "0.3.1" @@ -4307,34 +4270,6 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" -[[package]] -name = "plotters" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" -dependencies = [ - "num-traits", - "plotters-backend", - "plotters-svg", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "plotters-backend" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" - -[[package]] -name = "plotters-svg" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" -dependencies = [ - "plotters-backend", -] - [[package]] name = "png" version = "0.17.16" @@ -4485,6 +4420,15 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" +[[package]] +name = "quick-xml" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" +dependencies = [ + "memchr", +] + [[package]] name = "quick-xml" version = "0.37.4" @@ -4746,9 +4690,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.10" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" +checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" dependencies = [ "bitflags 2.9.0", ] @@ -4767,7 +4711,7 @@ dependencies = [ [[package]] name = "refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#c1259c136e41b5870d15271190198ff0075ba35c" +source = "git+https://github.com/zed-industries/zed#f0b7f355a29486566a8ed7c2be4acad4b305b881" dependencies = [ "derive_refineable", "workspace-hack", @@ -4897,7 +4841,7 @@ dependencies = [ [[package]] name = "reqwest_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#c1259c136e41b5870d15271190198ff0075ba35c" +source = "git+https://github.com/zed-industries/zed#f0b7f355a29486566a8ed7c2be4acad4b305b881" dependencies = [ "anyhow", "bytes", @@ -5194,6 +5138,27 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scap" +version = "0.0.8" +source = "git+https://github.com/zed-industries/scap?rev=08f0a01417505cc0990b9931a37e5120db92e0d0#08f0a01417505cc0990b9931a37e5120db92e0d0" +dependencies = [ + "anyhow", + "cocoa 0.25.0", + "core-graphics-helmer-fork", + "log", + "objc", + "rand 0.8.5", + "screencapturekit", + "screencapturekit-sys", + "sysinfo", + "tao-core-video-sys", + "windows 0.61.1", + "windows-capture", + "x11", + "xcb", +] + [[package]] name = "schannel" version = "0.1.27" @@ -5240,6 +5205,29 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "screencapturekit" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5eeeb57ac94960cfe5ff4c402be6585ae4c8d29a2cf41b276048c2e849d64e" +dependencies = [ + "screencapturekit-sys", +] + +[[package]] +name = "screencapturekit-sys" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22411b57f7d49e7fe08025198813ee6fd65e1ee5eff4ebc7880c12c82bde4c60" +dependencies = [ + "block", + "dispatch", + "objc", + "objc-foundation", + "objc_id", + "once_cell", +] + [[package]] name = "scrypt" version = "0.11.0" @@ -5310,7 +5298,7 @@ checksum = "c2fdfc24bc566f839a2da4c4295b82db7d25a24253867d5c64355abb5799bdbe" [[package]] name = "semantic_version" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#c1259c136e41b5870d15271190198ff0075ba35c" +source = "git+https://github.com/zed-industries/zed#f0b7f355a29486566a8ed7c2be4acad4b305b881" dependencies = [ "anyhow", "serde", @@ -5633,7 +5621,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sum_tree" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#c1259c136e41b5870d15271190198ff0075ba35c" +source = "git+https://github.com/zed-industries/zed#f0b7f355a29486566a8ed7c2be4acad4b305b881" dependencies = [ "arrayvec", "log", @@ -5806,6 +5794,20 @@ dependencies = [ "libc", ] +[[package]] +name = "sysinfo" +version = "0.31.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "355dbe4f8799b304b05e1b0f05fc59b2a18d36645cf169607da45bde2f69a1be" +dependencies = [ + "core-foundation-sys", + "libc", + "memchr", + "ntapi", + "rayon", + "windows 0.57.0", +] + [[package]] name = "system-configuration" version = "0.6.1" @@ -5859,6 +5861,18 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8bdb6fa0dfa67b38c1e66b7041ba9dcf23b99d8121907cd31c807a332f7a0bbb" +[[package]] +name = "tao-core-video-sys" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271450eb289cb4d8d0720c6ce70c72c8c858c93dd61fc625881616752e6b98f6" +dependencies = [ + "cfg-if", + "core-foundation-sys", + "libc", + "objc", +] + [[package]] name = "target-lexicon" version = "0.12.16" @@ -6004,16 +6018,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "tinytemplate" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" -dependencies = [ - "serde", - "serde_json", -] - [[package]] name = "tinyvec" version = "1.9.0" @@ -6042,9 +6046,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.44.1" +version = "1.44.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a" +checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" dependencies = [ "backtrace", "bytes", @@ -6312,7 +6316,6 @@ dependencies = [ "anyhow", "chrono", "common", - "criterion", "gpui", "image", "itertools 0.13.0", @@ -6510,7 +6513,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "util" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#c1259c136e41b5870d15271190198ff0075ba35c" +source = "git+https://github.com/zed-industries/zed#f0b7f355a29486566a8ed7c2be4acad4b305b881" dependencies = [ "anyhow", "async-fs", @@ -6829,7 +6832,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "896fdafd5d28145fce7958917d69f2fd44469b1d4e861cb5961bcbeebc6d1484" dependencies = [ "proc-macro2", - "quick-xml", + "quick-xml 0.37.4", "quote", ] @@ -6943,6 +6946,26 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +dependencies = [ + "windows-core 0.57.0", + "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.1" @@ -6950,19 +6973,58 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419" dependencies = [ "windows-collections", - "windows-core", + "windows-core 0.61.0", "windows-future", "windows-link", "windows-numerics", ] +[[package]] +name = "windows-capture" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6001b777f61cafce437201de46a019ed7f4afed3b669f02e5ce4e0759164cb3e" +dependencies = [ + "clap", + "ctrlc", + "parking_lot", + "rayon", + "thiserror 1.0.69", + "windows 0.58.0", +] + [[package]] name = "windows-collections" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" dependencies = [ - "windows-core", + "windows-core 0.61.0", +] + +[[package]] +name = "windows-core" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +dependencies = [ + "windows-implement 0.57.0", + "windows-interface 0.57.0", + "windows-result 0.1.2", + "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]] @@ -6971,8 +7033,8 @@ version = "0.61.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" dependencies = [ - "windows-implement", - "windows-interface", + "windows-implement 0.60.0", + "windows-interface 0.59.1", "windows-link", "windows-result 0.3.2", "windows-strings 0.4.0", @@ -6984,10 +7046,32 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32" dependencies = [ - "windows-core", + "windows-core 0.61.0", "windows-link", ] +[[package]] +name = "windows-implement" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[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.100", +] + [[package]] name = "windows-implement" version = "0.60.0" @@ -6999,6 +7083,28 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "windows-interface" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[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.100", +] + [[package]] name = "windows-interface" version = "0.59.1" @@ -7022,7 +7128,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ - "windows-core", + "windows-core 0.61.0", "windows-link", ] @@ -7048,6 +7154,15 @@ dependencies = [ "windows-targets 0.53.0", ] +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-result" version = "0.2.0" @@ -7374,9 +7489,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36" +checksum = "63d3fcd9bba44b03821e7d699eeee959f3126dcc4aa8e4ae18ec617c2a5cea10" dependencies = [ "memchr", ] @@ -7427,6 +7542,16 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + [[package]] name = "x11-clipboard" version = "0.9.3" @@ -7456,6 +7581,18 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" +[[package]] +name = "xcb" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1e2f212bb1a92cd8caac8051b829a6582ede155ccb60b5d5908b81b100952be" +dependencies = [ + "bitflags 1.3.2", + "libc", + "quick-xml 0.30.0", + "x11", +] + [[package]] name = "xcursor" version = "0.3.8" diff --git a/Cargo.toml b/Cargo.toml index f3a3b2a..8a0f2e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ reqwest_client = { git = "https://github.com/zed-industries/zed" } nostr = { git = "https://github.com/rust-nostr/nostr", features = ["parser"] } nostr-relay-builder = { git = "https://github.com/rust-nostr/nostr" } nostr-connect = { git = "https://github.com/rust-nostr/nostr" } +nostr-keyring = { git = "https://github.com/rust-nostr/nostr" } nostr-sdk = { git = "https://github.com/rust-nostr/nostr", features = [ "lmdb", "nip96", diff --git a/crates/account/src/lib.rs b/crates/account/src/lib.rs index 161dc9d..5099dca 100644 --- a/crates/account/src/lib.rs +++ b/crates/account/src/lib.rs @@ -1,7 +1,6 @@ use std::time::Duration; use anyhow::Error; -use common::profile::NostrProfile; use global::{ constants::{ALL_MESSAGES_SUB_ID, NEW_MESSAGE_SUB_ID}, get_client, @@ -15,12 +14,19 @@ struct GlobalAccount(Entity); impl Global for GlobalAccount {} pub fn init(cx: &mut App) { - Account::set_global(cx.new(|_| Account { profile: None }), cx); + Account::set_global( + cx.new(|_| Account { + profile: None, + loading: false, + }), + cx, + ); } #[derive(Debug, Clone)] pub struct Account { - pub profile: Option, + pub profile: Option, + loading: bool, } impl Account { @@ -36,7 +42,13 @@ impl Account { where S: NostrSigner + 'static, { - let task: Task> = cx.background_spawn(async move { + if self.loading { + return; + } + + self.set_loading(true, cx); + + let task: Task> = cx.background_spawn(async move { let client = get_client(); // Use user's signer for main signer _ = client.set_signer(signer).await; @@ -44,6 +56,7 @@ impl Account { // Verify nostr signer and get public key let signer = client.signer().await?; let public_key = signer.get_public_key().await?; + log::info!("Logged in with public key: {:?}", public_key); // Fetch user's metadata let metadata = client @@ -51,7 +64,7 @@ impl Account { .await? .unwrap_or_default(); - Ok(NostrProfile::new(public_key, metadata)) + Ok(Profile::new(public_key, metadata)) }); cx.spawn_in(window, async move |this, cx| match task.await { @@ -59,6 +72,7 @@ impl Account { cx.update(|_, cx| { this.update(cx, |this, cx| { this.profile = Some(profile); + this.set_loading(false, cx); this.subscribe(cx); cx.notify(); }) @@ -79,14 +93,14 @@ impl Account { let client = get_client(); let keys = Keys::generate(); - let task: Task> = cx.background_spawn(async move { + let task: Task> = cx.background_spawn(async move { let public_key = keys.public_key(); // Update signer client.set_signer(keys).await; // Set metadata client.set_metadata(&metadata).await?; - Ok(NostrProfile::new(public_key, metadata)) + Ok(Profile::new(public_key, metadata)) }); cx.spawn_in(window, async move |this, cx| { @@ -115,7 +129,7 @@ impl Account { }; let client = get_client(); - let user = profile.public_key; + let user = profile.public_key(); let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); let metadata = Filter::new() @@ -164,4 +178,9 @@ impl Account { }) .detach(); } + + fn set_loading(&mut self, loading: bool, cx: &mut Context) { + self.loading = loading; + cx.notify(); + } } diff --git a/crates/app/src/chat_space.rs b/crates/app/src/chat_space.rs index 5626a9a..fb06f83 100644 --- a/crates/app/src/chat_space.rs +++ b/crates/app/src/chat_space.rs @@ -1,4 +1,5 @@ use account::Account; +use common::profile::SharedProfile; use global::get_client; use gpui::{ actions, div, img, impl_internal_actions, prelude::FluentBuilder, px, App, AppContext, Axis, @@ -172,7 +173,7 @@ impl ChatSpace { .icon(Icon::new(IconName::ChevronDownSmall)) .when_some( Account::global(cx).read(cx).profile.as_ref(), - |this, profile| this.child(img(profile.avatar.clone()).size_5()), + |this, profile| this.child(img(profile.shared_avatar()).size_5()), ) .popup_menu(move |this, _, _cx| { this.menu( diff --git a/crates/app/src/main.rs b/crates/app/src/main.rs index 3a7b71f..4a1ecba 100644 --- a/crates/app/src/main.rs +++ b/crates/app/src/main.rs @@ -18,7 +18,8 @@ use gpui::{point, SharedString, TitlebarOptions}; use gpui::{WindowBackgroundAppearance, WindowDecorations}; use nostr_sdk::{ pool::prelude::ReqExitPolicy, Event, EventBuilder, EventId, Filter, JsonUtil, Keys, Kind, - PublicKey, RelayMessage, RelayPoolNotification, SubscribeAutoCloseOptions, SubscriptionId, Tag, + Metadata, PublicKey, RelayMessage, RelayPoolNotification, SubscribeAutoCloseOptions, + SubscriptionId, Tag, }; use smol::Timer; use std::{collections::HashSet, mem, sync::Arc, time::Duration}; @@ -34,6 +35,8 @@ actions!(coop, [Quit]); enum Signal { /// Receive event Event(Event), + /// Receive metadata + Metadata(Box<(PublicKey, Option)>), /// Receive EOSE Eose, } @@ -149,6 +152,14 @@ fn main() { event_tx.send(Signal::Event(event)).await.ok(); } } + Kind::Metadata => { + let metadata = Metadata::from_json(&event.content).ok(); + + event_tx + .send(Signal::Metadata(Box::new((event.pubkey, metadata)))) + .await + .ok(); + } Kind::ContactList => { if let Ok(signer) = client.signer().await { if let Ok(public_key) = signer.get_public_key().await { @@ -241,14 +252,23 @@ fn main() { while let Ok(signal) = event_rx.recv().await { cx.update(|window, cx| { match signal { - Signal::Eose => { - chats.update(cx, |this, cx| this.load_rooms(window, cx)); - } Signal::Event(event) => { chats.update(cx, |this, cx| { this.push_message(event, window, cx) }); } + Signal::Metadata(data) => { + chats.update(cx, |this, cx| { + this.add_profile(data.0, data.1, cx) + }); + } + Signal::Eose => { + chats.update(cx, |this, cx| { + // This function maybe called multiple times + // TODO: only handle the last EOSE signal + this.load_rooms(window, cx) + }); + } }; }) .ok(); diff --git a/crates/app/src/views/chat.rs b/crates/app/src/views/chat.rs index 9f3e182..bca717d 100644 --- a/crates/app/src/views/chat.rs +++ b/crates/app/src/views/chat.rs @@ -1,7 +1,7 @@ use anyhow::{anyhow, Error}; use async_utility::task::spawn; use chats::{message::RoomMessage, room::Room, ChatRegistry}; -use common::utils::nip96_upload; +use common::{nip96_upload, profile::SharedProfile}; use global::{constants::IMAGE_SERVICE, get_client}; use gpui::{ div, img, list, prelude::FluentBuilder, px, relative, svg, white, AnyElement, App, AppContext, @@ -27,7 +27,7 @@ use ui::{ const ALERT: &str = "has not set up Messaging (DM) Relays, so they will NOT receive your messages."; pub fn init(id: &u64, window: &mut Window, cx: &mut App) -> Result>, Error> { - if let Some(room) = ChatRegistry::global(cx).read(cx).get(id, cx) { + if let Some(room) = ChatRegistry::global(cx).read(cx).room(id, cx) { Ok(Arc::new(Chat::new(id, room, window, cx))) } else { Err(anyhow!("Chat Room not found.")) @@ -137,14 +137,14 @@ impl Chat { this.update(cx, |this, cx| { result.into_iter().for_each(|item| { if !item.1 { - if let Some(profile) = - this.room.read_with(cx, |this, _| this.member(&item.0)) - { - this.push_system_message( - format!("{} {}", profile.name, ALERT), - cx, - ); - } + let profile = this + .room + .read_with(cx, |this, _| this.profile_by_pubkey(&item.0, cx)); + + this.push_system_message( + format!("{} {}", profile.shared_name(), ALERT), + cx, + ); } }); }) @@ -367,7 +367,7 @@ impl Chat { this.bg(cx.theme().accent.step(cx, ColorScaleStep::NINE)) }), ) - .child(img(item.author.avatar.clone()).size_8().flex_shrink_0()) + .child(img(item.author.shared_avatar()).size_8().flex_shrink_0()) .child( div() .flex() @@ -381,17 +381,11 @@ impl Chat { .gap_2() .text_xs() .child( - div().font_semibold().child(item.author.name.clone()), + div().font_semibold().child(item.author.shared_name()), ) - .child( - div() - .child(item.created_at.human_readable()) - .text_color( - cx.theme() - .base - .step(cx, ColorScaleStep::ELEVEN), - ), - ), + .child(div().child(item.ago()).text_color( + cx.theme().base.step(cx, ColorScaleStep::ELEVEN), + )), ) .child(div().text_sm().child(text.element( "body".into(), @@ -445,11 +439,7 @@ impl Panel for Chat { fn title(&self, cx: &App) -> AnyElement { self.room.read_with(cx, |this, _| { - let facepill: Vec = this - .members - .iter() - .map(|member| member.avatar.clone()) - .collect(); + let facepill: Vec = this.avatars(cx); div() .flex() @@ -461,13 +451,19 @@ impl Panel for Chat { .flex_row_reverse() .items_center() .justify_start() - .children(facepill.into_iter().enumerate().rev().map(|(ix, face)| { - div() - .when(ix > 0, |div| div.ml_neg_1()) - .child(img(face).size_4()) - })), + .children( + facepill + .into_iter() + .enumerate() + .rev() + .map(|(ix, facepill)| { + div() + .when(ix > 0, |div| div.ml_neg_1()) + .child(img(facepill).size_4()) + }), + ), ) - .when_some(this.subject(), |this, name| this.child(name)) + .child(this.display_name(cx)) .into_any() }) } diff --git a/crates/app/src/views/contacts.rs b/crates/app/src/views/contacts.rs index 14fb63f..e0525ea 100644 --- a/crates/app/src/views/contacts.rs +++ b/crates/app/src/views/contacts.rs @@ -1,10 +1,14 @@ -use common::profile::NostrProfile; +use std::collections::BTreeSet; + +use anyhow::Error; +use common::profile::SharedProfile; use global::get_client; use gpui::{ div, img, prelude::FluentBuilder, px, uniform_list, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, - Render, SharedString, Styled, Window, + Render, SharedString, Styled, Task, Window, }; +use itertools::Itertools; use nostr_sdk::prelude::*; use ui::{ button::Button, @@ -20,7 +24,7 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity { } pub struct Contacts { - contacts: Entity>>, + contacts: Option>, // Panel name: SharedString, closable: bool, @@ -29,48 +33,42 @@ pub struct Contacts { } impl Contacts { - pub fn new(_window: &mut Window, cx: &mut App) -> Entity { - let contacts = cx.new(|_| None); - let async_contact = contacts.clone(); + pub fn new(window: &mut Window, cx: &mut App) -> Entity { + cx.new(|cx| Self::view(window, cx)) + } - cx.spawn(async move |cx| { + fn view(_window: &mut Window, cx: &mut Context) -> Self { + cx.spawn(async move |this, cx| { let client = get_client(); - let (tx, rx) = oneshot::channel::>(); - cx.background_executor() - .spawn(async move { - let signer = client.signer().await.unwrap(); - let public_key = signer.get_public_key().await.unwrap(); + let task: Task, Error>> = cx.background_spawn(async move { + let signer = client.signer().await?; + let public_key = signer.get_public_key().await?; + let profiles = client.database().contacts(public_key).await?; - if let Ok(profiles) = client.database().contacts(public_key).await { - let members: Vec = profiles - .into_iter() - .map(|profile| { - NostrProfile::new(profile.public_key(), profile.metadata()) - }) - .collect(); + Ok(profiles) + }); - _ = tx.send(members); - } + if let Ok(contacts) = task.await { + cx.update(|cx| { + this.update(cx, |this, cx| { + this.contacts = Some(contacts.into_iter().collect_vec()); + cx.notify(); + }) + .ok(); }) - .detach(); - - if let Ok(contacts) = rx.await { - _ = cx.update_entity(&async_contact, |this, cx| { - *this = Some(contacts); - cx.notify(); - }); + .ok(); } }) .detach(); - cx.new(|cx| Self { - contacts, + Self { + contacts: None, name: "Contacts".into(), closable: true, zoomable: true, focus_handle: cx.focus_handle(), - }) + } } } @@ -111,7 +109,7 @@ impl Focusable for Contacts { impl Render for Contacts { fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context) -> impl IntoElement { div().size_full().pt_2().px_2().map(|this| { - if let Some(contacts) = self.contacts.read(cx).clone() { + if let Some(contacts) = self.contacts.clone() { this.child( uniform_list( cx.entity().clone(), @@ -141,9 +139,9 @@ impl Render for Contacts { .child( div() .flex_shrink_0() - .child(img(item.avatar).size_6()), + .child(img(item.shared_avatar()).size_6()), ) - .child(item.name), + .child(item.shared_name()), ) .hover(|this| { this.bg(cx.theme().base.step(cx, ColorScaleStep::THREE)) diff --git a/crates/app/src/views/login.rs b/crates/app/src/views/login.rs index e28bb29..c2a49dd 100644 --- a/crates/app/src/views/login.rs +++ b/crates/app/src/views/login.rs @@ -1,7 +1,7 @@ use std::{sync::Arc, time::Duration}; use account::Account; -use common::utils::create_qr; +use common::create_qr; use global::get_client_keys; use gpui::{ div, img, prelude::FluentBuilder, relative, AnyElement, App, AppContext, Context, Entity, diff --git a/crates/app/src/views/new_account.rs b/crates/app/src/views/new_account.rs index fdb06e5..9ccbe91 100644 --- a/crates/app/src/views/new_account.rs +++ b/crates/app/src/views/new_account.rs @@ -1,6 +1,6 @@ use account::Account; use async_utility::task::spawn; -use common::utils::nip96_upload; +use common::nip96_upload; use global::{constants::IMAGE_SERVICE, get_client}; use gpui::{ div, img, prelude::FluentBuilder, px, relative, AnyElement, App, AppContext, Context, Entity, diff --git a/crates/app/src/views/profile.rs b/crates/app/src/views/profile.rs index a859c85..c5c6e60 100644 --- a/crates/app/src/views/profile.rs +++ b/crates/app/src/views/profile.rs @@ -1,5 +1,5 @@ use async_utility::task::spawn; -use common::utils::nip96_upload; +use common::nip96_upload; use global::{constants::IMAGE_SERVICE, get_client}; use gpui::{ div, img, prelude::FluentBuilder, AnyElement, App, AppContext, Context, Entity, EventEmitter, diff --git a/crates/app/src/views/sidebar/compose.rs b/crates/app/src/views/sidebar/compose.rs index 463b74c..0ff9062 100644 --- a/crates/app/src/views/sidebar/compose.rs +++ b/crates/app/src/views/sidebar/compose.rs @@ -1,8 +1,9 @@ +use anyhow::Error; use chats::{ room::{Room, RoomKind}, ChatRegistry, }; -use common::{profile::NostrProfile, utils::random_name}; +use common::{profile::SharedProfile, random_name}; use global::get_client; use gpui::{ div, img, impl_internal_actions, prelude::FluentBuilder, px, relative, uniform_list, App, @@ -14,7 +15,11 @@ use nostr_sdk::prelude::*; use serde::Deserialize; use smallvec::{smallvec, SmallVec}; use smol::Timer; -use std::{collections::HashSet, rc::Rc, time::Duration}; +use std::{ + collections::{BTreeSet, HashSet}, + rc::Rc, + time::Duration, +}; use ui::{ button::{Button, ButtonRounded}, input::{InputEvent, TextInput}, @@ -33,7 +38,7 @@ impl_internal_actions!(contacts, [SelectContact]); pub struct Compose { title_input: Entity, user_input: Entity, - contacts: Entity>, + contacts: Entity>, selected: Entity>, focus_handle: FocusHandle, is_loading: bool, @@ -80,26 +85,17 @@ impl Compose { }, )); - let client = get_client(); - let (tx, rx) = oneshot::channel::>(); - - cx.background_spawn(async move { - let signer = client.signer().await.unwrap(); - let public_key = signer.get_public_key().await.unwrap(); - - if let Ok(profiles) = client.database().contacts(public_key).await { - let members: Vec = profiles - .into_iter() - .map(|profile| NostrProfile::new(profile.public_key(), profile.metadata())) - .collect(); - - _ = tx.send(members); - } - }) - .detach(); - cx.spawn(async move |this, cx| { - if let Ok(contacts) = rx.await { + let task: Task, Error>> = cx.background_spawn(async move { + let client = get_client(); + let signer = client.signer().await?; + let public_key = signer.get_public_key().await?; + let profiles = client.database().contacts(public_key).await?; + + Ok(profiles) + }); + + if let Ok(contacts) = task.await { cx.update(|cx| { this.update(cx, |this, cx| { this.contacts.update(cx, |this, cx| { @@ -107,6 +103,7 @@ impl Compose { cx.notify(); }); }) + .ok() }) .ok(); } @@ -174,7 +171,7 @@ impl Compose { .ok(); let chats = ChatRegistry::global(cx); - let room = Room::new(&event, RoomKind::Ongoing); + let room = Room::new(&event).kind(RoomKind::Ongoing); chats.update(cx, |chats, cx| { match chats.push(room, cx) { @@ -215,7 +212,7 @@ impl Compose { // Show loading spinner self.set_loading(true, cx); - let task: Task> = if content.contains("@") { + let task: Task> = if content.contains("@") { cx.background_spawn(async move { let profile = nip05::profile(&content, None).await?; let public_key = profile.public_key; @@ -225,7 +222,7 @@ impl Compose { .await? .unwrap_or_default(); - Ok(NostrProfile::new(public_key, metadata)) + Ok(Profile::new(public_key, metadata)) }) } else { let Ok(public_key) = PublicKey::parse(&content) else { @@ -240,7 +237,7 @@ impl Compose { .await? .unwrap_or_default(); - Ok(NostrProfile::new(public_key, metadata)) + Ok(Profile::new(public_key, metadata)) }) }; @@ -249,7 +246,7 @@ impl Compose { Ok(profile) => { cx.update(|window, cx| { this.update(cx, |this, cx| { - let public_key = profile.public_key; + let public_key = profile.public_key(); this.contacts.update(cx, |this, cx| { this.insert(0, profile); @@ -441,7 +438,7 @@ impl Render for Compose { for ix in range { let item = contacts.get(ix).unwrap().clone(); - let is_select = selected.contains(&item.public_key); + let is_select = selected.contains(&item.public_key()); items.push( div() @@ -458,12 +455,10 @@ impl Render for Compose { .items_center() .gap_2() .text_xs() - .child( - div().flex_shrink_0().child( - img(item.avatar).size_6(), - ), - ) - .child(item.name), + .child(div().flex_shrink_0().child( + img(item.shared_avatar()).size_6(), + )) + .child(item.shared_name()), ) .when(is_select, |this| { this.child( @@ -484,7 +479,7 @@ impl Render for Compose { .on_click(move |_, window, cx| { window.dispatch_action( Box::new(SelectContact( - item.public_key, + item.public_key(), )), cx, ); diff --git a/crates/app/src/views/sidebar/mod.rs b/crates/app/src/views/sidebar/mod.rs index da11d0d..9434eec 100644 --- a/crates/app/src/views/sidebar/mod.rs +++ b/crates/app/src/views/sidebar/mod.rs @@ -138,31 +138,18 @@ impl Sidebar { 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 id = room.id; + let ago = room.ago(); + let label = room.display_name(cx); + let img = room.display_image(cx).map(img); - 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) + let item = FolderItem::new(id as usize) .label(label) .description(ago) .img(img) .on_click({ cx.listener(move |this, _, window, cx| { - this.open_room(room_id, window, cx); + this.open_room(id, window, cx); }) }); diff --git a/crates/chats/Cargo.toml b/crates/chats/Cargo.toml index 2a294a4..66ad5a2 100644 --- a/crates/chats/Cargo.toml +++ b/crates/chats/Cargo.toml @@ -5,6 +5,7 @@ edition.workspace = true publish.workspace = true [dependencies] +account = { path = "../account" } common = { path = "../common" } global = { path = "../global" } ui = { path = "../ui" } diff --git a/crates/chats/src/constants.rs b/crates/chats/src/constants.rs new file mode 100644 index 0000000..ef2505e --- /dev/null +++ b/crates/chats/src/constants.rs @@ -0,0 +1,5 @@ +pub(crate) const NOW: &str = "now"; +pub(crate) const SECONDS_IN_MINUTE: i64 = 60; +pub(crate) const MINUTES_IN_HOUR: i64 = 60; +pub(crate) const HOURS_IN_DAY: i64 = 24; +pub(crate) const DAYS_IN_MONTH: i64 = 30; diff --git a/crates/chats/src/lib.rs b/crates/chats/src/lib.rs index e58ced0..f5999c2 100644 --- a/crates/chats/src/lib.rs +++ b/crates/chats/src/lib.rs @@ -1,7 +1,7 @@ use std::{cmp::Reverse, collections::HashMap}; -use anyhow::anyhow; -use common::{last_seen::LastSeen, utils::room_hash}; +use anyhow::{anyhow, Error}; +use common::room_hash; use global::get_client; use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task, Window}; use itertools::Itertools; @@ -11,6 +11,7 @@ use smallvec::{smallvec, SmallVec}; use crate::room::Room; +mod constants; pub mod message; pub mod room; @@ -22,35 +23,56 @@ struct GlobalChatRegistry(Entity); impl Global for GlobalChatRegistry {} +/// Main registry for managing chat rooms and user profiles +/// +/// The ChatRegistry is responsible for: +/// - Managing chat rooms and their states +/// - Tracking user profiles +/// - Loading room data from the lmdb +/// - Handling messages and room creation pub struct ChatRegistry { + /// Collection of all chat rooms rooms: Vec>, + + /// Map of user public keys to their profile metadata + profiles: Entity>>, + + /// Indicates if rooms are currently being loaded loading: bool, + + /// Subscriptions for observing changes #[allow(dead_code)] subscriptions: SmallVec<[Subscription; 1]>, } impl ChatRegistry { - pub fn global(cx: &mut App) -> Entity { + /// Retrieve the global ChatRegistry instance + pub fn global(cx: &App) -> Entity { cx.global::().0.clone() } + /// Set the global ChatRegistry instance pub fn set_global(state: Entity, cx: &mut App) { cx.set_global(GlobalChatRegistry(state)); } + /// Create a new ChatRegistry instance fn new(cx: &mut Context) -> Self { + let profiles = cx.new(|_| HashMap::new()); let mut subscriptions = smallvec![]; + // Observe new Room creations to collect profile metadata subscriptions.push(cx.observe_new::(|this, _, cx| { - let load_metadata = this.load_metadata(cx); + let task = this.metadata(cx); - cx.spawn(async move |this, cx| { - if let Ok(profiles) = load_metadata.await { + cx.spawn(async move |_, cx| { + if let Ok(data) = task.await { cx.update(|cx| { - this.update(cx, |this, cx| { - this.update_members(profiles, cx); - }) - .ok(); + for (public_key, metadata) in data.into_iter() { + Self::global(cx).update(cx, |this, cx| { + this.add_profile(public_key, metadata, cx); + }) + } }) .ok(); } @@ -61,24 +83,73 @@ impl ChatRegistry { Self { rooms: vec![], loading: true, + profiles, subscriptions, } } + /// Get the global loading status + pub fn loading(&self) -> bool { + self.loading + } + + /// Get a room by its ID. + pub fn room(&self, id: &u64, cx: &App) -> Option> { + self.rooms + .iter() + .find(|model| model.read(cx).id == *id) + .cloned() + } + + /// Get all rooms grouped by their kind. + 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 IDs of all rooms. + pub fn room_ids(&self, cx: &mut Context) -> Vec { + self.rooms.iter().map(|room| room.read(cx).id).collect() + } + + /// Load all rooms from the lmdb. + /// + /// This method: + /// 1. Fetches all private direct messages from the lmdb + /// 2. Groups them by ID + /// 3. Determines each room's type based on message frequency and trust status + /// 4. Creates Room entities for each unique room pub fn load_rooms(&mut self, window: &mut Window, cx: &mut Context) { - let client = get_client(); - let room_ids = self.room_ids(cx); + type Rooms = Vec<(Event, usize, bool)>; - type LoadResult = Result, Error>; - - let task: Task = cx.background_spawn(async move { + let task: Task> = cx.background_spawn(async move { + let client = get_client(); let signer = client.signer().await?; let public_key = signer.get_public_key().await?; + // Get messages sent by the user let send = Filter::new() .kind(Kind::PrivateDirectMessage) .author(public_key); + // Get messages received by the user let recv = Filter::new() .kind(Kind::PrivateDirectMessage) .pubkey(public_key); @@ -89,26 +160,26 @@ impl ChatRegistry { let mut room_map: HashMap = HashMap::new(); + // Process each event and group by room hash for event in events .into_iter() .filter(|ev| ev.tags.public_keys().peekable().peek().is_some()) { 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; + 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)); - } + room_map + .entry(hash) + .and_modify(|(_, count, trusted)| { + *count += 1; + *trusted = is_trust; + }) + .or_insert((event, 1, is_trust)); } + // Sort rooms by creation date (newest first) let result: Vec<(Event, usize, bool)> = room_map .into_values() .sorted_by_key(|(ev, _, _)| Reverse(ev.created_at)) @@ -119,26 +190,31 @@ 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| { + let ids = this.room_ids(cx); + let rooms: Vec> = events + .into_iter() + .filter_map(|(event, count, trusted)| { + let hash = room_hash(&event); + if !ids.iter().any(|this| this == &hash) { + 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 + }; + Some(cx.new(|_| Room::new(&event).kind(kind))) + } else { + None + } + }) + .collect(); + this.rooms.extend(rooms); - this.rooms.sort_by_key(|r| Reverse(r.read(cx).last_seen())); + this.rooms.sort_by_key(|r| Reverse(r.read(cx).created_at)); this.loading = false; cx.notify(); @@ -151,48 +227,40 @@ impl ChatRegistry { .detach(); } - /// 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() + /// Add a user profile to the registry + /// + /// Only adds the profile if it doesn't already exist or is currently none + pub fn add_profile( + &mut self, + public_key: PublicKey, + metadata: Option, + cx: &mut Context, + ) { + self.profiles.update(cx, |this, _cx| { + this.entry(public_key) + .and_modify(|entry| { + if entry.is_none() { + *entry = metadata.clone(); + } + }) + .or_insert_with(|| metadata); + }); } - /// 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()); + /// Get a user profile by public key + pub fn profile(&self, public_key: &PublicKey, cx: &App) -> Profile { + let metadata = if let Some(profile) = self.profiles.read(cx).get(public_key) { + profile.clone().unwrap_or_default() + } else { + Metadata::default() + }; - for room in self.rooms.iter() { - let kind = room.read(cx).kind(); - groups.entry(kind).or_insert_with(Vec::new).push(room); - } - - groups + Profile::new(*public_key, metadata) } - /// 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() - .find(|model| model.read(cx).id == *id) - .cloned() - } - - /// Push a room to the list. + /// Add a new room to the registry + /// + /// Returns an error if the room already exists pub fn push(&mut self, room: Room, cx: &mut Context) -> Result<(), anyhow::Error> { let room = cx.new(|_| room); @@ -210,21 +278,24 @@ impl ChatRegistry { } } - /// Push a message to a room. + /// Push a new message to a room + /// + /// If the room doesn't exist, it will be created. + /// Updates room ordering based on the most recent messages. pub fn push_message(&mut self, event: Event, window: &mut Window, cx: &mut Context) { let id = room_hash(&event); if let Some(room) = self.rooms.iter().find(|room| room.read(cx).id == id) { room.update(cx, |this, cx| { - this.set_last_seen(LastSeen(event.created_at), cx); + this.created_at(event.created_at, cx); this.emit_message(event, window, cx); }); // Re-sort rooms by last seen self.rooms - .sort_by_key(|room| Reverse(room.read(cx).last_seen())); + .sort_by_key(|room| Reverse(room.read(cx).created_at)); } else { - let new_room = cx.new(|_| Room::new(&event, RoomKind::default())); + let new_room = cx.new(|_| Room::new(&event)); // Push the new room to the front of the list self.rooms.insert(0, new_room); diff --git a/crates/chats/src/message.rs b/crates/chats/src/message.rs index 6d0f96a..edbdc49 100644 --- a/crates/chats/src/message.rs +++ b/crates/chats/src/message.rs @@ -1,36 +1,101 @@ -use common::{last_seen::LastSeen, profile::NostrProfile}; +use chrono::{Local, TimeZone}; use gpui::SharedString; use nostr_sdk::prelude::*; +/// # Message +/// +/// Represents a message in the application. +/// +/// ## Fields +/// +/// - `id`: The unique identifier for the message +/// - `content`: The text content of the message +/// - `author`: Profile information about who created the message +/// - `mentions`: List of profiles mentioned in the message +/// - `created_at`: Timestamp when the message was created #[derive(Debug, Clone, PartialEq, Eq)] pub struct Message { pub id: EventId, pub content: String, - pub author: NostrProfile, - pub mentions: Vec, - pub created_at: LastSeen, + pub author: Profile, + pub mentions: Vec, + pub created_at: Timestamp, } impl Message { - pub fn new( - id: EventId, - content: String, - author: NostrProfile, - mentions: Vec, - created_at: Timestamp, - ) -> Self { - let created_at = LastSeen(created_at); - + /// Creates a new message with the provided details + /// + /// # Arguments + /// + /// * `id` - Unique event identifier + /// * `content` - Message text content + /// * `author` - Profile of the message author + /// * `created_at` - When the message was created + /// + /// # Returns + /// + /// A new `Message` instance + pub fn new(id: EventId, content: String, author: Profile, created_at: Timestamp) -> Self { Self { id, content, author, - mentions, created_at, + mentions: vec![], } } + + /// Adds or replaces mentions in the message + /// + /// # Arguments + /// + /// * `mentions` - New list of mentioned profiles + /// + /// # Returns + /// + /// The same message with updated mentions + pub fn with_mentions(mut self, mentions: impl IntoIterator) -> Self { + self.mentions.extend(mentions); + self + } + + /// Formats the message timestamp as a human-readable relative time + /// + /// # Returns + /// + /// A formatted string like "Today at 12:30 PM", "Yesterday at 3:45 PM", + /// or a date and time for older messages + pub fn ago(&self) -> SharedString { + let input_time = match Local.timestamp_opt(self.created_at.as_u64() as i64, 0) { + chrono::LocalResult::Single(time) => time, + _ => return "Invalid timestamp".into(), + }; + + let now = Local::now(); + let input_date = input_time.date_naive(); + let now_date = now.date_naive(); + let yesterday_date = (now - chrono::Duration::days(1)).date_naive(); + + let time_format = input_time.format("%H:%M %p"); + + match input_date { + date if date == now_date => format!("Today at {time_format}"), + date if date == yesterday_date => format!("Yesterday at {time_format}"), + _ => format!("{}, {time_format}", input_time.format("%d/%m/%y")), + } + .into() + } } +/// # RoomMessage +/// +/// Represents different types of messages that can appear in a room. +/// +/// ## Variants +/// +/// - `User`: A message sent by a user +/// - `System`: A message generated by the system +/// - `Announcement`: A special message type used for room announcements #[derive(Debug, Clone, PartialEq, Eq)] pub enum RoomMessage { /// User message @@ -43,14 +108,37 @@ pub enum RoomMessage { } impl RoomMessage { + /// Creates a new user message + /// + /// # Arguments + /// + /// * `message` - The message content + /// + /// # Returns + /// + /// A `RoomMessage::User` variant pub fn user(message: Message) -> Self { Self::User(Box::new(message)) } + /// Creates a new system message + /// + /// # Arguments + /// + /// * `content` - The system message content + /// + /// # Returns + /// + /// A `RoomMessage::System` variant pub fn system(content: SharedString) -> Self { Self::System(content) } + /// Creates a new announcement placeholder + /// + /// # Returns + /// + /// A `RoomMessage::Announcement` variant pub fn announcement() -> Self { Self::Announcement } diff --git a/crates/chats/src/room.rs b/crates/chats/src/room.rs index 915e003..fea117f 100644 --- a/crates/chats/src/room.rs +++ b/crates/chats/src/room.rs @@ -1,18 +1,19 @@ -use std::{collections::HashSet, sync::Arc}; +use std::sync::Arc; +use account::Account; use anyhow::Error; -use common::{ - last_seen::LastSeen, - profile::NostrProfile, - utils::{compare, room_hash}, -}; +use chrono::{Local, TimeZone}; +use common::{compare, profile::SharedProfile, room_hash}; use global::get_client; use gpui::{App, AppContext, Context, EventEmitter, SharedString, Task, Window}; use itertools::Itertools; use nostr_sdk::prelude::*; -use smallvec::SmallVec; -use crate::message::{Message, RoomMessage}; +use crate::{ + constants::{DAYS_IN_MONTH, HOURS_IN_DAY, MINUTES_IN_HOUR, NOW, SECONDS_IN_MINUTE}, + message::{Message, RoomMessage}, + ChatRegistry, +}; #[derive(Debug, Clone)] pub struct IncomingEvent { @@ -29,15 +30,13 @@ pub enum RoomKind { pub struct Room { pub id: u64, - pub last_seen: LastSeen, + pub created_at: Timestamp, /// Subject of the room pub subject: Option, /// All members of the room - pub members: Arc>, + pub members: Arc>, /// Kind pub kind: RoomKind, - /// All public keys of the room members - pubkeys: Vec, } impl EventEmitter for Room {} @@ -49,10 +48,25 @@ impl PartialEq for Room { } impl Room { - /// Create a new room from an Nostr Event - pub fn new(event: &Event, kind: RoomKind) -> Self { + /// Creates a new Room instance from a Nostr event + /// + /// # Arguments + /// + /// * `event` - The Nostr event containing chat information + /// + /// # Returns + /// + /// A new Room instance with information extracted from the event + pub fn new(event: &Event) -> Self { let id = room_hash(event); - let last_seen = LastSeen(event.created_at); + let created_at = event.created_at; + + // Get all pubkeys from the event's tags + let mut pubkeys: Vec = event.tags.public_keys().cloned().collect(); + pubkeys.push(event.pubkey); + + // Convert pubkeys into members + let members = Arc::new(pubkeys.into_iter().unique().sorted().collect()); // Get the subject from the event's tags let subject = if let Some(tag) = event.tags.find(TagKind::Subject) { @@ -61,112 +75,264 @@ impl Room { None }; - // Get all public keys from the event's tags - let mut pubkeys = vec![]; - pubkeys.extend(event.tags.public_keys().collect::>()); - pubkeys.push(event.pubkey); - Self { id, - last_seen, + created_at, subject, - kind, - members: Arc::new(SmallVec::with_capacity(pubkeys.len())), - pubkeys, + members, + kind: RoomKind::Unknown, } } - /// Get room's id - pub fn id(&self) -> u64 { - self.id + /// Sets the kind of the room + /// + /// # Arguments + /// + /// * `kind` - The kind of room to set + /// + /// # Returns + /// + /// The room with the updated kind + pub fn kind(mut self, kind: RoomKind) -> Self { + self.kind = kind; + self } - /// Get room's member by public key - pub fn member(&self, public_key: &PublicKey) -> Option { - self.members - .iter() - .find(|m| &m.public_key == public_key) - .cloned() - } - - /// Get room's first member's public key - pub fn first_member(&self) -> Option<&NostrProfile> { - self.members.first() - } - - /// Collect room's member's public keys - pub fn public_keys(&self) -> Vec { - self.pubkeys.clone() - } - - /// Get room's display name - 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 - pub fn is_group(&self) -> bool { - self.members.len() > 2 - } - - /// Get room's last seen - pub fn last_seen(&self) -> LastSeen { - self.last_seen - } - - /// Set room's last seen - pub fn set_last_seen(&mut self, last_seen: LastSeen, cx: &mut Context) { - self.last_seen = last_seen; - cx.notify(); - } - - /// Get room's last seen as ago format + /// Calculates a human-readable representation of the time passed since room creation + /// + /// # Returns + /// + /// A SharedString representing the relative time since room creation: + /// - "now" for less than a minute + /// - "Xm" for minutes + /// - "Xh" for hours + /// - "Xd" for days + /// - Month and day (e.g. "Jan 15") for older dates pub fn ago(&self) -> SharedString { - self.last_seen.ago() + let input_time = match Local.timestamp_opt(self.created_at.as_u64() as i64, 0) { + chrono::LocalResult::Single(time) => time, + _ => return "1m".into(), + }; + + let now = Local::now(); + let duration = now.signed_duration_since(input_time); + + match duration { + d if d.num_seconds() < SECONDS_IN_MINUTE => NOW.into(), + d if d.num_minutes() < MINUTES_IN_HOUR => format!("{}m", d.num_minutes()), + d if d.num_hours() < HOURS_IN_DAY => format!("{}h", d.num_hours()), + d if d.num_days() < DAYS_IN_MONTH => format!("{}d", d.num_days()), + _ => input_time.format("%b %d").to_string(), + } + .into() } - 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 + /// Gets the profile for a specific public key + /// + /// # Arguments + /// + /// * `public_key` - The public key to get the profile for + /// * `cx` - The App context + /// + /// # Returns + /// + /// The Profile associated with the given public key + pub fn profile_by_pubkey(&self, public_key: &PublicKey, cx: &App) -> Profile { + ChatRegistry::global(cx).read(cx).profile(public_key, cx) + } + + /// Gets the first member in the room that isn't the current user + /// + /// # Arguments + /// + /// * `cx` - The App context + /// + /// # Returns + /// + /// The Profile of the first member in the room + pub fn first_member(&self, cx: &App) -> Profile { + let account = Account::global(cx).read(cx); + let profile = account.profile.clone().unwrap(); + + if let Some(public_key) = self + .members + .iter() + .filter(|&pubkey| pubkey != &profile.public_key()) + .collect::>() + .first() + { + self.profile_by_pubkey(public_key, cx) + } else { + profile + } + } + + /// Gets all avatars for members in the room + /// + /// # Arguments + /// + /// * `cx` - The App context + /// + /// # Returns + /// + /// A vector of SharedString containing all members' avatars + pub fn avatars(&self, cx: &App) -> Vec { + let profiles: Vec = self + .members + .iter() + .map(|pubkey| ChatRegistry::global(cx).read(cx).profile(pubkey, cx)) + .collect(); + + profiles + .iter() + .map(|member| member.shared_avatar()) + .collect() + } + + /// Gets a formatted string of member names + /// + /// # Arguments + /// + /// * `cx` - The App context + /// + /// # Returns + /// + /// A SharedString containing formatted member names: + /// - For a group chat: "name1, name2, +X" where X is the number of additional members + /// - For a direct message: just the name of the other person + pub fn names(&self, cx: &App) -> SharedString { + if self.is_group() { + let profiles = self + .members + .iter() + .map(|pubkey| ChatRegistry::global(cx).read(cx).profile(pubkey, cx)) + .collect::>(); + let mut name = profiles .iter() .take(2) - .map(|profile| profile.name.to_string()) + .map(|profile| profile.shared_name()) .collect::>() .join(", "); - // Create a specific name for group if profiles.len() > 2 { name = format!("{}, +{}", name, profiles.len() - 2); } - self.subject = Some(name.into()); - }; + name.into() + } else { + self.first_member(cx).shared_name() + } + } - // Update the room's members - self.members = Arc::new(profiles.into()); + /// Gets the display name for the room + /// + /// # Arguments + /// + /// * `cx` - The App context + /// + /// # Returns + /// + /// A SharedString representing the display name: + /// - The subject of the room if it exists + /// - Otherwise, the formatted names of the members + pub fn display_name(&self, cx: &App) -> SharedString { + if let Some(subject) = self.subject.as_ref() { + subject.clone() + } else { + self.names(cx) + } + } + /// Gets the display image for the room + /// + /// # Arguments + /// + /// * `cx` - The App context + /// + /// # Returns + /// + /// An Option containing the avatar: + /// - For a direct message: the other person's avatar + /// - For a group chat: None + pub fn display_image(&self, cx: &App) -> Option { + if !self.is_group() { + Some(self.first_member(cx).shared_avatar()) + } else { + None + } + } + + /// Checks if the room is a group chat + /// + /// # Returns + /// + /// true if the room has more than 2 members, false otherwise + pub fn is_group(&self) -> bool { + self.members.len() > 2 + } + + /// Updates the creation timestamp of the room + /// + /// # Arguments + /// + /// * `created_at` - The new Timestamp to set + /// * `cx` - The context to notify about the update + pub fn created_at(&mut self, created_at: Timestamp, cx: &mut Context) { + self.created_at = created_at; cx.notify(); } - /// Verify messaging_relays for all room's members + /// Fetches metadata for all members in the room + /// + /// # Arguments + /// + /// * `cx` - The context for the background task + /// + /// # Returns + /// + /// A Task that resolves to Result)>, Error> + #[allow(clippy::type_complexity)] + pub fn metadata( + &self, + cx: &mut Context, + ) -> Task)>, Error>> { + let client = get_client(); + let public_keys = self.members.clone(); + + cx.background_spawn(async move { + let mut output = vec![]; + + for public_key in public_keys.iter() { + let metadata = client.database().metadata(*public_key).await?; + output.push((*public_key, metadata)); + } + + Ok(output) + }) + } + + /// Checks which members have inbox relays set up + /// + /// # Arguments + /// + /// * `cx` - The App context + /// + /// # Returns + /// + /// A Task that resolves to Result, Error> where + /// the boolean indicates if the member has inbox relays configured pub fn messaging_relays(&self, cx: &App) -> Task, Error>> { let client = get_client(); - let pubkeys = self.public_keys(); + let pubkeys = Arc::clone(&self.members); cx.background_spawn(async move { let mut result = Vec::with_capacity(pubkeys.len()); - for pubkey in pubkeys.into_iter() { + for pubkey in pubkeys.iter() { let filter = Filter::new() .kind(Kind::InboxRelays) - .author(pubkey) + .author(*pubkey) .limit(1); let is_ready = client @@ -177,17 +343,27 @@ impl Room { .and_then(|events| events.first_owned()) .is_some(); - result.push((pubkey, is_ready)); + result.push((*pubkey, is_ready)); } Ok(result) }) } - /// Send message to all room's members + /// Sends a message to all members in the room + /// + /// # Arguments + /// + /// * `content` - The content of the message to send + /// * `cx` - The App context + /// + /// # Returns + /// + /// A Task that resolves to Result, Error> where the + /// strings contain error messages for any failed sends pub fn send_message(&self, content: String, cx: &App) -> Task, Error>> { let client = get_client(); - let pubkeys = self.public_keys(); + let pubkeys = self.members.clone(); cx.background_spawn(async move { let signer = client.signer().await?; @@ -218,48 +394,29 @@ 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 + /// Loads all messages for this room from the database + /// + /// # Arguments + /// + /// * `cx` - The App context + /// + /// # Returns + /// + /// A Task that resolves to Result, Error> containing + /// all messages for this room pub fn load_messages(&self, cx: &App) -> Task, Error>> { let client = get_client(); - let pubkeys = self.public_keys(); - let members = Arc::clone(&self.members); + let pubkeys = Arc::clone(&self.members); + + let profiles: Vec = pubkeys + .iter() + .map(|pubkey| ChatRegistry::global(cx).read(cx).profile(pubkey, cx)) + .collect(); let filter = Filter::new() .kind(Kind::PrivateDirectMessage) - .authors(pubkeys.clone()) - .pubkeys(pubkeys.clone()); + .authors(pubkeys.to_vec()) + .pubkeys(pubkeys.to_vec()); cx.background_spawn(async move { let mut messages = vec![]; @@ -282,14 +439,16 @@ impl Room { for event in events.into_iter() { let mut mentions = vec![]; + let id = event.id; + let created_at = event.created_at; let content = event.content.clone(); let tokens = parser.parse(&content); - let author = members + let author = profiles .iter() - .find(|profile| profile.public_key == event.pubkey) + .find(|profile| profile.public_key() == event.pubkey) .cloned() - .unwrap_or_else(|| NostrProfile::new(event.pubkey, Metadata::default())); + .unwrap_or_else(|| Profile::new(event.pubkey, Metadata::default())); let pubkey_tokens = tokens .filter_map(|token| match token { @@ -303,22 +462,16 @@ impl Room { .collect::>(); for pubkey in pubkey_tokens { - if let Some(profile) = - members.iter().find(|profile| profile.public_key == pubkey) - { - mentions.push(profile.clone()); - } else { - let metadata = client - .database() - .metadata(pubkey) - .await? - .unwrap_or_default(); - - mentions.push(NostrProfile::new(pubkey, metadata)); - } + mentions.push( + profiles + .iter() + .find(|profile| profile.public_key() == pubkey) + .cloned() + .unwrap_or_else(|| Profile::new(pubkey, Metadata::default())), + ); } - let message = Message::new(event.id, content, author, mentions, event.created_at); + let message = Message::new(id, content, author, created_at).with_mentions(mentions); let room_message = RoomMessage::user(message); messages.push(room_message); @@ -328,22 +481,37 @@ impl Room { }) } - /// Emit message to GPUI + /// Emits a message event to the GPUI + /// + /// # Arguments + /// + /// * `event` - The Nostr event to emit + /// * `window` - The Window to emit the event to + /// * `cx` - The context for the room + /// + /// # Effects + /// + /// Processes the event and emits an IncomingEvent to the UI when complete pub fn emit_message(&self, event: Event, window: &mut Window, cx: &mut Context) { - let client = get_client(); - let members = Arc::clone(&self.members); + let pubkeys = self.members.clone(); + let profiles: Vec = pubkeys + .iter() + .map(|pubkey| ChatRegistry::global(cx).read(cx).profile(pubkey, cx)) + .collect(); let task: Task> = cx.background_spawn(async move { let parser = NostrParser::new(); + let id = event.id; + let created_at = event.created_at; let content = event.content.clone(); let tokens = parser.parse(&content); let mut mentions = vec![]; - let author = members + let author = profiles .iter() - .find(|profile| profile.public_key == event.pubkey) + .find(|profile| profile.public_key() == event.pubkey) .cloned() - .unwrap_or_else(|| NostrProfile::new(event.pubkey, Metadata::default())); + .unwrap_or_else(|| Profile::new(event.pubkey, Metadata::default())); let pubkey_tokens = tokens .filter_map(|token| match token { @@ -357,23 +525,16 @@ impl Room { .collect::>(); for pubkey in pubkey_tokens { - if let Some(profile) = members - .iter() - .find(|profile| profile.public_key == event.pubkey) - { - mentions.push(profile.clone()); - } else { - let metadata = client - .database() - .metadata(pubkey) - .await? - .unwrap_or_default(); - - mentions.push(NostrProfile::new(pubkey, metadata)); - } + mentions.push( + profiles + .iter() + .find(|profile| profile.public_key() == pubkey) + .cloned() + .unwrap_or_else(|| Profile::new(pubkey, Metadata::default())), + ); } - let message = Message::new(event.id, content, author, mentions, event.created_at); + let message = Message::new(id, content, author, created_at).with_mentions(mentions); let room_message = RoomMessage::user(message); Ok(room_message) diff --git a/crates/common/src/last_seen.rs b/crates/common/src/last_seen.rs deleted file mode 100644 index 49035a6..0000000 --- a/crates/common/src/last_seen.rs +++ /dev/null @@ -1,57 +0,0 @@ -use chrono::{Local, TimeZone}; -use gpui::SharedString; -use nostr_sdk::prelude::*; - -const NOW: &str = "now"; -const SECONDS_IN_MINUTE: i64 = 60; -const MINUTES_IN_HOUR: i64 = 60; -const HOURS_IN_DAY: i64 = 24; -const DAYS_IN_MONTH: i64 = 30; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct LastSeen(pub Timestamp); - -impl LastSeen { - pub fn ago(&self) -> SharedString { - let now = Local::now(); - let input_time = match Local.timestamp_opt(self.0.as_u64() as i64, 0) { - chrono::LocalResult::Single(time) => time, - _ => return "Invalid timestamp".into(), - }; - let duration = now.signed_duration_since(input_time); - - match duration { - d if d.num_seconds() < SECONDS_IN_MINUTE => NOW.into(), - d if d.num_minutes() < MINUTES_IN_HOUR => format!("{}m", d.num_minutes()), - d if d.num_hours() < HOURS_IN_DAY => format!("{}h", d.num_hours()), - d if d.num_days() < DAYS_IN_MONTH => format!("{}d", d.num_days()), - _ => input_time.format("%b %d").to_string(), - } - .into() - } - - pub fn human_readable(&self) -> SharedString { - let now = Local::now(); - let input_time = match Local.timestamp_opt(self.0.as_u64() as i64, 0) { - chrono::LocalResult::Single(time) => time, - _ => return "Invalid timestamp".into(), - }; - - let input_date = input_time.date_naive(); - let now_date = now.date_naive(); - let yesterday_date = (now - chrono::Duration::days(1)).date_naive(); - - let time_format = input_time.format("%H:%M %p"); - - match input_date { - date if date == now_date => format!("Today at {time_format}"), - date if date == yesterday_date => format!("Yesterday at {time_format}"), - _ => format!("{}, {time_format}", input_time.format("%d/%m/%y")), - } - .into() - } - - pub fn set(&mut self, created_at: Timestamp) { - self.0 = created_at - } -} diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index 2e0fd5a..24d2623 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -1,3 +1,78 @@ -pub mod last_seen; +use std::{ + collections::HashSet, + hash::{DefaultHasher, Hash, Hasher}, + sync::Arc, +}; + +use anyhow::Context; +use global::constants::NIP96_SERVER; +use gpui::Image; +use itertools::Itertools; +use nostr_sdk::prelude::*; +use qrcode_generator::QrCodeEcc; +use rnglib::{Language, RNG}; + pub mod profile; -pub mod utils; + +pub async fn nip96_upload(client: &Client, file: Vec) -> anyhow::Result { + let signer = client.signer().await?; + let server_url = Url::parse(NIP96_SERVER)?; + + let config: ServerConfig = nip96::get_server_config(server_url, None).await?; + let url = nip96::upload_data(&signer, &config, file, None, None).await?; + + Ok(url) +} + +pub fn room_hash(event: &Event) -> u64 { + let mut hasher = DefaultHasher::new(); + let mut pubkeys: Vec<&PublicKey> = vec![]; + + // Add all public keys from event + pubkeys.push(&event.pubkey); + pubkeys.extend(event.tags.public_keys().collect::>()); + + // Generate unique hash + pubkeys + .into_iter() + .unique() + .sorted() + .collect::>() + .hash(&mut hasher); + + hasher.finish() +} + +pub fn device_pubkey(event: &Event) -> Result { + let n_tag = event.tags.find(TagKind::custom("n")).context("Invalid")?; + let hex = n_tag.content().context("Invalid")?; + let pubkey = PublicKey::parse(hex)?; + + Ok(pubkey) +} + +pub fn random_name(length: usize) -> String { + let rng = RNG::from(&Language::Roman); + rng.generate_names(length, true).join("-").to_lowercase() +} + +pub fn create_qr(data: &str) -> Result, anyhow::Error> { + let qr = qrcode_generator::to_png_to_vec_from_str(data, QrCodeEcc::Medium, 256)?; + let img = Arc::new(Image { + format: gpui::ImageFormat::Png, + bytes: qr.clone(), + id: 1, + }); + + Ok(img) +} + +pub fn compare(a: &[T], b: &[T]) -> bool +where + T: Eq + Hash, +{ + let a: HashSet<_> = a.iter().collect(); + let b: HashSet<_> = b.iter().collect(); + + a == b +} diff --git a/crates/common/src/profile.rs b/crates/common/src/profile.rs index 5b3d7b3..fa63cb5 100644 --- a/crates/common/src/profile.rs +++ b/crates/common/src/profile.rs @@ -2,27 +2,14 @@ use global::constants::IMAGE_SERVICE; use gpui::SharedString; use nostr_sdk::prelude::*; -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct NostrProfile { - pub public_key: PublicKey, - pub avatar: SharedString, - pub name: SharedString, +pub trait SharedProfile { + fn shared_avatar(&self) -> SharedString; + fn shared_name(&self) -> SharedString; } -impl NostrProfile { - pub fn new(public_key: PublicKey, metadata: Metadata) -> Self { - let name = Self::extract_name(&public_key, &metadata); - let avatar = Self::extract_avatar(&metadata); - - Self { - public_key, - name, - avatar, - } - } - - fn extract_avatar(metadata: &Metadata) -> SharedString { - metadata +impl SharedProfile for Profile { + fn shared_avatar(&self) -> SharedString { + self.metadata() .picture .as_ref() .filter(|picture| !picture.is_empty()) @@ -36,20 +23,20 @@ impl NostrProfile { .unwrap_or_else(|| "brand/avatar.png".into()) } - fn extract_name(public_key: &PublicKey, metadata: &Metadata) -> SharedString { - if let Some(display_name) = metadata.display_name.as_ref() { + fn shared_name(&self) -> SharedString { + if let Some(display_name) = self.metadata().display_name.as_ref() { if !display_name.is_empty() { return display_name.into(); } } - if let Some(name) = metadata.name.as_ref() { + if let Some(name) = self.metadata().name.as_ref() { if !name.is_empty() { return name.into(); } } - let pubkey = public_key.to_hex(); + let pubkey = self.public_key().to_hex(); format!("{}:{}", &pubkey[0..4], &pubkey[pubkey.len() - 4..]).into() } diff --git a/crates/common/src/utils.rs b/crates/common/src/utils.rs deleted file mode 100644 index e1da789..0000000 --- a/crates/common/src/utils.rs +++ /dev/null @@ -1,82 +0,0 @@ -use anyhow::Context; -use global::constants::NIP96_SERVER; -use gpui::Image; -use itertools::Itertools; -use nostr_sdk::prelude::*; -use qrcode_generator::QrCodeEcc; -use rnglib::{Language, RNG}; -use std::{ - collections::HashSet, - hash::{DefaultHasher, Hash, Hasher}, - sync::Arc, -}; - -pub async fn nip96_upload(client: &Client, file: Vec) -> anyhow::Result { - let signer = client.signer().await?; - let server_url = Url::parse(NIP96_SERVER)?; - - let config: ServerConfig = nip96::get_server_config(server_url, None).await?; - let url = nip96::upload_data(&signer, &config, file, None, None).await?; - - Ok(url) -} - -pub fn room_hash(event: &Event) -> u64 { - let mut hasher = DefaultHasher::new(); - let mut pubkeys: Vec<&PublicKey> = vec![]; - - // Add all public keys from event - pubkeys.push(&event.pubkey); - pubkeys.extend( - event - .tags - .public_keys() - .unique() - .sorted() - .collect::>(), - ); - - // Generate unique hash - pubkeys - .into_iter() - .unique() - .sorted() - .collect::>() - .hash(&mut hasher); - - hasher.finish() -} - -pub fn device_pubkey(event: &Event) -> Result { - let n_tag = event.tags.find(TagKind::custom("n")).context("Invalid")?; - let hex = n_tag.content().context("Invalid")?; - let pubkey = PublicKey::parse(hex)?; - - Ok(pubkey) -} - -pub fn random_name(length: usize) -> String { - let rng = RNG::from(&Language::Roman); - rng.generate_names(length, true).join("-").to_lowercase() -} - -pub fn create_qr(data: &str) -> Result, anyhow::Error> { - let qr = qrcode_generator::to_png_to_vec_from_str(data, QrCodeEcc::Medium, 256)?; - let img = Arc::new(Image { - format: gpui::ImageFormat::Png, - bytes: qr.clone(), - id: 1, - }); - - Ok(img) -} - -pub fn compare(a: &[T], b: &[T]) -> bool -where - T: Eq + Hash, -{ - let a: HashSet<_> = a.iter().collect(); - let b: HashSet<_> = b.iter().collect(); - - a == b -} diff --git a/crates/global/src/lib.rs b/crates/global/src/lib.rs index d9262b0..0c027e9 100644 --- a/crates/global/src/lib.rs +++ b/crates/global/src/lib.rs @@ -19,9 +19,12 @@ pub fn get_client() -> &'static Client { // Client options let opts = Options::new() // NIP-65 + // Coop is don't really need to enable this option, + // but this will help the client discover user's messaging relays efficiently. .gossip(true) // Skip all very slow relays - .max_avg_latency(Duration::from_secs(2)); + // Note: max delay is 800ms + .max_avg_latency(Duration::from_millis(800)); // Setup Nostr Client ClientBuilder::default().database(lmdb).opts(opts).build() diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml index 6326038..899f302 100644 --- a/crates/ui/Cargo.toml +++ b/crates/ui/Cargo.toml @@ -24,10 +24,3 @@ uuid = "1.10" once_cell = "1.19.0" image = "0.25.1" linkify = "0.10.0" - -[dev-dependencies] -criterion = "0.5" - -[[bench]] -name = "text_benchmark" -harness = false diff --git a/crates/ui/benches/text_benchmark.rs b/crates/ui/benches/text_benchmark.rs deleted file mode 100644 index df4b31c..0000000 --- a/crates/ui/benches/text_benchmark.rs +++ /dev/null @@ -1,142 +0,0 @@ -use common::profile::NostrProfile; -use criterion::{black_box, criterion_group, criterion_main, Criterion}; -use gpui::SharedString; -use nostr_sdk::prelude::*; -use ui::text::render_plain_text_mut; - -fn create_test_profiles() -> Vec { - let mut profiles = Vec::new(); - - // Create a few test profiles - for i in 0..5 { - let keypair = Keys::generate(); - let profile = NostrProfile { - public_key: keypair.public_key(), - name: SharedString::from(format!("user{}", i)), - avatar: SharedString::from(format!("avatar{}", i)), - // Add other required fields based on NostrProfile definition - // This is a simplified version - adjust based on your actual NostrProfile struct - }; - profiles.push(profile); - } - - profiles -} - -fn benchmark_plain_text(c: &mut Criterion) { - let profiles = create_test_profiles(); - - // Simple text without any links or entities - let simple_text = "This is a simple text message without any links or entities."; - - // Text with URLs - let text_with_urls = - "Check out https://example.com and https://nostr.com for more information."; - - // Text with nostr entities - let text_with_nostr = "I found this note nostr:note1qw5uy7hsqs4jcsvmjc2rj5t6f5uuenwg3yapm5l58srprspvshlspr4mh3 from npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft"; - - // Mixed content with urls and nostr entities - let mixed_content = "Check out https://example.com and my profile nostr:npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft along with this event nevent1qw5uy7hsqs4jcsvmjc2rj5t6f5uuenwg3yapm5l58srprspvshlspr4mh3"; - - // Long text with multiple links and entities - let long_text = "Here's a long message with multiple links like https://example1.com, https://example2.com, and https://example3.com. It also has nostr entities like npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft, note1qw5uy7hsqs4jcsvmjc2rj5t6f5uuenwg3yapm5l58srprspvshlspr4mh3, and nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuerpd46hxtnfdupzp8xummjw3exgcnqvmpw35xjueqvdnyqystngfxk5hsnfd9h8jtr8a4klacnp".repeat(3); - - // Benchmark with simple text - c.bench_function("render_plain_text_simple", |b| { - b.iter(|| { - let mut text = String::new(); - let mut highlights = Vec::new(); - let mut link_ranges = Vec::new(); - let mut link_urls = Vec::new(); - - render_plain_text_mut( - black_box(simple_text), - black_box(&profiles), - &mut text, - &mut highlights, - &mut link_ranges, - &mut link_urls, - ) - }) - }); - - // Benchmark with URLs - c.bench_function("render_plain_text_urls", |b| { - b.iter(|| { - let mut text = String::new(); - let mut highlights = Vec::new(); - let mut link_ranges = Vec::new(); - let mut link_urls = Vec::new(); - - render_plain_text_mut( - black_box(text_with_urls), - black_box(&profiles), - &mut text, - &mut highlights, - &mut link_ranges, - &mut link_urls, - ) - }) - }); - - // Benchmark with nostr entities - c.bench_function("render_plain_text_nostr", |b| { - b.iter(|| { - let mut text = String::new(); - let mut highlights = Vec::new(); - let mut link_ranges = Vec::new(); - let mut link_urls = Vec::new(); - - render_plain_text_mut( - black_box(text_with_nostr), - black_box(&profiles), - &mut text, - &mut highlights, - &mut link_ranges, - &mut link_urls, - ) - }) - }); - - // Benchmark with mixed content - c.bench_function("render_plain_text_mixed", |b| { - b.iter(|| { - let mut text = String::new(); - let mut highlights = Vec::new(); - let mut link_ranges = Vec::new(); - let mut link_urls = Vec::new(); - - render_plain_text_mut( - black_box(mixed_content), - black_box(&profiles), - &mut text, - &mut highlights, - &mut link_ranges, - &mut link_urls, - ) - }) - }); - - // Benchmark with long text - c.bench_function("render_plain_text_long", |b| { - b.iter(|| { - let mut text = String::new(); - let mut highlights = Vec::new(); - let mut link_ranges = Vec::new(); - let mut link_urls = Vec::new(); - - render_plain_text_mut( - black_box(&long_text), - black_box(&profiles), - &mut text, - &mut highlights, - &mut link_ranges, - &mut link_urls, - ) - }) - }); -} - -criterion_group!(benches, benchmark_plain_text); -criterion_main!(benches); diff --git a/crates/ui/src/switch.rs b/crates/ui/src/switch.rs index 5de5c9c..f3fc7b4 100644 --- a/crates/ui/src/switch.rs +++ b/crates/ui/src/switch.rs @@ -161,7 +161,7 @@ impl Element for Switch { if !self.disabled && prev_checked .borrow() - .map_or(false, |prev| prev != checked) + .is_some_and(|prev| prev != checked) { let dur = Duration::from_secs_f64(0.15); cx.spawn(async move |cx| { diff --git a/crates/ui/src/text.rs b/crates/ui/src/text.rs index 0805289..1ebcee8 100644 --- a/crates/ui/src/text.rs +++ b/crates/ui/src/text.rs @@ -1,4 +1,4 @@ -use common::profile::NostrProfile; +use common::profile::SharedProfile; use gpui::{ AnyElement, AnyView, App, ElementId, FontWeight, HighlightStyle, InteractiveText, IntoElement, SharedString, StyledText, UnderlineStyle, Window, @@ -43,7 +43,7 @@ pub struct RichText { } impl RichText { - pub fn new(content: String, profiles: &[NostrProfile]) -> Self { + pub fn new(content: String, profiles: &[Profile]) -> Self { let mut text = String::new(); let mut highlights = Vec::new(); let mut link_ranges = Vec::new(); @@ -154,7 +154,7 @@ impl RichText { pub fn render_plain_text_mut( content: &str, - profiles: &[NostrProfile], + profiles: &[Profile], text: &mut String, highlights: &mut Vec<(Range, Highlight)>, link_ranges: &mut Vec>, @@ -164,9 +164,9 @@ pub fn render_plain_text_mut( text.push_str(content); // Create a profile lookup using PublicKey directly - let profile_lookup: HashMap<&PublicKey, &NostrProfile> = profiles + let profile_lookup: HashMap = profiles .iter() - .map(|profile| (&profile.public_key, profile)) + .map(|profile| (profile.public_key(), profile.clone())) .collect(); // Process regular URLs using linkify @@ -263,18 +263,18 @@ pub fn render_plain_text_mut( let profile_match = if entity_without_prefix.starts_with("npub") { PublicKey::from_bech32(entity_without_prefix) .ok() - .and_then(|pubkey| profile_lookup.get(&pubkey).copied()) + .and_then(|pubkey| profile_lookup.get(&pubkey).cloned()) } else if entity_without_prefix.starts_with("nprofile") { Nip19Profile::from_bech32(entity_without_prefix) .ok() - .and_then(|profile| profile_lookup.get(&profile.public_key).copied()) + .and_then(|profile| profile_lookup.get(&profile.public_key).cloned()) } else { None }; if let Some(profile) = profile_match { // Profile found - create a mention - let display_name = format!("@{}", profile.name); + let display_name = format!("@{}", profile.shared_name()); // Replace mention with profile name text.replace_range(range.clone(), &display_name); diff --git a/rust-toolchain.toml b/rust-toolchain.toml index d2f1b1c..68578e1 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,5 +1,5 @@ [toolchain] -channel = "1.85" +channel = "1.86" profile = "minimal" components = ["rustfmt", "clippy"] targets = [