diff --git a/Cargo.lock b/Cargo.lock index 82a0241..5faf353 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,20 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "account" +version = "0.2.11" +dependencies = [ + "anyhow", + "gpui", + "log", + "nostr", + "nostr-sdk", + "smallvec", + "smol", + "states", +] + [[package]] name = "adler2" version = "2.0.1" @@ -596,7 +610,7 @@ dependencies = [ "bitflags 2.10.0", "cexpr", "clang-sys", - "itertools 0.11.0", + "itertools 0.13.0", "log", "prettyplease", "proc-macro2", @@ -616,7 +630,7 @@ dependencies = [ "bitflags 2.10.0", "cexpr", "clang-sys", - "itertools 0.11.0", + "itertools 0.13.0", "log", "prettyplease", "proc-macro2", @@ -968,9 +982,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.43" +version = "1.2.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "739eb0f94557554b3ca9a86d2d37bebd49c5e6d0c1d2bda35ba5bdac830befc2" +checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3" dependencies = [ "find-msvc-tools", "jobserver", @@ -1048,6 +1062,59 @@ dependencies = [ "zeroize", ] +[[package]] +name = "chat" +version = "0.2.11" +dependencies = [ + "account", + "anyhow", + "common", + "futures", + "fuzzy-matcher", + "gpui", + "itertools 0.13.0", + "log", + "nostr", + "nostr-sdk", + "person", + "serde", + "serde_json", + "settings", + "smallvec", + "smol", + "states", +] + +[[package]] +name = "chat_ui" +version = "0.2.11" +dependencies = [ + "account", + "anyhow", + "chat", + "common", + "emojis", + "gpui", + "gpui_tokio", + "indexset", + "itertools 0.13.0", + "linkify", + "log", + "nostr", + "nostr-sdk", + "once_cell", + "person", + "regex", + "serde", + "serde_json", + "settings", + "smallvec", + "smol", + "states", + "theme", + "ui", +] + [[package]] name = "chrono" version = "0.4.42" @@ -1166,7 +1233,7 @@ dependencies = [ [[package]] name = "collections" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#8b051d6cc3c7c3bcda16702f30dc0fabe7b9f881" +source = "git+https://github.com/zed-industries/zed#ecbdffc84f1165323f256e8485ae84320550c759" dependencies = [ "indexmap", "rustc-hash 2.1.1", @@ -1276,9 +1343,12 @@ checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" name = "coop" version = "0.2.11" dependencies = [ + "account", "anyhow", "assets", "auto_update", + "chat", + "chat_ui", "common", "dirs 5.0.1", "flume", @@ -1294,7 +1364,7 @@ dependencies = [ "nostr-connect", "nostr-sdk", "oneshot", - "registry", + "person", "reqwest_client", "rust-i18n", "serde", @@ -1606,7 +1676,7 @@ dependencies = [ [[package]] name = "derive_refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#8b051d6cc3c7c3bcda16702f30dc0fabe7b9f881" +source = "git+https://github.com/zed-industries/zed#ecbdffc84f1165323f256e8485ae84320550c759" dependencies = [ "proc-macro2", "quote", @@ -1873,7 +1943,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2503,13 +2573,14 @@ dependencies = [ [[package]] name = "gpui" version = "0.2.2" -source = "git+https://github.com/zed-industries/zed#8b051d6cc3c7c3bcda16702f30dc0fabe7b9f881" +source = "git+https://github.com/zed-industries/zed#ecbdffc84f1165323f256e8485ae84320550c759" dependencies = [ "anyhow", "as-raw-xcb-connection", "ashpd 0.11.0", "async-task", "bindgen 0.71.1", + "bitflags 2.10.0", "blade-graphics", "blade-macros", "blade-util", @@ -2583,6 +2654,7 @@ dependencies = [ "wayland-cursor", "wayland-protocols 0.31.2", "wayland-protocols-plasma", + "wayland-protocols-wlr", "windows 0.61.3", "windows-core 0.61.2", "windows-numerics", @@ -2598,7 +2670,7 @@ dependencies = [ [[package]] name = "gpui_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#8b051d6cc3c7c3bcda16702f30dc0fabe7b9f881" +source = "git+https://github.com/zed-industries/zed#ecbdffc84f1165323f256e8485ae84320550c759" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -2609,7 +2681,7 @@ dependencies = [ [[package]] name = "gpui_tokio" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#8b051d6cc3c7c3bcda16702f30dc0fabe7b9f881" +source = "git+https://github.com/zed-industries/zed#ecbdffc84f1165323f256e8485ae84320550c759" dependencies = [ "anyhow", "gpui", @@ -2838,7 +2910,7 @@ dependencies = [ [[package]] name = "http_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#8b051d6cc3c7c3bcda16702f30dc0fabe7b9f881" +source = "git+https://github.com/zed-industries/zed#ecbdffc84f1165323f256e8485ae84320550c759" dependencies = [ "anyhow", "async-compression", @@ -2863,7 +2935,7 @@ dependencies = [ [[package]] name = "http_client_tls" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#8b051d6cc3c7c3bcda16702f30dc0fabe7b9f881" +source = "git+https://github.com/zed-industries/zed#ecbdffc84f1165323f256e8485ae84320550c759" dependencies = [ "rustls", "rustls-platform-verifier", @@ -3091,9 +3163,9 @@ dependencies = [ [[package]] name = "ignore" -version = "0.4.24" +version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81776e6f9464432afcc28d03e52eb101c93b6f0566f52aef2427663e700f0403" +checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" dependencies = [ "crossbeam-deque", "globset", @@ -3350,23 +3422,16 @@ name = "key_store" version = "0.2.11" dependencies = [ "anyhow", - "common", "futures", "gpui", - "i18n", - "itertools 0.13.0", "log", "nostr", "nostr-sdk", - "rust-i18n", "serde", "serde_json", - "settings", "smallvec", "smol", "states", - "theme", - "ui", ] [[package]] @@ -3676,7 +3741,7 @@ dependencies = [ [[package]] name = "media" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#8b051d6cc3c7c3bcda16702f30dc0fabe7b9f881" +source = "git+https://github.com/zed-industries/zed#ecbdffc84f1165323f256e8485ae84320550c759" dependencies = [ "anyhow", "bindgen 0.71.1", @@ -3932,7 +3997,7 @@ dependencies = [ [[package]] name = "nostr" version = "0.43.0" -source = "git+https://github.com/rust-nostr/nostr#8927f6630c071d982f9aa79fb9cd6807ae554abe" +source = "git+https://github.com/rust-nostr/nostr#6befa6de8ab080a8153b7f8b788981d7be365ebf" dependencies = [ "aes", "base64", @@ -3956,7 +4021,7 @@ dependencies = [ [[package]] name = "nostr-connect" version = "0.43.0" -source = "git+https://github.com/rust-nostr/nostr#8927f6630c071d982f9aa79fb9cd6807ae554abe" +source = "git+https://github.com/rust-nostr/nostr#6befa6de8ab080a8153b7f8b788981d7be365ebf" dependencies = [ "async-utility", "nostr", @@ -3968,7 +4033,7 @@ dependencies = [ [[package]] name = "nostr-database" version = "0.43.0" -source = "git+https://github.com/rust-nostr/nostr#8927f6630c071d982f9aa79fb9cd6807ae554abe" +source = "git+https://github.com/rust-nostr/nostr#6befa6de8ab080a8153b7f8b788981d7be365ebf" dependencies = [ "flatbuffers", "lru", @@ -3979,7 +4044,7 @@ dependencies = [ [[package]] name = "nostr-gossip" version = "0.43.0" -source = "git+https://github.com/rust-nostr/nostr#8927f6630c071d982f9aa79fb9cd6807ae554abe" +source = "git+https://github.com/rust-nostr/nostr#6befa6de8ab080a8153b7f8b788981d7be365ebf" dependencies = [ "nostr", ] @@ -3987,7 +4052,7 @@ dependencies = [ [[package]] name = "nostr-gossip-memory" version = "0.43.0" -source = "git+https://github.com/rust-nostr/nostr#8927f6630c071d982f9aa79fb9cd6807ae554abe" +source = "git+https://github.com/rust-nostr/nostr#6befa6de8ab080a8153b7f8b788981d7be365ebf" dependencies = [ "indexmap", "lru", @@ -3999,7 +4064,7 @@ dependencies = [ [[package]] name = "nostr-lmdb" version = "0.43.0" -source = "git+https://github.com/rust-nostr/nostr#8927f6630c071d982f9aa79fb9cd6807ae554abe" +source = "git+https://github.com/rust-nostr/nostr#6befa6de8ab080a8153b7f8b788981d7be365ebf" dependencies = [ "async-utility", "flume", @@ -4013,7 +4078,7 @@ dependencies = [ [[package]] name = "nostr-relay-pool" version = "0.43.0" -source = "git+https://github.com/rust-nostr/nostr#8927f6630c071d982f9aa79fb9cd6807ae554abe" +source = "git+https://github.com/rust-nostr/nostr#6befa6de8ab080a8153b7f8b788981d7be365ebf" dependencies = [ "async-utility", "async-wsocket", @@ -4030,7 +4095,7 @@ dependencies = [ [[package]] name = "nostr-sdk" version = "0.43.0" -source = "git+https://github.com/rust-nostr/nostr#8927f6630c071d982f9aa79fb9cd6807ae554abe" +source = "git+https://github.com/rust-nostr/nostr#6befa6de8ab080a8153b7f8b788981d7be365ebf" dependencies = [ "async-utility", "nostr", @@ -4056,7 +4121,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4548,13 +4613,29 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perf" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#8b051d6cc3c7c3bcda16702f30dc0fabe7b9f881" +source = "git+https://github.com/zed-industries/zed#ecbdffc84f1165323f256e8485ae84320550c759" dependencies = [ "collections", "serde", "serde_json", ] +[[package]] +name = "person" +version = "0.2.11" +dependencies = [ + "anyhow", + "common", + "gpui", + "itertools 0.13.0", + "log", + "nostr", + "nostr-sdk", + "smallvec", + "smol", + "states", +] + [[package]] name = "phf" version = "0.11.3" @@ -4932,7 +5013,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -5165,7 +5246,7 @@ dependencies = [ [[package]] name = "refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#8b051d6cc3c7c3bcda16702f30dc0fabe7b9f881" +source = "git+https://github.com/zed-industries/zed#ecbdffc84f1165323f256e8485ae84320550c759" dependencies = [ "derive_refineable", ] @@ -5199,27 +5280,6 @@ version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" -[[package]] -name = "registry" -version = "0.2.11" -dependencies = [ - "anyhow", - "common", - "futures", - "fuzzy-matcher", - "gpui", - "itertools 0.13.0", - "log", - "nostr", - "nostr-sdk", - "serde", - "serde_json", - "settings", - "smallvec", - "smol", - "states", -] - [[package]] name = "reqwest" version = "0.12.24" @@ -5272,7 +5332,7 @@ dependencies = [ [[package]] name = "reqwest_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#8b051d6cc3c7c3bcda16702f30dc0fabe7b9f881" +source = "git+https://github.com/zed-industries/zed#ecbdffc84f1165323f256e8485ae84320550c759" dependencies = [ "anyhow", "bytes", @@ -5326,11 +5386,11 @@ dependencies = [ [[package]] name = "rope" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#8b051d6cc3c7c3bcda16702f30dc0fabe7b9f881" +source = "git+https://github.com/zed-industries/zed#ecbdffc84f1165323f256e8485ae84320550c759" dependencies = [ "arrayvec", + "gpui", "log", - "rayon", "sum_tree", "unicode-segmentation", "util", @@ -5485,7 +5545,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -5792,7 +5852,7 @@ checksum = "16c2f82143577edb4921b71ede051dac62ca3c16084e918bf7b40c96ae10eb33" [[package]] name = "semantic_version" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#8b051d6cc3c7c3bcda16702f30dc0fabe7b9f881" +source = "git+https://github.com/zed-industries/zed#ecbdffc84f1165323f256e8485ae84320550c759" dependencies = [ "anyhow", "serde", @@ -6239,11 +6299,12 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sum_tree" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#8b051d6cc3c7c3bcda16702f30dc0fabe7b9f881" +source = "git+https://github.com/zed-industries/zed#ecbdffc84f1165323f256e8485ae84320550c759" dependencies = [ "arrayvec", + "futures", + "itertools 0.14.0", "log", - "rayon", ] [[package]] @@ -6516,7 +6577,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix 1.1.2", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -7088,20 +7149,13 @@ version = "0.2.11" dependencies = [ "anyhow", "common", - "emojis", "gpui", - "i18n", "image", "itertools 0.13.0", - "linkify", "log", "lsp-types", - "nostr-sdk", - "once_cell", "regex", - "registry", "rope", - "rust-i18n", "serde", "serde_json", "smallvec", @@ -7150,9 +7204,9 @@ checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" [[package]] name = "unicode-ident" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-linebreak" @@ -7162,18 +7216,18 @@ checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" [[package]] name = "unicode-normalization" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" dependencies = [ "tinyvec", ] [[package]] name = "unicode-properties" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" [[package]] name = "unicode-script" @@ -7275,7 +7329,7 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "util" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#8b051d6cc3c7c3bcda16702f30dc0fabe7b9f881" +source = "git+https://github.com/zed-industries/zed#ecbdffc84f1165323f256e8485ae84320550c759" dependencies = [ "anyhow", "async-fs", @@ -7310,7 +7364,7 @@ dependencies = [ [[package]] name = "util_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#8b051d6cc3c7c3bcda16702f30dc0fabe7b9f881" +source = "git+https://github.com/zed-industries/zed#ecbdffc84f1165323f256e8485ae84320550c759" dependencies = [ "perf", "quote", @@ -7391,9 +7445,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version-compare" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" [[package]] name = "version_check" @@ -7612,6 +7666,19 @@ dependencies = [ "wayland-scanner", ] +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efd94963ed43cf9938a090ca4f7da58eb55325ec8200c3848963e98dc25b78ec" +dependencies = [ + "bitflags 2.10.0", + "wayland-backend", + "wayland-client", + "wayland-protocols 0.32.9", + "wayland-scanner", +] + [[package]] name = "wayland-scanner" version = "0.31.7" @@ -7758,7 +7825,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 2725454..4e20754 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,6 @@ nostr-sdk = { git = "https://github.com/rust-nostr/nostr", features = [ "nip96", anyhow = "1.0.44" chrono = "0.4.38" dirs = "5.0" -emojis = "0.6.4" futures = "0.3" itertools = "0.13.0" log = "0.4" diff --git a/crates/account/Cargo.toml b/crates/account/Cargo.toml new file mode 100644 index 0000000..6100cca --- /dev/null +++ b/crates/account/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "account" +version.workspace = true +edition.workspace = true +publish.workspace = true + +[dependencies] +states = { path = "../states" } + +gpui.workspace = true + +nostr.workspace = true +nostr-sdk.workspace = true + +anyhow.workspace = true +smallvec.workspace = true +smol.workspace = true +log.workspace = true diff --git a/crates/account/src/lib.rs b/crates/account/src/lib.rs new file mode 100644 index 0000000..1a79a46 --- /dev/null +++ b/crates/account/src/lib.rs @@ -0,0 +1,54 @@ +use gpui::{App, AppContext, Context, Entity, Global, Task}; +use nostr_sdk::prelude::*; +use smallvec::{smallvec, SmallVec}; + +pub fn init(public_key: PublicKey, cx: &mut App) { + Account::set_global(cx.new(|cx| Account::new(public_key, cx)), cx); +} + +struct GlobalAccount(Entity); + +impl Global for GlobalAccount {} + +pub struct Account { + /// The public key of the account + public_key: PublicKey, + + /// Tasks for asynchronous operations + _tasks: SmallVec<[Task<()>; 1]>, +} + +impl Account { + /// Retrieve the global account state + pub fn global(cx: &App) -> Entity { + cx.global::().0.clone() + } + + /// Check if the global account state exists + pub fn has_global(cx: &App) -> bool { + cx.has_global::() + } + + /// Remove the global account state + pub fn remove_global(cx: &mut App) { + cx.remove_global::(); + } + + /// Set the global account instance + pub(crate) fn set_global(state: Entity, cx: &mut App) { + cx.set_global(GlobalAccount(state)); + } + + /// Create a new account instance + pub(crate) fn new(public_key: PublicKey, _cx: &mut Context) -> Self { + Self { + public_key, + _tasks: smallvec![], + } + } + + /// Get the public key of the account + pub fn public_key(&self) -> PublicKey { + self.public_key + } +} diff --git a/crates/registry/Cargo.toml b/crates/chat/Cargo.toml similarity index 82% rename from crates/registry/Cargo.toml rename to crates/chat/Cargo.toml index 4ba17ac..405f2e6 100644 --- a/crates/registry/Cargo.toml +++ b/crates/chat/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "registry" +name = "chat" version.workspace = true edition.workspace = true publish.workspace = true @@ -7,6 +7,8 @@ publish.workspace = true [dependencies] common = { path = "../common" } states = { path = "../states" } +account = { path = "../account" } +person = { path = "../person" } settings = { path = "../settings" } gpui.workspace = true diff --git a/crates/registry/src/lib.rs b/crates/chat/src/lib.rs similarity index 63% rename from crates/registry/src/lib.rs rename to crates/chat/src/lib.rs index fdfbf38..c1c2e98 100644 --- a/crates/registry/src/lib.rs +++ b/crates/chat/src/lib.rs @@ -1,18 +1,17 @@ use std::cmp::Reverse; use std::collections::{HashMap, HashSet}; +use account::Account; use anyhow::Error; use common::event::EventUtils; use fuzzy_matcher::skim::SkimMatcherV2; use fuzzy_matcher::FuzzyMatcher; -use gpui::{ - App, AppContext, AsyncApp, Context, Entity, EventEmitter, Global, Task, WeakEntity, Window, -}; +use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, Task, Window}; use nostr_sdk::prelude::*; use room::RoomKind; use settings::AppSettings; use smallvec::{smallvec, SmallVec}; -use states::app_state; +use states::{app_state, NewMessage}; use crate::room::Room; @@ -20,162 +19,54 @@ pub mod message; pub mod room; pub fn init(cx: &mut App) { - Registry::set_global(cx.new(Registry::new), cx); + ChatRegistry::set_global(cx.new(ChatRegistry::new), cx); } -struct GlobalRegistry(Entity); - -impl Global for GlobalRegistry {} - -#[derive(Debug)] -pub enum RegistryEvent { - Open(WeakEntity), - Close(u64), - NewRequest(RoomKind), +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum ChatEvent { + OpenRoom(u64), + CloseRoom(u64), + NewChatRequest(RoomKind), } -pub struct Registry { +struct GlobalChatRegistry(Entity); + +impl Global for GlobalChatRegistry {} + +pub struct ChatRegistry { /// Collection of all chat rooms pub rooms: Vec>, - /// Collection of all persons (user profiles) - pub persons: HashMap>, - /// Loading status of the registry pub loading: bool, - /// Public Key of the currently activated signer - signer_pubkey: Option, - /// Tasks for asynchronous operations _tasks: SmallVec<[Task<()>; 2]>, } -impl EventEmitter for Registry {} +impl EventEmitter for ChatRegistry {} -impl Registry { +impl ChatRegistry { /// Retrieve the global registry state pub fn global(cx: &App) -> Entity { - cx.global::().0.clone() - } - - /// Retrieve the registry instance - pub fn read_global(cx: &App) -> &Self { - cx.global::().0.read(cx) + cx.global::().0.clone() } /// Set the global registry instance pub(crate) fn set_global(state: Entity, cx: &mut App) { - cx.set_global(GlobalRegistry(state)); + cx.set_global(GlobalChatRegistry(state)); } /// Create a new registry instance - pub(crate) fn new(cx: &mut Context) -> Self { - let mut tasks = smallvec![]; - - tasks.push( - // Load all user profiles from the database - cx.spawn(async move |this, cx| { - match Self::load_persons(cx).await { - Ok(profiles) => { - this.update(cx, |this, cx| { - this.set_persons(profiles, cx); - }) - .ok(); - } - Err(e) => { - log::error!("Failed to load persons: {e}"); - } - }; - }), - ); - + pub(crate) fn new(_cx: &mut Context) -> Self { Self { rooms: vec![], - persons: HashMap::new(), - signer_pubkey: None, loading: true, - _tasks: tasks, - } - } - - /// Create a task to load all user profiles from the database - fn load_persons(cx: &AsyncApp) -> Task, Error>> { - cx.background_spawn(async move { - let client = app_state().client(); - let filter = Filter::new().kind(Kind::Metadata).limit(200); - let events = client.database().query(filter).await?; - - let mut profiles = vec![]; - - for event in events.into_iter() { - let metadata = Metadata::from_json(event.content).unwrap_or_default(); - let profile = Profile::new(event.pubkey, metadata); - profiles.push(profile); - } - - Ok(profiles) - }) - } - - /// Returns the public key of the currently activated signer. - pub fn signer_pubkey(&self) -> Option { - self.signer_pubkey - } - - /// Update the public key of the currently activated signer. - pub fn set_signer_pubkey(&mut self, public_key: PublicKey, cx: &mut Context) { - self.signer_pubkey = Some(public_key); - cx.notify(); - } - - /// Insert batch of persons - pub fn set_persons(&mut self, profiles: Vec, cx: &mut Context) { - for profile in profiles.into_iter() { - self.persons - .insert(profile.public_key(), cx.new(|_| profile)); - } - cx.notify(); - } - - /// Get single person - pub fn get_person(&self, public_key: &PublicKey, cx: &App) -> Profile { - self.persons - .get(public_key) - .map(|e| e.read(cx)) - .cloned() - .unwrap_or(Profile::new(public_key.to_owned(), Metadata::default())) - } - - /// Get group of persons - pub fn get_group_person(&self, public_keys: &[PublicKey], cx: &App) -> Vec { - let mut profiles = vec![]; - - for public_key in public_keys.iter() { - let profile = self.get_person(public_key, cx); - profiles.push(profile); - } - - profiles - } - - /// Insert or update a person - pub fn insert_or_update_person(&mut self, profile: Profile, cx: &mut App) { - let public_key = profile.public_key(); - - match self.persons.get(&public_key) { - Some(person) => { - person.update(cx, |this, cx| { - *this = profile; - cx.notify(); - }); - } - None => { - self.persons.insert(public_key, cx.new(|_| profile)); - } + _tasks: smallvec![], } } + /// Set the loading status of the chat registry pub fn set_loading(&mut self, loading: bool, cx: &mut Context) { self.loading = loading; cx.notify(); @@ -216,7 +107,7 @@ impl Registry { /// Close a room. pub fn close_room(&mut self, id: u64, cx: &mut Context) { if self.rooms.iter().any(|r| r.read(cx).id == id) { - cx.emit(RegistryEvent::Close(id)); + cx.emit(ChatEvent::CloseRoom(id)); } } @@ -252,12 +143,7 @@ impl Registry { /// Reset the registry. pub fn reset(&mut self, cx: &mut Context) { - // Clear the current identity - self.signer_pubkey = None; - - // Clear all current rooms self.rooms.clear(); - cx.notify(); } @@ -378,22 +264,15 @@ impl Registry { } } - /// Push a new Room to the global registry + /// Push a new room to the chat registry pub fn push_room(&mut self, room: Entity, cx: &mut Context) { - let other_id = room.read(cx).id; - let find_room = self.rooms.iter().find(|this| this.read(cx).id == other_id); + let id = room.read(cx).id; - let weak_room = if let Some(room) = find_room { - room.downgrade() - } else { - let weak_room = room.downgrade(); - // Add this room to the registry + if !self.rooms.iter().any(|r| r.read(cx).id == id) { self.add_room(room, cx); + } - weak_room - }; - - cx.emit(RegistryEvent::Open(weak_room)); + cx.emit(ChatEvent::OpenRoom(id)); } /// Refresh messages for a room in the global registry @@ -413,24 +292,15 @@ impl Registry { /// /// If the room doesn't exist, it will be created. /// Updates room ordering based on the most recent messages. - pub fn event_to_message( - &mut self, - gift_wrap: EventId, - event: UnsignedEvent, - window: &mut Window, - cx: &mut Context, - ) { - let id = event.uniq_id(); - let author = event.pubkey; - - let Some(public_key) = self.signer_pubkey else { - return; - }; + pub fn new_message(&mut self, msg: NewMessage, window: &mut Window, cx: &mut Context) { + let id = msg.rumor.uniq_id(); + let author = msg.rumor.pubkey; + let account = Account::global(cx); if let Some(room) = self.rooms.iter().find(|room| room.read(cx).id == id) { - let is_new_event = event.created_at > room.read(cx).created_at; - let created_at = event.created_at; - let event_for_emit = event.clone(); + let is_new_event = msg.rumor.created_at > room.read(cx).created_at; + let created_at = msg.rumor.created_at; + let event_for_emit = msg.rumor.clone(); // Update room room.update(cx, |this, cx| { @@ -439,14 +309,14 @@ impl Registry { } // Set this room is ongoing if the new message is from current user - if author == public_key { + if author == account.read(cx).public_key() { this.set_ongoing(cx); } // Emit the new message to the room let event_to_emit = event_for_emit.clone(); cx.defer_in(window, move |this, _window, cx| { - this.emit_message(gift_wrap, event_to_emit, cx); + this.emit_message(msg.gift_wrap, event_to_emit, cx); }); }); @@ -458,11 +328,11 @@ impl Registry { } } else { // Push the new room to the front of the list - self.add_room(cx.new(|_| Room::from(&event)), cx); + self.add_room(cx.new(|_| Room::from(&msg.rumor)), cx); // Notify the UI about the new room cx.defer_in(window, move |_this, _window, cx| { - cx.emit(RegistryEvent::NewRequest(RoomKind::default())); + cx.emit(ChatEvent::NewChatRequest(RoomKind::default())); }); } } diff --git a/crates/registry/src/message.rs b/crates/chat/src/message.rs similarity index 100% rename from crates/registry/src/message.rs rename to crates/chat/src/message.rs diff --git a/crates/registry/src/room.rs b/crates/chat/src/room.rs similarity index 95% rename from crates/registry/src/room.rs rename to crates/chat/src/room.rs index 337256c..b55f0c7 100644 --- a/crates/registry/src/room.rs +++ b/crates/chat/src/room.rs @@ -3,15 +3,15 @@ use std::collections::{HashMap, HashSet}; use std::hash::{Hash, Hasher}; use std::time::Duration; +use account::Account; use anyhow::{anyhow, Error}; use common::display::RenderedProfile; use common::event::EventUtils; use gpui::{App, AppContext, Context, EventEmitter, SharedString, SharedUri, Task}; use nostr_sdk::prelude::*; +use person::PersonRegistry; use states::{app_state, SignerKind, SEND_RETRY}; -use crate::Registry; - #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub struct SendOptions { pub backup: bool, @@ -119,10 +119,12 @@ pub enum RoomKind { #[derive(Debug)] pub struct Room { + /// Conversation ID pub id: u64, + /// The timestamp of the last message in the room pub created_at: Timestamp, /// Subject of the room - pub subject: Option, + pub subject: Option, /// All members of the room pub members: Vec, /// Kind @@ -169,7 +171,7 @@ impl From<&Event> for Room { let subject = val .tags .find(TagKind::Subject) - .and_then(|tag| tag.content().map(|s| s.to_owned())); + .and_then(|tag| tag.content().map(|s| s.to_owned().into())); Room { id, @@ -193,7 +195,7 @@ impl From<&UnsignedEvent> for Room { let subject = val .tags .find(TagKind::Subject) - .and_then(|tag| tag.content().map(|s| s.to_owned())); + .and_then(|tag| tag.content().map(|s| s.to_owned().into())); Room { id, @@ -262,8 +264,11 @@ impl Room { } /// Updates the subject of the room - pub fn set_subject(&mut self, subject: String, cx: &mut Context) { - self.subject = Some(subject); + pub fn set_subject(&mut self, subject: T, cx: &mut Context) + where + T: Into, + { + self.subject = Some(subject.into()); cx.notify(); } @@ -279,8 +284,8 @@ impl Room { /// Gets the display name for the room pub fn display_name(&self, cx: &App) -> SharedString { - if let Some(subject) = self.subject.clone() { - SharedString::from(subject) + if let Some(value) = self.subject.clone() { + value } else { self.merged_name(cx) } @@ -299,28 +304,29 @@ impl Room { /// /// Display member is always different from the current user. pub fn display_member(&self, cx: &App) -> Profile { - let registry = Registry::global(cx); - let signer_pubkey = registry.read(cx).signer_pubkey(); + let persons = PersonRegistry::global(cx); + let account = Account::global(cx); + let public_key = account.read(cx).public_key(); let target_member = self .members .iter() - .find(|&member| Some(member) != signer_pubkey.as_ref()) + .find(|&member| member != &public_key) .or_else(|| self.members.first()) .expect("Room should have at least one member"); - registry.read(cx).get_person(target_member, cx) + persons.read(cx).get_person(target_member, cx) } /// Merge the names of the first two members of the room. fn merged_name(&self, cx: &App) -> SharedString { - let registry = Registry::read_global(cx); + let persons = PersonRegistry::global(cx); if self.is_group() { let profiles: Vec = self .members .iter() - .map(|public_key| registry.get_person(public_key, cx)) + .map(|public_key| persons.read(cx).get_person(public_key, cx)) .collect(); let mut name = profiles @@ -452,8 +458,8 @@ impl Room { let relay_cache = state.relay_cache.read_blocking(); // Get current user - let registry = Registry::global(cx); - let public_key = registry.read(cx).signer_pubkey().unwrap(); + let account = Account::global(cx); + let public_key = account.read(cx).public_key(); // Get room's subject let subject = self.subject.clone(); @@ -481,9 +487,9 @@ impl Room { } // Add subject tag if it's present - if let Some(subject) = subject { + if let Some(value) = subject { tags.push(Tag::from_standardized_without_cell(TagStandard::Subject( - subject, + value.to_string(), ))); } diff --git a/crates/chat_ui/Cargo.toml b/crates/chat_ui/Cargo.toml new file mode 100644 index 0000000..b381a2a --- /dev/null +++ b/crates/chat_ui/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "chat_ui" +version.workspace = true +edition.workspace = true +publish.workspace = true + +[dependencies] +ui = { path = "../ui" } +theme = { path = "../theme" } +common = { path = "../common" } +states = { path = "../states" } +account = { path = "../account" } +person = { path = "../person" } +chat = { path = "../chat" } +settings = { path = "../settings" } + +gpui.workspace = true +gpui_tokio.workspace = true + +nostr.workspace = true +nostr-sdk.workspace = true +anyhow.workspace = true +itertools.workspace = true +smallvec.workspace = true +smol.workspace = true +log.workspace = true +serde.workspace = true +serde_json.workspace = true + +indexset = "0.12.3" +emojis = "0.6.4" +once_cell = "1.19.0" +linkify = "0.10.0" +regex = "1" diff --git a/crates/chat_ui/src/actions.rs b/crates/chat_ui/src/actions.rs new file mode 100644 index 0000000..3b6dd33 --- /dev/null +++ b/crates/chat_ui/src/actions.rs @@ -0,0 +1,22 @@ +use gpui::Action; +use nostr_sdk::prelude::*; +use serde::Deserialize; +use states::SignerKind; + +#[derive(Action, Clone, PartialEq, Eq, Deserialize)] +#[action(namespace = chat, no_json)] +pub struct SeenOn(pub EventId); + +#[derive(Action, Clone, PartialEq, Eq, Deserialize)] +#[action(namespace = chat, no_json)] +pub struct SetSigner(pub SignerKind); + +/// Define a open public key action +#[derive(Action, Clone, PartialEq, Eq, Deserialize, Debug)] +#[action(namespace = pubkey, no_json)] +pub struct OpenPublicKey(pub PublicKey); + +/// Define a copy inline public key action +#[derive(Action, Clone, PartialEq, Eq, Deserialize, Debug)] +#[action(namespace = pubkey, no_json)] +pub struct CopyPublicKey(pub PublicKey); diff --git a/crates/ui/src/emoji_picker.rs b/crates/chat_ui/src/emoji.rs similarity index 81% rename from crates/ui/src/emoji_picker.rs rename to crates/chat_ui/src/emoji.rs index 23f6070..c60aeeb 100644 --- a/crates/ui/src/emoji_picker.rs +++ b/crates/chat_ui/src/emoji.rs @@ -6,11 +6,10 @@ use gpui::{ RenderOnce, SharedString, StatefulInteractiveElement, Styled, WeakEntity, Window, }; use theme::ActiveTheme; - -use crate::button::{Button, ButtonVariants}; -use crate::input::InputState; -use crate::popover::{Popover, PopoverContent}; -use crate::{Icon, Sizable, Size}; +use ui::button::{Button, ButtonVariants}; +use ui::input::InputState; +use ui::popover::{Popover, PopoverContent}; +use ui::{Icon, Sizable, Size}; static EMOJIS: OnceLock> = OnceLock::new(); @@ -31,27 +30,33 @@ fn get_emojis() -> &'static Vec { #[derive(IntoElement)] pub struct EmojiPicker { + target: Option>, icon: Option, - size: Size, anchor: Option, - target_input: WeakEntity, + size: Size, } impl EmojiPicker { - pub fn new(target_input: WeakEntity) -> Self { + pub fn new() -> Self { Self { - target_input, size: Size::default(), + target: None, anchor: None, icon: None, } } + pub fn target(mut self, target: WeakEntity) -> Self { + self.target = Some(target); + self + } + pub fn icon(mut self, icon: impl Into) -> Self { self.icon = Some(icon.into()); self } + #[allow(dead_code)] pub fn anchor(mut self, corner: Corner) -> Self { self.anchor = Some(corner); self @@ -67,7 +72,7 @@ impl Sizable for EmojiPicker { impl RenderOnce for EmojiPicker { fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - Popover::new("emoji-picker") + Popover::new("emojis") .map(|this| { if let Some(corner) = self.anchor { this.anchor(corner) @@ -76,13 +81,13 @@ impl RenderOnce for EmojiPicker { } }) .trigger( - Button::new("emoji-trigger") + Button::new("emojis-trigger") .when_some(self.icon, |this, icon| this.icon(icon)) .ghost() .with_size(self.size), ) .content(move |window, cx| { - let input = self.target_input.clone(); + let input = self.target.clone(); cx.new(|cx| { PopoverContent::new(window, cx, move |_window, cx| { @@ -104,18 +109,18 @@ impl RenderOnce for EmojiPicker { .hover(|this| this.bg(cx.theme().ghost_element_hover)) .on_click({ let item = e.clone(); - let input = input.upgrade(); + let input = input.clone(); move |_, window, cx| { if let Some(input) = input.as_ref() { - input.update(cx, |this, cx| { - let current = this.value(); - let new_text = if current.is_empty() { + _ = input.update(cx, |this, cx| { + let value = this.value(); + let new_text = if value.is_empty() { format!("{item}") - } else if current.ends_with(" ") { - format!("{current}{item}") + } else if value.ends_with(" ") { + format!("{value}{item}") } else { - format!("{current} {item}") + format!("{value} {item}") }; this.set_value(new_text, window, cx); }); diff --git a/crates/coop/src/views/chat/mod.rs b/crates/chat_ui/src/lib.rs similarity index 92% rename from crates/coop/src/views/chat/mod.rs rename to crates/chat_ui/src/lib.rs index 5dc7bc3..fe64b8b 100644 --- a/crates/coop/src/views/chat/mod.rs +++ b/crates/chat_ui/src/lib.rs @@ -1,61 +1,58 @@ use std::collections::HashSet; use std::time::Duration; +pub use actions::*; +use chat::message::{Message, RenderedMessage}; +use chat::room::{Room, RoomKind, RoomSignal, SendOptions, SendReport}; use common::display::{RenderedProfile, RenderedTimestamp}; use common::nip96::nip96_upload; use gpui::prelude::FluentBuilder; use gpui::{ - div, img, list, px, red, relative, rems, svg, white, Action, AnyElement, App, AppContext, + div, img, list, px, red, relative, rems, svg, white, AnyElement, App, AppContext, ClipboardItem, Context, Element, Entity, EventEmitter, Flatten, FocusHandle, Focusable, InteractiveElement, IntoElement, ListAlignment, ListOffset, ListState, MouseButton, ObjectFit, ParentElement, PathPromptOptions, Render, RetainAllImageCache, SharedString, SharedUri, StatefulInteractiveElement, Styled, StyledImage, Subscription, Task, Window, }; use gpui_tokio::Tokio; -use i18n::{shared_t, t}; use indexset::{BTreeMap, BTreeSet}; use itertools::Itertools; use nostr_sdk::prelude::*; -use registry::message::{Message, RenderedMessage}; -use registry::room::{Room, RoomKind, RoomSignal, SendOptions, SendReport}; -use registry::Registry; -use serde::Deserialize; +use person::PersonRegistry; use settings::AppSettings; use smallvec::{smallvec, SmallVec}; use smol::fs; -use states::{app_state, SignerKind}; +use states::{app_state, SignerKind, QUERY_TIMEOUT}; use theme::ActiveTheme; -use ui::actions::{CopyPublicKey, OpenPublicKey}; use ui::avatar::Avatar; use ui::button::{Button, ButtonVariants}; use ui::context_menu::ContextMenuExt; use ui::dock_area::panel::{Panel, PanelEvent}; -use ui::emoji_picker::EmojiPicker; use ui::input::{InputEvent, InputState, TextInput}; use ui::modal::ModalButtonProps; use ui::notification::Notification; use ui::popup_menu::PopupMenuExt; -use ui::text::RenderedText; use ui::{ h_flex, v_flex, ContextModal, Disableable, Icon, IconName, InteractiveElementExt, Sizable, StyledExt, }; +use crate::emoji::EmojiPicker; +use crate::text::RenderedText; + +mod actions; +mod emoji; mod subject; +mod text; -#[derive(Action, Clone, PartialEq, Eq, Deserialize)] -#[action(namespace = chat, no_json)] -pub struct SeenOn(pub EventId); +const NIP17_WARN: &str = "has not set up Messaging Relays, they cannot receive your message."; +const EMPTY_WARN: &str = "Something is wrong. Coop cannot display this message"; -#[derive(Action, Clone, PartialEq, Eq, Deserialize)] -#[action(namespace = chat, no_json)] -pub struct SetSigner(pub SignerKind); - -pub fn init(room: Entity, window: &mut Window, cx: &mut App) -> Entity { - cx.new(|cx| Chat::new(room, window, cx)) +pub fn init(room: Entity, window: &mut Window, cx: &mut App) -> Entity { + cx.new(|cx| ChatPanel::new(room, window, cx)) } -pub struct Chat { +pub struct ChatPanel { // Chat Room room: Entity, @@ -83,11 +80,11 @@ pub struct Chat { _tasks: SmallVec<[Task<()>; 3]>, } -impl Chat { +impl ChatPanel { pub fn new(room: Entity, window: &mut Window, cx: &mut Context) -> Self { let input = cx.new(|cx| { InputState::new(window, cx) - .placeholder(t!("chat.placeholder")) + .placeholder("Message...") .auto_grow(1, 20) .prevent_new_line_on_enter() .clean_on_escape() @@ -140,21 +137,23 @@ impl Chat { // Connect and verify all members messaging relays cx.spawn_in(window, async move |this, cx| { // Wait for 5 seconds before connecting and verifying - cx.background_executor().timer(Duration::from_secs(5)).await; + cx.background_executor() + .timer(Duration::from_secs(QUERY_TIMEOUT)) + .await; let result = verify_connections.await; this.update_in(cx, |this, window, cx| { match result { Ok(data) => { - let registry = Registry::global(cx); + let persons = PersonRegistry::global(cx); for (public_key, status) in data.into_iter() { if !status { - let profile = registry.read(cx).get_person(&public_key, cx); - let content = t!("chat.nip17_warn", u = profile.display_name()); + let profile = persons.read(cx).get_person(&public_key, cx); + let name = profile.display_name(); - this.insert_warning(content, cx); + this.insert_warning(format!("{NIP17_WARN} {name}"), cx); } } } @@ -295,7 +294,7 @@ impl Chat { // Return if message is empty if content.trim().is_empty() { - window.push_notification(t!("chat.empty_message_error"), cx); + window.push_notification("Cannot send an empty message", cx); return; } @@ -322,10 +321,10 @@ impl Chat { // Optimistically update message list cx.spawn_in(window, async move |this, cx| { - let delay = Duration::from_millis(100); - // Wait for the delay - cx.background_executor().timer(delay).await; + cx.background_executor() + .timer(Duration::from_millis(100)) + .await; // Update the message list and reset the states this.update_in(cx, |this, window, cx| { @@ -476,8 +475,8 @@ impl Chat { } fn profile(&self, public_key: &PublicKey, cx: &Context) -> Profile { - let registry = Registry::read_global(cx); - registry.get_person(public_key, cx) + let persons = PersonRegistry::global(cx); + persons.read(cx).get_person(public_key, cx) } fn signer_kind(&self, cx: &App) -> SignerKind { @@ -629,7 +628,9 @@ impl Chat { .size_10() .text_color(cx.theme().elevated_surface_background), ) - .child(shared_t!("chat.notice")) + .child(SharedString::from( + "This conversation is private. Only members can see each other's messages.", + )) .into_any_element() } @@ -704,8 +705,8 @@ impl Chat { let view = Box::new(OpenPublicKey(public_key)); let copy = Box::new(CopyPublicKey(public_key)); - this.menu(t!("profile.view"), view) - .menu(t!("profile.copy"), copy) + this.menu("View Profile", view) + .menu("Copy Public Key", copy) }), ) }) @@ -806,14 +807,14 @@ impl Chat { fn render_message_sent(&self, id: &EventId, _cx: &Context) -> impl IntoElement { div() .id(SharedString::from(id.to_hex())) - .child(shared_t!("chat.sent")) + .child(SharedString::from("• Sent")) .when_some(self.sent_reports(id).cloned(), |this, reports| { this.on_click(move |_e, window, cx| { let reports = reports.clone(); window.open_modal(cx, move |this, _window, cx| { this.show_close(true) - .title(shared_t!("chat.reports")) + .title(SharedString::from("Sent Reports")) .child(v_flex().pb_4().gap_4().children({ let mut items = Vec::with_capacity(reports.len()); @@ -836,14 +837,16 @@ impl Chat { .text_xs() .italic() .child(Icon::new(IconName::Info).xsmall()) - .child(shared_t!("chat.sent_failed")) + .child(SharedString::from( + "Failed to send message. Click to see details.", + )) .when_some(self.sent_reports(id).cloned(), |this, reports| { this.on_click(move |_e, window, cx| { let reports = reports.clone(); window.open_modal(cx, move |this, _window, cx| { this.show_close(true) - .title(shared_t!("chat.reports")) + .title(SharedString::from("Sent Reports")) .child(v_flex().gap_4().pb_4().w_full().children({ let mut items = Vec::with_capacity(reports.len()); @@ -859,8 +862,8 @@ impl Chat { } fn render_report(report: &SendReport, cx: &App) -> impl IntoElement { - let registry = Registry::read_global(cx); - let profile = registry.get_person(&report.receiver, cx); + let persons = PersonRegistry::global(cx); + let profile = persons.read(cx).get_person(&report.receiver, cx); let name = profile.display_name(); let avatar = profile.avatar(true); @@ -871,7 +874,7 @@ impl Chat { h_flex() .gap_2() .text_sm() - .child(shared_t!("chat.sent_to")) + .child(SharedString::from("Sent to:")) .child( h_flex() .gap_1() @@ -897,7 +900,7 @@ impl Chat { .flex_1() .w_full() .text_center() - .child(shared_t!("chat.nip17_warn", u = name)), + .child(SharedString::from("Messaging Relays not found")), ), ) }) @@ -918,7 +921,7 @@ impl Chat { .flex_1() .w_full() .text_center() - .child(shared_t!("chat.device_error", u = name)), + .child(SharedString::from("Encryption Key not found")), ), ) }) @@ -997,7 +1000,7 @@ impl Chat { .text_sm() .text_color(cx.theme().secondary_foreground) .line_height(relative(1.25)) - .child(shared_t!("chat.sent_success")), + .child(SharedString::from("Successfully")), ), ) } @@ -1035,7 +1038,7 @@ impl Chat { .child( Button::new("reply") .icon(IconName::Reply) - .tooltip(t!("chat.reply_button")) + .tooltip("Reply") .small() .ghost() .on_click({ @@ -1048,7 +1051,7 @@ impl Chat { .child( Button::new("copy") .icon(IconName::Copy) - .tooltip(t!("chat.copy_message_button")) + .tooltip("Copy") .small() .ghost() .on_click({ @@ -1066,9 +1069,7 @@ impl Chat { .ghost() .popup_menu({ let id = id.to_owned(); - move |this, _window, _cx| { - this.menu(t!("common.seen_on"), Box::new(SeenOn(id))) - } + move |this, _, _| this.menu("Seen on", Box::new(SeenOn(id))) }), ) .group_hover("", |this| this.visible()) @@ -1123,8 +1124,8 @@ impl Chat { fn render_reply(&self, id: &EventId, cx: &Context) -> impl IntoElement { if let Some(text) = self.message(id) { - let registry = Registry::read_global(cx); - let profile = registry.get_person(&text.author, cx); + let persons = PersonRegistry::global(cx); + let profile = persons.read(cx).get_person(&text.author, cx); div() .w_full() @@ -1143,7 +1144,7 @@ impl Chat { .gap_1() .text_xs() .text_color(cx.theme().text_muted) - .child(SharedString::new(t!("chat.replying_to_label"))) + .child(SharedString::from("Replying to:")) .child( div() .text_color(cx.theme().text_accent) @@ -1201,7 +1202,7 @@ impl Chat { Button::new("subject") .icon(IconName::Edit) - .tooltip(t!("chat.subject_tooltip")) + .tooltip("Change the subject of the conversation") .on_click(move |_, window, cx| { let view = subject::init(subject.clone(), window, cx); let room = room.clone(); @@ -1212,9 +1213,9 @@ impl Chat { let weak_view = weak_view.clone(); this.confirm() - .title(shared_t!("chat.subject_tooltip")) + .title("Change the subject of the conversation") .child(view.clone()) - .button_props(ModalButtonProps::default().ok_text(t!("common.change"))) + .button_props(ModalButtonProps::default().ok_text("Change")) .on_ok(move |_, _window, cx| { if let Ok(subject) = weak_view.read_with(cx, |this, cx| this.new_subject(cx)) @@ -1236,13 +1237,12 @@ impl Chat { Button::new("reload") .icon(IconName::Refresh) - .tooltip(t!("chat.reload_tooltip")) - .on_click(move |_, window, cx| { - window.push_notification(t!("common.refreshed"), cx); - room.update(cx, |this, cx| { + .tooltip("Reload") + .on_click(move |_ev, window, cx| { + _ = room.update(cx, |this, cx| { this.emit_refresh(cx); - }) - .ok(); + window.push_notification("Reloaded", cx); + }); }) } @@ -1275,8 +1275,9 @@ impl Chat { if let Ok(urls) = task.await { cx.update(|window, cx| { window.open_modal(cx, move |this, _window, cx| { - this.title(shared_t!("common.seen_on")).child( - v_flex().pb_4().gap_2().children({ + this.show_close(true) + .title(SharedString::from("Seen on")) + .child(v_flex().pb_4().gap_2().children({ let mut items = Vec::with_capacity(urls.len()); for url in urls.clone().into_iter() { @@ -1288,13 +1289,12 @@ impl Chat { .rounded(cx.theme().radius) .font_semibold() .text_xs() - .child(url.to_string()), + .child(SharedString::from(url.to_string())), ) } items - }), - ) + })) }); }) .ok(); @@ -1311,7 +1311,7 @@ impl Chat { } } -impl Panel for Chat { +impl Panel for ChatPanel { fn panel_id(&self) -> SharedString { self.id.clone() } @@ -1338,15 +1338,15 @@ impl Panel for Chat { } } -impl EventEmitter for Chat {} +impl EventEmitter for ChatPanel {} -impl Focusable for Chat { +impl Focusable for ChatPanel { fn focus_handle(&self, _: &App) -> FocusHandle { self.focus_handle.clone() } } -impl Render for Chat { +impl Render for ChatPanel { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let kind = self.signer_kind(cx); @@ -1376,7 +1376,7 @@ impl Render for Chat { Message::System(_timestamp) => this.render_announcement(ix, cx), } } else { - this.render_warning(ix, shared_t!("chat.not_found"), cx) + this.render_warning(ix, SharedString::from(EMPTY_WARN), cx) } }), ) @@ -1418,7 +1418,8 @@ impl Render for Chat { )), ) .child( - EmojiPicker::new(self.input.downgrade()) + EmojiPicker::new() + .target(self.input.downgrade()) .icon(IconName::EmojiFill) .large(), ), diff --git a/crates/chat_ui/src/subject.rs b/crates/chat_ui/src/subject.rs new file mode 100644 index 0000000..57d873f --- /dev/null +++ b/crates/chat_ui/src/subject.rs @@ -0,0 +1,60 @@ +use gpui::{ + div, App, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, + Styled, Window, +}; +use theme::ActiveTheme; +use ui::input::{InputState, TextInput}; +use ui::{v_flex, Sizable}; + +pub fn init(subject: Option, window: &mut Window, cx: &mut App) -> Entity { + cx.new(|cx| Subject::new(subject, window, cx)) +} + +pub struct Subject { + input: Entity, +} + +impl Subject { + pub fn new(subject: Option, window: &mut Window, cx: &mut Context) -> Self { + let input = cx.new(|cx| InputState::new(window, cx).placeholder("Plan for holiday")); + + if let Some(value) = subject { + input.update(cx, |this, cx| { + this.set_value(value, window, cx); + }); + }; + + Self { input } + } + + pub fn new_subject(&self, cx: &App) -> SharedString { + self.input.read(cx).value() + } +} + +impl Render for Subject { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + v_flex() + .gap_2() + .child( + v_flex() + .gap_1() + .child( + div() + .text_sm() + .text_color(cx.theme().text_muted) + .child(SharedString::from("Subject:")), + ) + .child(TextInput::new(&self.input).small()), + ) + .child( + div() + .text_xs() + .italic() + .text_color(cx.theme().text_placeholder) + .child(SharedString::from( + "Subject will be updated when you send a new message.", + )), + ) + } +} diff --git a/crates/ui/src/text.rs b/crates/chat_ui/src/text.rs similarity index 98% rename from crates/ui/src/text.rs rename to crates/chat_ui/src/text.rs index a36c1d9..73c4fca 100644 --- a/crates/ui/src/text.rs +++ b/crates/chat_ui/src/text.rs @@ -9,8 +9,8 @@ use gpui::{ use linkify::{LinkFinder, LinkKind}; use nostr_sdk::prelude::*; use once_cell::sync::Lazy; +use person::PersonRegistry; use regex::Regex; -use registry::Registry; use theme::ActiveTheme; use crate::actions::OpenPublicKey; @@ -91,6 +91,7 @@ impl RenderedText { } } + #[allow(dead_code)] pub fn set_tooltip_builder_for_custom_ranges(&mut self, f: F) where F: Fn(usize, Range, &mut Window, &mut App) -> Option + 'static, @@ -315,8 +316,8 @@ fn render_plain_text_mut( link_urls: &mut Vec, cx: &App, ) { - let registry = Registry::read_global(cx); - let profile = registry.get_person(&public_key, cx); + let persons = PersonRegistry::global(cx); + let profile = persons.read(cx).get_person(&public_key, cx); let display_name = format!("@{}", profile.display_name()); // Replace token with display name diff --git a/crates/coop/Cargo.toml b/crates/coop/Cargo.toml index 612bece..a07892e 100644 --- a/crates/coop/Cargo.toml +++ b/crates/coop/Cargo.toml @@ -34,9 +34,12 @@ theme = { path = "../theme" } common = { path = "../common" } states = { path = "../states" } key_store = { path = "../key_store" } -registry = { path = "../registry" } +chat = { path = "../chat" } +chat_ui = { path = "../chat_ui" } settings = { path = "../settings" } auto_update = { path = "../auto_update" } +account = { path = "../account" } +person = { path = "../person" } rust-i18n.workspace = true i18n.workspace = true diff --git a/crates/coop/src/actions.rs b/crates/coop/src/actions.rs index cb7ec1b..36a4788 100644 --- a/crates/coop/src/actions.rs +++ b/crates/coop/src/actions.rs @@ -1,10 +1,9 @@ use std::sync::Mutex; -use gpui::{actions, App, AppContext}; +use gpui::{actions, App}; use key_store::backend::KeyItem; use key_store::KeyStore; use nostr_connect::prelude::*; -use registry::Registry; use states::app_state; actions!(coop, [ReloadMetadata, DarkMode, Settings, Logout, Quit]); @@ -48,31 +47,30 @@ pub fn load_embedded_fonts(cx: &App) { } pub fn reset(cx: &mut App) { - let registry = Registry::global(cx); let backend = KeyStore::global(cx).read(cx).backend(); cx.spawn(async move |cx| { - cx.background_spawn(async move { - let client = app_state().client(); - client.unset_signer().await; - }) - .await; + let client = app_state().client(); + // Remove the signer + client.unset_signer().await; + + // Delete user's credentials backend .delete_credentials(&KeyItem::User.to_string(), cx) .await .ok(); + // Remove bunker's credentials if available backend .delete_credentials(&KeyItem::Bunker.to_string(), cx) .await .ok(); - registry - .update(cx, |this, cx| { - this.reset(cx); - }) - .ok(); + cx.update(|cx| { + cx.restart(); + }) + .ok(); }) .detach(); } diff --git a/crates/coop/src/chatspace.rs b/crates/coop/src/chatspace.rs index 399d8b7..0c59448 100644 --- a/crates/coop/src/chatspace.rs +++ b/crates/coop/src/chatspace.rs @@ -2,8 +2,11 @@ use std::borrow::Cow; use std::collections::{HashMap, HashSet}; use std::sync::Arc; +use account::Account; use anyhow::{anyhow, Error}; use auto_update::AutoUpdater; +use chat::{ChatEvent, ChatRegistry}; +use chat_ui::{CopyPublicKey, OpenPublicKey}; use common::display::{shorten_pubkey, RenderedProfile}; use common::event::EventUtils; use gpui::prelude::FluentBuilder; @@ -18,7 +21,7 @@ use key_store::backend::KeyItem; use key_store::KeyStore; use nostr_connect::prelude::*; use nostr_sdk::prelude::*; -use registry::{Registry, RegistryEvent}; +use person::PersonRegistry; use settings::AppSettings; use smallvec::{smallvec, SmallVec}; use states::{ @@ -27,7 +30,6 @@ use states::{ }; use theme::{ActiveTheme, Theme, ThemeMode}; use title_bar::TitleBar; -use ui::actions::{CopyPublicKey, OpenPublicKey}; use ui::avatar::Avatar; use ui::button::{Button, ButtonVariants}; use ui::dock_area::dock::DockPlacement; @@ -42,7 +44,8 @@ use crate::actions::{reset, DarkMode, Logout, ReloadMetadata, Settings}; use crate::views::compose::compose_button; use crate::views::setup_relay::SetupRelay; use crate::views::{ - account, chat, login, new_account, onboarding, preferences, sidebar, user_profile, welcome, + account as account_view, login, new_account, onboarding, preferences, sidebar, user_profile, + welcome, }; pub fn init(window: &mut Window, cx: &mut App) -> Entity { @@ -59,6 +62,7 @@ pub fn new_account(window: &mut Window, cx: &mut App) { ChatSpace::set_center_panel(panel, window, cx); } +#[derive(Debug)] pub struct ChatSpace { /// App's Title Bar title_bar: Entity, @@ -84,7 +88,7 @@ pub struct ChatSpace { impl ChatSpace { pub fn new(window: &mut Window, cx: &mut Context) -> Self { - let registry = Registry::global(cx); + let chat = ChatRegistry::global(cx); let keystore = KeyStore::global(cx); let title_bar = cx.new(|_| TitleBar::new()); @@ -102,55 +106,49 @@ impl ChatSpace { ); subscriptions.push( - // Observe device changes - cx.observe_in(&keystore, window, move |this, state, window, cx| { + // Observe keystore changes + cx.observe_in(&keystore, window, move |_this, state, window, cx| { if state.read(cx).initialized { let backend = state.read(cx).backend(); - if state.read(cx).initialized { - if state.read(cx).is_using_file_keystore() { - this.render_keyring_installation(window, cx); - } + cx.spawn_in(window, async move |this, cx| { + let result = backend + .read_credentials(&KeyItem::User.to_string(), cx) + .await; - cx.spawn_in(window, async move |this, cx| { - let result = backend - .read_credentials(&KeyItem::User.to_string(), cx) - .await; + this.update_in(cx, |this, window, cx| { + match result { + Ok(Some((user, secret))) => { + let public_key = PublicKey::parse(&user).unwrap(); + let secret = String::from_utf8(secret).unwrap(); - this.update_in(cx, |this, window, cx| { - match result { - Ok(Some((user, secret))) => { - let public_key = PublicKey::parse(&user).unwrap(); - let secret = String::from_utf8(secret).unwrap(); - - this.set_account_layout(public_key, secret, window, cx); - } - _ => { - this.set_onboarding_layout(window, cx); - } - }; - }) - .ok(); + this.set_account_layout(public_key, secret, window, cx); + } + _ => { + this.set_onboarding_layout(window, cx); + } + }; }) - .detach(); - } + .ok(); + }) + .detach(); } }), ); subscriptions.push( // Handle registry events - cx.subscribe_in(®istry, window, move |this, _, ev, window, cx| { + cx.subscribe_in(&chat, window, move |this, chat, ev, window, cx| { match ev { - RegistryEvent::Open(room) => { - if let Some(room) = room.upgrade() { + ChatEvent::OpenRoom(id) => { + if let Some(room) = chat.read(cx).room(id, cx) { this.dock.update(cx, |this, cx| { - let panel = chat::init(room, window, cx); + let panel = chat_ui::init(room, window, cx); this.add_panel(Arc::new(panel), DockPlacement::Center, window, cx); }); } } - RegistryEvent::Close(..) => { + ChatEvent::CloseRoom(..) => { this.dock.update(cx, |this, cx| { this.focus_tab_panel(window, cx); @@ -217,7 +215,8 @@ impl ChatSpace { while let Ok(signal) = states.signal().receiver().recv_async().await { view.update_in(cx, |this, window, cx| { - let registry = Registry::global(cx); + let chat = ChatRegistry::global(cx); + let persons = PersonRegistry::global(cx); let settings = AppSettings::global(cx); match signal { @@ -234,8 +233,8 @@ impl ChatSpace { this.receive_encryption(response, window, cx); } SignalKind::SignerSet(public_key) => { - // Close all opened modals - window.close_all_modals(cx); + // Set the global account state + account::init(public_key, cx); // Load user's settings settings.update(cx, |this, cx| { @@ -243,11 +242,13 @@ impl ChatSpace { }); // Load all chat rooms - registry.update(cx, |this, cx| { - this.set_signer_pubkey(public_key, cx); + chat.update(cx, |this, cx| { this.load_rooms(window, cx); }); + // Close all opened modals + window.close_all_modals(cx); + // Setup the default layout for current workspace this.set_default_layout(window, cx); } @@ -271,7 +272,7 @@ impl ChatSpace { if matches!(s, UnwrappingStatus::Processing | UnwrappingStatus::Complete) { let all_panels = this.get_all_panel_ids(cx); - registry.update(cx, |this, cx| { + chat.update(cx, |this, cx| { this.load_rooms(window, cx); this.refresh_rooms(all_panels, cx); @@ -282,13 +283,13 @@ impl ChatSpace { } } SignalKind::NewProfile(profile) => { - registry.update(cx, |this, cx| { + persons.update(cx, |this, cx| { this.insert_or_update_person(profile, cx); }); } - SignalKind::NewMessage((gift_wrap_id, event)) => { - registry.update(cx, |this, cx| { - this.event_to_message(gift_wrap_id, event, window, cx); + SignalKind::NewMessage(msg) => { + chat.update(cx, |this, cx| { + this.new_message(msg, window, cx); }); } SignalKind::GossipRelaysNotFound => { @@ -621,7 +622,7 @@ impl ChatSpace { window: &mut Window, cx: &mut Context, ) { - let panel = Arc::new(account::init(public_key, secret, window, cx)); + let panel = Arc::new(account_view::init(public_key, secret, window, cx)); let center = DockItem::panel(panel); self.dock.update(cx, |this, cx| { @@ -785,32 +786,6 @@ impl ChatSpace { } } - fn render_keyring_installation(&mut self, window: &mut Window, cx: &mut App) { - window.open_modal(cx, move |this, _window, cx| { - this.overlay_closable(false) - .show_close(false) - .keyboard(false) - .alert() - .button_props(ModalButtonProps::default().ok_text(t!("common.continue"))) - .title(shared_t!("keyring_disable.label")) - .child( - v_flex() - .gap_2() - .text_sm() - .child(shared_t!("keyring_disable.body_1")) - .child(shared_t!("keyring_disable.body_2")) - .child(shared_t!("keyring_disable.body_3")) - .child(shared_t!("keyring_disable.body_4")) - .child( - div() - .text_xs() - .text_color(cx.theme().danger_foreground) - .child(shared_t!("keyring_disable.body_5")), - ), - ) - }); - } - fn render_request(&mut self, ann: Announcement, window: &mut Window, cx: &mut Context) { let client_name = SharedString::from(ann.client().to_string()); let target = ann.public_key(); @@ -907,8 +882,25 @@ impl ChatSpace { ), ) .on_cancel(move |_ev, window, cx| { - _ = view.update(cx, |this, cx| { - this.render_reset(window, cx); + _ = view.update(cx, |_, cx| { + cx.spawn_in(window, async move |this, cx| { + let state = app_state(); + let result = state.init_encryption_keys().await; + + this.update_in(cx, |_, window, cx| { + match result { + Ok(_) => { + window.push_notification(t!("encryption.success"), cx); + window.close_all_modals(cx); + } + Err(e) => { + window.push_notification(e.to_string(), cx); + } + }; + }) + .ok(); + }) + .detach(); }); // false to keep modal open false @@ -916,27 +908,6 @@ impl ChatSpace { }); } - fn render_reset(&mut self, window: &mut Window, cx: &mut Context) { - cx.spawn_in(window, async move |this, cx| { - let state = app_state(); - let result = state.init_encryption_keys().await; - - this.update_in(cx, |_, window, cx| { - match result { - Ok(_) => { - window.push_notification(t!("encryption.success"), cx); - window.close_all_modals(cx); - } - Err(e) => { - window.push_notification(e.to_string(), cx); - } - }; - }) - .ok(); - }) - .detach(); - } - fn render_setup_gossip_relays_modal(&mut self, window: &mut Window, cx: &mut App) { let relays = default_nip65_relays(); @@ -1139,13 +1110,39 @@ impl ChatSpace { }) } - fn render_titlebar_left_side( - &mut self, - _window: &mut Window, - cx: &Context, - ) -> impl IntoElement { - let registry = Registry::global(cx); - let status = registry.read(cx).loading; + fn render_keyring_warning(window: &mut Window, cx: &mut App) { + window.open_modal(cx, move |this, _window, cx| { + this.overlay_closable(false) + .show_close(false) + .keyboard(false) + .alert() + .button_props(ModalButtonProps::default().ok_text(t!("common.continue"))) + .title(shared_t!("keyring_disable.label")) + .child( + v_flex() + .gap_2() + .text_sm() + .child(shared_t!("keyring_disable.body_1")) + .child(shared_t!("keyring_disable.body_2")) + .child(shared_t!("keyring_disable.body_3")) + .child(shared_t!("keyring_disable.body_4")) + .child( + div() + .text_xs() + .text_color(cx.theme().danger_foreground) + .child(shared_t!("keyring_disable.body_5")), + ), + ) + }); + } + + fn titlebar_left(&mut self, _window: &mut Window, cx: &Context) -> impl IntoElement { + let chat = ChatRegistry::global(cx); + let status = chat.read(cx).loading; + + if !Account::has_global(cx) { + return div(); + } h_flex() .gap_2() @@ -1166,12 +1163,8 @@ impl ChatSpace { }) } - fn render_titlebar_right_side( - &mut self, - profile: &Profile, - _window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { + fn titlebar_right(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let file_keystore = KeyStore::global(cx).read(cx).is_using_file_keystore(); let proxy = AppSettings::get_proxy_user_avatars(cx); let updating = AutoUpdater::read_global(cx).status.is_updating(); let updated = AutoUpdater::read_global(cx).status.is_updated(); @@ -1179,6 +1172,19 @@ impl ChatSpace { h_flex() .gap_1() + .when(file_keystore, |this| { + this.child( + Button::new("keystore-warning") + .icon(IconName::Warning) + .label("Keyring Disabled") + .ghost() + .xsmall() + .rounded() + .on_click(move |_ev, window, cx| { + Self::render_keyring_warning(window, cx); + }), + ) + }) .when(updating, |this| { this.child( h_flex() @@ -1236,7 +1242,7 @@ impl ChatSpace { Button::new("setup-relays-button") .icon(IconName::Info) .label(t!("messaging.button")) - .warning() + .ghost() .xsmall() .rounded() .on_click(move |_ev, window, cx| { @@ -1244,22 +1250,29 @@ impl ChatSpace { }), ) }) - .child( - Button::new("user") - .small() - .reverse() - .transparent() - .icon(IconName::CaretDown) - .child(Avatar::new(profile.avatar(proxy)).size(rems(1.49))) - .popup_menu(|this, _window, _cx| { - this.menu(t!("user.dark_mode"), Box::new(DarkMode)) - .menu(t!("user.settings"), Box::new(Settings)) - .separator() - .menu(t!("user.reload_metadata"), Box::new(ReloadMetadata)) - .separator() - .menu(t!("user.sign_out"), Box::new(Logout)) - }), - ) + .when(Account::has_global(cx), |this| { + let persons = PersonRegistry::global(cx); + let account = Account::global(cx); + let public_key = account.read(cx).public_key(); + let profile = persons.read(cx).get_person(&public_key, cx); + + this.child( + Button::new("user") + .small() + .reverse() + .transparent() + .icon(IconName::CaretDown) + .child(Avatar::new(profile.avatar(proxy)).size(rems(1.49))) + .popup_menu(|this, _window, _cx| { + this.menu(t!("user.dark_mode"), Box::new(DarkMode)) + .menu(t!("user.settings"), Box::new(Settings)) + .separator() + .menu(t!("user.reload_metadata"), Box::new(ReloadMetadata)) + .separator() + .menu(t!("user.sign_out"), Box::new(Logout)) + }), + ) + }) } } @@ -1267,24 +1280,14 @@ impl Render for ChatSpace { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let modal_layer = Root::render_modal_layer(window, cx); let notification_layer = Root::render_notification_layer(window, cx); - let registry = Registry::read_global(cx); - // Only render titlebar child elements if user is logged in - if let Some(public_key) = registry.signer_pubkey() { - let profile = registry.get_person(&public_key, cx); + let left = self.titlebar_left(window, cx).into_any_element(); + let right = self.titlebar_right(window, cx).into_any_element(); - let left_side = self - .render_titlebar_left_side(window, cx) - .into_any_element(); - - let right_side = self - .render_titlebar_right_side(&profile, window, cx) - .into_any_element(); - - self.title_bar.update(cx, |this, _cx| { - this.set_children(vec![left_side, right_side]); - }) - } + // Update title bar children + self.title_bar.update(cx, |this, _cx| { + this.set_children(vec![left, right]); + }); div() .id(SharedString::from("chatspace")) diff --git a/crates/coop/src/main.rs b/crates/coop/src/main.rs index 30ffeca..9c467d3 100644 --- a/crates/coop/src/main.rs +++ b/crates/coop/src/main.rs @@ -83,9 +83,12 @@ fn main() { ui::init(cx); // Initialize app registry - registry::init(cx); + chat::init(cx); - // Initialize backend for credentials storage + // Initialize person registry + person::init(cx); + + // Initialize backend for keys storage key_store::init(cx); // Initialize settings diff --git a/crates/coop/src/views/account.rs b/crates/coop/src/views/account.rs index 2e4bc8e..1673049 100644 --- a/crates/coop/src/views/account.rs +++ b/crates/coop/src/views/account.rs @@ -12,7 +12,7 @@ use i18n::{shared_t, t}; use key_store::backend::KeyItem; use key_store::KeyStore; use nostr_connect::prelude::*; -use registry::Registry; +use person::PersonRegistry; use smallvec::{smallvec, SmallVec}; use states::{app_state, BUNKER_TIMEOUT}; use theme::ActiveTheme; @@ -207,8 +207,8 @@ impl Focusable for Account { impl Render for Account { fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context) -> impl IntoElement { - let registry = Registry::global(cx); - let profile = registry.read(cx).get_person(&self.public_key, cx); + let persons = PersonRegistry::global(cx); + let profile = persons.read(cx).get_person(&self.public_key, cx); let bunker = self.secret.starts_with("bunker://"); v_flex() diff --git a/crates/coop/src/views/chat/subject.rs b/crates/coop/src/views/chat/subject.rs deleted file mode 100644 index cd074b8..0000000 --- a/crates/coop/src/views/chat/subject.rs +++ /dev/null @@ -1,55 +0,0 @@ -use gpui::{ - div, App, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, - Styled, Window, -}; -use i18n::{shared_t, t}; -use theme::ActiveTheme; -use ui::input::{InputState, TextInput}; -use ui::{v_flex, Sizable}; - -pub fn init(subject: Option, window: &mut Window, cx: &mut App) -> Entity { - Subject::new(subject, window, cx) -} - -pub struct Subject { - input: Entity, -} - -impl Subject { - pub fn new(subject: Option, window: &mut Window, cx: &mut App) -> Entity { - let input = cx.new(|cx| { - let mut this = InputState::new(window, cx).placeholder(t!("subject.placeholder")); - if let Some(text) = subject.as_ref() { - this.set_value(text, window, cx); - } - this - }); - - cx.new(|_| Self { input }) - } - - pub fn new_subject(&self, cx: &App) -> String { - self.input.read(cx).value().to_string() - } -} - -impl Render for Subject { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - v_flex() - .gap_1() - .child( - div() - .text_sm() - .text_color(cx.theme().text_muted) - .child(shared_t!("subject.title")), - ) - .child(TextInput::new(&self.input).small()) - .child( - div() - .text_xs() - .italic() - .text_color(cx.theme().text_placeholder) - .child(shared_t!("subject.help_text")), - ) - } -} diff --git a/crates/coop/src/views/compose.rs b/crates/coop/src/views/compose.rs index d12b950..db46013 100644 --- a/crates/coop/src/views/compose.rs +++ b/crates/coop/src/views/compose.rs @@ -2,6 +2,8 @@ use std::ops::Range; use std::time::Duration; use anyhow::{anyhow, Error}; +use chat::room::Room; +use chat::ChatRegistry; use common::display::{RenderedProfile, TextUtils}; use common::nip05::nip05_profile; use gpui::prelude::FluentBuilder; @@ -13,8 +15,7 @@ use gpui::{ use gpui_tokio::Tokio; use i18n::{shared_t, t}; use nostr_sdk::prelude::*; -use registry::room::Room; -use registry::Registry; +use person::PersonRegistry; use settings::AppSettings; use smallvec::{smallvec, SmallVec}; use states::{app_state, BOOTSTRAP_RELAYS}; @@ -311,7 +312,7 @@ impl Compose { } fn submit(&mut self, window: &mut Window, cx: &mut Context) { - let registry = Registry::global(cx); + let chat = ChatRegistry::global(cx); let receivers: Vec = self.selected(cx); let subject_input = self.title_input.read(cx).value(); let subject = (!subject_input.is_empty()).then(|| subject_input.to_string()); @@ -327,10 +328,9 @@ impl Compose { this.update_in(cx, |this, window, cx| { match result { Ok(room) => { - registry.update(cx, |this, cx| { + chat.update(cx, |this, cx| { this.push_room(cx.new(|_| room), cx); }); - window.close_modal(cx); } Err(e) => { @@ -372,7 +372,7 @@ impl Compose { fn list_items(&self, range: Range, cx: &Context) -> Vec { let proxy = AppSettings::get_proxy_user_avatars(cx); - let registry = Registry::read_global(cx); + let persons = PersonRegistry::global(cx); let mut items = Vec::with_capacity(self.contacts.read(cx).len()); for ix in range { @@ -381,7 +381,7 @@ impl Compose { }; let public_key = contact.public_key; - let profile = registry.get_person(&public_key, cx); + let profile = persons.read(cx).get_person(&public_key, cx); items.push( h_flex() diff --git a/crates/coop/src/views/mod.rs b/crates/coop/src/views/mod.rs index f60093a..4af54c8 100644 --- a/crates/coop/src/views/mod.rs +++ b/crates/coop/src/views/mod.rs @@ -1,6 +1,5 @@ pub mod account; pub mod backup_keys; -pub mod chat; pub mod compose; pub mod edit_profile; pub mod login; diff --git a/crates/coop/src/views/preferences.rs b/crates/coop/src/views/preferences.rs index 4cd72ec..3ade856 100644 --- a/crates/coop/src/views/preferences.rs +++ b/crates/coop/src/views/preferences.rs @@ -1,12 +1,12 @@ +use account::Account; use common::display::RenderedProfile; use gpui::http_client::Url; -use gpui::prelude::FluentBuilder; use gpui::{ div, px, relative, rems, App, AppContext, Context, Entity, InteractiveElement, IntoElement, ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, Window, }; use i18n::{shared_t, t}; -use registry::Registry; +use person::PersonRegistry; use settings::AppSettings; use theme::ActiveTheme; use ui::avatar::Avatar; @@ -53,7 +53,7 @@ impl Preferences { .on_ok(move |_, window, cx| { weak_view .update(cx, |this, cx| { - let registry = Registry::global(cx); + let persons = PersonRegistry::global(cx); let set_metadata = this.set_metadata(cx); cx.spawn_in(window, async move |this, cx| { @@ -62,7 +62,7 @@ impl Preferences { this.update_in(cx, |_, window, cx| { match result { Ok(profile) => { - registry.update(cx, |this, cx| { + persons.update(cx, |this, cx| { this.insert_or_update_person(profile, cx); }); } @@ -115,7 +115,11 @@ impl Render for Preferences { let proxy = AppSettings::get_proxy_user_avatars(cx); let hide = AppSettings::get_hide_user_avatars(cx); - let registry = Registry::read_global(cx); + let persons = PersonRegistry::global(cx); + let account = Account::global(cx); + let public_key = account.read(cx).public_key(); + let profile = persons.read(cx).get_person(&public_key, cx); + let input_state = self.media_input.downgrade(); v_flex() @@ -130,54 +134,48 @@ impl Render for Preferences { .font_semibold() .child(shared_t!("preferences.account_header")), ) - .when_some(registry.signer_pubkey(), |this, public_key| { - let profile = registry.get_person(&public_key, cx); - - this.child( - h_flex() - .w_full() - .justify_between() - .child( - h_flex() - .id("user") - .gap_2() - .child(Avatar::new(profile.avatar(proxy)).size(rems(2.4))) - .child( - div() - .flex_1() - .text_sm() - .child( - div() - .font_semibold() - .line_height(relative(1.3)) - .child(profile.display_name()), - ) - .child( - div() - .text_xs() - .text_color(cx.theme().text_muted) - .line_height(relative(1.3)) - .child(shared_t!( - "preferences.account_btn" - )), - ), - ) - .on_click(cx.listener(move |this, _e, window, cx| { - this.open_edit_profile(window, cx); - })), - ) - .child( - Button::new("relays") - .label("Messaging Relays") - .xsmall() - .ghost_alt() - .rounded() - .on_click(cx.listener(move |this, _e, window, cx| { - this.open_relays(window, cx); - })), - ), - ) - }), + .child( + h_flex() + .w_full() + .justify_between() + .child( + h_flex() + .id("user") + .gap_2() + .child(Avatar::new(profile.avatar(proxy)).size(rems(2.4))) + .child( + div() + .flex_1() + .text_sm() + .child( + div() + .font_semibold() + .line_height(relative(1.3)) + .child(profile.display_name()), + ) + .child( + div() + .text_xs() + .text_color(cx.theme().text_muted) + .line_height(relative(1.3)) + .child(shared_t!("preferences.account_btn")), + ), + ) + .on_click(cx.listener(move |this, _e, window, cx| { + this.open_edit_profile(window, cx); + })), + ) + .child( + Button::new("relays") + .label("Messaging Relays") + .xsmall() + .ghost_alt() + .rounded() + .on_click(cx.listener(move |this, _e, window, cx| { + this.open_relays(window, cx); + })), + ), + ), ) .child( v_flex() diff --git a/crates/coop/src/views/screening.rs b/crates/coop/src/views/screening.rs index 1bd1239..1aec337 100644 --- a/crates/coop/src/views/screening.rs +++ b/crates/coop/src/views/screening.rs @@ -10,7 +10,7 @@ use gpui::{ use gpui_tokio::Tokio; use i18n::{shared_t, t}; use nostr_sdk::prelude::*; -use registry::Registry; +use person::PersonRegistry; use settings::AppSettings; use smallvec::{smallvec, SmallVec}; use states::{app_state, BOOTSTRAP_RELAYS}; @@ -35,8 +35,8 @@ pub struct Screening { impl Screening { pub fn new(public_key: PublicKey, window: &mut Window, cx: &mut Context) -> Self { - let registry = Registry::read_global(cx); - let profile = registry.get_person(&public_key, cx); + let persons = PersonRegistry::global(cx); + let profile = persons.read(cx).get_person(&public_key, cx); let mut tasks = smallvec![]; diff --git a/crates/coop/src/views/sidebar/list_item.rs b/crates/coop/src/views/sidebar/list_item.rs index 33363b1..a19964b 100644 --- a/crates/coop/src/views/sidebar/list_item.rs +++ b/crates/coop/src/views/sidebar/list_item.rs @@ -1,5 +1,8 @@ use std::rc::Rc; +use chat::room::RoomKind; +use chat::ChatRegistry; +use chat_ui::{CopyPublicKey, OpenPublicKey}; use gpui::prelude::FluentBuilder; use gpui::{ div, rems, App, ClickEvent, InteractiveElement, IntoElement, ParentElement as _, RenderOnce, @@ -7,11 +10,8 @@ use gpui::{ }; use i18n::t; use nostr_sdk::prelude::*; -use registry::room::RoomKind; -use registry::Registry; use settings::AppSettings; use theme::ActiveTheme; -use ui::actions::{CopyPublicKey, OpenPublicKey}; use ui::avatar::Avatar; use ui::context_menu::ContextMenuExt; use ui::modal::ModalButtonProps; @@ -187,7 +187,7 @@ impl RenderOnce for RoomListItem { .ok_text(t!("screening.response")), ) .on_cancel(move |_event, _window, cx| { - Registry::global(cx).update(cx, |this, cx| { + ChatRegistry::global(cx).update(cx, |this, cx| { this.close_room(room_id, cx); }); // false to prevent closing the modal diff --git a/crates/coop/src/views/sidebar/mod.rs b/crates/coop/src/views/sidebar/mod.rs index 4c874ee..e5b9e51 100644 --- a/crates/coop/src/views/sidebar/mod.rs +++ b/crates/coop/src/views/sidebar/mod.rs @@ -3,6 +3,8 @@ use std::ops::Range; use std::time::Duration; use anyhow::{anyhow, Error}; +use chat::room::{Room, RoomKind}; +use chat::{ChatEvent, ChatRegistry}; use common::debounced_delay::DebouncedDelay; use common::display::{RenderedProfile, RenderedTimestamp, TextUtils}; use gpui::prelude::FluentBuilder; @@ -16,8 +18,6 @@ use i18n::{shared_t, t}; use itertools::Itertools; use list_item::RoomListItem; use nostr_sdk::prelude::*; -use registry::room::{Room, RoomKind}; -use registry::{Registry, RegistryEvent}; use settings::AppSettings; use smallvec::{smallvec, SmallVec}; use states::{app_state, BOOTSTRAP_RELAYS, SEARCH_RELAYS}; @@ -73,7 +73,7 @@ impl Sidebar { let find_input = cx.new(|cx| InputState::new(window, cx).placeholder(t!("sidebar.search_label"))); - let registry = Registry::global(cx); + let chat = ChatRegistry::global(cx); let mut subscriptions = smallvec![]; subscriptions.push( @@ -87,8 +87,8 @@ impl Sidebar { subscriptions.push( // Subscribe for registry new events - cx.subscribe_in(®istry, window, move |this, _, event, _window, cx| { - if let RegistryEvent::NewRequest(kind) = event { + cx.subscribe_in(&chat, window, move |this, _, event, _window, cx| { + if let ChatEvent::NewChatRequest(kind) = event { this.indicator.update(cx, |this, cx| { *this = Some(kind.to_owned()); cx.notify(); @@ -326,8 +326,8 @@ impl Sidebar { Ok(room) => { cx.update(|window, cx| { this.update(cx, |this, cx| { - let registry = Registry::read_global(cx); - let result = registry.search_by_public_key(public_key, cx); + let chat = ChatRegistry::global(cx); + let result = chat.read(cx).search_by_public_key(public_key, cx); if !result.is_empty() { this.results(result, false, window, cx); @@ -394,9 +394,9 @@ impl Sidebar { } } - let chats = Registry::read_global(cx); + let chat = ChatRegistry::global(cx); // Get all local results with current query - let local_results = chats.search(&query, cx); + let local_results = chat.read(cx).search(&query, cx); if !local_results.is_empty() { // Try to update with local results first @@ -495,7 +495,7 @@ impl Sidebar { } fn open_room(&mut self, id: u64, window: &mut Window, cx: &mut Context) { - let room = if let Some(room) = Registry::read_global(cx).room(&id, cx) { + let room = if let Some(room) = ChatRegistry::global(cx).read(cx).room(&id, cx) { room } else { let Some(result) = self.global_result.read(cx).as_ref() else { @@ -514,13 +514,13 @@ impl Sidebar { room }; - Registry::global(cx).update(cx, |this, cx| { + ChatRegistry::global(cx).update(cx, |this, cx| { this.push_room(room, cx); }); } fn on_reload(&mut self, _ev: &Reload, window: &mut Window, cx: &mut Context) { - Registry::global(cx).update(cx, |this, cx| { + ChatRegistry::global(cx).update(cx, |this, cx| { this.load_rooms(window, cx); }); window.push_notification(t!("common.refreshed"), cx); @@ -661,8 +661,8 @@ impl Focusable for Sidebar { impl Render for Sidebar { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let registry = Registry::read_global(cx); - let loading = registry.loading; + let chat = ChatRegistry::global(cx); + let loading = chat.read(cx).loading; // Get rooms from either search results or the chat registry let rooms = if let Some(results) = self.local_result.read(cx).as_ref() { @@ -672,9 +672,9 @@ impl Render for Sidebar { } else { #[allow(clippy::collapsible_else_if)] if self.active_filter.read(cx) == &RoomKind::Ongoing { - registry.ongoing_rooms(cx) + chat.read(cx).ongoing_rooms(cx) } else { - registry.request_rooms(cx) + chat.read(cx).request_rooms(cx) } }; @@ -738,9 +738,9 @@ impl Render for Sidebar { .tooltip(t!("sidebar.all_conversations_tooltip")) .when_some(self.indicator.read(cx).as_ref(), |this, kind| { this.when(kind == &RoomKind::Ongoing, |this| { - this.child(deferred( + this.child( div().size_1().rounded_full().bg(cx.theme().cursor), - )) + ) }) }) .small() @@ -759,9 +759,9 @@ impl Render for Sidebar { .tooltip(t!("sidebar.requests_tooltip")) .when_some(self.indicator.read(cx).as_ref(), |this, kind| { this.when(kind != &RoomKind::Ongoing, |this| { - this.child(deferred( + this.child( div().size_1().rounded_full().bg(cx.theme().cursor), - )) + ) }) }) .small() diff --git a/crates/coop/src/views/user_profile.rs b/crates/coop/src/views/user_profile.rs index a7d3698..13ac16e 100644 --- a/crates/coop/src/views/user_profile.rs +++ b/crates/coop/src/views/user_profile.rs @@ -10,7 +10,7 @@ use gpui::{ use gpui_tokio::Tokio; use i18n::{shared_t, t}; use nostr_sdk::prelude::*; -use registry::Registry; +use person::PersonRegistry; use settings::AppSettings; use smallvec::{smallvec, SmallVec}; use states::app_state; @@ -33,8 +33,8 @@ pub struct UserProfile { impl UserProfile { pub fn new(target: PublicKey, window: &mut Window, cx: &mut Context) -> Self { - let registry = Registry::read_global(cx); - let profile = registry.get_person(&target, cx); + let persons = PersonRegistry::global(cx); + let profile = persons.read(cx).get_person(&target, cx); let mut tasks = smallvec![]; diff --git a/crates/key_store/Cargo.toml b/crates/key_store/Cargo.toml index dc33a36..49f93f3 100644 --- a/crates/key_store/Cargo.toml +++ b/crates/key_store/Cargo.toml @@ -5,21 +5,14 @@ edition.workspace = true publish.workspace = true [dependencies] -common = { path = "../common" } states = { path = "../states" } -ui = { path = "../ui" } -theme = { path = "../theme" } -settings = { path = "../settings" } -rust-i18n.workspace = true -i18n.workspace = true gpui.workspace = true nostr.workspace = true nostr-sdk.workspace = true anyhow.workspace = true -itertools.workspace = true smallvec.workspace = true smol.workspace = true log.workspace = true diff --git a/crates/key_store/src/lib.rs b/crates/key_store/src/lib.rs index beeaff3..60ad3d9 100644 --- a/crates/key_store/src/lib.rs +++ b/crates/key_store/src/lib.rs @@ -54,7 +54,6 @@ impl KeyStore { // Only used for testing keyring availability on the user's system let read_credential = cx.read_credentials("Coop"); - let mut tasks = smallvec![]; tasks.push( diff --git a/crates/person/Cargo.toml b/crates/person/Cargo.toml new file mode 100644 index 0000000..b604911 --- /dev/null +++ b/crates/person/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "person" +version.workspace = true +edition.workspace = true +publish.workspace = true + +[dependencies] +common = { path = "../common" } +states = { path = "../states" } + +gpui.workspace = true +nostr.workspace = true +nostr-sdk.workspace = true +anyhow.workspace = true +itertools.workspace = true +smallvec.workspace = true +smol.workspace = true +log.workspace = true diff --git a/crates/person/src/lib.rs b/crates/person/src/lib.rs new file mode 100644 index 0000000..bcc0944 --- /dev/null +++ b/crates/person/src/lib.rs @@ -0,0 +1,127 @@ +use std::collections::HashMap; + +use gpui::{App, AppContext, AsyncApp, Context, Entity, Global, Task}; +use nostr_sdk::prelude::*; +use smallvec::{smallvec, SmallVec}; +use states::app_state; + +pub fn init(cx: &mut App) { + PersonRegistry::set_global(cx.new(PersonRegistry::new), cx); +} + +struct GlobalPersonRegistry(Entity); + +impl Global for GlobalPersonRegistry {} + +pub struct PersonRegistry { + /// Collection of all persons (user profiles) + pub persons: HashMap>, + + /// Tasks for asynchronous operations + _tasks: SmallVec<[Task<()>; 2]>, +} + +impl PersonRegistry { + /// Retrieve the global person registry state + pub fn global(cx: &App) -> Entity { + cx.global::().0.clone() + } + + /// Set the global person registry instance + pub(crate) fn set_global(state: Entity, cx: &mut App) { + cx.set_global(GlobalPersonRegistry(state)); + } + + /// Create a new person registry instance + pub(crate) fn new(cx: &mut Context) -> Self { + let mut tasks = smallvec![]; + + tasks.push( + // Load all user profiles from the database + cx.spawn(async move |this, cx| { + match Self::load_persons(cx).await { + Ok(profiles) => { + this.update(cx, |this, cx| { + this.bulk_insert_persons(profiles, cx); + }) + .ok(); + } + Err(e) => { + log::error!("Failed to load persons: {e}"); + } + }; + }), + ); + + Self { + persons: HashMap::new(), + _tasks: tasks, + } + } + + /// Create a task to load all user profiles from the database + fn load_persons(cx: &AsyncApp) -> Task, Error>> { + cx.background_spawn(async move { + let client = app_state().client(); + let filter = Filter::new().kind(Kind::Metadata).limit(200); + let events = client.database().query(filter).await?; + + let mut profiles = vec![]; + + for event in events.into_iter() { + let metadata = Metadata::from_json(event.content).unwrap_or_default(); + let profile = Profile::new(event.pubkey, metadata); + profiles.push(profile); + } + + Ok(profiles) + }) + } + + /// Insert batch of persons + fn bulk_insert_persons(&mut self, profiles: Vec, cx: &mut Context) { + for profile in profiles.into_iter() { + self.persons + .insert(profile.public_key(), cx.new(|_| profile)); + } + cx.notify(); + } + + /// Insert or update a person + pub fn insert_or_update_person(&mut self, profile: Profile, cx: &mut App) { + let public_key = profile.public_key(); + + match self.persons.get(&public_key) { + Some(person) => { + person.update(cx, |this, cx| { + *this = profile; + cx.notify(); + }); + } + None => { + self.persons.insert(public_key, cx.new(|_| profile)); + } + } + } + + /// Get single person + pub fn get_person(&self, public_key: &PublicKey, cx: &App) -> Profile { + self.persons + .get(public_key) + .map(|e| e.read(cx)) + .cloned() + .unwrap_or(Profile::new(public_key.to_owned(), Metadata::default())) + } + + /// Get group of persons + pub fn get_group_person(&self, public_keys: &[PublicKey], cx: &App) -> Vec { + let mut profiles = vec![]; + + for public_key in public_keys.iter() { + let profile = self.get_person(public_key, cx); + profiles.push(profile); + } + + profiles + } +} diff --git a/crates/states/src/state/mod.rs b/crates/states/src/state/mod.rs index f35b3d8..1e02c14 100644 --- a/crates/states/src/state/mod.rs +++ b/crates/states/src/state/mod.rs @@ -1148,7 +1148,8 @@ impl AppState { match event.created_at >= self.initialized_at { // New message: send a signal to notify the UI true => { - self.signal.send(SignalKind::NewMessage((id, event))).await; + let new_message = NewMessage::new(id, event); + self.signal.send(SignalKind::NewMessage(new_message)).await; } // Old message: Coop is probably processing the user's messages during initial load false => { diff --git a/crates/states/src/state/signal.rs b/crates/states/src/state/signal.rs index e220b76..dcbb854 100644 --- a/crates/states/src/state/signal.rs +++ b/crates/states/src/state/signal.rs @@ -1,6 +1,18 @@ use flume::{Receiver, Sender}; use nostr_sdk::prelude::*; +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct NewMessage { + pub gift_wrap: EventId, + pub rumor: UnsignedEvent, +} + +impl NewMessage { + pub fn new(gift_wrap: EventId, rumor: UnsignedEvent) -> Self { + Self { gift_wrap, rumor } + } +} + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct AuthRequest { pub url: RelayUrl, @@ -111,7 +123,7 @@ pub enum SignalKind { NewProfile(Profile), /// A signal to notify UI that a new gift wrap event has been received - NewMessage((EventId, UnsignedEvent)), + NewMessage(NewMessage), /// A signal to notify UI that no messaging relays for current user was found MessagingRelaysNotFound, diff --git a/crates/title_bar/src/lib.rs b/crates/title_bar/src/lib.rs index 1d6d198..08d0791 100644 --- a/crates/title_bar/src/lib.rs +++ b/crates/title_bar/src/lib.rs @@ -89,11 +89,11 @@ impl Render for TitleBar { .h(height) .map(|this| { if window.is_fullscreen() { - this.pl_2() + this.px_2() } else if cx.theme().platform_kind.is_mac() { - this.pl(px(platforms::mac::TRAFFIC_LIGHT_PADDING)) + this.pl(px(platforms::mac::TRAFFIC_LIGHT_PADDING)).pr_2() } else { - this.pl_2() + this.px_2() } }) .map(|this| match decorations { diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml index ed44a7b..ce03c8a 100644 --- a/crates/ui/Cargo.toml +++ b/crates/ui/Cargo.toml @@ -7,11 +7,7 @@ publish.workspace = true [dependencies] common = { path = "../common" } theme = { path = "../theme" } -registry = { path = "../registry" } -rust-i18n.workspace = true -i18n.workspace = true -nostr-sdk.workspace = true gpui.workspace = true smol.workspace = true serde.workspace = true @@ -20,14 +16,11 @@ smallvec.workspace = true anyhow.workspace = true itertools.workspace = true log.workspace = true -emojis.workspace = true -regex = "1" unicode-segmentation = "1.12.0" uuid = "1.10" -once_cell = "1.19.0" +regex = "1" image = "0.25.1" -linkify = "0.10.0" lsp-types = "0.97.0" rope = { git = "https://github.com/zed-industries/zed" } sum_tree = { git = "https://github.com/zed-industries/zed" } diff --git a/crates/ui/src/actions.rs b/crates/ui/src/actions.rs index 5669f5a..af2cab9 100644 --- a/crates/ui/src/actions.rs +++ b/crates/ui/src/actions.rs @@ -1,17 +1,6 @@ use gpui::{actions, Action}; -use nostr_sdk::prelude::PublicKey; use serde::Deserialize; -/// Define a open public key action -#[derive(Action, Clone, PartialEq, Eq, Deserialize, Debug)] -#[action(namespace = pubkey, no_json)] -pub struct OpenPublicKey(pub PublicKey); - -/// Define a copy inline public key action -#[derive(Action, Clone, PartialEq, Eq, Deserialize, Debug)] -#[action(namespace = pubkey, no_json)] -pub struct CopyPublicKey(pub PublicKey); - /// Define a custom confirm action #[derive(Clone, Action, PartialEq, Eq, Deserialize)] #[action(namespace = list, no_json)] diff --git a/crates/ui/src/button.rs b/crates/ui/src/button.rs index 9f52ea6..2cf0bd8 100644 --- a/crates/ui/src/button.rs +++ b/crates/ui/src/button.rs @@ -359,6 +359,7 @@ impl RenderOnce for Button { .justify_center() .cursor_default() .overflow_hidden() + .refine_style(&self.style) .map(|this| match self.rounded { false => this.rounded(cx.theme().radius), true => this.rounded_full(), @@ -436,29 +437,6 @@ impl RenderOnce for Button { } } }) - .text_color(normal_style.fg) - .when(!self.disabled && !self.selected, |this| { - this.bg(normal_style.bg) - .hover(|this| { - let hover_style = style.hovered(cx); - this.bg(hover_style.bg).text_color(hover_style.fg) - }) - .active(|this| { - let active_style = style.active(cx); - this.bg(active_style.bg).text_color(active_style.fg) - }) - }) - .when(self.selected, |this| { - let selected_style = style.selected(cx); - this.bg(selected_style.bg).text_color(selected_style.fg) - }) - .when(self.disabled, |this| { - let disabled_style = style.disabled(cx); - this.cursor_not_allowed() - .bg(disabled_style.bg) - .text_color(disabled_style.fg) - }) - .refine_style(&self.style) .on_mouse_down(gpui::MouseButton::Left, |_, window, _| { // Avoid focus on mouse down. window.prevent_default(); @@ -505,6 +483,28 @@ impl RenderOnce for Button { }) .children(self.children) }) + .text_color(normal_style.fg) + .when(!self.disabled && !self.selected, |this| { + this.bg(normal_style.bg) + .hover(|this| { + let hover_style = style.hovered(cx); + this.bg(hover_style.bg).text_color(hover_style.fg) + }) + .active(|this| { + let active_style = style.active(cx); + this.bg(active_style.bg).text_color(active_style.fg) + }) + }) + .when(self.selected, |this| { + let selected_style = style.selected(cx); + this.bg(selected_style.bg).text_color(selected_style.fg) + }) + .when(self.disabled, |this| { + let disabled_style = style.disabled(cx); + this.cursor_not_allowed() + .bg(disabled_style.bg) + .text_color(disabled_style.fg) + }) .when(self.loading && !self.disabled, |this| { this.bg(normal_style.bg.opacity(0.8)) .text_color(normal_style.fg.opacity(0.8)) diff --git a/crates/ui/src/input/element.rs b/crates/ui/src/input/element.rs index 69385ab..40619d4 100644 --- a/crates/ui/src/input/element.rs +++ b/crates/ui/src/input/element.rs @@ -541,9 +541,15 @@ impl Element for TextElement { let mut bounds = bounds; let (display_text, text_color) = if is_empty { - (Rope::from(placeholder.as_str()), cx.theme().text_muted) + ( + Rope::from_str_small(placeholder.as_str()), + cx.theme().text_muted, + ) } else if state.masked { - (Rope::from("*".repeat(text.chars_count())), cx.theme().text) + ( + Rope::from_str_small("*".repeat(text.chars_count()).as_str()), + cx.theme().text, + ) } else { (text.clone(), cx.theme().text) }; diff --git a/crates/ui/src/input/state.rs b/crates/ui/src/input/state.rs index 795701c..fb8545c 100644 --- a/crates/ui/src/input/state.rs +++ b/crates/ui/src/input/state.rs @@ -328,7 +328,7 @@ impl InputState { Self { focus_handle: focus_handle.clone(), - text: "".into(), + text: Rope::default(), text_wrapper: TextWrapper::new( text_style.font(), text_style.font_size.to_pixels(window.rem_size()), @@ -718,7 +718,7 @@ impl InputState { /// Set the default value of the input field. pub fn default_value(mut self, value: impl Into) -> Self { let text: SharedString = value.into(); - self.text = Rope::from(text.as_str()); + self.text = Rope::from_str_small(text.as_str()); self.text_wrapper.set_default_text(&self.text); self } @@ -2099,7 +2099,9 @@ impl EntityInputHandler for InputState { .unwrap_or(self.selected_range.into()); let old_text = self.text.clone(); - self.text.replace(range.clone(), new_text); + let executor = cx.background_executor(); + + self.text.replace(range.clone(), new_text, executor); let mut new_offset = (range.start + new_text.len()).min(self.text.len()); @@ -2113,7 +2115,7 @@ impl EntityInputHandler for InputState { if !self.mask_pattern.is_none() { let mask_text = self.mask_pattern.mask(&pending_text); - self.text = Rope::from(mask_text.as_str()); + self.text = Rope::from_str_small(mask_text.as_str()); let new_text_len = (new_text.len() + mask_text.len()).saturating_sub(pending_text.len()); new_offset = (range.start + new_text_len).min(mask_text.len()); @@ -2121,8 +2123,13 @@ impl EntityInputHandler for InputState { } self.push_history(&old_text, &range, new_text); - self.text_wrapper - .update(&self.text, &range, &Rope::from(new_text), false, cx); + self.text_wrapper.update( + &self.text, + &range, + &Rope::from_str_small(new_text), + false, + cx, + ); self.selected_range = (new_offset..new_offset).into(); self.ime_marked_range.take(); self.update_preferred_column(); @@ -2154,7 +2161,9 @@ impl EntityInputHandler for InputState { .unwrap_or(self.selected_range.into()); let old_text = self.text.clone(); - self.text.replace(range.clone(), new_text); + let executor = cx.background_executor(); + + self.text.replace(range.clone(), new_text, executor); if self.mode.is_single_line() { let pending_text = self.text.to_string(); @@ -2165,8 +2174,13 @@ impl EntityInputHandler for InputState { } self.push_history(&old_text, &range, new_text); - self.text_wrapper - .update(&self.text, &range, &Rope::from(new_text), false, cx); + self.text_wrapper.update( + &self.text, + &range, + &Rope::from_str_small(new_text), + false, + cx, + ); if new_text.is_empty() { // Cancel selection, when cancel IME input. self.selected_range = (range.start..range.start).into(); diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index 48b8c41..06a1573 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -17,7 +17,6 @@ pub mod checkbox; pub mod divider; pub mod dock_area; pub mod dropdown; -pub mod emoji_picker; pub mod history; pub mod indicator; pub mod input; @@ -31,7 +30,6 @@ pub mod scroll; pub mod skeleton; pub mod switch; pub mod tab; -pub mod text; pub mod tooltip; mod event; @@ -42,8 +40,6 @@ mod root; mod styled; mod window_border; -i18n::init!(); - /// Initialize the UI module. /// /// This must be called before using any of the UI components. diff --git a/crates/ui/src/popover.rs b/crates/ui/src/popover.rs index 28d57a8..c142e12 100644 --- a/crates/ui/src/popover.rs +++ b/crates/ui/src/popover.rs @@ -14,13 +14,7 @@ use crate::{Selectable, StyledExt as _}; const CONTEXT: &str = "Popover"; -actions!( - popover, - [ - /// Action when user presses escape button - Escape - ] -); +actions!(popover, [Escape]); pub fn init(cx: &mut App) { cx.bind_keys([KeyBinding::new("escape", Escape, Some(CONTEXT))]) diff --git a/locales/app.yml b/locales/app.yml index f5b3c47..18e50a1 100644 --- a/locales/app.yml +++ b/locales/app.yml @@ -236,16 +236,6 @@ manage_relays: time: en: "Last activity: %{t}" -subject: - title: - en: "Subject:" - placeholder: - en: "Exciting Project..." - room_not_found: - en: "Room not found" - help_text: - en: "Subject will be updated when you send a message." - screening: ignore: en: "Ignore" @@ -378,40 +368,6 @@ compose: subject_label: en: "Subject:" -chat: - notice: - en: "This conversation is private. Only members can see each other's messages." - placeholder: - en: "Message..." - not_found: - en: "Something is wrong. Coop cannot display this message" - empty_message_error: - en: "Cannot send an empty message" - copy_message_button: - en: "Copy Message" - reply_button: - en: "Reply" - reload_tooltip: - en: "Refresh messages" - subject_tooltip: - en: "Change the subject of the conversation" - replying_to_label: - en: "Replying to:" - sent_to: - en: "Sent to:" - sent: - en: "• Sent" - sent_failed: - en: "Failed to send message. Click to see details." - sent_success: - en: "Successfully" - reports: - en: "Sent Reports" - nip17_warn: - en: "%{u} has not set up Messaging Relays, they cannot receive your message." - device_error: - en: "You're sending with an encryption key, but %{u} has not set up an encryption key yet. Try sending with your identity instead." - sidebar: reload_menu: en: "Reload"