chore: restructure and refine the ui (#199)

* update deps

* clean up

* add account crate

* add person crate

* add chat and chat ui crates

* .

* clean up the ui crate

* .

* .
This commit is contained in:
reya
2025-11-01 09:16:02 +07:00
committed by GitHub
parent a1bd4954eb
commit 7091fa1cab
42 changed files with 980 additions and 794 deletions

231
Cargo.lock generated
View File

@@ -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]]

View File

@@ -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"

18
crates/account/Cargo.toml Normal file
View File

@@ -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

54
crates/account/src/lib.rs Normal file
View File

@@ -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<Account>);
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<Self> {
cx.global::<GlobalAccount>().0.clone()
}
/// Check if the global account state exists
pub fn has_global(cx: &App) -> bool {
cx.has_global::<GlobalAccount>()
}
/// Remove the global account state
pub fn remove_global(cx: &mut App) {
cx.remove_global::<GlobalAccount>();
}
/// Set the global account instance
pub(crate) fn set_global(state: Entity<Self>, 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 {
Self {
public_key,
_tasks: smallvec![],
}
}
/// Get the public key of the account
pub fn public_key(&self) -> PublicKey {
self.public_key
}
}

View File

@@ -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

View File

@@ -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<Registry>);
impl Global for GlobalRegistry {}
#[derive(Debug)]
pub enum RegistryEvent {
Open(WeakEntity<Room>),
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<ChatRegistry>);
impl Global for GlobalChatRegistry {}
pub struct ChatRegistry {
/// Collection of all chat rooms
pub rooms: Vec<Entity<Room>>,
/// Collection of all persons (user profiles)
pub persons: HashMap<PublicKey, Entity<Profile>>,
/// Loading status of the registry
pub loading: bool,
/// Public Key of the currently activated signer
signer_pubkey: Option<PublicKey>,
/// Tasks for asynchronous operations
_tasks: SmallVec<[Task<()>; 2]>,
}
impl EventEmitter<RegistryEvent> for Registry {}
impl EventEmitter<ChatEvent> for ChatRegistry {}
impl Registry {
impl ChatRegistry {
/// Retrieve the global registry state
pub fn global(cx: &App) -> Entity<Self> {
cx.global::<GlobalRegistry>().0.clone()
}
/// Retrieve the registry instance
pub fn read_global(cx: &App) -> &Self {
cx.global::<GlobalRegistry>().0.read(cx)
cx.global::<GlobalChatRegistry>().0.clone()
}
/// Set the global registry instance
pub(crate) fn set_global(state: Entity<Self>, 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>) -> 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 {
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<Result<Vec<Profile>, 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<PublicKey> {
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>) {
self.signer_pubkey = Some(public_key);
cx.notify();
}
/// Insert batch of persons
pub fn set_persons(&mut self, profiles: Vec<Profile>, cx: &mut Context<Self>) {
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<Profile> {
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>) {
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<Self>) {
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<Self>) {
// 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<Room>, cx: &mut Context<Self>) {
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<Self>,
) {
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<Self>) {
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()));
});
}
}

View File

@@ -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<String>,
pub subject: Option<SharedString>,
/// All members of the room
pub members: Vec<PublicKey>,
/// 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>) {
self.subject = Some(subject);
pub fn set_subject<T>(&mut self, subject: T, cx: &mut Context<Self>)
where
T: Into<SharedString>,
{
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<Profile> = 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(),
)));
}

34
crates/chat_ui/Cargo.toml Normal file
View File

@@ -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"

View File

@@ -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);

View File

@@ -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<Vec<SharedString>> = OnceLock::new();
@@ -31,27 +30,33 @@ fn get_emojis() -> &'static Vec<SharedString> {
#[derive(IntoElement)]
pub struct EmojiPicker {
target: Option<WeakEntity<InputState>>,
icon: Option<Icon>,
size: Size,
anchor: Option<Corner>,
target_input: WeakEntity<InputState>,
size: Size,
}
impl EmojiPicker {
pub fn new(target_input: WeakEntity<InputState>) -> Self {
pub fn new() -> Self {
Self {
target_input,
size: Size::default(),
target: None,
anchor: None,
icon: None,
}
}
pub fn target(mut self, target: WeakEntity<InputState>) -> Self {
self.target = Some(target);
self
}
pub fn icon(mut self, icon: impl Into<Icon>) -> 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);
});

View File

@@ -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<Room>, window: &mut Window, cx: &mut App) -> Entity<Chat> {
cx.new(|cx| Chat::new(room, window, cx))
pub fn init(room: Entity<Room>, window: &mut Window, cx: &mut App) -> Entity<ChatPanel> {
cx.new(|cx| ChatPanel::new(room, window, cx))
}
pub struct Chat {
pub struct ChatPanel {
// Chat Room
room: Entity<Room>,
@@ -83,11 +80,11 @@ pub struct Chat {
_tasks: SmallVec<[Task<()>; 3]>,
}
impl Chat {
impl ChatPanel {
pub fn new(room: Entity<Room>, window: &mut Window, cx: &mut Context<Self>) -> 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<Self>) -> 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<Self>) -> 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<Self>) -> 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<PanelEvent> for Chat {}
impl EventEmitter<PanelEvent> 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<Self>) -> 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(),
),

View File

@@ -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<String>, window: &mut Window, cx: &mut App) -> Entity<Subject> {
cx.new(|cx| Subject::new(subject, window, cx))
}
pub struct Subject {
input: Entity<InputState>,
}
impl Subject {
pub fn new(subject: Option<String>, window: &mut Window, cx: &mut Context<Self>) -> 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<Self>) -> 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.",
)),
)
}
}

View File

@@ -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<F>(&mut self, f: F)
where
F: Fn(usize, Range<usize>, &mut Window, &mut App) -> Option<AnyView> + 'static,
@@ -315,8 +316,8 @@ fn render_plain_text_mut(
link_urls: &mut Vec<String>,
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

View File

@@ -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

View File

@@ -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();
}

View File

@@ -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<ChatSpace> {
@@ -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<TitleBar>,
@@ -84,7 +88,7 @@ pub struct ChatSpace {
impl ChatSpace {
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> 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(&registry, 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<Self>,
) {
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<Self>) {
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<Self>) {
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<Self>,
) -> 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<Self>) -> 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<Self>,
) -> impl IntoElement {
fn titlebar_right(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> 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<Self>) -> 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"))

View File

@@ -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

View File

@@ -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<Self>) -> 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()

View File

@@ -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<String>, window: &mut Window, cx: &mut App) -> Entity<Subject> {
Subject::new(subject, window, cx)
}
pub struct Subject {
input: Entity<InputState>,
}
impl Subject {
pub fn new(subject: Option<String>, window: &mut Window, cx: &mut App) -> Entity<Self> {
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<Self>) -> 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")),
)
}
}

View File

@@ -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<Self>) {
let registry = Registry::global(cx);
let chat = ChatRegistry::global(cx);
let receivers: Vec<PublicKey> = 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<usize>, cx: &Context<Self>) -> Vec<impl IntoElement> {
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()

View File

@@ -1,6 +1,5 @@
pub mod account;
pub mod backup_keys;
pub mod chat;
pub mod compose;
pub mod edit_profile;
pub mod login;

View File

@@ -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()

View File

@@ -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>) -> 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![];

View File

@@ -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

View File

@@ -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(&registry, 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<Self>) {
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<Self>) {
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<Self>) -> 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()

View File

@@ -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>) -> 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![];

View File

@@ -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

View File

@@ -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(

18
crates/person/Cargo.toml Normal file
View File

@@ -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

127
crates/person/src/lib.rs Normal file
View File

@@ -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<PersonRegistry>);
impl Global for GlobalPersonRegistry {}
pub struct PersonRegistry {
/// Collection of all persons (user profiles)
pub persons: HashMap<PublicKey, Entity<Profile>>,
/// Tasks for asynchronous operations
_tasks: SmallVec<[Task<()>; 2]>,
}
impl PersonRegistry {
/// Retrieve the global person registry state
pub fn global(cx: &App) -> Entity<Self> {
cx.global::<GlobalPersonRegistry>().0.clone()
}
/// Set the global person registry instance
pub(crate) fn set_global(state: Entity<Self>, cx: &mut App) {
cx.set_global(GlobalPersonRegistry(state));
}
/// Create a new person registry instance
pub(crate) fn new(cx: &mut Context<Self>) -> 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<Result<Vec<Profile>, 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<Profile>, cx: &mut Context<Self>) {
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<Profile> {
let mut profiles = vec![];
for public_key in public_keys.iter() {
let profile = self.get_person(public_key, cx);
profiles.push(profile);
}
profiles
}
}

View File

@@ -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 => {

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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" }

View File

@@ -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)]

View File

@@ -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))

View File

@@ -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)
};

View File

@@ -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<SharedString>) -> 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();

View File

@@ -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.

View File

@@ -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))])

View File

@@ -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"